Commit Graph

13 Commits

Author SHA1 Message Date
Levi Neuwirth 7aa0b938e2 Stems point away from the middle line, and reach it
Every stem in the engine pointed up, on the notehead's right, at a constant
octave. A C6 three ledger lines above the staff grew an upward stem shooting past
everything -- visible in the two_staff and three_staff renders. It is also why the
slurs are wrong: an Auto slur is placed OPPOSITE the stems, so no correct side
could be chosen while every stem pointed the same way. Stems are therefore a
prerequisite for the slur fix, not merely adjacent to it.

  - Direction: away from the middle line, decided by the head furthest from it,
    ties going down (the convention for a note on the line, and for a chord that
    straddles it evenly).
  - Attachment: the side it points -- an up-stem at the lowest head's right edge,
    a down-stem at the highest head's left. Read from the head's own bounding box
    rather than the rounded NOTEHEAD_STEM_X: for noteheadBlack those are 1.1807
    and 0, and 1.1807 is Bravura's real stemUpSE (1.18). The old 1.15 was a
    rounding -- it accounts for the whole of ten_measure's churn, where every stem
    moved right by 0.031 and nothing else moved at all.
  - Length: an octave from the outer head, but drawn out to the middle line when
    the note lies beyond it, so no stem dangles in the ledger field.

No version moves. This is the PROJECTION changing, not a solver: to_constrained
emits different geometry from the same graph, so ENGRAVER_VERSION's promise (same
input => same output) is untouched. Every golden churns, stub and engrave alike,
because stems are constrained-stage geometry.

Locked by a_stem_points_away_from_the_middle_line_and_reaches_it, which re-pitches
a generated score across four octaves -- the corpus generator writes only low
notes, so nothing in the suite had ever exercised a down-stem. Mutation-verified:
restoring always-up plus fixed-octave fails it.

Parked, found on the way: BRAVURA_METRICS' notehead anchors are unusable as
written. They name stemUpNW/stemDownSE -- the corners an up- and down-stem never
attach to -- and their x of 1180 reads like 1.18 staff spaces written in
thousandths rather than the table's 1/1024 units (1.18 sp = 1208). Nothing
consumes them except metrics_hash, so correcting them would move the
GlyphCatalogIdentity every conformance claim declares. That makes three parked
candidates; the house rule opens a batch pass at three.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 13:47:47 -04:00
Levi Neuwirth c818992835 Push 3 Standard-tier: optimal break search (replace greedy first-fit)
Casting-off's greedy first-fit + tail-only widow rebalance is replaced by a
deterministic badness-minimizing break search (optimal_breaks, a Knuth-Plass-
style dynamic program over the measure boundaries). ENGRAVER_VERSION 9 -> 10.

