# 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](../research/CB-RES-0010-game-aspects-and-design-space.md), ground-game `editions/catalog.yaml` (schema 2), `editions/ASPECTS.md`, [specs/Taxonomy.md](../specs/Taxonomy.md), [ADR-0011](ADR-0011-vendor-the-edition.md) (the baseline is vendored), [CB-WP-0047](../workplans/CB-WP-0047-all-four-boards-and-all-three-modes.md) (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: ```rust 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 ```rust struct Configuration { // IDENTITY — GAME-stratum data baseline: String, modules: BTreeMap, // resolved, defaults filled profile: Option, // 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 } ``` `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. - `Variant` has 88 mentions across 13 files and is `Copy`; `Configuration` carries 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) |