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.
28 KiB
Position-attr Caller Audit (T4)
Catalogues every callsite in
packages/chess/src/that reads/writes/iterates thePositionattribute. 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 aPositionfact like pieces. The cross-cutting discriminator isEntityKind: 'piece' | 'marker'. Every callsite that reads Position assuming the bearer is a piece, or iterates all Position-bearing entities, must be revisited.
Methodology
grep -rn '"Position"' packages/chess/src/— string-literal callsites (241 hits)grep -rn '\bPosition\b' --include='*.ts' --include='*.tsx'— typed/identifier references (61 hits, mostly comments/docstrings/type defs — not callsites; onlyschema.ts:65is the type alias and one cast incontext.ts:151)- Manual classification per the spec:
- (a) PIECE-ASSUMING READ —
session.get(id, "Position")(or facts.find by id+attr) where the caller'sidwas derived without an EntityKind filter - (b) WRITE —
session.insert(X, "Position", v)orsession.retract(X, "Position"). INHERENTLY SAFE — anyone writing their own Position is correct - (c) ITERATION —
for (const f of facts) if (f.attr !== "Position") continuestyle scans, orfacts.find(f => f.attr === "Position" && f.value === sq)square-lookups. RISK: walks markers too once they have Position
- (a) PIECE-ASSUMING READ —
- 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 likeplacePiece/makePiece): pure(b) WRITEs, no risk. They simulate piece placement; tests will needEntityKind: "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.getPieceAtadding 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—getPieceAtprivate — add EntityKind filterrules/board-queries.ts:38—isPieceAt— EntityKind filterrules/board-queries.ts:46—isEnemyAt— EntityKind filter on the position-findrules/board-queries.ts:56—isAllyAt— EntityKind filter on the position-findrules/board-queries.ts:78—getPieceAt(public) — EntityKind filterrules/capture.ts:94—canCapture— EntityKind filterrules/check.ts:173—clearSquare(or fix atsnapshotSessionupstream) — EntityKind filterrules/stalemate.ts:77, 87— what-if construction — EntityKind filter on Position-skip and Position-findrules/draws.ts:40—computePositionHash— exclude marker Position from hashmodifiers/apply.ts:234—snapshotPositions— EntityKind filter (chains to L254, L1048)modifiers/apply.ts:502—buildSquareIndex— EntityKind filtermodifiers/primitives/context.ts:148—resolveBySquares— EntityKind filterpresets/queen-splits.ts:72— localpieceAtSquare— EntityKind filter (or consolidate)presets/explosive-rook.ts:34— localpieceAtSquare— samepresets/knight-immunity.ts:16— capture-target probe — EntityKind filterpresets/poisoned-squares.ts:45— victim collection — EntityKind filter (chains to L60)presets/weak-dual-king.ts:199— what-ifclearSquareanalogue — EntityKind filter
T7 (aura compute + marker participation) MUST address:
modifiers/auras.ts:102—collectPiecesWithPositions— DESIGN CALL: include markers as sources/targets per T0; emitAuraContributionsonly to consumers that read it. Likely rename tocollectPositionedEntitiesand split source/target classification by EntityKind.
Wave-3+ (marker rendering UI) MUST address:
ui/Board.tsx:103— add parallel marker-rendering pass (entities withEntityKind === 'marker')ui/GameView.tsx— surface marker overlays where appropriate (separate visual tier)
Recommended-but-not-required (PARTIAL-SAFE today, hardening for clarity):
rules/board-queries.ts:22—getBoardPiecespresets/last-piece-standing.ts:61presets/capture-all.ts:149ui/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— samepresets/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—getExtraMovescontract 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=kingrules/insufficient.ts:38— outer PieceType filtermodifiers/apply.ts:370, 405— royal/promotion candidate sets are PieceType-anchoredmodifiers/triggers.ts:590— samepresets/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
- 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:106andexplosive-rook.ts:57are safe via #43/#46 chain. - Aura targets: do markers carry
AuraContributions? T7 design call. Currentlyauras.ts:84writes contributions to ANY positioned entity within range. - Snapshot-based what-if (
check.ts:snapshotSession,stalemate.tsinline,weak-dual-king.ts:snapshotPieces): each independently copies "piece-level facts" viaisPieceAttr(f.attr). Once T6 addsEntityKindto the schema,isPieceAttrreturns true for it (it's not inGAME_LEVEL_ATTRS), so markers' EntityKind facts WILL flow into the temp session. This is correct — what-if simulation should preserve markers — but the helpers'clearSquareanalogues 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).