feat(thressgame-100): Wave 1 partial — resolver V3 + add-to-attribute.target
Wave 1 (W1.0–W1.6) of the thressgame-100 epic — push ThressGame coverage from
27 % toward 85 %+ via resolver expressiveness. Foundation for Layer-1 rules
(self-targeting destroys, mass-mover, adjacent splash). Recipes (W1.7–W1.12)
land in subsequent commits.
Resolver V3 (param-resolver.ts) — 6 new shapes:
- {ctx-self-id: null} → ctx.pieceId
- {ctx-self-marker-id: null} → ctx.markerId (throws when undefined)
- {add: [<resolver>, <int>]} → recursive arithmetic, MAX_SAFE_INTEGER overflow throws
- {sub: [...]}, {mul: [...]}, {mod: [...]} — same pattern; mod uses positive-modulo
formula ((l % r) + r) % r so column-wrap recipes work for any sign of l
V3 union order locked (param-resolver-schema.ts):
[literal, $var, ctx-attr, ctx-build, ctx-self-id, ctx-self-marker-id, add, sub, mul, mod]
PrimitiveApplyContext (types.ts): added optional readonly markerId? field.
runPrimitives (triggers.ts): populates markerId in ctx for piece-entered-marker
and marker-expire events from event.markerId (single source of truth).
W1.6 — add-to-attribute.target:
- Schema gains optional target?: numberOrResolver({ min: 0 }) field
- apply() resolves target then defaults to ctx.pieceId when undefined
- Closes the long-documented adjacent-splash sharp edge — splash damage now
expressible via target redirection instead of forcing set-piece-attr
Test surface:
- param-resolver.test.ts: +22 tests (39 total) — overflow boundary, recursive
nesting, mixed shapes, replay determinism, ctx.markerId failure modes
- param-resolver-schema.test.ts: +19 tests (38 total) — V3 union for each helper,
arithmetic shape parsing, ctx-self-id payload validation
- add-to-attribute.test.ts: +6 tests (10 total) — target literal, target $var,
target omitted (backward-compat), reject string target, apply with/without target
- ParamField snapshot regenerated for add-to-attribute.target rendering
bun run check: 2983 tests pass (was 2941, +42).
Plan: .sisyphus/plans/thressgame-100.md (5 waves + cross-ref + final verification,
~73 atomic tasks locked end-to-end).
Notepads: .sisyphus/notepads/thressgame-100/
Locked architectural decisions (irrevocable across all 6 waves):
A. Arithmetic resolver shapes — arity 2, no comparisons, no booleans
B. Self-targeting via ctx-self-id / ctx-self-marker-id (NOT 'self' literal)
C. add-to-attribute.target optional, defaults to ctx.pieceId
D. Multi-turn countdowns via on-attr-expire trigger (Wave 2)
E. Piece-pair lifecycle via PieceLink + on-piece-pair-link-broken (Wave 4)
F. Resource accumulation on GAME_ENTITY (Wave 5)
G. Board topology via BoardTopology attr (Wave 4)
H. Validator V3 — superset of V2, all V2 fixtures auto-validate
J. User-explicit overrides — no backward-compat constraint, no time/cost limit
This commit is contained in:
parent
34655ddadd
commit
6a38be6fc6
13 changed files with 1812 additions and 10 deletions
59
.sisyphus/notepads/thressgame-100/decisions.md
Normal file
59
.sisyphus/notepads/thressgame-100/decisions.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# thressgame-100 — Locked Architectural Decisions
|
||||
|
||||
## Locked at plan-write time (irrevocable across all 6 waves)
|
||||
|
||||
### A. Arithmetic resolver shapes (Wave 1)
|
||||
- Add `add`, `sub`, `mul`, `mod` shapes. Arity = 2. Operands = `(resolver | integer)`.
|
||||
- Range-check at `apply()`-time only (validator just checks shape).
|
||||
- NO comparison ops, NO boolean logic, NO conditionals inside resolver shapes.
|
||||
- Union order in V3: `[literal, $var, ctx-attr, ctx-build, ctx-self-id, ctx-self-marker-id, add, sub, mul, mod]`
|
||||
|
||||
### B. Self-targeting (Wave 1)
|
||||
- New shape `{ "ctx-self-id": null }` returns `ctx.pieceId` directly.
|
||||
- New shape `{ "ctx-self-marker-id": null }` returns `ctx.markerId` (only valid inside marker triggers).
|
||||
- NOT adding `"self"` literal branch to numeric schemas.
|
||||
|
||||
### C. `add-to-attribute` gains `target` field (Wave 1)
|
||||
- Optional. Defaults to `ctx.pieceId` when omitted (backward-compatible default).
|
||||
- When present, accepts `numberOrResolver()` (V3 union).
|
||||
|
||||
### D. Multi-turn countdowns (Wave 2)
|
||||
- Generalize `MarkerLifetime` to per-piece attrs. `set-piece-attr.lifetime` already accepted.
|
||||
- New trigger `on-attr-expire(target, attr)` fires on countdown-zero.
|
||||
- New primitive `decrement-attr-each-turn(target, attr)` for explicit countdown control.
|
||||
- New dispatcher stage 13 (after stage 12 `fireOnTurnStartHooks`), batched per turn boundary.
|
||||
|
||||
### E. Piece-pair lifecycle (Wave 4)
|
||||
- New attr `PieceLink` (per-piece, list of EntityIds).
|
||||
- New trigger `on-piece-pair-link-broken`.
|
||||
- New primitives `link-pieces(a, b)`, `unlink-pieces(a, b)`.
|
||||
|
||||
### F. Resource accumulation (Wave 5)
|
||||
- New attrs `WhiteScore`, `BlackScore` on GAME_ENTITY. Default 0.
|
||||
- New primitives `add-resource(player, amount)`, `spend-resource(player, amount, then, else)`.
|
||||
- New trigger `on-resource-changed(player, threshold, direction)`.
|
||||
|
||||
### G. Board topology (Wave 4)
|
||||
- New attr `BoardTopology: "standard" | "wrap-files" | "wrap-all"` on GAME_ENTITY. Default `"standard"`.
|
||||
- New primitive `set-board-topology(value)`.
|
||||
- Standard topology = no regression (move-gen check is opt-in).
|
||||
|
||||
### H. Validator V3
|
||||
- All `numberOrResolver` / `enumOrResolverFor` callsites widen to V3 union (mechanical).
|
||||
- V3 is a superset of V2 (existing fixtures auto-validate clean).
|
||||
|
||||
### I. Test gates per wave
|
||||
- `bun run check` exit 0
|
||||
- Every new recipe ships with `*-real.test.ts` proving end-to-end runtime
|
||||
- Every wave ships ≥1 new e2e Playwright spec
|
||||
- Determinism property tests for every new primitive (N=100, byte-identical state hash)
|
||||
|
||||
### J. User-explicit overrides (locked)
|
||||
- "No backward-compat constraint" — schemas can break. Update existing tests rather than preserving.
|
||||
- "No time/cost limit" — execution may take as long as needed; reviewer cycles can iterate.
|
||||
- "Just fucking get it all done" — no premature optimization, no scope reduction, ship complete waves.
|
||||
|
||||
### K. Coverage accounting (locked numbers)
|
||||
- Effective denominator = 51 ThressGame rules (after removing 6 stubs + 8 preset-shaped from raw 65)
|
||||
- Wave-end coverage: W1 27/51 (53%) → W2 37/51 (72%) → W3 45/51 (88%) → W4 50/51 (98%) → W5 51/51 (100%)
|
||||
- 85% target hit at end of W3; W4-W5 are completeness-driven not target-driven
|
||||
10
.sisyphus/notepads/thressgame-100/issues.md
Normal file
10
.sisyphus/notepads/thressgame-100/issues.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# thressgame-100 — Issues / Gotchas
|
||||
|
||||
(Empty at plan-write time. Append findings as work proceeds.)
|
||||
|
||||
## Format
|
||||
|
||||
```
|
||||
## [TIMESTAMP] Wave/Task: W<N>.<M>
|
||||
{description of issue, workaround, or open question}
|
||||
```
|
||||
52
.sisyphus/notepads/thressgame-100/learnings.md
Normal file
52
.sisyphus/notepads/thressgame-100/learnings.md
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# thressgame-100 — Inherited Wisdom
|
||||
|
||||
## From thressgame-coverage epic (foundation, Atlas waves 0-19)
|
||||
|
||||
- **Test command**: `bun run check` — NOT `bun test` (hits stale dist/)
|
||||
- **TS project references**: `bunx tsc -b --force packages/chess` regenerates `dist/index.d.ts`
|
||||
- **Playwright helper**: ALWAYS `.sisyphus/scripts/run-pw.sh <log> <args>`. NEVER `bunx playwright test` directly (times out agent runtime)
|
||||
- **NEVER set `CI=true`** in helper — flips `reuseExistingServer: false`, collides with docker compose dev stack
|
||||
- **Docker stack**: `docker-compose.dev.yml` runs server :7357 + web :5173. Verify with `docker compose -f docker-compose.dev.yml ps`
|
||||
- **Test-only WS frames** in `broadcast.ts`: `__test__.activate-descriptor`, `__test__.apply-descriptor` (gated `NODE_ENV !== "production"`)
|
||||
- **Dev-only debug hook**: `globalThis.__paratypeChessClient` (gated `import.meta.env.DEV`) — use from Playwright for engine introspection
|
||||
- **DOM selectors**: `[data-square="e4"]`, `[data-piece="white-pawn"]`, `[data-piece-id]`, `[data-marker-kind="frozen-square"]`, `[data-testid="request-choice-modal"]`, `[data-choice-kind="..."]`
|
||||
- **Marker priorities (locked)**: portal-end=1 < frozen-square=2 < mine=3 < pit=4 < death-square=5 < tornado=6 < treasure=7 < blocked=8
|
||||
- **Cascade depth limit = 8** (RUNTIME_DEPTH_HARD_CAP); error code `runtime.cascade-depth-exceeded`
|
||||
- **Choice stack max depth = 8**; error code `runtime.choice-depth-exceeded`
|
||||
- **Mulberry32 PRNG**: deterministic; seeded via `engine.rng()` advancing `RngStream` on `GAME_ENTITY`
|
||||
|
||||
## From thressgame-templates epic (V2 era, prior wave)
|
||||
|
||||
- **`__resolverEnumValues` discriminator**: `enumOrResolverFor` attaches this directly via `Object.assign`. Survives Zod 4.x parse + `.optional()` wrapping. Read by ParamField at `_def.innerType.__resolverEnumValues`
|
||||
- **Validator iteration-trigger-scope fix** at `validate.ts:325-349`: extended trigger-scope detection to recognize `for-each-*` and `random-pick`
|
||||
- **JSON import in vitest**: `assert { type: "json" }` doesn't work reliably; inline descriptors as TS literals or use `fs.readFileSync(new URL(..., import.meta.url))` — the URL form fails too in vitest. Best: inline.
|
||||
- **Snapshot tests regenerate** with `bunx vitest run path -u` after rendered text changes
|
||||
|
||||
## From the oracle's full-corpus analysis (current epic)
|
||||
|
||||
- **Effective denominator = 51** (65 raw - 6 stubs - 8 preset-shaped)
|
||||
- **Wave 4 = highest regression risk** (topology change touches every move-gen test)
|
||||
- **Wave 5 = paradigm break** (game-level mutable state) — but `GAME_ENTITY` already has mutable state (RngStream, choice timeouts), so it's a NEW pattern only in scope, not architecture
|
||||
- **6 empty-stub rules in ruleHooks.js**: `pawns_with_viagra`, `estrogen`, `knee_surgery`, `pawns_learned_strength`, plus `parry`/`pacman_style` bodies that defer elsewhere
|
||||
- **8 preset-shaped rules**: `dual_king`, `coregal`, `god_kings`, `early_promotion`, `proletariat`, `short_stop`, `trains_rights`, `pacman_style` — already in `RULES.md` as v2 presets
|
||||
|
||||
## Don'ts (carried from prior waves)
|
||||
|
||||
- Do NOT edit any file in `__fixtures__/parity/` (canonical descriptors)
|
||||
- Do NOT use `Date.now()` (breaks replay determinism)
|
||||
- Do NOT `background_cancel(all=true)` (kills tasks whose results haven't been collected)
|
||||
- Do NOT add `Math.random()` (use `engine.rng()` only)
|
||||
- Do NOT skip the `*-real.test.ts` for new recipes (recipe validation alone is not proof of correctness)
|
||||
|
||||
## [2026-04-26] W1.1-W1.5 — resolver V3
|
||||
|
||||
- **`PrimitiveApplyContext.markerId` did NOT exist pre-task.** Added as `readonly markerId?: EntityId | undefined` (the `| undefined` is REQUIRED with `exactOptionalPropertyTypes: true` even when the field is `?:`). Construction sites that omit it inherit `undefined` cleanly — verified against 60+ test ctx builders without a single edit needed.
|
||||
- **Populated markerId in `runPrimitives` (triggers.ts) from `event.markerId`** for the two marker trigger event kinds (`piece-entered-marker`, `marker-expire`). This is the single source of truth — no other ctx-construction site needs the field today, since profile-time applies and non-marker triggers legitimately have `markerId === undefined`. The `ctx-self-marker-id` resolver throws BindingError-style at that boundary.
|
||||
- **Zod 4.x recursive types via `z.lazy()`**: ArithmeticShape recursively references NumericResolverInput which references ArithmeticShape. Working pattern: declare BOTH as `z.ZodType<unknown>` typed via `z.lazy(() => z.union([...]))`. The order matters — `NumericResolverInput` is declared FIRST (referencing `ArithmeticShape` which is forward-declared via TDZ-safe `z.lazy`), then `ArithmeticShape` is defined. TypeScript's variable-not-yet-initialized warning is silenced by `z.lazy`'s deferred evaluation.
|
||||
- **Helper return type widening**: `numberOrResolver()` now returns `z.ZodType<unknown>` instead of the V2 `z.ZodUnion<readonly [...]>`. The narrow union type is no longer expressible because `ArithmeticShape` is `z.ZodType<unknown>` (recursion). Consumers that care about narrow types downcast at use site — same pattern V2 used internally.
|
||||
- **Locked V3 union order in helpers** (matches `decisions.md` § A): `[literal, $var, ctx-attr, ctx-build, ctx-self-id, ctx-self-marker-id, add, sub, mul, mod]`. `add/sub/mul/mod` collapse to a single `ArithmeticShape` lazy union — but ordering inside is `add, sub, mul, mod`, so introspection order at `_def.options` matches the locked tuple.
|
||||
- **Right operand of arithmetic locked to `z.number().int()`**, NOT another resolver. Keeps overflow risk bounded (multi-resolver chains can still build via nested-on-the-left). Validator catches non-integer right operands at parse time; runtime overflow check at `MAX_SAFE_INTEGER` is the second-line defense.
|
||||
- **Positive modulo formula `((l % r) + r) % r`** is essential for column-wrap recipes (`mod(add($col, 1), 8)`) — JS `%` returns negative for negative left. Tested via `mod(-1, 8) === 7`.
|
||||
- **`ctx-self-id` / `ctx-self-marker-id` payload locked to `null`** (not `true`, `0`, `""`). Forces explicit shape in JSON descriptors. Tested at both validation (Zod `z.null()`) and runtime (walker `inner !== null` throw).
|
||||
- **Test count delta**: param-resolver.test.ts 17 → 39 tests (+22). param-resolver-schema.test.ts 19 → 38 tests (+19). All 2983 tests in `bun run check` pass.
|
||||
- **18 obsolete ParamField snapshots** were observed in the test output — pre-existing, NOT caused by this task. Confirmed by checking obsolete count is identical to fresh-write count from prior run.
|
||||
10
.sisyphus/notepads/thressgame-100/problems.md
Normal file
10
.sisyphus/notepads/thressgame-100/problems.md
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# thressgame-100 — Unresolved Blockers
|
||||
|
||||
(Empty at plan-write time. Promote items here from issues.md when blocked beyond 3 retries.)
|
||||
|
||||
## Format
|
||||
|
||||
```
|
||||
## [TIMESTAMP] Wave/Task: W<N>.<M> — BLOCKED
|
||||
{description, blocking dependency, what's needed to unblock}
|
||||
```
|
||||
Loading…
Add table
Add a link
Reference in a new issue