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:
parent
abe5bf49a8
commit
defe56feb9
36 changed files with 1865 additions and 11 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue