Editor T4-pre W2: the glyph-asset contract, and three parallelism claims get finer

The W2 contract charters the shared typed glyph-asset seam Ruling A names as a
T4 prerequisite. Scoping it turned up the same shape W1 had: the seam is
already designed and merely unpopulated. PathCommand, GlyphRenderData, and
GlyphCatalog::render_data all exist in layout-ir, and BravuraCatalog returns
None by deliberate documented honesty -- reporting Some would claim render data
that does not exist. So the packet fills a seam rather than building one.

Two findings reshape it from the sketch carried in the W1 contract. Bravura.otf
is not in the tree -- tools/ holds only the extractor script and OFL.txt, and
the generated header pins source hashes verified at extraction time -- so
"have the generator emit typed paths alongside the d strings" cannot be
executed here. The contract replaces it with a dependency-free in-crate parser
over exactly the grammar the generator emits, and proves equivalence by
round-trip: parse every bundled d, re-emit, compare byte-for-byte, with a
sanctioned coordinate-sequence fallback that must be reported if used. And the
metrics table is conformance identity -- metrics_hash hashes (name, metrics)
pairs with values participating, and GlyphCatalogIdentity is encoded into the
resolved layout's canonical bytes -- so it is out of bounds entirely.

The test worth watching is the cross-table one. glyph.rs claims the metrics and
the outlines agree because both came from the same Bravura release; that is
asserted in prose and tested nowhere. The contract requires comparing each
glyph's real outline extent against its declared bbox, reporting the worst-case
deviation, and treating a failure as a finding rather than a reason to widen
the tolerance -- it would mean engraving reserves the wrong space for that
glyph, which is the bug class that twice bit the vertical metric.

Three parallelism claims are corrected in the same pass, all mine and all too
coarse. The plan and the ruling both said T1b's lease/save/single-writer
machinery could be contracted in parallel with the genesis work. That was
written before the ladder existed, and it is now per-rung rather than
unconditional: G1 needs no accept-set raise and never enters epiphany-bundle,
so T1b's bundle work runs beside it, while G2 spends the raise in bundle.rs
where T1b's single-writer enforcement also lands, so those two must not fly
together. Ruling B's blocker note also still described the identity disposition
as blocking; it is ruled, and now points at where.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Levi Neuwirth 2026-07-24 17:50:02 -04:00
parent 439e1e2e14
commit a35f534235
3 changed files with 174 additions and 7 deletions

View File

