The human's first F6 run found four defects headless GUT could not see, all
now fixed and break-proven.
1. Fifteen Labels shipped invisible. _fonts() sets a default font and size but
never a default font_color for the base Label type, so a Label with no
type-variation inherited Godot's built-in default — WHITE — and rendered
white-on-parchment: the four race names, four race blurbs, and seven calling
names, every unreadable string on the sheet. New CardTitle / CardBody roles
(mock #3a2f1c / #6a5a3a) fix it. NOT a global default Label colour: the Title
screen has 12 bare Labels on a dark background that rely on white.
2. The PRIMARY badge covered the rolled value. The ability card is a
PanelContainer, and a Container force-fits its children, so the badge's
authored anchors were dead letters — it stretched to the card's full width
and centred over the value. Nesting it under a plain Control (not a
Container) restores absolute positioning. The badges also carried _flat()'s
12/6 card padding; a new tight pill stylebox matches the mock's 2px 9px.
3. Race cards overflowed their own border (108px card, 114px of content). Bumped
to 140. The CHOSEN badge on the calling card moved from centre to the mock's
top-right, and the ability card got its own AbilityCard variation with 16px
top headroom for the badge (ParchmentCard is shared with the shell).
4. The default window (1600x900) was larger than a 1600x900 laptop's usable
area, so the WM clamped it to 1589x752 and the run came up pillarboxed.
Default is now 1280x720, resizable; the 1920x1080 design canvas is unchanged
(canvas_items + keep scales it, so no layout number moved).
Also closes M4-b's two open copy items: the cutpurse origin fragment (it
contradicted its own blurb) and the Hint label (it omitted the roll's floor of
8, now pinned to NewGame.roll_attributes).
Guards for 1-4 are new and each was re-broken and watched go red. The overflow
guard in 3 was FIRST WRITTEN AS A TAUTOLOGY — Godot clamps Control.size up to
its combined minimum, so `min <= size` is `x <= x` — and passed against the bug,
29/29, until it was rewritten to measure against the card that clips. That is
traps.md #17 and the thirteenth cannot-fail assertion this branch has caught.
319 client tests green, content build green, theme drift guard satisfied.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_committed_tres_matches_builder has now been "fixed" three times (traps.md #12).
v1 hardcoded 3 checks; v2 iterated ThemeKeys.ALL but missed the six font roles; v3
added a meta-guard specifically to catch "the builder styles a thing guarded by
nothing" — but that meta-guard contained `if fresh.get_type_variation_base(variation)
== &"": continue`, which whitelisted exactly the case it was built to catch. The
builder styles RichTextLabel directly (build_game_theme.gd's _rich_text()), with no
set_type_variation, so it reports base "" and was skipped unconditionally — and
RichTextLabel is what renders every line of DM prose the creation screen shows
(_origin_text, _detail_blurb). Nothing compared its fonts, font size, or colour, nor
the theme-level default_font/default_font_size, against the committed .tres.
Adds ThemeKeys.BASE_TYPES for base Control types the builder styles directly, folds
it into the drift guard's coverage, replaces the meta-guard's continue with an
assertion that names the offending type, and adds the missing theme-level default
font/size comparison. Proved with three reverted breaks: a RichTextLabel font-size
edit, a theme-level default_font_size edit, and an unregistered "GhostRole" variation
— all three now go red and name the drifted property.
Also: strengthens test_creation_copy's error-copy sweep to assert a mapped error
actually reaches its authored line rather than silently falling through to the
generic UNSPOKEN fallback (CreationCopy.UNSPOKEN satisfied all four prior checks,
so a renamed validator string could regress silently); and removes two assertions
that were true by construction / already true before their test's action ran.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
SelectCard (a Button whose `pressed` IS the chosen ring), SkillChip (a toggle
with unpicked/picked/granted-so-inert), the CHOSEN and PRIMARY tags, and the
gold PrimaryCard ring. Plus INK_LABEL_MUTED for the sheet's section headers.
Built by build_game_theme.gd and regenerated; the .tres is never hand-edited.
ThemeKeys.PARCHMENT_CARD is a PanelContainer-base variation (only a
`panel` stylebox) — a Button looks up normal/hover/pressed/disabled,
which don't exist there, so a Button styled with it renders unskinned.
Add PARCHMENT_BUTTON, a Button-base variation with those four
styleboxes plus an empty focus box, built from Palette colours only.
Same fix pattern as DOCK_BUTTON for the system dock. Regenerated
game_theme.tres from the builder.
DarkPanel is a PanelContainer-base variation (only a `panel`
stylebox); a Button looks up normal/hover/pressed, which don't exist
on it, so SystemDock's menu buttons rendered as default gray Godot
buttons instead of mock-faithful dark nav buttons.
Add a dedicated Button-base DockButton variation (Palette-only
colours, dock-dark normal/hover/pressed styleboxes), point
system_dock.gd's menu buttons at it, and regenerate the committed
game_theme.tres artifact from the builder.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- build_game_theme.gd: extract build_theme() as a static func returning the
fully-built Theme; _init() is now a thin save+quit wrapper. Enables a test
to build a fresh Theme in memory without running the SceneTree main loop.
- test_theme_resource.gd: add test_committed_tres_matches_builder() — builds
a fresh Theme via the preloaded builder and compares bg_color against the
committed game_theme.tres for PrimaryCTA/normal, ParchmentCard/panel, and
DarkPanel/panel. Catches "palette/builder changed, .tres not regenerated."
- build_game_theme.gd: give Tab, TabActive, and Chip explicit hover/pressed/
focus styleboxes (focus is StyleBoxEmpty to suppress the default ring) so
they no longer fall back to Godot's default gray button on interaction.
Add a disabled stylebox + font_disabled_color to PrimaryCTA for the
unaffordable-CTA state the 2a shell will need.
- theme_keys.gd: add HEADING/ACCENT/MONO font-role consts (kept out of ALL,
which is stylebox-variation-only); build_game_theme.gd and
theme_showcase.gd now reference them instead of raw StringName literals.
- theme_showcase.gd: add an italic DM-voice RichTextLabel (EB Garamond
italic face, spec §6) and two semantic-coloured chips (BLOOD for
aggressive actions, GOLD for pay/faith) alongside the existing neutral
chip, demonstrating the script-applied colouring from spec §5.3.
- dark_bay.gdshader: comment noting the stripe uses FRAGCOORD (screen-space)
while the vignette uses UV (node-local) — intended for full-bleed use.
- surfaces/*.gd: ## comments documenting the accepted deviation from spec
§5.4 (uniforms set at runtime from Palette rather than baked into the
.tscn) — no behavior change.
- Regenerated game_theme.tres via build_game_theme.gd.
Full suite: 125/125 (was 124, +1 drift-guard test).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GFDm1ku9WDUacK2S78m2be