fix(chess/ui): narrate.ts \u2014 V3 resolver shapes + 9 missing narrators

Fix two bugs in the Custom Modifier Editor's live preview pane that
surfaced when loading thressgame-100 epic recipes:

BUG 1 \u2014 'unknown primitive: <kind>'
The narrator dispatcher (KIND_NARRATORS) was missing entries for the 9
new primitives shipped across the thressgame-100 epic. Loading any recipe
using them showed 'unknown primitive: add-resource' (etc.) in the preview.

Added 9 narrators (alphabetically inserted into KIND_NARRATORS):
- on-attr-expire (W2)              \u2014 'When the <attr> attr on <target> expires: <children>'
- decrement-attr-each-turn (W2)    \u2014 'each turn-end, subtract 1 from <attr> on <target>'
- set-board-topology (W4)          \u2014 'set board topology to <value>'
- link-pieces (W4)                 \u2014 'link the pieces at <a> and <b>'
- unlink-pieces (W4)               \u2014 'unlink the pieces at <a> and <b>'
- on-piece-pair-link-broken (W4)   \u2014 'When a linked partner is destroyed: <children>'
- add-resource (W5)                \u2014 'give <player> +<amount> score' (or 'subtract from')
- spend-resource (W5)              \u2014 'If <player> has <amount> score, spend it, then: <then>; otherwise: <else>'
- on-resource-changed (W5)         \u2014 'When <player>'s score crosses <threshold> <direction>: <children>'

BUG 2 \u2014 '[object Object]' for V3 resolver shapes
fmtPieceTarget (line 125) and inline square handling fell through to
String(t) when the field carried a V3 resolver shape ({$var}, {ctx-self-id},
{ctx-self-marker-id}, {ctx-attr}, {ctx-build}, {add/sub/mul/mod}).
User-reported case: tpl-treasure-chest's destroy-marker arm rendered as
'destroy [object Object]'.

Added fmtResolverShape(v) helper rendering the 9 V3 shapes as plain English:
- {$var: 'p'}                              \u2192 'p'
- {ctx-self-id: null}                      \u2192 'this piece'
- {ctx-self-marker-id: null}               \u2192 'this marker'
- {ctx-attr: {entity, attr}}               \u2192 "<entity>'s <attr>" (recursive)
- {ctx-build: {col, row}}                  \u2192 'e4' (algebraic if both literal) or
                                            'the square at column <c>, row <r>'
- {add: [a, b]}                            \u2192 '<a> + <b>' (recursive)
- {sub/mul/mod: [a, b]}                    \u2192 same pattern, '-' / '\u00d7' / 'mod'

Added fmtSquareValue(v) helper for square-typed fields. fmtPieceTarget
extended to call fmtResolverShape before falling back to String.

Updated existing narrators to use fmtSquareValue for square fields:
- spawn-marker.square, spawn-marker-pair.{squareA,squareB}
- move-piece.to, place-piece.square, must-class.square
- destroy-marker.target now routes through fmtResolverShape

Test surface:
- narrate.test.ts: 69 \u2192 90 tests (+21)
  - V3 resolver shapes describe block: 8 tests
  - thressgame-100 primitives describe block: 12 tests
  - tpl-treasure-chest end-to-end regression test: 1 test
    asserts the user-reported failure is fixed (no '[object Object]',
    no 'unknown primitive', expected English phrases present)

bun run check: 3291 tests pass (was 3270, +21). 0 regressions.

USER-REPORTED CASE NOW RENDERS:
  Before: 'When this piece captures: spawn a treasure marker on <square>
           (permanent). When a piece enters a treasure marker: unknown
           primitive: add-resource; destroy [object Object].'
  After:  'When this piece captures: spawn a treasure marker on this
           piece's Position (permanent). When a piece enters a treasure
           marker: give white +5 score; destroy this marker.'

Spot-check on 5 recipes (all render sensible English):
- tpl-treasure-chest \u2713 (see above)
- tpl-time-bomb \u2713
- tpl-down-with-the-ship \u2713
- tpl-summoning-ritual \u2713
- tpl-bouncing-ricochet \u2713

