Final whole-branch review of M4-b. Six findings.
1. The player could press his way into an invalid draft. can_take_bonus()
refused an already-picked skill; can_pick() did not consider the bonus, so
the guard was one-directional. Take athletics as the human's bonus, pick it
again from the Sellsword's pool, and the draft is one NewGame.validate
rejects — the invalid draft "he did not cause and cannot see" that
set_calling's own comment forbids. Fixed in the DRAFT (the screen owns no
rules): can_pick() now mirrors can_take_bonus(), so the chip goes inert, the
idiom the pool row already uses for a race-granted skill. Recoverable: hand
the bonus back and the chip is live again. Both directions tested.
2. The theme drift guard STILL could not fail. ThemeKeys.ALL is stylebox
variations only, by its own docstring — so the six FONT roles the builder
sets fonts, sizes and colours for were entirely unguarded. Changing
Palette.CREAM and skipping the rebuild shipped a stale game_theme.tres with
the suite green (proven, on the committed test). Adds ThemeKeys.FONT_ROLES
beside ALL (never inside it — ALL's contract is relied on), compares fonts,
font sizes and font colours as well as styleboxes, and names the variation
AND the property that drifted. Plus a meta-guard that walks the builder's own
output and fails if a variation it styles is in neither collection — this is
the second time this guard has been fixed by "enumerate the right set", and
nothing was checking the set.
3. §13: the CTA label piped NewGame.validate's diagnostics straight to the
player. On the game's SECOND SCREEN he read "hedge_mage picks 2 skills, got
0". Now CreationCopy.error_line maps them to authored copy — "name yourself",
"choose two more proficiencies" (the number DERIVED from Callings.skill_count,
never written down), "one more proficiency, any of them — take it" — with an
authored fallback for anything unmapped, because §13 means the unmapped case
still speaks in voice. validate's strings are untouched: they are a contract,
so they are translated in the presentation layer, not rewritten.
4. §2 had no test that fails if the boundary is breached. The emit test compared
the signal to draft.to_creation() — both sides move together. Pins the seven
contract keys exactly, and feeds construct a hostile creation dict carrying an
18 in every stat, asserting the sheet is still roll_attributes(seed) + spend.
Every other test in test_new_game passed with that breach in place.
5. Three holes: the orphan sweep skipped PoolRow (a stray Pool6 rendered unbound
and visible, suite green — the exact bug the test exists to catch, in the one
row it forgot); the §7 sweep forbade "luck" but not Luck.BANDS' descriptors,
which are worse than the number (the player would re-roll until his Luck read
well — calculable Luck); and the DM panel's composition hardcodes the article
"a", so a vowel-initial calling would break it silently — guarded in the
content parity test, which names the template as the reason.
6. The calling card printed "talent second_wind". Four of seven talents carry an
underscore. Humanised, the way skill_label() already does. The test that
pinned the RAW form is moved to the full derived phrase ("talent second wind")
so it still fails if someone hardcodes a talent into the format string.
Every guard was proven by re-breaking the code and watching the named test go
red (docs/traps.md). Suite 302 -> 315, content build green, no new warnings.
Report: .superpowers/sdd/final-review-fix-report.md
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Review found three tests in test_character_creation_screen.gd could not fail
against the bug they named — including both tests claiming to enforce §7
(Luck is invisible) and the only test covering origin->calling gating. The
five-ability-cards test asserted the script's own range(5) loop bound, not
the scene. The Luck sweep filtered on `Label` and missed both RichTextLabels
(the only nodes rendering authored prose) plus every Button and the
LineEdit. The reroll test's pool assertion already held true before the
reroll ran. The origin-gating test ran only against the deserter origin,
which allows all 7 callings, so shown==allowed was a trivial 7==7.
Rewrote all four to assert against the scene, the rules tables, or a
restrictive injected origin instead of values the code already guaranteed.
Added a node-count guard tying authored cards/chips to Races/Callings/
Skills/Attributes table sizes. Deleted the test-only
_complete_a_valid_draft_for_test() from the shipped Control and replaced it
with a test-file helper that drives the real click handlers, closing the
gap where no test touched _on_race_pressed/_on_calling_pressed/
_on_pool_pressed/_on_bonus_pressed at all. Guarded _ready() against a
degenerate origin (empty/missing allowed_callings) indexing an empty array.
Each fixed guard was deliberately re-broken and confirmed red before being
reverted (see .superpowers/sdd/task-6-report-fix.md, gitignored). 291 tests
green, up from 289.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Variant C of the mock: seven nameplate calling cards in one row, with the chosen
calling's blurb AND mechanics in a detail panel below — the one card you
committed to is the only one that talks.
Editor-first (ADR 0001): the tree is in the .tscn, the script binds. No rules
here — CreationDraft holds them, CreationCopy derives the display strings, the
prose comes from ContentDB. Emits creation_confirmed(Dictionary); M4-c
constructs. Five ability cards. Never six (§7).
CreationDraft's docstring promised it owns no rules—it delegates to NewGame.validate.
But it kept a local const POOL := 3 that mirrored NewGame.SPEND_POOL, inviting
silent disagreement if the source was changed and the copy forgotten.
Delete POOL. Use NewGame.SPEND_POOL directly in points_left(), with a comment
making it clear this is the single authority. The coupling is now enforced by
code structure, not by hope.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure, node-free, headlessly testable. Carries a seed, not a stat block. Owns no
rules of its own: errors() delegates to NewGame.validate.
The interaction rules that make creation screens buggy are pinned by test:
changing calling clears the picks; changing race drops a pick the new race now
grants (the elf/cutpurse case); re-roll clears the spend and only the spend;
minus floors at the roll; the fourth point does not exist.
Display strings derived from Callings/Races/Skills at call time. Retune a hit
die and the card follows, because the card never knew the number. Prose stays in
content; numbers stay in code; nothing is written twice.
Deviation from the plan, approved by the human: the plan's test asserted the
rendered line contains Callings.armor(id) — the RAW table value — while the plan's
own ARMOR_WORD maps "none" to "no armour". "none" is not a substring of "no armour",
so the Hedge-Mage failed. The card's copy is what the plan's docstring example shows,
so the copy stands and the test was fixed: armor_word(id) is now public and the test
asserts against the derived word. The guard is unchanged — hardcode an armour string
and retune the table, and it still goes red.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>