feat(thressgame-coverage): Wave 3 (binding scope + param walker + validator extensions)

- T11: PrimitiveApplyContext.bindings (immutable Map<string,BindingValue>); withBinding helper; threaded through 22 test files + triggers.ts/apply.ts
- T12: param-resolver.ts walker resolves { $var }, { ctx-attr: { entity, attr } }, { ctx-build: { col, row } } shapes; wired before primitive.apply in triggers.ts + custom/apply.ts; BindingError class
- T13: validator binding-out-of-scope check (descriptor.primitives.binding-out-of-scope); BINDING_INTRODUCING_KINDS map (8 future kinds); cycle-guarded $var walker
- T14: validator imperative-in-passive check (descriptor.primitives.imperative-in-passive, 10 IMPERATIVE_KINDS); LastModifierChooser tracking on PRESET_STATE_ENTITY (chooser-entity stub)

Tests: 2014 -> 2048 (+34). bun run check exit 0.
This commit is contained in:
Joey Yakimowich-Payne 2026-04-26 09:10:21 -06:00
commit defe56feb9
No known key found for this signature in database
36 changed files with 1865 additions and 11 deletions

View file

@ -496,3 +496,398 @@ getMarkersAtSquare(square: Square): EntityId[]; // sorted by MARKER_KIND_PRIORIT
- `bun test packages/chess/src/engine.spawnMarker.test.ts` → 8 pass / 0 fail / 20 expects
- `bun run check` → exit 0, **2014 tests** across 171 files (was 1999 after T9; +8 from T10's new file, +7 from T7/T8 parity tests landing in same wave)
- Evidence: `.sisyphus/evidence/task-10-marker-priority.txt`
## [2026-04-26T08:38Z] T11 binding scope stack on PrimitiveApplyContext
### What landed
- `packages/chess/src/modifiers/primitives/context.ts` — added `BindingValue` union + `withBinding(ctx, name, value)` helper. `withBinding` clones the inner Map (`new Map(ctx.bindings)`), sets the new entry, and spreads `{...ctx, bindings: next}`. Outer ctx is NEVER mutated.
- `packages/chess/src/modifiers/primitives/types.ts` — added required field `readonly bindings: ReadonlyMap<string, BindingValue>` on `PrimitiveApplyContext`. Imported `BindingValue` from `./context.js`.
- `packages/chess/src/modifiers/triggers.ts` — `runPrimitives()` gained a 6th parameter `bindings: ReadonlyMap<string, BindingValue> = new Map()`. Recursive call into nested children threads the SAME map (no reset). Both context-construction sites (`runPrimitives` + `fireOnCapturedHooks`'s `resolverCtx`) seed `bindings: new Map()` / pass-through.
- `packages/chess/src/modifiers/custom/apply.ts:74` — profile-time apply seeds `bindings: new Map()`.
- `packages/chess/src/modifiers/primitives/context.test.ts` — +5 tests under `describe("binding scope (T11)")`, total 13 → 18.
- 22 test files under `packages/chess/src/modifiers/primitives/*.test.ts` updated by sed: insert `bindings: new Map(),` after `event: undefined,`. (`absorb-damage-with-attribute`, `add-aura`, `add-direction`, `add-to-attribute`, `block-move-type`, `conditional`, `modify-movement-range`, `multiply-attribute`, `on-captured`, `on-capture`, `on-check-delivered`, `on-check-received`, `on-damaged`, `on-moved-onto-square`, `on-move`, `on-promotion`, `on-turn-end`, `on-turn-start`, `override-promotion`, `reflect-damage`, `seed-attribute`, `set-capture-flag`.)
### API contract (REUSE for T12, T13, T31-T35, T47)
```ts
export type BindingValue =
| EntityId
| readonly EntityId[]
| number // covers Square (0..63 alias)
| string
| boolean;
export function withBinding(
ctx: PrimitiveApplyContext,
name: string,
value: BindingValue,
): PrimitiveApplyContext; // returns NEW ctx; outer untouched
```
- **NEVER mutate `ctx.bindings` in place**. Always `withBinding(...)`.
- **`null` / `undefined` are NOT valid binding values** by design — absence means "no such binding", which keeps `ctx.bindings.get(name) === undefined` an unambiguous "not bound" sentinel for T12's `{ $var }` resolver.
- **Lexical scope**: nested primitives inherit the caller's bindings unchanged (passed through `runPrimitives`). A primitive that calls `withBinding` only affects the inner sub-tree it itself recurses into.
- **Shadowing**: rebinding the same name in an inner ctx replaces the value for that scope; the outer ctx still sees the original (immutability proof — covered by `T11.shadow` test).
### Where to introduce bindings (downstream tasks)
- T31-T35 (iteration primitives `for-each-piece`, `for-each-square`, etc.) — call `withBinding(ctx, params.bindAs, currentItem)` per iteration, then recursively call into the nested primitive list with the new ctx.
- T37 (RNG primitive) — `withBinding(ctx, params.bindAs, rngPick)`.
- T47 (request-choice) — restored from `PendingChoice` deserialization, then `withBinding(ctx, params.bindAs, submission)` before resuming the post-choice primitive list.
### Why required, not optional
- Making `bindings` REQUIRED on the interface (with `new Map()` at every callsite) follows the same precedent as T1's `target`/`event`. Forces dispatchers + test fixtures to think about binding scope at construction; opt-in `bindings?: ...` would lose the load-time guarantee that no path silently passes `undefined` and drops scope.
- Empty-Map default at every callsite is byte-identically backward-compatible with the 22 pre-T11 primitives — they don't read `ctx.bindings` at all. Verified: 2014 tests → 2019 tests (only +5 new tests; zero regressions).
### Verification
- `bun test packages/chess/src/modifiers/primitives/context.test.ts` → 18 pass / 0 fail / 49 expects (was 13 / 35; +5 / +14)
- `bun run check` → exit 0, **2019 tests** across 171 files (was 2014 after T10; +5 from T11)
- LSP diagnostics: clean on `context.ts`, `types.ts`, `triggers.ts`, `context.test.ts`
- Evidence: `.sisyphus/evidence/task-11-bindings.txt`
### Surprises / gotchas
- `PrimitiveApplyContext` is defined in `./types.js`, NOT in `./context.ts`. The `BindingValue` type lives next to `withBinding` in `context.ts` (it's a value-and-type pair); `types.ts` imports the type back via `import type { BindingValue, ... } from "./context.js"`. The import direction stays one-way (`types` ← `context` for types only) so no cycle.
- The `event,` shorthand in `runPrimitives`/`fireOnCapturedHooks` was easy to miss when grepping for `event: undefined`. Confirmed both the existing `event,` shorthand sites and added `bindings,` / `bindings: new Map()` adjacent.
- `Square` is `number` per `schema.ts` — covered by the `number` arm of `BindingValue`. No need for a separate arm.
- A callsite-counting tip for future "add a required ctx field" tasks: `grep -rn ": PrimitiveApplyContext = {" packages/chess/src/` finds every literal construction; `event: undefined,` (and `event,`) catches both default and threaded-event sites uniformly.
## [2026-04-26T08:52Z] T14 validator: imperative-in-passive + chooser-entity stub
### What landed
- `packages/chess/src/modifiers/custom/validate.ts`:
- New module-level `IMPERATIVE_KINDS: ReadonlySet<string>` enumerating
the **10 LOCKED imperative kinds** (T0 ADR): `place-piece,
destroy-piece, move-piece, swap-pieces, convert-piece-type,
set-piece-attr, cancel-capture, spawn-marker, spawn-marker-pair,
destroy-marker`. Adding to or removing from this set is a
plan-amending event — Wave 5 (T21-T27) and Wave 6 (T28-T30)
register these primitives EXACTLY against the names here.
- `walkPrimitiveNodes()` extended with an `inTriggerScope: boolean`
flag threaded through recursion. Top-level invocation passes
`false` (descriptor body is passive scope). Recursion sets `true`
iff the parent kind is `"conditional"` OR matches `/^on-/`. Other
container primitives (e.g. `add-aura`) keep children in passive
scope.
- Imperative-in-passive check fires BEFORE the unknown-kind check,
so descriptors authored against the future Wave 5/6 runtime get
the precise activation-model error today (`descriptor.primitives.imperative-in-passive`).
The unknown-kind error is suppressed for IMPERATIVE_KINDS-named
nodes to avoid double-reporting.
- `packages/chess/src/modifiers/custom/apply.ts`:
- `applyCustomDescriptor` now writes the chooser color stub:
reads `Color` off the target piece, inserts
`LastModifierChooser=<color>` on `PRESET_STATE_ENTITY` (id -1).
This is the V1 stub — when the `apply-modifier` PlayerAction
handler lands (future task), it MUST overwrite this fact with
the actual triggering player's color BEFORE invoking
`applyCustomDescriptor`. Until then, "chooser" === "owner of
target piece", which is the natural reading for self/type-
applied modifiers.
- `packages/chess/src/schema.ts`:
- Added `LastModifierChooser: PieceColor` to `ChessAttrMap`.
- `packages/chess/src/modifiers/apply.ts`:
- Added `registerAttrConsumer("LastModifierChooser")` so the
load-time integrity check sees a consumer.
- `packages/chess/src/modifiers/custom/validate.test.ts`:
- +4 new tests under `describe("imperative-in-passive +
chooser-entity (T14)")`:
1. `destroy-piece` at top-level → REJECTED with code
`descriptor.primitives.imperative-in-passive` (and NO
`primitive.kind.unknown` double-error).
2. `destroy-piece` inside `on-capture.params.primitives` → no
imperative-in-passive error.
3. `spawn-marker` inside `on-turn-start → conditional → then`
→ no imperative-in-passive error.
4. `seed-attribute` carrying `value: { "ctx-attr": { entity:
"chooser", attr: "Color" } }` → validator does NOT reject
the deferred-resolution shape (T12 walker handles runtime).
- `packages/chess/src/schema.test.ts`:
- +1 round-trip test for `LastModifierChooser` on PRESET_STATE_ENTITY.
### Trigger-scope detection rule (LOCKED for T13/T15+)
- Children of a container primitive are in trigger scope iff the
parent kind is `"conditional"` OR starts with `"on-"`. This is a
closed rule — any future trigger primitive MUST either:
(a) match the `/^on-/` naming convention, OR
(b) be added explicitly to the trigger-scope detection in
`walkPrimitiveNodes` (the `childrenInTriggerScope` derivation).
- `add-aura` and any other passive emitter keep children in passive
scope. Currently no passive emitter declares `childPrimitives`, but
the rule is set up to default-passive — the safe assumption.
### Why imperative kinds bypass paramsSchema validation (for now)
- The 10 IMPERATIVE_KINDS aren't in `PRIMITIVE_REGISTRY` yet (Wave
5/6 lands them). The validator early-continues when a node's kind
is in IMPERATIVE_KINDS BUT has no registry entry — skipping
paramsSchema validation + child recursion. Once Wave 5/6 lands
those primitives WITH their schemas, the validator picks them up
via the standard registry-lookup path; the early-continue becomes
unreachable for those kinds.
- The cycle / self-reference scan still runs on imperative-kind
params (it runs BEFORE the registry lookup), so structural
hazards are caught even pre-Wave-5.
### Chooser tracking attr — name + location (FOR T12 / future apply-modifier handler)
- **Attr name**: `LastModifierChooser` (typed `PieceColor` =
`"white" | "black"`).
- **Stored on**: `PRESET_STATE_ENTITY` (id -1).
- **Written by**: `applyCustomDescriptor` in
`packages/chess/src/modifiers/custom/apply.ts` (lines ~50-55), at
the START of every descriptor application (BEFORE the primitive
walk). Read by the future T12 param walker for
`ctx-attr: { entity: "chooser", attr: "Color" }` resolution.
- **Consumer registration**: `packages/chess/src/modifiers/apply.ts`
appended right after T8's KingExtraReach.
- **Open hole** (deferred to apply-modifier action handler task):
the stub uses target piece's owner as proxy for chooser. When the
`apply-modifier` PlayerAction lands, it must write the actual
initiating player's color to `LastModifierChooser` BEFORE calling
`applyCustomDescriptor` so the stub's piece-color fallback is
superseded.
### Subtleties / gotchas
- `seed-attribute` is the only existing primitive with `z.unknown()`
on its `value` param — it's the natural carrier for the T14
ctx-attr-shape recognition test. Other primitives' Zod schemas
(e.g. `add-to-attribute.delta` is `z.number()`) would reject an
object value at the schema-validation stage, BUT the T12 param
walker is supposed to run BEFORE Zod validation, so that breakage
surfaces only when those schemas are exercised post-T12. T14's
test stays scoped to `seed-attribute` to avoid leaking into T12's
problem space.
- Existing `enforces max nesting depth of 3 container levels` test
uses a `destroy-piece`-free deeply-nested tree, so the new
imperative-in-passive check doesn't perturb it. Confirmed all 16
pre-existing validate.test.ts tests still pass byte-identical.
- `inTriggerScope: false` at top-level means a passive descriptor
body that's PURELY imperative (e.g. just a `destroy-piece` at
index 0) gets ONE error per offending node, not a tree of
errors — the early-continue prevents recursion into a kind that
isn't even registered.
- The chooser stub `applyCustomDescriptor` also needs to import
`PRESET_STATE_ENTITY` and `PieceColor` from `../../schema.js` —
those weren't previously imported in `custom/apply.ts`. Added
`import { PRESET_STATE_ENTITY, type PieceColor } from "../../schema.js"`.
### Verification
- `bun test packages/chess/src/modifiers/custom/validate.test.ts` →
20 pass / 0 fail / 36 expects (16 existing + 4 new T14).
- `bun test packages/chess/src/schema.test.ts` → 24 pass / 0 fail
/ 84 expects (23 existing + 1 new T14 chooser-attr round-trip).
- `bun run check` → **exit 0, 2024 tests across 171 files** (was
2019 after T11; +5 from T14 = 4 validate + 1 schema).
- LSP diagnostics: clean on validate.ts, validate.test.ts,
custom/apply.ts, schema.ts, modifiers/apply.ts, schema.test.ts.
- Evidence: `.sisyphus/evidence/task-14-validator.txt`
## [2026-04-26T09:06:29-06:00] T13 binding-scope validator
### What landed
- `packages/chess/src/modifiers/custom/validate.ts`:
- `BINDING_INTRODUCING_KINDS: ReadonlyMap<string, string>` — 8
LOCKED entries enumerating future binder primitives + their
bind-name param key:
`for-each-piece, for-each-square, for-each-adjacent,
for-each-marker, for-column, for-row, random-pick,
request-choice` — all map to `"bind"`.
- `BINDING_CHILD_SLOTS: ReadonlySet<string>` — 3 child-slot
names where the extended scope applies: `then, else, primitives`.
Mirrors the structural-slot convention used by `conditional`
(then/else) and trigger primitives (primitives).
- `walkBindingScope(node, inScope, errors, path)` — INDEPENDENT
second pass over the raw node tree (does NOT use
PRIMITIVE_REGISTRY child enumeration; binders aren't registered
yet). Splits a binder's params: child slots see the EXTENDED
scope (`new Set([...inScope, newName])`); non-child params
(filter, condition, count, etc.) see the OUTER scope. This
lexical-scope rule means a binder's `filter` cannot reference
its own bound name — exactly mirrors function-parameter scope.
- `checkParamsForVarRefs(value, inScope, errors, path, seen?)` —
recursive deep scan with cycle guard (the existing
`scanParamsForCyclesAndSelfReference` reports the structural
cycle separately; T13's walker just bails on `seen.has(value)`).
- `packages/chess/src/modifiers/custom/validate.test.ts`:
- +4 tests under `describe("binding-out-of-scope (T13)")`:
1. `$var` at descriptor top → REJECTED with code
`descriptor.primitives.binding-out-of-scope` + message
containing `(none)` and `$X`.
2. `$var` inside a synthetic `for-each-piece.then` → no
binding-out-of-scope error (other errors like unknown-kind
may still fire, that's OK).
3. Shadowing: inner `for-each-piece` re-binds `p` from outer →
no binding-out-of-scope error inside the inner `then`.
4. **Lexical scope guardrail**: `$p` inside the SAME binder's
`filter` (non-child slot) → REJECTED. Confirms the binder's
own non-child params see only OUTER scope.
### Walker integration approach — SEPARATE function
- Did NOT combine with T14's `walkPrimitiveNodes`. Rationale:
`walkPrimitiveNodes` recurses via
`primitiveDescriptor.childPrimitives(...)` — a registry-driven
child-enumeration that returns `[]` for unregistered kinds. The
binders in BINDING_INTRODUCING_KINDS are ALL unregistered today
(Wave 5/7/8 lands them), so a registry-driven walker would never
descend into their `then`/`else`/`primitives` slots. T13 must
walk the raw node tree directly via the structural slot names.
- Two separate top-level invocations in `validateCustomDescriptor`:
1. `walkPrimitiveNodes(...)` (T14 — registry-driven, threads
`inTriggerScope`)
2. `walkBindingScope(...)` per top-level node (T13 — structural,
threads `inScopeBindings`)
- This decoupling means T13 doesn't need to coordinate with T14's
registry-traversal logic at all. Each pass owns its own concern;
errors aggregate into the same `errors` array.
### Coordinated $var-shape detection (T12 ↔ T13 contract)
- The exact key check is `keys.length === 1 && "$var" in obj &&
typeof obj.$var === "string"`. T12's runtime param-resolver MUST
use the IDENTICAL check, otherwise the validator-runtime contract
breaks (a descriptor that validates clean would still throw at
runtime, or vice versa).
- Objects like `{ $var: "X", default: 0 }` are NOT $var refs by
this check — they get walked structurally. Future
`$var-with-default` extension can be added without breaking the
current shape.
### Subtleties
- **Cycle guard required**: T13's walker recurses into nested params
before any shape check, so the existing `t.circular` test fixture
blew the stack until I added a `seen: Set<object>` param defaulting
to a fresh set per top-level invocation.
- **Empty top-level scope**: descriptor.primitives sees
`new Set<string>()` — no $var refs are valid until a binder
brings a name into scope. The error message includes
`In-scope bindings: [(none)]` for top-level violations.
- **Set immutability for shadowing**: `new Set([...outer, name])`
creates a fresh set per scope; the caller's set is never
mutated. Outer scope is restored automatically when the inner
walk returns — no manual stack push/pop needed.
- **Path threading**: error paths are full
`["primitives", i, "params", "value"]`-style arrays so UI can
highlight the exact offending $var ref.
### Verification
- `bun test packages/chess/src/modifiers/custom/validate.test.ts`
→ 24 pass / 0 fail / 44 expects (was 20 / 36 pre-T13).
- `bun run check` → exit 0, **2048 tests across 172 files** (was
2024 after T14; +24 = T13 +4 + parallel tasks landing the rest).
- LSP diagnostics: clean on validate.ts and validate.test.ts.
- Evidence: `.sisyphus/evidence/task-13-binding-scope.txt`
## [2026-04-26T09:06:00Z] T12 param resolver
### What landed
- **NEW** `packages/chess/src/modifiers/primitives/param-resolver.ts` (~210 lines):
- `export class BindingError extends Error` — thrown when `{ $var: name }`
references an unbound name. Carries the unbound name + lists every
binding currently in scope in the message. **T13 should import
`BindingError` from this module** for static `$var` checks (the
runtime path already throws this exact class).
- `export function resolveParams(params: unknown, ctx: PrimitiveApplyContext): unknown`
— recursive walker. Returns NEW value, never mutates.
- **NEW** `packages/chess/src/modifiers/primitives/param-resolver.test.ts` (20 tests):
- 3 no-op tests (plain primitives / objects / arrays)
- 4 `$var` tests (success, unbound BindingError, message lists `$missing`/`$a`/`$b`, `(none)` when empty)
- 7 `ctx-attr` tests (self / chooser-set / chooser-unset / chooser-no-king / numeric id / nested $var / unset attr)
- 4 `ctx-build` tests (literal e4=28, $var col+row, out-of-range throws `0..7`, non-integer)
- 2 deep-walk tests (nested resolution at arbitrary depth, multi-key plain object NOT matched)
- **MOD** `packages/chess/src/modifiers/triggers.ts` (~line 152, `runPrimitives`):
```ts
const resolvedParams = resolveParams(node.params, ctx);
primitive.apply(ctx, resolvedParams);
```
Resolution happens BEFORE `primitive.apply`. `childPrimitives()`
introspection still uses the ORIGINAL unresolved params (structural
shape is independent of runtime values).
- **MOD** `packages/chess/src/modifiers/custom/apply.ts` `runPrimitive()` (~line 163):
symmetric wiring at the profile-time apply path.
### Three resolved shapes (LOCKED API for T13/Wave 5+)
```ts
// 1. Binding ref
{ $var: "name" }
→ ctx.bindings.get("name") // throws BindingError if unbound
// 2. Context attribute lookup
{ "ctx-attr": { entity, attr } }
→ ctx.session.get(resolvedEntityId, attr)
// entity ∈ "self" | "chooser" | numeric EntityId | { $var: "..." }
// throws if attr is undefined or chooser-resolution fails
// 3. Square computation
{ "ctx-build": { col, row } }
→ col + row * 8 // Square (0..63)
// col / row may themselves be { $var } shapes
// throws if col/row out of [0..7] or non-integer
```
### Single-key recognition rule
A magic shape ONLY matches when the object has EXACTLY one key. So
a primitive author who legitimately stores a field literally named
`$var` alongside other fields is NEVER ambiguously rewritten.
Pinned by `does NOT match shape when the magic key is one of
multiple keys` test.
### Chooser-entity resolution (T14 collaboration)
- `ctx-attr.entity = "chooser"` reads `LastModifierChooser` (PieceColor)
off `PRESET_STATE_ENTITY` (T14 stub stored by `applyCustomDescriptor`).
- Walker then finds king of that color, returns its EntityId; attr
lookup happens against THAT king id.
- Refinement opportunity for the apply-modifier action: store actual
chooser piece id so resolution doesn't fall back to "king of color".
### Backward compatibility (regression-pinned)
- 22 existing primitives store ZERO objects with `$var` / `ctx-attr` /
`ctx-build` as their sole key, so the walker is a structural-clone
no-op for their params. Verified: 223/223 primitive tests pass
byte-identical (561 expects).
### Wiring sites — definitive list
1. `packages/chess/src/modifiers/triggers.ts:152` (`runPrimitives`)
2. `packages/chess/src/modifiers/custom/apply.ts:163` (`runPrimitive`)
Both are the SAME apply pipeline at different entry points (trigger
dispatch vs profile-time descriptor walk). Future entry points
(e.g. T47 request-choice resume) MUST also call `resolveParams`
before `primitive.apply` — no central interceptor.
### Verification
- `bun test packages/chess/src/modifiers/primitives/param-resolver.test.ts` → 20 pass / 0 fail / 38 expects
- `bun test packages/chess/src/modifiers/primitives/` → 223 pass / 0 fail / 561 expects (regression intact)
- `bun run check` → **exit 0, 2048 tests across 172 files** (was 2024 after T14; +24)
- LSP diagnostics: clean on param-resolver.ts, param-resolver.test.ts, triggers.ts, custom/apply.ts
- Evidence: `.sisyphus/evidence/task-12-param-resolver.txt`
### Subtleties / gotchas
- `runPrimitives` calls `primitive.apply(ctx, resolvedParams)` but
`primitive.childPrimitives(node.params)` (ORIGINAL params) — because
`childPrimitives` introspects the structural shape, and children
resolve their own params on recursion (when iteration bindings are
in scope).
- `BindingError` message format: `Binding '$NAME' is not in scope.
Available bindings: $a, $b.` (or `(none)` when empty). T13 should
produce the same shape so users see consistent messages whether
the failure is caught at validation or runtime.
- `walk()` recurses into resolver-shape values: a `$var` that resolves
to an object containing another `$var` IS walked again. Documented
(not a bug). Iteration primitives bind primitive types (EntityId /
Square / readonly EntityId[] / string / boolean), so the recursion
is a no-op in practice.
- Test fixtures: `Session.nextId()` must be called before hard-coding
`id=2` — mirrors the pattern in `seed-attribute.test.ts`.
### Hand-off notes for T13 / downstream
- **T13** (static var-ref validator): import `BindingError` from
this module if it wants to throw the same class for static
unbound-ref errors. Validator's static analysis can borrow the
shape-detection rules verbatim (single-key match for `$var`,
`ctx-attr`, `ctx-build`).
- **Wave 5+ imperative primitives**: square selectors / entity refs
use `ctx-build` / `ctx-attr` shapes. Walker resolves before the
imperative primitive's `apply` sees them — primitives can assume
params are plain values.
- **T36/T37 (RNG)**: `bindAs` outputs flow into `ctx.bindings`;
downstream primitives read via `{ $var: bindAs }` through this
walker.
- **T47 (request-choice)**: bindings restored from `PendingChoice`
deserialisation are visible to subsequent primitives via standard
`$var` lookup.

View file

@ -775,7 +775,7 @@ Max Concurrent: 8 (Waves 5+6+7+9 overlap)
- Message: `feat(chess): marker entity factory + priority resolver`
- Files: `packages/chess/src/engine.ts`, `packages/chess/src/engine.test.ts`
- [ ] 11. Binding scope stack on PrimitiveApplyContext
- [x] 11. Binding scope stack on PrimitiveApplyContext
**What to do**:
- Edit `packages/chess/src/modifiers/primitives/context.ts`: extend `PrimitiveApplyContext` with `bindings: ReadonlyMap<string, EntityId | readonly EntityId[] | Square | number | string>`
@ -817,7 +817,7 @@ Max Concurrent: 8 (Waves 5+6+7+9 overlap)
- Message: `feat(chess): binding scope on PrimitiveApplyContext`
- Files: `packages/chess/src/modifiers/primitives/context.ts`, `context.test.ts`
- [ ] 12. Param walker resolves {$var}, {ctx-attr}, {ctx-build} shapes
- [x] 12. Param walker resolves {$var}, {ctx-attr}, {ctx-build} shapes
**What to do**:
- Create `packages/chess/src/modifiers/primitives/param-resolver.ts` exporting `resolveParams(params: unknown, ctx: PrimitiveApplyContext): unknown` — recursively walks params, substituting:
@ -861,7 +861,7 @@ Max Concurrent: 8 (Waves 5+6+7+9 overlap)
- Message: `feat(chess): param walker for binding/ctx-attr/ctx-build resolution`
- Files: `packages/chess/src/modifiers/primitives/param-resolver.{ts,test.ts}`, `packages/chess/src/modifiers/triggers.ts`
- [ ] 13. Validator: binding-ref-out-of-scope error
- [x] 13. Validator: binding-ref-out-of-scope error
**What to do**:
- Edit `packages/chess/src/modifiers/custom/validate.ts`: add a binding-scope walker that builds a binding-name set per primitive subtree and rejects any `{ $var: "X" }` reference where `X` not in scope
@ -893,7 +893,7 @@ Max Concurrent: 8 (Waves 5+6+7+9 overlap)
- Message: `feat(validator): binding-scope check`
- Files: `validate.ts`, `validate.test.ts`
- [ ] 14. Validator: imperative-in-passive + chooser-entity activation context
- [x] 14. Validator: imperative-in-passive + chooser-entity activation context
**What to do**:
- Edit validate.ts: walk descriptor tree; if a primitive whose `kind` is in IMPERATIVE_KINDS set (place-piece, destroy-piece, move-piece, swap-pieces, convert-piece-type, set-piece-attr, cancel-capture, spawn-marker, spawn-marker-pair, destroy-marker) appears OUTSIDE a trigger's `primitives` or `then`/`else` array → reject with error code `descriptor.primitives.imperative-in-passive`