252 Commits

Author SHA1 Message Date
e5fb173008 Merge M4-c: Title → Creation → Shell flow (the Main coordinator)
New Game now runs character creation and lands in the exploration shell as the
CREATED character — HP/MP/purse/location/opening-narration all real. A script-first
`Main` coordinator (scenes/Main.tscn, the main_scene) owns Title → Creation → Shell
via instantiate → inject → add_child; the title is emit-only; the shell consumes an
injected character (seed stays the F6 fallback). ⛨ saga fourth-path seam kept addable.

Completes M4. Full suite 327/327; whole-branch review clean; human F5 smoke passed.
Also adds the roadmap's next item (Ollama Cloud proxy provider + backend switch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 05:35:05 -05:00
f5e6c59f2a docs(roadmap): add Ollama Cloud provider + backend switch as the next item
A laptop-dev enabler (planned, not specced): extend the proxy's M1 model-call
pipeline with an Ollama Cloud backend reachable via API key, plus a switch to
flip local-ollama <-> ollama-cloud, so AI works off the homelab.

Reconciles against §4: the API key and provider choice stay server-side in the
proxy (the client never holds a key, never picks the backend). The title
Settings-button "config file" splits — the client may hold only the proxy
base URL / profile (folds into M9); the provider/key switch is proxy config.

Filed under a renamed "Landed / planned out of band" section and pointed to
from the sequencing table as the next task before M5.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 05:27:04 -05:00
7f155b8de2 docs(roadmap): M4-c lands — Title -> Creation -> Shell flow; M4 complete
Marks M4-c done (the Main coordinator wires new game -> creation -> shell,
the shell consumes the real created character, the saga fourth-path seam is
kept addable) and flips M4 -> done / M5 -> next in the milestone table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 05:20:45 -05:00
d553213563 refactor(m4c): rename GameFlow coordinator to Main, move to scenes/Main.tscn
The entry-point coordinator read as an internal name ("GameFlow" buried in
scenes/flow/), so it wasn't obvious THIS is the scene that boots first — the
title screen looked like the entry point. Renamed to the Godot-conventional
Main (scenes/Main.tscn + scripts/main.gd) so the boot scene is self-evident.
TitleScreen is unchanged and still shown BY Main as its first screen.

Also drops the hand-written gd_scene uid (now matches sibling scenes) —
closes the Minor review finding. Full suite 327/327 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 05:16:55 -05:00
5a9f39ea48 feat(m4c): GameFlow is the main scene — title->creation->shell is the boot path
Flips run/main_scene from TitleScreen to the GameFlow coordinator, so the
shipped boot path runs the real Title -> Creation -> Shell flow. Full suite
327/327 green headless; the human F5 smoke (ADR 0001 visual gate) is the
remaining acceptance step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 04:20:23 -05:00
583d32456a feat(m4c): GameFlow coordinator wires title -> creation -> shell 2026-07-15 04:15:28 -05:00
6b10b6ea6c feat(m4c): title screen is emit-only (routing moves to GameFlow) 2026-07-15 04:10:10 -05:00
8d963e8030 feat(m4c): shell consumes an injected character (seed stays the F6 fallback) 2026-07-15 04:06:44 -05:00
e169885d90 feat(m4c): ShellState.for_character derives vitals+location from the real character 2026-07-15 04:02:37 -05:00
f763aa0c31 docs(m4c): implementation plan — title → creation → shell flow
Five TDD tasks: ShellState.for_character, the shell's injected-character
seams, the emit-only title, the GameFlow coordinator (+ its flow and
construct-failure tests), and the main_scene flip with a human smoke gate.
Each task carries full test code, exact commands, and a revert-and-watch-red
step per docs/traps.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 03:54:18 -05:00
10f89baef9 Adding helper script to clean up stale superpowers sdd files 2026-07-15 03:47:30 -05:00
aaff2c8554 docs(m4c): spec — title → creation → shell flow
A GameFlow coordinator node (new main_scene) owns the Title → Creation →
Shell sequence in one file: instantiate → inject → add_child, reusing the
existing injection seam that change_scene_to_file cannot. Title becomes
emit-only; the shell gains injected_log/injected_state seams and stops
self-seeding on the real path (seed stays as the F6 fallback), so the
created character's vitals, location, and opening narration are real.

