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:
parent
439e1e2e14
commit
a35f534235
|
|
@ -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.
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).*
|
||||
|
|
|
|||
Loading…
Reference in New Issue