houserules/.sisyphus/notepads/thressgame-coverage/position-audit.md
Joey Yakimowich-Payne 2368a24b15
feat(thressgame-coverage): Wave 0-1 foundation (ADR + baseline + harness + audits)
Wave 0:
- T0: Architectural decisions (10 sections, 215 lines) + 5-rule paper exercise

Wave 1 (parallel):
- T1: Backward-compat baseline fixture (1961 tests / 167 files snapshot + regression guard)
- T2: Determinism property-test harness (runDeterminismCheck, N=100 default, 1.7s)
- T3: State-hash util (SHA256 of session.allFacts, insertion-order independent)
- T4: Position-attr caller audit (75 prod callsites classified, 17 fixes seeded for T6/T7)
- T5: $var conflict audit (CLEAN — T12 binding shape safe)

Tests: 1961 -> 1970 (+9). bun run check exits 0. No production source modified.
2026-04-26 08:16:26 -06:00

28 KiB
Raw Permalink Blame History

Position-attr Caller Audit (T4)

Catalogues every callsite in packages/chess/src/ that reads/writes/iterates the Position attribute. Risk-tagged for the marker-entity introduction in Wave 2 (T6, T7, T10).

Locked T0 Decision (decisions.md §"Square State via Marker Entities"): markers are first-class entities that carry a Position fact like pieces. The cross-cutting discriminator is EntityKind: 'piece' | 'marker'. Every callsite that reads Position assuming the bearer is a piece, or iterates all Position-bearing entities, must be revisited.

Methodology

  1. grep -rn '"Position"' packages/chess/src/ — string-literal callsites (241 hits)
  2. grep -rn '\bPosition\b' --include='*.ts' --include='*.tsx' — typed/identifier references (61 hits, mostly comments/docstrings/type defs — not callsites; only schema.ts:65 is the type alias and one cast in context.ts:151)
  3. Manual classification per the spec:
    • (a) PIECE-ASSUMING READ — session.get(id, "Position") (or facts.find by id+attr) where the caller's id was derived without an EntityKind filter
    • (b) WRITE — session.insert(X, "Position", v) or session.retract(X, "Position"). INHERENTLY SAFE — anyone writing their own Position is correct
    • (c) ITERATION — for (const f of facts) if (f.attr !== "Position") continue style scans, or facts.find(f => f.attr === "Position" && f.value === sq) square-lookups. RISK: walks markers too once they have Position
  4. Only PRODUCTION code is risk-graded. Test files (*.test.ts) and test helpers are listed in §Tests for completeness but are not blockers — the production risks they assert against will be caught by Wave-2 changes; tests can be updated in lockstep.

Total grep hits: 241 string-literal + 2 typed-cast = 243 callsite mentions across 52 files


Production Callsites (RISK-graded)

Engine core — packages/chess/src/engine.ts

# Line Class Snippet Risk / Fix
1 753 (b) WRITE this.session.insert(id, "Position", square) in spawnPiece none
2 1215 (b) WRITE this.session.insert(move.pieceId, "Position", move.to) (pawn-capture advance) none
3 1259 (b) WRITE this.session.insert(move.pieceId, "Position", move.to) (normal-move advance) none
4 1680 (c) ITER getPieceAt: find(x => x.attr === "Position" && x.value === square && x.id > 0) RISK — returns the FIRST entity at square; with markers also at square, may return a marker. FIX: add && session.get(x.id, "EntityKind") === "piece" (or wait until EntityKind lands in T6 and filter by it). T7-adjacent.

Starting position — packages/chess/src/starting-position.ts

# Line Class Snippet Risk / Fix
5 176 (b) WRITE session.insert(id, "Position", placement.square) none

Rules — packages/chess/src/rules/board-queries.ts

