Skip to content

Commit f42189b

Browse files
Contract § Invariants — state the cross-cutting set as C1–C20 (#445)
§ Invariants named six families — determinism, envelope ownership, projection, migration, validation, identifiers — and stated none of them, delegating to Core Specification. The repository's only assertable invariants were the 19 in two section-scoped blocks (§7.1's P1–P8, §7.4's L/D/B/A); all six named families had zero. Delegation to a section that does not carry the statement leaves the invariant unowned. Twenty statements, each derived from something 10-design.md or Core Specification already fixes, in the existing house style. Every one closes with an enforcement clause, because the four that hold only by instruction — C6 envelope ownership, C13 migration, C18 id stability, C19 opaque gameId/seed — are the ones a reader must not trust without checking. C6 cites the five-instance ledger. The projector's `invariants` marked region is deliberately still absent: with no Invariant records under design/state/ it would render an empty table and arm ProjectionStale over content nothing produces. Recorded in 90-decisions.md, alongside what adopting the projection properly would cost. Outside every human-doc block, so no page changes; guide digest re-stamped.
1 parent d701bc9 commit f42189b

3 files changed

Lines changed: 104 additions & 5 deletions

File tree

design/20-contract.md

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,99 @@ registered reason-code vocabulary.
2424

2525
## Invariants
2626

27-
Determinism, envelope ownership, projection, migration, validation, and identifier invariants are
28-
owned by Core Specification; kind-specific turn and state invariants are owned by each kind block.
29-
The profile mechanism's own assertable set is Core Specification §7.1, *Invariants* (P1–P8);
30-
session lifecycle's is §7.4.
27+
Statements that must hold at all times, each written so it could become an assertion. This
28+
section states the **cross-cutting** set — the invariants no kind block owns, which Core
29+
Specification's prose settles but no numbered list has carried. Section-scoped sets stay where
30+
they are and are not restated here: the profile mechanism's is Core Specification §7.1,
31+
*Invariants* (P1–P8); session lifecycle's is §7.4 (L1–L3, D1–D3, B1–B3, A1–A2). Kind-specific
32+
turn and state invariants are owned by each kind block.
33+
34+
**Read the enforcement clause.** An invariant marked *enforced by code*, *by the type*, or *by
35+
the guard* holds by construction and may be trusted without checking. One marked *by
36+
instruction* is a rule with no gate — it holds only while every author remembers it, and C6
37+
below is the one this repository has already watched fail five times.
38+
39+
**Determinism.** Maintained by the core kernel, the eslint determinism guard, and the harness
40+
(Core Specification §14). C5 is the extensibility rule (06 §2) stated as an assertion.
41+
42+
- **C1.** A `{ config, actionLog }` fixture replayed twice produces byte-identical `serialize()`
43+
output. *Enforced by code — §14's golden files and property tests.*
44+
- **C2.** No resolution path calls `Math.random`, `Date.now`, or a non-bit-stable `Math.*`.
45+
*Enforced by the guard in `src/engine/eslint.config.js`
46+
([Engine Package](/docs/guide/engine-package)), which fails the `engine` job rather than a
47+
review.*
48+
- **C3.** `{ seed, actionLog }` is the complete replay input. No generator state is persisted;
49+
every stream is a pure function of `(seed, streamId)` (§8). *Enforced by the type — `GameState`
50+
declares no RNG field, and adding one is a contract amendment.*
51+
- **C4.** Removing every event changes nothing: a fixture replayed with `nullEmitter` and with
52+
`createRecordingEmitter()` yields byte-identical `serialize()` output, an identical
53+
`AdvanceResult`, and an identical action log. *Enforced by code — §14's sink-independence
54+
check — and by `emit` returning `void`, which leaves a kind nothing to branch on.*
55+
- **C5.** A host-supplied port cannot change `serialize()` output. *Enforced by the harness —
56+
each port replays a fixture under its random default and under a controlled implementation
57+
(06 §6, step 6).*
58+
59+
**Envelope ownership.** Maintained by Core Specification §2. C6 is the highest-risk statement in
60+
this section, because it is the only one here with no gate behind it.
61+
62+
- **C6.** Every field a game has regardless of kind — `formatVersion`, `gameId`, `kindId`,
63+
`campaignId`, `campaignVersion`, `seed`, `status`, `actionLog` — lives on `GameState` and is
64+
duplicated by no `kindState`, no `Campaign`, no registry entry, and no kind's view type.
65+
*Enforced by instruction only. No check exists; `CLAUDE.md`'s ledger records five instances of
66+
this being violated, three in state and content and two on the view side.*
67+
- **C7.** No core module imports a kind. `kindState` is `unknown`, never a union of kind states,
68+
and a kind casts its own payload guarded by `kindId`. *Enforced by the type and by module
69+
structure.*
70+
- **C8.** No wall-clock value reaches `GameState`. Timestamps live on the session-store record
71+
(§7), outside replayable state. *Enforced by the guard's `Date.now` ban together with the
72+
envelope's declared fields.*
73+
74+
**Projection.** Maintained by the kernel, in one place, so that a kind neither implements nor can
75+
defeat it (§9.1).
76+
77+
- **C9.** Mutating anything reachable from an `Engine.view` or `Scene.view` result, to any depth,
78+
leaves `GameState`, every later projection, `serialize()` output, and the action log unchanged.
79+
*Enforced by code — a structural clone at the kernel boundary.*
80+
- **C10.** Two projections of one state are equal and are never the same object. A client may not
81+
use a view as a cache key or compare views by reference. *Enforced by construction, as the
82+
consequence of C9; a caller relying on identity is relying on something never promised.*
83+
- **C11.** No value a kind marks hidden appears in any projection. *Owner: each kind's projection
84+
section. Enforced by code per kind, not centrally — the core cannot inspect an opaque
85+
`kindState` to check it.*
86+
87+
**Migration.** Maintained by Core Specification §10.2.
88+
89+
- **C12.** A save records the `campaignVersion` it was made under, and loading it against a
90+
different version either migrates or fails with `save_requires_migration`. It never strands a
91+
save silently. *Enforced by code — `resolveSaveEnvelope`.*
92+
- **C13.** A migration may re-address published ids and drop or default what no longer resolves,
93+
and may never invent play. *Enforced by instruction for the second clause; the first is
94+
constrained by Tier 1, which rejects a migration naming an id domain outside the engine-owned
95+
reference-site table.*
96+
- **C14.** A migrated save is marked not-replay-compatible. *Enforced by code.*
97+
98+
**Validation.** Maintained by Core Specification §11, tier by tier.
99+
100+
- **C15.** Tier 1 fails the load; Tier 2 loads and flags; Tier 3 never runs at load time at all.
101+
*Enforced by the type — `errors` and `warnings` are separate fields, and Tier 3 is an
102+
out-of-band author-facing check with no load-time entry point.*
103+
- **C16.** No Tier 2 warning changes `GameState`, `AdvanceResult`, or whether a submission
104+
succeeded. *Enforced by code — the same guarantee §7.1's P4 states for the profile store,
105+
generalised.*
106+
107+
**Identifiers.** Maintained by Core Specification §17 and the two id ports (06 §5.1, §5.7).
108+
109+
- **C17.** Every published id is ASCII `[a-z0-9_-]` in the shape §17's table fixes for its
110+
category, and is unique within its scope. *Enforced by code — Tier 1 checks both the character
111+
set and uniqueness.*
112+
- **C18.** An id is stable once published; a rename is a migration, never an edit. *Enforced by
113+
instruction, with C12 as the backstop that makes a violation loud rather than silent.*
114+
- **C19.** `gameId` and `seed` are opaque to the core: never parsed, compared, ordered, or
115+
derived from. *Enforced by instruction — `IdSource` returns `string`, and nothing stops a
116+
reader; the reason it holds is that no core path has cause to look inside one.*
117+
- **C20.** A session id and a save id never enter `GameState`. They key host records and are
118+
never replay inputs. *Enforced by the type — neither is a declared envelope field, which is
119+
why `RecordIdSource` is a second port rather than a widening of `IdSource`.*
31120

32121
## Unresolved
33122

design/90-decisions.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1445,3 +1445,13 @@ Context: adopting `design/state-index.md` (above) immediately fired the exact re
14451445
Chosen: point the guard at `design/state/units`, matching `Test-DesignState.Tests.ps1` and `Update-DesignProjection.Tests.ps1` line for line, and rewrite the comment block that justified the old target. The guard's question is "has this repository adopted unit records," and `S4.6` reads unit records; `design/state/units` answers it directly where both `design/state/` and `design/state-index.md` are now proxies falsified by the work mirror and its projection respectively. All three guards agree again. Verified in both directions: the failure was observed with the old target before the change, and `tools/` returns to 324 passed / 0 failed / 35 skipped after it.
14461446
Rejected: **Creating `design/state/units/` records so the assertion passes** — the unscoped-feature-adoption rejection of 2026-08-21 and 2026-08-24, unchanged; `S4.6` demands a specific closure this repository has no use for. **Reverting `design/state-index.md`** — would trade a real, asked-for artifact for a stale guard. **Leaving the suite red** — a red gate that everyone knows is red stops being a gate.
14471447
Reversibility: cheap — one `Test-Path` target and its comment, reverted by pointing it back. Note for the next `/kit-sync`: this file is now divergent from the kit on this line again, deliberately, and the reasoning is here rather than in the diff.
1448+
1449+
### 2026-09-05 — `20-contract.md` § Invariants states the cross-cutting set as prose; the projector's `invariants` region stays unwritten
1450+
1451+
Context: `/contract` was invoked for "the invariants region", which resolves two ways. `tools/Update-DesignProjection.ps1:363` targets an `invariants` marked region in `design/20-contract.md`, rendering `Invariant` records from `design/state/`; the 2026-09-05 entry recording `design/state-index.md` as deliberately absent said writing a region "belongs to `/design` or `/contract` if it is wanted"; that entry has since been superseded for `state-index.md`, which #444 adopted the same day — but expressly not for this region, which #444 left unwritten on the same arithmetic reached here independently: "with zero `Invariant` records it would insert an empty machine-owned table into prose the Core Specification owns." Separately, `/contract` calls § Invariants "the highest-value section in the document," and this repository's copy was four lines of pure delegation: it named six cross-cutting families — determinism, envelope ownership, projection, migration, validation, identifiers — and stated none of them. The repository's only assertable invariants were the 19 in two section-scoped `#### Invariants` blocks (§7.1's P1–P8, §7.4's L1–L3/D1–D3/B1–B3/A1–A2), both added by recent slices. Every one of the six named families had zero.
1452+
1453+
Chosen: author § Invariants as twenty numbered statements, `C1``C20`, grouped under the six families and written in the existing house style — a bolded lead naming the maintaining module, then one bullet per invariant closing with an italic enforcement clause. Each is derived from a statement `10-design.md` or Core Specification already fixes; none is new behaviour. The section opens by declaring itself the cross-cutting set only, and names the two section-scoped sets rather than restating them. `C6` — the envelope-ownership invariant — is stated as *enforced by instruction only*, citing `CLAUDE.md`'s five-instance ledger, because a reader is entitled to know which of these hold by construction and which hold only while an author remembers them; that distinction is what the enforcement clause exists to carry, and `C6`, `C13`, `C18` and `C19` are the four that fail it.
1454+
1455+
Rejected: **Adding the `invariants:start`/`:end` markers** — the literal reading, and rejected on its own arithmetic: with zero `Invariant` records under `design/state/`, `Get-InvariantsProjectionContent` renders a bare table header with no rows and no `_(no … records yet)_` placeholder, unlike its six siblings, so the section's highest-value content would become an empty table. It would also newly arm `ProjectionStale` as a blocking finding on every `/track` — a gate over content nothing produces — and reverse the judgement #444 reached independently the same day. **Adopting the projection properly**, writing an `Invariant` record per statement so the table is machine-checked against records, is the version worth having and is not foreclosed: it needs the records and the region, and it is `/design`'s call whether this repository keeps `Invariant` records at all. #444 adopted the projection for `state-index.md` while declining exactly this region, so the mechanism is now in use here and only its invariant arm is unwired — which lowers the cost of adopting it later rather than raising it. **Leaving § Invariants as delegation** — rejected because delegation to a section that does not carry the statement is not single ownership, it is an unowned invariant.
1456+
1457+
Reversibility: cheap. Ninety-three lines in one canonical section, no type, signature, behaviour, test, or generated page changed — the section sits outside every `human-doc` block, so it publishes nowhere and only the guide's design digest was re-stamped. Adopting the projection later replaces this prose with records and a region, and the `C`-prefix is free for that: it collides with no existing invariant id.

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: 17f3780802b57d97b6c9a7b7f2088abb639eb28e6e1db2e0ef0217c1fa0b5288 -->
6+
<!-- design-digest: ba314b4ae8a75ab36f71c29ff18aaf6d215d1e6dafea208296f7cb8064ffdc67 -->
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.

0 commit comments

Comments
 (0)