The ⛨ saga constraint is honored structurally: _show_creation and
_start_campaign are separate, so the fourth entry path is an added arm,
not surgery on existing edges.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 03:43:57 -05:00
a106be673f Merge M4-b: character creation screen
The mock's Character Creation screen over the M4-a model: four race cards, seven
nameplate calling cards (blurb + mechanics on the chosen one), the +3 additive
spend pool, calling-gated proficiency chips (+ the Human bonus-skill row), five
ability cards — never six, and 'luck' nowhere (§7) — and the DM origin panel.
Editor-first per ADR 0001: layout in the .tscn, script only binds.

F6 human gate: screenshot-verified all 8 checklist items. Granted skills read as
owned (gold + ✓), the small text was enlarged for sub-1920 laptops, four load-time
script warnings cleared. 319 client tests green.

Emits creation_confirmed(creation); the title→creation→shell wiring is M4-c.
2026-07-14 20:35:42 -05:00
1ae62fe0db feat(creation): distinguish granted skills as owned, and enlarge the small text
Two F6-review follow-ups from the human's read of the running screen.

1. GRANTED skill chips now read as 'you already have this', not 'unavailable'.
   The elf's granted `perception` shared SkillChip's dim `disabled` look with the
   'picks are spent' chips, so it read as blocked. Split out a fourth state,
   SkillChipGranted — a solid gold fill, cream text, and a ✓ prefix (owned) — while
   the pool-is-spent chips keep the plain dim disabled. Applied per-chip in _bind_skills.

2. Enlarged the small mono/body/prose roles for readability on sub-1920 laptops
   (the 1920 design canvas scales DOWN on a smaller screen, so 11-13px text got
   tiny): DM prose 20->22, MONO 13->14, section labels 13->14, card title 19->20,
   card body 13->14, chips 11->12, skill chips 12->14, the two flag pills +1. Big
   serif headings unchanged.

   Two layout consequences the guards caught and I fixed, not silenced:
   - Race cards overflowed the taller blurb -> card min-height 140->180 (the overflow
     guard, which measures against the card the engine does NOT derive from the text,
     went red first; traps.md #17).
   - The 9-chip Human bonus row, wider at the bigger font, blew the whole sheet past
     the viewport -> BonusRow HBoxContainer -> HFlowContainer so it wraps instead of
     forcing the sheet wide. Nothing depends on the row's type (tests check child
     count + visibility only).

319 client tests green (theme drift guard + overflow guard included); screenshot-
verified at 1280x720: granted chip owned, no clipping, bonus row wraps cleanly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:33:08 -05:00
8f3aa75fa9 chore(warnings): silence the four pre-existing script warnings the creation screen surfaced
Not errors and not regressions — Godot printed them on every script reload
and they predate M4-b. Cleared so a real warning is not lost in the noise:

- Redundant `const ContentDB`/`const NewGame` preloads that shadowed their
  own `class_name` globals (character_creation.gd, creation_draft.gd) — the
  class is already global, so the const bought nothing.
- Two `log` locals shadowing the built-in `log()` — renamed to `out`
  (canon_log.from_dict) and `canon` (NewGame.construct); the "log" dict KEY
  is the public contract and is unchanged.
- currency.format's copper->denomination division is integer BY DESIGN (the
  remainder is carried, not lost) — annotated `@warning_ignore` to say so.

