# 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