houserules/.sisyphus/notepads/visual-modifier-builder/decisions.md
Joey Yakimowich-Payne 88581ff6a9
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.
2026-04-26 14:09:28 -06:00

3.1 KiB
Raw Permalink Blame History

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