houserules/.sisyphus/plans/modifier-profiles-t2.md

24 KiB

Modifier Profiles — T2 Polish

TL;DR

Quick Summary: UX and robustness polish on top of shipped T1 modifier profiles. Adds copy/paste between pieces, a visual "this piece is modified" indicator on the live board, inline conflict-resolution in the editor, undo/redo in the editor, proper turn-boundary queuing server-side (replaces T1's immediate-apply simplification), two-player consent model for mid-game swaps (replaces host-only), and richer source-chain attribution in the pinned inspection panel.

Deliverables:

  • Editor: copy/paste modifiers, undo/redo stack, inline conflict resolution UI
  • Board: subtle modifier-indicator badge on modified pieces (no hover required)
  • Server: turn-boundary queue (ADR-3 done properly), both-player consent for hot-swap
  • Inspection: enhanced source-chain breakdown in pinned panel
  • Full e2e vertical slice
  • Updated ADR + user docs

Estimated Effort: Medium (15 tasks) Parallel Execution: YES — 4 waves Critical Path: T1 (turn-queue server) → T3 (consent) → T13 (e2e)


Context

Original Request

"can we do t2 and t3? T2 first, then T3 (Recommended)"

T1 Recap (shipped)

T1 (completed) delivered the foundation: 6 built-in modifier descriptors, per-type + per-instance scope, editor UI, library persistence, URL sharing, hover tooltip, pinned panel, server-side validation, immediate-apply hot-swap, host-only authority. Full vertical slice working with 18/18 Playwright tests.

T2 Rationale

Two categories of work:

  1. UX polish: features users will notice immediately — copy/paste, visual indicators, undo/redo, better conflict messages.
  2. Robustness fixes: close the documented simplifications from T1's Implementation Retrospective — turn-boundary queue (not immediate) + two-player consent (not host-only).

Architectural Decisions (inherited from T1 ADRs)

Most T2 work doesn't need new ADR decisions. Three small extensions:

T2-ADR-1: Turn-boundary queue

  • Client sends modifier-profile.update at any time.
  • Server enqueues to room.pendingProfile. Apply runs in the server's onAfterMove hook after next move is validated.
  • If pending profile is invalid when apply fires → NACK to sender, clear pending, no broadcast.
  • Multiple updates before apply: last-write-wins (replace pending).
  • Replaces T1 simplification where swap applied immediately on receipt.

T2-ADR-2: Two-player consent

  • Host sends modifier-profile.propose with candidate profile.
  • Server broadcasts modifier-profile.proposal-pending to opponent with profile contents.
  • Opponent sends modifier-profile.consent with approve/reject.
  • If approve → apply at next turn boundary (T2-ADR-1). If reject → clear proposal, broadcast modifier-profile.rejected.
  • Timeout: 60s → auto-reject.
  • Replaces T1's unilateral host authority.

T2-ADR-3: Undo/redo in editor

  • Editor maintains a snapshot stack of working-profile states.
  • Every meaningful user action (add/delete/edit a modifier) pushes a snapshot.
  • Cmd/Ctrl+Z undoes; Cmd/Ctrl+Shift+Z redoes.
  • Stack capped at 50 snapshots.
  • Cleared on save or cancel.

Work Objectives

Core Objective

Polish the T1 modifier-profile system with features users expect from a mature editor + close the documented T1 simplifications (immediate-apply, host-only) to ship production-grade semantics.

Concrete Deliverables

Editor UX (packages/chess/src/ui/):

  • ModifierProfileEditor.tsx — add undo/redo stack, toolbar with history controls
  • PerTypePanel.tsx — add "Copy" + "Paste" buttons on each modifier row
  • PerInstancePanel.tsx — add copy/paste between pieces ("copy from b1" → "paste to g1")
  • ConflictResolutionPanel.tsx — NEW: shows validation errors inline with suggested fixes + auto-resolve options

Board UX (packages/chess/src/ui/):

  • ModifiedPieceIndicator.tsx — NEW: small badge/glow on modified pieces
  • GameView.tsx + Board.tsx — wire indicator into piece rendering

Server (packages/server/src/):

  • broadcast.ts — handleModifierProfileUpdate now enqueues instead of applying
  • game-session.ts — new applyPendingProfile() method called after applyMove()
  • New handlers: handleModifierProfilePropose, handleModifierProfileConsent
  • rooms.ts — Room.pendingProfile, Room.proposalState, Room.proposalTimeoutHandle
  • protocol.ts — new messages: modifier-profile.propose, modifier-profile.proposal-pending, modifier-profile.consent, modifier-profile.rejected

Inspection (packages/chess/src/ui/):

  • ModifierPinnedPanel.tsx — enhanced source breakdown:
    • Per-instance entries labeled "(from layout square X)"
    • Per-type entries labeled "(applies to all {color} {type}s)"
    • Preset entries labeled "(from {preset name})"
    • Base values labeled "(default)"

Docs:

  • Update docs/adr/modifier-profiles.md — add T2-ADR-1, T2-ADR-2, T2-ADR-3 sections
  • Update docs/user/modifier-profiles.md — document new features

E2E (packages/chess/e2e/):

  • Extend modifier-profiles.spec.ts — 8 new scenarios

Definition of Done

  • bun run check green (typecheck + lint + vitest)
  • Copy/paste between pieces: adding a per-instance modifier to b1, then copying to g1, produces identical entries at both squares
  • Undo/redo: 5 actions then undo 3 times then redo 2 times produces state equal to 4-action state
  • Visual indicator visible on modified pieces without requiring hover
  • Turn-boundary queue: profile update during mid-move doesn't apply until after opponent's move resolves
  • Two-player consent: black must approve before profile change broadcasts
  • Proposal timeout: 60s no-response → auto-reject
  • Inline conflict resolution shows specific error + suggested fix
  • Source chain in pinned panel distinguishes all 4 sources
  • Playwright e2e: 18 existing + 8 new = 26 scenarios all pass
  • ADR + user docs updated

Must Have

  • All 8 UX/polish deliverables working end-to-end
  • Turn-boundary queue replaces immediate-apply (no regression — hot-swap still works)
  • Two-player consent with 60s timeout
  • Editor undo/redo with 50-snapshot cap
  • Shared schema between client/server (no drift)

Must NOT Have (Guardrails)

  • ❌ Custom modifier authoring (T3)
  • ❌ Cross-piece aura effects (T3)
  • ❌ Multi-profile stacking (post-T3)
  • ❌ Editor persistence across sessions (reload = fresh editor, library is separate)
  • ❌ Server-side undo (client-side only — no WS messages for undo state)
  • ❌ Breaking changes to T1 WS messages (all additions are NEW messages; modifier-profile.update becomes an alias for propose + auto-approve when single-player)
  • ❌ Mid-turn swap (turn-boundary gate is enforced)
  • ❌ Client computing effective modifiers differently from server

Must NOT Have (AI Slop Patterns)

  • ❌ as any, @ts-ignore, or bypassing strict TS
  • ❌ Generic names (data, result, item, temp)
  • ❌ Empty catch blocks
  • ❌ console.log in production code
  • ❌ Premature abstraction

Verification Strategy

Test Decision

  • Infrastructure: vitest + Playwright, same as T1.
  • Approach: TDD for server + engine; Playwright scenarios for UX features.

QA Policy

Every task MUST include agent-executed QA scenarios. Evidence saved to .sisyphus/evidence/modifier-profiles-t2/task-{N}-{slug}.{ext}.


Execution Strategy

Parallel Execution Waves

Wave 1 (Server robustness foundation — PARALLEL):
├── T1: T2-ADR documentation (append to existing ADR)
├── T2: Turn-boundary queue server-side
└── T3: Two-player consent protocol

Wave 2 (Editor features — PARALLEL):
├── T4: Undo/redo snapshot stack in editor
├── T5: Copy/paste modifiers between pieces
└── T6: Inline conflict resolution panel

Wave 3 (Board UX + Inspection polish — PARALLEL):
├── T7: Modified-piece indicator on live board
├── T8: Enhanced source-chain in pinned panel
└── T9: Consent UI (proposal notification + approve/reject buttons)

Wave 4 (E2E + Docs — PARALLEL):
├── T10: Playwright e2e additions (8 scenarios)
├── T11: ADR updates
└── T12: User docs updates

Wave FINAL (4 parallel reviewers):
├── F1: Plan compliance audit
├── F2: Code quality review
├── F3: Manual QA
└── F4: Scope fidelity check

Dependency Matrix

Task Depends On Blocks
1 — 2-12
2 1 3, 10, 11
3 2 9, 10, 11
4 1 10
5 1 10
6 1 10
7 1 10
8 1 10
9 3 10
10 2-9 F1-F4
11 1, 2, 3 F1
12 4-9 F1

TODOs

  • 1. T2-ADR documentation

    What to do:

    • Append 3 new sections to docs/adr/modifier-profiles.md:
      • ## T2-ADR-1: Turn-boundary queue — full semantics, last-write-wins, NACK on invalid at apply time
      • ## T2-ADR-2: Two-player consent — proposal/consent flow, 60s timeout, auto-reject
      • ## T2-ADR-3: Editor undo/redo — snapshot stack, 50-item cap, cleared on save/cancel

    Must NOT do: Don't write any code in this task.

    Recommended Agent Profile: writing — no skills

    Parallelization: Wave 1 (solo). Blocks: ALL. Blocked By: None.

    Acceptance Criteria:

    • File has 3 new T2-ADR sections
    • grep -c "^## T2-ADR-" docs/adr/modifier-profiles.md → 3

    Commit: docs(adr): T2 polish architecture decisions

  • 2. Turn-boundary queue server-side

    What to do:

    • Replace handleModifierProfileUpdate's immediate-apply with queue semantics:
      • Add Room.pendingProfile?: ModifierProfile field (may already exist from T1 — re-use or add)
      • On receive: validate shape, store in room.pendingProfile, ack with modifier-profile.queued
      • On applyMove() success: check pendingProfile, validate against post-move session, apply via reconcileProfileSwap, broadcast modifier-profile.updated, clear pending
      • If pending profile invalid at apply time: NACK to original sender with specific error code, clear pending
      • Last-write-wins: new pending replaces old pending before apply fires

    Must NOT do: Don't break T1 e2e tests. Don't apply mid-move.

    Recommended Agent Profile: deep — concurrency-sensitive

    Parallelization: Wave 1 (PARALLEL with T1, T3). Blocks: T3, T10, T11. Blocked By: T1.

    References:

    • packages/server/src/broadcast.ts — current handleModifierProfileUpdate
    • packages/server/src/game-session.ts — applyMove, post-move hook location
    • docs/adr/modifier-profiles.md § T2-ADR-1

    Acceptance Criteria:

    • Update sent during move — profile does NOT apply until opponent completes their move
    • 2 rapid updates → only the last is applied
    • Invalid pending at apply time → NACK to sender, no broadcast
    • bun run test packages/server/src/ws.modifier-profile-update.test.ts — all pass (updated)

    Commit: feat(server): turn-boundary queue for modifier profile updates

  • 3. Two-player consent protocol

    What to do:

    • New WS messages in protocol.ts:
      • modifier-profile.propose (client→server) — host sends candidate profile
      • modifier-profile.proposal-pending (server→opponent) — notify with profile contents
      • modifier-profile.consent (client→server) — opponent sends approve/reject
      • modifier-profile.rejected (server→host) — proposal rejected or timed out
    • broadcast.ts:
      • handleModifierProfilePropose — store proposal + start 60s timeout
      • handleModifierProfileConsent — on approve: promote to pendingProfile (feeds into T2 queue); on reject: clear + broadcast rejected
      • On timeout: auto-reject
    • rooms.ts:
      • Room.proposalState?: { profile, proposedBy, timeoutHandle, proposedAt }
    • Solo mode / host-only mode: modifier-profile.update alias auto-approves

    Must NOT do: Don't change modifier-profile.update semantics in solo mode.

    Recommended Agent Profile: deep

    Parallelization: Wave 1. Blocks: T9, T10, T11. Blocked By: T2.

    References:

    • docs/adr/modifier-profiles.md § T2-ADR-2

    Acceptance Criteria:

    • Propose → opponent sees pending → approve → broadcast to both
    • Propose → opponent rejects → only host sees rejected, no broadcast to board
    • Propose → 60s timeout → auto-reject
    • Solo mode: propose = immediate apply (no consent step)

    Commit: feat(server): two-player consent for modifier profile swaps

  • 4. Editor undo/redo snapshot stack

    What to do:

    • In ModifierProfileEditor.tsx:
      • const [history, setHistory] = useState<ModifierProfile[]>([initialProfile])
      • const [historyIndex, setHistoryIndex] = useState(0)
      • const currentProfile = history[historyIndex]
      • Every mutation path goes through pushSnapshot(newProfile) which truncates forward history and caps at 50
      • undo() / redo() adjust historyIndex
      • Keyboard handlers for Cmd/Ctrl+Z and Cmd/Ctrl+Shift+Z
      • Header toolbar with Undo/Redo buttons (disabled when at ends of stack)
    • data-testid="undo-button", data-testid="redo-button"

    Recommended Agent Profile: visual-engineering, skills: [interface-design]

    Parallelization: Wave 2. Blocks: T10. Blocked By: T1.

    Acceptance Criteria:

    • Add 3 modifiers → undo 2 → profile has 1 modifier
    • Undo then redo → same state
    • Cap at 50 snapshots (add 51st → oldest dropped)
    • Save or cancel clears history

    Commit: feat(ui): modifier editor undo/redo

  • 5. Copy/paste modifiers between pieces

    What to do:

    • In PerInstancePanel.tsx:
      • When a square is selected: "Copy modifiers from this square" button
      • When a different square is selected after copy: "Paste modifiers here" button (disabled when clipboard empty)
      • Clipboard is component-local state (not OS clipboard)
      • Paste appends all clipboard entries to the target square, with the kind preserved
    • In PerTypePanel.tsx:
      • "Copy to clipboard" on each row
      • "Paste" button appends from clipboard
    • data-testid="copy-modifier-b1" etc.

    Recommended Agent Profile: visual-engineering, skills: [interface-design]

    Parallelization: Wave 2. Blocks: T10. Blocked By: T1.

    Acceptance Criteria:

    • Add HP+2 to b1 → copy → paste on g1 → both have HP+2
    • Paste when clipboard empty: button disabled
    • Copy doesn't delete source

    Commit: feat(ui): copy/paste modifiers between pieces

  • 6. Inline conflict resolution panel

    What to do:

    • New component ConflictResolutionPanel.tsx:
      • Shows validation errors from validateProfile() inline
      • Each error has a "Fix" button with a specific auto-resolve action:
        • E_PROFILE_NO_KING → "Add king to e1" button (or suggest switching layout)
        • E_PROFILE_INVULN_KING → "Remove invuln from king" button
        • E_PROFILE_ORPHAN_INSTANCE (warning) → "Remove orphan entry" button
        • E_PROFILE_ATTR_LIMIT → "Show affected pieces" + manual resolution
      • Panel visible whenever profile has errors/warnings; hidden when clean
    • Wire into ModifierProfileEditor.tsx — show panel at the top when dirty

    Recommended Agent Profile: visual-engineering, skills: [interface-design]

    Parallelization: Wave 2. Blocks: T10. Blocked By: T1.

    Acceptance Criteria:

    • Create profile with invuln on king → panel shows error + Fix button
    • Click Fix → error resolved
    • Panel hidden when profile validates clean

    Commit: feat(ui): inline conflict resolution panel

  • 7. Modified-piece indicator on live board

    What to do:

    • New component ModifiedPieceIndicator.tsx:
      • Small colored dot (or glow/border) shown on pieces that have any active modifier
      • Reads MODIFIER_REGISTRY.list() and checks session.get(pieceId, attr) for each
      • If any defined → indicator visible
      • CSS: absolute-positioned top-right corner of piece square, 8px dot
    • Wire into Board.tsx — render indicator alongside piece

    Recommended Agent Profile: visual-engineering, skills: [interface-design]

    Parallelization: Wave 3. Blocks: T10. Blocked By: T1.

    Acceptance Criteria:

    • Piece with modifier: indicator visible
    • Piece without modifier: no indicator
    • Indicator updates reactively on profile swap

    Commit: feat(ui): modified-piece indicator on board

  • 8. Enhanced source-chain in pinned panel

    What to do:

    • Update ModifierPinnedPanel.tsx:
      • For each modifier row, add source label:
        • If piece's attr value matches a profile.perInstance entry at its square → "(per-instance: {square})"
        • Else if matches a perType entry for this pieceType+color → "(per-type: all {color} {type}s)"
        • Else if some active preset declared this attr → "(preset: {preset.name})"
        • Else → "(default)"
      • Reads the engine's active profile + presets via a new getModifierSource(engine, pieceId, kind) helper in packages/chess/src/modifiers/source.ts

    Recommended Agent Profile: visual-engineering, skills: [interface-design]

    Parallelization: Wave 3. Blocks: T10. Blocked By: T1.

    Acceptance Criteria:

    • Per-instance modifier → source labels show "per-instance: b1"
    • Per-type modifier → source labels show "per-type: all white knights"
    • piece-hp preset → source labels show "preset: Hit Points"

    Commit: feat(ui): enhanced modifier source chain in pinned panel

  • 9. Consent UI (proposal notification + buttons)

    What to do:

    • New component ModifierProposalDialog.tsx:
      • Shows when opponent has pending proposal (modifier-profile.proposal-pending received)
      • Displays summary of proposed profile changes
      • Approve / Reject buttons
      • 60s countdown timer
    • Wire into GameView.tsx via WS message subscription
    • Host sees a "Proposal sent — waiting for opponent" state after proposing

    Recommended Agent Profile: visual-engineering, skills: [interface-design]

    Parallelization: Wave 3. Blocks: T10. Blocked By: T3.

    Acceptance Criteria:

    • Host proposes → opponent sees dialog within 500ms
    • Approve → profile applies at next turn boundary
    • Reject → dialog closes on both sides, host sees "Proposal rejected"
    • Timeout → auto-reject

    Commit: feat(ui): consent dialog for modifier profile proposals

  • 10. Playwright e2e additions (8 feature scenarios + solo regression guards)

    What to do:

    • Add 8 new T2-feature tests to packages/chess/e2e/modifier-profiles.spec.ts:
      1. Editor undo/redo: add 3 modifiers, undo 2, redo 1
      2. Copy modifier between squares (per-instance)
      3. Paste button disabled when clipboard empty
      4. Conflict panel shows error + Fix works
      5. Modified-piece indicator visible without hover
      6. Source chain distinguishes per-instance vs per-type
      7. (multiplayer) Proposal → approve → both clients see update
      8. (multiplayer) Proposal → reject → no update
    • Keep and expand packages/chess/e2e/solo-smoke.spec.ts (already exists from T1 post-audit fix). Adds ongoing regression coverage for solo play — these tests caught the T1 Rules-Drawer-Esc bug. Ensure the following scenarios remain and are extended:
      • play-solo button navigates to /game with fresh board
      • drag pawn e2→e4 resolves
      • 4-ply sequence (e4 e5 Nf3 Nc6) completes
      • Rules drawer opens, closes via Esc, board remains interactive (the T1 regression guard)
      • No console errors on solo game start
    • Add 2 more solo-regression scenarios in T2 since T2 touches drawer/editor further:
      • Rules drawer: backdrop click closes drawer (workaround path still works)
      • Modifier editor: Esc closes editor without dismissing drawer underneath (nested modal ordering)

    Why both? solo-smoke.spec.ts is the canary — it exercises the baseline game flow without any modifier-profile setup. modifier-profiles.spec.ts covers feature paths. Keeping both prevents T2/T3 work from silently regressing solo play.

    Recommended Agent Profile: unspecified-high, skills: [playwright]

    Parallelization: Wave 4. Blocks: F1-F4. Blocked By: T2-T9.

    Acceptance Criteria:

    • 26/26 modifier-profile tests pass (18 from T1 + 8 new)
    • 7+ solo-smoke tests pass (5 original + 2 T2 additions)
    • Multiplayer suite unchanged, still passes
    • Total Playwright runtime < 120s

    Commit: test(e2e): T2 polish vertical slice + solo regression guards

  • 11. ADR updates — Implementation Retrospective T2 addendum

    What to do:

    • Append to docs/adr/modifier-profiles.md:
      • ## T2 Implementation Retrospective section noting:
        • T1's immediate-apply simplification resolved (T2-ADR-1)
        • T1's host-only simplification resolved (T2-ADR-2)
        • Editor UX parity with modern standards (undo/redo, copy/paste)
        • Any deviations found during implementation

    Recommended Agent Profile: unspecified-low

    Parallelization: Wave 4. Blocks: F1. Blocked By: T1, T2, T3.

    Commit: docs(adr): T2 implementation retrospective

  • 12. User docs updates

    What to do:

    • Update docs/user/modifier-profiles.md:
      • Update "Hot-Swap" section: describe proposal/consent flow (remove T1's "host-only" note)
      • Add "Editor Features" section: undo/redo (keyboard shortcuts), copy/paste, conflict resolution
      • Add "Board Indicators" section: modified-piece glow
      • Update "In-Play Inspection" section: mention enhanced source chain
      • Update "Known Limitations" section: remove T1 items, add T3 preview

    Recommended Agent Profile: unspecified-low

    Parallelization: Wave 4. Blocks: F1. Blocked By: T4-T9.

    Commit: docs(user): T2 modifier profile features


Final Verification Wave

  • F1. Plan Compliance Audit — oracle Verify all 12 tasks' deliverables exist. Verify T1 simplifications resolved. Check evidence files. Output: Must Have [N/N] | Must NOT Have [N/N] | ADR Decisions [3/3] | VERDICT: APPROVE/REJECT

  • F2. Code Quality Review — unspecified-high bun run check. Scan for slop patterns. Verify server-shared schemas. Output: Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass] | VERDICT

  • F3. Manual QA — unspecified-high (+ playwright) Execute all 26 Playwright scenarios. Multiplayer proposal/consent roundtrip. Undo/redo combos. Output: Scenarios [N/N] | Integration [pass] | VERDICT

  • F4. Scope Fidelity — deep No T3 features smuggled (no custom authoring, no auras, no multi-profile stacking). No breaking WS changes. Output: Tasks [N/N compliant] | Contamination [CLEAN] | VERDICT


Commit Strategy

  1. docs(adr): T2 polish architecture decisions
  2. feat(server): turn-boundary queue for modifier profile updates
  3. feat(server): two-player consent for modifier profile swaps
  4. feat(ui): modifier editor undo/redo
  5. feat(ui): copy/paste modifiers between pieces
  6. feat(ui): inline conflict resolution panel
  7. feat(ui): modified-piece indicator on board
  8. feat(ui): enhanced modifier source chain in pinned panel
  9. feat(ui): consent dialog for modifier profile proposals
  10. test(e2e): T2 polish vertical slice
  11. docs(adr): T2 implementation retrospective
  12. docs(user): T2 modifier profile features

Success Criteria

bun run check                                                    # all green
bun run test packages/server/                                    # all pass (including updated ws tests)
bunx playwright test e2e/modifier-profiles.spec.ts               # 26/26 pass

Final Checklist

  • All 9 feature tasks shipped end-to-end
  • T1 simplifications (immediate-apply, host-only) fully resolved
  • 26 Playwright e2e scenarios green
  • ADR + user docs updated
  • bun run check green
  • F1-F4 all APPROVE
  • User explicit "okay"