97 Commits
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
449dd2ef8e |
Push 3 ratify: extend the slur-rendering requirement + refresh slur_shape
The three slur refinements (dashed rendering, curve splitting, slur_shape measured) lifted into the spec — Chapter 7 / Quality Metric Catalog only, non-canonical, so no wire form and no companion-version move. core_spec Ch7: req:layoutir:slur-curve extended — an authored non-Solid SpanStyle line renders faithfully (the LineStyle rides the Curve, whose listing gains the field); a slur spanning a system break splits into per-system sub-curves (de Casteljau; first segment keeps the slur's provenance, the rest synthesized continuations). Only the curvature-computing algorithm stays forward-referenced out now (dash rendering no longer deferred). quality_metric_catalog: the §slur_shape rationale and the notated-but-unrendered open question refreshed to record slurs now render and are measured; the formula (req:qmc:slur) is unchanged, so no catalog version move. Revision-history row (Push 3); a Push-3 tranche in PASS12_RATIFICATION_LOG.md; layout-ir DECISIONS cross-ref extended. Both specs build clean (latexmk -xelatex, 0 undefined refs). No code change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|
|
|
7d61271069 |
Push 3 (slur quality) 3/3: measure slur_shape_penalty (off the 0.0 placeholder)
The slur_shape quality axis was pinned at 0.0 (vacuous, then "by construction"). It now MEASURES, per the Quality Metric Catalog (req:qmc:slur): each drawn slur Curve's arc ratio ρ = apex height / chord length is penalized by its distance outside the shallow-arc band [0.08, 0.25] (max(0, 0.08-ρ, ρ-0.25)), meaned over curves, normalized by R_worst=0.25. Apex is the max perpendicular distance from the sampled cubic (32 points) to its endpoint chord; translation-invariant, so the post-cast curves are measured directly. Honest outcome: the Minimal tier's mid-span slurs sit at ρ = SLUR_HEIGHT_FACTOR = 0.16 (in band → 0), but the fixed min/max height clamps push short slurs above the band (bulgy) and very long ones below it (flat) — a genuine non-zero value a duration-aware Standard-tier height would improve. A curve-free layout measures 0 by the vacuous-geometry rule. No ENGRAVER_VERSION bump — measurement-only, resolved geometry / canonical bytes / render goldens untouched (quality-decision-8 rule); no RS entry carries a slur so the reference suite is unaffected. Test: an adjacent-event slur is penalized (>0), a wide-span slur is in-band (0). 935 tests, 8/8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|
|
|
586969125e |
Push 3 (slur quality) 2/3: split break-spanning slur curves (de Casteljau)
E2 drew a slur spanning a system break whole in its start system, with a floating end control point detached from its end note (the documented Minimal boundary). Casting now SPLITS such a curve into per-system sub-curves. curve_system → curve_fate, mirroring stroke_fate: a curve that fits in one system rides it whole (CurveFate::Rigid, byte-identical to before); a curve overlapping ≥2 systems' clip intervals splits (CurveFate::Split) into one sub-cubic per system, cut at each system's content clip edges. The cut uses de Casteljau subdivision (sub_cubic = two splits: take [0,t1], then its [t0/t1,1] tail) at the parameters param_at_x finds by bisecting the x-monotonic curve (a slur's control points are x-ascending by construction; a non-monotonic curve — not engraver-produced — falls back to riding its start system whole). The first segment carries the slur's exact provenance (the round-trip surjection recovers the source once); later segments synthesize continuations under SYSTEM_CONTINUATION_SYNTHESIS with a (stable_id, ordinal) key, as split strokes do. Each segment's control hull grows its own system's extent. ENGRAVER_VERSION 6→7 (a break-spanning slur's baked geometry differs); a slur that fits in one system is unchanged, so the fixture goldens are byte-identical (its slurs are short). Tests: sub_cubic reproduces the original curve on its sub-range + param_at_x inverts x; a whole-score slur splits into ≥2 segments across distinct system y-bands with correct provenance. 934 tests, 8/8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|
|
|
c5173d275e |
Push 3 (slur quality) 1/3: render dashed/dotted slurs faithfully
E2 drew every slur solid and surfaced a SlurLineStyleNotRendered diagnostic for a non-Solid authored line. That deferral is now lifted: the Curve primitive gains a `line: LineStyle` (from the slur's SpanStyle.line), threaded through the resolved canonical encode (a per-curve style byte), the engrave remap, and casting; render-svg emits stroke-dasharray (dashed = "0.5 0.35"; dotted = round-capped "0 0.28"). The diagnostic variant is removed — the style is rendered, not deferred, so `req:layoutir:slur-curve`'s "never silently rendered solid" is satisfied by faithful rendering rather than a surfaced gap. ENGRAVER_VERSION 5→6 (a dashed/dotted slur's resolved bytes and SVG differ). Zero churn beyond the one dashed slur: the slur fixture's two solid curves are byte-identical; only the editorial slur's <path> gained the dasharray. Solid slurs and slur-free scores are unchanged. 932 tests, conformance 8/8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|
|
|
693150e43e |
Schema major 2 Phase F: ratify repeat + slur rendering into Chapter 7
The schema-major-2 push's rendering consumers (E1 repeat barlines/volta brackets, E2 slur cubic-béziers) lifted into core spec Chapter 7 — layout-IR only, non-canonical, so no wire form and no companion-version bump (Operation Catalog stays 0.7.0, Binary Format 0.6.0), mirroring the schema-major-1 Phase-F engrave-ratify-only precedent. Three req:layoutir:* requirements added at §ResolvedLayoutIR: - resolved-primitives: ResolvedLayoutIR carries non-glyph line Strokes (staff lines, stems, barlines, volta brackets — present since staff lines, never previously ratified) and cubic-bézier Curves (slurs, E2); each provenance- traced (the hit-test basis), re-spaced like a glyph, non-canonical. The struct listing gains strokes/curves and the Stroke/Curve shapes; the RenderIR provenance requirement widened from "originating ResolvedGlyph" to "ResolvedGlyph, Stroke, or Curve". - repeat-render (E1): a barline-drawing RepeatStructure renders a repeat barline at each resolved boundary, each Volta a bracket with ending numbers; unresolvable boundaries draw no ink (honest placement); jump-kind marks and cross-region repeats deferred. - slur-curve (E2): a Slur renders as a cubic bézier honoring CurvatureOverride; an endpoint not on a single staff of one region draws no curve; a non-Solid line style MUST be surfaced (diagnostic), not silently rendered solid. The curvature-computing algorithm and the RenderIR encoding stay forward- referenced out. Ch5 §Slurs / §Repeat Structures and the Ch7 Forward References gain labels for the cross-references. Revision-history row + a schema-major-2 rendering tranche in PASS12_RATIFICATION_LOG.md; layout-ir DECISIONS carry the Phase-F cross-refs. Spec builds clean (latexmk -xelatex, 0 undefined refs). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
|
|
|
28f210e16f |
E2 follow-up: surface slur style deferral, conservative curve hit capsule
Three source-audit findings on
|
|
|
|
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> |
|
|
|
6651ae5fce |
E1 follow-up: slot-relative glyph remap (same-slot spacing preserved)
The spacing pass reserves a slot's full content extent in its advance, but HorizontalRemap::glyphs moved every glyph independently by piecewise interpolation — a same-slot companion whose absolute x crossed the next slot's source was dragged by the wrong interval. E1 made it reachable: a time signature after a morphed repeatLeft sits TIME_SIG_X + the sign's right extension (~1.8sp) right of its barline, past the 1.6sp constrained column step, collapsing the digits into the following note through the real Engraver. spacing::space_slots now returns each glyph-bearing slot's (source, target) beside the interpolation control points; glyphs translate by their own slot's rigid delta (intra-slot offsets survive verbatim), spanning strokes keep endpoint interpolation, and rigid ledger strokes use the owning glyph's slot delta exactly. Folded into ENGRAVER_VERSION 4 (unreleased this push); scores without same-slot companions are byte-identical — only the repeat fixture's engrave golden moved (volta digits). Regression: time_signature_digits_ride_their_barline_slot_past_a_repeat_sign (unbounded page so x-disjointness compares one line; verified to fail against the interpolated remap). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|
|
|
7a9bf42119 |
Schema major 2 Phase E1: repeat barlines + volta brackets render
The first repeat-structure ink, review-hardened (19 verified findings fixed pre-commit). Zero golden churn for repeat-free scores; four new goldens for the ten_measure_with_repeats fixture. layout-ir: repeats project LayoutContent::Repeat resolved at to_logical (RepeatPlacement::At/RegionEnd/Unresolved — honest, no origin fallback; zero-offsets judged by value; bare wall-clock boundaries draw no ink); deps now from anchor_sites(). Constrained: coinciding measure barlines morph into precomposed repeatLeft/Right/RightLeft (name-only, exact provenance kept); standalone signs synthesize under a semantic (site, staff) instance key; end repeats at the region close draw repeatDots beside a final barline (or the full sign on continuing staves); start marks there draw nothing; end-facing signs reserve their left reach via column overhang; time signatures clear morphed signs; volta brackets = 3 strokes above the top staff + timeSig digit numerals, kind-independent. engrave: ENGRAVER_VERSION 3 -> 4; casting classifies barline columns via is_barline_glyph AND direct Measure source, so standalone signs are never phantom break candidates or measure records; criterion-6 round-trip covers the repeat fixture. render-svg: repeatLeft/Right/RightLeft/Dots outlines + metrics + font subset regenerated from the SHA-pinned Bravura (fontTools 4.63.0); GlyphClass::Repeat. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
|
|
|
58eef41c7e |
Phase D follow-up: anchor-site liveness and containment, all object kinds
User-review findings on
|
|
|
|
9b5339f8f3 |
Schema major 2 Phase D: the repeat-authoring pair (code tranche)
CreateRepeatStructure / DeleteRepeatStructure land end-to-end,
review-hardened (five-dimension workflow review; 19 findings triaged —
12 verifier agents lost to a session limit were verified by hand).
epiphany-ops:
- CreateRepeatStructureOp { repeat } (lp(RepeatStructure), wire 28,
BORN AT V2: schema_major always 2 — kind/voltas are unconditional
fields) and DeleteRepeatStructureOp { repeat } (bare id, wire 29,
major 0: minimal stamping over a major-0 layout; the discriminant a
schema-minor append). OperationKindTag 28/29 name-verbatim with
validating decode; golden tables grown to 30; migrate/v0 identity
arms; lib re-exports.
- Reduction: set-union mint (live-id => AlreadyApplied without value
comparison — the cross-cutting discipline; RecreateContentMismatch
scope unchanged) with the ALL-anchor-sites-live precondition;
delete-wins tombstone; the "Repeat structure / Anchor" rule-table
row wired into BOTH the ledger (reanchor_for_tombstone arm, min-id
survivor, cascade when dry) and the graph (every dead site rewrites
to the same survivor), agreement asserted by tests incl. the
DalSegno jump-target case.
- Undo: materialize_graph_tombstones gains RepeatStructure — and the
pre-existing SPANNER GHOST fix (an undone spanner mint never left
the graph), regression-locked.
- validate.rs: VoltaEndingsIllFormed advisory (non-empty, 1-based,
strictly ascending; interactive-only).
- Canonical-base honesty: blake3 re-pinned for the corpus stream
shift, its comment corrected (the seeded repeat creates all no-op),
and the property covered surgically by
the_canonical_base_embeds_no_repeat_values (two reductions
differing only in repeat v2 content => byte-identical bases).
epiphany-core:
- RepeatStructure::anchor_sites()/anchor_sites_mut(): THE single
site-set walk (start/end, jump targets, volta spans), consumed by
reduction, the editor barrier seam, the invariant walk, and the
cross-reference index — replacing five hand-rolled copies and
fixing a SIXTH, silently-stale one (indexes.rs had never learned
the Phase-B kind/volta anchors; regression-locked).
- RepeatStructure joins the CanonicalValue seam (strict per-value
decode); decode-fuzz corpus gains valid_score_rich_with_repeats
(DalSegno + voltas) CORPUS-LOCAL — shared render fixtures stay
repeat-free until E1 (zero golden churn).
editor-core / gui / testkit / layout-ir:
- Barrier seam: repeat_event_refs + subjects_of arms +
graph_structure_endpoints arm, gated end-to-end by
repeat_authoring_kinds_derive_subjects_and_gate_on_barriers.
- Generators: operation_payload -> below(30); graph-session gen ->
below(14); ops fuzz gen_payload -> below(27); layout_stub
gen_operation_kind_tag unstaled 24..=29 (the Phase-3 tags were
already missing); GUI name table.
Spec touch-ups (review findings): catalog says "strictly ascending";
the survivor tie-break is now explicit in the core rule-table row and
catalog (identifier-order minimum among multiple survivors;
proximity-aware four-key selection deferred, as for spanners).
Trail: ops + core DECISIONS entries; spec/PASS13_CANDIDATES.md opens
the Pass-13 ledger at three candidates (P13-K1; P13-D1 undo-driven
tombstones bypass ledger re-anchoring — pre-existing class; P13-D2
cue-cascade repair-trail ordering).
Full gate: fmt, clippy -D warnings, rustdoc -D warnings, 30 workspace
suites, conformance scale 1 (8/8).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
|
|
|
|
b816144a43 |
Schema major 2 Phase D: ratify the repeat-authoring pair (spec tranche)
CreateRepeatStructure / DeleteRepeatStructure enter the ratified operation set — the dedicated pair for RepeatStructure (repeats live in the cross-cutting registry but are deliberately NOT CrossCuttingValue wire kinds; the reconciliation note follows Ch6's CrossCuttingStructure listing). operation_catalog (0.6.0 -> 0.7.0): - New K0 §"Repeat Structures": the full six-part schema for the pair. Set-union mint with the ALL-anchors-live precondition (start/end, the kind's jump targets, each volta's span — the mint must leave the graph satisfying reference-resolution invariants; dead anchor => TargetMissing no-op); live-id re-create reads AlreadyApplied without value comparison (cross-cutting discipline; RecreateContentMismatch scope unchanged); delete-wins tombstone; create-undo tombstones the mint, delete-undo does not restore (P11-C8); volta well-formedness stays advisory. - K1 gains the "Added in the schema-major-2 revision" entry (net-new primitives, never drafted as slots). binary_format (0.5.0 -> 0.6.0): - OperationKind wire table appends 28 (lp(RepeatStructure)) / 29 (bare identifier); OperationKindTag 28/29, name-verbatim projection; requirement bounds move past 29. - Honest per-op stamping ratified: the CREATE is born at v2 (kind/voltas are unconditional fields — no lower-major payload layout exists, so every block carrying one stamps major 2 under minimal stamping); the DELETE's bare-id payload is a major-0 layout, so minimal stamping gives its blocks major 0 — the kind discriminant itself being a schema-minor vocabulary append (mechanism claim only; the stamp always follows minimal stamping, as discriminant 24's always-2 CreateStaff shows). - §Schema Major 2 restructured: RepeatStructure moves from the snapshot-only bullet to the canonical operation layer (eight embedding payloads now); the CanonicalValue seam grows to twenty-three (RepeatStructure joins — CreateRepeatStructure embeds it per req:catalog:value-encoding). core_spec: - Ch6 OperationKind + Ch8 OperationKindTag listings gain the variants; the CrossCuttingStructure/wire-vocabulary reconciliation note; the re-anchoring rule table gains the "Repeat structure / Anchor" row (re-anchor to the nearest surviving anchor across EVERY event-referencing anchor site; cascade-delete only when none survives — the spanner discipline); Ch8's schema-versioning paragraph now enumerates eight embedding payloads with the born-at-v2/major-0 split; revision-history row. Review-hardened: five-dimension workflow review, 22 findings, 9 confirmed + 1 recovered from a failed verifier (an API-error casualty, assessed by hand) — all fixed (the load-bearing ones: Ch8's stale seven-payload enumeration; the mis-attributed "Phase-3 precedent" gloss that would have taught a false stamping rule; the pair filed under "Snapshot-only"; the CanonicalValue seam omission). All three documents compile clean, zero undefined references. Code tranche follows this commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
437e0758a0 |
Phase B follow-up: real acceleration-snapshot path + base role bound
Review findings on
|
|
|
|
794400c4c7 |
Schema major 2 Phase B: snapshot side + honest stamps (data-model fills)
The nine type bodies fill to the ratified Ch5 shapes and the wire moves
to the Binary Format §Schema Major 2 layouts, review-hardened (high-
effort review; 8 findings, 7 fixed + 1 deferral sharpened).
epiphany-core:
- 19 new leaf types in graph.rs (SlurKind/CurveDirection/
CurvatureOverride/SpanStyle/LineStyle/SpaceUnit/SubBeam/
BeamGeometryOverride/SpannerKind+5 payload types/RepeatKind/Volta/
StaffBracketKind/MetadataEntry/MetadataValue/Timestamp/
SoundConfiguration/TranspositionInterval/UnpitchedMember) with the
ratified discriminants (cstyle_enum_codec! reused; tagged unions
hand-written); nine struct fills appended in wire order.
- The frozen wire forms generalized to a SHARED sub-codec layer
(enc_/dec_*_v1, v0==v1 for every type major 2 changed; vec framing
through enc_/dec_vec_v1) used by the new decode_v1_score/
encode_v1_score AND the rerouted v0 pair — major 2 touched types the
v0 walk had treated as unchanged (metadata, staves, cross_cutting,
staff instances transitively). Strict-canonical guards on every
versioned path; decode_canonical_versioned dispatches {0,1,2} with
composed default-fill migration.
- Invariants extended to the new reference-bearing fields (REVIEW
FIX): Beam.sub_beams events, RepeatKind DaCapo/DalSegno anchors,
Volta spans now covered by CrossCuttingRefsResolve + the anchor
model walk, with negative tests.
- Tests: v1 migration size-anchor (v1 omits exactly the appended
default bytes — the frozen encoder cannot drift), a non-default
round-trip covering every new field and every SpannerKind/RepeatKind
wire arm, fuzzer corpus gains genuine-v1 forms + the major-2 seam
with enforced must-decode-Ok on unmutated frozen forms.
epiphany-ops + epiphany-bundle (Phase C's semantic core, landed here
deliberately — the live codec flip makes CrossCutting/Staff/Metadata
payload bytes v2 immediately; shimming nine transitively-embedded
types the major-1 D1 way was throwaway):
- Minimal-stamping OperationKind::schema_major per the ratified rule
(CrossCutting/CreateStaff/SetMetadata always 2; CreateRegion 2 iff a
carried instance bears Some(staff_lines_override) else 1;
CreateStaffInstance/SetStaffLayout 2 iff Some else 0), unit-locked.
- Bundle op-block accept-set [0,2]; SchemaVersion::V2;
beyond-accept-set tests moved to major 3; testkit V2
stamp-derivation test.
- the_canonical_base_is_byte_identical_across_data_model_majors:
pinned blake3 of a seeded reduction — the companion's SHOULD that
the canonical base never moves across data-model majors.
- The op-payload migrate-on-read deferral restated precisely in
DECISIONS (no consumer byte-reconstructs op payloads today; the
first one must bring per-type frozen payload decoders).
Zero golden churn (fixtures deliberately carry v2 defaults).
Instrument::new consolidates the sweep's default fills. Full gate:
fmt, clippy -D warnings, rustdoc -D warnings, 30 workspace suites,
conformance scale 1 (8/8).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
|
|
|
|
7c9d1c40aa |
Schema major 2 Phase A: ratify the v2 data-model wire form (spec only)
Push 2's spec-first phase. core_spec (schema-major-2 revision): defines
the nineteen leaf types Ch5's ratified shapes referenced but never
pinned (SlurKind, CurveDirection, CurvatureOverride, the shared
SpanStyle record consolidating the identical SlurStyle/TieStyle/
SpannerStyle triplet, SubBeam, BeamGeometryOverride, SpannerKind's five
payload types + a leading Generic migration default, Volta,
MetadataValue, Timestamp, SoundConfiguration, TranspositionInterval
(structural, advisory until Ch4 per the P12-K2 discipline),
UnpitchedMember + member-resolution semantics (first-match, no-match
tolerated; StaffPosition vertical convention explicitly deferred),
SpaceUnit, LineStyle, StaffBracketKind); ratifies the visible-slice
clef/key content model into Ch5 (Clef/ClefShape/KeySignature/
ClefChange/KeySignatureChange) and retires the ClefId sketch;
strictly-authored metadata timestamps (req:graph:metadata-timestamps);
metadata `additional` = ordered list, duplicates permitted; Ch8 gains
the major-2 paragraph; Ch6 OperationKind listing gains its seven
missing variants (staleness fix).
binary_format 0.4.0 -> 0.5.0: new "Schema Major 2" section — per-
payload-type major assignment (snapshot-only Instrument/
RepeatStructure; SEVEN canonical embedding payloads incl. CreateRegion
transitively and SetStaffLayout's direct staff_lines_override field);
the MINIMAL-STAMPING rule (a block stamps the lowest major whose
layouts decode its bytes — deterministic, per-payload table given;
extends the major-1 no-restamp principle); append-after-prior-major
wire layouts with field-order divergences called out; new leaf layouts
with framing pinned (SubBeam EventIds = framed leaves; UnpitchedMember
fields = bare primitives; SpaceUnit = framed 12); total default-fill
v1->v2 migration table (complete StaffLineConfiguration defaults at
all four embed sites; RepeatKind default SimpleRepeat{count:2});
accept-set {0,1,2} with per-role staging note; CanonicalValue seam
list corrected 18 -> 22 (staleness fix).
Two review rounds (8 + 9 findings, all fixed pre-commit — the
stamping-rule determinism contradiction, the CreateRegion transitive
embedding, SubBeam leaf framing, and the SetStaffLayout direct-field
site being the load-bearing ones). Both PDFs build clean, zero
undefined references. No code changes; Phase B (snapshot side) next.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
|
|
|
|
93f3dfde93 |
G-pass follow-up: unsupported pre-pass algorithm ids error
Post-commit review finding: req:pitch:spelling-algorithm / req:time:decomposition-algorithm ratified MUST-error for unregistered algorithm ids, but derive_annotations kept the pre-ratification derive-nothing-under-honest-profile behavior (and a test locked it). The spec text stands; the code moves: derive_annotations returns Result<DerivedAnnotations, PrePassError>, rejecting unregistered ids up front — a silently-empty derivation is indistinguishable from a legitimately empty score, and would silently disagree with an implementation that does support the requested id. All production callers use the default profile (.expect); the stale lock test is rewritten as unknown_algorithm_ids_error; PrePassError re-exported. Full gate green: fmt, clippy -D warnings, rustdoc -D warnings, 30 workspace suites, conformance scale 1 (8/8). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
e4edea6a3e |
Pass 12 G-pass: code tranche (K3/K9/C4/H7) + review fixes
The decided code work behind
|
|
|
|
aff1809421 |
Pass 12 G-pass: ratify the full batch (28 rows) — spec tranche
The G-ratification pass over the accumulated PASS12_BATCH backlog (worklist: spec/PASS12_WORKLIST.md; dispositions: PASS12_RATIFICATION_LOG "G-pass tranche"). Four project-lead decisions: K12 slur permission = AND, H7 authored-uninferred annotations SURFACE, K4 ResolveConflict = no supersede, K8 genesis outside the operation set. Three named deferrals: H2 (narrowed; spelling-v2 + notation refinement), K2 (tuning catalog), K5 (Profile Conformance companion). core_spec: spelling + decomposition move to profile-declared with ratified v1 defaults (req:pitch:spelling-algorithm, req:time:decomposition-algorithm — two open-question boxes closed); authored-uninferred surfacing (req:pitch:authored-uninferred); decomposition precedence pinned FIXED; system-derived content immutability (K3); genesis note (K8); slur AND advisory (K12); re-anchoring table C1/C2/C3 + SameCanvasNearer variant (C4); barrier matching + unsafe-tombstone semantics (req:format:barrier-matching, req:format:unsafe-tombstone); solver kind-strength / sub-conformant-report / Minimal-floor requirements (I4/I5/I6); stale OperationKindTag listing gains the eleven appended tags. operation_catalog 0.5.0 -> 0.6.0: K1 migration fallback long-term; K4 no-supersede; K6 edge semantics (single-pass, quarantine excluded, pending governs); K5 deferral pinned; K8 slots RETIRED; K10 reuse blessed; K11 asymmetry normative; K12 AND; K2 prototype pin; K3/K9 preconditions. binary_format 0.3.0 -> 0.4.0: appended vocab discriminants SystemDerivedContentImmutable(12), RecreateContentMismatch(13), SameCanvasNearer(6); E5 tombstone-encoding open question. All three PDFs rebuilt clean, zero undefined references. Batch rows struck 28 -> 0 (tracker CLOSED); CONFORMANCE.md caveat dropped; DECISIONS cross-refs in core/ops/layout-ir/editor-core (new file). Code tranche (H7 surfacing, K3/K9/C4 discriminants) follows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
7e7a43b050 |
Fuzzer P1: adversarial decode fuzz + strict-canonical decode + bounded counts
Stands up the Binary Format companion's wire-format-fuzzer charter item as a core adversarial byte-decode harness, and lands the three robustness fixes it drove out. The canonical decoders are a trust boundary (a hostile bundle, a bit-rot chunk, a mismatched implementation), so every byte string must decode to a clean Err -- never panic, over-allocate, or loop unboundedly -- and any accepted string must re-encode to itself (canonical decode is injective). - epiphany-core/src/fuzz.rs (new): run_decode_fuzz mutates a once-built corpus (random, substitution, truncation, trailing garbage, length-prefix corruption, wrong-type payload, genuine-v0-form) against Score::decode_canonical, the versioned seam (v1 + the frozen v0 migration), and a per-value decoder; asserts no-panic + injective decode over ~40K inputs/run. Two seeds, plus deterministic prefix-rejection sanity tests. - Strict-canonical decode (the fuzzer's first finding): decode reconstructed via normalizing constructors (RationalTime reduces, BTreeSet/BTreeMap re-sort, a CanonicalF64/ReferencePitch/Tempo normalizes via new), so distinct byte strings could map to one value. Fixed complete-by-construction: Score:: decode_canonical and the CanonicalValue macro re-encode and reject any input not already its canonical form; decode_v0_score does the same against the frozen v0 wire form (encode_v0_score promoted to production), so major-0 snapshots are injective too. - Bounded collection count (Reader::count): reject any count/length exceeding the bytes remaining. A garbage u32 count was a soft-DoS -- decoders looped element-by-element toward EOF (e.g. misparsing v1 bytes as v0), ~100ms per adversarial input; the bound also caps Vec/set allocation and gives a ~2600x fuzz speedup (422s -> 0.16s). Codec round-trips confirm no valid data has zero-byte-element collections, so the bound never rejects a real encoding. Full gate green (workspace tests, clippy -D warnings, fmt, rustdoc -D warnings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
7045a13a18 |
Schema major 1 Phase F: ratify engrave I8/I9/I10 + process trail
Spec/process only, no code. Closes the schema-major-1 track (Phases A-F): the two data-model batch rows parked behind the frozen-layout rule (P12-I7, P12-K7) landed via Phases C/D1, and the three engrave/layout-ir dispositions the implementation already made are ratified into the core spec. - core_spec: three normative requirements ratifying the implemented behavior -- break-constraint satisfaction (req:layoutir:break-satisfaction, I8: a SystemBreakAt/PageBreakAt is satisfied iff the final ResolvedLayoutIR starts a system/page at that slot), break-override attribution via a ConstrainedLayoutIR.break_origins sidecar declining to widen the constraint record (req:layoutir:break-origin-attribution, I9), and system-continuation synthesis Registered(SYSTEM_CONTINUATION_SYNTHESIS) with an (original, ordinal) instance key (req:layoutir:continuation-synthesis, I10). Revision-history row. PDF rebuilt (latexmk -xelatex, 0 undefined refs). - PASS12_RATIFICATION_LOG: schema-major-1 tranche (I7/K7 landed + I8/I9/I10 adopt), with the open cross-region-slur item flagged. - PASS12_BATCH: struck I7/K7/I8/I9/I10; added P12-K12 (which region governs a cross-region slur's spanning permission -- implemented as conservative AND). - engrave/layout-ir DECISIONS: ratification cross-refs; the P12-I7 note reworded so deferred Phase C' does not read as landed. - .gitignore: spec/*.xdv (xelatex intermediate; the tracked PDF is committed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
64de92605f |
Schema major 1 Phase E: resolved-layout length prefixes u64 -> u32
Unifies the resolved-layout's 13 length/count prefixes to u32 (schema major 1), matching the core codec's put_len; the manifest-embedded barrier blobs stay u64 (regime (b), canonical). The resolved layout is an encode-only, non-persisted determinism fingerprint, so there is no migrate-on-read and no bundle LayoutCache machinery (that would be ahead of a producer) — a cross-major layout cache is regenerated, never decoded. - resolved.rs: push_u64 length helper -> push_len (u32 LE, debug_assert n <= u32::MAX). Data fields (rgba, layer, page.number, smufl_version) untouched. - Byte-shape lock: count_prefixes_are_u32_width_locked asserts an empty layout's four counts occupy 4x4 bytes after the 32-byte ScoreVersion (catalog length recomputed independently), so a revert to u64 fails (verified: 128 vs 112). Zero golden churn (every existing resolved test is self-comparison). Full gate green (workspace tests, clippy -D warnings, fmt, rustdoc -D warnings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
4598f30ddd |
Schema major 1 Phase D2: CreateRegion op-block major + cross-major read-only
The canonical op-block side of Region.permits_spanning_slurs: CreateRegion now encodes at schema major 1, blocks carrying one are stamped major 1, the reader admits them per-role, and a bundle whose op history is beyond this reader's accept-set opens read-only. The migrate-on-read primitive is deferred (op payloads are never reconstructed-to-values from bytes, so it has no consumer). - ops: CreateRegionOp::encode_canonical embeds the region's full (v1) canonical bytes; OperationKind/OperationPayload/OperationEnvelope::schema_major report the payload's binary-format major (CreateRegion => 1, else 0). Removed D1's transitional Region::canonical_bytes_v0 (dec_region_v0 stays for snapshots). - bundle: max_supported_major(kind) raises the OperationEnvelopeBlock role to [0,1] (every other role stays exact-0); the read gate is now major > max_supported_major(r.kind). StagedChunk::operation_block_versioned + SchemaVersion::for_major project a derived block major to a version. - bundle: commit-time canonical-root validation checks structure without the accept-set (a newer writer's higher-major root is publishable); the accept-set is a read concern. Both open and commit consult unsupported_operation_root_major and go read-only (+ the new IntegrityAnomaly::UnsupportedCanonicalChunkMajor) when a canonical op root exceeds the accept-set, so the live bundle refuses further commits at once. - testkit: stage_operation_block derives a block's schema version from its operations (max schema_major); staged_envelope_blocks routes through it so a generated CreateRegion stream is never mis-stamped v0. Tests: CreateRegion payload is v1 and carries the flag; the op reports major 1; a derived CreateRegion block stamps V1 and reopens read-write; a major-2 block opens read-only (open and post-commit); the per-role accept-set shape. Full gate green (workspace tests, clippy -D warnings, fmt, rustdoc -D warnings). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
03758bbae0 |
Schema major 1 Phase D1: Instrument.range + Region flag (snapshot side)
The full-Score snapshot side of the three schema-major-1 data-model fields,
on a struct-level frozen v0 decoder. Snapshot-only: the CreateRegion op
payload stays byte-v0 (D2 owns the op-block migration + read-only).
Data model (epiphany-core):
- PitchRange { lowest, highest: Pitch } in pitch.rs; contains() is frame-guarded
(decides only within a shared PitchSpaceId) and treats a reversed range as
undecidable (None), not "everything out of range".
- Instrument.range: Option<PitchRange>; Region.permits_spanning_slurs: bool.
Codec:
- struct_codec! for PitchRange; Instrument -> {id,name,range}; Region appends
permits_spanning_slurs.
- Replaced the Phase-C byte-splice with a struct-level decode_v0_score: a
hand-written 19-field Score walk using dec_canvas_v0/dec_region_v0/
dec_instruments_v0 for the two changed fields (nested in Vecs) and the current
Codec for the other 17. Removed the now-unused Reader::pos().
- Region.canonical_bytes_v0() (+ enc_region_v0) is the frozen v0 op-payload
surface: CreateRegionOp embeds it so the op-envelope block stays byte-v0.
Advisory preconditions (epiphany-ops/validate.rs):
- PitchOutsideInstrumentRange: pitch-in-range via voice->instance->staff->
instrument (honoring instrument_override); "if any"/indeterminate-frame pass.
- Slur-spanning suppressed only when BOTH endpoint regions permit (AND
semantics; documented pending spec ratification of which region governs).
Tests: the three frozen-decoder fixtures (non-default v1 round-trip; a mirror
v0 encoder synthesizing genuine v0 bytes that migrate default-filling all three
fields, anchored by an independent byte-length check; the nested-Vec multi-
region case); the byte-v0 CreateRegion payload; the frame-aware/reversed-range
contains(); and 5 advisory tests. ~29 construction sites updated. Full gate
green (workspace tests, clippy -D warnings, fmt, rustdoc -D warnings).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
|
|
|
|
e199fabe3f |
Schema major 1 Phase C: Canvas.layout_defaults + real migrate-on-read
The first real data-model field of the schema-major-1 bump, and the point where the dispatch seam flips from the Phase-B identity to a genuine migration. Zero golden churn: CanvasLayoutDefaults::default() equals the engraver's existing A4/8mm default, so no resolved geometry moves. - Data model (core/graph.rs): Canvas gains `layout_defaults: CanvasLayoutDefaults`, with new core geometry primitives CanvasSize / CanvasMargins (staff-space CanonicalF64, A4/8mm Default -- core has no geometry types of its own and must not depend on layout-ir). Exported from the crate root; ~7 Canvas construction sites updated with `..Default::default()`. - Codec (core/codec.rs): struct_codec! for the three new types; Canvas v1 encodes `regions` then `layout_defaults`. - The frozen migrate-on-read (decode_v0_score): a byte-level splice. v0 Score bytes are the v1 layout minus Canvas.layout_defaults; Canvas is Score field 2 and its v0 layout was just `regions`. Read the v0 prefix (metadata, canvas.regions) to find the split, insert the default CanvasLayoutDefaults encoding, then decode the resulting v1 bytes. Total and default-filling (no score context), frozen by value. A `pos()` accessor was added to Reader for the splice. - Test (v0_score_migrates_by_default_filling_layout_defaults): derives REAL v0 bytes by stripping the field from a v1 encoding, then migrates them back and checks the original score is reconstructed with the default refilled. A wrong splice offset corrupts the bytes and fails the decode, so it guards the frozen v0 assumptions. Deferred: routing the migrate through a bundle acceleration_snapshots slot (a Phase-B Finding-3 item). The migrate logic is proven by the core unit test and the bundle read path by the canonical-base roundtrip; combining them hits cross-crate friction (constructing v0 bytes needs core-internal Reader, which testkit can't reach and core-below-bundle can't stage) for marginal coverage. The forward scenario (v1 reader migrating a v0 bundle's major-0 acceleration snapshot) needs no new bundle code -- the v0 snapshot passes the exact gate. 863 workspace tests pass; clippy -D warnings, fmt --check, rustdoc -D warnings all clean; no render/snapshot goldens changed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
9f688cc592 |
Schema major 1 Phase B: the dispatch seam + version infrastructure (no-op)
Stands up the schema-evolution machinery as a tested behavioral no-op, the load-bearing one-way-door piece the later phases build on. - SchemaVersion::V1 (bundle/ids.rs) -- infrastructure, an identity today. - The core dispatch seam Score::decode_canonical_versioned(bytes, major) (core/codec.rs), with the decode_v0_score / migrate_v0_score scaffold. It is the identity at major 1's introduction (v0 layout == v1 layout), with explicit "Phase C/D freeze this by value + default-fill the new field" contracts baked into the doc comments so the freeze is a clean edit later. Unit-tested by versioned_decode_is_identity_across_majors. A first-pass review caught that the initial gate widening over-reached: it admitted major 1 for every chunk kind, but the bundle's own op-block decoder (block::decode_block) and manifest decoder are unversioned, so a spec-valid major-1 op block would have passed the gate and then been mis-read rather than migrated / opened read-only. The accept-set ran ahead of the decoders. Corrected: the gates stay EXACT to major 0 in this phase -- the manifest gate to Manifest::SCHEMA.major (the manifest never grows a v1 layout in this bump), the generic-chunk gate to SUPPORTED_SCHEMA_MAJOR = 0. Admission of major 1 is raised PER CHUNK ROLE by the phase that adds that role's versioned decode or discard path (snapshot -> C, op block -> D, layout cache -> E), never as a blanket accept-set ahead of a decoder that can read it. The roundtrip seam-exercise was reverted too (it conflated the canonical-base MaterializedState role with the acceleration-snapshot Score role); the acceleration-snapshot read path + the first usable_* wrapper land in Phase C. So Phase B is version infrastructure + the dispatch seam only; the gate widening, usable_* wrappers, and ops symmetry move to the phases that exercise them. 863 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 |
|
|
|
f4a2f1fdf0 |
Schema major 1 Phase A: ratify the v1 wire form + migration (spec only)
The first phase of the first-ever binary-format schema-major bump (v0 -> v1),
the machinery-first minimal major. Spec-only: ratifies the contract that
Phases B-F build against; no code changes.
core_spec.tex: defines the two referenced-but-undefined types that were the
P12-I7 / P12-K7 gaps -- CanvasLayoutDefaults (with core geometry primitives
CanvasSize/CanvasMargins in staff spaces via CanonicalF64, A4/8mm default,
since core has no geometry types and must not depend on layout-ir) and
PitchRange (advisory pitch compass used by Instrument.range and
IndeterminacyHints) -- and adds Region.permits_spanning_slurs (default false).
Records schema major 1 as the first data-model expansion major and tightens
the minor-version rule (a field add, even Option, is major; minor = append
discriminants to the companion's append-safe vocabularies only).
binary_format.tex -> 0.3.0: the full "Schema Major 1" section --
- Where the changed fields reach: Canvas.layout_defaults and
Instrument.range are snapshot-only (no CreateCanvas/CreateInstrument op),
but Region.permits_spanning_slurs also reaches the CANONICAL CreateRegion
operation payload (CreateRegion embeds the full Region). The canonical-base
MaterializedState embeds none of these and stays major 0, byte-identical.
- Cross-major reader rules: discard-and-regenerate non-canonical chunks;
parse-or-read-only for canonical ones, so a major-0 reader opens a bundle
carrying v1 CreateRegion ops read-only.
- Accept-set gate [min,max] (rejects majors outside the set); per-payload-
type major assignment; the changed v1 value layouts (the wire form ratifies
the reduced reference-code layout, not the fuller data model); the total
default-filling v0->v1 migration table (including the CreateRegion payload).
- Length-prefix unification NARROWED to the resolved-layout (its own
non-canonical LayoutCache): the barrier/extension blobs stay regime (b) u64
because they ride the canonical manifest, which stays major 0.
Two review passes hardened this checkpoint. The first caught that Region is a
canonical operation payload (not cache-only, as the architecture analysis had
assumed) -- user chose to embrace it and build the canonical op-payload
migration. It also surfaced the barrier-blobs-in-manifest constraint that
narrows the unification. The second refined the minor-version delegation, the
accept-set outside-[min,max] semantics, and stale "no defined type" text in
the reference-suite / quality-metric companions and the engrave DECISIONS.
P12-I7 moved to IN PROGRESS (spec type defined here; code graph home lands in
Phase C). Both companions and the engrave DECISIONS reworded accordingly. All
four affected PDFs rebuild clean (0 undefined references).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
|
|
|
|
9d28dd6615 |
Clear P12-I12: scope spacing_distortion to rhythmic columns (QMC 0.2.0)
The Standard-tier spacing floor warned on short healthy scores: three reference-suite entries measured spacing_distortion 0.36-0.41, above the 0.32 warning floor (0.8 x 0.40), a spurious diagnostic (every score still passed Minimal). Root cause: the CV folded in the clef-to-first-note lead advance, which is sized by notational-furniture width, not by rhythm. Unlike P12-I11 (an engrave-side fix, no spec change), the defect here lived in the metric's own normative definition, so the honest fix is a catalog change - the mirror of I11: correct the measurement rather than relax the threshold. Quality Metric Catalog 0.1.0 -> 0.2.0: spacing_distortion is scoped to the system's rhythmic columns (spring slots bearing a notehead or rest). The clef / key-signature / time-signature lead and barlines contribute no column, so a note-to-note advance spans them. quality::census now builds its spacing columns only from a precomputed rhythmic-slot set (is_rhythmic: a notehead*/rest* glyph anywhere in the slot); the CV and the >= 3-column contributing-unit rule are otherwise unchanged. Results: RS-3/5/6 drop to 0.2188 / 0.0819 / 0.0856 (all below the floor), and the axis stays honest on real irregularity (RS-3 keeps 0.2188 from a mid-line accidental). RS-2/RS-4 go vacuous-0.0 (their systems carry < 3 rhythmic columns - honestly "too little to measure," cleaner than the old furniture noise). RS-1 0.1341 -> 0.1967 (cross-barline note advances). The 1.0 anchor, orientation, range, tier thresholds, and the eight other axes are unchanged. This is measurement-only: the resolved layout, canonical bytes, render goldens, and ENGRAVER_VERSION are all untouched - only the reported spacing_distortion value moves. The duration-aware optical-spacing open question (deviation from duration-proportional spacing) stays open; it needs the pipeline's deferred duration-aware preferred widths. - catalog: spacing_distortion requirement (req:qmc:spacing) redefined over rhythmic columns, rationale + open-question note updated, version 0.2.0, revision-history row; PDF rebuilt clean (0 undefined refs). - engrave: quality::census rhythmic-column filter + is_rhythmic; module and spacing_raw docs; the floor-column contrast test re-pointed from b-flat's spacing (no longer warns) to RS-1's casting-off (still between the Standard 0.28 and Minimal 0.72 floors); new short_scores_do_not_trip_the_standard_ spacing_floor locks the fix. - QMC version breadcrumbs bumped to 0.2.0 (engrave + layout-ir quality.rs, both DECISIONS.md, testkit RS-1 comment); reference suite companion unchanged (cites the catalog by name, no pinned values). - process trail: PASS12_BATCH I12 struck; PASS12_RATIFICATION_LOG I12 section (Version movements: QMC 0.1.0 -> 0.2.0); engrave DECISIONS quality decision 8 + item 7 + candidate. 861 workspace tests pass; clippy -D warnings, fmt --check, rustdoc -D warnings, and the catalog PDF build all clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
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
|
|
|
|
b9a9921d50 |
Phase 3 tranche 2: quality conformance companions, real metrics, multi-system click fix
857 workspace tests pass; clippy -D warnings, fmt, and rustdoc clean; both new companions build with zero undefined references. Quality Metric Catalog v0.1.0 (spec/quality_metric_catalog.tex, new): - Formal definitions for all nine normative quality metrics, each with a raw measurement over resolved geometry and a clamped-linear normalization n = min(1, raw/R_worst) with pinned anchors. - The vacuous-geometry rule (a metric over absent geometry evaluates to 0.0; the notated-but-unrendered honesty edge is an open question), all-1.0 default tie-breaking weights, and the per-tier threshold table — Minimal's uniform 0.90 deliberately fails the all-worst placeholder, forcing real measurement. - Pins QualityMetricKind (referenced but never defined by the core spec) and the registered SolverProfile catalog (Draft selects the Minimal threshold column; Standard/Publication select Standard). - QualityFloorApproached fires at 0.8x the applicable threshold and is status-neutral by requirement. Reference Suite v0.1.0 (spec/reference_suite.tex, new): - Six entries referenced by deterministic builder + seed (RS-1 ten_measure_single_staff, RS-2 valid_score_rich, RS-3..6 corpus fixtures), each with the declared A4-at-8mm-staff solve geometry (Canvas.layout_defaults has no graph home yet, P12-I7). - All entries required at Minimal; the same set is the pre-declared Standard bar (no implementation claims Standard yet). Fixed- expectation tests deliberately unused in v0.1. Real metrics in the engraver (engrave/src/quality.rs, new; layout-ir/src/quality.rs = the catalog constants transcribed): - QualityMetricVector::unmeasured() replaced with computed values: collision sweep with the catalog's same-slot-cluster and stroke exclusions, per-system spacing CV, vertical gap deviations, system-break slack, page fill, casting-off width CV, symbol density; slur/beam vacuously 0.0 (no drawn geometry exists). - Bit-identical across repeated solves (tested); floor warnings never change solve status; malformed inputs keep unmeasured(). The two all-worst test pins now assert real values; the StubSolver's unmeasured() stays (Stub genuinely computes nothing). Reference-suite harness (testkit reference_suite module + tests): - Each RS entry asserts the four-condition Minimal pass (hard constraints, byte/bit determinism, well-formed Minimal report, every axis within threshold) under the F1 Pass/Xfail discipline, with the measured table printed per run. - HONEST FINDING, day one: RS-1 fails Minimal casting-off (measured 1.0 vs 0.90) — greedy first-fit leaves a two-measure stub last system (width CV 0.6145). Encoded as an asserted Xfail row (fails on XPASS) and filed as P12-I11 (engrave balance pass, or catalog revision). P12-I12: the Standard spacing floor warns on short scores with wide lead measures. Multi-system click-to-insert fix (editor-core): - Casting-off exposed two inversion breaks: position_anchors fed a non-monotonic cross-system anchor list into a monotonic inverter (system-2 clicks resolved to system-1 times), and nearest_manifestation found only system 1's staff-line segment (system-2 clicks got system-1 pitch geometry). - Fixed with a containing-system lookup over the resolved pages tree (containment, else nearest by vertical distance), per-system staff resolution, and per-system anchor filtering; degenerate-geometry fallback preserves the flat path, so all 84 pre-existing editor-core tests pass unmodified. - Five regression tests through the real Engraver over the wrapped ten-measure fixture, each shown to fail without the fix; testkit gains dev-only dependencies on editor-core and engrave. Trackers: P12-I11/I12 filed; DECISIONS entries in engrave, layout-ir, and testkit; Phase-3 memory updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
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 |
|
|
|
3e91a8302a |
Push 4: Binary Format companion, F1 benches, subquadratic reduction order
The audit's fourth push: the biggest outstanding Phase-2 item plus the performance gate. 793 workspace tests pass; clippy -D warnings, fmt, and rustdoc (deny-warnings) clean; all three spec documents build with zero undefined references. Binary Format companion (spec/binary_format.tex, v0.1.0 — Agent J's deliverable, 43 pages): - Twelve chapters transcribed from the golden-locked implementation: encoding conventions (the three prefix/endianness regimes, a normative no-varint rule, reject-never-normalize decode discipline), identifiers imported from the core spec's Canonical Byte-Layout Reference, primitive value encodings, the whole-Score positional codec ratified as the schema-major-0 wire form, operation wire forms (envelope field order with the normative id-leads property, the OperationPayload 0..=3 and OperationKind 0..=23 tables, effects/conflict/anomaly/MaterializedState vocabulary), the bundle physical layout (64-byte header, 256-byte superblock, chunk preimages and framing, ChunkRef, manifest body order), the operation-index payload, and the extension-blob/edit-barrier byte forms. - Ratifies P12-D1 (req:binfmt:opindex), P12-E1 (req:binfmt:ext-blobs), P12-E2 (req:binfmt:condition-depth, MAX_CONDITION_DEPTH = 64 normative), and P12-E3 (req:binfmt:object-kind-open) — batch rows struck through; discharges the provisional-codec notes in core (P11-4), ops, and bundle (P11-D2/D4/D5) DECISIONS with ratification cross-references. - Pins the frozen-layout schema-evolution keystone: within schema major 0 every positional struct layout is frozen; a field-set change is a schema-major change with migration — formally grounding the data-model-expansion staging decision. Open questions kept honest in-document: SnapshotId derivation, index-refresh threshold, u64/u32 prefix unification at the next major. - Not yet delivered from J's charter: the cross-implementation decoder test and the wire-format fuzzer (follow-up harnesses). F1 benches (crates/epiphany-testkit/benches/, per the F0 decision): - criterion 0.5.1 (workspace dev-dependency; MSRV 1.77 respected with documented transitive pins: clap 4.5.53, half 2.4.1). - reduction bench at 1K/10K/50K envelopes with the Chapter-10 budget (>10,000 envelopes/second cold) written in the bench as a Pass/Xfail gate; bundle benches for the typical-edit commit (<=50 ms; measured ~14.7 ms on real disk after catching that tmpfs neuters fsync) and the open/bootstrap read (<=200 ms; measured ~60 us). - CI: quick budget gates in the conformance job, full gates nightly. Subquadratic canonical_reduction_order (the F-surfaces/K-fixes handshake, closing K's 10K-envelope acceptance gate): - The bench documented the failure (50K at ~1.7K env/s, a 29 s cold reduction; two O(n^2) loops); the fix replaces pair enumeration with threshold/frontier readiness per replica plus explicit-dot dependent lists and a stamp-tuple binary heap — O((n + sum(context)) log n), never materializing covered pairs. - Byte-identical order: same edge relation, same ready predicate, same total order; the old implementation is retained as a test-only oracle with element-for-element order-equality property tests over fuzz sets, adversarial sets, and directed shapes (2,000-envelope full-coverage chains, dot cycles, duplicate-id stamp ties), mutation-tested for sensitivity. - Measured: 1K 155K->674K env/s, 10K 12.5K->257K, 50K 1.7K->87K; all three scale points now pass and the 50K row is promoted from Xfail. Also: fixed nine rustdoc private/unresolved intra-doc links that had accumulated across the pushes (the CI deny-doc-warnings job would have failed on them). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
92aaccf7e2 |
Pushes 1+3: fix the MUST-level violations, wire the types-only machinery
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
|
|
|
|
80e3fed99c |
spec: Pass 12 tranche 1 — align spec text with ratified dispositions
The 2026-07 compliance audit's spec-side batch: every place where normative text trailed a disposition the project had already made (the ratified Operation Catalog, a crate DECISIONS entry, or the PHASE2_QUICKSTART model the implementation follows). No byte-layout or architecture changes. - Re-anchoring table: the Slur/Spanner rows now state the catalog's surviving-endpoint semantics (re-anchor while >=1 endpoint survives, cascade-delete only when none does). - RewriteTuplets adopts the catalog's id-only v1 payload (TupletRewrite removed); graph-aware reduction MUST refuse the variant rather than fabricate rewritten values. - Operation-envelope blocks are pure envelope vectors (a content- addressed chunk cannot embed its own id); per-block summaries (dvv_summary/min_stamp/max_stamp) relocated to the manifest's operation_block_summaries map; the order-blocks-by-min_stamp SHOULD dropped (the canonical encoding sorts chunk refs by encoded form). - Ch2+Ch3 pre-passes respecified as canonical derived annotations — deterministic functions of (graph, profile, versioned algorithm id), recomputed on materialization, never stored; the incremental-re-run MUST demoted to MAY with unobservability required. - Spelling precedence ties break by canonical attachment order (the creation-timestamp tie-break deleted; attachments carry none). - ConflictRegistry moved off the Score root into canonical materialized state, matching Chapter 6. - SolverTier gains the non-conformance Stub variant. - The drafted ModifyEvent M2e text landed in the catalog (0.2.0 -> 0.3.0): metric placement changes materialize behind a voice-occupancy placement precondition. Dispositions logged in spec/PASS12_RATIFICATION_LOG.md; the Pass 12 batch itself stays open (no P12-H/K rows resolved). Also carries the audit-filed batch rows P12-H6/H7/K3/K4. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd |
|
|
|
7f8d1a1f95 |
editor-core: a meter-derived default grid for click-to-insert
default_grid_at(point) gives the GridResolution the pencil should snap to, derived from the meter of the region under the cursor instead of a fixed quarter. region_default_grid resolves the governing time signature the way the prepass's resolve_measure_units does (a region measure's, else the score's first), and time_signature_beat reads its beat — 1/denominator from the display (4/4 -> quarter, 6/8 -> eighth, 2/2 -> half) — defaulting to a quarter when the meter has no single denominator or none is declared. None only when the click resolves to no staff. This is additive: position_at / insert_note_at still take an explicit GridResolution, so their signatures and tests are untouched. The GUI's pencil click (and the empty-staff preview) now pick the grid via default_grid_at(world).unwrap_or_else(GridResolution::quarter). Tests: the beat is 1/denominator (4/4, 2/2; mixed-denominator -> none); a declared 6/8 yields an eighth-note grid; a meter-less score defaults to a quarter and a non-finite click yields no grid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
1496c81302 |
editor-gui: undo/redo buttons and shortcuts
Wires the session's undo/redo into the GUI: ↶ Undo / ↷ Redo toolbar buttons, add_enabled-gated by can_undo()/can_redo() so they grey out when there is nothing to do, and Ctrl/Cmd+Z (undo) / Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y (redo) shortcuts read with modifier checks (first in handle_keys, so a modified Z is not also taken as a plain key). A run_history helper handles the Option-returning steps, reporting "nothing to undo/redo" when the stack is empty. These run in the toolbar/key path, before the frame's rerender slot, so there is no stale-texture concern. Module doc, help text, and the crate description are updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
285d7bfd95 |
editor-core: undo/redo by re-reducing the op-log prefix
The CRDT is delete-wins (a tombstone is permanent), so undo cannot invert an edit — it re-reduces the log without it. The materialized score is always reduce(applied onto base); undo drops the last unit (a primitive, or a whole transaction) so even a delete is undone — its tombstone is simply never produced. One user action is one unit; a transaction's descriptor and members undo together. redo re-appends the unit; a new edit forks history (clears the redo stack). undo/redo/can_undo/can_redo on EditorSession; a shared materialize/install path keeps the score, render, hit-test, and clefs in step. Undo must not let ids be reused (a streamed op would equivocate; an entity id would collide), so the session keeps a permanent append-only `authored` log beside the active `applied` prefix. New op-id counters come from authored.len() (monotonic), and event/pitch/transaction minting scans authored — so a unit's ids stay reserved after it is undone or forked away. The causal context is derived from the active head (active_prior_context + extend_context): it covers the whole active prefix, compact while contiguous and dot-based after a fork, so it never asserts coverage of a removed counter (which would strand a later op pending). with_identity now guards on authored history, and applied_operations is documented as the active prefix with authored_operations the full record. authored history is a local high-water source, not streaming-consistent undo. Tests: undo/redo a transpose; undo restores a deleted note; undo a split insert and a make-room overwrite as one unit; undo an override-aware move (value + spelling); a new edit forks history; a fork mints fresh op and entity ids (not the undone ones); edits after a fork re-reduce cleanly; with_identity is refused after an undone edit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
872ff03dd5 |
editor-gui: a duration palette over set_selection_duration
A toolbar palette — 1 / 1/2 / 1/4 / 1/8 / 1/16 — sets the selected note or rest's written value via set_selection_duration(NoteValue::whole_note_fraction), with the editor's make-room overwrite when lengthening; refusals (tuplet member, decomposed event, non-note selection) surface in the status line. The palette lives in the toolbar, which runs before the frame's rerender slot, so it has no stale-texture concern. Adds epiphany-core as a direct dependency for NoteValue (already a transitive workspace member, so no MSRV/CI impact). The module doc and crate description are updated; only an undo UI remains unbuilt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
0f298508af |
editor-core: a set_selection_duration intent with make-room overwrite
The duration-palette gesture: resize the selected note/rest. Shrinking frees the space after it; lengthening makes room over the events it grows into (trim, delete, or split), atomically with the resize — the same overwrite policy as insert. Built on phase 3's metric placement materialization. Extracts the make-room logic shared with insert_note_at: make_room classifies a voice's events overlapping a span (excluding the event being inserted/resized) into deletes/trims/split-tails; make_room_ops turns that into ops (cloning a split tail's shape with fresh ids, carrying authored spellings); a Minter struct replaces the inline id closures. insert_note_at now calls these (its 7 tests confirm behaviour is preserved). Refuses cleanly: InvalidDuration (non-positive), WrongSelection (nothing apt / non-metric / not a note or rest — replace_span only resizes Pitched/Rest), OverlapsTuplet (a tuplet member, whose duration is ratio-governed), and DecomposedEvent — a resize/trim/split would change the duration of an event with a persistent decomposition attachment, leaving its notated components no longer summing to it (invariant 15); a full-cover delete is allowed, since the tombstoned target's decomposition is no longer checked. make_room also skips non-positive existing spans, matching the reducer's overlap rule. Tests: shrink, lengthen-into-empty, lengthen-deletes-a-covered-note, lengthen-trims-a-partial-overlap, refuse non-positive/tuplet, refuse a decomposed event, refuse a non-note/rest event, requires a selection. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
7387eeafc5 |
editor-core: resolved-spelling-aware chord stacking
add_note_to_selection no longer refuses when a note carries an authored spelling override. It ranks the chord's notes by their *resolved* staff position — the spelling the layout draws, from derive_annotations (the same prepass to_logical uses), so an authored override or an inferred respelling is honored — and steps one staff step above the rendered-highest note. The new note's raw position lands at rendered_top + 1: stepping the top's pitch by rendered_top + 1 - raw_top carries its alteration and places it there (for an un-respelled note this is the old +1 behavior); the acoustic realization still resets to Implicit. A note with no resolved CMN spelling falls back to its raw pitch position. rendered_top_of_event replaces highest_pitch_in_event; the event-level override refusal, the has_authored_spelling_override helper, and the now-unraised EditorError::PitchSpellingOverridden variant are removed; note_above becomes note_stepped(_, 1). The refusal test is replaced by one that pins a note three staff steps above its raw position and asserts the add stacks at rendered + 1 (raw + 4), not raw + 1 — verified to fail under raw-position ranking. This closes the last override-refusal in the editing track. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
ba84df67fb |
editor-gui: a pencil mode that clicks notes onto the staff
Wires the click-to-insert into the GUI: a pencil ("insert") mode, toggled by a
toolbar button or the P key, turns a staff click into insert_note_at on a
default quarter-note grid (make-room overwrite), instead of selecting. A
non-pencil click still selects, and its empty-staff status now reports what a
pencil click would insert (pitch + grid-snapped beat).
The pencil edit runs in score_view, after the frame's rerender slot, so it
requests a repaint and returns before the selection overlay would paint the new
hit map over the not-yet-rerendered texture. Non-pencil selection keeps the
immediate overlay path (it does not mutate the rendered score).
This completes the beat-grid click-to-insert feature (vertical inverse →
horizontal inverse → trim/move reducer semantics → insert_note_at → this GUI
wiring). The GUI's rendering/interaction is still not visually verifiable in
this environment; only the ViewMap coordinate logic is unit-tested.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS
|
|
|
|
ce37de8834 |
editor-core: an insert_note_at click→insert with make-room overwrite
The pencil: a click-to-insert that composes the two inverses — staff_pitch_at (the natural pitch at the height) and position_at (the grid-snapped onset) — into a fresh CMN note (duration = grid step) in the staff's primary voice, and makes room under the overwrite policy as one atomic transaction. Make-room over the overlapping notes/rests of the voice: a fully-covered event is deleted; one the new note partially overlaps is trimmed (a phase-3 ModifyEvent placement change); one the new note lands inside is split — the original is trimmed to its head and its tail re-inserted. A split tail is a clone of the original event's whole shape (a rest's visibility, a note's articulations/dynamics/stem/grace) with fresh event and pitch ids, and any authored spelling is carried onto the fresh pitches via RespellPitch (an inferred spelling re-derives) — the same atomic copy insert_note_after_selection does. The several ids minted in one intent advance local checked counters, since the session high-water mark does not move until commit. Refuses cleanly rather than corrupt: an overlapped tuplet member raises OverlapsTuplet (ratio compensation is out of scope); an off-staff or non-metric click, or a non-note/rest overlap, raises NoInsertTarget. Tests (against valid_score's tuplet-free metric region; the rich fixture's only metric region is a triplet): empty-space insert, full-cover overwrite, the split structure, the split tail's shape fidelity and authored-spelling carry, the tuplet refusal, and the non-metric refusal. A click_for_position helper inverts position_at so a test targets an exact onset. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
b3403c1d6a |
ops: ModifyEvent materializes metric placement changes (trim/move)
The make-room enabler: a ModifyEvent that moves a metric event's span (different Musical position or duration) is now applied to the graph and the owning voice re-sorted by ascending position (id-tiebroken, the order an insert maintains), so invariant 3 (VoiceEventsSortedNonOverlap) holds. The deferral documented in DECISIONS.md is lifted. modify_event computes one sanction — the move is a valid metric move (PlacementVerdict::Moved) and the replacement is well-formed — and uses it to gate both the graph mutation (graph_replace_event) and the voice_occupancy update, so the canonical index and the graph never diverge. The verdict is read from voice_occupancy, the graph-independent placement index, so reduce() and reduce_onto() agree on it; a move with a non-positive span or one that would overlap a live sibling is refused as a clean NoOp(EventDurationInvalid) rather than skipped silently. A non-metric move stays deferred, and a malformed (empty) pitched replacement is neither materialized nor allowed to move occupancy. Tests: trim frees the voice slot (a later insert fits); a move onto a sibling is refused; a trim materializes in the graph (reduce_onto, invariants hold); a non-metric event is not rewritten onto the musical grid; a malformed move does not free the slot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
1ab3e5f298 |
editor-core: a position_at click→beat-grid horizontal inverse
The horizontal half of click-to-insert: resolve a world point to the metric region under the cursor and the grid-snapped musical position to insert at. Together with staff_pitch_at it gives the (pitch, position) an insert needs. The region and staff are picked through a shared nearest_manifestation helper (extracted from staff_pitch_at, the non-finite guard moved into it), so the vertical and horizontal halves agree on which staff the click is over. The x→time inverse is solver-agnostic: position_anchors reads resolved glyph x-positions and maps each to its event's onset through the glyph's Pitch/Event provenance, so the samples come from the actual rendered layout. Only directly -manifested onset glyphs anchor — synthesized glyphs (accidentals sit left of the notehead, ledgers, …) are skipped so they cannot pull the time column off the notehead. invert_x is piecewise-linear through those anchors, extrapolating the end segment past the last note — the make-room case of clicking the empty staff after the last note. snap_to_grid rounds to the nearest multiple of the grid step, rebuilt by exact rational arithmetic so it lands on the grid. GridResolution is the caller-supplied beat grid (a musical-time step, also the default duration of a note entered there); a meter-derived default is deferred. position_at refuses a non-positive grid, a non-metric region (no musical onset), and a region with fewer than two rendered events (no scale to extrapolate from). The GUI shows the resolved pitch and snapped position on an empty-staff click, on a default quarter-note grid. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
8bffd2f2fd |
editor-core: a staff_pitch_at click→pitch vertical inverse
The vertical half of click-to-insert: resolve a world point to the staff under the cursor and the natural diatonic pitch at that height under the staff's clef. Staff selection is manifestation-aware. Each rendered bottom staff line carries its staff's manifestation id as the stroke's stable_id, so a click maps back to a concrete (region, staff_instance) by matching that id, then chooses the nearest staff by 2D proximity — horizontal span first (which region, since one staff tiles across regions that can share a y band), then the vertical band. The earlier StaffId-collapsing helpers are gone. The clef is resolved by time, not vector order: render_score builds a per-staff start-clef table from the logical layout via active_clef (now exported from epiphany-layout-ir) and threads it through open/commit, so a mid-vector [bass@1, treble@0] still spells the staff start as treble. A non-finite click is rejected up front — NaN would slip through dist_to_band's comparisons as distance 0 and saturate the round() as i32 step into a bogus pitch. Tests cover finite resolution, x-aware manifestation selection across staves sharing a y band, non-finite rejection, and time-ordered clef resolution. The GUI surfaces the resolved pitch on an empty-staff click. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
e5efe22d5b |
layout-ir: staff_step_pitch, the clef-relative position inverse
The inverse of staff_position: a staff step (a clicked staff height) back to the diatonic pitch (nominal, octave) written there under a clef. This is the vertical half of the click-to-insert inverse the GUI will use — turning where you click on a staff into the pitch to place. It mirrors the forward map's clef and octave-shift convention exactly, computes the diatonic index in i64 so an extreme step cannot overflow the intermediate, and returns None for a percussion clef (no diatonic mapping) or a position whose octave falls outside i8 (rather than wrapping to a wrong-looking octave). A round-trip test covers treble/bass/alto/tenor and octave-shifted (8va/8vb) trebles across octaves and nominals, plus the out-of-range refusals. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
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 |
|
|
|
c56ea4845f |
editor-gui: a thin native (egui) editor over the editor-core seam
A new demo binary crate that proves the EditorSession seam end to end. It renders the session's resolved layout with epiphany-render-svg, rasterizes that SVG with resvg into an egui texture, resolves clicks back to staff-space world coordinates to select, and drives all six note-editing intents from a toolbar and keyboard (delete, chromatic transpose, staff-step move, add chord note, insert-after). A debug panel shows the selection and the last applied op, proving the op log is usable. Intentionally narrow: no duration editing, empty-space insert, or undo UI. The one piece of nontrivial logic — the screen<->world map a click depends on — is a pure ViewMap that inverts render-svg's translate(-min_x, max_y) scale(1 -1) transform, unit-tested for round-trip, the y-up corners, and the logical-vs-ceiled size mapping (the texture is displayed at the SVG's logical size with a uv crop, so the click plane is not stretched by the pixmap's rounded-up dimensions). rerender updates view box, logical size, and texture together on success and clears the texture on a rasterization failure, so the pixels never disagree with the click plane. eframe + resvg are the workspace's first heavy external deps. eframe is pinned to 0.29 for the classic App::update(ctx) + panel API (0.35 redesigned App around ui()). The crate needs a newer toolchain than the workspace MSRV, so CI excludes it from the pinned-1.77 workspace commands and checks it in a separate current-stable editor-gui job; the rest of the workspace is unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
d16435f2a9 |
editor-core: expose the resolved layout for renderers
EditorSession kept only the RenderIR (its hit-test projection) and discarded the ResolvedLayoutIR the solver produced — but a renderer like epiphany-render-svg consumes the ResolvedLayoutIR, not the RenderIR. Keep it on the session and add resolved(), so a GUI can draw the score without re-running the solve. render_score now threads all three (resolved, render, map); they are updated together on open and on every committed edit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
d6fb8afcdc |
editor-core: override-carrying insert_note_after_selection
An insert after a pitch that carries an authored spelling override no longer refuses — it carries the spelling onto the copy. With no override the insert is still a plain InsertEvent (the copied value takes the inferred spelling); with one, it emits a transaction of InsertEvent plus a RespellPitch on the new note's fresh PitchId set to the anchor's authored spelling. The members apply in counter order, so the insert mints the pitch before the respell spells it, and the copy renders like the original. The spelling is copied verbatim, not stepped: an insert-after is a same-pitch time copy, so it keeps the same staff position. This completes the four-step transaction plan: foundation, override-aware move, override-carrying insert. The only remaining override refusal is the chord add (PitchSpellingOverridden), where picking the visual top note needs resolved-spelling-aware stacking — a separate follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |
|
|
|
703d9d7e35 |
editor-core: override-aware move_selection_staff_step
A staff-step move of a pitch that carries an authored spelling override no longer refuses — it rebases the override atomically. With no override the move is still a plain ModifyIdentifiedPitch (the inferred spelling follows the new value); with one, it emits a transaction of ModifyIdentifiedPitch (step the value) plus RespellPitch (step the spelling by the same staff step), so the pinned notehead and the sound move together as one undoable unit. authored_spelling returns the winning override spelling, mirroring resolve_spelling's selection (lowest precedence rank, then highest priority, then first in canonical order); has_authored_spelling_override now delegates to it. Because RespellPitch materializes a UserChosen attachment (top precedence), rebasing the winning spelling works even when the original override was Imported or Propagated — the new UserChosen shadows it. staff_step_spelling is the spelling analogue of staff_step: it moves the CMN spelling nominal and octave (B-C carry) while preserving the accidental stack and render hints, so pitch and spelling move the same number of staff positions and stay enharmonically consistent. This is the first intent built on transaction-aware apply. The chord add and the copy-insert still refuse on an override (PitchSpellingOverridden); insert gets its override-carrying version next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NAtEiJtt9yKVV1zjKYmZhS |