Two open candidates, staged until a third reopens the pass.
P13-S1: 169 of core_spec's 207 requirement blocks carry no \label, so no
conformance claim can cite them. The audit that surfaced this scoped it to
Chapter 4 (9/9 unlabeled). It is not a Chapter 4 problem: Determinism Contract
is 15/15, Semantic Operations 24/27, The Score Graph 22/28, Pitch 10/13. Every
req:* label this repo cites was added ad hoc by whichever pass needed it.
P13-S2: cmn-24 is in the built-in pitch-space table as "CMN extended with
24-EDO quarter-tone accidentals" and cannot be represented -- Cmn.alteration is
an i8 of whole semitones, and a quarter-tone is half of one. Blocks Push 4b.
Also parked: the Push 4b prerequisites. Push 4a proved transposition needs no
tuning catalog; what the catalog is still for -- resolving a scale position to
a frequency, applying an instrument's transposition at the written/sounding
boundary -- remains, and Chapter 4 is not implementable as written.
Two further claims from the audit are recorded as UNVERIFIED rather than
inherited: that the JI dimension convention conflicts with its own prime-2
requirement, and that the named historical tunings lack exact ratio data.
Neither was needed for 4a; neither was confirmed. An audit finding that has not
been executed is a hypothesis.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes P12-K2. The reducer, the payload at wire discriminant 30, and the
editor authoring that emits it.
TransposeIntervalOp carries targets: CanonicalSet<PitchId> -- a set at the
type level, not a Vec plus a dedup() someone can forget. PitchId's Ord is its
canonical byte order, so a BTreeSet iterates in canonical order and cannot
hold a duplicate. Encoding it is the wire table's seq-strictly-increasing by
construction. The frozen Transpose keeps sorted_canonical and its multiset.
Reduction refuses atomically. Every mutable target is resolved before any is
written, so an untransposable one leaves the whole chord alone -- a chord
transposed except for one note is a different chord. Tombstoned and
SYSTEM_DERIVED targets are still skipped: a deleted pitch is not an
untransposable pitch, it is one the operation has nothing to say about. The
three refusals map to PitchSpaceMismatch (6, un-reserved -- detecting a
non-Cmn position reads a discriminant, never the tuning catalog its doc
claimed to need), AcousticRealizationPinned (14), TranspositionOutOfRange (15).
The refusal reads pitch values, which exist only under reduce_onto, so it is a
graph-aware-only precondition that passes base-free -- the convention
modify_identified_pitch's system-derived check already set. It writes nothing
base-free either, so both modes agree on objects, and on the effect log for
every operation whose targets are all transposable, which is all base-free
reduction can see.
Spelling propagates. Core Ch2 requires transposing operations to produce
Propagated attachments; Transpose produced none, so an authored spelling
survived a transposition still pinned to the notehead it was written against.
simplest_spelling on a Cmn position returns the authored letter verbatim, so
the attachment carries exactly what the interval's diatonic component decided:
a diminished sixth up from C4 records A-double-flat, not the enharmonic G.
Editor. transpose_selection now takes a TranspositionInterval; a scalar cannot
tell "up an octave" (7,12) from "C with twelve sharps" (0,12), which is
P12-K2 itself. The "+1 semitone" key became alter_selection(+-1). TransposeOp
is now unused in editor-core's lib, so the compiler enforces "never authored".
Tests, five mutations verified: the graph write removed; the refusal made
non-atomic (skip the offender, move the rest); spelling propagation dropped;
and -- for the freeze -- graph_transpose_pitch "helpfully" repaired to use the
real algebra, which the_frozen_transpose_keeps_its_saturating_alteration_
semantics correctly rejects. That test guards against rewriting history, not
against a bug.
The two old transpose tests were false locks, but the fix was not to rewrite
them as the design gate promised. What they assert -- skip-tombstoned,
skip-system-derived, refuse-missing -- are effect-log properties, correctly
checked base-free. The defect was one test's NAME: it claimed the live target
"shifts" and checked nothing of the kind. Renamed to say what it proves; the
shift itself is now locked by two graph-aware tests against reduce_onto.
fuzz::gen_payload gained arm 27, so below(27) became below(28) and the seeded
stream reshuffled; the canonical-base digest is re-pinned consciously, per
that test's own instruction and the Phase-D precedent. Nothing leaked --
canonical_bytes embeds effects, conflicts and anomalies, never payload values.
The frozen Transpose keeps fuzz arm 6 and its testkit corpus authoring: it
must reduce correctly forever, and a generator is now the only thing that will
ever produce one.
Gate: fmt clean, clippy 0, 30 targets / 982 passed / 0 failed, docs 0 under
-D warnings, conformance 8/8, zero golden churn.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
req:pitch:transposition, implemented as Pitch::transposed. With n the
nominal's normative discriminant and s = nominal.chromatic() + alteration +
12*octave:
nominal' = CmnNominal((n + d).rem_euclid(7))
octave' = octave + (n + d).div_euclid(7)
alteration' = (s + c) - (nominal'.chromatic() + 12*octave')
The diatonic component alone picks the nominal and octave; the alteration
absorbs the residue. C4 + (7, 12) is C5, not C with twelve sharps. C4 + (0, 1)
is C#4, so the editor's sharpen is unchanged. A perfect fifth (4, 7) and a
diminished sixth (5, 7) from C4 give G4 and A-double-flat-4: same sound,
different spelling, which is the whole reason the interval is a pair.
TranspositionInterval moves from graph.rs to pitch.rs. It already existed --
Instrument.transposition has carried it since schema major 2, already codec'd,
already exported -- and it is byte-for-byte the pair this needs. The spec now
declares it once, in Chapter 2 where transposition lives, so the struct sits
where its algebra does. Wire form and codec are untouched.
Its doc claimed it was "ADVISORY until the Chapter 4 tuning catalog pins
interval algebra". That was the P12-K2 false coupling repeated a third time.
The field stays advisory because nothing applies it at the written/sounding
boundary -- for that reason, not for want of algebra, which needs no catalog.
Refusal, never saturation: TransposeRefusal::{NonCmnPosition, AcousticPinned,
OutOfRange}. An AbsoluteHz pitch overrides the tuning system, so transposing
it would move the notehead and leave the sound behind; the mutation that drops
that guard returns Ok(G4 ... AbsoluteHz(261.6)), a notehead reading G4 that
still sounds middle C. TranspositionInterval::inverse is exact precisely
because nothing saturates.
Six tests, four mutations verified: the old alteration-shift semantics, a
clamp in place of the refusal, a truncating `/` in place of div_euclid (which
breaks C4 down a step to B3), and dropping the AbsoluteHz guard.
One process note. The AbsoluteHz mutation appeared to survive on the first
run. It had not: rustfmt had wrapped the matches! across three lines, so the
string replacement silently matched nothing and the test was never challenged.
A mutation that no-ops looks exactly like a test that passes. Mutations now
assert their anchor before applying.
Gate: clippy 0, core 165+5 passed / 0 failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An audit reopened P12-K2, whose Pass-12 pin promised the repair would be "a
payload schema-major landing with the Chapter 4 tuning catalog". Both halves
were wrong, and the operation was more broken than the pin admitted.
Measured through EditorSession, not inferred. On a C4: +12 yields alteration
12 (six double-sharps, not C5); +128 clamps to 127 and still reports Applied;
targets [p, p] transposes twice; a non-Cmn position is silently untouched.
transpose(1000) then transpose(-1000) lands on -128, so the operation is not
invertible. Nothing downstream is at fault -- prepass::accidental_ids renders
alteration 12 faithfully. The defect is entirely in what Transpose means.
The false coupling is why this looked big. Pitch has orthogonal scale_position
and acoustic fields. Transposition adds an interval to a scale position;
tuning decides what frequency a scale position sounds at. Adding a fifth to C4
needs no tuning catalog. The same weld had spread: PitchSpaceMismatch was
"Reserved: requires the Chapter 4 tuning catalog" (it reads a discriminant),
and TranspositionInterval was "ADVISORY until the Chapter 4 tuning catalog
pins interval algebra". Push 4 splits: 4a is the algebra and needs no catalog;
4b is the catalog, which has its own blockers (cmn-24 is in the pitch-space
table but cannot exist while Cmn.alteration is i8 semitones).
Ratified by the user:
- New kind, freeze the old. An operation is history; a corrected reduction
rule would rewrite every score that used one. Transpose (disc 9) keeps its
exact semantics, now written as normative replay semantics rather than as
apologies. TransposeInterval takes disc 30. This is cheap: appending a kind
at >= 30 is a schema MINOR, and the payload's constituents are all major-0
layouts, so it stamps major 0. No major 3, no migration.
- Diatonic + chromatic interval, reusing TranspositionInterval -- which
already existed in graph.rs at major 2 for Instrument.transposition, already
codec'd, byte-for-byte the required pair. Minting an Interval beside it
would have been a second normative listing of one type, the drift P13-I1
just closed. Declared once now, in Chapter 2; Chapter 5 references it.
- Atomic refusal. Non-Cmn, AbsoluteHz, or an out-of-range result refuses the
whole operation. Never saturate, never partially apply. Tombstoned and
SYSTEM_DERIVED targets are still skipped: a deleted pitch is not an
untransposable pitch, it is one the operation has nothing to say about.
targets becomes CanonicalSet<PitchId> at the type level, not a Vec plus a
dedup() someone can forget (PitchId's Ord is its canonical byte order). This
was never a convergence bug -- every replica replaying [p, p] double-
transposes identically -- but a canonicalization one. It is free today because
no operation-payload decoder exists yet; once one lands in Push 5, dedup
normalization would change the meaning of stored operations. Push 4a blocks
Push 5, and that is why.
Spec: req:pitch:transposition (algebra + the three refusals), and four
req:opcat:transpose-* requirements. Operation Catalog 0.7.0 -> 0.8.0; Binary
Format 0.6.0 -> 0.7.0 (disc 30, and a seq-strictly-increasing notation whose
decoder must reject a duplicate rather than normalize it away).
This commit is the design gate: the spec now declares MUSTs the code does not
yet satisfy -- editor-core still authors Transpose, and TransposeInterval does
not exist. The implementation follows in this push.
Also recorded: the two existing transpose tests are false locks. Gutting
graph_transpose_pitch leaves both green -- they call base-free reduce(), where
graph is None and the function never runs, and assert only OperationEffect.
Only editor-core's undo_and_redo_a_transpose, three crates away, catches it.
Gate: clippy 0, 30 targets / 964 passed / 0 failed, docs 0 under -D warnings,
conformance 8/8, all three spec documents build with no undefined references.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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
Review findings on 794400c. The criterion-4 harness stored current
(v2) Score bytes stamped V0 in the canonical_base slot and decoded
them with the unversioned decoder — bypassing the schema-major
snapshot contract Phase B built, and misusing the base role. Fixed
substantively: the harness stages a properly-roled ACCELERATION
snapshot (ChunkKind::Snapshot stamped for_major(2), referenced from
Manifest::acceleration_snapshots) and decodes through
Score::decode_canonical_versioned keyed by the stamped major — the
versioned seam now exercised end-to-end through the bundle, giving
the snapshot role its first real producer/consumer.
Consequences: max_supported_major(Snapshot) -> 2; and since the
per-kind gate no longer implies it, the canonical-base-stays-major-0
rule is enforced per ROLE (mis_stamped_canonical_base, consulted at
open and commit -> read-only + UnsupportedCanonicalChunkMajor;
regression test covers commit and reopen).
Also: three stale-doc fixes (decode_canonical now "current (schema
major 2)"; CreateRegionOp + StagedChunk::operation_block +
stage_operation_block docs describe minimal stamping, not just "v1
CreateRegion").
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
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
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
The decided code work behind aff1809, review-hardened (high-effort
multi-agent review; 3 correctness findings fixed pre-commit, all
regression-locked).
epiphany-ops:
- K3: SystemDerivedContentImmutable (12) — modify_event /
modify_identified_pitch refuse intrinsic-content rewrites of
SYSTEM_DERIVED pitches, checked against the system_mints registry
(base-seeded + op mints, so reduce()/reduce_onto() agree).
REVIEW FIX: Transpose now *skips* system-derived targets like
tombstoned ones (an in-place alteration shift desynchronized content
from the derivation registry -> checkpoint-dependent verdicts); an
all-system transpose reduces as the K3 no-op. Catalog Transpose
updated.
- K9: RecreateContentMismatch (13) replaces the TargetMissing misnomer
at both value-retaining re-create sites (CreateStaff, carried
TimeSignature). Catalog corrected: container creates are plain
set-union (no value comparison), not K9 sites.
- C4: ReanchorReason::SameCanvasNearer (6); rank-4 recording routes
through rank_reason. REVIEW FIX: an *unestablished* rank 4
(unresolvable placement fallthrough in containment_rank) keeps the
honest ExplicitFallback — selection order unchanged.
- Filed P13-K1 (pre-existing checkpoint asymmetry for ModifyEvent-
introduced system pitches) in DECISIONS rather than improvising.
epiphany-core (H7, req:pitch:authored-uninferred):
- Authored attachments surface for inference-ineligible targets in
both pre-passes (best_authored_spelling/_decomposition refactor,
candidates-first liveness via the existing Score::live_pitch_ids);
two new taxonomy buckets enter the derivation fingerprint. The old
test locking the pre-decision mirror is flipped to the ratified
behavior.
epiphany-testkit (review fixes):
- prepass_harness: spelling-unavailable pitches may carry *authored*
surfacings (inferred still forbidden); decomposition reconstruction
invariants scoped to algorithm output (authored entries are
invariant-15's graph-level jurisdiction); accounting identity gains
the authored-uninferred bucket.
- generators: the three appended discriminants (12/13/6) now generate.
Full gate green: fmt, clippy -D warnings, rustdoc -D warnings, 30
workspace suites, conformance scale 1 (8/8). Wire goldens + decode
bounds for the appended discriminants; 12 new regression tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
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
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
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
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
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
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
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
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
Prerequisite for the visible-slice engraving milestones (I-1+): give the
score graph the clef/key data the engrave pipeline needs for clef-relative
staff positioning and key-signature rendering, replacing the Chapter-7
TimeAnchor-only placeholders.
epiphany-core:
- ClefShape { G, F, C, Percussion } — each family's reference pitch (G4 / F3 /
middle C4) pins the staff-position mapping.
- Clef { shape, line, octave_shift } — the SMuFL family, the staff line its
reference pitch sits on (1 = bottom line), and an octave transposition; with
treble/bass/alto/tenor constructors and a treble Default. Generalizes to any
C-clef line and octave-transposing clefs.
- KeySignature — circle-of-fifths position, validated -7..=7: a private field
behind KeySignature::new (Option) + fifths(), and a custom Codec that rejects
an out-of-range count on decode (Reconstruct, as PowerOfTwo / Tempo do).
- ClefChange now carries clef: Clef; KeySignatureChange carries key: KeySignature.
- Codec (cstyle_enum_codec / struct_codec / the validating KeySignature impl),
lib.rs re-exports, a round-trip test over every shape x line x octave and every
fifths value, and an out-of-range decode-rejection regression test.
Zero blast radius: nothing constructed these with data (clef/key sequences are
always empty) and invariants only read .anchor, so existing Score bytes, hashes,
and goldens are unchanged. Fixtures get populated and the engrave pipeline reads
the data in I-1.
Gates: build/fmt/clippy -D warnings clean; cargo test --workspace green;
conformance_suite scale 1 passes. Stages only epiphany-core; the pre-existing
Agent-I working tree (engrave/layout-ir/render-svg + .gitignore) stays unstaged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Address the five-finding review of M2d (1e4ab24) plus the two-finding
follow-up review; all fixes are code/test/doc only, no spec change needed
(the catalog/core-spec already classify metadata as advisory LWW).
- SetMetadata is now a true advisory LWW: it silently last-writer-wins in
canonical order and records no conflict, matching operation_catalog
§set-user-system-break. Dropped the StructuralFieldCollision, the
`last_metadata` working slot, and the `env` parameter; rewrote the
conflict test as `concurrent_differing_set_metadata_is_advisory_lww`
(no conflict, state stays clean, permutation-independent resolution).
- SetMetricGrid / SetUserPageBreak / SetUserSystemBreak share a new
`layout_region_slot` precondition backed by a `staff_based_regions`
index: the target must be live and staff-based (FreeGraphic regions have
neither a metric-grid nor a break slot). The index is read from base-free
state, so reduce() and reduce_onto() reach the same verdict for missing,
tombstoned, and FreeGraphic regions.
- SetMetricGrid now rejects a grid whose meter_sequence names an undeclared
time signature, rather than installing an invariant-violating grid.
- User breaks materialize under the canonical LWW key: `apply_break_lww`
drops any existing anchor resolving to the same position before adding,
so the graph break list stays in lockstep with the resolved-position
ledger map (shared `resolved_anchor_position`). Applied to page and
system breaks alike.
- Coverage: SetMetadata/SetMetricGrid/CreateVoice/DeleteVoice added to the
tag-distinctness test; layout_stub `gen_operation_kind_tag` extended to
every normative tag; the MaterializedState decode test populates
page_breaks; four direct regression tests pin each fixed bug; the stale
SetMetadata/score_metadata doc comments now say advisory LWW.
Gates: build/fmt/clippy -D warnings clean; cargo test --workspace green
(533); conformance_suite scale 1 passes. Stages only core/ops/testkit; the
unrelated Agent-I working tree is left untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourth broad-K0 subsystem group — three value-typed LWW field-overwrite ops, one
per settings cluster (additive: OperationKind variants 21-23; framework frozen):
- SetMetadata { metadata: ScoreMetadata } — overwrite the score-singleton
metadata (title/composer/copyright); concurrent differing ⇒
StructuralFieldCollision.
- SetMetricGrid { region, grid: Option<MetricGrid> } — overwrite a region's
default metric grid (LWW keyed by region; concurrent differing ⇒ collision).
- SetUserPageBreak { region, anchor, present } — the page-break sibling of
SetUserSystemBreak: a canonical LWW advisory.
- core: expose ScoreMetadata + MetricGrid via CanonicalValue (no new byte layout
— they already have whole-score Codec impls).
- SetMetadata / SetMetricGrid mirror the modify ops: the resolved value lives in
the graph (reduce_onto), with new last_metadata / last_metric_grid LWW working
state for concurrent-differing detection; MaterializedState records only the
effect and conflict.
- SetUserPageBreak mirrors SetUserSystemBreak's canonical advisory: a new
MaterializedState.page_breaks map (encode + decode added, parallel to breaks),
plus the graph's region user_page_breaks.
Migration: v1-native (no lossy v0 predecessor) -> project/migrate by identity;
the round-trip identity test now covers all four M2 groups.
Coverage:
- testkit operation_payload + ops fuzz gen_payload emit the three kinds, so the
convergence / determinism / migration-equivalence and MaterializedState
decode-roundtrip gates exercise them (incl. page_breaks) at scale.
- reduce_onto graph tests: the three settings materialize in graph and ledger
(metadata title, region default metric grid, region user page break +
MaterializedState.page_breaks), invariant-clean; plus a concurrent-differing
SetMetadata conflict test.
Gates: build/fmt/clippy -D warnings clean; cargo test --workspace green (519);
conformance_suite scale 1 passes. Catalog sections + DECISIONS for the M2 groups
land in M2e per the staged plan. The unrelated Agent-I working tree is left
uncommitted; this commit stages only core/ops/testkit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Addresses four findings spanning the H (pre-pass) and K (reduction) seams plus
the Operation Catalog.
1. [High] A reduced RespellPitch is now visible to the pre-pass. The reducer
stored overrides only in MaterializedState.spellings, but Agent H's
derive_annotations resolves authored spellings from score.spelling_attachments
— so a real respelling accepted by reduce_onto was lost before annotation
derivation, violating manual-override precedence. respell_pitch now upserts a
user-chosen explicit SpellingAttachment into the materialized graph
(materialize_respell / graph_respell_pitch); DeleteIdentifiedPitch drops that
attachment (graph_delete_pitch) so none dangles (it does NOT tombstone the
pitch — the event survives a pitch delete and a later ModifyEvent may reuse
the id, which would make it both live and tombstoned). New testkit gate
assert_reduced_respell_is_honored reduces a real RespellPitch and proves
derive_annotations honors it as Authored(UserChosen); wired into run_all.
2. [Medium] PrePassProfile algorithm ids are now enforced, not just recorded.
derive_annotations ran the default logic and labeled the result with the
requested algorithm. It now runs each pre-pass only when its requested id is
the implemented "default"; an unknown/future id yields no annotations for that
pre-pass (the requested id stays in the result profile), so a future algorithm
can no longer silently alias the default in a derivation cache. Test:
unknown_algorithm_ids_are_not_honored.
3. [Medium/Low] The determinism gate now fingerprints canonical bytes, not Debug.
DerivedAnnotations gains canonical_fingerprint(): embedded graph values
(PitchSpelling, DecompositionAttachment, SpellingSourceKind — the latter two
added to the CanonicalValue surface) use their ratified bytes; counts/ids are
little-endian, length-framed. The pre-pass harness fingerprints with it. A
discrimination check confirms it is not a degenerate constant.
4. [Low] operation_catalog.tex K1 chapter reconciled with the implemented M2
work: the now-dispatched ops (event/pitch leaf-field, cross-cutting CRUD,
structural container CRUD) are listed as implemented-since-M2 (available under
the Phase-2 profile), and the "MUST reject" scope is narrowed to the genuinely
deferred slots (create score/canvas/staff, set metadata, metric-grid/time-sig/
tempo, layout/page-break). PDF rebuilt clean (0 undefined refs).
Gates: build/fmt/clippy -D warnings clean; cargo test --workspace green (criterion
1 + the pre-pass and convergence gates); conformance scale 1 passes. The unrelated
Agent-I working tree is left uncommitted.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Third broad-K0 subsystem group — six new value-typed ops for the score-graph
containers, reusing M1's disciplines (additive: OperationKind variants 15-20,
new apply arms + reduction methods; framework frozen). Per the project lead's
call, container deletes are EMPTY-ONLY (no cascade): a precondition NoOp unless
the container has no live children, so the caller deletes contents first.
- CreateRegion / DeleteRegion, CreateStaffInstance / DeleteStaffInstance,
CreateVoice / DeleteVoice. Creates are value-typed mints of an empty container
(set-union creation); deletes are delete-wins tombstones gated on emptiness.
- core: expose Region / StaffInstance / Voice via CanonicalValue (no new byte
layout — they already have whole-score Codec impls), with round-trip coverage.
- New PreconditionFailureReason::ContainerNotEmpty (additive discriminant 10;
encode + decode), reported when an empty-only delete hits a non-empty container.
Reduction (reduce.rs):
- Two child-existence indices, region_instances and instance_voices, drive the
emptiness checks (a voice's events are read from voice_occupancy), so the
ledger projection and the graph agree on every delete result. Populated by
seed_from_graph, the create ops, and implicit voice creation in insert_event.
- CreateStaffInstance / DeleteStaffInstance maintain the region's staff_extent so
it lists exactly the manifested staves (Chapter 5 RegionExtents); valuegen's
fresh region uses a far-future wall-clock extent so it never overlaps an
existing region in both time and staff.
Migration: v1-native (no lossy v0 predecessor) -> project/migrate by identity;
group1_and_group2_kinds_round_trip_by_identity extended to cover Group 3.
Coverage:
- testkit operation_payload + ops fuzz gen_payload emit the six kinds, so the
convergence / determinism / migration-equivalence gates exercise the
bookkeeping projection at scale.
- A reduce_onto graph test materializes a region -> staff instance -> voice
subtree (invariant-clean), asserts the empty-only delete refuses a non-empty
container with ContainerNotEmpty, and verifies an ordered teardown clears the
subtree from both graph and ledger.
Gates: build/fmt/clippy -D warnings clean; cargo test --workspace green (criterion
1 green with the new container objects in the corpus); conformance_suite scale 1
passes. Catalog sections + DECISIONS for these ops land in M2e per the staged
plan. The unrelated Agent-I working tree is left uncommitted; this commit stages
only core/ops/testkit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First broad-K0 subsystem group — five new value-typed ops reusing M1's proven
disciplines (additive: new OperationKind variants 8–12, new apply arms +
reduction methods; framework frozen):
- ModifyEvent { event: Event } — field-overwrite LWW by EventId; concurrent
differing ⇒ StructuralFieldCollision.
- Transpose { targets, chromatic_steps } — order-dependent; pitch ids preserved;
canonical footprint = effect-log entry; reduce_onto applies a minimal CMN
alteration shift (rich interval algebra deferred — P12-K2).
- InsertIdentifiedPitch / DeleteIdentifiedPitch — pitch-within-event mint /
delete-wins tombstone.
- ModifyIdentifiedPitch { pitch, value: Pitch } — field-overwrite LWW (the pitch
VALUE, distinct from RespellPitch's spelling-only overwrite).
Design (honesty rule): the modify/transpose ops record effect + conflict
canonically — the resolved values live in the graph (reduce_onto), since
MaterializedState is bookkeeping, not a second graph; respell stays special
because spelling is a bookkeeping-owned annotation. LWW diff uses new
`last_event_modify`/`last_pitch_modify` working maps (synced through
WorkingSnapshot/snapshot/restore).
- core: expose Pitch + IdentifiedPitch via CanonicalValue (no new layout).
- The five kinds are v1-native (no lossy v0 predecessor): project/migrate them by
identity; only the original kinds reconstruct from a lossy v0 form.
- Generators (testkit operation_payload, ops fuzz gen_payload) now emit the new
kinds, so the convergence / determinism / migration-equivalence gates exercise
them at scale; plus targeted migrate identity + reduce LWW/mint/delete tests.
Gates: build/fmt/clippy -D warnings clean; cargo test --workspace green (ops lib
51 tests); conformance_suite scale 1 passes. Catalog sections + DECISIONS for
these ops land in M2e per the staged plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Foundation milestone for Track B's Operation Catalog: shift epiphany-ops from
the v0 identifier-only payload projections to durable value-typed payloads, and
scaffold the companion spec. Scope: the representative §6.10 set (7 primitives +
2 meta-ops); the slice-driven K0 expansion follows.
Core (the K↔J seam):
- epiphany-core exposes a public `CanonicalValue` trait (canonical_bytes /
decode_canonical) delegating to the existing private `Codec` machinery, for
Event/Rest/PitchSpelling/Tie/Slur/Beam/Spanner/RegionTimeModel/TimeAnchor.
No new byte layout — a value's bytes equal what the whole-score codec emits,
so all goldens / criterion 4 stay byte-identical.
Ops (value-typed payloads, frozen reduction rules):
- InsertEventOp{staff_instance,event:Event}, RespellPitchOp{pitch,spelling:
PitchSpelling}, CreateCrossCuttingOp{structure:CrossCuttingValue},
ChangeRegionTimeModelOp{...,new_time_model:RegionTimeModel},
SetUserSystemBreakOp{...,anchor:TimeAnchor}, ReplaceWithRest{rest:Rest}.
Payloads frame each value's CanonicalValue bytes behind a u32 length prefix.
- reduce.rs: read-sites only moved onto the value (rules, conflicts, ordering,
promotion, re-anchoring, undo, transactions unchanged); reduce_onto now
materializes the real event/structure instead of the C4 placeholder.
MaterializedState.spellings now stores PitchSpelling (encode + decode updated).
- v0.rs: frozen identifier-only shapes (migration regression guard).
- migrate.rs: migrate_v0_envelope(v0, &Score) + project_v1_to_v0 + MigrationError;
deterministic and equivalence-preserving. Respell spelling recovered from the
score context; irreversible case is P12-K1.
- valuegen.rs: shared value-type builders (reused by fuzz, migration, tests,
testkit). Resolves P11-C1; P11-C10 Dismiss recorded.
Testkit (Agent F merge gate):
- migration.rs: reduce(v1)==reduce(migrate(project(v1))) byte-identical, plus
migration determinism and a non-vacuity guard; wired into acceptance.rs as
agent_k_migration_equivalence_gate. Generators/harnesses build v1 payloads.
Spec:
- spec/operation_catalog.{tex,pdf}: new companion (independent semver) — framework
+ per-primitive template, the 7+2 representative primitives, the v0→v1 migration
contract, and K1 framework slots for the remaining K0 primitives. Builds clean.
- PASS12_BATCH.md: P12-K1 (respell fingerprint irreversibility).
Gates: cargo build/fmt/clippy -D warnings clean; cargo test --workspace green
(incl. criteria 1/4/5/6 and the new K gate); conformance_suite scale 1 passes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Land the two real pre-passes as canonical *derived annotations* (pure
functions of the materialized Score + profile, recomputed on
materialization, never serialized into canonical Score bytes), exposed via
`derive_annotations`:
- Spelling: a Temperley-style line-of-fifths centre-of-gravity preference
rule (key-free, deterministic), preserving authored CMN letters and only
inferring spelling for chromatic/integer input. `resolve_spelling`
layers authored overrides above the inferred default (the RespellPitch
precedence rule). `spell` now takes `&Pitch` and delegates to
`simplest_spelling`.
- Decomposition: metric greedy-aligned splitting on a 1/4096 integer grid
(barline + dyadic-boundary ties), with exact sounding->notated tuplet
conversion before gridding. Components reconstruct the event duration
(invariant 15).
- A per-event-kind eligibility `TaxonomyReport` so "ineligible" is always
explicit and counted, never silently absent.
Test infrastructure (Agent F): a 29-fixture representative corpus +
taxonomy harness (corpus.rs), the H spelling/decomposition merge gate
(prepass_harness.rs), a discrete `tests/prepass.rs` CI target, conformance
stage [7b], a dedicated CI job, and the Pass-12 batch tracker.
Review hardening folded in (nine findings):
- Guard `decompose_metric` against a zero-length measure (was a
divide-by-zero panic; now reported ungriddable).
- Resolve spelling-override priority via `Reverse` instead of negation
(was an i32::MIN overflow).
- Verify spelling *register* (octave), not just pitch class, in the gate.
- Close the decomposition under-emission gap: the unusual-outcome
taxonomy buckets are an exact per-fixture whitelist (classify_corpus
step 5b).
- Generalize `accidental_ids` to a glyph stack so authored extreme
alterations (triple-sharp+) reconstruct exactly instead of being clamped.
- Per-fixture spread checks in the non-vacuity tripwire and broad-bucket
coverage, so no single rich fixture can carry a signal (partial-stub
resistance); added a `mixed_rhythm` fixture for margin.
- Pin the integer-grid note-value math to the canonical rational helpers
via an exhaustive test; cross-reference comments.
- Replace the O(n^2) tuplet innermost-resolution scan with an id index.
fmt + clippy -D warnings clean; 199 tests pass; conformance suite green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011giSRaHCFCGm1Z2SWv6JHt
Independent audit of b2f2e20 / a7adbdc. The canonical, document-state byte
layouts were already correct and golden-locked; this closes the one real
spec-vs-code gap (in the non-canonical layout namespace) and the smaller
doc/test gaps around it.
LayoutObjectId (item 2.6): the spec normatively stated the id "is derived ...
with MUSCLOID" and the ratification log / layout DECISIONS header called it
"pinned" -- but no code uses MUSCLOID (stable_layout_id and
manifestation_layout_id are untagged; synthesized_layout_id borrows MUSCCONF),
and MUSCLOID cannot even be constructed: DomainTag::from_bytes accepts only
built-ins or MUSCS-prefixed tags, so wiring it requires changing the frozen
determinism crate (out of scope for a spec pass, and the spec itself says
MUSCLOID is not a canonical system tag). Kept MUSCLOID as the pinned Track-A
target and made every artifact honest that the v0 code is provisional:
- spec: "is derived" -> "MUST be derived" (forward contract) + a note that
the prototype mints provisional ids; changelog "pinned" -> "specified ...
as the Track-A target".
- records: ratification-log line 2.6 and layout DECISIONS header/body now
state spec-pinned-but-code-provisional; stale "Pass 11 candidate 3"
pointer now cites the ratified requirement.
- provenance.rs comments name the MUSCLOID target and label the current
derivation provisional.
Other fixes:
- epiphany-core/DECISIONS.md: the Tuplet bullet still claimed degenerate
ratios are caught by runtime invariant 16 "since a Tuplet is a plain
struct" -- stale after the construction-time TupletRatio change. Rewritten.
- codec.rs: added degenerate_tuplet_ratio_is_rejected_on_decode, guarding
the TupletRatio::dec re-validation branch that no test exercised (the
constructor was tested, the decode path was not).
- spec: integrity-anomaly snippet kind.canonical_bytes() ->
to_canonical_bytes() (the actual method).
- ids.rs / reduce.rs: clarifying comments (ManifestId's intentional,
golden-locked document_id/generation duplication; compute_promotions
bucketing by voice == (staff_instance, original_voice) via Invariant 5).
Verification: cargo test --workspace (434 pass, +1), clippy --all-targets clean
(0 warnings), fmt clean; spec rebuilds (lualatex, 0 undefined refs, 254pp).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Spec-revision pass (architecture unchanged) converting the v0
implementation's provisional, golden-locked choices into ratified
core_spec.tex text, so durable byte layouts are fixed before the
next-phase build-outs. Worklist: spec/PASS11_WORKLIST.md; per-item
dispositions: spec/PASS11_RATIFICATION_LOG.md.
Adopt-and-pin (bytes): TypedObjectId 16-bit BE discriminant table
0..=27 (added the 5 variants the code carried); promoted-voice
(MUSCSVCE), synthetic-pitch (MUSCSPCH, tuning always in identity),
and integrity-anomaly (MUSCSANM, now a reserved built-in tag)
derivations; ChunkKind/ProfileId/CompressionAlgorithm discriminants;
ManifestId preimage (manifest_id excluded); RationalTime/scalar
layouts + the codec convention baseline the Binary Format companion
inherits.
Decide-and-pin: tempo Linear interpolates speed (not bpm);
StructuralFieldCollision tags the winner Conflicted; lifted the
>2-way / partial-overlap voice-promotion rule to normative; pinned
TransactionCategory and ObjectKind core vocabularies; added
ResolutionAction::Dismiss so the Dismissed state is reachable by an
authored op; pinned the (non-canonical) LayoutObjectId derivation
(MUSCLOID).
Fixes: blob hashing is bare MUSCBLOB||payload (deleted the
contradictory "identically to chunks" phrasing); equal-generation
superblock rule (DivergentSameGeneration); defined ProfileConstraints
with the required RetentionPolicy + first-declared precedence; made
the DVV zero-based floor normative; reconciled the invariant count to
19 and named the three construction-time MUSTs — TupletRatio now
rejects degenerate ratios at construction (zero term or
actual==notated), enforced by a checked constructor + codec decode
validation.
Code changes carry regression tests; byte-layout golden tests now
cite their ratified requirements. Workspace green: cargo test, clippy
-D warnings, fmt; spec builds (lualatex, 253pp). Per-crate DECISIONS
files annotated with the ratification status.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011giSRaHCFCGm1Z2SWv6JHt
Closes the three remaining item-5 sub-parts found in the audit.
② Typed tolerance + overflow (tempo.rs, invariants.rs):
- Replace the ad-hoc `f64::EPSILON` speed-degeneracy guards with a named
TempoIntegration `Tolerance` (relative, non-finite-safe), per Appendix D
"no ad-hoc epsilons"; aligns code with the module's own claim.
- Guard the continued-fraction convergent recurrence with checked i128 ops
(break on overflow) and bound the residual-fraction stop by 1/max_den
instead of f64::EPSILON, so a pathological input can't silently wrap.
- Endpoints::of: wall-clock event end uses checked_add -> Endpoints::Unknown
on overflow, not saturating_add (which could mask an ordering violation).
- Regression tests: equal-endpoint linear segment uses the constant limit;
extreme inversion inputs don't overflow.
① Region-overlap honesty (invariants.rs):
- Unresolvable region-overlap checks (symbolic anchors + shared staff extent)
were silently treated as valid. Add DeferredCheck + deferred_checks() to
surface them explicitly; check_invariants stays sound (no false positives).
- Test proves an undecidable overlap is reported as deferred, not passed, and
that a wall-clock-resolvable disjoint pair is neither violation nor deferred.
③ Identifier-derivation lock-down (graph.rs, pitch.rs, ids.rs):
- Golden-bytes tests pin derive_promoted_voice_id (MUSCSVCE 64-byte preimage),
derive_system_pitch_id (MUSCSPCH input layout), and the TypedObjectId
discriminant table + Registered layout, so an accidental layout change is
caught (the derivations were concrete but unlocked).
- canonical_pitch_bytes NFC-normalizes strings at the derivation boundary,
making the documented NFC guarantee explicit (no-op for the already-NFC
catalog ids).
- DECISIONS P11-1/3/6 updated to record the pinned-and-locked layouts.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements a total, reversible canonical byte form for the entire
epiphany_core::Score graph, unblocking the byte-level full-Score serialization
gate that M3 had to defer.
epiphany-core/src/codec.rs:
- Score::canonical_bytes() / Score::decode_canonical() with a validating
ScoreDecodeError and a forward-only Reader cursor.
- A local Codec trait with generic combinators (Option/Vec/BTreeSet/BTreeMap/
tuple) and macros (struct_codec!/cstyle_enum_codec!/unit_codec!/
catalog_id_codec!) so encode and decode stay symmetric across ~110 types
spanning graph.rs, event.rs, pitch.rs, time.rs, tempo.rs.
- Uniform form: LE integers, one discriminant byte per tagged union, u32
counts/length-prefixes, every variable-width leaf length-prefixed, raw UTF-8
for free text (so decode(encode(x)) == x for any valid score; catalog ids are
already NFC). EventArena round-trips via iter_canonical + insert.
- Two pub(crate) accessors added for the codec: EventOrderingDAG::edges_ref,
SpellingPrecedence::order_ref.
- Tests: generator-score corpus (valid_score + valid_score_rich), exotic
event/pitch variants the generators omit, distinctness, and decoder
rejection of trailing/truncated/empty bytes.
epiphany-testkit:
- roundtrip::assert_score_serialization_stable: encode the real Score, store it
as a bundle Snapshot, reopen + hash-verify, decode to an equal Score, and
assert a byte-identical re-encode.
- convergence::materialized_score builds a real ~50-bar reduce_onto
materialization for the gate.
- criterion_4_full_score_byte_roundtrip flips from #[ignore] to a live gate;
wired into the conformance suite. Docs (lib.rs, README, core DECISIONS P11-4)
updated to reflect the landed codec.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M1 — fix Agent C framework defects, tests-first:
- causal ordering: topologically order DVV edges instead of assuming HLC
alone implies causal order (false for adversarial remote envelopes);
HLC only breaks ties among ready operations.
- anomaly cutoff: quarantine from the earliest counter participating in
any violating HLC pair (suffix-minima), e.g. [100,200,50] quarantines
from counter 0, not counter 1.
- pending detection: DVV contiguous ranges use the zero-based per-replica
counter floor; first absent id in any asserted range holds the dependent
pending (vector coverage, not only dots).
- transaction snapshots: rollback removes member-generated conflicts.
- edge tests in concurrent_reduction.rs for all six audited cases.
M2 — reduce onto Agent B's real score graph:
- OperationSet::reduce_onto(&Score) -> GraphMaterialization { state, score }
mutates the real arena, voices, regions, tombstones, indexes, and
cross-cutting structures; base-free reduce() retained.
- VoiceOrigin::SystemPromoted now carries { winning_operation,
losing_operation, original_voice }; spec and Invariant 18 updated.
- graph-aware migration, forward undo, system breaks, promotion pre-pass.
- tests/graph_reduction.rs: 11 tests asserting check_invariants is clean,
plus a 64-seed order-independence sweep.
Pass-11 spec decisions resolved (blocking subset): DVV floor (P11-C7),
HLC-vs-causality, promoted-voice derivation inputs (P11-C4 / core P11-3).
Payload/Score canonical encoding remain deferred to the companion docs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>