8.6 KiB
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> }, andGlyphCatalog::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_datareturnsNonetoday by deliberate, documented honesty — "reportingSomewould claim render data that does not exist." - The outlines exist, as SVG
dstrings, inrender-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()ispub(crate);bundled_glyph_count()andsmufl_codepoint()arepubbut have no callers outsiderender-svg(verified workspace-wide). Bravura.otfis NOT in the tree.render-svg/tools/holds onlyextract_bravura_outlines.pyandOFL.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_METRICSlives inlayout-ir/src/glyph.rs;metrics_hash_forhashes(name, metrics)pairs with the values participating (the crate's own note atglyph.rs:372-375exists precisely to prove a field participates), andGlyphCatalogIdentityis encoded intoResolvedLayoutIR's canonical bytes.
Design pins
- A new crate,
epiphany-glyphs, owns the glyph assets: the movedoutlines_generated.rs, the extractor tool, andOFL.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 onepiphany-layout-irand exposes a catalog type implementingGlyphCatalogwith a realrender_data. No cycle: glyphs → layout-ir, never the reverse. render-svgkeeps its public API and its exact output. It depends onepiphany-glyphs, andbundled_glyph_count/smufl_codepointremain exported fromrender-svg(delegating or re-exporting) even though nothing outside calls them — an API that costs onepub useis not worth breaking.font_subset_generated.rsstays inrender-svg: an embeddable font subset is a renderer concern, not a shared asset.- 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.
- Typed paths are derived from the
dstrings 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 — absoluteM/L/C/Z, decimal coordinates — and the crate stays dependency-free (it is in the MSRV closure; no new dependency, nobuild.rs). - 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 exactf32. Report which was used and why — do not silently take the weaker one. render_datais 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)
Closesurvives parsing — mutation: drop the trailingZ→ dies. - (g3) every pipeline glyph has render data — the
BRAVURA_METRICSname set all resolve through the new catalog'srender_data, mirroringrender-svg's existingevery_pipeline_glyph_has_a_bundled_outline. Mutation: returnNonefor 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-109claims 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 itsBRAVURA_METRICSbbox (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 thedfrom the typed form instead of the stored string → the goldens die. - (g6) absolute, not relative — mutation: treat a
Cas 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)andten_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-glyphsshows 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.