Skip to content

fix: advance synced 5/3/1 template cycles#638

Merged
9thLevelSoftware merged 12 commits into
mainfrom
codex/issue-632-mobile-531-auto-week
Jul 8, 2026
Merged

fix: advance synced 5/3/1 template cycles#638
9thLevelSoftware merged 12 commits into
mainfrom
codex/issue-632-mobile-531-auto-week

Conversation

@9thLevelSoftware

Copy link
Copy Markdown
Owner

Summary

  • persist template_id and week_number for training cycles in SQLDelight and backups
  • sync template/current-week state with the portal while preserving local values when older pull payloads omit them
  • keep generated 5/3/1 cycles linked to template_531
  • auto-advance 5/3/1 weeks only after the final workout-bearing day routine is complete, regenerate routines, and bump TMs after week 4

Verification

  • .\gradlew.bat --% :shared:testAndroidHostTest --tests *ConflictResolutionTest* --tests *PortalSyncAdapterTest* --tests *ActiveSessionEngineIntegrationTest* --tests *RegenerateFiveThreeOneRoutinesUseCaseTest* -Pskip.supabase.check=true --console=plain
  • .\gradlew.bat --% :shared:testAndroidHostTest -Pskip.supabase.check=true --console=plain
  • git diff --check
  • final code review pass found no blocking findings

Rollout notes

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements persistence and automatic regeneration of 5/3/1 training cycles by adding template_id and week_number columns to the TrainingCycle table (migration 41) and introducing the RegenerateFiveThreeOneRoutinesUseCase. The changes integrate this regeneration flow into the ActiveSessionEngine upon cycle completion and update sync, backup, and repository layers accordingly. Feedback from the review highlights a fragile day-name matching and abort logic in the regeneration use case, suggesting inspecting routines directly instead, and points out redundant database queries in SqlDelightSyncRepository when fetching cycle metadata.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +710 to +711
template_id = queries.selectTrainingCycleById(portalActiveCycleId).executeAsOne().template_id,
week_number = queries.selectTrainingCycleById(portalActiveCycleId).executeAsOne().week_number,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Redundant Database Queries

Executing queries.selectTrainingCycleById(portalActiveCycleId).executeAsOne() twice in a row to retrieve template_id and week_number is inefficient as it triggers two separate database queries.

We should query the record once, store it in a local variable, and reuse it.

Note: The same pattern of redundant queries is also present in lines 730-731, 1726-1727, and 1743-1744.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b07613125d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@9thLevelSoftware

Copy link
Copy Markdown
Owner Author

Companion portal PR and live Supabase status:

  • Portal companion PR: fix(sync): preserve training cycle template ids phoenix-portal#85
  • Applied the required Supabase migrations on project ilzlswmatadlnsuxatcv.
  • Deployed the updated mobile-sync-push and mobile-sync-pull Edge Functions.
  • Portal post-deploy checks passed: npm run check:edge-functions, npm run typecheck, npm run test:sync, and git diff --check.

This mobile branch still depends on the portal/database surface from PR #85 for synced template_id and cycle-week propagation.

val percentOfTm = (it.percent * 100).roundToInt()
(percentOfTm * TRAINING_MAX_FACTOR).roundToInt()
},
setWeightsPercentOfPR = computeFiveThreeOneSetWeightsForWeek(weekNumber),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: You refactored seven lines of week-1 percentage math into a one-line helper that ignores the very activeSets you just computed three lines above. The reps and AMRAP flag honor the template's custom percentageSets; the weights silently snap to canonical 5/3/1 numbers. That is a 'looks fine, behaves fine, until a user picks a deload week-1 template and gets 65/75/85% instead of 50/60/70% — and then their bench goes sideways because the integer rep count was right but the bar was a fortress.'

🩹 The Fix: Drive the weight list off activeSets for week 1 (and only week 1), and use computeFiveThreeOneSetWeightsForWeek(weekNumber) for weeks 2-4 — symmetric with how setReps and isAMRAP already work:

Suggested change
setWeightsPercentOfPR = computeFiveThreeOneSetWeightsForWeek(weekNumber),
setWeightsPercentOfPR = activeSets.map { it.percent.toInt() }.coerceAtLeast(0).let { percents ->
if (weekNumber == 1) percents.map { p -> (p * 90).toInt() } else computeFiveThreeOneSetWeightsForWeek(weekNumber)
}.ifEmpty { computeFiveThreeOneSetWeightsForWeek(weekNumber) },

Or, cleaner: change computeFiveThreeOneSetWeightsForWeek(weekNumber) to accept an optional activeSets override and pick the right shape inside. Either way, custom week-1 percentages must reach the bar.

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

}
}