# Line Class Snippet Risk / Fix
6 22 (c) ITER getBoardPieces: walks f.attr === "Position" and joins on Color PARTIAL-SAFE: requires Color fact on same id; markers have no Color → naturally filtered out. Recommend: add explicit EntityKind === 'piece' precondition for clarity & to harden against future marker variants that do carry Color (e.g. side-aware portal markers).
7 38 (c) ITER isPieceAt: returns true for ANY Position-bearing entity at square RISK — function name promises "piece"; will return true for marker. FIX: add EntityKind filter. Wave-2/T6 OWNER.
8 46 (c) ITER isEnemyAt: find(f.attr === "Position" && f.value === square) RISK — if a marker is at square before a piece in fact-order, returns marker, then Color lookup fails → returns false even though there's an enemy piece on the same square. FIX: add EntityKind filter to the find predicate.
9 56 (c) ITER isAllyAt: same shape as isEnemyAt RISK — same as #8. FIX: same.
10 65 (a) READ getPiecePosition(session, pieceId): session.get(pieceId, "Position") API-LAYER: contract is "pass piece id". Caller-dependent. No fix needed in this file, but downstream callers must continue to pass piece ids only. Mark FN as "piece-only — caller responsible".
11 78 (c) ITER getPieceAt: returns FIRST Position-bearing id at square RISK — same as #4. FIX: EntityKind filter. Wave-2/T6.

Rules — packages/chess/src/rules/capture.ts

# Line Class Snippet Risk / Fix
12 22 static CORE_PIECE_ATTRS includes "Position" n/a — constant for retraction list. Markers carry their own attr list (per T0 — Kind, Position, MarkerLifetime, MarkerLinks). No fix; just confirm effectivePieceAttrs retraction logic doesn't accidentally retract marker-only attrs (separate concern, T6).
13 94 (c) ITER canCapture: find(f.attr === "Position" && f.value === square) then color check RISK — same shape as isEnemyAt. Marker at target square ⇒ canCapture returns false even when an enemy piece IS there (different fact-order win). FIX: EntityKind filter.

Rules — packages/chess/src/rules/turn.ts

# Line Class Snippet Risk / Fix
14 163 (b) WRITE session.insert(move.pieceId, "Position", move.to) none

Rules — packages/chess/src/rules/castling.ts

# Line Class Snippet Risk / Fix
15 174 (b) WRITE session.insert(move.pieceId, "Position", move.to) (king) none
16 178 (b) WRITE session.insert(rookId, "Position", move.rookTo) none

Rules — packages/chess/src/rules/enpassant.ts

# Line Class Snippet Risk / Fix
17 103 (b) WRITE/RETRACT for (const attr of [..., "Position", ...]) session.retract(capturedId, attr) none — retracts captured pawn
18 109 (b) WRITE session.insert(move.pieceId, "Position", move.to) none

Rules — packages/chess/src/rules/check.ts

