sots-re/findings/subsystems/value-domain-census.md

100 lines
7.2 KiB
Markdown

# The value-domain census: a third of the save format has only ever held one value
**Owner / date:** integrator · 2026-09-09 · host only, no VM
**Instrument:** `mars::stream::DomainArchive` (`sots-engine/src/mars/stream/probe.h`),
`tests/mars_stream/test_domains.cpp`, run over the 22-save corpus.
## Why this exists
Board row 402 predicted that lane AC's two new saves would break the coverage ratchet: they carry
`CnRad`, `tscr = 253`, three `rt` route records, and a StarFleet whose `LocID` is a TradeSector id —
the first fleet in the corpus located anywhere but a star.
**The prediction was wrong, and being wrong is the finding.** Both saves parse `--strict` clean, round
trip byte-identically, and score **100.0% typed**. The ratchet did not move a hair.
The reason is structural, and it is the same shape as the defect the resolver found in lane Z's RNG
ledger. `CoverageArchive` is indexed by **shape**: it asks "does a field name this item". Lane Z's
audit was indexed by **firing**: it asked "did a draw happen here in a state we ran". Both instruments
are sound at what they measure, and both are blind along the axis where the surprises have actually
been coming from. A save can carry content nobody has ever modelled and still score 100%, because the
novelty lands in a field that was already typed. `LocID` holding a trade-sector id is still a `LocID`.
So the value-axis companion asks the other question: **what has the corpus ever put in this field?**
## The number
Over 22 saves:
```
724 field(s) observed: 490 vary, 234 constant across the whole corpus
```
**234 of 724 typed fields — 32% — have been seen holding exactly one value.** We call this format
"99.99% typed" and mean it, but for nearly a third of it the typing has never been tested against more
than a single observation. `tscr` was one of those 234 until Commerce Raiding moved it, and every lane
that read 252 and inferred a fact about the game was reading a fact about our save set (rule 28,
practice 3).
This is rule 23's "thin coverage" with a number on it, and rule 8's eighteen fields generalised: those
eighteen split a roster identically because nothing exercised them. There are 234.
## Where the constants cluster
| Parent | Constants | Reading |
|---|---:|---|
| `root.Sim.Player` | 24 | incl. 11 in `dipstats` — no corpus game ever ran diplomacy to a treaty |
| `root.Sim.Sys` | 19 | |
| `root.Sim.SvSctOb.EncObj` | 16 | the random-encounter object; **no corpus game triggered an encounter** |
| `root.CD` | 13 | |
| `root.Sim.spymgr.spy` | 12 | see below — this is lane AS's ground |
| `root.Summary.Players.Slot` | 11 | |
| `root.CD.CmbR.CRPlSv2.TacReports` | 9 | tactical combat reports never populated |
| `root.Sim.trdmgr.rt` | 5 | trade routes — even with AC's three `rt` records |
Two individually suspicious constants, both non-zero and non-round, i.e. written by *something*:
- `root.Sim.spymgr.spy.atto = 3728` in all 22 saves.
- `root.Sim.SvSctOb.EncObj.infest.mtrn = 2147483647` (`INT32_MAX`) — a sentinel, almost certainly
"no turn scheduled".
- `root.CD.researchBoostFraction = 0.999215` constant, with `researchBoostSpend = 216383`.
## Direct consequence for the lanes now running
**Lane AS (the spy half)** is looking for the detection roll gated inline in `vslot13`. The census says
the entire spy sub-object is unexercised: `sdet = -1`, `cbh = 0`, `cm = 0`, `cmo = 0`, `deat = 0`,
`sdo = 0` in every save we own. `sdet = -1` is a sentinel in the position a detection result would
occupy. That is consistent with the gate never having fired in the corpus, and it means **the corpus
count for any spy-path predicate is 0 of 22 before AS starts** — which is what rule 28 asks a lane to
state before it calls a zero a negative.
**Lane AG (the gate-indexed audit)** gets a corpus-count column it can compute mechanically rather
than by hand: for a decoded predicate over save fields, the census already knows which fields never
vary, so any predicate resting on one of the 234 has a corpus count of 0 by construction, and its
"unread gate" status is not a matter of opinion.
## What the instrument is not
It does not know *why* a field is constant. `.Version` is constant because we can only read one format
version; `.GameType` because every corpus save is single-player. Those two are whitelisted as explained.
The other 234 are unexplained, which is a statement about our reading, not an accusation against the
game. **A constant field is a coincidence until its writer is found** — the census names the candidates;
it does not resolve them.
It also folds array elements into one domain per field, deliberately: twenty fleets sharing a `LocID`
kind is the observation we want, not twenty rows.
## Ratchet
`kVaryingBaseline = 490`, on the count of fields with observed variation. Growing the corpus must
exercise more of the format, never less; a drop means a save was removed or a shape stopped being
reached. Raise it deliberately (rule 27). The constant count is printed but not ratcheted — it is not
monotone, since a newly typed body can add constants as easily as it adds variation.
Full list: `SOTS_DOMAIN_ALL=1 SOTS_SAVES_DIR=<corpus> mars_stream_test_domains`.
## Proposed board rows
| the coverage ratchet is blind along the value axis | verify | verified | high | 100% | 2026-09-09 | Board row 402 predicted AC's two saves would break the ratchet. **They did not** - both score 100.0% typed and round trip byte-identically, because their novelty (`LocID` at a TradeSector, `tscr = 253`, three `rt` records) lands in fields that were **already typed**. `CoverageArchive` is indexed by **shape**, exactly as lane Z's ledger was indexed by **firing**: both instruments are sound at what they measure and both are blind where the surprises come from. New instrument `DomainArchive` + `test_domains` measures the other axis |
| 234 of 724 typed fields have only ever held one value | verify | verified | high | 100% | 2026-09-09 | The census over 22 saves: **724 fields observed, 490 vary, 234 constant across the whole corpus - 32%**. `tscr` was one of the 234 until a one-turn tech moved it. Clusters: `Sim.Player.dipstats` 11 (no corpus game reached a treaty), `SvSctOb.EncObj` 16 (**no corpus game triggered a random encounter**), `spymgr.spy` 12, `CmbR.CRPlSv2.TacReports` 9 (tactical reports never populated). Individually suspicious because non-zero and non-round: `spy.atto = 3728`, `EncObj.infest.mtrn = INT32_MAX` (a sentinel), `CD.researchBoostFraction = 0.999215`. This is rule 23's thin coverage with a number on it and rule 8's eighteen fields generalised. Ratchet at 490 varying; see `findings/subsystems/value-domain-census.md` |
| the spy sub-object is unexercised: 0 of 22 before lane AS starts | verify | verified | high | 100% | 2026-09-09 | Direct input to lane AS. The census reads `spymgr.spy.sdet = -1` in **all 22 saves**, with `cbh`/`cm`/`cmo`/`deat`/`sdo` all 0 - `sdet = -1` sits in the position a detection result would occupy and is a sentinel. So the **corpus count for any spy-path predicate is 0 of 22 before the lane takes a single measurement**, which is exactly the statement rule 28 requires before a zero may be called a negative. Also gives lane AG a mechanically computable corpus-count column: a predicate resting on any of the 234 constants has count 0 by construction |