Objective: minimize the sum over ALL systems of the squared normalized underfill
((width_limit - w)/width_limit)^2. Squaring evens the systems; including the
FINAL system in the sum subsumes the old widow rebalance (the optimizer won't
leave a narrow final stub if a balanced partition is cheaper). It is the
additive, DP-tractable analog of the retired distribution_cost (max of the
catalog's break penalty and width-CV imbalance). On the ten-measure fixture the
search settles on 5/4 measures where greedy left a fuller-then-shorter split,
filling the final system more and pulling casting_off_quality down (~0.80 ->
~0.61) — the payoff, visible now that horizontal justification drives
system_break to ~0 so casting_off mostly sees the last system's fullness.

Break requirements (hard/soft/page) bound the DP's segments — a system may not
span a forced break — and walk_region still honours them and records skipped
content-less soft breaks as IrOverride, unchanged; optimal_breaks reports only
the automatic breaks. A system may exceed the width only as a single
unsplittable measure. Minimal still makes no optimality claim.

Deterministic: minimizes lexicographic (cost, system_count). Tests:
optimal_breaks_{balances_systems_and_avoids_a_final_widow, never_spans_a_forced_
break, is_deterministic_and_empty_when_unbounded}; the widow test now checks the
balanced measure distribution; wrapping-fixture metrics updated (casting_off
improved). Removed rebalance_widows/rebalance_region/distribution_cost + their
tests. Goldens regenerated (balanced systems; view_box stable — justification
still fills to width). 944 tests, clippy 0, docs -D warnings, conformance 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 21:29:08 -04:00
Levi Neuwirth fea446a182 Justification review fix: keep stems attached to their noteheads
An adversarial review of the justification commit found a SEVERE bug: stems
detach from their noteheads (~0.75 ss, up to ~1.5) in every justified system.
Root cause: the code used is_rigid_width_stroke to select slot-anchored strokes
on the false premise it covered stems. It is LEDGER-ONLY. A stem is an
Event-sourced stroke drawn at notehead_x + 1.15 with no same-source glyph
(noteheads are Pitch-sourced) and no baseline in its x-span, so it fell to the
affine branch and its intra-slot offset was scaled by the justification factor a,
floating it off its head into the gap. The spacing pass shared the same
classification (a smaller latent drift).

Fix: component_glyph classifies a stroke — a Staff (staff line) or
RepeatStructure (volta bracket, whose ending-number glyphs share its source)
source SPANS (affine); else owning_glyph (a ledger over its notehead, same Pitch
source); else the glyph with the greatest baseline <= the stroke's x — a stem's
own in-column notehead (stem_offset 1.15 < column step 1.6, so exactly its slot).
Applied in BOTH the spacing remap and casting, so stems ride their heads through
the whole pipeline. Ledgers are unchanged (owning_glyph path).

Regression: stem_offsets_from_the_notehead_survive_justification (verified to
fail without the fix). Goldens regenerated (stems now on their heads). The minor
slur-inset drift (same root cause, ~0.3 ss on a soft connector) is deferred with
a note. Folded into ENGRAVER_VERSION 8 (unreleased). 941 tests, clippy 0, docs
-D warnings, conformance 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 19:54:18 -04:00
Levi Neuwirth 7c038721d7 Push 3 Standard-tier: per-system justification (fill the content width)
Every non-final system of a multi-system region now stretches its horizontal
slack so its ink fills the content width, instead of sitting at its natural
left-aligned width. ENGRAVER_VERSION 7 → 8 (any wrapping score's baked geometry
differs; a single-system score is unchanged — its only system is ragged-right).

Casting bakes each system by a Placement: a vertical dy plus a horizontal affine
world_x = a·x + b (rigid = a:1, b:dx). A justified system spreads the slack
linearly (a = 1 + extra/span). The map is CLAMPED to the slot-source range: affine
within it, rigid slope-1 beyond it (bearing overhangs, staff lines drawn to the
ink edge), so the mapped ink extremes agree exactly with the per-slot deltas and
the ink spans exactly [left_margin, left_margin + content_width] (no over/under-
shoot). Slot-relative like the E1 remap: glyphs translate by the map at their
SLOT's source (intra-slot offsets survive), spanning strokes map endpoints through
the affine (they stretch), rigid-width strokes (stems/ledgers, via owning_glyph)
track their slot, slur control points map through the affine.

Not justified: a region's last system (ragged-right by convention), a degenerate
span, or a system already at/over width (never compressed into overlap).

Quality consequences (honest): system_break_penalty collapses to ~0 (the point);
the width-uniformity axes rise as the full non-final system contrasts with the
ragged last line (ten-measure casting_off ~0.45 → ~0.80) — a metric-semantics
follow-up noted in DECISIONS. Tests retargeted: the widow rebalance now shows in
measure distribution not baked widths; the floor-column test moved to a synthetic
vector (robust to fixture values). Goldens regenerated (view_box widens to the
content width; same 147 primitives, no collisions). 939 tests, clippy 0, docs
-D warnings, conformance 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 19:21:46 -04:00
Levi Neuwirth 81b7f42f0a Schema major 2 Phase E2: slur curves + cubic-bézier primitive
The third pipeline primitive kind, review-hardened (5 verified findings fixed
pre-commit). A `Curve` (four control points, mirroring `Stroke`) threads
through all three IR stages, the canonical-encode fingerprint (a 5th u32 count
prefix; width-lock 4→5), the round-trip provenance chains and count identity,
`to_render`, the stub solver, engrave remap + casting, SVG path emission, and
hit-testing.

Slurs draw as one cubic bézier per slur carrying the slur's exact provenance
(no synthesis). LayoutContent::Slur resolves each endpoint event to a Note
column at to_logical (SlurEndpoint At/Unresolved — E1's honest-placement
discipline); a symmetric arc whose apex sits `height` from the endpoint line;
curvature_override direction+height honored, style.line (dashed) deferred to
Push 3. Honest non-drawing (traced anchor kept) for an unresolved endpoint, a
non-left-to-right span, or a cross-staff slur (no single staff — would float
at yo=0). Authored height/thickness sanitized to defaults when non-positive
(a negative thickness would else fail validation and blank the layout).

Hit-test: HitShape::Curve (Copy) flattens the cubic to a 16-segment capsule
inside contains/intersects (one region per curve); a slur click resolves to
Slur generically (no editor arm; edit ops refuse it). render-svg: stroked
unfilled <path C> after strokes/before glyphs + curve_count + content-hull
bounds. engrave: HorizontalRemap::curves; casting curve_system = Rigid-to-
start-system (no de Casteljau split — deferred; break-spanning slur draws
whole in its start system, kept for the source surjection); ENGRAVER_VERSION
4→5; slur_shape_penalty now 0.0 by construction (Minimal draws the ideal arc).

Existing SVG goldens changed only in the provenance-note comment (curves now
enumerated) — geometry byte-identical; 6 snapshots gained curve_count; new
ten_measure_with_slurs goldens. 929 tests, conformance 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 15:33:00 -04:00
Levi Neuwirth bc9fb27d36 Clear P12-I11: casting-off widow-rebalance evens the stub last system
RS-1 honestly failed the Minimal casting_off_quality threshold under the
reference engraver: greedy first-fit left a two-measure stub last system
(width CV 0.6145 -> clamped 1.0 > 0.90). Cleared the honest way — an
engrave-side balance pass, no Quality Metric Catalog or core-spec change.

Casting-off gains a second phase, a widow rebalance
(casting::rebalance_widows, run between the greedy walk and vertical
stacking): it moves whole trailing measures from a region's penultimate
system into its final one, choosing the shift that minimizes the larger of
the two distribution penalties the catalog defines for the break family —
the width imbalance (casting_off_quality, the CV of the region's system
widths) and the non-final break penalty (system_break_penalty, the mean of
|W-w|/W over non-final systems). distribution_cost computes each raw by the
same formula as quality.rs's casting_off_raw / system_break_raw (mean not
worst, abs not clamp), so the rebalance optimizes the values the metric
census will report. The two axes pull against each other, so their min-max
lands on a 6/4 split for RS-1 (casting_off 1.0 -> 0.4463, system_break
0.254 -> 0.677, every axis <= 0.90) — with comfortable margin, over the
fragile full-balance 5/5 (system_break 0.889, a hair under 0.90).