val mainLiftIndex = matchingIndexes.first()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 The Roast: matchingIndexes.first() is the silent-rewrite of an accessory. You already know about it — the Logger.w two lines up basically yells 'I'm rewriting the wrong row' — and the answer was to skip duplicates anyway. As written, a routine that legitimately contains the main lift at index 1 and a same-id accessory at index 0 will rewrite the accessory's percentages and leave the actual main lift at last week's values, then updateWeekNumber writes week N+1 — best case the user does this week's main at last week's weights; worst case TM bumps off a stale value and you get a regression report that 'just started out of nowhere.'

🩹 The Fix: Treat multiple matches as a hard error, not a logger warning. Reuse the failedExpectedMainLift gate that already aborts the entire use case and surface the duplicate in the abort log. Concretely, replace the Logger.w { ... } and .first() with:

Suggested change
val mainLiftIndex = matchingIndexes.first()
}
if (matchingIndexes.size > 1) {
throw IllegalStateException(
"5/3/1 regeneration found multiple expected main lift matches: routineId=${routine.id} dayNumber=${day.dayNumber} expectedExerciseId=$expectedMainLiftId matches=${matchingIndexes.joinToString(\",\")}",
)
}
check(matchingIndexes.size == 1) { "5/3/1 regeneration requires exactly one main lift match" }

…and propagate it through the existing failedExpectedMainLift flag so the week-sentinel is preserved. Loud failure beats silent rewrite every time.

📏 Severity: warning


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Code Review Roast 🔥

Verdict: No Issues Found | Recommendation: Merge

The incremental commits on top of 9ccb2a40 cleanly retire the two outstanding review threads from the prior pass — and they do it with deletion-first moves, which is the rarest and most welcome shape a code review can take.

38f76e3e (refactor: inline 531 detector calls) deletes the two private extension wrappers (fiveThreeOneMainLiftId() at the old L176 and hasFiveThreeOneSetShape() at the old L183) that the prior review flagged as single-call-site aliases. The two call sites in regenerateRoutineForWeek() now go straight through FiveThreeOneRoutineDetector.mainLiftId(exercise) / hasKnownSetShape(exercise). Net file change: −12 lines, +6 lines. The unused RoutineExercise import goes with them. Pure shrink, no behavior delta, no drift risk.

ec8ec764 (fix: preserve cycle template metadata on edit) addresses the P2 finding on SqlDelightTrainingCycleRepository:277. CycleEditorViewModel.saveCycle() now does a scoped repository.getCycleById(state.cycleId) (guarded by if (isNewCycle) null else …) and threads templateId = existingCycle?.templateId and weekNumber = existingCycle?.weekNumber ?: 1 through TrainingCycle.create(...). The existingCycle?.profileId clause added to the profileId fallback chain is mildly redundant with state.originalProfileId (already populated from cycle.profileId in initialize) but reads correctly as a defense against concurrent updates between init and save. The companion test editing cycle preserves template id and week number pins exactly the regression scenario — edit a week-3 5/3/1 cycle, save, assert week 3 and template_531 survive.

🏆 Best part: 38f76e3e is the rarest kind of follow-up — it deletes code that the previous review pointed at. No new helpers, no new abstractions, no new tests, no new comments. Just a call-site expansion and a quieter file. The detector-as-truth-source pattern is now actually thin.

💀 Worst part: Nothing to roast. The existingCycle?.profileId redundancy is noted but it's three lines of fall-through in a save path that runs once per user edit, and pulling template metadata into CycleEditorUiState to drop the DB read would be more state plumbing for break-even. Stays parked.

📊 Overall: Like the second pancake — turns out better than the first because someone actually flipped when the previous reviewer said 'this is overcooked.'

Files Reviewed (3 changed files)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/domain/usecase/RegenerateFiveThreeOneRoutinesUseCase.kt — 0 issues (single-use extension wrappers at old L176/L183 deleted; detector calls inlined at L115/L119)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/viewmodel/CycleEditorViewModel.kt — 0 issues (existingCycle read at L310, profileId fallback extended at L315, templateId/weekNumber preserved at L353-354)
  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/viewmodel/CycleEditorViewModelTest.kt — 0 issues (new regression test at L199-237 pins templateId="template_531" + weekNumber=3 survival through a name-only edit)

Correctness / Safety Findings

No correctness or safety findings. The existingCycle lookup is correctly scoped to edit paths (if (isNewCycle) null else repository.getCycleById(state.cycleId)), the templateId carry-over is straight pass-through, and the weekNumber fallback to 1 for new cycles matches TrainingCycle.create()'s default. The new test exercises the exact failure mode called out in the previous review: edit a week-3 5/3/1 cycle, save it, assert week 3 (not 1) and template_531 (not null) survive.

