Contract G2a: the split, and the two setters that move no wire bound
G2 was scoped as one packet of three LWW setters. Reading the frozen codec walks rather than the type labels shows they do not sit at the same major: SpellingPrecedence has never been versioned, CanvasLayoutDefaults is versioned in the containing Canvas walk and not the leaf, and only ScoreTuningContext is born at v3. So the accept-set raise — a one-way door — is charged to one surface, not amortised across nine as the ruling's framing implied. G2 splits: G2a is the two major-0 setters and touches epiphany-bundle not at all; G2b is SetTuningContext alone, carrying the raise and the S13 close. Withdraws plan trap 5. SetMetadata already answers it: Score::empty seeds metadata exactly as it seeds tuning_context, the base ingest seeds the LWW chain from it, and restoring that seed is correct for both never-authored and authored-to-default. These are always-valued fields, not map keys, so the Predecessor::Base/::Write distinction that matters for spellings and breaks does not apply here. Records the G1 lesson as a trap in its own right: an OperationKind variant is not containable to core+ops, and the G2a contract budgets all three downstream literal sites up front instead of discovering them mid-dispatch. Corrects S13's amortisation claim, and states the closure argument properly — the canonical base is a MaterializedState that embeds no graph values for any field, so S13 closes on the metadata precedent, not on the base. Notes the consequence: after G2b, pruning would discard authored state. 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
3b09595196
commit
be40eb2d7d
|
|
@ -0,0 +1,288 @@
|
|||
# Contract: genesis tranche G2a — the two major-0 settings setters
|
||||
|
||||
Repo root `/home/jeans/Repos/active/epiphany`. Governed by
|
||||
`spec/RULING_GENESIS_PERSISTENCE.md` (011c68a) and `spec/PLAN_GENESIS_OPS.md` §4
|
||||
(ladder ratified 2026-07-24, split into G2a/G2b 2026-07-28). Predecessor:
|
||||
`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md`, landed at 3b09595.
|
||||
|
||||
Execution model as every tranche on this track: Sonnet subagent, coordinator
|
||||
line-level review with **independent mutation re-runs**, user deep-dive at
|
||||
contract sign-off and final report. Mutation discipline throughout: anchor-assert
|
||||
before substituting, restore by reversing, never `git checkout`.
|
||||
|
||||
**Parallel safety.** The editor track owns `epiphany-editor-gui`,
|
||||
`epiphany-render-svg`, `epiphany-glyphs`, every
|
||||
`crates/epiphany-editor-gui/goldens/*.png`, `spec/PLAN_EDITOR_APP.md`,
|
||||
`spec/CONTRACT_EDITOR_*.md`, and `spec/ANALYSIS_TEXT_RUN_PRIMITIVES.md`
|
||||
(currently untracked — do not stage it). **This packet does cross into
|
||||
`epiphany-editor-core` and `epiphany-layout-ir`**, by necessity, at the two
|
||||
sites named in §"The boundary crossing". That crossing is bounded to those two
|
||||
files and is authorized for this packet only. Stage files explicitly; never
|
||||
`git add -A`.
|
||||
|
||||
---
|
||||
|
||||
## What this packet does, in one sentence
|
||||
|
||||
Adds two LWW settings operations — `SetCanvasLayoutDefaults` and
|
||||
`SetSpellingPrecedence` — on the `SetMetadata` pattern, so that two more of the
|
||||
ruling's nine `Score` surfaces become operation-authored, **without touching any
|
||||
wire bound**.
|
||||
|
||||
## Why these two, and why not the third
|
||||
|
||||
G2's three setters do not sit at the same schema major. Verified by reading the
|
||||
frozen codec walks, not by inference:
|
||||
|
||||
| Op | Carried type | Major | Evidence |
|
||||
|---|---|---|---|
|
||||
| `SetSpellingPrecedence` | `SpellingPrecedence` | **0** | plain `Codec::dec` at v0/v1/v2 and live (`core/src/codec.rs:2673`, `:3249`, `:3372`) — never versioned |
|
||||
| `SetCanvasLayoutDefaults` | `CanvasLayoutDefaults` | **0** | versioning lives in the containing `Canvas` walk, not the leaf: `dec_canvas_v0` default-fills the field (`codec.rs:2775`), `enc_canvas_v1` writes it through the live `Codec` (`:3158`) |
|
||||
| `SetTuningContext` | `ScoreTuningContext` | **3** | `enc_tuning_context_v2` writes 3 fields (`codec.rs:3293`), the live `Codec` writes 5; a default `smufl` + empty `overrides` still append bytes |
|
||||
|
||||
`SetTuningContext` alone drags `max_supported_major(OperationEnvelopeBlock)`
|
||||
from 2 to 3, and that raise is a one-way door — an older reader meeting a v3
|
||||
block preserves the bundle read-only. It is **G2b**, a separate packet, and this
|
||||
one must not anticipate it in any way.
|
||||
|
||||
## Design pins
|
||||
|
||||
1. **No wire layout is designed.** Both carried types already have `Codec` impls
|
||||
and already ship inside `Score`. Add `CanvasLayoutDefaults` and
|
||||
`SpellingPrecedence` to `canonical_value!` (`core/src/codec.rs:3455`) and
|
||||
encode each payload as `push_lp_bytes(out, &self.<field>.canonical_bytes())`
|
||||
— the `SetMetadataOp` template verbatim (`ops/src/payload.rs:1360`). The
|
||||
generated `decode_canonical` already does decode → `finish()` → re-encode →
|
||||
reject-on-mismatch, so strict-form enforcement is inherited, not written.
|
||||
2. **Discriminants: kind 32/33, tag 32/33.** Both spaces currently top out at 31
|
||||
(`CreateInstrument`). Assign `SetCanvasLayoutDefaults` = 32,
|
||||
`SetSpellingPrecedence` = 33 in **both** spaces. They coincide here by
|
||||
accident, not by rule — the spaces are independent and misaligned elsewhere
|
||||
(`RespellPitch` is kind 2 / tag 3). Append-only under
|
||||
`req:binfmt:kind-discriminants`.
|
||||
3. **`schema_major()` gains NO arm.** Both fall into the existing catch-all
|
||||
`_ => 0` (`payload.rs:262`). **Adding them to the `=> 2` arm alongside
|
||||
`SetMetadata` would be the bug** — `SetMetadata` is there because
|
||||
`ScoreMetadata` has six mandatory major-2 appends, which is a property of
|
||||
*that* type and nothing else. Test i5 exists to catch exactly this.
|
||||
4. **No `epiphany-bundle` change of any kind.**
|
||||
`max_supported_major(OperationEnvelopeBlock)` is 2 (`bundle.rs:69`) and
|
||||
**stays 2**. Do not edit `bundle.rs`, and do not touch its cap comment at
|
||||
`:58` — that prose belongs to G2b and moving it early leaves a different
|
||||
falsehood behind.
|
||||
5. **Reduction copies `set_metadata` structurally** (`reduce.rs:2814`): advisory
|
||||
LWW, **no conflict, no idempotence short-circuit**, no `AlreadyApplied`. The
|
||||
write chain records unconditionally; the graph field is overwritten when a
|
||||
graph is present. Do **not** import the `create_staff` mint discipline
|
||||
(`AlreadyApplied` / `RecreateContentMismatch`) — these are field overwrites,
|
||||
not mints, and a re-write of an identical value is a legitimate new write.
|
||||
6. **Both chains must be seeded from the base**, beside
|
||||
`metadata_chain.seed(...)` (`reduce.rs:1385`). This is what makes
|
||||
value-restoring undo of the *first* operational write restore the
|
||||
pre-operational value rather than nothing. Under from-empty the base is
|
||||
`Score::empty`, so the seed is the type's `Default` — and restoring the
|
||||
default is correct for both the never-authored and the authored-to-default
|
||||
case. There is no "never authored" state to distinguish: these are
|
||||
always-valued `Score` fields, not map keys, so the `Predecessor::Base` vs
|
||||
`::Write` distinction that matters for `spellings`/`breaks`
|
||||
(`reduce.rs:707`) does **not** apply. Do not invent an `Option` wrapper.
|
||||
7. **Undo plumbing is three sites per setter, and one is easy to miss.** Mirror
|
||||
`metadata_chain` at: the `undo_verdict` walk (`reduce.rs:5184`), the
|
||||
`ValueRestoration` enum variant (`:785`), and the restoration *application*
|
||||
(`:5404`). Missing the application site fails silently — the verdict is
|
||||
computed and discarded.
|
||||
8. **The snapshot/restore pair.** Both chains must appear in the
|
||||
snapshot/restore pair alongside `metadata_chain` (`reduce.rs:7386`, `:7423`).
|
||||
**This is the G1 silent-failure site**: omitting it surfaces only under
|
||||
undo/replay, never in a straight-line test.
|
||||
9. **No deletes.** These are field overwrites; there is nothing to tombstone.
|
||||
|
||||
## The boundary crossing
|
||||
|
||||
Per `PLAN_GENESIS_OPS.md` §5 trap 6 — the G1 lesson, budgeted up front rather
|
||||
than discovered mid-dispatch. Adding an `OperationKind` variant is **not**
|
||||
containable to core + ops:
|
||||
|
||||
* **`crates/epiphany-editor-core/src/barriers.rs:468`** — `subjects_of` is
|
||||
exhaustive; without arms the workspace does not compile, and because
|
||||
`epiphany-testkit` depends on editor-core, **the entire gate is blocked** —
|
||||
conformance and `requirement_labels` included. Both new kinds are score-level
|
||||
field overwrites with no resolvable region or object, exactly like
|
||||
`SetMetadata`: **join the existing `OperationKind::SetMetadata(_) |
|
||||
OperationKind::DeclareTransaction(_)` arm** rather than writing new ones.
|
||||
That file's module doc (`:22`) already names `SetMetadata` as the
|
||||
score-level exemplar; extend that sentence.
|
||||
* **`crates/epiphany-layout-ir/src/barrier.rs:1105`** — the "one past the
|
||||
vocabulary" literal, currently `32`, becomes `34`. It is deliberately a
|
||||
literal rather than `PAYLOAD_FREE.len()` so the bump is a conscious act; keep
|
||||
it that way and update the comment's kind list.
|
||||
* **`crates/epiphany-testkit/tests/text_projection_grammar.rs:307`** — a
|
||||
hardcoded kind *count*, currently `32`, becomes `34`. Its own comment explains
|
||||
why it stays a literal; do not "fix" it into a derivation.
|
||||
|
||||
## The companion version bump
|
||||
|
||||
Two new **kind** productions are a document-surface grammar change, so
|
||||
`COMPANION_VERSION` moves **0.8.0 → 0.9.0** (`textproj/src/lib.rs:29`). This is
|
||||
the G1 precedent and it is not optional: holding the version while extending the
|
||||
grammar leaves two incompatible grammars claiming `(0 8 0)`, and
|
||||
`req:textproj:header-version` requires a parser to accept exactly the version it
|
||||
implements and reject all others. Cached projections do not migrate —
|
||||
`TextProjection` is a non-canonical accelerator, so stale ones regenerate.
|
||||
|
||||
Sites: `textproj/src/lib.rs:29` (the constant and its doc block), and in
|
||||
`spec/text_projection.tex` the five live sites at `:237`, `:470`, `:521`,
|
||||
`:1121`, `:1309` plus a **new** changelog row.
|
||||
|
||||
**Do not bulk-replace `0.8.0` / `(0 8 0)` across the `.tex`.** The 0.8.0
|
||||
changelog row is history and must keep its version; a blind sweep falsified
|
||||
exactly that row during G1 and had to be reverted. Edit the five live sites
|
||||
individually and append the new row.
|
||||
|
||||
Also flip `textproj/src/vectors.rs`'s negative
|
||||
`superseded_companion_version` vector: its "wrong version" must become the newly
|
||||
superseded `(0 8 0)`, since `(0 9 0)` is now the accepted one.
|
||||
|
||||
## Touch points
|
||||
|
||||
Derived by enumerating every `SetMetadata` site. Each row is **per setter**
|
||||
unless noted.
|
||||
|
||||
| # | File | Site (SetMetadata analogue) |
|
||||
|---|---|---|
|
||||
| 1 | `core/src/codec.rs` | `canonical_value!` — add both carried types (`:3455`) |
|
||||
| 2 | `ops/src/payload.rs` | op struct + `CanonicalEncode` (`:1356`, `:1360`) |
|
||||
| 3 | `ops/src/payload.rs` | `OperationKind` variant (`:167`) |
|
||||
| 4 | `ops/src/payload.rs` | `discriminant()` → 32 / 33 (`:289`) |
|
||||
| 5 | `ops/src/payload.rs` | `tag()` (`:336`) + encode dispatch (`:385`) |
|
||||
| 6 | `ops/src/payload.rs` | `OperationKindTag` variant (`:427`) + `operation_kind_tag_vocabulary!` entry (`:533`) — **compile-enforced** |
|
||||
| 7 | `ops/src/payload.rs` | `schema_major()` — **no edit** (pin 3) |
|
||||
| 8 | `ops/src/envdecode.rs` | discriminant decode (`:530`) + tag→kind (`:819`) |
|
||||
| 9 | `ops/src/v0.rs` | `V0OperationKind` variant (`:92`) |
|
||||
| 10 | `ops/src/migrate.rs` | both directions (`:167`, `:323`) |
|
||||
| 11 | `ops/src/reduce.rs` | dispatch (`:2736`) + the setter fn beside `set_metadata` (`:2814`) |
|
||||
| 12 | `ops/src/reduce.rs` | chain decls (`:901`, `:1012`), init (`:1292`), base seed (`:1385`) |
|
||||
| 13 | `ops/src/reduce.rs` | undo verdict (`:5184`), `ValueRestoration` variant (`:785`), restoration apply (`:5404`) |
|
||||
| 14 | `ops/src/reduce.rs` | snapshot / restore (`:7386`, `:7423`) |
|
||||
| 15 | `ops/src/textproj_kind.rs` | production (`:174`) + parse (`:443`) |
|
||||
| 16 | `ops/src/fuzz.rs` | generator arm (`:197`) |
|
||||
| 17 | `ops/src/valuegen.rs` | LWW generator (`:325`) |
|
||||
| 18 | `ops/src/vectors.rs` | a decode vector per new payload |
|
||||
| 19 | `editor-core/src/barriers.rs` | join the score-level arm (`:468`) + module doc (`:22`) |
|
||||
| 20 | `layout-ir/src/barrier.rs` | literal 32 → 34 (`:1105`) |
|
||||
| 21 | `testkit/tests/text_projection_grammar.rs` | count 32 → 34 (`:307`) |
|
||||
| 22 | `textproj/src/lib.rs` | `COMPANION_VERSION` → `(0, 9, 0)` (`:29`) |
|
||||
| 23 | `textproj/src/vectors.rs` | flip the superseded-version negative vector |
|
||||
| 24 | `spec/text_projection.tex` | `kind` production + five version sites + changelog row |
|
||||
| 25 | `spec/operation_catalog.tex` | §`SetCanvasLayoutDefaults`, §`SetSpellingPrecedence` |
|
||||
|
||||
The tag vocabulary macro (#6) is a **single source of truth**: the decoder, fuzz
|
||||
corpus, conformance vectors, and edit-barrier round-trip all read
|
||||
`PAYLOAD_FREE`, and a variant missing an entry **fails to compile**. It exists
|
||||
because Push 4a added `TransposeInterval` to a hand-written match and nothing
|
||||
else, and four hand-maintained lists stayed green while the decoder rejected its
|
||||
own encoding. `OperationKind::discriminant()` (#4) has **no such guard**.
|
||||
|
||||
## Tests + minimum mutations
|
||||
|
||||
Every test must be mutation-verified: re-introduce the bug it exists to catch and
|
||||
show it dies. A test that cannot see its own bug is not a test.
|
||||
|
||||
* **(s1) from-empty authoring.** From `Score::empty(identity)` through
|
||||
`reduce_operation_set_onto`, each setter produces the authored value in the
|
||||
materialized `Score`. **Mutation:** drop the graph write in the setter fn →
|
||||
the field stays at its `Default` while the effect still reads `Applied`.
|
||||
* **(s2) LWW, last write wins, no conflict.** Two concurrent differing writes
|
||||
resolve to the later in canonical order and record **no** conflict — matching
|
||||
`SetMetadata` (`ops/tests/graph_reduction.rs:1408`). **Mutation:** reverse the
|
||||
canonical comparison → the earlier value wins.
|
||||
* **(s3) re-write of an identical value is a new write, not a no-op.** Assert
|
||||
the effect is `Applied` and the chain grew. **Mutation:** add an
|
||||
`AlreadyApplied` short-circuit → dies. *This test exists because pin 5 is the
|
||||
most likely thing for a subagent to get wrong by pattern-matching on
|
||||
`create_staff`.*
|
||||
* **(s4) value-restoring undo reaches the seeded base.** Author once inside a
|
||||
transaction, undo it, and assert the field returns to the **base** value —
|
||||
run this both from-empty (base = `Default`) and onto a loaded base with a
|
||||
non-default value, so the test distinguishes "restored the base" from
|
||||
"restored the type default". **Mutation:** remove the `.seed(...)` call →
|
||||
the undo produces `Restore(None)` and the field does not move.
|
||||
* **(s5) minimal stamping stays 0.** Both kinds report `schema_major() == 0`
|
||||
for every value, including a non-default one. Extend
|
||||
`schema_majors_follow_the_minimal_stamping_rule` (`reduce.rs:10546`).
|
||||
**Mutation:** move either kind into the `=> 2` arm → dies.
|
||||
* **(s6) the accept-set did not move.** Assert
|
||||
`max_supported_major(ChunkKind::OperationEnvelopeBlock) == 2`, and assert a
|
||||
block carrying either new kind stamps at major 0. **Mutation:** stamp a
|
||||
payload at 3 → the assertion fires. *This is the packet's boundary against
|
||||
G2b.*
|
||||
* **(s7) undo survives snapshot/restore** — the pin-8 site. Snapshot the
|
||||
reducer mid-run, restore, then undo. **Mutation:** omit either chain from the
|
||||
snapshot/restore pair → the restored reducer loses the write history and the
|
||||
undo silently no-ops.
|
||||
* **(s8) decode vectors pinned to literal bytes**, not round-trip. Round-trip
|
||||
locking cannot see a self-consistent encoder/decoder reorder — the 3b-i
|
||||
lesson, where a swap applied to both halves passed 1283 tests and 8/8
|
||||
conformance.
|
||||
* **(s9) text-projection round-trip** for both kinds, matching the existing
|
||||
per-kind coverage, plus a negative test that a `(0 8 0)` header is now
|
||||
**rejected**.
|
||||
|
||||
## Blast radius
|
||||
|
||||
`crates/epiphany-core/src/codec.rs` + its `DECISIONS.md`;
|
||||
`crates/epiphany-ops/src/{payload,envdecode,v0,migrate,reduce,textproj_kind,fuzz,valuegen,vectors}.rs`
|
||||
+ its `DECISIONS.md`; `crates/epiphany-textproj/src/{lib,vectors}.rs`;
|
||||
`crates/epiphany-editor-core/src/barriers.rs` and
|
||||
`crates/epiphany-layout-ir/src/barrier.rs` (the two authorized crossings only);
|
||||
`crates/epiphany-testkit/tests/text_projection_grammar.rs`;
|
||||
`spec/{operation_catalog,text_projection}.tex` **and their rebuilt PDFs**;
|
||||
`spec/vectors/*.txt` (regenerated, not hand-edited).
|
||||
|
||||
**Nothing else.** No `epiphany-bundle`, no `binary_format.tex`, no
|
||||
`epiphany-editor-gui`, no golden re-blessing.
|
||||
|
||||
Expect `the_canonical_base_is_byte_identical_across_data_model_majors`
|
||||
(`reduce.rs:10715`) to require a **conscious re-pin**: the seeded corpus's
|
||||
`gen_payload` gains discriminants 32/33, shifting the RNG stream, exactly as at
|
||||
Phase D and at G1. That is a corpus shift, not a value leak. **State in the
|
||||
report which it is, and how you distinguished them** — a genuine leak of a
|
||||
settings value into the canonical base would be a ruling violation, and the
|
||||
easy mistake is to re-pin without checking.
|
||||
|
||||
## Gate (report actual output, never stale numbers)
|
||||
|
||||
`cargo fmt --check`; `cargo clippy --workspace --all-targets -D warnings` at
|
||||
**0** warnings; `cargo test --workspace` at **0** failed; `cargo test --doc` at
|
||||
0 failed; conformance **8/8**, and **9/9** with `--features golden-gate`;
|
||||
`requirement_labels` **6/6** with its three observed counts reported as seen
|
||||
(212/282/282 at 439e1e2; they move with the catalog sections). Plus:
|
||||
|
||||
* `max_supported_major(OperationEnvelopeBlock)` is **still 2** — assert it in
|
||||
code and state it in the report.
|
||||
* Vector corpora **regenerated** via
|
||||
`cargo run -q -p epiphany-testkit --example generate_vectors`, never
|
||||
hand-edited, with the decode-vector count reported before and after.
|
||||
* Both `.tex` PDFs rebuilt (`latexmk -xelatex`) with **0 undefined references**
|
||||
reported. A `.tex` commit without its PDF has been a repeat lapse.
|
||||
* No `crates/epiphany-editor-gui/goldens/*.png` byte changes.
|
||||
|
||||
## What I will verify independently before committing
|
||||
|
||||
Build to survive this. I re-run every mutation myself, and I check specifically:
|
||||
that #4's hand-written discriminant match and #6's macro entry agree; that pin 3
|
||||
really added no `schema_major` arm; that pin 8's snapshot/restore pair was not
|
||||
missed; that s3 fails for the stated reason rather than incidentally; that the
|
||||
canonical-base re-pin is a corpus shift and not a value leak; that the `.tex`
|
||||
0.8.0 changelog row was **not** rewritten; and that no claim in the report is
|
||||
copied forward from this contract rather than observed — the recurring failure
|
||||
mode on this project is a plausible claim propagating because nobody re-derived
|
||||
it.
|
||||
|
||||
## Report
|
||||
|
||||
Files + summary, exact asserted values, every mutation with kill evidence, gate
|
||||
output verbatim, deviations flagged explicitly. If any pin here turns out to be
|
||||
wrong, say so rather than working around it silently. In particular: if the
|
||||
boundary crossing turns out to be wider than the three sites named above, that
|
||||
is a finding about the contract, not a nuisance — report it.
|
||||
|
|
@ -90,4 +90,4 @@ visible — a value with a wire form and no canonical carrier to reach it.)
|
|||
| P13-S10 | **`PitchSpaceModification::Cents(f64)` puts a raw `f64` in canonical state, which the byte layer cannot encode.** `core_spec.tex:3112` declares `Cents(f64)`; `PitchSpaceModification` is reached from canonical score state through `AccidentalDefinition` → `ScoreAccidentalExtensions` → `ScoreTuningContext`. `req:determinism:canonical-floating-point` requires canonical stored floats to be finite IEEE 754 binary64, and the byte layer enforces it with no escape hatch: `serialize.rs:110` decodes floats *only* through `CanonicalF64::from_le_bytes → NonFiniteFloat`, so there is no `Codec for f64`. A raw `f64` is therefore not merely risky in canonical state — it is unencodable without inventing a new unvalidated codec. The same chapter already resolved this exact tension forty lines later: `EngravingBoundingBox` (`:3150`) carries `SpaceUnit` edges with a rationale (`:3191`) invoking the same requirement | this file (verified 2026-07-23 against `core_spec.tex:3112`, `:3150-3197`, and `crates/epiphany-determinism/src/serialize.rs:110`) | **resolved** (ratified: `Cents(CanonicalF64)`. A one-line spec correction, the only shape consistent with the existing byte layer — the identical maneuver Ruling D applied to the bounding box. Freezes in tranche 3b) |
|
||||
| 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:** the ruling's "one accept-set raise, spent once" lands every new kind as a single batch at `OperationEnvelopeBlock` major 3, amortising the raise across nine surfaces instead of charging it to this one. 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-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) |
|
||||
|
|
|
|||
|
|
@ -6,7 +6,10 @@ reverses Pass-12 K8 and makes every mutable field of `Score` operation-authored.
|
|||
This plan is the execution scope: what the tranche touches, in what order, and
|
||||
which questions must be answered before a dispatch contract can be written.
|
||||
|
||||
**Status:** scoped, not dispatched. §6 lists what needs ratification first.
|
||||
**Status:** **G1 landed** (3b09595, CI green) via
|
||||
`spec/CONTRACT_GENESIS_G1_INSTRUMENT.md`. **G2a contracted**
|
||||
(`spec/CONTRACT_GENESIS_G2A_SETTINGS.md`); G2b and G3 scoped, not contracted.
|
||||
§6 lists what still needs ratification.
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -122,20 +125,68 @@ G1 also carries the two constraints that are not per-surface:
|
|||
No accept-set raise. No wire change. Highest value per unit of risk in the whole
|
||||
tranche.
|
||||
|
||||
### G2 — the three settings setters
|
||||
### G2 — the settings setters, split in two
|
||||
|
||||
`SetCanvasLayoutDefaults`, `SetSpellingPrecedence`, `SetTuningContext` on the
|
||||
`SetMetadata` LWW pattern (`reduce.rs:2713`), seeded for value-restoring undo
|
||||
(`:1357`). This is where the **one accept-set raise is spent**: `bundle.rs`'s
|
||||
`max_supported_major(OperationEnvelopeBlock)` moves 2 → 3.
|
||||
All three ride the `SetMetadata` LWW pattern (`reduce.rs:2814`), seeded for
|
||||
value-restoring undo (`:1385`). But they do **not** sit at the same major, and
|
||||
only one of them is a compatibility event. Verified 2026-07-28 against the
|
||||
working tree:
|
||||
|
||||
Note `bundle.rs:58` documents the current cap **with the tuning-context
|
||||
rationale in prose** — "no operation payload embeds the tuning context, so no op
|
||||
block is ever born at v3". That comment becomes false the moment this lands and
|
||||
must move with the cap. Same for `DECISIONS.md`'s superseded prohibition, which
|
||||
is already marked.
|
||||
* `SetSpellingPrecedence` → `SpellingPrecedence`. Major **0**: the frozen
|
||||
`decode_v0_score`/`decode_v1_score`/`decode_v2_score` walks and the live
|
||||
`Codec` all read this field through plain `Codec::dec` (`codec.rs:2673`,
|
||||
`:3249`, `:3372`) — it has never been versioned.
|
||||
* `SetCanvasLayoutDefaults` → `CanvasLayoutDefaults`. Major **0**. The type is
|
||||
labelled "schema major 1" (`graph.rs:836`), but the versioning lives in the
|
||||
*containing* `Canvas` walk, not the leaf: `dec_canvas_v0` (`codec.rs:2775`)
|
||||
default-fills the whole field while `enc_canvas_v1` (`:3158`) writes it
|
||||
through the live `Codec`. As a standalone payload it has exactly one layout.
|
||||
* `SetTuningContext` → `ScoreTuningContext`. Major **3**, and
|
||||
**unconditionally** so. `enc_tuning_context_v2` (`codec.rs:3293`) writes three
|
||||
fields; the live `Codec` writes five. A default `smufl` and an empty
|
||||
`overrides` still append bytes, so there is no value for which a lower-major
|
||||
layout exists — this is a `CreateInstrument`-shaped arm, not a
|
||||
`CreateRegion`-shaped one.
|
||||
|
||||
**P13-S13 closes here.**
|
||||
So the split is not tidiness. Two of the three move no wire bound at all, and
|
||||
the accept-set raise is a **one-way door**: once a block can be born at v3, an
|
||||
older reader meeting one preserves the bundle read-only (`bundle/src/error.rs:257`).
|
||||
Spending that in the same packet as two major-0 leaf setters buries it.
|
||||
|
||||
**G2a — `SetCanvasLayoutDefaults` + `SetSpellingPrecedence`.** Both land in
|
||||
`schema_major()`'s catch-all `_ => 0` arm (`payload.rs:262`) with **no arm
|
||||
added**; adding them to the `=> 2` arm would be the bug. No `epiphany-bundle`
|
||||
change of any kind.
|
||||
|
||||
**G2b — `SetTuningContext` alone**, carrying the raise, the `bundle.rs` prose,
|
||||
and the S13 close. `bundle.rs:58` documents the current cap **with the
|
||||
tuning-context rationale in prose** — "no operation payload embeds the tuning
|
||||
context, so no op block is ever born at v3". G2b is precisely what falsifies
|
||||
that sentence, so the comment must move with the number. Same for
|
||||
`DECISIONS.md`'s superseded prohibition, which is already marked.
|
||||
|
||||
**The cost of splitting, stated honestly:** each packet appends *kind*
|
||||
productions to the text-projection grammar, and a kind append is a
|
||||
document-surface change (the G1 precedent). So the companion bumps twice —
|
||||
0.8.0 → 0.9.0 → 0.10.0 — and each bump re-sweeps five live version sites in
|
||||
`text_projection.tex` plus a changelog row, re-flips the negative
|
||||
`superseded_companion_version` vector, and regenerates the vector corpora.
|
||||
That is mechanical and pre-1.0; it is the cheaper of the two risks.
|
||||
|
||||
**P13-S13 closes at G2b — and on the metadata precedent, not on the canonical
|
||||
base.** The base cannot carry a v3 tuning context: it is role-bound to major 0
|
||||
(`mis_stamped_canonical_base`, `bundle.rs:866`). It does not need to. The
|
||||
canonical base is a `MaterializedState` (`reduce.rs:504`) — effects, conflicts,
|
||||
anomalies, objects, spellings, breaks, page-breaks, pending — which embeds **no
|
||||
graph values for any field**, including `metadata`, op-authored since M2d and
|
||||
durable purely through its operations. S13's claim was "no canonical carrier
|
||||
embeds it at all: no operation authors it". G2b makes an operation author it,
|
||||
and the op log is canonical.
|
||||
|
||||
**What that sharpens.** The standing prohibition on pruning (blocked on
|
||||
disposition C) stops being a performance concern the moment G2b lands: pruning
|
||||
would then discard authored genesis state, not merely re-derivable state. G2b's
|
||||
contract must state this as an explicit non-goal.
|
||||
|
||||
### G3 — the remaining entity families
|
||||
|
||||
|
|
@ -168,9 +219,37 @@ Consider splitting it out if G3 runs long.
|
|||
3b-i: a swap applied to both codec halves passed 1283 tests and 8/8
|
||||
conformance. New payloads want decode-vector entries pinned to literal bytes,
|
||||
not just round-trip tests.
|
||||
5. **`Score::empty` seeds `tuning_context` with a default**, so
|
||||
5. ~~**`Score::empty` seeds `tuning_context` with a default**, so
|
||||
`SetTuningContext`'s reduction must distinguish "never authored" from
|
||||
"authored to the default value" if undo is to restore correctly.
|
||||
"authored to the default value" if undo is to restore correctly.~~
|
||||
**Withdrawn 2026-07-28 — this is not a trap, and `SetMetadata` already
|
||||
proves it.** `Score::empty` seeds `metadata` with a default exactly as it
|
||||
seeds `tuning_context` (`graph.rs:1749`, `:1757`), and the base ingest then
|
||||
runs `metadata_chain.seed(score.metadata.clone())` (`reduce.rs:1385`) under
|
||||
a comment stating the purpose outright: the score-level LWW chains seed with
|
||||
the base values so a value-restoring undo of the *first* operational write
|
||||
restores the pre-operational state. Since from-empty reduces **onto**
|
||||
`Score::empty` (trap-free only through `reduce_operation_set_onto` — G1 pin
|
||||
10), the seed runs, and undoing the first write yields
|
||||
`Restore(Some(Predecessor::Base(default)))`. Restoring the default is
|
||||
correct in both the never-authored and the authored-to-default case, so the
|
||||
distinction is unobservable **and must stay so**. The `Predecessor::Base`
|
||||
vs `::Write` distinction earns its keep only for the canonical bookkeeping
|
||||
families (`spellings`, `breaks`, `page_breaks`, `reduce.rs:707`), where a
|
||||
base predecessor returns a *map key* to absence. `ScoreTuningContext` is an
|
||||
always-valued `Score` field, like metadata: there is no absent state to
|
||||
return to. All three G2 setters copy `set_metadata` structurally.
|
||||
6. **Adding an `OperationKind` variant is NOT containable to core + ops** —
|
||||
the G1 lesson, and the one claim this plan previously got wrong. Rust
|
||||
exhaustiveness forces an arm in `epiphany-editor-core`'s `subjects_of`
|
||||
(`barriers.rs:437`), and because `epiphany-testkit` depends on editor-core,
|
||||
a missing arm blocks conformance *and* `requirement_labels` — the gate
|
||||
cannot run at all. Three further sites bake in a literal that only surfaces
|
||||
once the workspace compiles: `layout-ir/src/barrier.rs:1105` (a tag
|
||||
"one past the vocabulary"), `testkit/tests/text_projection_grammar.rs:307`
|
||||
(a hardcoded kind *count*), and `textproj/src/vectors.rs` (a negative vector
|
||||
whose "wrong version" is the one each bump moves to). Every G2/G3 contract
|
||||
MUST enumerate these and budget the boundary crossing up front.
|
||||
|
||||
## 6. Open rulings — needed before a dispatch contract
|
||||
|
||||
|
|
@ -186,7 +265,10 @@ Consider splitting it out if G3 runs long.
|
|||
3. **The measure/meter invariant.** Measures are authored, not derived (ruling
|
||||
§2), so measure/meter consistency becomes an authoring obligation backed by a
|
||||
graph invariant. That invariant needs specifying — it belongs in G3.
|
||||
4. **Ladder shape.** G1/G2/G3 as above, or a different cut.
|
||||
4. ~~**Ladder shape.** G1/G2/G3 as above, or a different cut.~~ **Ratified
|
||||
2026-07-24**, and amended 2026-07-28: G2 splits into **G2a** (the two
|
||||
major-0 setters) and **G2b** (`SetTuningContext` alone, carrying the
|
||||
accept-set raise and the S13 close). See §4.
|
||||
|
||||
*Related: `spec/RULING_GENESIS_PERSISTENCE.md`, `spec/ANALYSIS_GENESIS_PERSISTENCE.md`,
|
||||
`spec/PLAN_EDITOR_APP.md` §Ruling B / §3.7, `spec/PLAN_PUSH4B_TUNING.md` (the
|
||||
|
|
|
|||
Loading…
Reference in New Issue