Revise the G-minor scope: the recommended policy could not work
All five findings verified against the tree; the recommended policy is rejected and replaced with the ratified one. Policy (b), minor = highest discriminant emitted, cannot represent an operation block. An envelope also emits the outer OperationPayload discriminant, and ResolveEquivocation is appended at 3 while carrying no OperationKind at all -- so the ambiguity is inside one role and one block, not between roles. Generalizing to "highest from any vocabulary" is worse: an old kind 23 would numerically mask a new payload 3. binary_format itself enumerates four independent minor-additive vocabularies. The rung's gating work is therefore an audit of every append-only discriminant reachable from each affected payload, which the first draft never scoped. Ratified instead: a global additive epoch with content-minimal stamping, an envelope's minor being the max across outer payload, primitive kind, and every nested additive variant actually emitted. The maintenance objection is answered by co-locating introduced_minor with each discriminant in an exhaustive match with no wildcard, so an unassigned variant cannot compile -- the operation_kind_tag_vocabulary! reasoning. Per-major counters are rejected too: mixed blocks do not compose after max_major. Two of my conclusions were wrong. Op-block minors do not reach the text projection -- block schemas are discarded there, and all seven accepted-corpus schema forms belong to extension chunks or canonical bases -- so no companion bump. I had also miscounted them as six by grepping lines rather than occurrences. And the manifest-ID promise is not threatened: it is conditional on the same manifest body, and a changed ChunkRef is a different body. Real address churn, not a broken guarantee. "Is the canonical base exempt" was the wrong binary question. It never emits the op-kind discriminant, so it holds its minor until MaterializedState's own bytes emit a later-added variant. Manifest::SCHEMA stays put; existing bundles need no migration. Construction-site count corrected 62 -> 66. 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
42a8cc6dc5
commit
82ac636982
|
|
@ -91,4 +91,4 @@ visible — a value with a wire form and no canonical carrier to reach it.)
|
|||
| P13-S11 | **`AnchorPoint`, referenced by `AccidentalEngraving.anchor`, is defined nowhere.** `core_spec.tex:3166` names `pub anchor: AnchorPoint`; no struct or enum of that name exists in the spec, and `epiphany-core` (whose `Cargo.toml` has no `epiphany-layout-ir` dependency) cannot borrow any layout-ir type even if one shared the name — the same core-native requirement that forced `EngravingBoundingBox`. An undefined leaf frozen onto the wire is the `KeyContext`-shaped gap. Compounding it: the bounding box is documented "relative to the glyph's anchor point" (`:3160`), so freezing an anchor with no defined coordinate frame freezes a point with an undefined origin | this file (verified 2026-07-23; `AnchorPoint` appears once in `core_spec.tex`, defined nowhere; `epiphany-core/Cargo.toml` lists no layout-ir) | **resolved** (ratified: core-native `AnchorPoint { x: SpaceUnit, y: SpaceUnit }`, over the same `SpaceUnit` as `advance_width` and `EngravingBoundingBox`. **Plus one normative sentence pinning the frame**: x/y in canonical space units, y-up, relative to the glyph's coordinate origin — matching the repo's existing Bravura outline convention — so the anchor and the box it anchors share an unambiguous origin. Freezes in tranche 3b) |
|
||||
| P13-S12 | **`SmuflVersion` is undefined, and its obvious representation orders SMuFL's real history backwards.** `SmuflVersionRequirement` (`core_spec.tex:3269`) carries `minimum`/`authored_against` of type `SmuflVersion`, which Chapter 4 references but does not define (it exists only as a **Chapter 7 / layout-ir** type, `glyph.rs:29`, with literal-minor encoding — see the resolution). Ordering is load-bearing (`SmuflVersionRequirement.minimum`, `:3271`, gates the fallback of `req:tuning:smufl-version-fallback`), and the type is dual-purpose — it also anchors Chapter 9's `GlyphCatalog::smufl_version()` / `GlyphCatalogIdentity` (`:10420`, `:10460`), so this freeze touches layout-conformance identity, not just tuning. The trap: SMuFL versions are decimal fractions — 1.12 (2015), 1.18, 1.20 (2016), 1.3 (2019), 1.4 (2021) — that succeed by fraction (0.12 < 0.18 < 0.20 < 0.30 < 0.40). A `{ major: u16, minor: u16 }` storing literal digits with derived `Ord` orders the minors 3 < 4 < 12 < 18 < 20, placing 1.3 and 1.4 **before** 1.12; old fonts declaring 1.18-era versions exist forever | this file (verified 2026-07-23 against `core_spec.tex:3269-3274` and SMuFL's published release history) | **resolved** (ratified shape `SmuflVersion { major: u16, minor_centi: u16 }`, **literal-minor storage rejected**: the minor is stored **fraction-normalized to hundredths** — 1.12→(1,12), 1.18→(1,18), 1.20→(1,20), 1.3→(1,30), 1.4→(1,40), rule normative, release table a note. Derived `Ord` is then correct across the whole real history and collapses the 1.2/1.20 ambiguity (both → (1,20)). **Correction on filing: the leaf is NOT undefined — it exists in `epiphany-layout-ir/src/glyph.rs:29` as `SmuflVersion { major, minor }` with LITERAL minor (`{1,4}`) and derived `Ord`, so the backwards-ordering bug is LIVE there today (1.3 < 1.12), and it is a direct field of `GlyphCatalogIdentity` — conformance identity.** `epiphany-core` cannot depend on layout-ir, and `SmuflVersionRequirement` is core, so the type MUST be defined in core and layout-ir must reuse it — a **unification**, not a fresh definition, which moves `GlyphCatalogIdentity` (`{1,4}`→`minor_centi 40`). Tranche 3a defines `core::SmuflVersion` for the tuning use and leaves layout-ir's alone (a bounded, core-invisible homonym since core can't import layout-ir's); tranche 3b performs the unification and the deliberate `GlyphCatalogIdentity` move. **Correction (2026-07-23, tranche 3b-ii): "with golden/vector regen" above was verified false before that tranche's dispatch.** No golden, baseline, or vector is pinned to the catalog identity — every assertion on `ResolvedLayoutIR::canonical_bytes()` is relative (stability, determinism, and a `metrics_hash[0] ^= 1` sensitivity check that never touches `smufl_version`), and the committed SVG/PNG goldens do not embed it. The move changes the catalog's emitted bytes (`encode_catalog`'s minor field: `04 00` → `28 00`) in *value*, with nothing to regenerate. The hundredths scale is blocking for 3b, free to adjust in 3a) |
|
||||
| P13-S13 | **The score tuning context has no canonical persistence path: it can be saved, but never authored, replicated, or merged.** Schema major 3 (Push 4b tranche 3b-i) put `smufl` and `overrides` on the wire, but the only *persisted* carrier that embeds a `ScoreTuningContext` is the **acceleration snapshot**, which Chapter 8 makes explicitly non-canonical and regenerable and which the bundle may discard and rebuild at will. No **canonical** carrier embeds it at all: no operation authors it (`epiphany-ops` has no tuning-context payload anywhere — the vocabulary's only `tuning` references are the per-pitch `TuningReference::Inherit`), and `MaterializedState` carries effects, conflicts, anomalies, objects, spellings, breaks, page-breaks, and pending, but no tuning context. So a user who selects a tuning system or sets a per-voice override has authored something the format cannot durably represent, and `req:tuning:tuning-resolution-order`'s scopes 2–4 (`overrides`) are in practice unreachable from any document a replica could exchange | this file (found 2026-07-24 while scoping text-projection parity; verified by searching the whole `epiphany-ops` operation vocabulary and `MaterializedState`'s field list) | **resolved by the genesis tranche** — `spec/RULING_GENESIS_PERSISTENCE.md` (ratified 2026-07-24, 011c68a) reverses Pass-12 K8 and absorbs genesis into the operation set, naming `SetTuningContext` in its §2 settings table. **This was not a tuning-specific defect and did not get a tuning-specific fix:** the operation arrives as one of nine surfaces in a single coordinated tranche, and the wire layout is untouched. **The disposition, and the evidence that produced it:** `spec/ANALYSIS_GENESIS_PERSISTENCE.md` maps the same gap across the whole `Score`: **eight fields have no operation that can produce them** — `canvas.layout_defaults`, `instruments`, `staff_groups`, `parts`, `tuning_context` (this entry, its field 10), `spelling_precedence`, `analysis_layers`, `views`, plus `identity` — and one more (`decomposition_attachments`) can only be pruned back, never authored. Independently re-verified against the working tree: each field's sole `reduce.rs` mention is a read-only base-seed read. Of the four dispositions weighed there (a canonical genesis block; closing the op-coverage gap; promoting the canonical base to carry graph values; scope-limiting), **the second was taken** — every mutable `Score` field becomes operation-authored — so S13 closes when that tranche lands and is tracked *there*, not here. **One cost this entry contributes as evidence:** the per-field operation route is not the free schema-minor it appears. Blocks stamp *minimally*, at the lowest major whose layouts decode them, and `bundle.rs`'s `max_supported_major` currently caps `OperationEnvelopeBlock` at **2** on the explicit ground that no operation payload embeds the tuning context. A `SetTuningContext`-style operation would make its blocks stamp v3, dragging a role accept-set raise along with the kind append — for one field of eight. That is a concrete instance of the analysis's closing constraint, that any option enumerating fields must be re-audited against its table whenever `Score` gains a field. **This cost is now paid deliberately rather than avoided — but NOT amortised, as first written.** The ruling's "one accept-set raise, spent once" implied a single batch landing every new kind at `OperationEnvelopeBlock` major 3, spreading the raise across nine surfaces. Re-derived 2026-07-28 against the working tree, that is wrong: minimal stamping is a pure function of each payload's value, so the other eight surfaces stamp at major 0, 2, or 2 and never reach the raised bound at all. `SetTuningContext` is the sole payload born at v3, so the raise is charged to **exactly this surface** after all — which is why `spec/PLAN_GENESIS_OPS.md` §4 splits G2 and isolates it in **G2b**. **And the closure argument is not the canonical base:** the base is role-bound to major 0 (`mis_stamped_canonical_base`) and is a `MaterializedState`, which embeds no graph values for *any* field — including `metadata`, op-authored since M2d and durable purely through its operations. S13 closes on that precedent: the op log is canonical, and G2b makes an operation author the tuning context. Consequence to carry forward: once G2b lands, pruning would discard *authored* genesis state rather than merely re-derivable state, so the standing prohibition on pruning (blocked on disposition C) gains real teeth. Note `bundle.rs` documents the cap of 2 *with the tuning-context rationale in prose*, so that comment must move with the cap. Note the wire layouts themselves are already frozen and correct under every disposition: this was never about how the tuning context encodes, only about which carrier embeds it. Not a regression — nothing ever worked; major 3 made the gap visible by giving the value a wire form and no way to reach it) |
|
||||
| P13-S14 | **The chunk schema *minor* is a normative MUST that no writer has ever honoured, so every appended operation discriminant is unattributable to version skew.** `binary_format.tex:2330` requires a writer to raise the chunk schema minor when it emits any discriminant appended after the minor it otherwise declares, with a stated rationale: so that a decode failure on an unknown appended discriminant is attributable to a version skew rather than to corruption. `SchemaVersion::for_major` (`bundle/src/ids.rs:204`) maps a major to a fixed constant — `V0` is `{0, 1}`, not `{0, 0}` (`ids.rs:173`), and `V1`/`V2`/`V3` carry minor 0 — and, decisively, **takes only a major**, so no per-kind additive minor can reach it — and **both** writer-side staging paths derive nothing but the major (`testkit/src/bundle_harness.rs:25`, `textproj/src/serialize.rs:183`, each mapping `max(OperationEnvelope::schema_major)` through `for_major`). So every kind appended after the initial vocabulary has no additive-version record: **24–27** (Phase-3 first tranche), **28–29** (the schema-major-2 repeat revision), **30** (Push 4a `TransposeInterval`), and **31** (genesis G1 `CreateInstrument`). The requirement's own failure mode is exactly what the gap produces: a reader meeting kind 31 from a newer writer cannot distinguish "my vocabulary is stale" from "these bytes are damaged" | this file (found 2026-07-28 during G2a contract review; verified against the working tree — `for_major`'s signature admits no minor, and neither staging path computes one) | **open, and deliberately extended.** Ruled 2026-07-28: the fix is its own rung, sequenced **after G2a and before G2b** — G2b appends kind 34, so running it first would either grow this sweep mid-flight or ship 34 with the very defect the rung retires. G2a appends kinds 32/33 and takes the violation from eight kinds to ten, knowingly — the debt is already eight deep, the +2 is immaterial against it, and one retroactive sweep over 24–33 is strictly less work than 24–31 now and 32–33 later. **What the rung owes:** a minor-assignment policy (which minor each past append belongs to — this is a retroactive judgement, not a derivation), a per-kind `schema_minor()` or equivalent, block minor = max over payloads, a `for_major` replacement that accepts one, and both staging paths. **Check before scoping it** whether the chunk header's minor reaches any pinned corpus, since that decides whether this is a vector-moving change. Note this is orthogonal to the *major* accept-set: `OperationEnvelopeBlock` stays at 2 through G2a and rises to 3 only at G2b |
|
||||
| P13-S14 | **The chunk schema *minor* is a normative MUST that no writer has ever honoured, so every appended operation discriminant is unattributable to version skew.** `binary_format.tex:2330` requires a writer to raise the chunk schema minor when it emits any discriminant appended after the minor it otherwise declares, with a stated rationale: so that a decode failure on an unknown appended discriminant is attributable to a version skew rather than to corruption. `SchemaVersion::for_major` (`bundle/src/ids.rs:204`) maps a major to a fixed constant — `V0` is `{0, 1}`, not `{0, 0}` (`ids.rs:173`), and `V1`/`V2`/`V3` carry minor 0 — and, decisively, **takes only a major**, so no per-kind additive minor can reach it — and **both** writer-side staging paths derive nothing but the major (`testkit/src/bundle_harness.rs:25`, `textproj/src/serialize.rs:183`, each mapping `max(OperationEnvelope::schema_major)` through `for_major`). So every kind appended after the initial vocabulary has no additive-version record: **24–27** (Phase-3 first tranche), **28–29** (the schema-major-2 repeat revision), **30** (Push 4a `TransposeInterval`), and **31** (genesis G1 `CreateInstrument`). The requirement's own failure mode is exactly what the gap produces: a reader meeting kind 31 from a newer writer cannot distinguish "my vocabulary is stale" from "these bytes are damaged" | this file (found 2026-07-28 during G2a contract review; verified against the working tree — `for_major`'s signature admits no minor, and neither staging path computes one) | **open, and deliberately extended.** Ruled 2026-07-28: policy ratified 2026-07-28 as a **global additive epoch with content-minimal stamping** (`spec/PLAN_GMINOR_SCHEMA_MINOR.md` §4); the fix is its own rung, sequenced **after G2a and before G2b** — G2b appends kind 34, so running it first would either grow this sweep mid-flight or ship 34 with the very defect the rung retires. G2a appends kinds 32/33 and takes the violation from eight kinds to ten, knowingly — the debt is already eight deep, the +2 is immaterial against it, and one retroactive sweep over 24–33 is strictly less work than 24–31 now and 32–33 later. **What the rung owes:** a minor-assignment policy (which minor each past append belongs to — this is a retroactive judgement, not a derivation), a per-kind `schema_minor()` or equivalent, block minor = max over payloads, a `for_major` replacement that accepts one, and both staging paths. **Scoped 2026-07-28:** the minor IS in the chunk content-address preimage (`chunk_content_hash`, `chunk.rs:177`), so raising it moves every affected `ChunkId` and the manifest body naming it — real address churn, though **not** a `req:format:manifest-id` problem, since that promise is conditional on the same manifest body. It does **not** reach the text projection: op-block schemas are discarded during projection (`textproj/src/project.rs:424`), so no companion bump. And the problem is **not** per-`OperationKind`: an envelope also emits the outer `OperationPayload` discriminant (`ResolveEquivocation` = 3 carries no kind at all), so the gating work is an audit of every append-only vocabulary reachable from an affected payload. Note this is orthogonal to the *major* accept-set: `OperationEnvelopeBlock` stays at 2 through G2a and rises to 3 only at G2b |
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
# Plan — G-minor: the chunk schema minor, and why it is not a small rung
|
||||
# Plan — G-minor: the chunk schema minor
|
||||
|
||||
**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).
|
||||
G2a and before G2b** (`spec/PLAN_GENESIS_OPS.md` §4).
|
||||
|
||||
**Status:** scoped, not contracted. §5 lists what needs ratification first.
|
||||
**Status:** scoped; policy ratified 2026-07-28 (§4). Not contracted — §5 lists
|
||||
the audit that must complete first.
|
||||
|
||||
> **Revision 2026-07-28.** The first draft of this plan got three things wrong
|
||||
> and recommended a policy that cannot work. Corrections are marked inline
|
||||
> rather than deleted, because two of them are the kind of mistake that
|
||||
> regenerates: an under-scoped vocabulary, and a corpus claim asserted from a
|
||||
> `grep -c` over *lines* rather than occurrences.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -22,135 +28,164 @@ 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".
|
||||
minor could travel through. (`V0` is `{0, 1}`; `V1`/`V2`/`V3` are `{n, 0}`.
|
||||
The baselines are already inconsistent — §4 preserves that rather than
|
||||
normalising it.)
|
||||
2. Both writer-side staging paths derive only the major
|
||||
(`testkit/src/bundle_harness.rs:32`, `textproj/src/serialize.rs:189`).
|
||||
3. So every post-baseline append ships with no additive record. The
|
||||
requirement's own failure mode is what the gap produces: a reader meeting an
|
||||
unknown discriminant cannot distinguish a stale vocabulary from damaged
|
||||
bytes.
|
||||
|
||||
## 2. The decisive scoping finding: the minor is content-addressed
|
||||
## 2. 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`) pushes
|
||||
`schema.canonical_bytes()` into the preimage, and that is "major then minor,
|
||||
little-endian" (`ids.rs:222`). `chunk_id` dispatches to the same function
|
||||
(`chunk.rs:202`).
|
||||
|
||||
`chunk_content_hash` (`bundle/src/chunk.rs:177`) builds the preimage as
|
||||
**So raising a chunk's minor changes its `ChunkId`**, which changes the
|
||||
manifest body naming it, and therefore the `ManifestId`. This is real **address
|
||||
churn** — a migration and deduplication cost — and it is the main reason the
|
||||
rung is not a patch.
|
||||
|
||||
```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);
|
||||
```
|
||||
> **Correction 1 (was P2).** The first draft framed this as tension with
|
||||
> `req:format:manifest-id`. It is not. That requirement
|
||||
> (`core_spec.tex:11213`) promises that *"two conforming writers committing
|
||||
> **the same manifest body** at the same generation of the same document derive
|
||||
> identical `ManifestId`s"*. Once a `ChunkRef` changes, the bodies are not the
|
||||
> same, so the promise does not apply. Two conforming writers both following
|
||||
> one normative minor derivation still agree — and the *historical* writer was
|
||||
> already violating the minor MUST. Churn, not a broken guarantee.
|
||||
|
||||
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`).
|
||||
> **Correction 2 (was P1).** The first draft claimed op-block minor changes
|
||||
> reach the text projection and therefore force another `COMPANION_VERSION`
|
||||
> bump. **They do not.** Operation blocks are decoded into envelopes and the
|
||||
> block's physical schema is discarded during projection
|
||||
> (`textproj/src/project.rs:424`); schemas are carried only for preserved
|
||||
> extension chunks and the canonical base (`:454`). The committed corpus holds
|
||||
> **seven** decoded `(schema 0 1)` forms in its *accepted* vectors — not the
|
||||
> six the draft claimed, an error from counting matching lines instead of
|
||||
> occurrences — and every one belongs to an extension chunk or a canonical
|
||||
> base. **No companion bump is required for op-block stamping.** A separate
|
||||
> decision to move canonical-base or extension schemas would change this;
|
||||
> the op-block sweep alone does not.
|
||||
|
||||
**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.
|
||||
**Sixty-six** occurrences of `SchemaVersion::{V0, new, for_major}` across
|
||||
sixteen files (corrected from 62), concentrated in `epiphany-bundle`,
|
||||
`epiphany-testkit`, and `epiphany-textproj`. Most are fixtures stamping `V0`
|
||||
and are unaffected; the load-bearing ones are the two staging paths.
|
||||
|
||||
**Two corpora move with it:**
|
||||
## 3. The vocabulary inventory — the part the first draft missed
|
||||
|
||||
* `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.
|
||||
> **Correction 3 (was P1), and the reason the recommended policy died.** The
|
||||
> draft reduced the problem to a per-`OperationKind` minor. An envelope also
|
||||
> emits the **independent outer `OperationPayload` discriminant**, and
|
||||
> `ResolveEquivocation` — appended at 3 — contains no `OperationKind` at all
|
||||
> (`ops/src/payload.rs:60`). So the ambiguity lives **inside one role and one
|
||||
> block**, not merely between roles, and no per-kind method can discharge the
|
||||
> MUST.
|
||||
|
||||
**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.
|
||||
`binary_format.tex:2369` states that the *only* minor-additive mechanism in
|
||||
schema major 0 is appending discriminants to open vocabularies, and enumerates
|
||||
them:
|
||||
|
||||
## 3. What the rung owes
|
||||
| Vocabulary | Appends at | Note |
|
||||
|---|---|---|
|
||||
| `OperationKind` | ≥ 30 | 24–27, 28–29 also took this mechanism |
|
||||
| `OperationKindTag` | ≥ 30 | independent space from `OperationKind` |
|
||||
| `OperationPayload` | ≥ 4 | **outer**; `ResolveEquivocation` carries no kind |
|
||||
| value-layer unions closed under `req:binfmt:frozen-layout` | — | appendable only via a ratified revision of that document; also minor-additive |
|
||||
|
||||
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.
|
||||
Two boundaries that keep the audit finite:
|
||||
|
||||
## 4. The policy question — needs ratification before contracting
|
||||
* **`ChunkKind` is closed.** It has no `Registered` variant and its
|
||||
discriminant enters every chunk's hash preimage, so a new chunk kind is a
|
||||
format-**major** event, not a minor one.
|
||||
* **`Registered` escape variants are not schema changes at all** — the wire
|
||||
form is already defined. `binary_format.tex` lists fifteen carriers
|
||||
(`RepairKind`, `ReanchorReason`, `PreconditionFailureReason`,
|
||||
`IntegrityAnomalyKind`, `TypedObjectId`, …). Extension through an escape is
|
||||
out of scope; **appending a native variant to one of those same enums is
|
||||
not**, and that distinction is the audit's sharpest edge.
|
||||
|
||||
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:
|
||||
**The rung must inventory every append-only discriminant reachable from each
|
||||
affected chunk payload**, not just `OperationKind`. For the canonical base that
|
||||
means the vocabularies `MaterializedState` actually emits — `OperationEffect`,
|
||||
`NoOpReason`, `PreconditionFailureReason`, `ConflictKind`,
|
||||
`IntegrityAnomalyKind`, `PendingReason`, `ObjectState`, `TypedObjectId` —
|
||||
which notably do **not** include `OperationKind`.
|
||||
|
||||
**(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.
|
||||
## 4. Policy — RATIFIED 2026-07-28
|
||||
|
||||
**(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.
|
||||
**A global additive epoch with content-minimal stamping.** The first draft
|
||||
recommended "minor = highest discriminant emitted"; that is rejected. It cannot
|
||||
represent multiple independent vocabularies in one block, and generalising it
|
||||
to "highest from any vocabulary" is worse than useless — an old `OperationKind`
|
||||
23 would numerically mask a newly appended `OperationPayload` 3.
|
||||
|
||||
**(c) Per-major append counter.** A hybrid: minor counts vocabulary appends
|
||||
within a major. Inherits (a)'s bookkeeping without (b)'s redefinition.
|
||||
The ratified rule:
|
||||
|
||||
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.
|
||||
1. Each additive format revision receives **one globally meaningful minor
|
||||
epoch**.
|
||||
2. Every appended variant is annotated with the epoch that introduced it.
|
||||
3. An **envelope's** required minor is the maximum across its outer payload
|
||||
variant, its primitive kind, and every nested additive variant **actually
|
||||
emitted**.
|
||||
4. A **block** takes the maximum required minor over its envelopes. The major
|
||||
remains the independent maximum of `schema_major()`.
|
||||
5. Old content keeps its major's baseline — `V0`'s existing `1`, the others'
|
||||
`0`. Baselines are not normalised.
|
||||
|
||||
**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.
|
||||
**Per-major counters are also rejected:** a mixed block can carry a new major-0
|
||||
kind beside a major-2 payload, so independently numbered minor namespaces do
|
||||
not compose after the block takes `max_major`.
|
||||
|
||||
## 5. Open questions
|
||||
**Maintenance risk, and how it is controlled.** The obvious objection to an
|
||||
epoch table is the one this track keeps proving — hand-maintained parallel
|
||||
lists go stale (four at Push 4a, six found during G2a). The control is to
|
||||
**co-locate `introduced_minor` with each discriminant in an exhaustive macro or
|
||||
match with no wildcard arm**, so a newly appended variant *cannot compile*
|
||||
without being assigned an epoch. That assignment is an unavoidable schema
|
||||
decision, not a fallible parallel list — the same reasoning that made
|
||||
`operation_kind_tag_vocabulary!` safe.
|
||||
|
||||
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.
|
||||
**Tentative epoch mapping** — Phase 3 → 2, repeats → 3, Push 4a → 4, G1 → 5,
|
||||
G2a → 6. Reasonable, but **not ratified**: it must wait until every
|
||||
post-baseline additive vocabulary is audited (§3), not just `OperationKind`.
|
||||
|
||||
*Related: `spec/PASS13_CANDIDATES.md` (P13-S14), `spec/PLAN_GENESIS_OPS.md` §4
|
||||
(the ladder), `spec/binary_format.tex` §"Schema Versioning".*
|
||||
### Consequent calls, all ruled 2026-07-28
|
||||
|
||||
* **Keep the MUST.** Its cost is lower than §2 first claimed.
|
||||
* **No text-companion bump** for op-block stamping.
|
||||
* **The canonical base does not move merely because its source operations are
|
||||
newer.** It never emits the op-kind discriminant. It retains its current
|
||||
minor while its payload uses only baseline vocabulary, and rises only when
|
||||
the `MaterializedState` bytes themselves emit a later-added discriminant —
|
||||
a newly introduced effect or reason variant, say. *(This replaces the first
|
||||
draft's "is the base exempt?", which was the wrong binary question: the base
|
||||
is neither exempt nor automatically dragged, it is content-minimally
|
||||
stamped like everything else.)*
|
||||
* **`Manifest::SCHEMA` stays unchanged.** A changed child `ChunkRef` is changed
|
||||
manifest *data*, not a new manifest-layout discriminant; its body, id, and
|
||||
chunk hash move naturally without raising the manifest's own schema.
|
||||
* **No migration.** Existing bundles are accepted as-is; newly emitted or
|
||||
repacked blocks are stamped correctly.
|
||||
* **Scope is "all additive discriminants reachable in affected chunk
|
||||
payloads"**, not "kinds 24–33".
|
||||
|
||||
## 5. What must complete before a contract
|
||||
|
||||
1. **The vocabulary audit** (§3). This is now the gating work, and it is the
|
||||
only thing standing between here and a contract. Its output is the epoch
|
||||
annotation for every post-baseline additive variant in every vocabulary
|
||||
reachable from an affected payload.
|
||||
2. Ratify the epoch mapping once that audit lands.
|
||||
3. Confirm no third staging path has appeared beside the two in §1.
|
||||
4. Decide whether `decode_vectors.txt` moves — it is value-level, so it should
|
||||
not, but that is a check rather than an assumption.
|
||||
|
||||
*Related: `spec/PASS13_CANDIDATES.md` (P13-S14), `spec/PLAN_GENESIS_OPS.md` §4,
|
||||
`spec/binary_format.tex` §"Schema Versioning" / §"What ``Additive'' Means
|
||||
Here".*
|
||||
|
|
|
|||
Loading…
Reference in New Issue