Scope G-minor: the schema minor is content-addressed, and that changes the rung
The scoping question was whether the chunk header's minor reaches a pinned corpus. It does, and worse: chunk_content_hash pushes SchemaVersion's canonical bytes -- major AND minor -- into the preimage, so raising a minor changes the ChunkId and propagates to the manifest that names it. Not a semantic break, since readers gate on the major only, but a content-address-moving change landing on the structure req:format:manifest-id makes a two-writer promise about. The text projection also projects the minor as a document surface, and the committed corpus carries six literal (schema 0 1) occurrences, so op-block minors rising means another companion bump on the G1/G2a precedent. The policy is the real work and is left for ratification: one minor per tranche (a retroactive judgement needing a hand-maintained table -- a seventh such site on a track whose defining lesson is that they go stale), minor as the highest discriminant emitted (derivable, self-describing, nothing to maintain, but redefines the term and is per-vocabulary), or a per-major append counter. I recommend the second and say why the recommendation is not a default. Four further questions logged, including whether the canonical base is exempt: its major is role-pinned to 0 and its byte-identity test pins the payload, not the header, so a base whose minor rises keeps its payload and moves its id. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
This commit is contained in:
parent
55eff00778
commit
a229a977a2
|
|
@ -0,0 +1,156 @@
|
|||
# Plan — G-minor: the chunk schema minor, and why it is not a small rung
|
||||
|
||||
**Filed as** P13-S14. **Ruled** 2026-07-28: its own rung, sequenced **after
|
||||
G2a and before G2b** (`spec/PLAN_GENESIS_OPS.md` §4 — the sweep is scoped to
|
||||
kinds 24–33, and G2b appends 34).
|
||||
|
||||
**Status:** scoped, not contracted. §5 lists what needs ratification first.
|
||||
|
||||
---
|
||||
|
||||
## 1. The requirement, and the evidence it has never been met
|
||||
|
||||
`binary_format.tex:2330` (§"Schema Versioning", the Minor bullet):
|
||||
|
||||
> **Minor** = additive. v0 readers verify the major only; the minor is a
|
||||
> *record*, not a gate — but it is a mandatory record: a writer **MUST** raise
|
||||
> the chunk schema minor when it emits any discriminant appended after the
|
||||
> minor it otherwise declares, so that a decode failure on an unknown appended
|
||||
> discriminant is attributable to a version skew rather than corruption.
|
||||
|
||||
Three facts, each read from the tree:
|
||||
|
||||
1. `SchemaVersion::for_major` (`bundle/src/ids.rs:204`) maps a major to a
|
||||
**fixed constant** and **accepts only a major**. There is no parameter a
|
||||
per-kind minor could travel through. (Note `V0` is `{0, 1}` — `ids.rs:173`
|
||||
— while `V1`/`V2`/`V3` are `{n, 0}`. The baselines are already inconsistent,
|
||||
which matters for §4.)
|
||||
2. Both writer-side staging paths derive only the major:
|
||||
`testkit/src/bundle_harness.rs:32` and `textproj/src/serialize.rs:189`, each
|
||||
computing `max(schema_major)` and handing it to `for_major`.
|
||||
3. Therefore kinds **24–27** (Phase 3), **28–29** (major-2 repeats), **30**
|
||||
(Push 4a), **31** (G1), and **32–33** (G2a) all ship with no additive
|
||||
record. The requirement's own failure mode is exactly what the gap
|
||||
produces: a reader meeting kind 33 from a newer writer cannot distinguish
|
||||
"my vocabulary is stale" from "these bytes are damaged".
|
||||
|
||||
## 2. The decisive scoping finding: the minor is content-addressed
|
||||
|
||||
**This is what makes G-minor a real tranche rather than a one-line fix, and it
|
||||
must be settled before any contract is written.**
|
||||
|
||||
`chunk_content_hash` (`bundle/src/chunk.rs:177`) builds the preimage as
|
||||
|
||||
```rust
|
||||
p.push_bytes(&kind.canonical_bytes());
|
||||
p.push_bytes(&schema.canonical_bytes()); // <-- major AND minor
|
||||
p.push_u64_le(payload.len() as u64);
|
||||
p.push_bytes(payload);
|
||||
```
|
||||
|
||||
and `SchemaVersion::canonical_bytes` (`ids.rs:222`) is "major then minor,
|
||||
little-endian" — the minor is *in* the preimage. `chunk_id` dispatches through
|
||||
`content_hash_for` to the same function (`chunk.rs:202`).
|
||||
|
||||
**So raising a chunk's minor changes its `ChunkId`.** Every affected chunk gets
|
||||
a new content address, which propagates to the manifest that names it. This is
|
||||
not a semantic break — readers gate on the major only, exactly as the spec says
|
||||
— but it is a **content-address-moving change**, and it lands on the one
|
||||
structure `req:format:manifest-id` promises two conforming writers derive
|
||||
identically.
|
||||
|
||||
**Two corpora move with it:**
|
||||
|
||||
* `spec/vectors/textproj_document_vectors.txt` — the text projection projects
|
||||
the minor as a document surface: `project_schema` (`textproj/src/project.rs:270`)
|
||||
emits `(schema <major> <minor>)`, and the committed corpus contains six
|
||||
literal `(schema 0 1)` occurrences. If op-block minors rise, those texts
|
||||
change — which makes this **another `COMPANION_VERSION` bump** on the G1/G2a
|
||||
precedent.
|
||||
* `spec/vectors/decode_vectors.txt` — value-level, so it moves only if the
|
||||
packet touches value codecs. It should not, and that is a check, not an
|
||||
assumption.
|
||||
|
||||
**Sixty-two sites across sixteen files construct a `SchemaVersion`**
|
||||
(`SchemaVersion::{V0,new,for_major}`), concentrated in `epiphany-bundle`
|
||||
(`chunk.rs`, `manifest.rs`, `superblock.rs`, `opindex.rs`, `bundle.rs`,
|
||||
`vectors.rs`, `fuzz.rs`), `epiphany-testkit` (`bundle_harness.rs`,
|
||||
`generators.rs`, `roundtrip.rs`, `benches/bundle.rs`), and `epiphany-textproj`
|
||||
(`serialize.rs`, `parse.rs`, `project.rs`, `vectors.rs`). Most are fixtures
|
||||
stamping `V0` and are unaffected; the ones that matter are the two staging
|
||||
paths and anything asserting a literal id.
|
||||
|
||||
## 3. What the rung owes
|
||||
|
||||
1. A **minor-assignment policy** — see §4. This is a judgement, not a
|
||||
derivation, and it is the reason this is a rung and not a patch.
|
||||
2. A per-kind minor (or an equivalent watermark) reachable from a payload.
|
||||
3. Block minor = **max over payloads**, mirroring how the major is derived.
|
||||
4. A `for_major` replacement that accepts a minor — `SchemaVersion::new`
|
||||
already exists (`ids.rs:200`), so this is a call-site change, not a new API.
|
||||
5. Both staging paths, and a check that no third path has appeared.
|
||||
6. Regenerated corpora, a companion bump if §2's projection finding holds, and
|
||||
the `binary_format.tex` accounting for whichever policy §4 ratifies.
|
||||
|
||||
## 4. The policy question — needs ratification before contracting
|
||||
|
||||
The spec says a writer must raise the minor "when it emits any discriminant
|
||||
appended **after the minor it otherwise declares**". That phrasing presumes a
|
||||
correspondence between each minor and a vocabulary watermark, but no such
|
||||
correspondence has ever been written down. Three candidate policies:
|
||||
|
||||
**(a) One minor per tranche, monotonic.** Phase 3 → 2, repeats → 3, Push 4a →
|
||||
4, G1 → 5, G2a → 6. Matches the intuitive reading of "minor" as a format
|
||||
revision counter. **Cost:** the assignment is a retroactive judgement with no
|
||||
derivation behind it, so it must be written into the spec as a table and
|
||||
maintained by hand forever — a seventh hand-maintained site, on a track whose
|
||||
defining lesson is that those go stale.
|
||||
|
||||
**(b) Minor = the highest kind discriminant the chunk emits.** A block whose
|
||||
largest kind is 33 stamps minor 33. **Derivable, self-describing, and it
|
||||
satisfies the rationale exactly** — a reader seeing minor 33 knows precisely
|
||||
which vocabulary it needs. Nothing to remember and nothing to maintain.
|
||||
**Cost:** it redefines "minor" from *format revision* to *vocabulary
|
||||
watermark*, it collides with `V0`'s existing minor of `1`, and it is
|
||||
per-vocabulary — the layout cache and the operation index have their own
|
||||
discriminant spaces, so "the minor" would mean different things per role.
|
||||
|
||||
**(c) Per-major append counter.** A hybrid: minor counts vocabulary appends
|
||||
within a major. Inherits (a)'s bookkeeping without (b)'s redefinition.
|
||||
|
||||
My recommendation is **(b)**, on the strength of the track's own history: every
|
||||
hand-maintained parallel list on this project has gone stale (four at Push 4a,
|
||||
six found during G2a), and (b) is the only option with nothing to maintain. But
|
||||
it is a genuine redefinition of a spec term and the per-role ambiguity is real,
|
||||
so it is a ruling, not a default.
|
||||
|
||||
**A fourth option worth pricing before choosing:** amend the MUST. It was
|
||||
declined at P13-S14 filing on the ground that its rationale — distinguishing
|
||||
skew from corruption — is sound and unchallenged. If §2's content-address cost
|
||||
is judged too high for the benefit, that trade deserves an explicit re-look
|
||||
rather than a silent deferral.
|
||||
|
||||
## 5. Open questions
|
||||
|
||||
1. **The policy** (§4). Blocks everything.
|
||||
2. **Does the canonical base move?** Its major is pinned to 0 by role
|
||||
(`mis_stamped_canonical_base`, `bundle.rs:866`), but its *minor* is
|
||||
unconstrained, and `the_canonical_base_is_byte_identical_across_data_model_majors`
|
||||
pins the `MaterializedState` **payload** bytes, not the chunk header. A base
|
||||
whose minor rises keeps its payload and changes its `ChunkId`. Decide
|
||||
deliberately whether the base is exempt.
|
||||
3. **Per-role or global?** Op blocks, layout cache, and operation index have
|
||||
independent discriminant spaces. Policy (b) forces this question; (a) and
|
||||
(c) can dodge it.
|
||||
4. **Does the manifest's own `manifest_schema_version`
|
||||
(`superblock.rs:174`) participate?** The manifest is carried opaquely and
|
||||
never grows a versioned layout, so probably not — but it is a
|
||||
`SchemaVersion` and should be ruled in or out explicitly.
|
||||
5. **Migration.** Existing bundles carry minor 0/1 with appended kinds inside.
|
||||
After this rung they are, by the new rule, mis-stamped. No production corpus
|
||||
exists (local repo and test bundles only — the standing
|
||||
`epiphany-ops/DECISIONS.md` position), so the answer is probably "nothing to
|
||||
do", but it should be *stated* rather than assumed.
|
||||
|
||||
*Related: `spec/PASS13_CANDIDATES.md` (P13-S14), `spec/PLAN_GENESIS_OPS.md` §4
|
||||
(the ladder), `spec/binary_format.tex` §"Schema Versioning".*
|
||||
Loading…
Reference in New Issue