Skip to content

Commit 71e7364

Browse files
W89 — A Game-Length Life (#376)
Closes #370. ## What was built The first evidence that the simulation kind survives being played as a game rather than as a test. Two engine-owned, unpublished regression fixtures under `src/engine/src/campaigns/long-horizon.ts` (permitted by `20-contract.md` §19), sharing one job market, course, item, event pair, opportunity, achievement and headline: - **`long-horizon-win`** — reaches `goals_met` around week 153-156 via `player.career.totalWeeksEmployed` hitting 150 and holding two weeks. - **`long-horizon-loss`** — reaches `failed` via the eviction ladder (`goalFailurePrecedence: "failure_wins"`), the terminal path no short fixture had walked before. `check-content.ts`'s campaign catalogue now imports and registers `buildLongHorizonWinCampaign` — needed so the content-validation script can author/build it the way it does every other fixture campaign; `long-horizon-loss` isn't registered there because it's exercised only by its own replay test, mirroring the catalogue's existing one-entry-per-file relationship to other multi-fixture campaign files (e.g. `stable-life*`). `replay-corpus.ts` needed no edit — it discovers fixtures by filename prefix scan, so dropping the new `long-horizon-*.fixture/outcome.json` pairs into `fixtures/replay/` was sufficient. Both fixtures' action logs were generated, not hand-authored — a scripted weekly policy function drove a live engine week by week (via a temporary, uncommitted generator script), reading current `SimulationKindState` each week to decide the next plan, exactly like `design/30-slices.md`'s own W89 sizing argument requires. ## W89.1-W89.8 status - **W89.1** ✅ `long-horizon-win` plays 150+ weeks to a terminal `outcome()`, frozen in the corpus like every other entry. - **W89.2** ✅ Both win and loss committed; the loss ends via the eviction ladder, not a goal-failure condition. - **W89.3** ✅ All 27 dispatched `ActionType`s resolve at least once across the two runs, asserted against `RESOLVER_TABLE`'s non-stub entries. - **W89.4** ✅ (with a logged caveat) 14 of the 15 end-of-week systems are observed acting: 13 non-relationship/non-week_limit systems across the win+loss runs, plus `week_limit_reached` via an isolated third side-test campaign (a short scenario with no goals — neither long-horizon run can reach it without contradicting W89.2's own terminal paths). `relationships` is excluded by name and logged as a decision: no weekly relationship rule is specified anywhere in `10-simulation-kind.md`, so the system can never emit anything beyond `system.ran`. - **W89.5** ✅ Byte-identical on replay under the determinism harness, and byte-identical between a continuous session and a save/restore split partway through (`engine.serialize`/`.deserialize` mid-run). - **W89.6** ✅ Serialized size and unbounded-collection counts (week one vs. final week) are asserted with stated ceilings, not printed. - **W89.7** ✅ Every previously-committed replay outcome is unchanged byte-for-byte (verified by the full `npm test` run — all 80 suites / 1193 tests green, including every existing kind's own corpus-match loop). - **W89.8** ✅ `npm run typecheck`, `npm run lint`, `npm test` all pass from `src/engine/`. ## Decisions logged (design/90-decisions.md, mirrored into docs/docs/engine/OPEN-QUESTIONS.md) 1. **`relationships` stays the simulation kind's only remaining end-of-week stub.** No weekly relationship rule (decay, drift, or otherwise) is specified in `10-simulation-kind.md` — §6.11 declares the state and §7.7 the NPC, but nothing names what a week does to either. W89.4's coverage assertion counts 14 systems, not 15, excluding it by name. Revisit when a weekly rule is actually specified (`/contract` work) — the same call W56's own *Out of scope* already made once. 2. **`long-horizon-loss`'s `pendingEventResponses` grows unbounded** — 0 to 37 entries over its own 160-week run — because nothing in `endOfWeek.ts`'s `events` system expires an unanswered `PendingEventResponse`, and the loss run's weekly policy is deliberately inactive so the eviction arithmetic stays exact. This is a real instance of the exact defect shape W89 exists to find, found and deliberately **not fixed** here per W89's own out-of-scope line. W89.6's ceiling assertion states this as an observed fact of the fixture, not a claim of boundedness. Revisit by giving `PendingEventResponse` an expiry or a default-declined resolution — a content/contract decision, not a slice-sized fix. Neither is an oversight — both are the interpretations this unit's own instructions named in advance, applied and recorded rather than re-litigated. ## Verified - `cd src/engine && npm run typecheck` — pass. - `cd src/engine && npm run lint` — pass. - `cd src/engine && npm test` — pass (80 test files, 1193 tests), confirming W89.5 and W89.7. - `./build/ConvertTo-HumanDocumentation.ps1` — run, regenerated `docs/docs/engine/OPEN-QUESTIONS.md` from the `design/90-decisions.md` edit (the only marked-block change this PR makes); diff is exactly the mirrored decision-log entries. - `./build/Test-Documentation.ps1` — **run to completion, exit code 0.** Links, terminology, and generated-doc drift all pass. - `./docs.ps1 -BuildOnly` — not run (Docker-based production build; relying on the PR's own required *Verify Documentation Build* check for that gate). ## Still undecided Both logged decisions above are open follow-ups, not blocking this PR: the `relationships` system needs a weekly rule specified before it can be observed, and `pendingEventResponses` needs an expiry/default-resolution policy before long simulation runs stop growing that collection unboundedly.
2 parents bc74a62 + 68ec922 commit 71e7364

12 files changed

Lines changed: 20426 additions & 6 deletions

design/90-decisions.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,34 @@ fully correct fix, but it touches the shared `applyWorldEffects` interpreter sea
314314
all six call sites). **Accepted as-is for this MVP slice** — the event is debug severity
315315
and `scenario` is the only caller reading `.applied` today. Revisit if a second caller
316316
starts reading `.applied`, or as part of whatever unit closes #349.
317+
318+
**`relationships` (`endOfWeek.ts`) is the simulation kind's only remaining end-of-week
319+
stub, and W89 is the first thing able to observe that it stays one.** No weekly
320+
relationship rule — decay, drift, or otherwise — is specified anywhere in
321+
[`10-simulation-kind.md`](10-simulation-kind.md): §6.11 declares the state and §7.7 the
322+
NPC, but nothing names what a week does to either, so the system can never emit anything
323+
beyond `system.ran` (`resolvers.ts`'s own `socialize` is the only thing that moves a
324+
`RelationshipState`, and only on the player's own action). W89's coverage
325+
assertion (`long-horizon.replay.test.ts`) therefore reaches fourteen of the fifteen —
326+
`relationships` is excluded by name rather than silently passed. Thirteen of those
327+
fourteen are asserted over the two long runs together; `week_limit`, which neither long run
328+
can reach without contradicting W89.2's two terminal paths, is the fourteenth and has its
329+
own isolated test in the same file. **Revisit when** a weekly
330+
relationship rule is actually specified; writing one is `/contract` work, not a slice's
331+
(W56's own *Out of scope* already made this call once).
332+
333+
**`long-horizon-loss`'s `pendingEventResponses` grows unbounded across its own 160-week
334+
run — a real instance of the exact defect shape W89 exists to be the first thing able to
335+
see, found and deliberately not fixed here (W89's own *Out of scope*).** Nothing in
336+
`endOfWeek.ts`'s `events` system (or anywhere else) expires a `PendingEventResponse` that
337+
`respond_to_event` never answers; `long-horizon-loss`'s own weekly policy is deliberately
338+
inactive (`long-horizon.ts`'s header explains why — the eviction-ladder arithmetic has to
339+
stay exact), so it never answers one, and the collection grows from 0 to 37 entries over
340+
the run. `long-horizon.replay.test.ts`'s own W89.6 assertion states this as an observed
341+
ceiling for this fixture, not a claim of boundedness. **Revisit** by giving a
342+
`PendingEventResponse` some expiry (an `expiresAtWeek`, mirroring `Opportunity`'s own
343+
field) or an explicit "declined by default" resolution once its `presentWeek` has passed
344+
by some stated margin — a real content/contract decision, not a slice-sized fix.
317345
---
318346

319347
## 3. Judgement Calls to Revisit (Settled for the MVP)
@@ -1120,3 +1148,25 @@ ledger is one renumbering away from being unexplained.
11201148
Rejected: **Leave it in §19 alone** — the rule is stated and the reasoning is one link away, which is
11211149
defensible today and gets weaker every time the slice ledger is reorganised.
11221150
Reversibility: cheap — documentation only; no code, contract or behaviour change.
1151+
1152+
### 2026-08-24 — The work mirror's own directory retripped the design-state skip guard it predates
1153+
Context: the 2026-08-21 entry above guarded `tools/Test-DesignState.Tests.ps1`,
1154+
`tools/Read-DesignState.Tests.ps1` and `tools/Update-DesignProjection.Tests.ps1`'s self-referential
1155+
Describe blocks with `-Skip:` computed from whether `design/state/` exists, on the reasoning that this
1156+
repository's compatibility promise (2026-08-19) leaves it unmigrated. `c26b803`/`c8c2e29` (work-mirror
1157+
sync) then started writing `design/state/work/*.md` — a real, intentional adoption of the WorkRef
1158+
record shape, unrelated to full design-state migration — which made the directory exist and flipped
1159+
the guard, un-skipping 9 tests that immediately failed against a repository with no `state-index.md`,
1160+
no `Contract`/`Unit`/`Invariant`/`Decision` records, and no `Check the design state against the tree`
1161+
CI step. `verify.yml`'s "Run Pester tests" step went red on `main` at `7c86a22` as a result; the prior
1162+
session's `.claude/verify-report.json` entry on this branch's predecessor state (commit `8128a0f`)
1163+
already isolated the failures to this cause without fixing it.
1164+
Chosen: point the three guards at `design/state-index.md` instead of the `design/state/` directory —
1165+
the file the self-tests (S16.5) already treat as the migration's own marker, and one `design/state/`
1166+
can no longer answer now that a subdirectory of it has a second, legitimate occupant.
1167+
Rejected: **Exclude `design/state/work/` from the guard's Test-Path instead** — couples a generic
1168+
design-state guard to one specific mirror subdirectory's name, and breaks again the next time
1169+
something else adopts a `design/state/` subpath without adopting the whole migration. **Adopt
1170+
design-state here to satisfy the tests** — the same unscoped-feature-adoption rejection as
1171+
2026-08-21, unchanged by this branch.
1172+
Reversibility: cheap — three `Test-Path` targets, reverted by pointing them back.

docs/docs/engine/OPEN-QUESTIONS.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,34 @@ fully correct fix, but it touches the shared `applyWorldEffects` interpreter sea
304304
all six call sites). **Accepted as-is for this MVP slice** — the event is debug severity
305305
and `scenario` is the only caller reading `.applied` today. Revisit if a second caller
306306
starts reading `.applied`, or as part of whatever unit closes #349.
307+
308+
**`relationships` (`endOfWeek.ts`) is the simulation kind's only remaining end-of-week
309+
stub, and W89 is the first thing able to observe that it stays one.** No weekly
310+
relationship rule — decay, drift, or otherwise — is specified anywhere in
311+
[`10-simulation-kind.md`](10-simulation-kind.md): §6.11 declares the state and §7.7 the
312+
NPC, but nothing names what a week does to either, so the system can never emit anything
313+
beyond `system.ran` (`resolvers.ts`'s own `socialize` is the only thing that moves a
314+
`RelationshipState`, and only on the player's own action). W89's coverage
315+
assertion (`long-horizon.replay.test.ts`) therefore reaches fourteen of the fifteen —
316+
`relationships` is excluded by name rather than silently passed. Thirteen of those
317+
fourteen are asserted over the two long runs together; `week_limit`, which neither long run
318+
can reach without contradicting W89.2's two terminal paths, is the fourteenth and has its
319+
own isolated test in the same file. **Revisit when** a weekly
320+
relationship rule is actually specified; writing one is `/contract` work, not a slice's
321+
(W56's own *Out of scope* already made this call once).
322+
323+
**`long-horizon-loss`'s `pendingEventResponses` grows unbounded across its own 160-week
324+
run — a real instance of the exact defect shape W89 exists to be the first thing able to
325+
see, found and deliberately not fixed here (W89's own *Out of scope*).** Nothing in
326+
`endOfWeek.ts`'s `events` system (or anywhere else) expires a `PendingEventResponse` that
327+
`respond_to_event` never answers; `long-horizon-loss`'s own weekly policy is deliberately
328+
inactive (`long-horizon.ts`'s header explains why — the eviction-ladder arithmetic has to
329+
stay exact), so it never answers one, and the collection grows from 0 to 37 entries over
330+
the run. `long-horizon.replay.test.ts`'s own W89.6 assertion states this as an observed
331+
ceiling for this fixture, not a claim of boundedness. **Revisit** by giving a
332+
`PendingEventResponse` some expiry (an `expiresAtWeek`, mirroring `Opportunity`'s own
333+
field) or an explicit "declined by default" resolution once its `presentWeek` has passed
334+
by some stated margin — a real content/contract decision, not a slice-sized fix.
307335
---
308336

309337
## 3. Judgement Calls to Revisit (Settled for the MVP)

0 commit comments

Comments
 (0)