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>
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>
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
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
Two audit pushes whose code edits interleave line-by-line in the same
files (reduce.rs, bundle.rs, the DECISIONS logs), committed together so
the tree at every commit builds. Gate: 784 workspace tests pass, clippy
-D warnings clean, fmt clean.
Push 1 — the true MUST violations, all fixed:
- bundle: zstd read support on both read paths, output bounded by the
declared uncompressed_length, typed decompression errors, explicit
CompressedManifest rejection (zstd 0.13 workspace dep; write path
stays uncompressed per the Phase-3 deferral).
- ops: system-derived counter collision check — mint registry seeded
from the base graph, canonical-order pre-walk, halt via the new
PendingReason::HaltedBySystemCollision (discriminant 4, additive)
with transaction-atomicity and causal-dependent closure; neither
input set occupies a collided counter. canonical_pitch_bytes made
pub in core for the MUSCSPCH preimage.
- ops: Transpose skips tombstoned targets per the catalog; missing
targets still refuse the whole operation.
- ops: marker re-anchoring recorded as a RepairRecord in the
triggering operation's effect; ResolveConflict meta-conflicts name
both resolvers; base-free pitch-id freshness; reserved effect
vocabulary annotated.
- core: decomposition pre-pass honors authored attachments
(resolve_decomposition, spec-default precedence); inversion
tolerance typed as a TempoIntegration-class Tolerance.
- CONFORMANCE.md: the determinism conformance statement required by
Appendix D — all seven declarations.
Push 3 — wiring the types-only machinery:
- layout-ir/engrave: to_constrained emits real constraints (successive
notehead no-collision chains, per-glyph region containment, soft
user-break constraints); ConstraintStrength{Required, Preferred}
with strength-by-rule; Preferred violations surface as warnings, not
failures; StubSolver reworked honest-but-renderable. SVG goldens
byte-identical; snapshot constraint counts regenerated (0->90/15).
- layout-ir: to_logical projects user system/page breaks as anchored
EngravingOverrides with paired UserOverride-sourced decisions
(OverrideKind::SystemBreak/PageBreak carry TimeAnchor, ratified in
the spec alongside).
- layout-ir/ops/editor-core: edit-barrier bridge — decode mirrors for
the whole barrier tree (reject-never-normalize, NFC revalidation,
MAX_CONDITION_DEPTH = 64), golden-locked blob codec for the
ExtensionDeclaration fields, a barrier gate in apply and
apply_transaction backed by a Score oracle and real containment
contexts, and apply_unsafe recording the crossed extensions in
extensions_requiring_tombstone() for the next bundle write.
- ops: ResolveEquivocation meta-operation per the newly ratified
catalog entry — payload discriminant 3 (appended), set-level
earliest-resolve-governs promotion, ResolveConflict-mirrored
meta-conflicts, permutation-invariance fuzz; the missing golden
locks on the OperationKind/OperationPayload wire tables added.
- ops/editor-core: validation modes — ValidationMode + a non-canonical
advisory layer (validate.rs), an authoring gate before minting, and
reduction pinned as replay mode by construction (canonical bytes
untouched).
- bundle: the operation index (opindex.rs) — provisional golden-locked
payload, binary-search locate, staleness defined as full-ChunkRef
set equality against operation_roots, and the reject-and-rebuild
discipline (a defective index is never bundle corruption).
- ops: re-anchoring rule table completed — the four-key "nearest"
ordering computed from base-free ledger indices; markers re-anchor
to the nearest live event in the same staff instance (replacing the
Push-1 region-start stand-in); cue-source cascade; graphic-gesture
Events/Range/Free rows; comment and analytical-annotation orphaning.
Zero appended discriminants.
Spec enablers ratified with Push 3: catalog §ResolveEquivocation
(0.3.0 -> 0.4.0) and anchored break overrides; 16 new Pass-12 rows
filed (C1-C4, K5-K7, I4-I6, D1, E1-E5). The data-model payload
expansion (SlurKind, beam geometry, voltas, instrument bodies,
metadata) is deliberately staged to the Binary Format companion — the
positional graph codec has no value-level versioning, so filling those
structs is a schema-major break that should land once, with J.
Also carries the pre-existing editor-track increment: the atomic
tuplet overwrite (CascadeDeleteTuplets prunes decomposition
attachments naming the cascaded tuplet).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
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
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>
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>