clay-borg/decisions/ADR-0022-a-configuration-is-a-point-in-aspect-space.md
tegwick bff64053ca ADR-0022 + CB-WP-0048 T00: the selector decision, and the mirror held
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>
2026-08-08 22:07:57 +02:00

173 lines
7.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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) |