F1 Plan Compliance Audit — APPROVE Primitives [15/15] | Tasks [17/17 top-level] | ADRs [7/7] F2 Code Quality Review — APPROVE Build [PASS] | Lint [PASS] | Tests [1386 pass] | No 'as any' / '@ts-ignore' in non-test source | Registry-dispatch pattern throughout (no hardcoded kind switches) F3 Manual QA — APPROVE e2e [79/79] including 18/18 custom-modifiers.spec.ts scenarios (plan called for 15, shipped 18). All former fixmes passing. F4 Scope Fidelity — APPROVE Recursion cap [3, enforced by MAX_RECURSION_DEPTH in validate.ts] Primitive count cap [50, enforced by MAX_PRIMITIVE_COUNT in validate.ts + server Zod .max(50)] Per-room cap [10, enforced by CUSTOM_MODIFIER_ROOM_CAP in broadcast.ts] Per-engine custom registry [CustomModifierRegistry owned by ChessEngine, never global — cross-room leakage structurally impossible] T4 smuggling [CLEAN — scripted type is rejected in both validate.test.ts and schema.test.ts; no runtime scripted descriptor shipped] T3 boulder complete.
27 KiB
Modifier Profiles — T3 User-Authored Custom Modifiers (DSL)
TL;DR
Quick Summary: Users can author their own modifier categories at runtime via a constrained config DSL. A custom modifier is a named, versioned, data-only descriptor composed from atomic effect primitives ("add N to attribute X", "add direction Y", "reduce damage by N%"). No code execution — purely structured data. Custom modifiers register into a per-game registry (alongside built-ins), persist in a separate library, and travel through the network via
ModifierProfileSchemawith full validation. Forward-designed for a T4 scripted-modifier extension.Deliverables:
- Effect primitives catalog + registry for user-composable atoms
CustomModifierDescriptortype — data-only, validated, sandboxedCustomModifierEditor.tsx— visual composer (no code typing)- Per-room custom modifier registration via WS protocol
- Server-side validation of custom descriptors (anti-DoS, legality)
- Modifier Profile editor: custom modifiers appear in kind dropdown alongside built-ins
- Per-user library for custom descriptors + sharing
- Cross-piece aura effects (built on the same primitive model)
- Multi-profile stacking (ordered composition)
- Full e2e vertical slice
- T4 design note: how scripted modifiers would plug in
Estimated Effort: Large (25 tasks, ~3-4 execution waves like T1) Parallel Execution: YES — 5 waves Critical Path: T1 (primitives ADR) → T3 (primitive registry) → T5 (custom descriptor type) → T13 (engine apply) → T19 (e2e)
Context
Original Request
"Start DSL, design for script upgrade" (T3 approach) "T2 first, then T3 (Recommended)" (execution order)
Pre-requisites
- T1 (shipped): built-in modifier descriptors + registry + editor + server integration
- T2 (must ship first): turn-boundary queue, two-player consent, undo/redo, copy/paste, conflict resolution
T3 Rationale
Extend the T1/T2 foundation so users can define new modifier CATEGORIES (not just configure existing ones).
Example user-authored modifier: "Shield — piece has 3 shield charges. Each incoming damage spends one charge instead of reducing HP. No charges → damage falls through." Users compose this from primitives:
consume-attribute-on-damageprimitive with attr="ShieldCharges", consume=1, absorb=trueseed-attributeprimitive with attr="ShieldCharges", value=3
Architecture (new ADRs)
T3-ADR-1: DSL is structured data, not code Custom modifiers are composed from a fixed catalog of ~15 effect primitives. Each primitive is a TypeScript function (shipped in the engine) that takes parameters. Users compose primitives in the UI; the resulting JSON object IS the modifier. No eval, no sandbox, no script parsing — structured validation only.
Rejected alternatives:
- Sandboxed script runtime (QuickJS, etc.) — defers to T4. Security surface too large for T3.
- AST-based mini-language — same complexity as sandboxed script.
- String template interpolation — too limited.
T3-ADR-2: Effect Primitive catalog (T3 v1)
Primitives are atomic, composable, pure. Full catalog:
| Primitive | Parameters | Effect |
|---|---|---|
seed-attribute |
attr, value | Seeds a fact on the piece at profile apply time |
add-to-attribute |
attr, delta | Adds delta to existing attr value (additive stacking) |
multiply-attribute |
attr, factor | Multiplies existing attr value |
add-direction |
directions[] | Adds to DirectionAdditions |
set-capture-flag |
flag | ORs into CaptureFlags |
absorb-damage-with-attribute |
attr, rate | Each damage point consumes rate of attr instead of HP |
reflect-damage |
percentage | Damage sends back to attacker at percentage |
block-move-type |
moveType (capture / step / slide) | Filter out moves matching criteria |
add-aura |
radius, targetAttr, delta | For each piece within radius, add delta to attr |
on-turn-start |
primitive[] | Runs contained primitives at turn start |
on-capture |
primitive[] | Runs contained primitives when this piece captures |
on-damaged |
primitive[] | Runs contained primitives when this piece takes damage |
conditional |
condition, then-primitive[], else-primitive[] | Condition-branch (e.g., "if HP < 2, do X") |
modify-movement-range |
delta | RangeBonus integration |
override-promotion |
target | PromotionOverride integration |
Users compose these in a visual editor. Each primitive has a known shape, validation, UI form, and engine integration. Adding a new primitive = one file (same pattern as descriptors in T1).
T3-ADR-3: Custom modifier authoring scope
- Per-room: custom modifiers are registered on a room-by-room basis. Registration = send full descriptor via WS at room.create or via
custom-modifier.registermessage. - Per-user library: users save their custom descriptors locally (like profiles). Separate library key
houserules:custom-modifiers:v1. - Sharing: custom descriptors travel with profiles that reference them. A profile using a custom modifier "shield-v1" embeds the full
shield-v1descriptor. - Versioning: custom descriptors have a
version: 1field. v2+ is a new modifier id. - Validation: server-side validator checks primitive catalog membership, parameter bounds, recursion depth ≤ 3 (for on-turn-start / conditional nesting), total primitive count ≤ 50 per descriptor.
T3-ADR-4: Registry extension — per-engine not per-process
Built-in descriptors use module-level MODIFIER_REGISTRY. Custom descriptors use a per-engine customModifiers: Map<string, CustomModifierDescriptor>. MODIFIER_REGISTRY.get(id) transparently consults per-engine custom registry when global misses. This prevents user modifiers from leaking across rooms.
T3-ADR-5: T4 forward-design (scripted modifiers)
Future scripted modifiers would plug in as a new ModifierDescriptor type:
interface ScriptedModifierDescriptor {
type: "scripted";
id: string;
script: string; // QuickJS or similar
permissions: Permission[];
// ... other fields
}
The engine's integration point (same apply signature) doesn't know the difference. T3's validator gets a validateCustomDescriptor branch-point that's currently type: "data" only; T4 adds type: "scripted" with separate validation + sandboxed execution.
T3-ADR-6: Multi-profile stacking (bundled with T3 since primitives enable it)
Profiles can be stacked. Engine maintains activeProfiles: readonly ModifierProfile[] instead of a single profile. Stacking rules per modifier kind (from ADR-4 of T1) apply across profiles. Conflict resolution: explicit priority order set by user. Default: profiles applied in registration order.
T3-ADR-7: Aura effects (primitive add-aura)
Auras are effect primitives that apply to OTHER pieces within a radius. At profile-apply time, auras create derived facts on affected pieces. Derived facts are re-computed on every move (affected pieces may change). Implementation: effectivePieceAttrs engine integration for aura-derived attrs.
Work Objectives
Core Objective
Ship user-authored custom modifier descriptors composed from an effect primitive catalog. Design the whole system so T4's scripted modifiers can plug in without redesigning.
Concrete Deliverables
Primitive catalog (packages/chess/src/modifiers/primitives/):
types.ts—EffectPrimitive,PrimitiveKind, primitive-specific parameter typesregistry.ts—PRIMITIVE_REGISTRY- Individual primitive files (15 files):
seed-attribute.ts,add-to-attribute.ts,multiply-attribute.tsadd-direction.ts,set-capture-flag.tsabsorb-damage-with-attribute.ts,reflect-damage.tsblock-move-type.ts,modify-movement-range.ts,override-promotion.tsadd-aura.tson-turn-start.ts,on-capture.ts,on-damaged.ts,conditional.ts
index.ts— barrel + side-effect registrationvalidate.ts— descriptor-level validator (recursion depth, primitive count, parameter validation per primitive)
Custom modifier descriptor (packages/chess/src/modifiers/custom/):
types.ts—CustomModifierDescriptor,CustomModifierIdapply.ts— executes primitive list at profile apply time (replaces built-inapplyfor custom modifiers)library.ts— persistence athouserules:custom-modifiers:v1schema.ts— Zod schema for full descriptor serialization
Registry extension (packages/chess/src/modifiers/registry.ts):
- Extend
ModifierRegistryClasswithcustomDescriptors: Map<string, CustomModifierDescriptor> .registerCustom(engine, descriptor)and.getCustom(engine, id)methodsget(id)falls back to customDescriptors if not in built-ins
Aura engine support (packages/chess/src/modifiers/auras.ts):
computeAuraFacts(session)— runs all active auras, computes derived attrs, updates session- Hooks: invoked after every move via pseudo-preset
Multi-profile stacking (packages/chess/src/modifiers/apply.ts + reconcile.ts):
applyProfilesToSession(session, profiles: readonly ModifierProfile[], layout)— stack in orderreconcileProfilesSwap(session, oldProfiles, newProfiles, layout)
Server protocol (packages/server/src/protocol.ts):
- New message:
custom-modifier.register— attach custom descriptor to room - Extend
ModifierProfileto includecustomModifiers: readonly CustomModifierDescriptor[](embedded, not by-ref) - Validator:
validateCustomDescriptor(descriptor)runs before allowing registration
UI (packages/chess/src/ui/):
CustomModifierEditor.tsx— visual primitive composerPrimitivePalettePanel.tsx— catalog of 15 primitives, drag/drop or click-to-addPrimitiveInspectorPanel.tsx— parameter editor for selected primitiveCustomModifierLibrary.tsx— library drawer for saved custom modifiers- Extend
PerTypePanel.tsx+PerInstancePanel.tsx— "kind" dropdown shows custom modifiers alongside built-ins
Profile stacking UI (packages/chess/src/ui/Lobby.tsx):
- Allow selecting multiple profiles (stacked, ordered)
- Reorder via drag/drop
- Show stacked effect preview
Docs:
- Update
docs/adr/modifier-profiles.md— add T3-ADR-1 through T3-ADR-7 sections - New
docs/user/custom-modifiers.md— user guide for authoring custom modifiers - New
docs/adr/T4-scripted-modifiers-design.md— forward-looking design doc
E2E (packages/chess/e2e/):
- New
custom-modifiers.spec.ts— ~15 scenarios
Definition of Done
bun run checkgreen- All 15 primitives registered and individually tested
- A user can create a "Shield" custom modifier in the UI using
seed-attribute+absorb-damage-with-attributeprimitives - Custom modifier saves to library, reloads after page refresh, applies to a game
- Multi-profile stacking: 2 profiles stacked → HP bonuses add, direction additions union
- Aura: a piece with
add-auraradius=2 targetAttr=HpBonus delta=+1 gives +1 HP to all pieces within 2 squares - Server rejects malformed custom descriptors (recursion too deep, unknown primitive, parameter out of range)
- Playwright e2e: 15 new scenarios pass in
custom-modifiers.spec.ts - Existing 26 T1+T2 e2e tests still pass (no regression)
- ADR + user docs + T4 design note published
Must Have
- Full DSL with 15 T3 primitives covering the 6 built-in modifier behaviors + new capabilities (auras, conditionals, event hooks)
- Custom modifiers compose in the editor alongside built-ins
- Per-engine custom registry (no cross-room leakage)
- Multi-profile stacking with explicit priority
- Aura effects via
add-auraprimitive - Server-side validation of custom descriptors (anti-DoS)
- T4 forward-design document
Must NOT Have (Guardrails)
- ❌ Scripted modifiers (T4 — only the forward-design note lives here)
- ❌ Sandboxed script runtime in T3
- ❌ User-defined primitives (primitives are engine-shipped)
- ❌ Cross-room custom modifier sharing (explicit re-registration per room)
- ❌ Breaking changes to T1/T2 built-in modifiers
- ❌ Breaking WS protocol changes (custom-modifier messages are additive)
- ❌ Recursion depth > 3 in primitive nesting (DoS guard)
- ❌ More than 50 primitives per custom descriptor
- ❌ More than 10 custom modifiers per room (DoS guard)
Must NOT Have (AI Slop Patterns)
- ❌
as any,@ts-ignore, or bypassing strict TS - ❌ Generic names (
data,result,item,temp) - ❌ Empty catch blocks
- ❌
console.login production code - ❌ Switch statements on primitive kinds (use registry dispatch)
Verification Strategy
Test Decision
- Infrastructure: vitest + Playwright, same as T1/T2
- Approach: TDD per primitive, Playwright for UX
QA Policy
Every task MUST include agent-executed QA scenarios. Evidence saved to .sisyphus/evidence/modifier-profiles-t3/task-{N}-{slug}.{ext}.
Execution Strategy
Parallel Execution Waves
Wave 1 (Foundation — PARALLEL):
├── T1: T3-ADR documentation
├── T2: Primitive types + registry class
└── T3: Custom modifier descriptor types
Wave 2 (Primitive implementations — HIGHLY PARALLEL, 15 in parallel if caps allow):
├── T4-T8: 5 state primitives (seed-attribute, add-to-attribute, multiply-attribute, add-direction, set-capture-flag)
├── T9-T11: 3 damage primitives (absorb-damage-with-attribute, reflect-damage, modify-movement-range)
├── T12-T14: 3 control primitives (block-move-type, override-promotion, add-aura)
└── T15-T18: 4 event primitives (on-turn-start, on-capture, on-damaged, conditional)
Wave 3 (Integration — PARALLEL):
├── T19: Custom descriptor validator (depth cap, parameter check)
├── T20: Custom descriptor Zod schema
├── T21: Custom descriptor library persistence
├── T22: Engine integration — applyCustomDescriptor
└── T23: Multi-profile stacking in apply/reconcile
Wave 4 (Server + UI — PARALLEL):
├── T24: Server custom-modifier.register handler + validation
├── T25: CustomModifierEditor UI (primitive composer)
├── T26: Extend PerTypePanel/PerInstancePanel for custom kinds
├── T27: Multi-profile picker in Lobby
└── T28: Aura engine integration (computeAuraFacts hook)
Wave 5 (E2E + Docs — PARALLEL):
├── T29: Playwright e2e suite (15 scenarios)
├── T30: ADR updates
├── T31: User docs (docs/user/custom-modifiers.md)
└── T32: T4 forward-design document
Wave FINAL (4 parallel reviewers):
├── F1: Plan compliance audit
├── F2: Code quality review
├── F3: Manual QA
└── F4: Scope fidelity check
Dependency Matrix (Abbreviated)
| Group | Depends On | Blocks |
|---|---|---|
| T1 (ADR) | — | ALL |
| T2 (primitives registry) | T1 | T4-T18 |
| T3 (custom desc types) | T1 | T19-T28 |
| T4-T18 (primitives) | T2 | T19, T22, T25 |
| T19 (validator) | T2, T4-T18 | T24 |
| T20 (schema) | T3 | T24 |
| T21 (library) | T3, T20 | T27 |
| T22 (engine apply) | T4-T18 | T23, T28 |
| T23 (stacking) | T22 | T27, T29 |
| T24 (server) | T19, T20 | T29 |
| T25 (editor) | T4-T18 | T26, T29 |
| T26 (panel ext) | T25 | T29 |
| T27 (lobby stack) | T21, T23 | T29 |
| T28 (aura hook) | T22 | T29 |
| T29 (e2e) | T23-T28 | F1-F4 |
| T30-T32 (docs) | T1, T29 | F1 |
TODOs
(Abbreviated — each task follows the same 7-section format as T1/T2. Full details TBD when executing.)
-
1. T3-ADR documentation
What to do: Append T3-ADR-1 through T3-ADR-7 to
docs/adr/modifier-profiles.md.Recommended Agent Profile:
writingParallelization: Wave 1 (solo). Blocks: ALL. Blocked By: None.
Commit:
docs(adr): T3 custom modifier DSL architecture decisions -
2. Primitive types + registry class
What to do: Create
packages/chess/src/modifiers/primitives/types.ts(EffectPrimitive, PrimitiveKind, parameter type families). Createprimitives/registry.tswithPRIMITIVE_REGISTRYsingleton.Recommended Agent Profile:
deepParallelization: Wave 1. Blocks: T4-T18. Blocked By: T1.
Commit:
feat(engine): primitive types and registry -
3. Custom modifier descriptor types
What to do: Create
packages/chess/src/modifiers/custom/types.ts—CustomModifierDescriptorshape with { id, name, description, version, primitives[], targetAttrs[] }.Recommended Agent Profile:
unspecified-lowParallelization: Wave 1. Blocks: T19-T28. Blocked By: T1.
Commit:
feat(engine): custom modifier descriptor types -
4-18. 15 primitive implementations (one per primitive)
Pattern (one task per primitive):
- Create
packages/chess/src/modifiers/primitives/{kind}.ts:- Export descriptor implementing
EffectPrimitive<Params> - Zod schema for params
- Apply function (session mutation or engine hook registration)
- Side-effect register in
primitives/index.ts
- Export descriptor implementing
- Create
.test.tswith 3+ scenarios
Primitives to implement:
- T4: seed-attribute
- T5: add-to-attribute
- T6: multiply-attribute
- T7: add-direction
- T8: set-capture-flag
- T9: absorb-damage-with-attribute
- T10: reflect-damage
- T11: modify-movement-range
- T12: block-move-type
- T13: override-promotion
- T14: add-aura
- T15: on-turn-start
- T16: on-capture
- T17: on-damaged
- T18: conditional
Recommended Agent Profile:
unspecified-high(all)Parallelization: Wave 2 (PARALLEL). Blocks: T19, T22, T25. Blocked By: T2.
Commits:
feat(engine): {kind} effect primitive(×15) - Create
-
19. Custom descriptor validator
What to do:
packages/chess/src/modifiers/custom/validate.ts— validates a CustomModifierDescriptor:- Every primitive kind is in PRIMITIVE_REGISTRY
- Primitive params satisfy primitive's Zod schema
- Recursion depth ≤ 3 (count nesting of on-turn-start / on-capture / on-damaged / conditional)
- Total primitive count ≤ 50
- No circular references (descriptor referencing itself is banned)
Recommended Agent Profile:
deepParallelization: Wave 3. Blocks: T24. Blocked By: T2, T4-T18.
Commit:
feat(engine): custom modifier descriptor validator -
20. Zod schema for custom descriptor serialization
Recommended Agent Profile:
unspecified-lowParallelization: Wave 3. Blocks: T24. Blocked By: T3.
Commit:
feat(engine): custom modifier Zod schema -
21. Custom modifier library persistence
What to do:
packages/chess/src/modifiers/custom/library.ts— localStorage athouserules:custom-modifiers:v1. Mirror T1 library API.Recommended Agent Profile:
unspecified-lowParallelization: Wave 3. Blocks: T27. Blocked By: T3, T20.
Commit:
feat(engine): custom modifier library persistence -
22. Engine integration — applyCustomDescriptor
What to do:
packages/chess/src/modifiers/custom/apply.ts— when a profile's perType/perInstance entry references a custom kind, resolve the descriptor from per-engine registry, execute its primitive list on the target piece.Recommended Agent Profile:
deepParallelization: Wave 3. Blocks: T23, T28. Blocked By: T4-T18.
Commit:
feat(engine): apply custom modifier descriptors -
23. Multi-profile stacking in apply/reconcile
What to do: Extend
applyProfileToSession→applyProfilesToSession(session, profiles, layout). ExtendreconcileProfileSwap→reconcileProfilesSwap(session, oldProfiles, newProfiles, layout). Stacking per ADR-4 rules apply across multiple profiles.Recommended Agent Profile:
deepParallelization: Wave 3. Blocks: T27, T29. Blocked By: T22.
Commit:
feat(engine): multi-profile stacking -
24. Server custom-modifier.register handler
What to do:
- New WS message
custom-modifier.registerwith full descriptor payload - Server validates via T19's validator
- Registers per-room (per-engine registry)
- Rejects if > 10 custom modifiers per room
- Broadcasts
custom-modifier.registeredto opponent
Recommended Agent Profile:
deepParallelization: Wave 4. Blocks: T29. Blocked By: T19, T20.
Commit:
feat(server): custom-modifier.register WS handler - New WS message
-
25. CustomModifierEditor.tsx — primitive composer UI
What to do: Visual editor with:
- PrimitivePalettePanel — 15 primitives grouped by category
- Primitive tree view — shows nesting for on-turn-start / conditional
- PrimitiveInspectorPanel — parameter form per primitive, driven by primitive's Zod schema
- Add/delete/reorder primitives
- Save/load from library
- Inline validation (run T19 validator live)
Recommended Agent Profile:
visual-engineering, skills: [interface-design]Parallelization: Wave 4. Blocks: T26, T29. Blocked By: T4-T18.
Commit:
feat(ui): custom modifier editor -
26. Extend PerTypePanel/PerInstancePanel for custom kinds
What to do: Kind dropdown iterates
MODIFIER_REGISTRY.list() + customRegistry.list(). Custom kinds use the primitive composer for value input (or show a simplified parameter form).Recommended Agent Profile:
visual-engineering, skills: [interface-design]Parallelization: Wave 4. Blocks: T29. Blocked By: T25.
Commit:
feat(ui): custom modifiers in modifier profile panels -
27. Multi-profile picker in Lobby
What to do: Lobby's profile picker becomes multi-select with ordering. Drag to reorder. Selected profiles stack in UI-visible order.
Recommended Agent Profile:
visual-engineering, skills: [interface-design]Parallelization: Wave 4. Blocks: T29. Blocked By: T21, T23.
Commit:
feat(ui): multi-profile stacking in lobby -
28. Aura engine integration
What to do:
packages/chess/src/modifiers/auras.ts—computeAuraFacts(session)runs after every move via pseudo-preset. Walks all aura-primitive-declared modifiers, finds affected pieces within radius, updates derived facts.Recommended Agent Profile:
deepParallelization: Wave 4. Blocks: T29. Blocked By: T22.
Commit:
feat(engine): aura effect computation -
29. Playwright e2e suite (15 scenarios)
What to do: Create
packages/chess/e2e/custom-modifiers.spec.ts:- Open custom modifier editor from modifier profile editor
- Create "Shield" custom modifier via primitive composer
- Save custom modifier to library
- Reload page → custom modifier still in library
- Use custom modifier in a profile (per-type entry)
- Start solo game with profile using custom modifier — verify behavior
- Multi-profile stacking (2 profiles) — HP bonuses add
- Aura effect — piece within radius gets derived attr
- Aura updates after move (target moves out of radius → fact retracted)
- Server rejects custom modifier with > 50 primitives
- Server rejects custom modifier with > 3 recursion depth
- Custom modifier sharing via multiplayer — both clients see same behavior
- Conditional primitive works (if HP < 2, do X)
- on-turn-start primitive fires
- absorb-damage-with-attribute works (shield absorbs before HP)
Recommended Agent Profile:
unspecified-high, skills: [playwright]Parallelization: Wave 5. Blocks: F1-F4. Blocked By: T22-T28.
Commit:
test(e2e): custom modifier DSL vertical slice -
30. ADR updates — T3 Implementation Retrospective
Recommended Agent Profile:
unspecified-lowParallelization: Wave 5. Blocks: F1. Blocked By: T1, T22-T28.
Commit:
docs(adr): T3 implementation retrospective -
31. User docs: docs/user/custom-modifiers.md
What to do: New user guide:
- What are custom modifiers?
- Opening the editor
- The 15 effect primitives (one subsection each with 1 example)
- Composing primitives (simple to complex)
- Saving & sharing
- Multi-profile stacking
- Aura effects (radius, affected pieces)
- Limitations (50 primitive cap, depth cap, 10 per room)
Recommended Agent Profile:
unspecified-lowCommit:
docs(user): custom modifier DSL user guide -
32. T4 forward-design document
What to do: Create
docs/adr/T4-scripted-modifiers-design.md:- Why T4 is deferred (security complexity)
- Sandbox candidates evaluated (QuickJS, Duktape, custom mini-interpreter)
- Descriptor shape extension (
type: "data" | "scripted") - Permission system sketch
- Validation strategy (static analysis of script before execution)
- How T3 primitives can be migrated (scripted modifier that wraps a primitive sequence)
- Open questions
Recommended Agent Profile:
writingCommit:
docs(adr): T4 scripted modifiers forward-design
Final Verification Wave
-
F1. Plan Compliance Audit —
oracleVerify all 15 primitives, all 32 tasks' deliverables, T3-ADR decisions reflected. No T4 scripted modifiers shipped (only design doc). Output:Primitives [15/15] | Tasks [N/N] | ADRs [7/7] | VERDICT -
F2. Code Quality Review —
unspecified-highbun run check. Scan for slop. Verify registry-dispatch pattern (no hardcoded kind switches). Output:Build [PASS] | Lint [PASS] | Tests [N pass] | VERDICT -
F3. Manual QA —
unspecified-high(+playwright) Execute all 15 new e2e scenarios. Author a Shield custom modifier via UI, play a game with it, verify full behavior end-to-end. Output:Scenarios [N/N] | Integration [pass] | VERDICT -
F4. Scope Fidelity —
deepNo scripted modifiers (only forward-design doc). No cross-room leakage. Recursion cap enforced. Primitive count cap enforced. Output:Tasks [N/N compliant] | T4 smuggling [CLEAN] | VERDICT
Commit Strategy
docs(adr): T3 custom modifier DSL architecture decisionsfeat(engine): primitive types and registryfeat(engine): custom modifier descriptor types4-18.feat(engine): {kind} effect primitive(×15)feat(engine): custom modifier descriptor validatorfeat(engine): custom modifier Zod schemafeat(engine): custom modifier library persistencefeat(engine): apply custom modifier descriptorsfeat(engine): multi-profile stackingfeat(server): custom-modifier.register WS handlerfeat(ui): custom modifier editorfeat(ui): custom modifiers in modifier profile panelsfeat(ui): multi-profile stacking in lobbyfeat(engine): aura effect computationtest(e2e): custom modifier DSL vertical slicedocs(adr): T3 implementation retrospectivedocs(user): custom modifier DSL user guidedocs(adr): T4 scripted modifiers forward-design
Success Criteria
bun run check # all green
bun run test packages/chess/src/modifiers/primitives/ # all 15 primitive tests pass
bun run test packages/chess/src/modifiers/custom/ # apply + validate + library pass
bunx playwright test e2e/custom-modifiers.spec.ts # 15/15 pass
bunx playwright test e2e/modifier-profiles.spec.ts # 26/26 pass (no regression)
Final Checklist
- All 15 primitives registered and individually tested
- Custom modifier editor UI functional
- Server validates + registers custom descriptors per-room
- Multi-profile stacking works
- Auras work (derived facts update on move)
- 15 Playwright e2e + 26 existing pass (41 total)
- ADR + user docs + T4 design note published
bun run checkgreen- F1-F4 all APPROVE
- User explicit "okay"