319 client tests green; headless parse reports none of the four sites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 20:09:29 -05:00
63cbc8c16e fix(creation-screen): the white text, the badge on top of the roll, and a window too big for the laptop
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>
2026-07-14 19:56:09 -05:00
7471d2e37f docs(traps): the drift guard took three fixes, and the third one whitelisted the gap
Trap 12 told only the first act. The full story is the lesson: ThemeKeys.ALL excludes
the font roles by its own docstring, and the builder styles some base types directly
(RichTextLabel — which renders every line of DM prose on the creation screen). The
meta-guard written specifically to prevent a third miss contained a `continue` that
exempted exactly the gap it existed to catch.

A guard-of-a-guard with an exemption in it is not a guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 16:48:27 -05:00
6291f21a00 fix(theme-guard): close the drift guard's third miss — the meta-guard's own continue whitelisted the gap
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>
2026-07-13 16:47:24 -05:00
34bf064904 fix(creation-screen): the bonus-skill hole, the theme guard that could not fail, and the strings the player was reading
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>
2026-07-13 16:25:12 -05:00
31759ef848 docs(roadmap): M4-b lands; the Title screen was already built
The creation screen is done — 302 client tests (up from the 250 baseline),
content build green. Also flips the M3 Title screen from planned to done: it
shipped in a77bf03/f071392 and the roadmap never caught up, which would have
misled M4-c into thinking it must build one.

Adds six new trap species to docs/traps.md from this milestone's review
passes: substring-collision assertions, a BBCode wrapper masking an empty
ContentDB, a test asserting the script's own loop bound, a trivially-true
gating fixture, assertions already true before the action under test ran, an
"iterates everything" guard that checked three hardcoded pairs, a node-type
sweep that missed RichTextLabel, tests that called handlers instead of
pressing nodes, GUT silently skipping an unparseable class_name test file at
the old count, and a test that can hang instead of fail. Also documents the
new client-docs convention: clickable cards are Buttons with mouse_filter = 2
on every child, and pressed on a toggle_mode Button is the chosen state.

Human F6 confirmation of the rendered screen against the mock is still
outstanding and is not claimed here.
2026-07-13 16:00:17 -05:00
4494856a44 test(creation-screen): close the §7 sweep's own emptiness guard, and the M3-a viewport-fit guard
The Luck sweep's non-empty-label assertions could not fail: _bind_bay and
_bind_callings always wrap prose in BBCode italics ("[i]%s[/i]"), and
_origin_prose() always emits the literal connective " Now you carry a %s's
work — " even when every authored fragment/blurb is blank. So
"[i][/i]".strip_edges().is_empty() reads false regardless of what ContentDB
served — the 28-combination sweep was proving the wrapper is non-empty, not
that §7's own guard against a blank ContentDB actually holds. Replaced the
emptiness checks with assertions that the DM panel and detail panel render
the WORLD's own fragment/blurb strings, and that those strings are
themselves non-blank, for every race x calling combination.

The M3-a collapse guard (`assert_gt(s.size.x, 0.0, ...)`) also could not
fail: the .tscn root is authored at exactly 1920x1080, matching the
project's viewport size, so the assertion passed on the authored offsets
alone with _fit_to_viewport() and its size_changed connection deleted
entirely. Replaced it with an actual resize proof: setting
Window.content_scale_size (the property this project's canvas_items stretch
mode actually keys Control.get_viewport_rect() off of — get_viewport().size
does not move it headless) to a non-1920x1080 value and asserting the root
followed, then restoring it.

Also covers three previously-unasserted binder behaviours: a race-granted
skill chip going inert (and a chip going inert once the pool's picks are
spent), the bonus-skill row's visibility gate per race, the ability card's
primary-stat Tag/theme_type_variation, and _on_enter_pressed's early-out on
an illegal draft.

Every new/changed guard was proven red against the bug it names (blanked a
calling's blurb; hardcoded a placeholder origin string; deleted
_fit_to_viewport's wiring; dropped the chip.disabled line; forced the bonus
row visible) and reverted. Suite: 298 -> 302, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:53:47 -05:00
c5f127e0f1 test(creation-screen): close 3 vacuous guards — point-buy display, signal wiring, Luck sweep
Adversarial re-review of 6589d7c found three assertions in the same species as
the seven it fixed: green against the bug they name.