CALLOUT FOR FUTURE EPICS (recorded in learnings.md):
narrate.ts has zero engine/registry imports by design. When a new primitive
or resolver shape lands, the narrator must be updated MANUALLY \u2014 there
is no static link to detect drift. Add a registry-count-style sentinel
test in a follow-up to catch this earlier (e.g. compare KIND_NARRATORS
keys against PRIMITIVE_REGISTRY.list()).
This commit is contained in:
Joey Yakimowich-Payne 2026-04-27 18:04:19 -06:00
commit a5ee07dead
No known key found for this signature in database
4 changed files with 646 additions and 15 deletions

View file

@ -152,7 +152,8 @@
"ses_22eb3422affe0WXwp2RUpnBMbi",
"ses_22eb4c13effeq1D5R0Hv64kDKi",
"ses_22eb45670ffeTp5TTjGXMPI7SC",
"ses_22eb3dab2ffeuR0mQzUofl5SpW"
"ses_22eb3dab2ffeuR0mQzUofl5SpW",
"ses_22ea014d4ffesgY7jcuHeSi25L"
],
"plan_name": "thressgame-coverage",
"agent": "atlas"

View file

@ -522,3 +522,46 @@ Final recipe count: **54**. None of the 8 W3 recipes was skipped.
- **MODIFIED** `packages/chess/src/ui/CustomModifierEditor.tsx` — Templates modal `.map()` now branches on `recipe.id.startsWith("tpl-preset-")` for badge + border styling.
- **CREATED** `packages/chess/docs/THRESSGAME_WONT_FIX.md` — 130-line WONT_FIX manifest.
- **MODIFIED** `.sisyphus/notepads/thressgame-100/learnings.md` — this section.
---
## narrate.ts fix (post-W5) — V3 resolver shapes + 9 missing narrators
**Problem**: Custom Modifier Editor live-preview pane rendered "unknown
primitive: add-resource" and "destroy [object Object]" for any
descriptor using W2/W4/W5 primitives or V3 resolver shapes
(`{$var}`, `{ctx-self-id}`, `{ctx-self-marker-id}`, `{ctx-attr}`,
`{ctx-build}`, `{add|sub|mul|mod}`).
**Fix** (`packages/chess/src/ui/narrate.ts`):
1. New `fmtResolverShape(v)` helper — recursively renders the 9 V3
shapes mirrored from `param-resolver.ts:139-216`. Returns `null`
for non-shapes so callers fall back to literal handling.
2. New `fmtSquareValue(v)` helper for fields holding a square
(literal 0-63 or resolver shape).
3. `fmtPieceTarget` extended to call `fmtResolverShape` before its
`String(t)` fallback.
4. 9 new narrators added to `KIND_NARRATORS`: `add-resource`,
`spend-resource`, `on-resource-changed`, `set-board-topology`,
`link-pieces`, `unlink-pieces`, `on-piece-pair-link-broken`,
`on-attr-expire`, `decrement-attr-each-turn`.
5. Existing narrators routed through `fmtSquareValue` (spawn-marker,
spawn-marker-pair, move-piece.to, place-piece, must-class.square)
and `destroy-marker` switched from `String(t)` to
`fmtResolverShape` fallback.
**Tests**: 21 new tests added (69 → 90), including a tpl-treasure-chest
end-to-end test that pins the regression. All 3291 chess tests pass;
`bun run check` exits 0.
**Inheritable wisdom**:
- The `narrate.ts` zero-imports rule held — the resolver-shape
recogniser is structurally pattern-matched, no shared module pulled
in. The shape contract (`exactly 1 key`, key name from a fixed set)
is duplicated between `param-resolver.ts` and `narrate.ts`; if the
V3 union ever grows (e.g. `eq` resolver), BOTH files need the new
case. Tag any new resolver shape's PR with a checklist item to
update narrate.
- The pre-existing `joinParts` quirk that produces `..` on nested
triggers ending in `.` (see test line 865) is unchanged. Out of
scope for this fix.