@ -0,0 +1,156 @@
# Contract: Editor T4-pre W2 — the shared typed glyph-asset seam
Repo root `/home/jeans/Repos/active/epiphany`. Governed by
`spec/CONTRACT_EDITOR_T4PRE_IR.md` §W2 and `spec/PLAN_EDITOR_APP.md` §3.7 /
Ruling A, which names "a shared typed glyph-asset seam" as a T4 prerequisite:
a canvas tessellator needs typed vector paths from a crate both the renderer
and the app can depend on. W1 landed at `dd33b34`.
Execution model as T1a/T2/T3/W1: Sonnet subagent, 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 genesis-operation tranche is in flight on the other
track, laddered as G1/G2/G3 in `spec/PLAN_GENESIS_OPS.md` under
`spec/RULING_GENESIS_PERSISTENCE.md`. Across its rungs it owns
`epiphany-core`, `epiphany-ops`, all `.tex` (so the requirement counts move),
testkit's requirement-label constants, vectors and fuzz generators, and — at
G2 — `epiphany-bundle` for the `OperationEnvelopeBlock` accept-set raise.
**This packet touches none of them**, at any rung. Report observed requirement
actuals; never assert stale numbers.
---
## The verified starting point
Confirm these as you go; they make the packet far smaller than §3.7's wording
suggests:
* **The typed path type already exists, in `layout-ir`.** `PathCommand`,
`GlyphRenderData { outline: Vec<PathCommand>, bitmap: Option<GlyphBitmap> }`,
and `GlyphCatalog::render_data(&self, name) -> Option<GlyphRenderData>` are
all defined (`layout-ir/src/glyph.rs:~285-315`). **W2 populates a seam that
is already designed; it does not design one.** `BravuraCatalog::render_data`
returns `None` today by deliberate, documented honesty — "reporting `Some`
would claim render data that does not exist."
* **The outlines exist, as SVG `d` strings**, in
`render-svg/src/outlines_generated.rs`: `BravuraOutline { name, codepoint,
path: &'static str, bbox: [f32; 4] }`, staff-space units, y-up, 4 decimals,
sorted by name. `outline()` is `pub(crate)`; `bundled_glyph_count()` and
`smufl_codepoint()` are `pub` but have **no callers outside `render-svg`**
(verified workspace-wide).
* **`Bravura.otf` is NOT in the tree.** `render-svg/tools/` holds only
`extract_bravura_outlines.py` and `OFL.txt`; the generated header pins
source SHA-256s verified *at extraction time*. Re-running the extractor
therefore requires fetching the font and **cannot be assumed**. This
determines pin 4.
* **The metrics table is conformance identity.** `BRAVURA_METRICS` lives in
`layout-ir/src/glyph.rs`; `metrics_hash_for` hashes `(name, metrics)` pairs
with the *values* participating (the crate's own note at `glyph.rs:372-375`
exists precisely to prove a field participates), and `GlyphCatalogIdentity`
is encoded into `ResolvedLayoutIR`'s canonical bytes.
## Design pins
1. **A new crate, `epiphany-glyphs`**, owns the glyph assets: the moved
`outlines_generated.rs`, the extractor tool, and **`OFL.txt`, which travels
with the redistributed outlines** (the generated file's license header
stays intact — Bravura is SIL OFL 1.1 and the notice is a redistribution
condition, not decoration). It depends on `epiphany-layout-ir` and exposes
a catalog type implementing `GlyphCatalog` with a **real `render_data`**.
No cycle: glyphs → layout-ir, never the reverse.
2. **`render-svg` keeps its public API and its exact output.** It depends on
`epiphany-glyphs`, and `bundled_glyph_count`/`smufl_codepoint` remain
exported from `render-svg` (delegating or re-exporting) even though nothing
outside calls them — an API that costs one `pub use` is not worth breaking.
`font_subset_generated.rs` stays in `render-svg`: an embeddable font subset
is a renderer concern, not a shared asset.
3. **The metrics table does not move and does not change.** Out of bounds
entirely. Any edit to it churns every conformance byte in the repo.
4. **Typed paths are derived from the `d` strings by an in-crate parser, not
by re-running the extractor.** The font is not in the tree, so regeneration
is not reproducible here. The parser covers exactly the grammar the
generator emits — absolute `M`/`L`/`C`/`Z`, decimal coordinates — and the
crate stays **dependency-free** (it is in the MSRV closure; no new
dependency, no `build.rs`).
5. **Equivalence is proven by round-trip, not asserted.** Parse every bundled
glyph's `d`, re-emit it in the generator's exact formatting, and compare to
the original string **byte-for-byte**. If all bundled glyphs round-trip,
the typed form provably describes the same geometry and no geometric
spot-checking is needed. *Sanctioned fallback if the generator's number
formatting cannot be reproduced exactly:* parse both the original and the
re-emitted string and compare the full coordinate sequences as exact `f32`.
**Report which was used and why** — do not silently take the weaker one.
6. **`render_data` is deterministic and side-effect-free.** Caching (a
one-time lazily built table) is permitted; it must not change results, and
two calls must return equal data.
## Tests + minimum mutations
* **(g1) parser round-trip** over every bundled glyph (pin 5) — the packet's
load-bearing test. *Mutation:* perturb one parsed coordinate → dies.
* **(g2) `Close` survives parsing** — *mutation:* drop the trailing `Z`
dies.
* **(g3) every pipeline glyph has render data** — the `BRAVURA_METRICS` name
set all resolve through the new catalog's `render_data`, mirroring
`render-svg`'s existing `every_pipeline_glyph_has_a_bundled_outline`.
*Mutation:* return `None` for one bundled glyph → dies.
* **(g4) outline ink fits the declared metrics bbox** — the cross-table
consistency the tree asserts in prose and tests nowhere: `glyph.rs:107-109`
claims the metrics and outlines come from the same release "so reserved
advances/bboxes and the drawn ink agree." Compare each glyph's outline
extent (staff spaces) against its `BRAVURA_METRICS` bbox (font units, 250
per staff space), with a stated tolerance for the outlines' 4-decimal
rounding. **Report the actual worst-case deviation.** *Mutation:* inflate
one parsed coordinate past the declared box → dies. **If a real glyph fails,
that is a finding to report — not a reason to widen the tolerance.** It
would mean engraving reserves the wrong space for that glyph, the same bug
class that twice bit the vertical metric.
* **(g5) `render-svg`'s bytes do not move** — *mutation:* emit the `d` from
the typed form instead of the stored string → the goldens die.
* **(g6) absolute, not relative** — *mutation:* treat a `C` as relative →
round-trip dies.
## Blast radius
New `crates/epiphany-glyphs/**` (including the moved
`outlines_generated.rs`, `tools/extract_bravura_outlines.py`, `tools/OFL.txt`,
and a `DECISIONS.md`); `crates/epiphany-render-svg/src/{lib.rs, outline.rs,
svg.rs}` and its `DECISIONS.md`; the workspace `Cargo.toml` member list.
Nothing else — no `layout-ir` edit (the seam is already there), no
`epiphany-core`, no `epiphany-ops`, no `epiphany-bundle`, no `.tex`, no CI
change (the MSRV job runs `--workspace --exclude epiphany-editor-gui`, so a
new crate joins automatically — which is exactly why it must be MSRV-clean and
dependency-free).
## Gate (actual output)
The standard six; conformance **9/9 with `--features golden-gate`, 8/8
without** (both run); requirement labels reported as observed. Plus the two
W1 byte locks, run as before/after comparisons against the base commit and
reported with their actual sizes:
* **Layout canonical bytes byte-identical** for the reference-suite fixtures
plus `ten_measure_single_staff(0)` and `ten_measure_with_slurs(0)`. (W1's
numbers, for reference: 21615 / 6948 / 3839 / 5602 / 2299 / 2299 / 21615 /
22119.)
* **All five GUI goldens byte-identical.** Nothing here may change a pixel;
no golden is re-blessed in this packet.
* **`cargo tree -p epiphany-glyphs` shows no third-party dependency.**
## Coordinator deliverable, alongside
`spec/ANALYSIS_TEXT_RUN_PRIMITIVES.md` — W3, the text-run primitive decision
(Ruling A criterion 3: shaping, fallback, bidi, and metrics consistent across
canvas, SVG/PDF export, hit testing, and the accessibility tree). A decision
document, not an implementation, written by Fable while W2 runs, because
getting it wrong forecloses the toolkit choice T4's spike exists to make
freely.
## Report
Per packet, as every tranche: files + summary, exact asserted values
(including g4's worst-case deviation), every mutation with kill evidence, gate
output, deviations flagged explicitly.

View File

@ -232,8 +232,12 @@ after the resolver tranche lands.
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
lease/save/single-writer machinery does not *depend* on that tranche, and
its parallel-safety is **per-rung** (`spec/PLAN_GENESIS_OPS.md`): G1
(`CreateInstrument`) needs no accept-set raise and never enters
`epiphany-bundle`, so T1b's bundle work runs beside it; **G2 spends the raise
in `bundle.rs`**, where T1b's single-writer enforcement also lands, so the
two must not fly together (corrected 2026-07-24); (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
@ -598,8 +602,11 @@ which is why T1b sequences after the resolver.)*
> 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.
> the `identity` cursor is **ruled** in that document's §3 (it stays on `Score`,
> byte-equality claims confine to `MaterializedState`, and from-empty reduction
> derives the counter). Execution is laddered in `spec/PLAN_GENESIS_OPS.md`;
> T1b's bundle work is parallel-safe with G1 but not with G2. 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

View File

@ -237,9 +237,13 @@ existing types do not move.
`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.
blocker (ii) plus the Ruling-D ownership API. The lease/save/single-writer
machinery does not depend on genesis being authorable, but its parallel-safety
is **per-rung** (`spec/PLAN_GENESIS_OPS.md`): G1 needs no accept-set raise and
never enters `epiphany-bundle`, so T1b's bundle work runs beside it; **G2
spends the raise in `bundle.rs`**, where T1b's single-writer enforcement also
lands, so those two must not fly together. (Corrected 2026-07-24: this section
originally claimed unconditional parallel-safety.)
*Related: `spec/ANALYSIS_GENESIS_PERSISTENCE.md`, `spec/PLAN_EDITOR_APP.md`
§Ruling B / §3.7, `spec/PLAN_PUSH4B_TUNING.md` (the tranche mold).*