# Line Class Snippet Risk / Fix
19 115 (a) READ find(f => f.id === royalId && f.attr === "Position") SAFE — royalId comes from defaultRoyalIds which filters PieceType === "king". Markers have no PieceType. No fix needed because royal-set is already piece-typed.
20 173 (c) ITER clearSquare(temp, square): find(f.attr === "Position" && f.value === square) RISK — used in self-check what-if simulation. Marker at the destination square would be the wrongly-cleared occupant. FIX: EntityKind filter. Note: temp sessions are built via snapshotSession which copies all isPieceAttr(f.attr) facts — if markers are excluded from snapshotSession upfront, this is moot. Recommend snapshotSession-level fix instead (see #50).
21 240 (b) WRITE temp.insert(move.pieceId, "Position", move.to) (what-if) none

Rules — packages/chess/src/rules/stalemate.ts

# Line Class Snippet Risk / Fix
22 77 (c) ITER if (move.isCapture && f.attr === "Position" && f.value === move.to) continue (what-if build) RISK — copies all Position facts EXCEPT one at the capture square. Marker at the same square would be wrongly skipped during what-if construction. FIX: when copying, filter by EntityKind; the capture-square skip should also be piece-only.
23 87 (c) ITER find(g.attr === "Position" && g.value === move.to && g.id !== move.pieceId) RISK — same as #22.
24 102 (b) WRITE temp.insert(move.pieceId, "Position", move.to) none

Rules — packages/chess/src/rules/draws.ts

# Line Class Snippet Risk / Fix
25 40 (c) ITER computePositionHash: includes facts where attr === "Position" (alongside PieceType, Color) for id > 0 RISK — marker positions would change the hash, breaking 3-fold-repetition: a turn-counter marker that ticks every turn would defeat repetition detection. FIX: filter to EntityKind === "piece" so only piece-Position facts contribute to the hash. Wave-2/T6 OWNER.

Rules — packages/chess/src/rules/insufficient.ts

# Line Class Snippet Risk / Fix
26 38 (a) READ via JOIN getPieces: for each PieceType fact, facts.find(p.attr === "Position") SAFE — outer iteration is PieceType-keyed; markers have no PieceType. No fix.

Modifiers — packages/chess/src/modifiers/auras.ts

# Line Class Snippet Risk / Fix
27 102 (c) ITER collectPiecesWithPositions: walks Position facts, filters id > 0 RISK / DESIGN POINT — per T0 decisions.md L55: "Markers participate in aura compute". So this iteration SHOULD include markers AS targets/sources. Current code names the function "Pieces…" but post-T7 it should also yield markers. FIX (T7): rename to collectPositionedEntities, RETAIN the include-all behaviour, but split source-vs-target classification by EntityKind (markers can be aura sources, e.g. portal aura debuff; pieces are typical targets). The staging-write loop (L84) writes AuraContributions to the target — marker targets are valid per T0, but markers don't currently consume AuraContributions. T7 must decide: (1) skip markers as targets, OR (2) let markers carry AuraContributions for downstream consumers. AUDIT-RECORDED, NEEDS T7 DESIGN CALL.

Modifiers — packages/chess/src/modifiers/apply.ts

# Line Class Snippet Risk / Fix
28 234 (c) ITER snapshotPositions: walks Position facts, id > 0 filter RISK — used by diffMovedPieceIds (L248) → fireOnMoveHooks (L1040). A marker that "moves" each turn (e.g. a future moving-marker variant) would be reported as a moved piece, firing on-move hooks on a marker. FIX: filter to EntityKind === "piece". Wave-2/T6 OWNER.
29 254 (a) READ diffMovedPieceIds: session.get(id, "Position") for ids from snapshot CHAINED RISK from #28 — if snapshot already filters to pieces, this is safe. FIX is upstream at #28.
30 370 (a) READ find(f.id === royalId && f.attr === "Position") SAFE — same as #19; royalId is piece-typed.
31 405 (a) READ via JOIN capturePromotionCandidates: outer is PieceType=pawn SAFE — same as #26.
32 502 (c) ITER buildSquareIndex: maps square→EntityId from Position facts RISK — if marker and piece share a square, last-write-wins on the Map. Modifier perInstance lookup (b1 → entityId) would resolve to whichever wrote last. FIX: filter to EntityKind === "piece" — modifiers target pieces, not markers. Wave-2/T6 OWNER.
33 1048 (a) READ session.get(id, "Position") for id in movedIds CHAINED from #28. Fix at upstream snapshot.

Modifiers — packages/chess/src/modifiers/source.ts

# Line Class Snippet Risk / Fix
34 26 (a) READ engine.session.get(pieceId, "Position") API-LAYER — function takes pieceId: EntityId. Caller responsibility to pass piece ids. No fix here; document piece-only contract.

Modifiers — packages/chess/src/modifiers/primitives/context.ts

# Line Class Snippet Risk / Fix
35 148 (c) ITER resolveBySquares: walks Position facts, filters id > 0, returns matching ids RISK — used by modifier consumers to resolve target ids by square. With markers at the same square, marker ids leak into the consumer's effect target list (e.g. add-to-attribute would try to mutate a marker's HpBonus). FIX: filter to EntityKind === "piece". Wave-2/T6 OWNER.
36 151 typed-cast f.value as ChessAttrMap["Position"] n/a — type cast inside #35.

Modifiers — packages/chess/src/modifiers/primitives/seed-attribute.ts