Scope is tight: only a region's last boundary moves, and only when greedy
placed it (an Automatic boundary with no break requirement or page force
pinned to its slot); a user/IR-anchored or page-forced boundary is never
disturbed, the penultimate system keeps >= 1 measure, the final never grows
past its predecessor, and the system count is unchanged — so every
break-count and page-assignment invariant (and all break-constraint tests)
hold untouched.

The casting_off 0.5 anchor and the 0.90 Minimal column were vindicated, not
relaxed: the engraver improved, no anchor rescale / threshold loosening /
RS-1 override. Core spec Chapter 9's "Minimal makes no optimality claim"
already permits the heuristic, so nothing normative changed (no .tex/PDF
rebuild). P12-I12 (the Standard-tier spacing floor on short scores) stays
open.

- engrave: rebalance_widows + distribution_cost + two-phase module docs;
  ENGRAVER_VERSION 2 -> 3 (a wrapping score's baked geometry differs from
  pure greedy); three new casting tests (even-split preference, the
  mean-not-worst break penalty for 3+ systems, and the resolved final
  system); the_wrapping_fixture_is_measured_honestly re-pinned to the 6/4
  values (both axes floor-warn under Standard, status untouched).
- testkit: RS-1 minimal_xfail row removed (promoted to a plain Pass); the
  suite ships no xfail rows.
- render-svg: ten_measure.engrave.{svg,snapshot} goldens regenerated
  (view_box width 83.99 -> 64.95; still two systems).
- process trail: PASS12_BATCH I11 struck; PASS12_RATIFICATION_LOG
  "no spec change" section; engrave DECISIONS casting-off decision 9 +
  quality item 7 + candidate promoted.

860 workspace tests pass; clippy -D warnings, fmt --check, rustdoc
-D warnings all clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
2026-07-03 19:09:51 -04:00
Levi Neuwirth 0316160395 Phase 3 tranche 1: casting-off, K1 schema-fill, value-restoring undo
The chosen Phase-3 attack, run as two parallel waves. 829 workspace
tests pass; clippy -D warnings, fmt, and rustdoc clean; all three
spec documents build with zero undefined references.