Ponytail Review

Ponytail: Lean already. Ship.

(Minor consistency observation, not flagged: existingCycle?.profileId at L315 duplicates information already in state.originalProfileId. Lifting templateId/weekNumber into CycleEditorUiState next to originalProfileId would drop the extra DB read on save and unify the pattern, but the trade-off is more state plumbing for a single read on an infrequent path — break-even, not worth a follow-up.)

Ponytail net: 0 lines.

Suggested Minimal Patch

No patch needed.

Final Merge Guidance

Can merge as-is. Both active review threads (RegenerateFiveThreeOneRoutinesUseCase.kt:180 nitpick and SqlDelightTrainingCycleRepository:277 P2 metadata preservation) are now closed by code and test on changed lines. The P1 backfill thread on ActiveSessionEngine.kt:1915 was addressed in dbb2c787 (structurally-detected legacy 5/3/1 cycles qualify for advancement even with templateId = null) and is also closed.

Previous Review Summary (commit 9ccb2a4)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 9ccb2a4)

Verdict: No Issues Found | Recommendation: Merge

The incremental commit (ec8ec76, "fix: preserve cycle template metadata on edit") correctly addresses the P2 reviewer concern from the previous pass — the SqlDelightTrainingCycleRepository:277 finding ("Preserve 5/3/1 metadata on cycle edits") is now closed. CycleEditorViewModel.saveCycle() re-reads the existing cycle and carries templateId and weekNumber through to the new TrainingCycle.create(...) call, and the new editing cycle preserves template id and week number test pins the behavior end-to-end against FakeTrainingCycleRepository.

No new findings on changed lines. The existingCycle?.profileId clause added to the profileId fallback chain is mildly redundant with the existing state.originalProfileId (which is already populated from cycle.profileId in initialize), but it is defensive against concurrent updates between init and save and reads correctly, so it stays.

🏆 Best part: The fix is surgical — one DB read inside saveCycle, two extra lines on the TrainingCycle.create(...) call, one new regression test that covers exactly the bug it would have prevented. No new abstraction, no new helper, no new state field. Someone resisted the urge to bolt on a TemplateMetadata value object.

💀 Worst part: There is nothing to roast on this commit. The two-line metadata carry-over is the right shape. The only thing left to complain about is the still-pending nitpick from the prior review (single-use extension wrappers in RegenerateFiveThreeOneRoutinesUseCase.kt:180), but that lives outside this incremental diff and stays parked as optional cleanup.

📊 Overall: Like the maintenance engineer who actually fixes the leak you reported instead of just signing off on the work order — minimum scope, maximum correctness, with the receipt attached.

Files Reviewed (2 changed files)
  • shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/viewmodel/CycleEditorViewModel.kt — 0 issues (existing-cycle read at L310, profileId fallback extended at L315, templateId/weekNumber preserved at L353-354)
  • shared/src/androidHostTest/kotlin/com/devil/phoenixproject/presentation/viewmodel/CycleEditorViewModelTest.kt — 0 issues (new regression test at L199-237 pins templateId="template_531" + weekNumber=3 survival through a name-only edit)

Correctness / Safety Findings

No correctness or safety findings. The new existingCycle lookup on save is correctly scoped (if (isNewCycle) null else repository.getCycleById(state.cycleId)), the templateId carry-over is straight pass-through, and the weekNumber fallback to 1 for new cycles matches the existing default in TrainingCycle.create(). The test exercises the exact failure mode the previous review called out: edit a week-3 5/3/1 cycle, save it, assert week 3 (not 1) and template_531 (not null) survive.

Ponytail Review

Ponytail: Lean already. Ship.

(Minor consistency observation, not flagged as a finding: the existingCycle?.profileId clause at L315 duplicates information already in state.originalProfileId. Lifting templateId/weekNumber into CycleEditorUiState next to originalProfileId would drop the extra DB read on save and unify the pattern, but the trade-off is more state plumbing for a single read on an infrequent path — break-even, not worth a follow-up.)

Ponytail net: 0 lines.

Suggested Minimal Patch

No patch needed.

Final Merge Guidance

Can merge as-is. The P2 metadata-preservation concern from the prior review pass is closed by both code and test in this incremental commit. The remaining nitpick on RegenerateFiveThreeOneRoutinesUseCase.kt is unchanged outside this diff and remains optional cleanup.

Fix these issues in Kilo Cloud


Reviewed by minimax-m3 · Input: 38.8K · Output: 4.5K · Cached: 516.1K