- test_the_displayed_scores_are_the_draft_s_scores spent zero points, so
  final() == rolled() and a Box/Value bound to `rolled[stat]` instead of
  `final[stat]` would pass unnoticed. Now presses the Plus node to force a
  divergence, and checks the Box/Rolled sub-label too (previously unchecked).
- No test ever emitted a node's `pressed` signal — every interaction went
  through the _on_*_pressed handler directly, so _wire() itself, and every
  card-to-index binding, had zero coverage. Minus had no coverage of any
  kind. Helper and interaction tests now press the authored nodes; added
  dedicated press tests for race/calling/pool/bonus/minus, plus the
  Plus/Minus disabled-boundary bindings.
- The Luck sweep only ever checked the default binding (human + sellsword),
  covering 1 of the 28 race x calling combinations whose authored prose
  (fragment/blurb) actually renders on screen. Now sweeps all 28.

Plus three cheap adds: a degenerate-origin (§13) guard, orphan-authored-card
counts for the race/calling/bonus rows, and a non-empty assertion on the
origin-card RichTextLabel.

Each fix verified by deliberately reintroducing its named bug, confirming the
guard test goes red, then reverting — see
.superpowers/sdd/task-6-report-fix2.md. 291 -> 298 tests, all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 15:38:31 -05:00
6589d7c4ed fix(creation-screen): the guards did not guard
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>
2026-07-13 15:19:10 -05:00
2227d703f6 feat(creation): the character creation screen
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).
2026-07-13 15:03:24 -05:00
fe8e24cf39 fix(tests): make theme drift guard iterate ThemeKeys.ALL, not 3 hardcoded pairs
test_committed_tres_matches_builder claimed to catch a builder/artifact
drift in game_theme.tres, but its checks array hardcoded exactly three
variation/state pairs (PrimaryCTA, ParchmentCard, DarkPanel) checking only
bg_color. Every other variation — including all five added for the
creation screen (SelectCard, SkillChip, ChosenTag, PrimaryTag,
PrimaryCard) — could drift from the builder silently, and this test would
keep passing on stale data. Textbook docs/traps.md failure: a test that
cannot fail against the bug it names.

Rewrote it to derive checks from ThemeKeys.ALL: for every variation,
every stylebox state the builder actually set is compared (bg_color,
border_color, border widths, corner radii for StyleBoxFlat), and every
font colour role the builder actually set is compared too. Failures name
the variation and the drifted property.

Verified by deliberately drifting SkillChip's pressed stylebox in
build_game_theme.gd without regenerating the .tres: the new test failed
and named SkillChip/pressed/bg_color+border_color; the old three-pair
version would not have caught it (SkillChip was never in its list).
Reverted the drift; suite is green at 279 again. No pre-existing drift
found in the committed artifact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:57:28 -05:00
0bbf94289a feat(theme): the creation screen's five variations
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.
2026-07-13 14:48:17 -05:00
1621a38b29 fix(creation-draft): delete the duplicate SPEND_POOL const
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>
2026-07-13 14:44:50 -05:00
866158cc0d feat(creation): CreationDraft — the screen's brain, with no screen
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.
2026-07-13 14:38:18 -05:00
1c4d19492d fix(test): skill-count assertion collided with hit-die digit for Reaver
test_calling_detail_reads_the_table_rather_than_repeating_it checked for
str(Callings.skill_count(id)) — bare digit "2" — which appeared in d12 for
the Reaver (hit_die=12, skill_count=2). Hardcoding "picks 3 skills" in the
card would pass the test, satisfied by the "2" in "d12".