Casting-off (epiphany-engrave/src/casting.rs, wired into the
Engraver):
- Greedy first-fit system breaking per region at measure-start
  barline columns; a measure that would overflow the content width
  starts a new system. Hard SystemBreakAt/PageBreakAt always
  honoured; soft breaks honoured unless pathological (skipped with
  the spec's warning + IrOverride-recorded decision).
- Vertical stacking from real content extents with the inter-system
  gap read from the vertical band model; page overflow starts the
  next page. World frame: pages stacked vertically, coordinates
  baked into glyphs/strokes, so the SVG renderer, hit-testing, and
  the GUI viewport are unchanged.
- Real ResolvedPage/ResolvedSystem trees (1-based page numbers,
  content bounding boxes, staves from staff-line segments, measures
  from barline columns); every chosen break appends an
  EngravingDecision with MUSCLOID EngravedBreak provenance,
  UserOverride-attributed via the new ConstrainedLayoutIR
  break_origins sidecar; staff lines split per system with
  synthesized continuation provenance.
- Break-constraint evaluation flips: satisfied iff the layout breaks
  at the slot. The two single-system tests invert deliberately
  (a hard break is now honoured; a user break is honoured and
  attributed instead of warned). Geometric constraints evaluate in
  the pre-casting spaced frame (documented).
- Page geometry is engraver-side PageGeometry (A4 portrait at an
  8 mm staff: page 105 x 148.5 staff spaces, margins 7.5, content
  90 x 133.5; arithmetic documented) — Canvas.layout_defaults has no
  graph home and is a schema-major addition (P12-I7).
  ENGRAVER_VERSION = 2. Goldens regenerated: ten_measure_single_staff
  engraves as 2 systems (viewBox 84x20.6, was 103x11);
  valid_score_rich as 3 systems; stub goldens byte-identical.

K1 schema-fill (Operation Catalog 0.4.0 -> 0.5.0, ratified first;
wire discriminants strictly appended):
- CreateStaff (24 / tag InsertStaff 24): set-union mint of a global
  Staff; CreateStaffInstance now preconditions that its referenced
  staff is live.
- SetTimeSignature (25): value-carrying meter-change LWW keyed by
  (region, resolved position); the carried TimeSignature mints
  set-union; StructuralFieldCollision on meter_sequence.
- SetTempoSegment (26): LWW keyed by (scope, resolved start) over
  the score or region tempo map; a write that would malform the map
  refuses with the appended PreconditionFailureReason 11
  (TempoMapMalformed).
- SetStaffLayout (27): LWW advisory over the staff instance's three
  inline layout fields.
- Create score/canvas remain deliberately unavailable slots: the
  root and canvas are inline singletons with no addressable object
  model (P12-K8), not force-designed.

Value-restoring undo (P11-C8 narrowed; catalog §UndoTransaction
rewritten and per-primitive undo notes updated):
- Canonical-order write chains (base-seeded) across all eleven LWW
  families. StrictInverse restores each written key to its
  chain-predecessor value iff the transaction's write is still the
  key's last writer, else refuses the whole undo with a
  TransactionConflict naming the superseder; BestEffort restores the
  still-last keys. Clean compensations are Applied; only minted-
  object tombstone repairs ride AppliedWithRepair (no new repair
  vocabulary). Mixed mint+overwrite transactions compose; strand
  guards refuse tombstoning mints still referenced by live
  non-members.
- Undo-of-undo pinned and tested: restorations are chain writes, so
  undoing the undo's transaction restores the undone value, and a
  second undo of the same transaction conflicts (absence-restores
  repeat idempotently — documented asymmetry, P12-K11).
- Permutation invariance pinned across five delivery orders; the
  convergence generators gain the new ops and a tx-then-undo flow.
- Still deferred in normative text: delete resurrection (needs a
  system-derived tag outside the ratified closed set), Transpose
  inversion (P12-K2), Cascade dependent closure.

Trackers: Binary Format companion 0.1.0 -> 0.2.0 (appended wire/tag
tables, PreconditionFailureReason 11, payload layouts, history row —
a schema-minor evolution under its own rules); nine new Pass-12 rows
(C5, K8-K11, I7-I10); core-spec OperationKind listing gains the four
kinds; revision-history rows in core spec and companion.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
2026-07-02 21:55:26 -04:00
Levi Neuwirth 9539b1652b layout-ir, engrave: ledger lines
Notes above or below the staff now get ledger lines — the gap the GUI surfaced
the moment a note is moved off the staff. Each notehead carries its StaffStep;
to_constrained emits one short horizontal stroke per whole step between the
staff (lines at steps 0..=8) and the note, reaching LEDGER_LINE_EXTENSION past
each side of the note's actual bounding box (so a wide whole note gets a wider
ledger), synthesized from the pitch so the strokes are deterministic and
hit-testable. The synthesis key splits component (high 64 bits) from signed step
(low 64) so two components of a very low note can never collide.

