The operation set absorbs genesis
Ratified 2026-07-24, resolving Ruling B blocker (i). Pass-12 K8 is reversed: every mutable field of Score becomes operation-authored, and a document is Score::empty(identity) plus its envelope log. The instrument, staff, staff instance, voice, event chain is authorable end to end. The decision removes machinery rather than adding it. Every alternative kept genesis outside the operation set and then had to pay for that: a new chunk role, a manifest field, an immutability rule, and a merge or fail-closed rule for a canonical payload with no CRDT semantics. Genesis state is edited — instruments get added, page geometry changes, temperaments are chosen — and each alternative made those edits single-writer, unmergeable, or impossible. Concurrency is a first-order product commitment, so the exception was not worth institutionalising in the format. Scope is nine surfaces over two templates already proven in reduce.rs: three LWW settings setters on the SetMetadata pattern (canvas.layout_defaults, tuning_context, spelling_precedence) and six entity mint families on the CreateStaff pattern (instruments, staff_groups, parts, analysis_layers, views, and StaffInstance.measures), each with graph-aware referential preconditions. Delete and modify coverage is left to the tranche contract rather than assumed, since CreateStaff itself ships today with no DeleteStaff. Measures are ruled authored rather than derived. TimeAnchor::Measure carries a measure id that cross-cutting structures anchor to, so deriving measures from the metric grid would make their identity a function of the meter and every time-signature change would orphan the anchors pointing into them. The cost accepted is that measure/meter consistency becomes an authoring obligation backed by a graph invariant. Three constraints are written in rather than left implicit. Pruning may not be implemented until the canonical base carries graph values: a prune installs a MaterializedState base whose effects are outcomes, not payloads, so nothing rebuilds the score afterward — silent and total, and free to prohibit now because no prune exists to break. The from-empty path must reduce through new_onto with an empty Score rather than base-free, because the base-free mode skips graph-aware preconditions by design and would silently lose referential enforcement from the first operation. And the OperationEnvelopeBlock accept-set raise 2 to 3 is spent once, so the new kinds land as one batch — this is a different major from Push 4b's schema major 3, the Score and Snapshot role wire that tranche 3b-i froze, and there is no free ride between them. The analysis is corrected in place rather than rewritten, so the evidence the ruling rests on stays readable. Two amendments: Measure is a ninth uncovered surface the original table missed by scoring canvas.regions at container granularity, and identity is promoted from a stated question to a blocking one — IdentityContext is replica-scoped yet lives on Score and is encoded, so under from-empty reduction two replicas with an identical log produce Scores differing in an encoded field while the music is identical. That disposition blocks specification of the tranche and is deliberately not ruled here. Execution belongs to the Push-4b-class coordinated track; the editor track consumes it. T1b's lease, save, and single-writer machinery does not depend on the tranche landing and may be contracted in parallel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
dd33b34f08
commit
011c68a831
|
|
@ -1,5 +1,11 @@
|
|||
# Analysis: canonical graph-state persistence across genesis and pruning
|
||||
|
||||
> **Ruled 2026-07-24 — see `spec/RULING_GENESIS_PERSISTENCE.md`.** Disposition
|
||||
> **B** was taken: the operation set absorbs genesis. This document stays as
|
||||
> analysis and is not rewritten to match, so the evidence the ruling rests on
|
||||
> remains readable; the two amendments it carries (the `Measure` correction and
|
||||
> the promotion of `identity` to blocking) are marked inline where they belong.
|
||||
|
||||
**Status: analysis, not a ruling.** This is the field-by-field `Score` table
|
||||
that `spec/PLAN_EDITOR_APP.md` §Ruling B blocker (i) requires *before* the
|
||||
blocker can be resolved, and that the T1b runway names as step (1). It
|
||||
|
|
@ -73,6 +79,7 @@ without a minting operation.
|
|||
|---|---|---|---|---|
|
||||
| 1 | `metadata` | ✅ | `SetMetadata` (LWW) | `reduce.rs:2713`, `:5249` |
|
||||
| 2 | `canvas.regions` | ✅ | `CreateRegion` / `DeleteRegion`, `ChangeRegionTimeModel`, `SetMetricGrid`, `SetTimeSignature`, `SetUserSystemBreak` / `SetUserPageBreak` | 29 sites incl. `:2656` |
|
||||
| 2a | ↳ `StaffInstance.measures` | ❌ **none** | — see the correction below | `Measure {` built only in `testkit/src/fixtures.rs` |
|
||||
| 3 | `canvas.layout_defaults` | ❌ **none** | — page size and margins | zero hits in `epiphany-ops` |
|
||||
| 4 | `instruments` | ❌ **none** | — no `CreateInstrument` exists | read-only at `:1318` |
|
||||
| 5 | `staves` | ✅ | `CreateStaff` (mint), tombstone removal | `:3850`, `:2560` |
|
||||
|
|
@ -92,11 +99,27 @@ without a minting operation.
|
|||
| 19 | `tombstoned_pitches` | ✅ derived | delete/tombstone paths | `:2526` |
|
||||
| 20 | `tombstoned_events` | ✅ derived | delete/tombstone paths | 3 sites |
|
||||
|
||||
**Eight fields have no operation that can produce them** (3, 4, 6, 7, 10, 15,
|
||||
16, 17, plus `identity` at 18), and one more (14) can only be pruned back,
|
||||
never authored. Of the covered ones, several are covered *only in the
|
||||
**Eight top-level fields have no operation that can produce them** (3, 4, 6, 7,
|
||||
10, 15, 16, 17, plus `identity` at 18), and one more (14) can only be pruned
|
||||
back, never authored. Of the covered ones, several are covered *only in the
|
||||
graph-aware mode* — they write through `if let Some(score) = self.graph`.
|
||||
|
||||
> **Correction (2026-07-24, made while ruling on this analysis).** Row 2 scored
|
||||
> `canvas.regions` op-covered at *container* granularity — regions, staff
|
||||
> instances, voices — and that hid a ninth gap one level deeper. **`Measure` is
|
||||
> authored by nothing**: no operation mints it, no reducer path writes it, and
|
||||
> `Measure {` is constructed only in `testkit/src/fixtures.rs` (`:137`, `:235`,
|
||||
> `:389`). `CreateStaffInstance` moreover *refuses* an instance carrying
|
||||
> measures (`reduce.rs:3715`, container-not-empty), so they cannot enter at
|
||||
> instance-mint either. A from-empty document therefore cannot have a measure,
|
||||
> which puts this on the critical path rather than in the margins. Ruled
|
||||
> **authored, not derived** (2026-07-24): `TimeAnchor::Measure { id, .. }` means
|
||||
> cross-cutting structures anchor to measure ids, so deriving measures from the
|
||||
> metric grid would make their identity a function of the meter and every
|
||||
> time-signature change would orphan the anchors pointing into them. The cost
|
||||
> accepted with that ruling is that measure/meter consistency becomes an
|
||||
> authoring obligation backed by a graph invariant, not a model guarantee.
|
||||
|
||||
---
|
||||
|
||||
## 3. The three findings
|
||||
|
|
@ -224,6 +247,15 @@ added to `Score` — which argues, again, for C.
|
|||
2. **Is `identity` document state or session state?** It is on `Score` today
|
||||
with no op coverage. If a document has one `IdentityContext` and each
|
||||
session mints under its own replica id, the field's role needs stating.
|
||||
**Promoted to blocking (2026-07-24):** under the ruled disposition,
|
||||
reduction runs onto `Score::empty(identity)`, so whoever opens the document
|
||||
chooses the value — and the codec *encodes* it (`codec.rs:2766`, `:3223`),
|
||||
so two replicas reducing an identical log produce Scores differing in an
|
||||
encoded field while the music is identical. `IdentityContext` is
|
||||
`{replica_id, next_counter}`, replica-scoped by construction
|
||||
(`ids.rs:711-717`). This must be dispositioned before the tranche can be
|
||||
specified; the likely answer is that it belongs in the manifest rather than
|
||||
the graph.
|
||||
3. **`decomposition_attachments` and `spelling_precedence`** are consumed by
|
||||
the prepass. Are they derived state that should be rebuilt rather than
|
||||
persisted — in which case they leave this table — or authored state?
|
||||
|
|
|
|||
|
|
@ -229,10 +229,11 @@ after the resolver tranche lands.
|
|||
* **T1b — the document layer (post-resolver; contract after its blockers
|
||||
resolve).** Rulings B and D: `EditorDocument` + single-writer enforcement
|
||||
in `epiphany-bundle`. Its runway, in order: (1) the **graph-state
|
||||
persistence decision** — how canonical graph state is persisted across
|
||||
*both* genesis *and* pruning, driven by the field-by-field `Score` table
|
||||
(§Ruling B, blocker i); a coordinated spec/core/ops/format decision, or an
|
||||
explicit T1b scope limit to empty/metadata/region documents; (2) the
|
||||
persistence decision — RESOLVED 2026-07-24**
|
||||
(`spec/RULING_GENESIS_PERSISTENCE.md`): the operation set absorbs genesis,
|
||||
and pruning is parked until the canonical base carries graph values. The
|
||||
lease/save/single-writer machinery does **not** depend on that tranche
|
||||
landing and may be contracted in parallel with it; (2) the
|
||||
**versioned-decode disposition** — the migrate-on-read API in
|
||||
`epiphany-ops`, preferred and eventually mandatory, or the enforceable
|
||||
current-layout restriction (§Ruling B, blocker ii); (3) the **Ruling-D
|
||||
|
|
@ -433,9 +434,14 @@ consumes them and never blocks on them — and the sequencing currency is the
|
|||
**schema-major budget** ("a major is a budget to spend deliberately",
|
||||
`PLAN_PUSH4B_TUNING.md`), not editor tranche numbers:
|
||||
|
||||
* **Genesis persistence (T1b blocker i, §Ruling B):** how genesis-only graph
|
||||
data — instruments, canvas — is canonically persisted and travels with a
|
||||
document. The one registry item that *does* gate an editor tranche.
|
||||
* **Genesis persistence (T1b blocker i, §Ruling B) — RULED 2026-07-24,
|
||||
tranche not yet dispatched.** `spec/RULING_GENESIS_PERSISTENCE.md`: the
|
||||
operation set absorbs genesis, so this stops being "how is genesis-only data
|
||||
persisted" and becomes a normal operation-vocabulary tranche — three LWW
|
||||
settings setters and six entity mint families, one `OperationEnvelopeBlock`
|
||||
accept-set raise spent once, Pass-12 K8 reversed. Blocked on the `identity`
|
||||
disposition. The one registry item that *did* gate an editor tranche; the
|
||||
gate is now the tranche's execution, not the decision.
|
||||
* **Articulations, dynamics, ornaments are empty wire types.**
|
||||
`ArticulationMark` / `DynamicMark` / `OrnamentMark` are unit structs
|
||||
(`event.rs:49-57`) that already encode — giving them real payloads is a
|
||||
|
|
@ -573,13 +579,28 @@ The T2 ruling the ladder reserved, now drafted:
|
|||
Granting this ruling unblocks T2's W4 packet (copy/paste); W1–W3 do not
|
||||
depend on it.
|
||||
|
||||
### Ruling B — the document layer and persistence semantics — **REDRAFTED ×2 — NOT grantable yet**
|
||||
### Ruling B — the document layer and persistence semantics — **REDRAFTED ×2 — blocker (i) RESOLVED 2026-07-24; blocker (ii) still open**
|
||||
|
||||
*(First draft withdrawn for violating the grow-only operation set. Second
|
||||
draft amended by the 2026-07-23 second review. Two blockers stand between
|
||||
draft amended by the 2026-07-23 second review. Two blockers stood between
|
||||
this ruling and grant; both are outside the resolver-parallel blast radius,
|
||||
which is why T1b sequences after the resolver.)*
|
||||
|
||||
> **Blocker (i) is RESOLVED — `spec/RULING_GENESIS_PERSISTENCE.md`, ratified
|
||||
> 2026-07-24.** The field-by-field table this blocker demanded was produced as
|
||||
> `spec/ANALYSIS_GENESIS_PERSISTENCE.md` (nine uncovered surfaces, not the
|
||||
> handful this text guessed at: `canvas.layout_defaults`, `instruments`,
|
||||
> `staff_groups`, `parts`, `tuning_context`, `spelling_precedence`,
|
||||
> `analysis_layers`, `views`, and `StaffInstance.measures`, plus `identity`).
|
||||
> The ruling takes **neither** (a)-as-written nor (b): the operation set
|
||||
> **absorbs** genesis — Pass-12 K8 is reversed, there is no genesis block, and
|
||||
> a document is `Score::empty(identity)` plus its envelope log. Execution is a
|
||||
> Push-4b-class coordinated tranche; the editor track consumes it. Two
|
||||
> constraints carry forward into T1b: **pruning may not be implemented until
|
||||
> disposition C lands** (a `MaterializedState` base cannot rebuild a score), and
|
||||
> `identity`'s disposition blocks specification of that tranche. The text below
|
||||
> is kept as the record of what the blocker was.
|
||||
|
||||
**Blocker (i) — canonical graph-state persistence, across genesis AND
|
||||
pruning.** Two halves of one question. *Genesis:* "every piece of content
|
||||
enters as operations" is false for more than instruments — genesis is
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
# Ruling: the operation set absorbs genesis
|
||||
|
||||
**Ratified 2026-07-24.** Resolves `spec/PLAN_EDITOR_APP.md` §Ruling B blocker
|
||||
(i) — canonical graph-state persistence across genesis and pruning. The
|
||||
evidence is `spec/ANALYSIS_GENESIS_PERSISTENCE.md`, which stays as analysis;
|
||||
this document is the decision and its constraints.
|
||||
|
||||
Execution belongs to the **Push-4b-class coordinated spec+core+ops track**,
|
||||
not to the editor track. The editor track consumes it and does not block on
|
||||
it (§3.7's standing posture).
|
||||
|
||||
---
|
||||
|
||||
## 1. The decision
|
||||
|
||||
**Every mutable field of `Score` becomes operation-authored.** The Pass-12 K8
|
||||
ratification — "genesis is outside the operation set", `binary_format.tex:2420`
|
||||
— is **reversed**. A document is `Score::empty(identity)` plus its envelope
|
||||
log; the instrument → staff → staff instance → voice → event chain is
|
||||
authorable end to end.
|
||||
|
||||
**There is no genesis block.** No new chunk role, no manifest field, no
|
||||
write-once payload, no immutability rule, and no merge or fail-closed rule for
|
||||
a non-CRDT canonical blob. That machinery was the price of leaving genesis
|
||||
outside the operation set, and this ruling declines to pay it. Genesis becomes
|
||||
the first few operations of an ordinary log, converging under the rules
|
||||
already in force.
|
||||
|
||||
**Rationale, in one line:** genesis state is edited — instruments are added,
|
||||
page geometry is changed, temperaments are chosen — and every alternative made
|
||||
those edits either single-writer, unmergeable, or impossible. Concurrency is a
|
||||
first-order product commitment (§3.4), so the exception was not worth
|
||||
institutionalising in the format.
|
||||
|
||||
### What was considered and rejected
|
||||
|
||||
* **A — a canonical genesis block.** Rejected: it fixes genesis but leaves the
|
||||
*music* unreconstructable after a prune, and a non-op canonical payload has
|
||||
no merge rule, forcing either single-writer genesis or a fail-closed sync
|
||||
rule. Considered in a staged form (immutable block + settings ops) and
|
||||
rejected once it became clear the block buys nothing that B does not.
|
||||
* **C — the canonical base carries graph values.** Not rejected — **deferred**,
|
||||
and still required. See §4.
|
||||
* **D — scope-limit T1b.** Rejected as a destination; unnecessary as a
|
||||
sequencing device once B is committed to.
|
||||
|
||||
---
|
||||
|
||||
## 2. Scope
|
||||
|
||||
Two existing reduction patterns cover the whole tranche. Neither is new
|
||||
semantics; both are templates already proven in `reduce.rs`.
|
||||
|
||||
**Settings — LWW field-overwrite, the `SetMetadata` pattern** (`reduce.rs:2713`,
|
||||
seeded for value-restoring undo at `:1357`):
|
||||
|
||||
| Field | Operation |
|
||||
|---|---|
|
||||
| `canvas.layout_defaults` | `SetCanvasLayoutDefaults` |
|
||||
| `tuning_context` | `SetTuningContext` |
|
||||
| `spelling_precedence` | `SetSpellingPrecedence` |
|
||||
|
||||
**Entities — set-union mint with byte-identical re-carry idempotence, the
|
||||
`CreateStaff` pattern** (`reduce.rs:3850`, graph-aware preconditions at
|
||||
`:3824`):
|
||||
|
||||
| Field | Family |
|
||||
|---|---|
|
||||
| `instruments` | `CreateInstrument` … |
|
||||
| `staff_groups` | `CreateStaffGroup` … |
|
||||
| `parts` | `CreatePartDefinition` … |
|
||||
| `analysis_layers` | `CreateAnalysisLayer` … |
|
||||
| `views` | `CreateView` … |
|
||||
| `StaffInstance.measures` | `CreateMeasure` … |
|
||||
|
||||
Delete and modify coverage per family is the tranche contract's design work,
|
||||
guided by the existing precedent (Group 3's "mint + empty-only delete" for
|
||||
containers; `CreateStaff` lands today with no `DeleteStaff`, so full CRUD is
|
||||
not automatically owed). What **is** owed is that every family's referential
|
||||
preconditions are **graph-aware**, in the shape `CreateStaff` already uses:
|
||||
|
||||
* `CreateStaffGroup.members`, `CreatePartDefinition.staves` → live `Staff`s;
|
||||
* `CreateView.active_layers` → live `AnalysisLayer`s;
|
||||
* `CreateMeasure` → a live `StaffInstance`;
|
||||
* deleting an entity with live dependents → refuse (container-not-empty).
|
||||
|
||||
### Measures: authored, not derived
|
||||
|
||||
Ruled 2026-07-24. `TimeAnchor::Measure { id, .. }` means cross-cutting
|
||||
structures anchor to measure ids; deriving measures from the metric grid would
|
||||
make their identity a function of the meter, so every time-signature change
|
||||
would re-derive a different measure set and orphan the anchors pointing into
|
||||
it. Authored ids survive a meter change.
|
||||
|
||||
**Accepted cost:** measure/meter consistency becomes an authoring obligation
|
||||
backed by a graph invariant, not a model guarantee. The invariant belongs in
|
||||
the tranche.
|
||||
|
||||
### Out of scope, with reasons
|
||||
|
||||
* **`identity`** — not an authored field; see §3, which must be settled first.
|
||||
* **`decomposition_attachments`** — **derived, not authored.** The prepass
|
||||
creates it (`core/src/prepass.rs:382`); reduction only ever *retains*
|
||||
(`reduce.rs:2342`). It leaves the table rather than gaining operations.
|
||||
Flagged for ratification with the tranche.
|
||||
|
||||
---
|
||||
|
||||
## 3. The one open sub-decision, and it blocks specification
|
||||
|
||||
**`identity` must be dispositioned before the tranche can be specified.**
|
||||
|
||||
`IdentityContext { replica_id, next_counter }` is replica-scoped by
|
||||
construction (`ids.rs:711-717`) yet lives on `Score`, which the codec
|
||||
**encodes** (`codec.rs:2766`, `:3223`). Today the value is inherited from a
|
||||
shared base, which masks the tension. Under this ruling reduction runs onto
|
||||
`Score::empty(identity)`, so whoever opens the document chooses it — and two
|
||||
replicas reducing an identical log then produce Scores differing in an encoded
|
||||
field while the music is identical.
|
||||
|
||||
Options: move it to the manifest (document-scoped, recommended); keep it on
|
||||
`Score` but exclude it from the canonical encoding; or accept
|
||||
replica-dependent `Score` bytes and confine byte-equality claims to
|
||||
`MaterializedState`. **Not ruled here.**
|
||||
|
||||
---
|
||||
|
||||
## 4. Standing constraints
|
||||
|
||||
**Pruning MUST NOT be implemented until disposition C lands.** This ruling
|
||||
makes documents openable, editable, and collaborative; it does **not** make
|
||||
them prunable. A prune deletes the covered envelopes and installs a
|
||||
`MaterializedState` base carrying no graph values, so the score is
|
||||
unreconstructable afterward — and `MaterializedState.effects` are outcomes,
|
||||
not payloads, so nothing rebuilds it. Pruning is unimplemented today (no
|
||||
`fn prune`; `opset.rs:7` calls it out-of-scope-for-v0), which is why this costs
|
||||
nothing now and would cost everything later. C — the canonical base carrying
|
||||
graph values, the same checkpoint T4b needs for incremental materialization —
|
||||
remains **required before any pruning implementation**.
|
||||
|
||||
**The from-empty path must reduce with a graph, not base-free.** Reduction has
|
||||
two modes, and graph-aware preconditions are skipped in the base-free one
|
||||
*because it has no universe to check against* (`reduce.rs:3822`, `:3721`).
|
||||
`Score::empty(identity)` **is** a graph, so `new_onto` with an empty score
|
||||
enforces every precondition from the first operation; the base-free mode does
|
||||
not. A from-empty document reduced through the wrong entry point silently
|
||||
loses referential enforcement. Name this in the tranche and test it.
|
||||
|
||||
**One accept-set raise, spent once.** `OperationEnvelopeBlock` is capped at
|
||||
major 2 (`bundle.rs:69`) because no operation payload embeds these types. The
|
||||
first new kind that does raises it to 3; every later kind in that major is
|
||||
free. The new kinds therefore land as **one batch**, not dribbled out per
|
||||
need. Note this is a *different* major from Push-4b's schema major 3, which is
|
||||
the Score/Snapshot-role wire that tranche 3b-i opened and permanently froze —
|
||||
there is no free ride between them.
|
||||
|
||||
**Additive discriminants.** New kinds extend past `TransposeInterval` under
|
||||
the existing convention (`req:binfmt:kind-discriminants`); canonical bytes for
|
||||
existing types do not move.
|
||||
|
||||
---
|
||||
|
||||
## 5. Acceptance
|
||||
|
||||
* A document created empty and given **only operations** materializes a
|
||||
note-bearing `Score` — the full chain, no fixture, no base.
|
||||
* Two replicas applying concurrent genesis-era operations in any delivery
|
||||
order converge to byte-identical `MaterializedState`.
|
||||
* Opening such a document from a bundle reaches a note, which is what
|
||||
unblocks T1b.
|
||||
* Existing canonical bytes, goldens, and conformance gates unmoved except
|
||||
where the accept-set raise is the deliberate change.
|
||||
|
||||
---
|
||||
|
||||
## 6. What this unblocks
|
||||
|
||||
`spec/PLAN_EDITOR_APP.md` §Ruling B blocker (i) is **resolved**. Blocker (ii)
|
||||
— the version-aware envelope decoder — is unaffected and still open; its
|
||||
residual is one bounded ops packet. T1b's remaining runway is therefore
|
||||
blocker (ii) plus the Ruling-D ownership API, and the lease/save/single-writer
|
||||
machinery can be contracted in parallel with this tranche, since it does not
|
||||
depend on genesis being authorable.
|
||||
|
||||
*Related: `spec/ANALYSIS_GENESIS_PERSISTENCE.md`, `spec/PLAN_EDITOR_APP.md`
|
||||
§Ruling B / §3.7, `spec/PLAN_PUSH4B_TUNING.md` (the tranche mold).*
|
||||
Loading…
Reference in New Issue