@9thLevelSoftware

Copy link
Copy Markdown
Owner Author

Addressed the active review feedback in 17bb06d2:

  • Regeneration now identifies 5/3/1 main lifts from routine contents instead of fragile cycle-day names, while skipping accessory-only days.
  • Ambiguous or duplicate same-lift routine matches now fail safely instead of silently rewriting the first match.
  • Custom template percentage sets now drive generated set weights.
  • saveWorkoutSession now updates cycle progress before triggering online sync.
  • Removed duplicate active-cycle SQLDelight row lookups in sync enforcement paths.

Verification rerun:

  • ./gradlew.bat --% :shared:testAndroidHostTest --tests *RegenerateFiveThreeOneRoutinesUseCaseTest* --tests *TemplateConverterTest* --tests *ActiveSessionEngineIntegrationTest* --tests *ConflictResolutionTest* -Pskip.supabase.check=true --console=plain
  • ./gradlew.bat --% :shared:testAndroidHostTest -Pskip.supabase.check=true --console=plain
  • git diff --check

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 17bb06d20a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@9thLevelSoftware

Copy link
Copy Markdown
Owner Author

Follow-up in dbb2c787: legacy upgraded 5/3/1 cycles with templateId = null now still qualify for week advancement when their routines structurally contain all four 5/3/1 main lifts with known 5/3/1 set shapes. Added an integration regression covering a null-template 5/3/1 cycle advancing from week 1 to week 2.

Verification rerun:

  • ./gradlew.bat --% :shared:testAndroidHostTest --tests *ActiveSessionEngineIntegrationTest* --tests *RegenerateFiveThreeOneRoutinesUseCaseTest* --tests *TemplateConverterTest* --tests *ConflictResolutionTest* -Pskip.supabase.check=true --console=plain
  • ./gradlew.bat --% :shared:testAndroidHostTest -Pskip.supabase.check=true --console=plain
  • git diff --check

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbb2c787a3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@9thLevelSoftware

Copy link
Copy Markdown
Owner Author

Follow-up in 9ccb2a40: moved the 5/3/1 main-lift IDs and set-shape detection into FiveThreeOneRoutineDetector and reused it from both ActiveSessionEngine and RegenerateFiveThreeOneRoutinesUseCase, so the progress gate and regeneration logic cannot drift.

Verification rerun:

  • ./gradlew.bat --% :shared:testAndroidHostTest --tests *ActiveSessionEngineIntegrationTest* --tests *RegenerateFiveThreeOneRoutinesUseCaseTest* --tests *TemplateConverterTest* --tests *ConflictResolutionTest* -Pskip.supabase.check=true --console=plain
  • ./gradlew.bat --% :shared:testAndroidHostTest -Pskip.supabase.check=true --console=plain
  • git diff --check

@9thLevelSoftware

Copy link
Copy Markdown
Owner Author

Follow-up in ec8ec764: edited cycles now preserve existing templateId and weekNumber when CycleEditorViewModel.saveCycle() reconstructs a TrainingCycle, so editing a week-3 5/3/1 cycle no longer resets it to week 1/null template metadata. Added a regression in CycleEditorViewModelTest.

Verification rerun:

  • ./gradlew.bat --% :shared:testAndroidHostTest --tests *CycleEditorViewModelTest* --tests *ActiveSessionEngineIntegrationTest* --tests *RegenerateFiveThreeOneRoutinesUseCaseTest* --tests *TemplateConverterTest* --tests *ConflictResolutionTest* -Pskip.supabase.check=true --console=plain
  • ./gradlew.bat --% :shared:testAndroidHostTest -Pskip.supabase.check=true --console=plain
  • git diff --check

@9thLevelSoftware

Copy link
Copy Markdown
Owner Author

Follow-up in 38f76e3e: removed the two single-use detector wrapper extensions and now call FiveThreeOneRoutineDetector directly from regeneration.

Verification rerun:

  • ./gradlew.bat --% :shared:testAndroidHostTest --tests *RegenerateFiveThreeOneRoutinesUseCaseTest* --tests *ActiveSessionEngineIntegrationTest* --tests *CycleEditorViewModelTest* -Pskip.supabase.check=true --console=plain
  • ./gradlew.bat --% :shared:testAndroidHostTest -Pskip.supabase.check=true --console=plain
  • git diff --check

@9thLevelSoftware 9thLevelSoftware merged commit d4ecd4e into main Jul 8, 2026
10 checks passed
@9thLevelSoftware 9thLevelSoftware deleted the codex/issue-632-mobile-531-auto-week branch July 8, 2026 02:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5/3/1 week auto-advancement for template cycles

1 participant