fix(thressgame-coverage): F1+F4 remediation (T58 RequestChoiceModal + coin-flip kind)

Final Verification Wave found two real blockers:
1. T58 RequestChoiceModal.tsx was marked complete but did NOT exist on disk.
2. request-choice locked 6-kind enum was shipped as 5 (missing 'coin-flip').

Remediation:
- Build RequestChoiceModal.tsx with role=dialog, aria-modal=true, ESC/backdrop close, kind-specific input UI for all 6 kinds (rps / coin-flip / piece / square / column / row); 4 tests
- Add 'coin-flip' to:
  - request-choice primitive paramsSchema enum
  - PendingChoice.kind union (schema.ts + util/pending-choices.ts)
  - WS protocol ChoiceKindSchema (server/protocol.ts)
  - choice-timeout.firstDefaultForKind (defaults to 'heads')
  - broadcast.isValidChoiceValue (accepts 'heads' | 'tails')
- AutoChoiceResolver: deterministic alternating heads/tails for coin-flip

T68 e2e tests remain .skip()'d pending UI integration (modal-into-GameView wiring + activate-descriptor UI) — a follow-up task. The sentinel test asserts the gap exists so when integration lands, skips lift in the documented order.

Tests: 2740 -> 2744 (+4). bun run check exit 0.
This commit is contained in:
Joey Yakimowich-Payne 2026-04-26 14:09:28 -06:00
commit 88581ff6a9
No known key found for this signature in database
17 changed files with 2248 additions and 10 deletions

View file

@ -55,7 +55,31 @@
"ses_23525bc2dffe2BqHbsMG5X7EHn",
"ses_23526101fffeonGIpO7HY1na2X",
"ses_235254abeffe5rnNrqeb7sDsgd",
"ses_235251f33ffeXIhn18D3PFrX04"
"ses_235251f33ffeXIhn18D3PFrX04",
"ses_235117916ffeVIqFPhwOaX9AFG",
"ses_235115205ffeSz44RxSp2ceFek",
"ses_235111588ffe2CDfEN7cBiY2Tf",
"ses_2351133beffeWP9yUObY2S64ag",
"ses_234efcbd3ffe8BuHY2u90Wz1rK",
"ses_234efdeafffe08yS4JRWZ7S4uD",
"ses_234f006feffefL5q6WEqDYJtCf",
"ses_234eff09effed1uGpw4ugug3Yv",
"ses_234ead2eeffe5Zkywtv8XYTIL5",
"ses_234eab9afffeZM2ThzxvFbS7we",
"ses_234cf3732ffeG6qApsZze3p4CL",
"ses_234ea9ed5ffe3LO8eIohGogxE1",
"ses_234eaeabaffeUxClwG9AhyELI5",
"ses_234cf1fbbffepuEU0T4t2zL7vl",
"ses_234d69497ffeDpUTy4YmOP9Vzq",
"ses_234cf71d4ffeqORkJSGYcrCRNf",
"ses_234cf894dffef5jKrFXEXL5WaH",
"ses_234b28fafffe6oqUjKGisITV54",
"ses_234b26e14ffeuEORqZKKNh2byv",
"ses_234a6fb95ffeM3IHl6Ejoty4rI",
"ses_234a71fb6ffexvyikAILJb5T5V",
"ses_234a6dabaffeuQB1zQgChArKtq",
"ses_234a740d3ffe9EIQvGhSVIprrg",
"ses_2349f31ffffesMHCS2O8YUYzaB"
],
"plan_name": "thressgame-coverage",
"agent": "atlas"

View file

@ -0,0 +1,65 @@
# Architectural Decisions — Visual Modifier Builder
## DSL Extensions
### `PrimitiveApplyContext` extension (T1)
- Add `target?: TargetResolver` (default `'self'` if omitted)
- Add `event?: PrimitiveEvent` (carries trigger-specific metadata)
- TargetResolver = `'self' | 'attacker' | 'defender' | { squares: Square[] } | { relation: 'ally' | 'enemy', filter?: { pieceType?: PieceType } }`
- Centralized resolver: `resolveTargets(ctx, target): readonly EntityId[]` — returns `[ctx.pieceId]` for default 'self'
- Existing 14 primitives: keep using `ctx.pieceId` directly when target is 'self' (default) — byte-identical behavior
### Trigger semantic decisions (locked)
- `on-promotion`: fires AFTER PieceType flip. event = `{promotedFrom: 'pawn', promotedTo: PieceType}`
- `on-check-received`: edge-triggered (transition only); royal pieces only
- `on-check-delivered`: attributes to revealing piece in discovered check
- `on-move`: fires whenever Position WME changes (captures fire BOTH on-capture and on-move)
- `on-moved-onto-square`: discriminated union `{kind:'squares', squares[]} | {kind:'predicate', file?, rank?}`
- `on-captured`: fires BEFORE retraction; nested primitives have access to defender attrs via `ctx.event = {attackerId, defenderId}`
- `on-turn-end`: fires for mover at end of their turn, BEFORE opponent's on-turn-start
## UI Decisions
### Mode toggle (T22)
- localStorage key: `houserules:custom-modifier-editor-mode:v1`
- Default: 'form' (preserves existing behavior on fresh install)
- Toggle in editor header next to Templates/Load/Save
- Visual mode replaces center+right panels (palette stays in left sidebar)
### dnd-kit choice (T18)
- Use `@dnd-kit/core + sortable + utilities` (3 packages)
- `PointerSensor` + `KeyboardSensor` (a11y mandatory)
- `DragOverlay` for smooth nested drag preview
- Each trigger's children list = nested SortableContext
- Depth-4 drop attempt = no-op + toast (use existing Sonner)
### Preview pane (T17)
- 3 tabs: Narrative / JSON / Board
- All 3 views memoized on descriptor identity
- BoardDiagramView shows squares from on-moved-onto-square filters, aura radius, poisoned squares
- Empty descriptor = "No board effect" placeholder
- Plain pre/JSON formatting (no react-syntax-highlighter — bundle bloat)
### `narrate.ts` (T15) — pure module
- Zero session/engine access
- Static kind→narrator function map (no PRIMITIVE_REGISTRY lookups)
- Cycle guard via visited Set
- Length cap 4000 chars with truncation suffix
- Perf budget: <1ms on 50-node descriptor
## Test Strategy
### TDD per primitive (T5–T11)
- RED: write test first, runs but fails
- GREEN: minimal implementation to pass
- One commit per primitive (atomic)
### Pure refactor (T14 — ParamField)
- Snapshot test BEFORE extraction (golden DOM)
- Extract verbatim from CustomModifierEditor.tsx:625-872
- Snapshot test AFTER extraction must match byte-equal
- Existing e2e tests must pass unchanged
### Backward compat (T20)
- Fixture: descriptor using ONLY 15 legacy kinds, includes 2 nested triggers
- 4 round-trip assertions: parse, validate, apply, serialize → byte-equal

File diff suppressed because it is too large Load diff