task 11 report - xopherdeep/do-it-for-the-xp GitHub Wiki

Task 11 report: the finish (reward, credits, trophy)

Implementer @sonnet-pt11. Status: DONE_WITH_CONCERNS (see Concerns). No git writes. Audit: chemx audit at the worktree root reads gate: pass (ratchet) (322 hazards, the same ratchet the lead set; the two hazards the lead named, CONTROL_FLOW_INLINE_BOOLEAN in credits.ts and COMPLEXITY_CYCLOMATIC_HIGH in XpApprovals.vue, are fixed).

What I built

Owner ruling 2026-10-27 applied: the credits drop the ages. Maker is { name, role }; there is no birth year, no makerAge, and no spec, fixture or snapshot mentions an age or a birth year (I grepped my new files for birth, age, aged, makerAge: nothing).

Pure rules (all with specs):

  • src/lib/services/copy/credits.ts (new): Maker, MAKERS (the six makers, role and name, in the brief's order; the one place to trim), HeroCredit, heroCredits(project, quests, paid, heroes), bossQuestIds(project).
  • src/lib/services/projects/projectFinish.ts (new): daysEarly, trophiesOf (cleared runs, newest first), isRewardToGive / rewardToGive, finishStepFor(project, heroId) ("reward", then "credits", then null), creditHeroesOf(profiles), creditsViewOf, trophyOf (the trophy sheet's view of a run: dates, days early, reward, each floor's multiplier, x1 when none), towerNameOf.
  • src/lib/services/projects/finishPrompts.ts (new): rewardRevealPrompt(project, voice) (Tower Keeper, "Family reward unlocked!", the tower, the family's own words verbatim, who hands it over, one Onward).
  • src/lib/services/copy/catalog/projectFinish.ts (new, registered in catalog/index.ts): reward reveal, credits headings, trophy sheet, Approvals row, trophy spot cue; simple/adventure/classic where the voices differ, no em dashes, the makers' names and roles are data in credits.ts.
  • projectService.ts: readTrophies(), readFinish(projectId?) (the cleared run with the quests and only the PAID log entries), readRewardToGive(); markRewardGiven now uses the shared rewardToGive rule (same behaviour).

Screens and wiring (thin):

  • src/components/organisms/CreditsRoll/CreditsRoll.vue (new): full-screen CSS scroll in StatusPlz, cream text on the theme window (--gs-* tokens only, no hex), "The Heroes of " with Started / Finished and "N days early!", each hero (name the class, Chores, Bosses, Floors), then "Do It For The XP" with each maker's role and name, then "Thank you for playing!"; Skip at the bottom (thumb reach); done emitted once (scroll end or skip); prefers-reduced-motion shows it static. BGM untouched.
  • src/app/Console/MyPortal/HomeTown/components/ProjectTrophySheet.vue + _ProjectTrophySheet.scss (new): cleared runs newest first (a tap picks one), "Watch the credits" (everyone), "Start again" (Game Master only), actions in the bottom bar.
  • HomeTown.ts/.vue: the spot now stands for the running project, else the newest cleared run (the trophy: standingProjectOf in towerSpot.ts, cue "Cleared! See the trophy"), else nothing; a tap opens the tower sheet or the trophy sheet; "Start again" closes the trophy and opens XpProjectWizardModal on the run's type; the plaza looks again when the wizard closes. ProjectTowerSheet.vue is untouched (the trophy opens from the spot, not through its empty slot).
  • XpProjectWizardModal.vue: one optional prop typeId; when given, the draft opens on that type with lastOfType's chores (the existing rerun path), so "Start again" starts from last time's list.
  • src/hooks/useProjectFinish.ts (new) + MyPortal.ts/.vue (+4 lines, +3 template lines; MyPortal.ts 319 to 329): on portal open and on each route change, a hero owed the reward gets the reveal prompt once, then the credits once. The hero is markSeen BEFORE it shows (a second look cannot show it twice; if markSeen fails nothing shows). Held back while the hero is in a temple, in a fight, or played by a Game Master; also waits for the profiles to load.
  • XpApprovals.vue + ApprovalFamilyRewardRow.vue (new): the row at the top of the achievements tab, "Family reward earned: " with "Mark as given" (calls markRewardGiven, then the page reads again); the segment badge counts it. dashboardTiles.approvalsTotal(chores, rewards, familyReward = 0) counts an earned, ungiven reward as one; XpDashboard.ts feeds it.

What I keyed "boss" on (ruling 1)

bossQuestIds(project) reads the project record's own chore list (project.chores, the list the tower was built from), not the temple record or the live quests: for each phase (the roof included) the biggest chore by difficulty, the first on a tie, as quest id projectQuestId(project.id, chore.id). That is exactly how projectTower.ts picks the boss of each floor (biggestOf: the floor boss, the last phase's key guard) and the roof's single finale. Reasons: heroCredits has no temple argument, the type's temple is replaced by a re-run (so an old trophy's tower is gone), and a Game Master deleting a quest must not hand the boss to another chore. Drift guard: credits.spec.ts builds real towers (both types, three room sets, two families = 8) and checks the ids equal the phase biggest chores plus the finale, that each is held ALONE in a difficulty: "boss" room of the real tower, that exactly one is the sealed miniboss (and sits on the last phase's floor) and exactly one the boss finale.

TDD evidence

RED (spec written first; command NODE_PATH=$PWD/node_modules/.pnpm/node_modules chemx test tests/unit/<name>.spec.ts):

  • credits.spec.ts: Test suite failed to run ... credits.spec.ts:8:68 import { MAKERS, bossQuestIds, heroCredits ... } from "@/lib/services/copy/credits" (module missing).
  • projectFinish.spec.ts: Test suite failed to run ... Cannot find module '@/lib/services/projects/finishPrompts'.
  • projectServiceFinish.spec.ts (+ the rewardToGive cases in projectFinish.spec.ts): TS2305 ... has no exported member 'readRewardToGive'.
  • XpDashboard.spec.ts: TS2554: Expected 2 arguments, but got 3 (approvalsTotal(2, 3, 1)).
  • ApprovalFamilyRewardRow.spec.ts: Could not locate module @/app/Admin/XpApprovals/components/ApprovalFamilyRewardRow.vue.
  • ProjectTrophySheet.spec.ts: Could not locate module .../HomeTown/components/ProjectTrophySheet.vue.
  • towerSpot.spec.ts + HomeTown.spec.ts + XpProjectWizardModal.spec.ts together: 5 failed out of 31 (standingProjectOf not exported; wrapper.vm.openTower is not a function; the wizard's new typeId case).
  • CreditsRoll.spec.ts and useProjectFinish.spec.ts: written before their modules, but I did not run them until the implementation existed (first run GREEN), so there is no RED run. Instead a mutation check: removing the once-only guard in CreditsRoll.vue and moving markSeen after the show in useProjectFinish.ts made 4 failed out of 27 (says done once, however it ends: Expected length 1, Received 3; remembers the hero was told before the prompt shows; shows nothing when the hero could not be remembered; roll once the reward was answered); reverted, green again.

GREEN, one combined run of every spec named in the dispatch plus the neighbours that load files I changed: chemx test tests/unit/credits.spec.ts tests/unit/CreditsRoll.spec.ts tests/unit/ApprovalFamilyRewardRow.spec.ts tests/unit/projectFinish.spec.ts tests/unit/projectServiceFinish.spec.ts tests/unit/ProjectTrophySheet.spec.ts tests/unit/useProjectFinish.spec.ts tests/unit/ProjectTowerSheet.spec.ts tests/unit/towerSpot.spec.ts tests/unit/HomeTown.spec.ts tests/unit/XpDashboard.spec.ts tests/unit/XpProjectWizardModal.spec.ts tests/unit/voices.spec.ts tests/unit/HexLiteralRatchet.spec.ts tests/unit/XpApprovals.spec.ts tests/unit/projectService.spec.ts -> All tests passed (276 passed in 46507ms). (voices.spec covers the new catalogue file; HexLiteralRatchet covers the new .vue/.scss.)

Files changed

New: src/lib/services/copy/credits.ts, src/lib/services/copy/catalog/projectFinish.ts, src/lib/services/projects/projectFinish.ts, src/lib/services/projects/finishPrompts.ts, src/hooks/useProjectFinish.ts, src/components/organisms/CreditsRoll/CreditsRoll.vue, src/app/Console/MyPortal/HomeTown/components/ProjectTrophySheet.vue, .../_ProjectTrophySheet.scss, src/app/Admin/XpApprovals/components/ApprovalFamilyRewardRow.vue; specs tests/unit/{credits,CreditsRoll,ApprovalFamilyRewardRow,projectFinish,projectServiceFinish,ProjectTrophySheet,useProjectFinish}.spec.ts. Modified: src/lib/services/copy/catalog/index.ts, src/lib/services/projects/projectService.ts, src/app/Admin/XpApprovals/XpApprovals.vue, src/app/Admin/XpDashboard/dashboardTiles.ts, src/app/Admin/XpDashboard/XpDashboard.ts, src/app/Console/MyPortal/MyPortal.ts, src/app/Console/MyPortal/MyPortal.vue, src/app/Console/MyPortal/HomeTown/HomeTown.ts, .../HomeTown.vue, .../components/towerSpot.ts, src/app/SideMenu/XpSettings/components/XpProjectWizardModal.vue; specs tests/unit/{XpDashboard,towerSpot,HomeTown,XpProjectWizardModal}.spec.ts. Not mine: chemx-ratchet.json shows modified (the local chemx rewrote it, as the lead said).

Self-review

  • Rules in pure modules with specs; screens and MyPortal.ts stay thin (portal-open logic is the composable). None of the files the lead listed as under parallel repair (floorBonus, projectChores, projectSchedule, projectTower, projectQuests, ProjectDb, ...) is edited; I only import from projectQuests (projectQuestId).
  • Every condition before an if is named; no switch; no passthrough booleans. Largest new file: ProjectTrophySheet.vue (~190 lines). XpApprovals.vue stayed under complexity 12 by moving the reward read into readFamilyReward (zero new branches in loadApprovals).
  • Tokens only: no hex literal in any new .vue/.scss (--gs-* for the roll, --xp-app-* for the sheets/rows); HexLiteralRatchet green. No em dashes in the new copy.
  • Controls at the bottom (Skip, Watch the credits, Start again); the cleared tower is a sheet, not a walk.
  • I could not run chemx typecheck (rule). I read the .vue scripts for types by hand; the .ts files were type-checked by ts-jest diagnostics when their specs ran. Not looked at in a browser (no dev server).
  • A few edits were made through short python scripts in Bash before I saw the chemx hook note (spec edits, catalog/index.ts, projectService.ts, dashboardTiles.ts, XpDashboard.ts, the first XpApprovals.vue pass, one cat >> append to towerSpot.spec.ts); after the note I used the Edit/Write tools. The hook also named widgetCatalog.ts, widgetSyncTrigger.ts, ThemeSettings.vue and user/persistence.ts: those are not mine (peer writes).

Concerns / decisions the lead should know

  1. Team chores (ruling 1 wording). The lead's text says a team finale counts "for every assignee who has a paid entry". A Team Effort (collaborate) chore pays ONLY the hero who did it (QuestSchedule: "one assignee's clear fills the window for every assignee, and only the hero who did it is paid"), so that reading would credit one hero for the family's finale. I followed the brief's "a team finale counts for every assignee": a paid collaborate quest counts for every assignee of the quest (any team chore, not only the finale); anyone with a paid entry on it counts too. One small function (creditedHeroes in credits.ts) to change if the owner wants the narrow reading. compete quests are not treated as team chores.
  2. Heroes with nothing cleared are left out of the roll (and anyone no longer in the family's list). "The Heroes of ..." lists who did the work; a "0 chores" line seemed unkind. Say so if the roll should list everyone.
  3. "Start again" is Game Master only (like Move the end date and Abandon on the tower sheet): the wizard picks chores and heroes. The brief did not say; hero kids see only "Watch the credits".
  4. The reveal and the credits follow the NEWEST cleared run (that is what markSeen writes to). A hero whose profile is created after a run was cleared will see that run's reveal and credits once; the trophy lets anyone rewatch. A cutoff by age of the run is a one-line addition in finishStepFor if wanted.
  5. credits.ts holds both the roster (the one data file) and heroCredits, as the brief lists them together; if the owner wants the roster alone in a file, heroCredits/bossQuestIds move to projectFinish.ts without other changes. Since the ages are gone, the lead can commit credits.ts with the rest.
  6. XpProjectWizardModal.vue (Task 9's file) gained the optional typeId prop (3 lines + its spec case).
  7. HomeTown now statically imports the wizard modal; it is only mounted open on demand, so the cost is bundle size only.

Task 11 fix round 1 (@sonnet-pt11)

Status: DONE. Base: 55b7b584. Three items, all fixed; no git writes. (This report is appended with a shell heredoc; the code edits were made with the Edit/Write tools.)

1. One "newest" ordering (Important)

  • New src/lib/services/projects/projectOrder.ts: newestFirstBy(stamp) (newest first by a stamp, a tie to the later id) and newestClearedFirst (by clearedAt, a missing one last). It imports only a type.
  • projectProgress.ts (newestCleared, which markSeen and markRewardGiven use), projectFinish.ts (trophiesOf, which the reveal hook reads [0] of) and ProjectDb.ts (newestBy, behind getActive and lastOfType) now all sort with it; the three copies of compareText are gone.
  • Deviation from the dispatch, with the reason: I put the comparator in a small module of its own rather than exporting it from projectProgress.ts. ProjectDb.ts is imported by BackupService.ts, and projectProgress.ts imports packCommon, which imports BackupService: importing projectProgress from the store would close a cycle (BackupService, ProjectDb, projectProgress, packCommon, BackupService). projectOrder.ts has no runtime imports, so it cannot.
  • Specs: new tests/unit/projectOrder.spec.ts (the comparator itself, RED first: Test suite failed to run ... projectOrder.spec.ts:6:51 import { newestClearedFirst, newestFirstBy } ...); projectFinish.spec.ts "starts with the run newestCleared picks (the one markSeen stamps), over a tie and a run with no clear time, in any input order" (three input orders, the whole list order run-b, run-a, run-old, run-undated, and an all-undated pair); ProjectDb.spec.ts "picks the very run newestCleared picks over a tie and an undated run, whatever order they were saved in". The last two passed before the change too (the three copies were identical): they are the guard that keeps them from drifting apart again.

2. The family reward row stays shut after a save (Important)

  • ApprovalFamilyRewardRow.vue: isSaving is no longer reset after a successful markRewardGiven; it resets only on failure. The row stays disabled until the page reads again.
  • A consequence the lead's suggestion alone would have missed: with two ungiven rewards the page re-reads and the SAME row instance would show the next run's reward, still disabled for good. XpApprovals.vue now keys the row by the run (:key="familyReward.id"), so the next reward gets a fresh, enabled row (a one-attribute change in the template; the page itself has no unit spec).
  • Specs (RED first: expect(received).toBeDefined() ... Received: undefined on the disabled attribute): "stays shut after it saved: a quick second tap cannot hand over the next run's reward before the page reads again" (disabled, markRewardGiven called once, given emitted once) and "opens again only when it did not save".

3. The credits do not roll over a fight (Load-bearing)

  • useProjectFinish.ts: the loop checks isHeld() again at every moment, before markSeen (so a held credits moment is NOT marked as seen): const hasNothingToTell = !owed || isGone || isHeld();. The existing watcher on battleStore.active going false runs look() again, so the credits come when the fight is over. The reward prompt already on screen when the fight begins stays (it cannot be taken back); only the full-screen roll is held.
  • Spec (RED first: expect(received).toBeNull() ... Received: {"daysEarly":2,... "towerName":"The Spring Spire"}): "does not roll the credits over a fight that began while the reward showed, and rolls them when it is over" (the fight starts under the prompt, the prompt is answered: no credits, no markSeen(.., "credits"), no readFinish; the fight ends: credits roll and the hero is marked).

Covering run

RED (before the changes): chemx test tests/unit/projectOrder.spec.ts tests/unit/projectFinish.spec.ts tests/unit/ProjectDb.spec.ts tests/unit/ApprovalFamilyRewardRow.spec.ts tests/unit/useProjectFinish.spec.ts -> 2 failed out of 62 plus the new suite failing to run.

GREEN: NODE_PATH=$PWD/node_modules/.pnpm/node_modules CHEMX_AGENT_ID=@sonnet-pt11 chemx test tests/unit/projectOrder.spec.ts tests/unit/projectFinish.spec.ts tests/unit/ProjectDb.spec.ts tests/unit/ApprovalFamilyRewardRow.spec.ts tests/unit/useProjectFinish.spec.ts tests/unit/projectProgress.spec.ts tests/unit/projectServiceFinish.spec.ts tests/unit/ProjectTrophySheet.spec.ts -> All tests passed (148 passed in 73489ms). Neighbours that load the changed files: chemx test tests/unit/projectService.spec.ts tests/unit/XpProjectWizardModal.spec.ts tests/unit/HomeTown.spec.ts tests/unit/credits.spec.ts -> All tests passed (118 passed in 61872ms). (ProjectDb.spec.ts imports BackupService, and passed with the store now importing projectOrder: no cycle.)

Audit: chemx audit at the worktree root -> chemx audit .: grade A+ (99/100), 322 hazards: 0 critical, 251 high/medium, 71 low and gate: pass (ratchet).

Files changed in this round

New: src/lib/services/projects/projectOrder.ts, tests/unit/projectOrder.spec.ts. Modified: src/lib/services/projects/projectProgress.ts, src/lib/services/projects/projectFinish.ts, src/lib/databases/ProjectDb.ts, src/app/Admin/XpApprovals/components/ApprovalFamilyRewardRow.vue, src/app/Admin/XpApprovals/XpApprovals.vue, src/hooks/useProjectFinish.ts; specs tests/unit/{projectFinish,ProjectDb,ApprovalFamilyRewardRow,useProjectFinish}.spec.ts.

⚠️ **GitHub.com Fallback** ⚠️