174 lines
7.6 KiB
Markdown
174 lines
7.6 KiB
Markdown
|
|
# 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<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.
|
|||
|
|
- `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) |
|