task 8 context - xopherdeep/do-it-for-the-xp GitHub Wiki

Task 8 context from the lead (decisions the brief cannot know)

Earlier tasks you consume (all on this branch, read them with chemx read <file> --outline / --symbol=<name>):

  • T3 src/lib/services/projects/projectSchedule.ts: PhaseWindow, ROOF_PHASE_ID ("roof"), daysBetween, extendWindows(windows, shares, clearedIds, today, newEndOn) (re-spreads uncleared phases from today by their renormalised shares; refuses a range too short).
  • T3 src/lib/services/projects/floorBonus.ts: floorMultiplier(window, clearedOn YYYY-MM-DD), floorBonusPool(quests {xp,gp}[], multiplier), splitFloorBonus(pool, workerIds, lastWorkerId). Use these; do not re-derive the math.
  • T1 src/lib/engine/quests/QuestSchedule.ts: isProjectQuest, the quest's project: { projectId, floor, dueOn, wokenAt? } link.
  • T6 src/lib/services/projects/projectQuests.ts: ProjectTowerInput (has projectId, heroes?), projectStartsOn(opensOn) (local midnight ISO), windowOfPhase, projectQuestId; src/lib/services/projects/projectTower.ts: buildProjectTower(input); src/lib/services/projects/towerRecord.ts: roofFloorKey.
  • T7 src/lib/services/projects/projectRecord.ts (ProjectRecord, ProjectFloor, newProjectRecord) and src/lib/databases/ProjectDb.ts (getProjectDb, getActive, getAll, save, lastOfType).
  • Rewards are paid by applyQuestRewards (see how src/lib/services/quests/QuestCompletionService.ts calls it at ~L166 and ~L230: plain label strings like Quest: <name>, so "Floor bonus: " follows that pattern).

Rulings (follow them; they are decided):

  1. The roof's floor key is roofFloorKey = ${phaseCount + 1}F (e.g. "4F" for three phases), NOT "RF"; its phaseId is "roof". The brief's "settling RF" test means the roof floor key. The roof's window is the last phase's window (already stored on its ProjectFloor).
  2. A floor's clear day (clearedOn) = the local calendar day (dayKey, or dateKey of completedAt in local time) of the LATEST completedAt among the floor's paid entries, never reviewedAt. The hero of that entry is lastWorkerId for the remainder.
  3. A "paid" entry is a quest-log entry for one of the floor's quest ids whose rewards were applied (rewardsApplied === true); a pending or rejected one does not count. A floor quest id with no quest record (deleted by the GM) does not count and does not block the clear.
  4. The bonus pool is (m - 1) x the XP and GP of every existing chore on the floor, each chore counted ONCE (a team chore paid to three heroes counts once), through floorBonusPool; the workers are the distinct userIds of the floor's paid entries, split through splitFloorBonus. AP is never multiplied: grants pay { xp, gp, ap: 0 }. A grant whose xp and gp are both 0 (a floor cleared late, x1) is not paid (no empty reward lines).
  5. "Already settled" = the floor has clearedAt. Settling twice (a second approval, two phones) returns null: bonus paid once, next floor woken once. Set multiplier, clearedAt and bonusPaidAt on the floor at settlement.
  6. Empty floors (owner rule: a deleted chore no longer counts): a floor whose quests no longer exist reads cleared. When a settlement wakes the next floor and that floor is itself already cleared (no live chores), it settles in the same pass with no grants and wakes the one above, and so on; an empty roof finishes the project. Put this cascade in the pure module with a test ("wakes past an empty floor"); keep the brief's exported signatures (you may add a pure helper). FloorSettlement.wake names the floor that ends up woken and open (the highest one woken).
  7. startProject(input, rewardText): refuse while getActive() returns a record; input.projectId is ${typeId}-${startOn} (earlier ruling R4); build with buildProjectTower(input) (input must carry heroes, which drive approvals: pass them through, never drop them); save the tower's temple record the way the Moving House pack saves its generated temple (find it with chemx q in src/lib/services/core/packs/movingPack.ts; temple id gen-project-<typeId>, a re-run replaces it), save its quests the way that pack saves quests (merge, never wipe), then save newProjectRecord(...).
  8. extendProject(newEndOn, today): refuse newEndOn before today (and let extendWindows refuse a range too short for the uncleared phases). Shares come from the project type's phases (src/lib/services/core/packs/projects/projectTypes.ts). Rewrite every UNCLEARED floor's window, its quests' startsOn with projectStartsOn(window.opensOn) and their project.dueOn with the window's dueOn; the roof takes the last phase's new window; set endOn. Cleared floors keep their window, multiplier and bonus.
  9. abandonProject(): delete the active project's quests that have no paid entry, delete its temple record, keep paid chores (their quest records and log entries stay), set status: "abandoned"; no trophy (abandoned runs never count as cleared, lastOfType already reads only cleared).
  10. markRewardGiven() sets reward.givenAt on the newest cleared record whose reward was earned and not given; markSeen(heroId, what) adds the hero once to reward.seenBy ("reward") or creditsSeenBy ("credits") of the newest cleared record. Both are no-ops when there is none.
  11. In QuestCompletionService, the hook runs only for isProjectQuest(quest), after rewards are applied, in completeQuest's paid branch and in approveCompletion when shouldPay; any failure is caught and logged with debug.warn and never blocks the payment. Keep the hook a one-line call into projectService (screens and services stay thin; the rules live in projectProgress.ts).
  12. Out of scope here (T10 owns the tower in play): the arrival wake from TempleGrounds and the Boss Key drop when the key-guard chore was deleted. Do not touch temple screens or room interactions.

Known parked notes in this area (ledger, for your awareness only): isQuestActiveOn ignores date for a woken project quest (T1, deferred); a malformed project.dueOn parses as today, so always write a valid YYYY-MM-DD dueOn. 13. One small extra (the board gate on T7 flags it): in src/lib/services/projects/projectRecord.ts:75, name the inline compound condition before the if (e.g. const hasDates = Boolean(first && last);), as the audit's CONTROL_FLOW_INLINE_BOOLEAN rule asks. Run tests/unit/projectRecord.spec.ts after it. Name your own compound booleans the same way in the new files.

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