Ledger lines are fixed-width marks, not system-spanning lines, so the Engraver's
horizontal spacing must not scale them. is_rigid_width_stroke marks them; the
remap translates such a stroke rigidly by its *owning glyph's* column delta
(found by source, not the stroke midpoint — which for a wide head can fall nearer
a neighbouring column), so it keeps both its length and its offset from the
notehead. The spacing pass folds each ledger's extent into its notehead's slot,
so adjacent off-staff notes' ledgers reserve room and do not overlap. The stub
solver passes ledgers through unchanged.

Tests cover the step geometry, key distinctness (incl. steps below -128), the
bbox span, width preservation and offset (no-drift) through the Engraver, the
adjacent-overlap spacing, and an explicit two-whole-note off-staff drift case.
Render goldens regenerated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS
2026-06-29 17:12:56 -04:00
Levi Neuwirth e2d389330d P12-I2: wire the ratified MUSCLOID layout-object id derivation
The spec's domain-tag registry reserves a non-canonical MUSCLOID layout tag for
LayoutObjectId derivation (req:layoutir:object-id-derivation, Pass-11 item 2.6),
but it was never realized in code: layout-ir minted provisional, untagged ids and
synthesized objects borrowed MUSCCONF. This wires the ratified derivation.

  - epiphany-determinism: add the reserved built-in DomainTag::LAYOUT_OBJECT_ID
    (`MUSCLOID`), non-canonical/layout-namespace like FONT_METRICS (following the
    SYSTEM_ANOMALY Pass-11 precedent of adding a reserved tag). The tag-enumeration
    tests now derive from BUILTINS so they cannot drift; the spelling is locked and
    from_bytes resolves it as a non-system builtin.
  - layout-ir provenance.rs: all three LayoutObjectId derivations route through
    MUSCLOID exactly per the requirement -- single keyed on source.canonical_bytes(),
    multiply-manifested on (source, region), synthesized on (source, synthesis_kind,
    instance_key); synthesized no longer borrows MUSCCONF. A reference-lock test
    pins the derivation and proves it is genuinely domain-separated; another asserts
    the three keying schemes do not collide (safe by the discriminant-led,
    fixed-width canonical_bytes).
  - layout-ir engraving.rs: EngravingDecisionId borrowed MUSCCONF for the same
    reason; moved it onto MUSCLOID too, keeping its `engraving-decision` prefix so it
    cannot alias a layout-object id within the namespace.

Layout ids are non-canonical (never document state, in no content hash), so this
changed id *values* but no durable or interchanged artifact: the only golden churn
is the data-prov hex in the four render goldens (every changed line is a data-prov;
geometry/structure byte-identical).

Spec/status sync: core_spec.tex descriptive notes (the requirement tail, the
domain-tag registry row, the registry intro, and the revision-history entry) now
say the reference implementation wires MUSCLOID as of P12-I2; MUSCLOID is moved out
of the "deferred to the companions" (not-ratified/provisional) list and given a
non-canonical anchor paragraph after the reference-implementation-locks table.
PASS12_BATCH.md marks P12-I2 resolved; PASS11_RATIFICATION_LOG.md keeps the
historical row with a "superseded by P12-I2" note; layout-ir/DECISIONS.md updates
the ratified-block note, the id bullet, and the open candidate (now RESOLVED).

