The maintainer's observation decided the design: aspects partition the GAME, strata partition our apparatus, and they are orthogonal. A module is one coordinate change in aspect space with an obligation in every stratum. So aspect identity must NOT be Rust types -- an aspect ground-game adds would make clay-borg fail to parse a configuration rather than fail to run it, welding the two coordinate systems at the one place they must stay independent. Chosen: identity as data (Configuration round-trips anything the catalog names), behaviour exhaustive (Rules, no catch-all), resolve() between. Decisive argument: the catalog ALREADY ships modules with a rules_delta and status: proposed, so a per-aspect enum would report them as "unknown module" -- indistinguishable from a typo, a false statement about the edition, and this project's signature failure shape. Two facts need two errors. Federating design authority is permanent, so the representation must outlive the implementation. Legacy ids alias forever through the catalog's own legacy_experiment_id, on the standard-Np precedent: 26 recordings name them and the expansion is exact, so there is nothing to deprecate. T00 done: the schema-2 mirror had arrived with no digests (19 files) and edition-check was red. Digests are now generated by WALKING editions/, not typed -- two reviews already found hand-written lists that made their own controls vacuous, and a mirror that grows a directory is what breaks a maintained list. PROVENANCE-catalog.md was a file inside the mirrored tree that upstream does not have; folded into our own PROVENANCE.md, since provenance about the mirror does not belong inside the thing it describes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7.6 KiB
ADR-0022: a configuration is a point in aspect space, and identity is not behaviour
status: accepted
date: 2026-08-08
decided by: maintainer, on options put by the agent
tier: M (changes the state and therefore the recording; replaces the
selector every consumer names)
references: CB-RES-0010,
ground-game editions/catalog.yaml (schema 2), editions/ASPECTS.md,
specs/Taxonomy.md,
ADR-0011 (the baseline is vendored),
CB-WP-0047
(the standard-Np precedent)
Context
ground-game's catalog moved to schema 2: a configuration is a baseline
plus at most one module per aspect, where an aspect is an orthogonal
design dimension (problem_stress, attack_relief, end_condition,
problem_deal). The monolithic experiments H1 and H2 became profiles
over modules.
clay-borg's selector is a three-armed enum:
pub enum Variant { Baseline, H1ProblemStress, H2ScopedProblemStress }
This cannot express the configurations the catalog now ships.
scoped_plus_attack_soothe — problem_stress.scoped × attack_relief.self_soothe_ge4
— is a named profile upstream and clay-borg has no way to name it, let
alone run it. H1 bundles flat pressure with self-soothe and cannot be
taken apart; H2 gives scoping alone. The product is unreachable.
The kernel is already decomposed; only the selector is a blob. Exactly four sites branch on the variant, and each belongs to exactly one aspect:
| site | behaviour | module |
|---|---|---|
lib.rs:342 |
assign Problem owners at setup | problem_stress.scoped |
lib.rs:1703 |
scoped End-of-Round pressure | problem_stress.scoped |
lib.rs:1741 |
flat End-of-Round pressure | problem_stress.flat_any_open |
lib.rs:1649 |
ATTACK self-soothe at Stress ≥4 | attack_relief.self_soothe_ge4 |
D0 — aspects and strata are two coordinate systems
Raised by the maintainer, and it decides the rest.
Aspects partition the game. They live entirely in the GAME stratum — they are ground-game's vocabulary for what a designer varies.
Strata partition our apparatus (Taxonomy.md): GAME, MODEL, ENGINE, INSTRUMENT, ACCOUNT, PRESENTATION.
So a module is one coordinate change in aspect space with an obligation in every stratum: a kernel delta (MODEL), a computation that must be right (ENGINE), a panel that must facet by it (INSTRUMENT), evidence that names it (ACCOUNT), a label on the table (PRESENTATION).
Two consequences we act on:
- "≤1 module per aspect" is GAME↔MODEL — validation. Whether the scoped delta computes the right Stress is MODEL↔ENGINE — verification. Same selector, two kinds of wrong, and they get separate controls.
- Aspect identity must not be Rust types. If it were, an aspect ground-game adds would make clay-borg fail to parse a configuration rather than fail to run it — welding the two coordinate systems at the one place they must stay independent.
D1 — identity is data; behaviour is exhaustive
struct Configuration { // IDENTITY — GAME-stratum data
baseline: String,
modules: BTreeMap<AspectId, ModuleId>, // resolved, defaults filled
profile: Option<String>, // if one was requested
}
struct Rules { // BEHAVIOUR — ENGINE-stratum
problem_stress: ProblemStress, // None | FlatAnyOpen | Scoped
attack_relief: AttackRelief, // None | SelfSootheGe4
}
impl Configuration { fn resolve(&self) -> Result<Rules, String> }
Configuration round-trips anything the catalog names, including
modules with no kernel path. Rules is exhaustive, so a new module cannot
be silently ignored — the compiler names it.
Why not a typed struct alone
Because the catalog already contains modules we must name and must
refuse to run: end_condition.hybrid_clear_collapse and
problem_deal.pressure_deck ship with a rules_delta and
status: proposed. A per-aspect enum cannot represent them, so selecting
one yields "unknown module" — indistinguishable from a typo. That is
a false statement about the edition, and it is this project's signature
failure: a right computation over the wrong subject (ADR-0018).
The split gives two errors because there are two facts:
problem_deal.pressure_deck is a known module with no kernel path
(catalog status: proposed)
problem_deal.presure_deck is not a module the catalog has
CB-RES-0010 §5.6 requires proposed modules to refuse or no-op loudly. A
no-op is our inert failure kind, so we refuse.
Why not an open map alone
An open map cannot tell you a module has no implementation. That check
becomes a runtime obligation at every read site, and a missed one is a
silent no-op — the same inert outcome, reached by omission. This repo
has been paid twice by exhaustive matching: CB-WP-0034's command_label
caught RejectReverse and BreakRivalry before any test ran.
Federating design authority is permanent, not transitional. The catalog will always be able to name more than the kernel implements, so the representation must outlive the implementation. That is the whole argument.
D2 — legacy ids alias forever
--variant h2, --variant h1, ground-darvo-r0 and every recording that
names them keep working permanently, expanding through the catalog's
own legacy_experiment_id to profiles and then to modules:
| legacy | profile | modules |
|---|---|---|
h1-problem-stress |
h1 |
problem_stress.flat_any_open + attack_relief.self_soothe_ge4 |
h2-scoped-problem-stress |
h2 |
problem_stress.scoped |
The standard-Np precedent (CB-WP-0047): twenty-six recordings name a
preset, and a grammar that redefined it would have moved every board while
the hashes still claimed to pin them. serde defaults the same way — a
state with no modules key is the baseline, which is what it was.
No deprecation warning. An alias that nags is an alias the maintainer
routes around, and the expansion is exact rather than approximate: h2
is [problem_stress.scoped], not a rough equivalent.
D3 — the resolved configuration is what gets recorded
Not the requested one. state.variant = v instead of with_variant()
left H2 inert at three call sites, and CB-WP-0046 stamps the trial log
from state.variant for exactly that reason. With N aspects the failure
is N times as likely, so the object that is applied and the object that
is recorded are the same object, and a panel cell, a replay and a trial
note all carry it.
Consequences
- The state gains a configuration and loses
variant; recordings written before this replay through D2's map. Varianthas 88 mentions across 13 files and isCopy;Configurationcarries a map and is not, so some sites take a reference.- Panels facet by aspect, not by legacy experiment name (CB-RES-0010 §5.4).
- Findings may be scoped to an aspect or a module.
- A module with no kernel path is a hard error naming the module, not a warning and not a no-op.
What was rejected
| rejected | why |
|---|---|
| enum arms per combination | 9 arms for two aspects; the blob schema 2 exists to retire |
| typed struct per aspect, alone | cannot name a proposed module, so it reads as a typo |
| open map, alone | no compile-time check that a module is implemented; a miss is silent |
| deprecating legacy ids | 26 recordings, and the expansion is exact — there is nothing to deprecate |
| deriving the aspect list ourselves | aspects are ground-game's; we validate against the catalog (ADR-0011) |