Skip to content

Commit 89a7b41

Browse files
W103 (partial) — Companion Contracts and Ownership Reconciliation (#424)
* W103 (partial) — Companion Contracts and Ownership Reconciliation Closes gaps found while reconciling W103's checklist against the tree and three companion repos (GameOfLife, Platform, Adventures): - W103.1: mirrors GameOfLife's StatusEffect and PendingEventResponse lifecycles (application/expiry, resolution) into 20-contract.md §2.3, the two confirmed missing of the fourteen GameOfLife S6/S7 lifecycles. - W103.4: every retained campaign fixture now states in its own header that it is an unpublished regression fixture and names SubZeroDev.Adventures.Content as publication authority (20-contract.md §19); fixes a stale 04-core.md §19 citation (now an empty stub) in bulgaria-bureaucracy.freeze.test.ts. - W103.5: records the public-vs-private package visibility decision as final in 90-decisions.md (mirrored into the generated OPEN-QUESTIONS.md copy by hand — no pwsh available locally to run the generator). - W103.6: already resolved by prior recorded decisions (90-decisions.md, 2026-08-10) — verified, no changes needed. - W103.7: verified the kit sync landed with all four -Skip: guards intact; closed issue #365. W103.2 and W103.3 remain open: Platform's vendored @subzerodev/service-contract (0.5.0, pinned to engine 0.8.0) predates W99's list_saves/branch_session/delete_save, and Adventures' HTTP layer wraps SessionStore directly rather than routing through the generated contract for catalog/save/branch/delete. Both require changes in SubZeroDev.Platform, SubZeroDev.ServiceContract and SubZeroDev.Adventures that this session has no push access to. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019hn3f2gYqEMoKbLPGFWbUK * Regenerate human docs and stamp the guide after W103 edits build/Test-Documentation.ps1 caught the generated-doc drift my previous commit introduced (PowerShell wasn't available locally when I made that commit, so I mirrored the OPEN-QUESTIONS.md edit by hand and missed the simulation-kind pointer file and the guide's digest stamp). Installed pwsh, ran the real generator instead of hand-editing further: - design/10-simulation-kind.md and docs/docs/engine/10-simulation-kind.md regenerated from design/20-contract.md's new Status Effect / Pending Event Response lifecycle sections. - docs/docs/guide.md restamped with the new design digest. Its content is unchanged: the guide already summarizes effect-expiry timing and pending-event blocking at the level "link back rather than duplicate detail" calls for, and links to Simulation Kind for the rest — the two new lifecycle sections are exactly that contract-level detail. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019hn3f2gYqEMoKbLPGFWbUK --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6e4a7bb commit 89a7b41

20 files changed

Lines changed: 132 additions & 10 deletions

design/10-simulation-kind.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,14 @@
2020

2121
### 2.3 Effects, Opportunities, and Scheduled Events
2222

23+
#### Status Effect Lifecycle
24+
2325
#### Opportunity Lifecycle
2426

2527
#### Scheduled Event Lifecycle
2628

29+
#### Pending Event Response Lifecycle
30+
2731
### 2.4 Goal State
2832

2933
### 2.5 Economy State

design/20-contract.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3680,6 +3680,25 @@ fresh budget. `end_week`, and any `plan.add` other than `respond_to_event`, refu
36803680
`PendingEventResponse` remains unaddressed by the current plan — `event_response_pending`
36813681
(§10, W94).
36823682
3683+
#### Status Effect Lifecycle
3684+
3685+
**Application.** A `StatusEffect` is inserted into `activeEffects` by whichever resolver grants
3686+
it — an item's own effects syncing with `activeEffects` on every resolution (`sourceKind:
3687+
"item"`), an event outcome (`sourceKind: "event"`), or any other source named by
3688+
`StatusEffect.sourceKind`. Insertion goes through one shared function regardless of source:
3689+
`stacking: "refresh"` drops any existing effect with the same `sourceId` before adding the new
3690+
one; `stacking: "stack"` adds alongside whatever is already active. `appliedWeek` is stamped from
3691+
the current week at insertion.
3692+
3693+
**Expiry.** The `effects` step of start-of-week (§12.1), run immediately after the week
3694+
increments and before any other system, removes every effect whose `expiresAtWeek` is strictly
3695+
before the new week — an effect expiring in week 12 still applies throughout week 12 and is
3696+
removed only once the new week moves past it, at the start of week 13. An effect with no
3697+
`expiresAtWeek` is permanent and is never removed by this step; it persists until its own source
3698+
is resynced or replaced (as item effects are, above). Expiry emits `effect.expired` (§11) per
3699+
effect and produces no `StateChange` — there is nothing for a client to undo when a timer
3700+
elapses.
3701+
36833702
#### Opportunity Lifecycle
36843703
36853704
**Generation**, three paths, all producing an `Opportunity` from an `OpportunityDefinition`
@@ -3753,6 +3772,18 @@ not implicit.
37533772
> cancellation path: it fires regardless of anything that happens between scheduling and
37543773
> firing. Content that wants a scheduled event to be cancellable must put it in a chain.
37553774
3775+
#### Pending Event Response Lifecycle
3776+
3777+
**Creation** is stated above: firing a `ScheduledEvent` (or a random roll) that carries choices
3778+
queues a `PendingEventResponse` for presentation at the start of the following week.
3779+
3780+
**Resolution.** A `respond_to_event` action naming the pending entry's id is its only removal
3781+
path: the resolver removes that entry from `pendingEventResponses` as part of the same
3782+
`StateChange` set that records the chosen `choiceId` and applies the choice's costs. There is no
3783+
expiry — a `PendingEventResponse` has no `expiresAtWeek` field, and `end_week` and every other
3784+
`plan.add` refuse outright while one is unaddressed (`event_response_pending`, §10), so it
3785+
cannot be outlived by the calendar the way an `Opportunity` or `ScheduledEvent` can.
3786+
37563787
### 2.4 Goal State
37573788
37583789
```typescript

design/90-decisions.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,15 @@ answer now lives — so a later reader finds the reasoning without re-opening th
5454

5555
Settled as out of MVP scope. Listed so they resurface deliberately, not by accident.
5656

57-
- **The engine package published *public*, and the plans specify private — tracked as
58-
[issue #302](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/302).**
57+
- **Package visibility, decided: public.** `src/engine/package.json` carries no `"private"`
58+
field and publishes to `npm.pkg.github.com`; [Engine Package](/docs/guide/engine-package)
59+
states the choice is deliberate. `plans/39-world-graph-kind-programme.md` and
60+
`plans/40-w41-engine-consumer-boundary.md` both specified private GitHub Packages at the
61+
time they were written and are annotated in place ("What shipped did not honour this") —
62+
left as historical record of the original plan rather than rewritten, since a plan
63+
document records intent at the time, not current fact. This is the final answer;
64+
[issue #302](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/302) tracked
65+
deciding it and is closed by this entry.
5966
- **Provisional simulation numbers** — drift rates, scenario economics, `demandBand`
6067
thresholds, housing-quality formula, travel costs. Need a balancing pass once the sim
6168
harness runs. Tracked as [issue #267](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/267).

docs/docs/engine/10-simulation-kind.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,25 @@ fresh budget. `end_week`, and any `plan.add` other than `respond_to_event`, refu
399399
`PendingEventResponse` remains unaddressed by the current plan — `event_response_pending`
400400
(§10, W94).
401401

402+
#### Status Effect Lifecycle
403+
404+
**Application.** A `StatusEffect` is inserted into `activeEffects` by whichever resolver grants
405+
it — an item's own effects syncing with `activeEffects` on every resolution (`sourceKind:
406+
"item"`), an event outcome (`sourceKind: "event"`), or any other source named by
407+
`StatusEffect.sourceKind`. Insertion goes through one shared function regardless of source:
408+
`stacking: "refresh"` drops any existing effect with the same `sourceId` before adding the new
409+
one; `stacking: "stack"` adds alongside whatever is already active. `appliedWeek` is stamped from
410+
the current week at insertion.
411+
412+
**Expiry.** The `effects` step of start-of-week (§12.1), run immediately after the week
413+
increments and before any other system, removes every effect whose `expiresAtWeek` is strictly
414+
before the new week — an effect expiring in week 12 still applies throughout week 12 and is
415+
removed only once the new week moves past it, at the start of week 13. An effect with no
416+
`expiresAtWeek` is permanent and is never removed by this step; it persists until its own source
417+
is resynced or replaced (as item effects are, above). Expiry emits `effect.expired` (§11) per
418+
effect and produces no `StateChange` — there is nothing for a client to undo when a timer
419+
elapses.
420+
402421
#### Opportunity Lifecycle
403422

404423
**Generation**, three paths, all producing an `Opportunity` from an `OpportunityDefinition`
@@ -472,6 +491,18 @@ not implicit.
472491
> cancellation path: it fires regardless of anything that happens between scheduling and
473492
> firing. Content that wants a scheduled event to be cancellable must put it in a chain.
474493
494+
#### Pending Event Response Lifecycle
495+
496+
**Creation** is stated above: firing a `ScheduledEvent` (or a random roll) that carries choices
497+
queues a `PendingEventResponse` for presentation at the start of the following week.
498+
499+
**Resolution.** A `respond_to_event` action naming the pending entry's id is its only removal
500+
path: the resolver removes that entry from `pendingEventResponses` as part of the same
501+
`StateChange` set that records the chosen `choiceId` and applies the choice's costs. There is no
502+
expiry — a `PendingEventResponse` has no `expiresAtWeek` field, and `end_week` and every other
503+
`plan.add` refuse outright while one is unaddressed (`event_response_pending`, §10), so it
504+
cannot be outlived by the calendar the way an `Opportunity` or `ScheduledEvent` can.
505+
475506
### 2.4 Goal State
476507

477508
```typescript

docs/docs/engine/OPEN-QUESTIONS.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,15 @@ answer now lives — so a later reader finds the reasoning without re-opening th
4444

4545
Settled as out of MVP scope. Listed so they resurface deliberately, not by accident.
4646

47-
- **The engine package published *public*, and the plans specify private — tracked as
48-
[issue #302](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/302).**
47+
- **Package visibility, decided: public.** `src/engine/package.json` carries no `"private"`
48+
field and publishes to `npm.pkg.github.com`; [Engine Package](/docs/guide/engine-package)
49+
states the choice is deliberate. `plans/39-world-graph-kind-programme.md` and
50+
`plans/40-w41-engine-consumer-boundary.md` both specified private GitHub Packages at the
51+
time they were written and are annotated in place ("What shipped did not honour this") —
52+
left as historical record of the original plan rather than rewritten, since a plan
53+
document records intent at the time, not current fact. This is the final answer;
54+
[issue #302](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/302) tracked
55+
deciding it and is closed by this entry.
4956
- **Provisional simulation numbers** — drift rates, scenario economics, `demandBand`
5057
thresholds, housing-quality formula, travel costs. Need a balancing pass once the sim
5158
harness runs. Tracked as [issue #267](https://github.com/The-Running-Dev/SubZeroDev.GameEngine/issues/267).

docs/docs/guide.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ sidebar_position: 1
33
sidebar_label: Developer Guide
44
---
55

6-
<!-- design-digest: c3d848ee9ce39ef41deb218339116cf62c2e3ccd9be4c4fb7a1d283519d9f625 -->
6+
<!-- design-digest: 515dcce1c756dbbb090a3a2bfef3abac13a039667fae94ae51fcae8bbbbe8edd -->
77

88
> Generated from `design/` by `/make-human-docs`. Do not edit by hand — edit the
99
> design docs and regenerate. `/reconcile` reports when this has gone stale.

src/engine/src/campaigns/bulgaria-bureaucracy.bg.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
* text), `buildStoryGraphCampaign` on this source produces byte-identical `content` to
99
* the English build — proven in `bulgaria-bureaucracy.locale.test.ts` — and the two
1010
* differ only in the `strings` table each build produces.
11+
*
12+
* Unpublished regression fixture, not a publication source: `SubZeroDev.Adventures.Content`
13+
* owns canonical narrative source and publication (`20-contract.md` §19).
1114
*/
1215

1316
import type { BuiltCampaign } from "../core/registry/types.js";

src/engine/src/campaigns/bulgaria-bureaucracy.broken.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
* the shape it emits has no `auto`-kind node left to mutate, so the two fixtures that
1111
* used to target one now target an equivalent `choice`/`random` node instead. Each
1212
* fixture's Tier and code are unchanged; only which node stands in for the mutation is.
13+
*
14+
* Unpublished regression fixture, not a publication source: `SubZeroDev.Adventures.Content`
15+
* owns canonical narrative source and publication (`20-contract.md` §19).
1316
*/
1417

1518
import type { ChoiceNodeSource, RandomNodeSource } from "../kinds/story-graph/source.js";

src/engine/src/campaigns/bulgaria-bureaucracy.freeze.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* The Bureaucracy campaign is frozen regression evidence, not a publication source (W74a,
3-
* `design/30-slices.md`; `02-architecture.md` §13; `04-core.md` §19). Today
3+
* `design/30-slices.md`; `02-architecture.md` §13; `20-contract.md` §19 — `SubZeroDev.
4+
* Adventures.Content` owns canonical narrative source and publication). Today
45
* `bulgaria-bureaucracy.ts`/`.bg.ts` are both the replay oracle's baseline and a shipped
56
* publication, so an edit made for the story's sake — a corrected typo, a rebalanced stat —
67
* silently moves the baseline everything else is measured against, and the move only shows

src/engine/src/campaigns/bulgaria-bureaucracy.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
/**
2+
* Content — "The Bureaucracy" (W15, `design/30-slices.md`).
3+
*
4+
* Unpublished regression fixture, not a publication source: `SubZeroDev.Adventures.Content`
5+
* owns canonical narrative source and publication (`20-contract.md` §19).
6+
*/
7+
18
import type { BuiltCampaign } from "../core/registry/types.js";
29
import type { CommandResult } from "../core/kernel/reasons.js";
310
import type { StoryGraphCampaignSource } from "../kinds/story-graph/source.js";

0 commit comments

Comments
 (0)