Kind 39 CreateMeasure and precondition reasons 16-18 reach the Binary Format's
kind table, tag table, payload layout, and reason table; graph invariant 20
reaches the core specification's enumeration, which now says twenty in all three
places it counts. Both normative listings in that document gain CreateMeasure --
earlier drafts of the contract named only the invariant, and prose fails silently.
Every version surface moves in pairs. Binary Format 0.15.0 -> 0.16.0, Operation
Catalog 0.12.0 -> 0.13.0, each with a changelog entry beside the title bump. The
Text Projection companion needed only the changelog: packet 1 bumped its header to
0.13.0 and stopped there, leaving the document claiming a version its own history
did not record. That was live from e64a4b7 until now, and no gate could see it.
Two public hooks exist that would otherwise look like leaks. epiphany-ops depends
on epiphany-core and never the reverse, so invariant 20 implements pin 6/6b's
comparable relation and musical delta a second time over the graph alone. Both
DECISIONS records name the divergence hazard that forces the duplication, name the
cross-crate agreement test as the hooks' only sanctioned use, and say so from each
side.
The monotonicity evidence chain gains only vocabulary-introducing events -- G2b
13c3d2f, G3a 6c5e69f, G3b -- and excludes G-minor and P13-S17 with the reason
stated: neither introduced an additive variant. The 2026-07-29 tie between G2b and
G3a is broken by ancestry, not timestamp.
P13-S18 (invariant 20's abstention residue) and P13-S19 (the pickup deferral) are
filed open by design. P13-S20 is recorded RESOLVED.
The genesis ladder G1 -> G2a -> G-minor -> G2b -> G3a -> G3b is CLOSED.
Executed against spec/CONTRACT_GENESIS_G3B_MEASURE.md rows 14a and 26-36,
mutation M71, which is now the contract's own guard: deleting the G3b Revision
History row fails the history test even though "genesis tranche G3b" still appears
twice in neighbouring prose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
CreateStaffGroup (35), CreatePartDefinition (36), CreateAnalysisLayer (37),
CreateView (38) -- all schema major 0, all schema-minor epoch 11, on
CreateStaff's set-union mint pattern with byte-identical re-carry
idempotence. Completes the four remaining root-level Score entity vectors.
No epiphany-bundle change of any kind: schema_major() gained NO arm (all
four fall through the catch-all _ => 0), so the op-block accept-set stays at
3 where G2b left it. The OperationKind golden lock stays at [(OperationKind,
u8); 30] -- kinds 35-38 remain outside it by design, per P13-S15's own rung.
Closes the satisfiability half of the from-empty defect: every G3 object
kind previously became Live only through base ingest, so CreateStaff's group
precondition was unsatisfiable from empty. It does NOT make a consistent
group authorable -- §1.1 disposition B rules Staff.group the sole authority
and StaffGroup.members a non-authoritative projection G3a stores without
maintaining, with both stale forms permitted. That ruling now reaches all
four normative sites: both field doc comments (neither field was documented
before), core_spec.tex, and both the new CreateStaffGroup and the existing
CreateStaff sections of operation_catalog.tex.
Carried-value maps for all four families thread through seven sites each,
including the base seed that fails silently -- a re-carry test reducing only
from empty cannot see a missing seed, which is why t5b reduces onto a
populated base.
Verified independently, not relayed. Full gate re-run: 1429 tests / 0
failed, clippy 0, fmt clean, git diff --check clean. Two mutations the agent
had not observed were run here and observed: reverting COMPANION_VERSION to
0.11.0 kills t11 (the agent had only reasoned this), and skipping the
staff_group_values base seed kills t5b. Both reversed by editing back, with
the baseline green.
Two findings recorded rather than silently absorbed:
- valuegen::analysis_layer's name format was accidentally exactly 16 bytes,
the same width as AnalysisLayerId's encoding, which made a field-swap
mutation on the two-field AnalysisLayer struct byte-invisible. Shortened
and documented -- a fixture that makes a mutation undetectable is the same
failure class as a guard that cannot fail.
- binary_format.tex has NO Revision History row for G2b. G2b's own contract
required one (touch row 27: "version, Revision History row"); it changed
the payload-layout row, tag row, accept-set text and §2373 history but
never bumped the version or added the row. The document went 0.12.0 (G2a)
-> 0.13.0 (G-minor) -> 0.14.0 (G3a) with the accept-set raise unrecorded.
G3a adds its own row without retroactively backfilling G2b's.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Kind and tag 34, schema major 3, minor epoch 10. The last rung before G3, and
the one that closes P13-S13: the tuning context becomes operation-authored, so
it finally has a canonical carrier. The closure argument is the metadata
precedent, not the canonical base - the base embeds no graph values for any
field, and metadata has been durable purely through its operations since M2d.
The payload carries epiphany_core::TuningContextSettings, a five-field subset
of ScoreTuningContext, not the full graph type. ScoreTuningContext's codec
deliberately drops accidental_extensions, so a full-value payload would have
diverged between a live session, where accept stores the envelope as a value,
and the same document reloaded, where the field decodes empty. canonical_value!
could not have caught that: it compares bytes and never the originating value,
so a field that never reached the bytes is structurally invisible to it. The
subset makes the divergence unrepresentable instead of relying on a
normalization step nothing can enforce, and it costs no wire design - the
encoding is byte-identical to the existing five-field walk, which
tuning_context_settings_canonical_bytes_match_score_tuning_context asserts
directly. Reduction leaves accidental_extensions untouched.
SetTuningContext is the sole genesis payload born at major 3, because minimal
stamping is a function of each payload's value, so the accept-set raise is
charged to this one surface: OperationEnvelopeBlock 2 to 3. The doc comment
above it did not merely record the cap, it asserted that no operation payload
embeds the tuning context - a sentence this rung falsifies - so it is rewritten
rather than left beside a corrected constant.
Undo restores the seeded base settings, default or not, and the
never-authored versus authored-to-default distinction stays unobservable. An
earlier draft of the contract had that backwards; PLAN_GENESIS_OPS section 5
trap 5 withdrew it, and SetMetadata is the disproof.
Fixes two undefined references the interrupted run had not yet reached:
operation_catalog.tex referenced sec:evolution:major3, a label defined in
binary_format.tex, which LaTeX cannot resolve across documents. Replaced with
the sectionsc convention already used for every other cross-companion citation
in that file.
Gate: 1409 tests, clippy 0, fmt clean, conformance 8/8 including [7f], both
vector corpora regenerated, all four PDFs at 0 undefined references. The t5 and
t7 mutations were re-run independently and observed to fail as specified; the
remaining eight are not signed off, because the implementing run was stopped
before it reported them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
SetCanvasLayoutDefaults and SetSpellingPrecedence, kinds and tags 32/33, on the
SetMetadata LWW pattern: advisory last-writer-wins, no conflict, no idempotence
short-circuit, seeded from the base so a value-restoring undo of the first
operational write reaches the pre-operational value. Both carried types already
had Codec impls, so the packet designs no wire layout -- canonical_value!
delegation and one push_lp_bytes each, inheriting strict canonical-form
rejection rather than writing it.
Both stamp schema major 0 unconditionally, staying in the catch-all arm. The
op-block accept-set is untouched at 2; epiphany-bundle has an empty diff. The
raise to 3 belongs to G2b, and the chunk schema minor -- a MUST no writer has
ever honoured -- belongs to G-minor, filed as P13-S14 and deliberately not
fixed here.
Most of this commit is documentation the vocabulary had outgrown. G1 shipped
five normative falsehoods because its contract declared the wire surfaces out
of scope, and Push 4a left TransposeInterval out of both of core_spec's
normative listings. So binary_format's payload-layout and tag tables now reach
33 rather than 30, its snapshot-only bullet records that it is empty and why,
two spelled-out payload counts move from eight to nine, and core_spec's
listings gain all four missing kinds. The Pass-12 K8 doctrine is narrowed
rather than reversed: the root and canvas remain structural givens no operation
mints, and only the claim that the score's contents arrive outside the
operation set is superseded.
Two generators were stale by two tranches and fed corpora other suites treat as
exhaustive. layout_stub's is now derived from PAYLOAD_FREE with an explicit
Registered append instead of a hand-written match, so future built-ins follow
structurally; s10 asserts PAYLOAD_FREE union {Registered} so deleting that
append cannot pass.
Companion 0.8.0 -> 0.9.0, binary format 0.11.0 -> 0.12.0, catalog 0.9.0 ->
0.10.0 (covering G1 retroactively). 102 decode vectors, regenerated.
Coordinator verification: 1371 passed / 0 failed, clippy 0 warnings, fmt clean,
conformance 8/8 and 9/9, requirement_labels 6/6, four PDFs at 0 undefined
references, goldens byte-identical, accept-set confirmed still 2. Mutations
s3/s5/s7/s10 re-run independently and each observed to kill its test.
Two findings beyond the contract. max_supported_major is not reachable outside
epiphany-bundle -- mod bundle is private and the symbol is absent from its
pub use -- so the gate's "assert it in code" was unsatisfiable without touching
a crate the contract forbids; verified by reading the unedited source instead.
And core_spec carries two P12-K8 references, not one; the contract named only
the first, so the second is annotated here for consistency.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Score::empty plus operations alone now materializes a note-bearing Score. The
chain CreateInstrument -> CreateStaff -> CreateRegion -> CreateStaffInstance ->
CreateVoice -> InsertEvent needed exactly one new link: CreateStaff already
demanded a live Instrument and nothing could create one.
Instrument is a root with no outbound references, so the operation carries no
referential preconditions -- only mint and byte-identical re-carry, on the
CreateStaff template. It designs no wire layout: Instrument joins
canonical_value! and the payload is one push_lp_bytes over the existing Codec,
so strict canonical-form rejection is inherited rather than written. Kind 31 and
tag 31 agree; schema_major is unconditionally 2 (Instrument's major-2 appends
are mandatory, not Option-hidden); bundle.rs is untouched and the op-block
accept-set stays 2, since that raise belongs to G2.
Two cross-cutting items the ruling required. Reduction now writes identity for
the first time, deriving next_counter from the log rather than trusting the
seed -- and the implementation is broader than contracted, covering minted
entity ids as well as operation ids, which is right: both burn counters. And the
from-empty path is pinned to reduce_operation_set_onto, since the base-free mode
skips referential preconditions by design; a test documents that asymmetry as
designed rather than as a bug to fix.
The contract's parallel-safety claim was WRONG and this commit corrects it.
Extending OperationKind is not containable to core+ops: Rust exhaustiveness
forces an arm in editor-core's barriers.rs, and because testkit depends on
editor-core, that one missing arm blocked conformance and requirement_labels
too. Three more downstream sites had 31 or a kind-count baked in as a literal --
layout-ir's barrier decode test, testkit's grammar vocabulary count, and the
textproj corpus generator. The subagent found the first two, reverted its
out-of-bounds edit, and reported rather than working around; the user authorized
the boundary crossing. Each literal now carries a comment saying it must move
with every tag append.
The text projection needed a companion bump, which the contract never
anticipated. Adding create-instrument to the kind production while holding
0.7.0 would leave two incompatible grammars claiming one version -- precisely
what the single-version gate exists to prevent -- so COMPANION_VERSION is now
0.8.0, the first kind appended since the header was gated. Cached projections do
not migrate and are not expected to: a TextProjection chunk is a non-canonical
accelerator, so a stale one is regenerated. The negative "wrong version" vector
had to flip, since 0.8.0 was the version it used as its future-and-therefore-
rejected example; it now names 0.7.0, which tests the deferred migrate-on-read
posture better anyway. Test headers that were literals now assert against the
constant.
Gate, all observed: fmt clean; clippy --workspace --all-targets 0 warnings;
1359 passed / 0 failed; requirement_labels 6/6; conformance 8/8 and 9/9 with
golden-gate, 96 decode vectors and 13 textproj vectors, every verdict agreed.
max_supported_major(OperationEnvelopeBlock) verified still 2. Both PDFs rebuilt.
Mutations i1, i3 and i5 re-run independently rather than taken on report: the
spine collapses to TargetMissing without the instrument, an unseeded
instrument_values misreports a base re-carry as RecreateContentMismatch, and a
seed-returning cursor yields 0 where 12 is required.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
RULING_GENESIS_PERSISTENCE.md §3 was the tranche's stated blocker. Ruled:
identity stays on Score and stays canonically encoded, byte-equality claims
confine to MaterializedState, and from-empty reduction derives next_counter
from the log.
Scoping it turned up that the section understated the problem. Verified in the
tree: epiphany-ops has no `.identity` reference at all, so reduction never
advances the cursor; invariant 11 checks only the reserved replica, never the
counter against ids present; and every mint from score.identity today is under
cfg(test). Score::identity is an authoring cursor reduction never touches, and
this tranche is what activates the hazard -- under from-empty the cursor sits at
the seed while the log already holds that replica's ids at 0..N. Divergent bytes
were the lesser problem, and none of the three options originally listed fixed
the larger one.
The manifest option, previously recommended, is rejected on evidence:
req:format:manifest-id promises two conforming writers derive identical
ManifestIds, which a replica-scoped field in a shipped content-addressed
structure cannot honour. The two wire options each cost schema major 4 on the
role 3b-i just froze at 3, and neither corrects the cursor.
Also reconciles two records against 011c68a. DECISIONS.md flatly prohibited a
SetTuningContext operation, which the ruling now requires; the prohibition is
marked superseded and re-scoped to what it was aimed at -- no tuning-only fix,
no wire widening to compensate -- both of which still hold. PASS13-S13 moves
from blocked-on to resolved-by, naming which of the four dispositions was taken.
Flagged for the tranche, not fixed here: bundle.rs documents the
OperationEnvelopeBlock cap of 2 with the tuning-context rationale in prose, so
that comment becomes false when the cap moves.
Gate: requirement_labels 6/6. No .md here is include_str'd or compiled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Ratified 2026-07-24. S13 is re-pointed rather than resolved: the tuning context
is not a special case, it is field 10 of an eight-field gap that
spec/ANALYSIS_GENESIS_PERSISTENCE.md tables across the whole Score —
canvas.layout_defaults, instruments, staff_groups, parts, tuning_context,
spelling_precedence, analysis_layers, views, plus identity, with
decomposition_attachments prunable-but-never-authored. Each is reachable only
through a base the reducer is handed, and pruning is licensed to replace that
base with a MaterializedState carrying none of them.
Independently re-verified against the working tree before recording: every one
of those fields' sole reduce.rs mention is a read-only base-seed read, and
epiphany-ops has no tuning-context payload at all.
So the entry drops the operation-vs-canonical-base framing it was filed with.
That framing was right about the axis and wrong about the scope: it maps onto
the analysis's dispositions B and C at eight times the size, and a
tuning-specific fix is exactly the shape the analysis argues against — "any
option that enumerates fields must be re-audited against this table every time
a field is added to Score". S13 is now marked blocked-on rather than open, and
whichever disposition the genesis ruling picks resolves it as a side effect.
The accept-set cost this entry surfaced is kept, repositioned as evidence
against enumerated per-field fixes rather than as a tuning-only tradeoff:
blocks stamp minimally, and bundle.rs caps OperationEnvelopeBlock at major 2
precisely because no operation payload embeds the tuning context, so a
SetTuningContext-style operation would drag a role accept-set raise along with
the kind append — for one field of eight.
The wire layouts stay frozen and correct under every disposition. This was
never about how the tuning context encodes, only about which carrier embeds it,
so epiphany-core's DECISIONS entry now records the matching prohibition: no
SetTuningContext operation, and no widening of ScoreTuningContext's layout to
compensate.
Docs only; no code path touched. Gate unchanged: fmt clean, 1336 passed /
0 failed, requirement labels 6/6 at 212/282/282.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Schema major 3 put `smufl` and `overrides` on the binary wire and left the text
surface alone. This closes that: `TextValue for ScoreTuningContext` projects
five fields in `fn enc` order, with four new leaf impls (SmuflVersion,
SmuflVersionRequirement, TuningScope, TuningOverride) mirroring the four Codec
impls 3b-i froze.
The rule that moved them is the one this projection always followed — the text
projection is the same canonical surface the binary codec is. What changed was
not the rule but a premise: the old doc comment excluded all three fields
because "no schema major 3 has been opened", and 3b-i opened it.
`accidental_extensions` was staged out of that major and stays in-memory only,
so it is still correctly absent — the staging line now falls in the same place
on both surfaces, which is what the inverted tests assert.
No header-version bump and no document vector moved. This is a value-projection
change, not a document-format one: epiphany-textproj has no reference to a
tuning context anywhere, and the canonical base projects as a single opaque
byte atom (req:textproj:base-snapshot-inline), not structurally. So
req:textproj:header-version's sole accepted version (0 7 0) is untouched, all
13 document vectors are unchanged, and req:textproj:roundtrip was never
violated — the tuning context is not part of the projected document.
Both tests were renamed and inverted, following 3b-i's treatment of the binary
pair rather than rewriting the tranche-2/3a decision entries. Mutation-verified:
making `overrides` project as an always-empty vector — the exact regression this
fixes — fails both.
Two stale-pointer classes fixed, both of the kind this pass was hunting: the
comment in codec.rs naming the old text-test names and claiming the text
surface was unchanged; and ScoreTuningContext's per-field docs in graph.rs,
which still read "In memory only this tranche" for `smufl` and `overrides` —
stale since 3b-i, and contradicting the struct-level wire note they point at.
The struct-level doc was already correct.
Filed P13-S13, found while scoping and deliberately not fixed here: the tuning
context has NO canonical persistence path. Its only *persisted* carrier is the
acceleration snapshot, which Chapter 8 makes non-canonical and regenerable; no
canonical carrier embeds it at all. No operation authors it (the ops
vocabulary's only `tuning` references are the per-pitch TuningReference::Inherit)
and MaterializedState does not carry it. So a per-voice tuning override survives
a snapshot round-trip but cannot be authored, replicated, or merged, which makes
req:tuning:tuning-resolution-order's scopes 2-4 unreachable from any exchangeable
document. Not a regression — nothing ever worked; major 3 made the gap visible
by giving the value a wire form and no way to reach it.
The candidate prices both routes. Ratification is about the carrier, not the
encoding — the layouts are frozen and correct either way — but the operation
route is not quite the free schema-minor it first looks: blocks stamp minimally,
and bundle.rs caps OperationEnvelopeBlock at major 2 precisely because no
operation payload embeds the tuning context. An authoring operation carrying a
TuningOverride would stamp v3, dragging a role accept-set raise along with the
kind append.
Gate: fmt clean, clippy 0, 1336 passed / 0 failed, doc 0, conformance 8/8,
requirement labels 6/6 at 212/282/282.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Closes P13-S12. epiphany-layout-ir defined its own SmuflVersion storing the
minor LITERALLY ({1,4} for SMuFL 1.4), so derived Ord sorted 1.3 and 1.4
BEFORE 1.12 — backwards versus SMuFL's real release order — on a direct field
of GlyphCatalogIdentity, which is layout-conformance identity. That bug was
live. The crate graph fixes the direction: layout-ir depends on core, so
core's fraction-normalized minor_centi type is necessarily the survivor.
layout-ir deletes its own and re-exports core's, keeping
epiphany_layout_ir::SmuflVersion resolving for downstream at zero churn.
No wire change: no schema major, no Codec, no decode vector. LayoutCache is a
regenerable major-0 role. The one line that moves bytes is encode_catalog's
.minor -> .minor_centi (04 00 -> 28 00 at offset 2, verified by encoding a
default identity: 60 bytes, unchanged elsewhere).
The tranche also closes S12's OTHER half, which had gone unnoticed: the
ratified shape reached Rust and the binary companion but core_spec.tex never
defined SmuflVersion at all — the exact "undefined leaf" S12 was filed about.
It is now declared once, with the hundredths rule as normative prose and the
release table as a rationale note, cross-referenced from both Chapter 9 sites
so the glyph catalog's version and the tuning context's are visibly one type.
No new req: label; counts stay 212/282/282.
Two ledger claims corrected, both verified false before dispatch rather than
after: PASS13_CANDIDATES.md's S12 entry and epiphany-core/DECISIONS.md's S12
bullet both promised this move would land "with golden regen". Nothing is
pinned to the catalog identity — every ResolvedLayoutIR::canonical_bytes()
assertion is RELATIVE (stability, determinism, and a sensitivity check that
mutates metrics_hash, never smufl_version), and the committed SVG/PNG goldens
embed no identity, only a fixed comment string. There was nothing to
regenerate, and conformance gate [9] never moved.
The fix is locked by a test that names the bug: layout-ir asserts the real
release order through GlyphCatalogIdentity.smufl_version. Mutation-verified
independently by making from_decimal store one-digit minors literally — it
fails with "minor_centi: 20 did not sort before minor_centi: 3", which is
precisely the deleted type's behaviour.
Gate: fmt clean, clippy 0, 1283 passed / 0 failed (+1, the new test), doc 0,
conformance 8/8, requirement labels 6/6 at 212/282/282. No golden, vector,
baseline, or editor-track file changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ScoreTuningContext gains `smufl` and `overrides` on the canonical wire.
`accidental_extensions` is deliberately STAGED to a later major: under
req:binfmt:frozen-layout a field addition is a major, so freezing a large
subtree whose consumer (the engraver) does not exist yet buys nothing and
risks a major-4 to fix a mis-shaped field. The three shapes 3a ratified
(Cents/CanonicalF64, AnchorPoint, SmuflVersion) stay reversible until
something exercises them.
Scope discovery: no operation payload embeds the tuning context, anywhere.
So the wire reached is the acceleration full-Score snapshot ONLY — the
canonical operation layer is untouched, no payload is born at v3, no frozen
v2 op-payload decoder is needed, and OperationEnvelopeBlock's accept-set
stays at 2 while Snapshot rises to 3. Major 3 is the first data-model bump
under which a chunk role's max does not move in lockstep. The canonical
base embeds no tuning context and stays major 0, byte-identical.
Migration: the live codec becomes v3; the 3-field form is frozen as
enc/dec_tuning_context_v2 and consumed by decode_v2_score (new, with its
byte-exact inverse encode_v2_score) and by the v0/v1 decoders — AND by
encode_v0_score/encode_v1_score, which the contract missed and which would
have silently corrupted the frozen forms through the strict-canonicality
re-encode check.
The frozen bytes are now pinned by a golden, because nothing else pinned
them. The cross-implementation decode corpus covers only the operation and
bundle surfaces — there is no epiphany-core vectors module — so every
existing test round-trips enc against dec and stays green under a
SELF-CONSISTENT reordering: swapping smufl and overrides in both halves
passed the entire workspace suite and 8/8 conformance, silently moving a
permanently frozen layout. schema_major_3_tuning_context_wire_bytes_are_frozen
asserts the exact encoding of the default (48 B) and a loaded (82 B)
context; it kills that swap. The S12 normalization is visible in the
literal: 1.4 stores minor_centi 40 (0x28), 1.12 stores 12.
Gate: fmt clean, clippy 0, 1282 passed / 0 failed, doc 0, conformance 8/8,
requirement labels 6/6 at 212/282/282 (unchanged — no new req: labels).
Mutations verified independently: breaking the v0 reroute kills 6 tests;
the wire-order swap kills the new golden.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The accidental / glyph / engraving type surface Chapter 4 puts on ScoreTuningContext
now exists in epiphany-core (new accidental.rs): ScoreAccidentalExtensions,
AccidentalDefinition, GlyphReference (Chapter 4's, recursive -- not layout-ir's
homonym), PitchSpaceModification, AccidentalEngraving with EngravingBoundingBox
and AnchorPoint, AccidentalCombination, SmuflVersion(Requirement), plus three
catalog_id! newtypes. All in memory, no Codec, canonical bytes unmoved -- the
reversible half; 3b freezes them on the wire.
The three ratified corrections land as filed:
- S10 Cents(CanonicalF64), not raw f64 -- reverting it to f64 is 9 compile
errors, the type system is the test.
- S11 AnchorPoint { x, y: SpaceUnit }, core-native, frame pinned in its doc.
- S12 SmuflVersion { major, minor_centi }, built only through from_decimal which
normalizes 1-digit x10 / 2-digit as-is, so derived Ord orders SMuFL's real
history right. layout-ir's SmuflVersion untouched; 3b unifies and moves
GlyphCatalogIdentity.
accidental_extensions and smufl join overrides as in-memory-only fields; the hand
codec's enc is byte-for-byte unchanged (three wire fields), only dec defaults the
new ones. The consumer that keeps this off the NOTEHEAD_ANCHORS path is real:
resolve_accidental (override > addition > base) and the
accidental-modification-compatibility invariant wired into check_invariants.
Glyph/engraving metadata is carried but its deep consumer is the engraver, out of
core -- said honestly, not faked.
Verified independently of the agent. Through Score::canonical_bytes: a non-empty
accidental_extensions + non-default smufl encode byte-identically to all-default
(268 both) and decode back to empty -- all three fields off the wire. SmuflVersion
orders 1.12 < 1.18 < 1.20 < 1.3 < 1.4 (the trap: 1.12 before 1.3). CanonicalF64
rejects NaN/inf. And the compatibility invariant is non-vacuous: weakening the
predicate myself made the edo-31 reject test fail. No Codec, no golden moved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The ten temperaments tranche 2 left Deferred now resolve, via
TuningResolution::Function with reserved built-in ids. Each is a
[FifthTempering; 12] descriptor over the fixed circle of fifths -- Pure,
NarrowPythagorean(f), WidePythagorean(f), NarrowSyntonic(f), NarrowSchisma, or
Residual (the closing wolf, computed as whatever brings the chain to seven
octaves). One walk places the notes and derives their ratios; the same code path
yields the wolf for the four non-circulating temperaments and full closure for
the six circulating ones. Comma sizes are 1200*log2(exact ratio), never
hardcoded cents. Still no Codec, no wire movement -- canonical bytes unmoved.
The closure invariant is recomputed in code, which is the whole reason this was
its own pass. Six circulating temperaments sum to one Pythagorean comma
(23.460010c); four non-circulating wolves match the ratified residues
(pythagorean 678.495, meantone 737.637/725.809/717.923) within 0.001c.
Verified independently of the agent. Through the public resolver, pythagorean,
kirnberger-ii and kirnberger-iii reproduce the spec's derived-cents tables to
0.01c -- and the Kirnberger tables only close WITH the schisma fifth, so the
match is end-to-end proof the schisma is present and the S6 trap avoided. The
kirnberger-ii/iii D discriminator holds (203.910 vs 193.157), meantone-1/4's
major third is the just 5/4 (386.314c), werckmeister-iii's C# is distinct from
tet-12's. And the closure test is non-vacuous: dropping kirnberger-ii's schisma
fifth myself made it fail -- the S6 defect reproduced as a mutation and caught.
TuningParameters is a documented zero-field marker (no built-in parameterizes
Function, its schema is unspecified); an unknown TuningFunctionId fails closed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A pitch now resolves to a frequency. The tuning-resolution vocabulary
(TuningSystem, TuningResolution, TuningOverride, TuningScope), a partial built-in
catalog, and the five-scope resolver land in epiphany-core, entirely off the
wire -- the reversible half of the remaining tuning work, exercised and proven
before the schema-major-3 bump freezes anything.
Nine of twenty systems resolve: the six tet-* (EqualTemperament, each paired to
the pitch space of matching chromatic cardinality -- tet-12/cmn-12,
tet-N/edo-N), and the three ji-static-5limit-{C,G,D}, whose twelve ratios are
COMPUTED from the lattice block {3^a 5^b | a in [-1,2], b in [-1,1]} in exact
integer arithmetic, never a pasted table. The other eleven fail closed with a
distinct NotYetSupported (vs UnknownTuningSystem): the ten historical
temperaments await tranche 2b, ji-adaptive-5limit awaits HarmonicContext.
overrides is added to ScoreTuningContext as the one field the scope walk needs,
in memory only. Its struct_codec! -- whose dec constructs a literal of exactly
the named fields -- is replaced by a hand-written Codec that encodes the three
wire fields and defaults overrides on decode, with a matching hand-written
TextValue.
Verified independently of the agent that wrote it. Through the real
Score::canonical_bytes() path: a populated overrides encodes byte-identically to
an empty one (268 bytes both) and decodes back to empty -- the field never
reaches the wire. tet-12 A4=440 resolves C5 to 523.2511306011972 Hz by hand.
ji-static-5limit-C's major third is 386.3137c against tet-12's 400.0000c, the
5/4 just third distinct by the expected 13.69c. The wire-invariant test
mutation-killed by leaking the override count from the codec (the first attempt,
encoding the field itself, was a compile error since TuningOverride has no Codec
-- meaningless as a mutation, redone). No golden or fuzz digest moved.
Also corrected a doc comment on pitch.rs's sounding_equivalent that this tranche
falsified: it said frequency resolution is "not modeled in this crate", which is
now untrue.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Chapter 4 pitch-space vocabulary lands in epiphany-core as in-memory types
with a real consumer, the built-in catalog as PitchSpaceId -> PositionStructure
data, and the P13-S2 "cmn-12" string guard is replaced -- not preserved beside --
by structural resolution over that catalog. Dispatched as one vertical slice
rather than the plan's three steps, because a Chapter 4 type surface with no
consumer is the shape that produced Staff::default_clef and NOTEHEAD_ANCHORS.
No Codec impls, no struct_codec! additions, no Score/ScoreTuningContext field:
these types stay in memory so Ruling C keeps them free to change. Canonical
bytes are byte-identical -- no golden or fuzz digest moved.
Six of the thirteen built-in pitch spaces are underdetermined by the spec (the
three ji-* generators, and all of maqam-base/gamelan-slendro/gamelan-pelog);
they resolve to None and fail closed at every consumer rather than being
invented. SpellingParameters, whose shape the spec calls an open question, is a
documented zero-field marker for the same reason.
The plan's proof of life -- cmn-24 transposing end-to-end -- held at the core
layer and FAILED one layer up, invisibly to the tranche's own tests.
resolve_transposed_spellings took the transposed 12-TET semitone with a ? before
checking whether any spelling needed it, so the 12-chromatic-only gate refused
every cmn-24 transpose, even one with nothing to spell (measured: NoOp
TranspositionOutOfRange, value untouched). P13-S3-shaped: latent in code, made
reachable the moment the space resolved. Moved the ? to point of use; a
spelling-less cmn-24 now transposes, one with an authored spelling still refuses
(the pre-pass cannot write a 24-chromatic accidental). Two tests, each killed by
a different mutation.
Verified independently of the agent that wrote the core half: the "cmn-12"
string survives only in doc comments and fixtures, not control flow; the
proof-of-life arithmetic (B4 -> natural C5 in cmn-24, chromatic 22, 118 - 120
carrying to octave 5) recomputed by hand; the strictly-increasing third clause
of req:tuning:diatonic-chromatic-mapping mutation-killed; full gate green (fmt,
clippy 0, workspace 0 failed, rustdoc 0, conformance 8/8, requirement counts
unmoved at 212/282/282).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`epiphany-core/DECISIONS.md` had carried two Push-4a audit claims as
**unverified** through two passes, with the standing note that they "should be
checked, not inherited". Checked. Both are real, and both are Chapter 4 defects
standing in front of Push 4b rather than inside it -- which is the point of
checking before scoping, not after.
**P13-S5 -- the JI prime basis is specified at two lengths.**
`req:pitch:ji-vector-basis` says the built-in JI spaces order primes ascending
*starting with 2*, and that `components.len()` MUST equal the basis size. The
built-in pitch-space table calls `ji-5limit` "Two-dimensional (prime axes 3,
5)", `ji-7limit` three-dimensional, `ji-11limit` four-dimensional -- each
exactly one short, consistently, because the table is octave-reduced and the
requirement is full-register. `req:tuning:builtin-tuning-catalog` makes the
table normative, so a 5-limit vector is required to be both length 2 and length
3. The requirement's own octave-reduction clause does not reconcile them: it
normalizes the first component to a canonical range, it does not remove it.
Same family as P13-S2 and P13-I1, and cheaper -- a one-side edit once ratified.
**P13-S6 -- no built-in tuning system's resolution is pinned to a versioned
definition, and 14 of 20 have no definition at all.** The claim as inherited
("the named historical tunings lack exact deterministic ratio data") understates
it. It is not a missing table: `TuningResolution::Function` delegates the
historical temperaments to a `TuningFunctionId`, which Chapter 10 lists as an
*extension point*, and no built-in is mapped to a function id nor any function
id pinned. Only the six `tet-*` entries are actually specified, by
`EqualTemperament`'s structural rule. The other 14 are names: three meantone
variants, `werckmeister-iii`/`iv`, `vallotti`, `kirnberger-ii`/`iii`,
`young-ii`, `pythagorean` (the 3:2 ratio is named, the fifth-chain construction
and wolf placement are not), three `ji-static-5limit-*`, and
`ji-adaptive-5limit`. Set against `req:tuning:tuning-resolution-determinism`,
which requires determinism *across platforms*, two conforming implementations
may each choose a different published Werckmeister III and both pass -- in a
project that runs a cross-implementation conformance step.
The fix pattern is already in-house and was found by looking for it:
`req:pitch:spelling-algorithm` pins `SpellingAlgorithmId "default"` at version 1
to a named algorithm and errors on any other identifier. Nothing equivalent
exists for tuning. Which published variant each built-in denotes is a
ratification question, not an implementation one.
Both are filed open in the Batch-3 ledger. Neither was needed for 4a; both are
load-bearing for 4b, whose whole job is resolving a scale position to a
frequency.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cmn-24` was declared in the built-in pitch-space table as "CMN extended with
24-EDO quarter-tone accidentals" while Chapter 2 fixed a `Cmn` position's
absolute semitone at `nominal.chromatic() + alteration + 12*octave` for every
space, unqualified. Two ratified MUSTs, each correct in its own chapter,
jointly unsatisfiable -- P13-I1's two-listings drift in another costume.
The tracker parked this as "a data-model major". It is not.
`req:binfmt:frozen-layout` names `PitchSpacePosition` as one of four open
value-layer vocabularies, so three of the four available options cost no schema
event at all. The one that *is* a major -- regrading `alteration` onto a finer
fixed unit -- is also the only one that rewrites the canonical bytes of every
pitch ever authored, and it buys a single grid while `edo-31`/`53`/`72` stay
unrepresentable. It is explicitly ruled out.
Ratified instead: a `Cmn` alteration and a `CmnChromatic` modification are
denominated in steps of the enclosing pitch space's chromatic layer. One rule,
not a special case for one catalog row -- it settles `cmn-24`, `maqam-base`,
and `PitchSpaceModification::CmnChromatic` together. `cmn-12`'s step is the
semitone, so every score in existence keeps its meaning and **not one canonical
byte moves**; `canonical_pitch_bytes` writes the space id before the position,
so a `cmn-24` E-half-flat and a `cmn-12` E-flat already derive different
`PitchId`s.
The defect was larger than `cmn-24`. Nothing bound a `Cmn` position to a
12-chromatic space: `ScalePosition { space: "edo-31", position: Cmn { .. } }`
constructed, validated, encoded and transposed, and meant nothing. Neither
`Pitch::transposed` nor `twelve_tet_semitone` ever read the space. Until Push
4b resolves `PitchSpaceId` to a `DiatonicOverChromatic` structure, both now
fail closed outside provable built-in `cmn-12` -- a capability check, not a
claim that the identifier defines the structure. False refusal for a
score-defined 12-chromatic space is accepted over silently wrong arithmetic.
No wire change. `TransposeRefusal::PitchSpaceUnavailable` is a new *diagnostic*
mapping to the existing `PreconditionFailureReason::PitchSpaceMismatch` (6);
appending a discriminant for a guard Push 4b must delete would have reserved a
permanent wire value for a temporary mechanism, in exactly the artifact the
fail-closed ruling exists to protect. Assignments 10-15 are untouched.
Operation Catalog 0.9.0 records the broadened case *and* amends the 0.8.0
rationale, which asserted that discriminant 6 never consults a pitch-space
registry -- true of the non-`Cmn` check, false of this one.
Two spec-side fixes found in review, neither visible to any test: the
`TranspositionInterval` listing had lost its `pub chromatic_steps: i32,`
declaration to a comment rewrite, leaving a one-field struct above the sentence
"Both components are load-bearing"; and `pitch.rs` still documented
`alteration` as "in semitones", contradicting the requirement landing beside
it. Both new requirements are now cited from the code that implements them --
the label checker catches cited-but-undefined, not the reverse.
Push 4b keeps the registry work and loses the contradiction. P13-S4 is the last
item open in Batch 3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`project` and `parse` for every value an operation payload can embed.
The codec macros now emit a `TextValue` impl beside the `Codec` impl, from the
same invocation: 116 types whose field order cannot disagree between the binary
form and the text, at zero call-site churn. That is the companion's own rationale
applied to code -- a rule cannot drift from the listing it reads, and two listings
of one struct is the drift P13-I1 already cost us. `struct_codec!` rebuilds through
a struct literal and `cstyle_enum_codec!` matches exhaustively, so a field or
variant added later fails to compile rather than silently vanishing from the text.
The other 44 types have hand-written codecs and so need hand-written projections.
Their field order is verified by a mechanical diff of the identifier sequence in
each `fn enc` against the one in each `project`; all 44 agree. This matters because
a `project`/`parse` pair that agrees with itself on a *wrong* order round-trips
perfectly -- neither the compiler nor any round-trip test can see it. The
neighbouring blind spot, a mistyped constructor symbol, is closed by
`textvalue_names.rs`, which recovers each type's Rust name from its derived `Debug`
and compares it against the symbol actually emitted.
Strictness turned out to need only one of its two layers, and mutation testing is
what established that. Every per-site check is live: the set/map strictly-increasing
walk, `RationalTime`'s lowest-terms compare before construction, the catalog-id NFC
intern-and-compare, and `EventArena`'s ascending-`EventId` walk. Every whole-value
`ensure_canonical` guard was dead -- `Tempo::new`, `ReferencePitch::new`,
`SpellingPrecedence::new` and `EventOrderingDAG::try_new` reject rather than adjust,
so an accepted value re-projects to exactly its input and the guard could never
fire. A probe confirmed `try_new` returns its input map unchanged. Helper and all
four call sites removed: a check that cannot fail invites weakening the real one.
Also moves `catalog_name` out of the grammar *test* and into
`operation_kind_tag_vocabulary!`, where the discriminant and decoder already live.
It was a hand-maintained list parallel to an enum -- the exact shape that has cost
this project four bugs.
Method note recorded in DECISIONS: the work list came from compiler errors, but the
compiler reports only the frontier. `AnchorOffset`, `VoiceSelector`, `PowerOfTwo`,
`OctaveOffset` and `NonZeroU16` were each hidden behind a type that had not compiled
yet, so the list must be iterated to a fixpoint.
Gate green -- clippy 0, 1109 tests, doc 0, conformance 8/8, no golden churn.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Audit finding 1, reproduced and fixed. Pitch::transposed did its arithmetic in
i32 while the interval's own components are i32, so intermediates overflowed:
diatonic_steps = i32::MAX panicked at `12 * new_octave`, chromatic_steps =
i32::MAX at `semitone + c`. TranspositionInterval::inverse negated i32::MIN.
The comment above the arithmetic even said "widen before arithmetic" -- it
widened i8 to i32, which is exactly not wide enough. All of it now widens to
i64, where the largest intermediate is bounded by ~3.7e9.
Refusing is the contract. Panicking on a value the public type admits is not.
I checked whether this was worse than a panic. The workspace sets
overflow-checks = true in release, but epiphany-core is a library and a
consumer's default release profile has them off, where these expressions wrap.
A 10.5M-case sweep of wrapping-vs-exact arithmetic (175 base pitches x 60225
interval pairs, edges plus random) found ZERO inputs where wrapping produced a
wrong Ok rather than a refusal. So this was a panic, not silent corruption, and
the audit's characterisation was exactly right.
inverse() now returns Option: -i32::MIN is not an i32. An interval whose
inverse cannot be written down is a fact about the type, and a caller composing
undo out of inverses must see it. Both regressions mutation-verified by
restoring the i32 arithmetic and the bare negation.
Also in this commit, two documentation corrections:
- The reducer's test-harness comment claimed both old transpose_* tests "now
reduce ONTO a base and assert the pitch value". They do not, and should not;
DECISIONS.md already said so. The comment now matches.
- The catalog's undo semantics for BOTH transpose kinds claimed value-restoring
undo recovers the pre-transpose pitch from the write chain. Neither kind
records into that chain, so this was false. The frozen Transpose reverts to
its honest pre-Push-4a statement -- undo does not negate it (P11-C8) -- and,
per the freeze doctrine, that is now pinned: making it record would change
what a stored {Transpose, UndoTransaction} history replays to. TransposeInterval's
paragraph states the requirement the next commit implements.
Gate: clippy 0, 30 targets / 985 passed / 0 failed, docs 0 under -D warnings,
conformance 8/8, zero golden churn, catalog rebuilds clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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
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 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
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>