# Line Class Snippet Risk / Fix
37 9 static "Position" listed in CHESS_ATTR_KEYS allow-list n/a — schema constant. Note: T6 adds EntityKind to schema; this set must grow accordingly (separate concern).

Modifiers — packages/chess/src/modifiers/triggers.ts

# Line Class Snippet Risk / Fix
38 590 (a) READ find(f.id === royalId && f.attr === "Position") SAFE — same as #19 (royalId is piece-typed).

Presets — packages/chess/src/presets/

# File:Line Class Snippet Risk / Fix
39 knights-leap-twice.ts:29 (a) READ via JOIN find(f.id === pieceId && f.attr === "Position") API-LAYER — pieceId from move-gen-extras callsite (engine guarantees piece-typed). No fix.
40 bishops-ignore-color.ts:35 (a) READ via JOIN same shape API-LAYER. No fix.
41 rook-warp.ts:53 (a) READ via JOIN same shape API-LAYER. No fix.
42 queen-splits.ts:72 (c) ITER find(f.attr === "Position" && f.value === sq && f.id > 0) (pieceAtSquare helper) RISK — local helper duplicates engine.getPieceAt. Marker at sq leaks. FIX: add EntityKind filter OR consolidate to engine helper.
43 queen-splits.ts:106 (a) READ session.get(target, "Position") where target is the capture target id from onBeforeCapture CHAINED — if engine routes a marker as a capture target (T19/T22 says markers don't get captured normally), safe. Audit: confirm T22 contract excludes marker-as-target.
44 king-heals.ts:65 (a) READ session.get(kingId, "Position") SAFE — kingId from royal set.
45 explosive-rook.ts:34 (c) ITER pieceAtSquare helper, same as #42 RISK / FIX: same as #42.
46 explosive-rook.ts:57 (a) READ session.get(target, "Position") CHAINED as #43.
47 explosive-rook.ts:116 (b) WRITE session.insert(attacker, "Position", targetPos) none
48 last-piece-standing.ts:61 (c) ITER walks Position facts, joins via Color map PARTIAL-SAFE — markers without Color are skipped. Recommend explicit EntityKind filter for clarity.
49 piece-hp.ts:242 (a) READ via JOIN find(p.id === f.id && p.attr === "Position") where outer is PieceType=king SAFE — outer filter is piece-typed.
50 knight-immunity.ts:16 (c) ITER find(f.attr === "Position" && f.value === m.to) to identify capture target type RISK — marker at m.to would be probed for PieceType (none) → falls through targetType !== "knight" → move kept. Mostly benign in this preset's logic but symptomatic of the pattern. FIX: EntityKind filter.
51 poisoned-squares.ts:45 (c) ITER iterates Position facts, victims are entities standing on poisoned squares RISK — markers on poisoned squares would be in victims; dealDamage then runs against marker (no Hp; pipeline likely no-ops, but emits visual poison effect uselessly and may interact with marker retraction). FIX: EntityKind filter on the victim-collection loop. Wave-2/T6 OWNER.
52 poisoned-squares.ts:60 (a) READ engine.session.get(id, "Position") for id from victim list CHAINED from #51.
53 wrap-board.ts:228 (a) READ via JOIN find(f.id === pieceId && f.attr === "Position") API-LAYER — getExtraMoves contract is piece-typed. No fix.
54 bouncing-pieces.ts:228 (a) READ via JOIN same API-LAYER.
55 bouncing-pieces-2.ts:217 (a) READ via JOIN same API-LAYER.
56 berolina-pawns.ts:348 (b) RETRACT iterates ["PieceType","Color","Position","HasMoved"] to retract on captured pawn id none
57 berolina-pawns-2.ts:244 (b) RETRACT same none
58 dual-king.ts:106 (c) ITER walks all facts; bucketizes into kingIds/colorById/hasPosition then composes SAFE — final filter is PieceType=king; markers excluded.
59 coregal.ts:105 (c) ITER same shape SAFE — final filter t === "king" || t === "queen".
60 weak-dual-king.ts:126 (c) ITER same shape SAFE — same as #58.
61 weak-dual-king.ts:166 (a) READ via JOIN find(f.id === id && f.attr === "Position") for id from royal set SAFE.
62 weak-dual-king.ts:199 (c) ITER find(f.attr === "Position" && f.value === move.to) (clearSquare in temp) RISK — same shape as #20. FIX: EntityKind filter.
63 weak-dual-king.ts:211 (b) WRITE temp.insert(move.pieceId, "Position", move.to) (what-if) none
64 suicide-chess.ts:261 (c) ITER composes Position presence with PieceType-anchored hasType set SAFE — final filter is piece-typed.
65 extinction-chess.ts:207 (c) ITER same shape SAFE.
66 transferable-royalty.ts:118 (a) READ via JOIN for each kingId, f.attr === "Position" to mark alive SAFE — kingIds are PieceType-anchored.
67 capture-all.ts:149 (c) ITER counts Position facts per Color PARTIAL-SAFE — same as #48; markers without Color naturally skipped. Recommend explicit EntityKind filter.

UI

# File:Line Class Snippet Risk / Fix
68 ui/GameView.tsx:319 (c) ITER find(p.id === f.id && p.attr === 'Position') for kings SAFE — outer iteration is PieceType=king; markers excluded.
69 ui/GameView.tsx:341 (a) READ engine.session.contains(id, "Position") for id from royal-set SAFE.
70 ui/GameView.tsx:350 (c) ITER find(pf.id === f.id && pf.attr === "Position") outer PieceType=king SAFE.
71 ui/GameView.tsx:374 (c) ITER find(pf.id === f.id && pf.attr === "Position") outer Color=mover RISK — outer filter is Color === turnAsColor. Markers don't have Color → naturally excluded. PARTIAL-SAFE; recommend explicit EntityKind filter for clarity.
72 ui/Board.tsx:103 (c) ITER if (fact.attr === 'Position') ent.pos = fact.value (collects entity rendering data) RISK — Board.tsx renders any entity with PieceType+Color+Position. Markers won't render as pieces (no PieceType+Color), but the collected entityFacts map populates marker entries unnecessarily. NO FUNCTIONAL FIX REQUIRED for piece rendering. HOWEVER, Wave-3 marker-rendering UI must add a parallel branch that consumes markers via EntityKind === 'marker'. AUDIT FLAG for UI work in T20+.

Net / Hooks (production)

# File:Line Class Snippet Risk / Fix
73 hooks/useMultiplayerGame.ts:308 comment derives last-move from Position facts (comment only) n/a — no live read here, just a tracking comment. (No code touches Position on this line; it's a doc comment in a memoization block.)
74 layouts/chess960.ts:177 string-literal "Position number shown in the name." (descriptor text) n/a — string is in the rule preset description, NOT the attr. False positive of grep.
75 presets/registry.ts:138 docstring comment referencing engine.session.get(pieceId, "Position") n/a — JSDoc.

Tests (production-confidence verification only — fix in lockstep when production lines change)

All test-suite callsites for "Position" fall into three categories:

  • Setup/seed writes (mostly session.insert(eid, "Position", sq) in helpers like placePiece/makePiece): pure (b) WRITEs, no risk. They simulate piece placement; tests will need EntityKind: "piece" seeded too once T6 lands.
  • Assertion reads (expect(session.get(X, "Position"))…): test-only, mirror production behaviour. Will need test updates iff the production behaviour they probe changes (e.g. getPieceAt adding EntityKind filter — tests currently passing piece ids will continue to pass).
  • Find-by-position queries (facts.find(f => f.attr === "Position" && f.value === sq)): mirrors production patterns; same risk surface but inside test-only state.

Files & rough line counts:

File Hits Notes
schema.test.ts 1 (L21) chessFact factory
starting-position.test.ts 5 (L37, 70, 107, 230, 232) reads + filter
rules/pawn.test.ts 2 seed
rules/knight.test.ts 1 seed
rules/sliding.test.ts 2 seed
rules/king.test.ts 1 seed
rules/capture.test.ts 3 seed + contains-checks
rules/turn.test.ts 6 seed + asserts
rules/castling.test.ts 7 seed + retract + asserts
rules/enpassant.test.ts 6 seed + asserts
rules/promotion.test.ts 3 seed + assert
rules/check.test.ts 1 seed
rules/stalemate.test.ts 1 seed
rules/checkmate.test.ts 1 seed
rules/draws.test.ts 7 seed
rules/insufficient.test.ts 1 seed
presets/pawns-move-backward.test.ts 3 seed/assert
presets/knights-bishop-rook.test.ts 1 seed
presets/wrap-board.test.ts 5 seed/retract
presets/piece-hp.test.ts 1 (c)-ITER find
presets/fleshed-presets.test.ts 11 mixed
presets/damage-pipeline.test.ts 13 mixed
presets/move-log.test.ts 1 (c) find
presets/visual-effect.test.ts 3 seed/loop
presets/phase-hooks.test.ts 1 (c) find
presets/test-utils.ts 2 helpers
presets/integration.test.ts 3 mixed
presets/transform-hook.test.ts 8 mixed
presets/royal-pieces.test.ts 1 seed
presets/override-piece-moves.test.ts 2 (a)+(c)
presets/knightmate-rules.test.ts 1 (c) find
presets/presets.test.ts 1 seed
presets/first-promotion-wins.test.ts 1 seed
presets/extinction-chess.test.ts 1 retract
presets/berolina-pawns.test.ts 5 seed/retract/assert
presets/berolina-pawns-2.test.ts 1 assert
presets/bouncing-pieces.test.ts 1 seed
presets/bouncing-pieces-2.test.ts 1 seed
presets/transferable-royalty.test.ts 5 retract
net/client.test.ts 2 fixture
net/prediction.test.ts 3 mixed
modifiers/descriptors/direction-additions.test.ts 6 seed
modifiers/descriptors/promotion-override.test.ts 1 seed
modifiers/reconcile.test.ts 3 (c) find / assert
modifiers/source.test.ts 4 (c) find
modifiers/custom/apply.test.ts 1 (c) find
modifiers/custom/legacy-descriptor.test.ts 1 (c) find
modifiers/primitives/consumer-integration.test.ts 2 (c) find / retract
modifiers/primitives/context.test.ts 1 seed
modifiers/apply.test.ts 6 (c) find
modifiers/auras.test.ts 4 (c) find / seed
modifiers/triggers.test.ts 1 (c) find
util/state-hash.test.ts 3 seed

Total test hits: ≈137. Action: when a production line changes (e.g. getPieceAt adds EntityKind filter), grep its test file for matching find patterns and add EntityKind to test fixtures. T6 must seed EntityKind: "piece" automatically in engine.spawnPiece so existing test setup paths inherit the discriminator without per-test edits.


Summary by Class (production code only — items 1–75)

Class Count Action
(a) PIECE-ASSUMING READ 8 of which 0 unsafe (4 SAFE via piece-typed precondition; 3 API-LAYER; 1 chained) Document piece-only contract on getPiecePosition, getModifierSource. T7 confirms aura compute design call.
(b) WRITE 15 Inherently safe. No fix.
(c) ITERATION 22 of which 9 unsafe + 5 partial-safe Wave-2/T6/T7 fix list below.
Static / typed / docstring / false-positive 5 n/a

Production callsites total: ~50 grep-distinct lines across 27 production files. Test callsites total: ~137 lines across 50 test files.


Fix Tracker (HIGH-PRIORITY — for downstream tasks)

These callsites MUST be revisited in Wave 2 (T6 — schema/EntityKind landing) or Wave 4 (T7 — aura compute):

T6 (schema / engine.spawnPiece + EntityKind seeding) MUST update:

  • engine.ts:1680 — getPieceAt private — add EntityKind filter
  • rules/board-queries.ts:38 — isPieceAt — EntityKind filter
  • rules/board-queries.ts:46 — isEnemyAt — EntityKind filter on the position-find
  • rules/board-queries.ts:56 — isAllyAt — EntityKind filter on the position-find
  • rules/board-queries.ts:78 — getPieceAt (public) — EntityKind filter
  • rules/capture.ts:94 — canCapture — EntityKind filter
  • rules/check.ts:173 — clearSquare (or fix at snapshotSession upstream) — EntityKind filter
  • rules/stalemate.ts:77, 87 — what-if construction — EntityKind filter on Position-skip and Position-find
  • rules/draws.ts:40 — computePositionHash — exclude marker Position from hash
  • modifiers/apply.ts:234 — snapshotPositions — EntityKind filter (chains to L254, L1048)
  • modifiers/apply.ts:502 — buildSquareIndex — EntityKind filter
  • modifiers/primitives/context.ts:148 — resolveBySquares — EntityKind filter
  • presets/queen-splits.ts:72 — local pieceAtSquare — EntityKind filter (or consolidate)
  • presets/explosive-rook.ts:34 — local pieceAtSquare — same
  • presets/knight-immunity.ts:16 — capture-target probe — EntityKind filter
  • presets/poisoned-squares.ts:45 — victim collection — EntityKind filter (chains to L60)
  • presets/weak-dual-king.ts:199 — what-if clearSquare analogue — EntityKind filter

T7 (aura compute + marker participation) MUST address:

  • modifiers/auras.ts:102 — collectPiecesWithPositions — DESIGN CALL: include markers as sources/targets per T0; emit AuraContributions only to consumers that read it. Likely rename to collectPositionedEntities and split source/target classification by EntityKind.

Wave-3+ (marker rendering UI) MUST address:

  • ui/Board.tsx:103 — add parallel marker-rendering pass (entities with EntityKind === 'marker')
  • ui/GameView.tsx — surface marker overlays where appropriate (separate visual tier)
  • rules/board-queries.ts:22 — getBoardPieces
  • presets/last-piece-standing.ts:61
  • presets/capture-all.ts:149
  • ui/GameView.tsx:374

API-LAYER contracts to document (no code change, but JSDoc):

  • rules/board-queries.ts:65 — getPiecePosition — "callers must pass piece ids"
  • modifiers/source.ts:26 — getModifierSource — same
  • presets/knights-leap-twice.ts:29, bishops-ignore-color.ts:35, rook-warp.ts:53, wrap-board.ts:228, bouncing-pieces.ts:228, bouncing-pieces-2.ts:217 — getExtraMoves contract is piece-typed by engine

Cross-Reference: Files NOT requiring action

These touched Position but are SAFE because the calling chain already filters by PieceType (which markers lack):

  • rules/check.ts:115 — defaultRoyalIds → PieceType=king
  • rules/insufficient.ts:38 — outer PieceType filter
  • modifiers/apply.ts:370, 405 — royal/promotion candidate sets are PieceType-anchored
  • modifiers/triggers.ts:590 — same
  • presets/dual-king.ts, coregal.ts, weak-dual-king.ts:126/166, suicide-chess.ts, extinction-chess.ts, transferable-royalty.ts, piece-hp.ts:242, king-heals.ts:65, ui/GameView.tsx:319/341/350 — all PieceType-anchored.

Open Questions for T6/T7 leads

  1. Marker-as-capture-target: confirm T22's contract that dealDamage(markerId, …) is a no-op (markers can't be "captured" through the pipeline). If yes, queen-splits.ts:106 and explosive-rook.ts:57 are safe via #43/#46 chain.
  2. Aura targets: do markers carry AuraContributions? T7 design call. Currently auras.ts:84 writes contributions to ANY positioned entity within range.
  3. Snapshot-based what-if (check.ts:snapshotSession, stalemate.ts inline, weak-dual-king.ts:snapshotPieces): each independently copies "piece-level facts" via isPieceAttr(f.attr). Once T6 adds EntityKind to the schema, isPieceAttr returns true for it (it's not in GAME_LEVEL_ATTRS), so markers' EntityKind facts WILL flow into the temp session. This is correct — what-if simulation should preserve markers — but the helpers' clearSquare analogues must then filter by EntityKind to avoid clearing markers. Logged here as a class-level concern; per-helper fixes already itemized above (#20, #22-23, #62).