Full gate green: build, fmt, clippy, 587 tests, conformance scale 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 12:46:19 -04:00
Levi Neuwirth 6293734fa0 Agent I-4c: embedded @font-face glyph mode (a second self-contained renderer)
A render mode that references each glyph by its SMuFL codepoint via `<text>`,
drawn from an `@font-face`-embedded Bravura subset, alongside the default inline
`<path>` outlines. The SVG stays self-contained (the font travels in it) and the
text is selectable, at a larger file size.

  - GlyphMode::EmbeddedFont: the SVG declares the font once via
    <defs><style>@font-face{...}</style></defs>, then emits one
    <text transform="translate(x y) scale(1 -1)" font-size="4" ...>&#xNNNN;</text>
    per glyph. Geometry is consistent with PathOutline by construction: same
    origin, a per-glyph counter-flip cancels the outer y-flip, and the SMuFL em is
    four staff spaces. A new `text_count` stat; unbundled glyphs still fall through
    to the visible bbox rect + diagnostic. The metadata comment declares which mode
    produced the SVG (and, on the empty canvas too, via a shared `glyph_note`).
    Path mode stays the byte-golden-locked, pixel-verified reference; the embedded
    mode is structurally tested (well-formed XML — also under `xmllint`; one
    @font-face; one <text>/codepoint per glyph; provenance preserved; determinism).

  - The subset is a GENERATED artifact, not a vendored binary: a deterministic
    base64 OTF emitted into src/font_subset_generated.rs by
    `tools/extract_bravura_outlines.py --font-out` (the same SHA-pinned 1.392 font
    the outlines come from). `recalcTimestamp=False` keeps the source font's fixed
    head.modified so the bytes are reproducible across runs, not just within one.

  - OFL compliance: the subset is a Modified Version, so its PRIMARY font name is
    renamed off the Reserved Font Name "Bravura" to "EpiphanyBravuraSubset" in BOTH
    naming structures an OTF carries — the SFNT `name` table AND the CFF (Name INDEX
    + top-dict FullName/FamilyName). The copyright/trademark/license records, which
    name Bravura as attribution, are kept; the renderer references the renamed
    family in @font-face and <text>. The generator reparses the saved bytes and
    fails if the reserved name leaks into a primary record, and validates the cmap
    covers every glyph.

  - Machine-locked payload: the generator emits the decoded length and a BLAKE3-256
    (the workspace's sole hash) of the font bytes; a render-svg test base64-decodes
    the payload (no new runtime dep) and asserts the length, the OTTO signature, the
    BLAKE3, and — parsing the SFNT name table and CFF Name INDEX — that neither
    primary name is the reserved name. Adds an epiphany-determinism dev-dep.

The demo example gains `--glyph-mode=path|embedded`; lib/README/DECISIONS document
the two modes, the subset's OFL rename, and the regeneration command (--font-out +
the blake3 dependency). PathOutline goldens are byte-unchanged; the outlines stay
byte-identical. Full gate green: build, fmt, clippy, 585 tests, conformance scale 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 11:09:34 -04:00
Levi Neuwirth 41cd8bf58b Agent I-4b: a trace-free render declares itself display-only
`RenderOptions::emit_provenance = false` dropped every data-prov trace silently,
so the output was indistinguishable from an archival render even though it no
longer satisfies the renderer's "every element traces to its source" contract.

Now the SVG's metadata comment declares the provenance state, so suppression is
announced rather than silent:

  - archival (default):    "...; every glyph and stroke carries a data-prov trace
                            to its score-graph source"
  - display-only (false):  "...; provenance traces suppressed (display-only
                            output, not archival)"

A shared `provenance_note(emit_provenance)` helper feeds both the main render and
the empty-canvas path, so an empty trace-free layout is held to the same honesty
contract as a full one (neither can drift). The module doc and the render-svg
DECISIONS.md non-overreach rule now frame data-prov as the default archival
contract plus an explicit, declared display-only mode. Tests assert the suppressed
marker (full and empty layouts) and that the default render declares traces
present.