Now anchors to "picks %d skills" format string, so the guard catches when
the card's hardcoded count diverges from the table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:34:12 -05:00
537b0e1d18 feat(creation): CreationCopy — the card reads the table
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>
2026-07-13 14:28:30 -05:00
057b83679b fix(content): bonesetter fragment restated the blurb's central non-claim
The fragment's closing clause — "you have stopped claiming to know whether
it is your hands or the Warden that does the saving" — restated the thesis
of the blurb the player is reading at the same moment, inches above it in
the calling detail panel. The blurb's whole argument already IS the
unresolved-Warden question: it states it outright ("distant and does not
explain himself") and dramatises it with its doubled "Often enough." The
fragment took that artful implicit doubt, said it flat, and put it in the
emphatic final position of the composed DM line. Different words, same beat.

It also collided lexically with the beastfolk race fragment, which the
composition can place directly before it: "You stopped taking that
personally" / "you have stopped claiming".

Keep the front clause — "you are the one they wake at the third hour" is the
best image in the set and gives the player a position in a village that
nothing else on the screen supplies. Rewrite the back clause to say the next
thing instead of the same thing: what the failed nights cost, and that the
job does not release you for failing. The fragment no longer mentions the
Warden at all, so it cannot re-argue him; it now makes a claim about the
player's life rather than about a god.

Also avoids "carry", which the composition template itself supplies ("Now
you carry a Bonesetter's work"), so the rendered sentence no longer risks
repeating its own verb.

Client suite 255/255. content_build --check exit 0.
2026-07-13 14:18:46 -05:00
c6b8a651aa fix(creation-content): four fragments were reading the blurb back to the player
The creation screen shows a race's blurb on its card and the chosen calling's
blurb in the detail panel directly above the DM panel. Blurb and fragment are on
screen at the same moment, inches apart — so a fragment that paraphrases its own
blurb makes the DM sound like it is reciting the card. Four did:

- bonesetter lifted two clauses verbatim ("you set the bone, you say the words")
- reaver re-ran the blurb's door image with the same sentence shape
- hedge_mage re-opened on "what you know" and re-landed the dead teacher
- beastfolk repeated both the claws and the being-judged beat

Each now says the NEXT thing instead: the hour they wake you and which of the
two trades you will not claim credit for; the employer who arranges to be
elsewhere; where you went to fail safely and what the book's gaps cost your
skin; the town that takes your coin and still wants you outside the walls.

beastfolk's rewrite also breaks the race-set monotony — it no longer opens "You
were born" (now unique to human) and drops the shared "X, and you have…" spine,
so clicking the four cards in a row stops reading as one voice.

The parity test guarded presence but could not fail on the three properties that
actually make all 28 race×calling concatenations grammatical. It now asserts
race fragments start uppercase and end with '.', and calling fragments start
lowercase and end with '.' — verified by breaking both and watching all four new
assertions fire.

255 client tests green; content build --check green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:11:16 -05:00
e4db022c4b content(creation): the eleven origin fragments
race.fragment + calling.fragment compose the DM origin panel on the creation
screen. Authored prose, not stubs — this is the first sentence the player reads.
Parity-tested: a missing or empty fragment fails the suite.
2026-07-13 14:02:47 -05:00
17ef142689 feat(newgame): one public, seeded roll the screen can show
roll_attributes(seed) is now the single implementation of the die — construct
calls it, and the creation screen (M4-b) will call it to DISPLAY the roll. So
the screen shows real numbers while still only ever handing over a seed (§2).
validate() goes public so the screen can gate its CTA on the real rules.

Neither takes a default parameter (traps.md #2).
2026-07-13 13:54:02 -05:00
7a74a8de94 docs(plan): M4-b — the creation screen, seven tasks
TDD task-by-task: NewGame's public seeded roll -> the eleven fragments ->
CreationCopy -> CreationDraft -> the theme -> the scene -> the docs.

Both of traps.md's bugs get a named guard rather than a hope: the pipeline test
that asserts what the screen SHOWED is what construct BUILT (and a step that
re-breaks the code to prove the test can fail), and a no-default-parameters rule
on the two new public functions.

Also corrects the spec: PrimaryCTA already has a disabled stylebox.
2026-07-13 07:07:36 -05:00
333d901931 docs(spec): M4-b — the character creation screen
The mock's screen over M4-a's proven model. Five mock/model reconciles
recorded: the skills section the mock never drew, seven callings not five,
nameplate cards with prose on selection, a minus button, no portrait arrows.

No AI call anywhere — the DM's entrance is the shell, one screen later.
The screen shows the roll by calling the same pure roll_attributes(seed)
construct does, so it displays numbers without ever handing one over.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 06:56:31 -05:00
b4b72e472e docs: traps.md — the bugs that passed a green suite
Six bugs this project shipped into a branch, what let each through, and the
guard that now catches it. They share a shape: the test passed, and the test
was wrong.

- a regression test that constructed its own correct object, so reverting the
  bug left the suite green
- GDScript default params turning an arity change into a silent miscompile,
  invisible to a grep for the renamed field (grep cannot see positional args)
- a determinism test comparing two runs that would change together
- a closed move vocabulary that was not bounded, because the model could repeat
  a tag and choose an amount in unary
- a roster written down in four places with only one pair guarded — including a
  shipped authoring skill that would have emitted origins allowing zero callings
- generated content that has a client consumer

Linked from CLAUDE.md §18 and docs/README.md. Also records M4-a on the roadmap
with its live end-to-end proof.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 21:07:39 -05:00
b663703da7 Merge feat/creation-model into dev (M4-a)
The character-creation model: race/calling/skill mechanics as static tables in
code, their blurbs as content, a CharacterSheet that stores only what was rolled
or chosen and derives the rest, and a seeded NewGame.construct.

Creation carries a seed, not a stat block — construct rebuilds the RNG and rolls
the attributes itself, so it never trusts a number handed to it (§2) and the same
seed yields an identical character, hidden Luck included (§10). A golden-vector
test pins the RNG stream. The roll is 3d6 with a hard floor of 8: straight 3d6
puts ~9% on a primary the +3 pool cannot rescue, and a character bad at the one
thing he is FOR is the 'fine' §7 forbids.

Migrates the cross-boundary contract: class_id -> calling_id (the contract should
say what the world says) and a required race_id, so the AI narrates 'a beastfolk
cutpurse' instead of 'a sellsword'.

No UI — the creation screen is M4-b, the title->creation->shell flow is M4-c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:59:36 -05:00
a9357fb787 fix(creation-model): migrate stray class_id/allowed_classes copies
The canon-log schema and NewGame code already migrated to race_id/
calling_id + allowed_callings, but two copies of the roster outside
that schema were missed, and no parity test guards them:

- docs/canon-log.md (the cross-boundary contract doc) still documented
  the OLD player shape and allowed_classes. Anyone building a player
  block from the doc would get a 422.
- .claude/skills/world-building's schema reference still emitted
  allowed_classes with two dead calling ids (assassin, priest). Author
  a new origin with that skill and it fails origin.schema.json
  (additionalProperties: false, allowed_callings required) AND, if that
  ever loosened, silently allows zero callings at runtime.

Also:
- add schema tests rejecting a dead class_id field and an unknown
  calling_id (priest)
- guard NewGame._validate's container types (spend/skills) so a
  malformed JSON round-trip (null spend, string skills) produces a
  front-loaded error list instead of a GDScript runtime crash
- extend the no-mechanics-in-content test to db.races, not just
  db.callings
- rewrite the roadmap's M4 bullet: the two contract migrations landed;
  only the creation screen and title->creation->shell flow remain

client: 250/250. api: 74 passed, 2 skipped. content_build --check: clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:58:49 -05:00
62b8113ffa fix(creation): pin RNG stream, tighten spend/seed validation
Finding 1 (important): test_same_seed_yields_an_identical_character only
compared two construct() runs against each other, so swapping dex/con in
Attributes.IDS still passed the whole suite while every persisted seed
would silently become a different character. Added a golden-vector test
that pins one fixed seed to its exact rolled attributes and hidden Luck,
so reordering Attributes.IDS or moving the Luck roll fails loudly instead
of silently rewriting every saved character. Proved it: swapped IDS,
watched the golden vector fail, restored, confirmed green.

Also: _validate_spend no longer lets a negative value mask the over-pool
check (continue after each error instead of falling through to `total +=
v`); non-integer spend values (float/bool/string) are now rejected instead
of coerced; a missing or non-integer seed is now a validation error instead
of silently defaulting to 0. GameState's header comment no longer
advertises "stats" it no longer holds.

243 -> 248 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:50:06 -05:00
bd49e118d5 feat(newgame): the seeded creation pipeline
construct() takes a seed, not a stat block. It rebuilds the RNG and rolls the
attributes itself, so it never trusts a number handed to it (§2) and the same seed
yields an identical character, hidden Luck included (§10). The whole character is
reproducible from four primitives: seed, race_id, calling_id, spend. The rng
parameter is dropped — the seed is now the only source of randomness.

The roll is 3d6 with a hard floor of 8. Straight 3d6 puts ~9% on a primary the +3
pool cannot rescue, and a character bad at the one thing he is FOR is not grit —
it is the fine §7 forbids, paid for twenty hours.

LCK is unspendable, not merely hidden: spending on it is a validation error.
GameState.stats becomes GameState.sheet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:43:51 -05:00
eba706b906 test: assert seed-log fields, not a fresh LogPlayer
The previous regression tests in test_entities.gd only constructed their
own LogPlayer and never touched the seed logs the real call sites build
— reverting main_window_shell.gd:124 to the broken 3-arg call still left
the whole suite green. Not a real regression guard.

Now assert against node._log.player in the shell test and against the
two dev-harness scenes' seed logs (narrate_harness, npc_harness — both
cheaply testable via instantiate + add_child_autofree, no scaffolding
needed). Proved the new shell assertion fails when the call site is
reverted to the 3-arg shape (226/227, one failure), then passes restored
(227/227). Removed a redundant ctor test from test_entities.gd; kept the
one that exercises the exact broken-shape call.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:39:48 -05:00
5149e817f6 fix(contract): repair 3-arg LogPlayer callers + digest article bug
Three callers (main_window_shell.gd, narrate_harness.gd, npc_harness.gd)
still built LogPlayer with the OLD 3-arg shape (name, class_id,
luck_descriptor) after LogPlayer.new() was migrated to the 4-arg
(name, race_id, calling_id, luck_descriptor) shape. This was invisible
to both grep and the test suite: every ctor param carries a default, so
GDScript compiles a 3-arg call against a 4-arg signature without error —
it just silently mis-binds positionally. "sellsword" landed in the
race_id slot (rejected — not a race), the luck descriptor landed in the
calling_id slot (rejected — not a calling), and luck_descriptor itself
fell back to its default "". Every field in the emitted dict came out
empty, which 422s against the canon-log schema's minLength/enum
constraints. Grepping for class_id can't see this, because there is no
class_id token left anywhere — the bug is purely positional.

Also fixes prompts.py's _article(""), which returned "an" because
Python's "" in "aeiou" is True, producing a doubled-space/wrong-article
digest line when race_id is empty. _describe_player now builds its
descriptor from whichever of race/calling are present and omits the
clause entirely when both are absent.

Adds a schema-parity guard for origin.schema.json's inlined
allowed_callings enum, which duplicated the calling roster with no
runtime check tying it to Callings.IDS. Renames leftover "class"
vocabulary in test names/comments to "calling".

Regression coverage:
- client/tests/unit/test_entities.gd: ctor populates both race_id and
  calling_id; a calling passed into the race_id slot (the exact shape
  of the three broken call sites) yields an all-empty row.
- api/tests/test_prompts.py: empty-race and empty-race-and-calling
  digest rendering, asserting no doubled space and no dangling article.
- client/tests/unit/test_schema_parity.gd: origin.schema.json's
  allowed_callings enum matches Callings.IDS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:34:05 -05:00
a668cf98eb refactor(contract)!: class_id -> calling_id, and the log gains race_id
class_id is the rulebook's word; the world has callings — that was the whole §17
reconcile. The contract the Narrator reads should say what the world says. There
are no saves (M9) and no deployed clients, so this is as cheap as it will ever be
and strictly more expensive every milestone after.

race_id reaches the log because the AI must be able to describe the player:
api/app/prompts.py rendered 'a sellsword' and now renders 'a beastfolk cutpurse'.
It humanizes the id (the model must never read 'hedge_mage') and picks the right
article ('an elf', not 'a elf').

LogPlayer's hardcoded CLASSES const is deleted — the roster is Callings.IDS. A
parity test reads the JSON Schema from the client and asserts its enum equals the
code, so the two sides of the HTTP boundary cannot drift.

The deserter allowed three callings that no longer exist. He now allows all seven;
a thematic gate is a content decision for the Greywater authoring.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:27:56 -05:00
bb5d176262 feat(state): CharacterSheet — store the inputs, derive the rest
Stores only what was rolled or chosen, plus current hp/mp (the only two numbers
that genuinely mutate). Everything else is a function, so no derived number can
drift from its inputs and M5's level curve is a function change, not a migration.

LCK has no row and no accessor here — §7 holds by construction.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:21:45 -05:00
a7e153884f feat(content): calling + race blurbs, loaded by ContentDB
Hand-written like items/ — no build-tool namespace needed. Blurbs are content;
mechanics are code, and a test asserts the blurb files carry no hit dice. The
prose is placeholder; swapping it later touches no code, which is what stops the
content BOM from blocking the engine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:18:19 -05:00
78e18107b2 feat(rules): the race, calling, skill and attribute tables
Mechanics are rules and live in code, beside Luck and Currency. The skill pools
are new — the races/classes spec said only 'the stat-appropriate slice' and never
enumerated them; each pool is larger than its pick count so the choice is real.

The dwarf's +2 vs poison is exposed separately from save() rather than folded
into it: baking a conditional into a flat number is how a sheet starts lying.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:14:44 -05:00
89d4c65a7a docs(plan): M4-a — the creation model, six tasks
TDD, green suite at every boundary. Task 4 is deliberately one cross-cutting
commit: the origin's key and the log's player field are read on both sides of
the HTTP boundary, so renaming them in separate tasks would leave a suite red
at the seam.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 20:08:21 -05:00
9de8ee40b3 docs(spec): M4-a — the character creation model
The rules a character is made of, with no screen attached. Races, callings and
skills become static tables in code; blurbs stay content (hand-written, like
items/), so the content BOM stops blocking the engine. The sheet stores only
what was rolled or chosen and derives the rest — one source of truth, and the
M5 level curve becomes a function change rather than a migration.

Creation is seeded. The creation Dictionary carries a seed, not a stat block:
construct reconstructs the RNG and rolls the attributes itself, so the same
seed yields an identical character and the UI cannot inject a number. §10's
seeding argument, applied where the races spec says it first matters.

3d6 with a hard floor of 8. Straight 3d6 puts a ~9% chance on a primary the +3
pool cannot rescue — a Hedge-Mage with a -1 MAG modifier is not grit, it is the
"fine" §7 forbids, paid for twenty hours.

Lands the two migrations the races spec said to do at M4 and not silently:
class_id -> calling_id (the contract should say what the world says) and a
required race_id, so api/app/prompts.py can render "a beastfolk cutpurse"
instead of "a sellsword".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 19:48:46 -05:00
198ea27d1a docs(canon): the Barbarian name no longer 'stays open'
Follow-on to bb80979 — the cosmology spec still asserted the STR-axis name was
outstanding. It is Reaver.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QYa9u7Kdxv5gX4AnwWexy8
2026-07-12 19:34:42 -05:00