Editor T4-pre: the IR readiness contract, and the genesis-persistence table
Two documents, both prerequisites rather than implementations. CONTRACT_EDITOR_T4PRE_IR.md charters the layout-IR readiness tranche Ruling A names as its prerequisite: per-system primitive ownership (W1), the shared typed glyph-asset seam (W2, contracted but not dispatched), and the text-run primitive decision (W3, an analysis packet). Its central finding reframes the work — casting-off already computes the ownership partition and the fold into ResolvedLayoutIR drops it on the floor, so W1 is "stop discarding it", exact rather than heuristic. Eight design pins, six named mutations, and a byte-neutrality gate that keeps the tranche compatible with the Push-4b freeze. The contract survived a source-level review that found one real defect: the blast radius audited ResolvedLayoutIR literals but not ResolvedSystem's, and pin 1 adds a field to both — editor-core's two-system hit-test fixture is a full literal with no spread, so the packet could not compile the workspace without it. Amended, along with the SystemId claim (no *production* constructor; testkit's random cache generator has one), the stub solver's total provenance aliasing, and the stroke-continuation citation. ANALYSIS_GENESIS_PERSISTENCE.md is the field-by-field Score table Ruling B blocker (i) requires before it can be resolved. It decides nothing; it establishes what is true. Eight of Score's twenty fields have no operation that can produce them — canvas.layout_defaults (the printed page geometry), instruments, staff_groups, parts, tuning_context, spelling_precedence, analysis_layers, views, plus identity — and decomposition_attachments can be pruned but never authored. Each is reachable only through a base the reducer is handed, and pruning is licensed to replace that base with a MaterializedState carrying none of them. Three findings follow: the genesis root is unreachable from an empty base (CreateStaff preconditions a live instrument; nothing creates an instrument), the canonical base cannot carry what the base-only fields hold, and there is no canonical wire path from bundle bytes to a Score at all. They are one problem from three sides — the format has no representation for graph state that no operation authors. Four dispositions are laid out with costs: a canonical genesis block, closing the op-coverage gap, promoting the canonical base to carry graph values, or scope-limiting T1b. The recommendation is the last now and the third as the destination, because only that one makes pruning safe for every field rather than for an enumerated list, and T4b needs the same checkpoint mechanism regardless. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
91d44dbe65
commit
962eab3ac5
|
|
@ -0,0 +1,238 @@
|
||||||
|
# Analysis: canonical graph-state persistence across genesis and pruning
|
||||||
|
|
||||||
|
**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
|
||||||
|
decides nothing. It establishes what is true, states the three findings that
|
||||||
|
follow, and lays out the disposition options with their real costs so the
|
||||||
|
choice can be made on evidence.
|
||||||
|
|
||||||
|
Written alongside T4-pre W1 (`CONTRACT_EDITOR_T4PRE_IR.md`). Analysis only:
|
||||||
|
no code, no `.tex`, no `epiphany-core` edit — no collision with the Push-4b
|
||||||
|
track, which owns those surfaces.
|
||||||
|
|
||||||
|
All citations verified against the working tree at `85d8af6`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The mechanism, as built
|
||||||
|
|
||||||
|
**Reduction has two modes, and only one of them produces a graph.**
|
||||||
|
`Reducer::new(op_set)` reduces bookkeeping alone; `new_onto(op_set, base:
|
||||||
|
&Score)` clones a base `Score` and reduces onto the clone
|
||||||
|
(`ops/src/reduce.rs:1302-1306`). Graph-aware preconditions are *skipped*
|
||||||
|
in the base-free mode — the code says so outright: "base-free reduction has
|
||||||
|
no instrument/group universe to check against" (`reduce.rs:3822`), "base-free
|
||||||
|
reduction has no staff universe to check" (`reduce.rs:3721`). So a reduction
|
||||||
|
with no base produces a `MaterializedState` and no `Score` at all.
|
||||||
|
|
||||||
|
**What the canonical document is.** `req:format:canonical-document-reduction`
|
||||||
|
(`core_spec.tex:11549-11592`): with `canonical_base = None`, the canonical
|
||||||
|
document is the deterministic reduction of the union of all
|
||||||
|
`operation_roots` envelopes; with `Some(base)`, it is the base snapshot's
|
||||||
|
**materialized state** plus the reduction of the envelopes its
|
||||||
|
`covers_causal_frontier` does not cover. Acceleration snapshots "**MUST be
|
||||||
|
ignored** for the purposes of canonical state" and must be rebuilt or
|
||||||
|
discarded on disagreement.
|
||||||
|
|
||||||
|
**What the canonical base actually carries.** `MaterializedState`
|
||||||
|
(`reduce.rs:503-522`) is: effects, conflicts, anomalies, object
|
||||||
|
existence (`objects`), spellings, system breaks, page breaks, pending. That
|
||||||
|
is **reducer bookkeeping**. It contains no `Region`, no `Staff`, no `Event`,
|
||||||
|
no `Instrument`, no `Part` — no graph values whatsoever. `GraphMaterialization`
|
||||||
|
pairs it with a `Score`, and says so plainly: the score "is derived state,
|
||||||
|
never the source of truth" (`reduce.rs:526-534`).
|
||||||
|
|
||||||
|
**What pruning does.** `req:format:pruning-state-preservation`
|
||||||
|
(`core_spec.tex:11596-11621`): materialize a snapshot covering a chosen DVV
|
||||||
|
frontier, replace `canonical_base`, **remove operation-envelope blocks
|
||||||
|
entirely covered by the new frontier**, commit atomically — and "pruning
|
||||||
|
MUSTNOT alter the canonical document state." The manifest carries
|
||||||
|
`canonical_base: Option<SnapshotRef>` and a separate, explicitly
|
||||||
|
non-canonical `acceleration_snapshots: Vec<SnapshotRef>`
|
||||||
|
(`bundle/src/manifest.rs:384-387`).
|
||||||
|
|
||||||
|
Put together: the values that make a score a score live **only** in the
|
||||||
|
envelopes and in whatever base was handed to the reducer. Pruning is licensed
|
||||||
|
to delete those envelopes, and the thing it puts in their place carries no
|
||||||
|
values.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The table
|
||||||
|
|
||||||
|
Every field of `Score` (`core/src/graph.rs:1710-1735`), with the operations
|
||||||
|
that write it under graph-aware reduction. "Op-covered" means *some*
|
||||||
|
operation in `OperationKind` (`ops/src/payload.rs:116-198`) can create or
|
||||||
|
change it; the writer citations are the actual mutation sites in
|
||||||
|
`reduce.rs`, distinguished from the base-seed reads at `reduce.rs:1313-1362`,
|
||||||
|
which are how a base graph's objects enter the reducer's existence index
|
||||||
|
without a minting operation.
|
||||||
|
|
||||||
|
| # | Field | Op-covered | Writing operations | Verified at |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| 1 | `metadata` | ✅ | `SetMetadata` (LWW) | `reduce.rs:2713`, `:5249` |
|
||||||
|
| 2 | `canvas.regions` | ✅ | `CreateRegion` / `DeleteRegion`, `ChangeRegionTimeModel`, `SetMetricGrid`, `SetTimeSignature`, `SetUserSystemBreak` / `SetUserPageBreak` | 29 sites incl. `:2656` |
|
||||||
|
| 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` |
|
||||||
|
| 6 | `staff_groups` | ❌ **none** | — | read-only at `:1329` |
|
||||||
|
| 7 | `parts` | ❌ **none** | — | read-only at `:1333` |
|
||||||
|
| 8 | `cross_cutting` | ✅ | `CreateCrossCutting` / `Delete` / `Modify`, `CreateRepeatStructure` / `DeleteRepeatStructure` | 38 sites |
|
||||||
|
| 9 | `time_signatures` | ✅ | `SetTimeSignature` (set-union mint) | `:3891`, `:2563` |
|
||||||
|
| 10 | `tuning_context` | ❌ **none** | — pitch space, tuning system, reference | zero hits in `epiphany-ops` |
|
||||||
|
| 11 | `tempo_map` | ✅ | `SetTempoSegment` | `:4142` |
|
||||||
|
| 12 | `events` | ✅ | `InsertEvent`, `DeleteEvent`, `ModifyEvent`, `Insert`/`Delete`/`ModifyIdentifiedPitch`, `Transpose`, `TransposeInterval` | 28 sites |
|
||||||
|
| 13 | `spelling_attachments` | ✅ | `RespellPitch` | 15 sites |
|
||||||
|
| 14 | `decomposition_attachments` | ⚠️ **removal only** | reduction only *retains*; the creator is the prepass | `:2342`; `core/src/prepass.rs:382` |
|
||||||
|
| 15 | `spelling_precedence` | ❌ **none** | — | zero hits in `epiphany-ops` |
|
||||||
|
| 16 | `analysis_layers` | ❌ **none** | — | read-only at `:1345` |
|
||||||
|
| 17 | `views` | ❌ **none** | — | read-only at `:1349` |
|
||||||
|
| 18 | `identity` | ❌ **none** | — the `IdentityContext` itself | zero hits |
|
||||||
|
| 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
|
||||||
|
graph-aware mode* — they write through `if let Some(score) = self.graph`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. The three findings
|
||||||
|
|
||||||
|
### Finding 1 — the genesis root is unreachable from an empty base
|
||||||
|
|
||||||
|
The creation chain is instrument → staff → staff instance → voice → event,
|
||||||
|
and each link is enforced under graph-aware reduction:
|
||||||
|
|
||||||
|
* `CreateStaff` refuses unless `TypedObjectId::Instrument(op.staff.instrument)`
|
||||||
|
is live (`reduce.rs:3824-3833`) — and **no operation creates an
|
||||||
|
`Instrument`**. Genesis is outside the operation set by ratified decision
|
||||||
|
("there is no `CreateCanvas`/`CreateInstrument`",
|
||||||
|
`binary_format.tex:2420`; Pass-12 K8).
|
||||||
|
* `CreateStaffInstance` refuses unless the referenced global `Staff` is live
|
||||||
|
(`reduce.rs:3723-3734`).
|
||||||
|
|
||||||
|
So reduction onto `Score::empty(identity)` can never reach a note. The escape
|
||||||
|
hatch that exists today is not a design — it is the base-free mode, which
|
||||||
|
skips the preconditions precisely *because* it has no graph to check, and
|
||||||
|
produces no graph either.
|
||||||
|
|
||||||
|
### Finding 2 — the canonical base cannot carry what the base-only fields hold
|
||||||
|
|
||||||
|
The eight uncovered fields can enter a `Score` only by being in the base
|
||||||
|
handed to the reducer. Pruning replaces that base with a `MaterializedState`
|
||||||
|
snapshot, which carries none of them, and deletes the covered envelopes —
|
||||||
|
which never carried them either. **After a prune, the uncovered fields are
|
||||||
|
gone with no canonical way to recover them**, and the acceleration snapshot
|
||||||
|
that does hold full-`Score` bytes is normatively forbidden as a source of
|
||||||
|
canonical state.
|
||||||
|
|
||||||
|
This is not only an editor concern. Field 3 is the printed page geometry;
|
||||||
|
field 10 is the tuning context the entire Push-4b resolver consumes; field 7
|
||||||
|
is the part definitions that `req:graph:part-content-projection` calls
|
||||||
|
normative.
|
||||||
|
|
||||||
|
### Finding 3 — there is no canonical wire path from bundle bytes to a `Score`
|
||||||
|
|
||||||
|
`canonical_base` holds `MaterializedState`; a base stamped above schema major
|
||||||
|
0 forces read-only (`bundle.rs:857`); full-`Score` bytes exist on the wire
|
||||||
|
only in the acceleration-snapshot role, which MUST be ignored. Consequently
|
||||||
|
the editor cannot open a document today — and Fact 3 of the plan records the
|
||||||
|
symptom: the GUI opens a hard-coded testkit fixture, and neither editor crate
|
||||||
|
even depends on `epiphany-bundle`.
|
||||||
|
|
||||||
|
The three findings are one problem seen from three sides: **the canonical
|
||||||
|
document format has no representation for graph state that no operation
|
||||||
|
authors.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Dispositions
|
||||||
|
|
||||||
|
Four coherent options. They are not mutually exclusive — A and D compose, and
|
||||||
|
C subsumes much of A.
|
||||||
|
|
||||||
|
**A. A canonical genesis block.** A new chunk role, canonical and never
|
||||||
|
pruned, carrying exactly the uncovered fields: canvas layout defaults,
|
||||||
|
instruments, staff groups, parts, tuning context, spelling precedence,
|
||||||
|
analysis layers, views, identity. Reduction takes it as the base. This keeps
|
||||||
|
the ratified "genesis outside the operation set" decision intact — genesis
|
||||||
|
becomes *persisted canonical input* rather than operations — and it is the
|
||||||
|
smallest change that makes documents openable. Cost: one format addition and
|
||||||
|
a manifest field; the hard question it must answer is what happens when two
|
||||||
|
replicas' genesis blocks disagree, since a non-op payload has no CRDT merge
|
||||||
|
rule. Concurrent editing of genesis fields would remain unsupported, which
|
||||||
|
is honest for v1 but is a real product ceiling (adding an instrument to a
|
||||||
|
shared score is a normal edit).
|
||||||
|
|
||||||
|
**B. Close the op-coverage gap.** Add `CreateInstrument`, `CreateStaffGroup`,
|
||||||
|
`CreatePart`, `SetCanvasLayoutDefaults`, `SetTuningContext`,
|
||||||
|
`SetSpellingPrecedence`, and analysis/view CRUD, making the op log
|
||||||
|
self-sufficient. This is the only option under which genesis state converges
|
||||||
|
concurrently like everything else, and it makes the grow-only log the whole
|
||||||
|
truth. Cost: it reverses a ratified Pass-12 decision, and each operation
|
||||||
|
needs reduction semantics, conflict behaviour, undo behaviour, and catalog
|
||||||
|
text — a schema major spent deliberately, in the Push-4b mold, not a
|
||||||
|
tranche this track can absorb. Note it does **not** by itself fix pruning:
|
||||||
|
the envelopes carrying these ops become prunable like any other.
|
||||||
|
|
||||||
|
**C. Promote the canonical base to carry graph values.** Make the
|
||||||
|
canonical-base snapshot *checkpointed reducer state plus graph state* rather
|
||||||
|
than bookkeeping alone. This is the same shape T4b needs for incremental
|
||||||
|
materialization ("checkpointed-reducer-state-plus-tail"), so the two tracks
|
||||||
|
would pay for one mechanism. It also repairs pruning for **every** field at
|
||||||
|
once, covered or not. Cost: the largest format change of the four; the base
|
||||||
|
role's schema-major-0 pin and the read-only-above-0 rule
|
||||||
|
(`bundle.rs:857`) must be revisited; and the `SnapshotId` derivation is
|
||||||
|
today an acknowledged test-harness stand-in with no normative derivation
|
||||||
|
(`binary_format.tex:698`), which would have to become real before snapshots
|
||||||
|
are load-bearing.
|
||||||
|
|
||||||
|
**D. Scope-limit T1b.** Ship the document layer for documents whose genesis
|
||||||
|
is empty, metadata-only, or region-only, and refuse the rest cleanly —
|
||||||
|
the plan's existing escape hatch. Cost: an editor that cannot open a score
|
||||||
|
with an instrument in it is not the editor; this is a way to land the
|
||||||
|
bundle/session/lease machinery (Ruling D's unforgeable lease, single-writer
|
||||||
|
enforcement, the save protocol) against a real format while the genesis
|
||||||
|
decision is made properly. It buys sequencing, not a solution.
|
||||||
|
|
||||||
|
**The recommendation I would defend:** **D now, C as the destination, A only
|
||||||
|
if C proves too large to sequence.** D unblocks the Ruling-D ownership API
|
||||||
|
and the save/dirty protocol — the parts of T1b that the editor track actually
|
||||||
|
needs next and that are independent of this question. C is where the format
|
||||||
|
wants to end up, because it is the only option that makes pruning safe for
|
||||||
|
every field rather than for an enumerated list, and because T4b needs the
|
||||||
|
same checkpoint mechanism regardless. B is the right long-run answer for
|
||||||
|
*concurrent* genesis editing, but it is a Push-4b-class spec tranche and
|
||||||
|
should be sequenced by product priority, not by this blocker.
|
||||||
|
|
||||||
|
Whatever is chosen inherits one hard constraint: pruning MUSTNOT alter the
|
||||||
|
canonical document state (`core_spec.tex:11612-11616`). Any option that
|
||||||
|
enumerates fields must be re-audited against this table every time a field is
|
||||||
|
added to `Score` — which argues, again, for C.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Open questions for the ruling
|
||||||
|
|
||||||
|
1. **Genesis divergence.** Under A, what is the merge rule when two replicas
|
||||||
|
present different genesis blocks for the same document id? (Under B this
|
||||||
|
question dissolves; under C it becomes snapshot-disagreement handling,
|
||||||
|
which the spec already answers for acceleration snapshots.)
|
||||||
|
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.
|
||||||
|
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?
|
||||||
|
Field 14's removal-only reduction path suggests the former.
|
||||||
|
4. **Does the canvas's page geometry belong to the document or to a view?**
|
||||||
|
§3.2's document ≠ session ≠ view split may relocate field 3 entirely,
|
||||||
|
and part-specific page geometry is a real product requirement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Related: `spec/PLAN_EDITOR_APP.md` §Ruling B, §3.1, §3.2, §3.7;
|
||||||
|
`spec/CONTRACT_EDITOR_T4PRE_IR.md`.*
|
||||||
|
|
@ -0,0 +1,245 @@
|
||||||
|
# Contract: Editor T4-pre — the layout-IR readiness tranche
|
||||||
|
|
||||||
|
Repo root `/home/jeans/Repos/active/epiphany`. Governed by
|
||||||
|
`spec/PLAN_EDITOR_APP.md` §3.7 ("the layout-IR readiness tranche (T4
|
||||||
|
prerequisite)") and named as a prerequisite by Ruling A: *"IR per-system
|
||||||
|
primitive ownership; the shared typed glyph-asset seam; the text-run
|
||||||
|
primitive decision. These are IR/render tranches this ruling depends on, not
|
||||||
|
work it smuggles in."* T3 is complete at `85d8af6`.
|
||||||
|
|
||||||
|
Execution model as T1a/T2/T3: Sonnet subagents per packet, coordinator
|
||||||
|
line-level review with independent mutation re-runs, user deep-dives at
|
||||||
|
contract sign-off, any new golden baseline, and the final report. Mutation
|
||||||
|
discipline throughout: anchor-assert before substituting, restore by
|
||||||
|
reversing, never `git checkout`.
|
||||||
|
|
||||||
|
**Parallel safety.** The Push-4b track owns `crates/epiphany-core/**`, its
|
||||||
|
`DECISIONS.md`, and all `.tex` (`CONTRACT_PUSH4B_RESOLVER.md` blast radius);
|
||||||
|
it is live in the working tree. This tranche touches **no `epiphany-core`
|
||||||
|
file, no `.tex`, and adds no requirement label** — counts stay at whatever
|
||||||
|
the parallel track has them at; report observed actuals, never assert stale
|
||||||
|
numbers.
|
||||||
|
|
||||||
|
**The literal census, both structs.** W1 adds a field to `ResolvedSystem` as
|
||||||
|
well as to `ResolvedLayoutIR`, and Rust struct literals are exhaustive, so
|
||||||
|
every literal of both must be updated or the workspace does not compile.
|
||||||
|
Verified:
|
||||||
|
|
||||||
|
* `ResolvedLayoutIR {` — `layout-ir/src/{resolved.rs:405, solver.rs:518}`,
|
||||||
|
`engrave/src/lib.rs:407`, `render-svg/src/svg.rs:1049`,
|
||||||
|
`testkit/src/layout_stub.rs:436`.
|
||||||
|
* `ResolvedSystem {` — `layout-ir/src/solver.rs:501`,
|
||||||
|
`engrave/src/casting.rs:1950`, `testkit/src/layout_stub.rs:461`, **and
|
||||||
|
`editor-core/src/lib.rs:4718`** — the two-system hit-test fixture in
|
||||||
|
editor-core's test module, a full four-field literal with no `..` spread,
|
||||||
|
and `ResolvedSystem` cannot implement `Default` (it carries a
|
||||||
|
`Provenance`).
|
||||||
|
|
||||||
|
So this tranche **does** reach one editor-crate file: that test literal, a
|
||||||
|
**mechanical field-add only**. `epiphany-editor-core` otherwise borrows
|
||||||
|
`&ResolvedLayoutIR` and mutates `.pages`; it **must not consume ownership** —
|
||||||
|
adoption stays at T4 per pin 8. Parallel safety is unaffected either way:
|
||||||
|
Push-4b owns `epiphany-core`, not the editor crate.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## W1 — per-system primitive ownership (dispatchable now)
|
||||||
|
|
||||||
|
### The verified starting point
|
||||||
|
|
||||||
|
Read these before designing; they change the shape of the work:
|
||||||
|
|
||||||
|
* **The partition already exists and is discarded.** Casting-off computes
|
||||||
|
`system_of_slot`, `stroke_system`, `curve_system`, and `region_of_system`
|
||||||
|
and returns them on `CastLayout` (`engrave/src/casting.rs:186-206`);
|
||||||
|
`engrave/src/lib.rs:386-391` folds `cast.glyphs/strokes/curves/pages` into
|
||||||
|
`ResolvedLayoutIR` and **drops every ownership vector on the floor**. W1 is
|
||||||
|
"stop discarding it", not "infer it".
|
||||||
|
* **Glyph ownership is derivable exactly, and one consumer already derives
|
||||||
|
it**: the quality census maps a glyph to a system through
|
||||||
|
`system_of_slot[constrained_glyph.horizontal_slot]`
|
||||||
|
(`engrave/src/quality.rs:232`), skipping slots no region claimed.
|
||||||
|
* **Cross-system primitives are already resolved at this stage.** A
|
||||||
|
system-spanning stroke is replaced by its first segment plus synthesized
|
||||||
|
continuation segments, and a system-spanning curve is split by de Casteljau
|
||||||
|
subdivision, the continuations carrying `SYSTEM_CONTINUATION_SYNTHESIS`
|
||||||
|
provenance (strokes `casting.rs:1051-1075`, curves
|
||||||
|
`casting.rs:1128-1165`). So **every resolved primitive belongs
|
||||||
|
to at most one system already**. Fact 2's warning that "cross-system curves
|
||||||
|
and boundary-straddling primitives make inference ambiguous" is an argument
|
||||||
|
against *spatial inference by the consumer*, not against publishing what
|
||||||
|
casting-off knows.
|
||||||
|
* **`SystemId(pub u128)` (`layout-ir/src/cache.rs:10`) has no production
|
||||||
|
constructor** — the single construction in the tree is testkit's random
|
||||||
|
cache generator (`layout_stub.rs:630`, property-test fodder for the cache
|
||||||
|
codec). Nothing maps a real system into
|
||||||
|
`LayoutCache.resolved: BTreeMap<SystemId, …>`; it is inert scaffolding for
|
||||||
|
T4b. W1 does **not** wire it; see the identity pin.
|
||||||
|
|
||||||
|
### Design pins
|
||||||
|
|
||||||
|
1. **Ownership lives on the system, not in a parallel table.**
|
||||||
|
`ResolvedSystem` gains one field holding index lists into the layout's
|
||||||
|
flat `glyphs`/`strokes`/`curves` arrays (`u32` indices — no layout nears
|
||||||
|
4 G primitives, and it matches the crate's existing count width), and
|
||||||
|
`ResolvedLayoutIR` gains one **unowned** bucket of the same shape.
|
||||||
|
Structural ownership
|
||||||
|
removes the "table order must match page order" invariant a top-level
|
||||||
|
parallel table would create, and each system already carries its own
|
||||||
|
identity in `provenance`.
|
||||||
|
2. **No primitive is split, merged, reordered, or renumbered.** The flat
|
||||||
|
arrays are exactly what they are today, in exactly today's order. W1
|
||||||
|
publishes indices into them and nothing else.
|
||||||
|
3. **Unowned is a first-class bucket, never coerced.** `None` — "claimed by
|
||||||
|
no region" — goes to the unowned bucket. It has a real producer: the stub
|
||||||
|
solver (`layout-ir/src/solver.rs:490-520`) builds a degenerate page tree
|
||||||
|
of default-rect systems and resolves no per-system geometry, so **it
|
||||||
|
publishes every primitive as unowned**. Fabricating an attribution there
|
||||||
|
would be a lie about what that path computed.
|
||||||
|
4. **The partition is total and disjoint.** For each of the three arrays, the
|
||||||
|
union of every system's indices plus the unowned bucket is exactly
|
||||||
|
`0..len`, each index appearing exactly once. This is the load-bearing
|
||||||
|
invariant; it is a test, not a comment.
|
||||||
|
5. **Identity is available, position is primary.** A consumer keys a system
|
||||||
|
by its `provenance.stable_id` (`LayoutObjectId`); systems remain addressed
|
||||||
|
positionally in page order. **Verified caution to record in DECISIONS.md,
|
||||||
|
because it will bite T4b:** a region's *first* system reuses the region's
|
||||||
|
own provenance verbatim (`casting.rs:1871`), and page 1 reuses the first
|
||||||
|
region's provenance too (`casting.rs:1192`) — so a system's `stable_id`
|
||||||
|
can equal a page's and a region's. On the **stub solver path the aliasing
|
||||||
|
is total, not just first-system**: every stub system reuses its region's
|
||||||
|
provenance (`solver.rs:501-502`), so the rule below has a producer on both
|
||||||
|
solver paths. Uniqueness *among systems* is structural, not incidental —
|
||||||
|
synthesized ids are domain-tagged hashes over (source, kind discriminant,
|
||||||
|
namespaced instance key), and `KEY_NS_SYSTEM` and `KEY_NS_PAGE` occupy
|
||||||
|
distinct namespaces — which is all this packet needs; **any future
|
||||||
|
cross-kind map keyed on raw `LayoutObjectId`/`SystemId` u128s must
|
||||||
|
disambiguate by kind.**
|
||||||
|
6. **Byte-neutral: ownership is NOT canonically encoded.** Precedent is in
|
||||||
|
the same module: `vertical_band` is carried through but excluded, because
|
||||||
|
"band ownership tells a vertical solver which staff owns a primitive; it
|
||||||
|
draws nothing, so two layouts differing only in it are the same rendered
|
||||||
|
layout and hash alike" (`resolved.rs:110-118`). System ownership draws
|
||||||
|
nothing either. `encode_canonical` is therefore **unchanged**, and the
|
||||||
|
module's canonical-serialization note gains ownership to its stated
|
||||||
|
exclusions. If a future normative requirement wants ownership pinned on
|
||||||
|
the wire, that is a spec-side schema-major decision — not this packet's
|
||||||
|
budget to spend. Two consequences the **DECISIONS.md entry must state
|
||||||
|
outright**, because they are what stop a later reader from "fixing" this:
|
||||||
|
encoding ownership would make the fingerprint *more fragile than the
|
||||||
|
rendering it fingerprints* — a casting-off refactor that re-partitions
|
||||||
|
without moving a pixel would become a byte-level break and force a schema
|
||||||
|
major for something no renderer and no conformance claim can observe; and
|
||||||
|
because two conformant implementations may legitimately partition
|
||||||
|
differently, **any future cross-implementation test of incremental
|
||||||
|
relayout compares final bytes, never intermediate partitions.**
|
||||||
|
7. **One derivation, not two.** Casting-off publishes `glyph_system` on
|
||||||
|
`CastLayout` (computed once, from `system_of_slot`) and the quality census
|
||||||
|
**consumes it** instead of re-deriving. Two copies of an attribution rule
|
||||||
|
drift; this is the packet that makes that impossible.
|
||||||
|
8. **One accessor.** `ResolvedLayoutIR` gains a `systems()` walk in page
|
||||||
|
order (pages → systems), which the partition tests use. Editor-core
|
||||||
|
hand-rolls this same flatten today (`editor-core/src/lib.rs:989`); it
|
||||||
|
adopts the accessor at T4, not here.
|
||||||
|
|
||||||
|
### Tests + minimum mutations
|
||||||
|
|
||||||
|
Value-asserting throughout; the multi-system fixtures already exist
|
||||||
|
(`ten_measure_single_staff` casts to 2 systems at the demo's scale;
|
||||||
|
`ten_measure_with_slurs` is the casting fixture behind golden G4).
|
||||||
|
|
||||||
|
* **(m1) totality/disjointness** — the partition test of pin 4 on a real
|
||||||
|
multi-system engrave. *Mutation:* drop the last glyph index from a system's
|
||||||
|
list → dies.
|
||||||
|
* **(m2) no coercion** — the stub-solver path publishes everything unowned.
|
||||||
|
*Mutation:* map `None` to system 0 → dies.
|
||||||
|
* **(m3) attribution correctness** — assert the *actual* per-system glyph /
|
||||||
|
stroke / curve counts of a two-system fixture (real numbers, reported in
|
||||||
|
the packet report). *Mutation:* off-by-one on the system index → dies.
|
||||||
|
* **(m4) the census really consumes the published vector** — *mutation:*
|
||||||
|
publish `Some(0)` for every glyph while leaving `system_of_slot` correct →
|
||||||
|
the existing quality-metric assertions die. If they survive, the census is
|
||||||
|
still deriving its own answer and pin 7 is not done.
|
||||||
|
* **(m5) continuation segments follow the later system** — a slur or stroke
|
||||||
|
crossing a system break: its continuation is owned by the system it was
|
||||||
|
split *into*. *Mutation:* attribute continuations to the source segment's
|
||||||
|
system → dies.
|
||||||
|
* **(m6) byte-neutrality is locked** — build a layout, clone it, perturb
|
||||||
|
**only** the ownership lists, assert `canonical_bytes()` equal while
|
||||||
|
`PartialEq` differs. *Mutation:* encode the ownership lists in
|
||||||
|
`encode_canonical` → dies.
|
||||||
|
|
||||||
|
### Blast radius
|
||||||
|
|
||||||
|
`crates/epiphany-layout-ir/src/{resolved.rs, solver.rs, lib.rs}`,
|
||||||
|
`crates/epiphany-engrave/src/{casting.rs, lib.rs, quality.rs}`, the
|
||||||
|
test-only literals at `render-svg/src/svg.rs:1049`,
|
||||||
|
`testkit/src/layout_stub.rs:{436, 461}` and
|
||||||
|
`editor-core/src/lib.rs:4718` (mechanical field-add only — editor-core
|
||||||
|
consumes no ownership in this packet), and the `DECISIONS.md` of `layout-ir`
|
||||||
|
and `engrave`. Nothing else. No new crate, no new dependency, no public API
|
||||||
|
removed or renamed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## W2 — the shared typed glyph-asset seam (contracted after W1 review)
|
||||||
|
|
||||||
|
Named now so the tranche's shape is visible; **not dispatched with W1**.
|
||||||
|
|
||||||
|
The Bravura table is private to `render-svg` (`outline()` is `pub(crate)`,
|
||||||
|
`outline.rs:7`) and stores SVG path `d` strings generated by
|
||||||
|
`tools/extract_bravura_outlines.py` (`outlines_generated.rs:1-20`). A canvas
|
||||||
|
tessellator needs typed vector paths from a crate both the renderer and the
|
||||||
|
app can depend on.
|
||||||
|
|
||||||
|
**The pin that makes it safe**, to be detailed when W1 lands: the generator
|
||||||
|
emits a **typed path representation alongside the existing `d` string**, and
|
||||||
|
`render-svg` keeps emitting the `d` string byte-for-byte — so every SVG
|
||||||
|
golden and every conformance byte stays identical while the canvas gets
|
||||||
|
typed geometry. The open decision W2's contract resolves is the home (a new
|
||||||
|
`epiphany-glyphs` crate vs. a `layout-ir` module), judged on dependency
|
||||||
|
weight and the MSRV/CI job structure, plus whether the typed form is
|
||||||
|
generated or parsed once at load.
|
||||||
|
|
||||||
|
## W3 — the text-run primitive decision (analysis packet, after W2)
|
||||||
|
|
||||||
|
Ruling A's criterion 3 makes the text pipeline a hard spike criterion:
|
||||||
|
shaping, fallback, bidi, and metrics consistent across canvas, SVG/PDF
|
||||||
|
export, hit testing, and the accessibility tree. W3 is a **decision
|
||||||
|
document**, not an implementation — what a text-run primitive is in the
|
||||||
|
resolved IR, and which of shaping/metrics the IR owns versus the renderer —
|
||||||
|
because getting this wrong forecloses the toolkit choice T4's spike is
|
||||||
|
supposed to make freely.
|
||||||
|
|
||||||
|
## Coordinator deliverable, alongside W1
|
||||||
|
|
||||||
|
`spec/ANALYSIS_GENESIS_PERSISTENCE.md` — the **field-by-field `Score` table**
|
||||||
|
Ruling B blocker (i) demands before it can be resolved, per the plan's T1b
|
||||||
|
runway. Analysis only: no code, no `.tex`, no core edits, therefore no
|
||||||
|
collision with the parallel track. Fable writes it while W1 runs; the user
|
||||||
|
deep-dives it as a coordination point.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Gate (every packet, actual output)
|
||||||
|
|
||||||
|
The standard six; conformance **9/9 with `--features golden-gate`, 8/8
|
||||||
|
without** (both run); requirement labels reported as observed. Plus two locks
|
||||||
|
specific to this tranche, run as before/after comparisons and reported with
|
||||||
|
their actual digests:
|
||||||
|
|
||||||
|
* **Layout canonical bytes are byte-identical.** Capture
|
||||||
|
`ResolvedLayoutIR::canonical_bytes()` for the reference-suite fixtures at
|
||||||
|
the base commit, then after the change; they must be equal.
|
||||||
|
* **All five GUI goldens are byte-identical** (`ten_measure_open.png` 53638,
|
||||||
|
`ten_measure_insert.png` 54590, `ten_measure_slurs_castoff.png` 57891,
|
||||||
|
`ten_measure_caret_entry.png` 57379, plus G3's reuse of G1). No golden is
|
||||||
|
re-blessed in this tranche — nothing here may change a pixel.
|
||||||
|
|
||||||
|
Blast radius per packet as stated. Do not commit.
|
||||||
|
|
||||||
|
## Report
|
||||||
|
|
||||||
|
Per packet, as every tranche: files + summary, exact asserted values
|
||||||
|
(including the real per-system primitive counts), every mutation with kill
|
||||||
|
evidence, gate output, deviations flagged explicitly.
|
||||||
Loading…
Reference in New Issue