Goldens regenerated: the default (archival) render's metadata comment now carries
the new "every glyph and stroke carries a data-prov trace" clause, so the four
`.svg` goldens change by that one line (the snapshots, which omit the comment, do
not). Full gate green: build, fmt, clippy, 581 tests, conformance scale 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-27 09:47:35 -04:00
Levi Neuwirth eec63aa244 Agent I-4a: reconcile the Bravura metrics with the 1.392 outlines
The layout metrics table claimed Bravura 1.38.0 while the renderer's outlines are
extracted from 1.392, so the advances/bboxes the engraver reserves space and
evaluates collisions from did not match the ink actually drawn (P12-I3). Both now
come from the SAME SHA-pinned bravura-1.392 font.

  - BRAVURA_METRICS is regenerated from 1.392 by tools/extract_bravura_outlines.py
    (the same font and script the outlines come from; the regenerated outlines are
    byte-identical to the committed ones, and the extracted timeSig rows match the
    values bundled earlier -- two integrity checks that the extraction is faithful).
    Several old rows were not just stale but wrong: flag8thUp had its ink above the
    origin when the glyph hangs below the stem tip; restWhole/restHalf and the
    clefs had approximate or mis-signed bounds.
  - The metric bbox is rounded OUTWARD from the outline bounds (floor the mins,
    ceil the maxes) rather than to the nearest 1/1024, so the integer metric box
    always CONTAINS the drawn outline -- the engraver evaluates collisions from
    that box, and a containing box keeps a hard no-collision result honest on
    paper. A new render-svg test (metric_bboxes_contain_the_drawn_outlines) proves
    the containment for every bundled glyph, so a future re-extraction cannot
    silently regress it.
  - BRAVURA_VERSION is 1.38.0 -> the literal font version: name-table ID 5 reads
    "Version 1.392" and head.fontRevision ~= 1.392 (a single decimal), recorded
    verbatim as SemVer { major: 1, minor: 392, patch: 0 } so the identifier
    round-trips to the font's own string. The canonical mapping rule is documented
    on BRAVURA_VERSION; DECISIONS.md is updated to match.

The corrected metrics surfaced a coupled placement bug: the old barlineSingle
metric falsely centred the glyph (+/-2048), so the bottom-origin Bravura barline
(which runs 0..4 staff spaces UP from its origin) was anchored at staff-centre and
floated above the midline. It is now anchored at the staff bottom (yo), so a
barline connects the bottom and top staff lines.

Goldens regenerated (stub + engrave) for the corrected geometry. Full gate green:
build, fmt, clippy, 581 tests, conformance scale 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 22:45:33 -04:00
Levi Neuwirth af297881e2 Agent I-3: criterion-6 round-trip and golden lock on the real Engraver
Criterion 6 (the Chapter 7 layout round-trip) and the render goldens previously
exercised only the verbatim StubSolver, so a regression in the real Engraver's
geometry could land unseen. I-3 drives both through the Engraver.

  - round_trip_with<S: ConstraintSolver> factors the solver-agnostic provenance
    contract out of round_trip (now a one-line stub wrapper): coverage, the
    complete Provenance surviving constrained -> resolved -> render, the source
    surjection, and no duplicate stable ids hold for *any* conformant solver. The
    Stub tier's verbatim-geometry clause is gated behind solver.tier() == Stub;
    every other tier re-spaces. The status gate accepts any renderable status
    (Solved / SolvedWithWarnings / PartialBudgetExhausted), not exactly Solved, so
    the helper matches its "arbitrary conformant solver" contract while still
    rejecting the diagnostic-only statuses that carry no authoritative layout.
  - criterion_six_round_trips_through_the_engravers_respacing (epiphany-engrave)
    runs the full graph -> logical -> constrained -> *engraved* -> render round
    trip over the criterion-6 hand-off fixtures -- ten_measure_single_staff (the
    measured fixture) and valid_score_rich (cross-cutting tuplet/tie/spanner),
    plus valid_score for breadth -- and asserts the whole provenance contract
    survives the Engraver's re-spacing. A non-vacuity check confirms the Engraver
    genuinely moved geometry, so provenance is preserved *through* a real geometry
    change -- the statement the verbatim stub can never make. This adds an
    epiphany-testkit dev-dep (no cycle: testkit does not depend on this crate).
  - The render-svg engraver acceptance test is upgraded from invariant-only to
    byte-locked: new .engrave.snapshot.txt / .engrave.svg goldens for both
    fixtures capture the Engraver's re-spaced output (e.g. ten_measure view_box
    width 82.26 vs the stub's 88.88, same glyph/stroke/class counts), so an
    Engraver geometry regression is caught at the byte level. A companion test
    asserts the engrave goldens genuinely differ from the stub goldens, catching
    the degeneracy where the Engraver echoes the stub (which would otherwise pass
    both golden checks independently).

Also corrects the epiphany-engrave package description, which still claimed it
reports SolverTier::Stub until it earns Minimal (it earned Minimal in I-2).

Full gate green: build, fmt, clippy, 580 tests, conformance scale 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 21:58:31 -04:00