Commit Graph

199 Commits

Author SHA1 Message Date
Levi Neuwirth 3b09595196 Genesis G1: CreateInstrument, and the from-empty spine reaches a note
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
2026-07-24 21:02:12 -04:00
Levi Neuwirth 24f8c8099a Editor T4-pre W2: the glyph outlines become a shared typed seam
A canvas tessellator needs typed vector paths, and the outlines were private to
render-svg as SVG `d` strings. They move to a new epiphany-glyphs crate, which
implements layout-ir's GlyphCatalog with a real render_data.

The seam was already designed and merely unpopulated: PathCommand,
GlyphRenderData, and GlyphCatalog::render_data all existed, and BravuraCatalog
returned None by deliberate documented honesty -- reporting Some would claim
render data that does not exist. Like W1, this packet fills a structure rather
than building one.

Typed paths are parsed from the `d` strings rather than regenerated, because
Bravura.otf is not vendored -- tools/ carries only the extractor and its OFL
notice, and the generated header pins source hashes verified at extraction
time. Equivalence is therefore proven rather than assumed: parse every bundled
glyph, re-emit, compare byte-for-byte. All thirty-seven round-trip exactly, so
the sanctioned coordinate-comparison fallback was not needed. The real grammar
is wider than the contract's summary claimed -- absolute V/H lineto shorthand
appears in twenty-three of thirty-seven glyphs, and numbers carry nought to
three fractional digits with trailing zeros stripped, never a fixed four. The
parser lowers V/H to LineTo and the emitter reconstructs the shorthand from
geometry.

Nothing rendered moves. The outline data is byte-identical to its previous home
(the only edit is BravuraOutline's visibility), render-svg keeps emitting the
stored strings and re-exports bundled_glyph_count and smufl_codepoint
unchanged, and BRAVURA_METRICS is untouched -- it feeds metrics_hash and hence
GlyphCatalogIdentity, which is inside the resolved layout's canonical bytes.
Verified: all eight reference fixtures produce byte-identical canonical layouts,
and all five GUI goldens are unchanged.

The cross-table test locks what glyph.rs asserted only in prose: every glyph's
declared metrics bbox contains its drawn ink, re-derived through true
cubic-bezier extrema rather than control points. Worst-case deviation across the
whole table is exactly zero, at noteheadBlack's left edge. Stated honestly, this
is a rounding-chain lock rather than an independent cross-check -- the same
extractor emits both tables and deliberately rounds the metric box outward so it
always contains the ink -- but it catches a unit-conversion error, a
regeneration mismatch, or a future inward rounding, none of which had a test
before.

Five of six mutations killed, all coordinator-re-verified independently. The
sixth is an honest survivor and a defect in the contract rather than the work:
it asked for proof that re-emitting from the typed form breaks the goldens,
which the round-trip pin makes a no-op. The goldens were re-proven live a
different way -- perturbing one glyph's stored path by one digit flips one pixel
of 279,992 and fails all five.

Gate: 35 suites / 1351 tests / 0 failed, conformance 9/9 with golden-gate and
8/8 without, requirement labels 6/6, clippy 0, cargo tree showing exactly one
dependency and no third-party crate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:54:30 -04:00
Levi Neuwirth 4e30fe8475 Contract G1: one operation, and the two things that make from-empty correct
The ladder is ratified, so this contracts its first rung. CreateInstrument is
the single missing link between Score::empty and a note -- CreateStaff already
demands a live Instrument and nothing can create one -- so one kind satisfies
the ruling's acceptance criteria 1 and 3. Instrument holds no outbound entity
references, so the operation needs no referential preconditions at all, only
mint and idempotence.

Enumerating every CreateStaff site turned up two surfaces PLAN_GENESIS_OPS
under-counted: v0.rs's V0OperationKind and migrate.rs in both directions. The
table now lists fifteen, each anchored to its CreateStaff analogue, and all
twenty-two line citations were checked against the working tree.

It also turned up a gap the plan missed. TypedObjectId::Instrument liveness is
already seeded from the base, but there is no instrument_values map, so
byte-identical re-carry against a BASE instrument has nothing to compare. Pin 8
adds one mirroring staff_values at all six of its sites and calls out the
snapshot/restore pair as the silent one -- missing it surfaces only under
undo/replay, which is why i3 tests that case specifically.

Pinned against the traps this project keeps hitting: schema_major is
unconditionally 2 (Instrument's major-2 appends are mandatory, not
Option-hidden, so the value-dependent arm shape would be wrong); the accept-set
stays 2 and epiphany-bundle is untouched, since the raise belongs to G2 alone;
kind 31 and tag 31 coincide by accident, not by rule, and only the tag half is
compile-enforced; and the decode vector is pinned to literal bytes because
round-trip locking cannot see a self-consistent reorder.

Ten tests, each with the mutation that must kill it, plus an explicit note that
i6 documents a designed asymmetry rather than guarding a bug -- base-free
reduction skips referential preconditions because it has no universe to check
against, and a later reader must not "fix" it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-24 18:04:30 -04:00
Levi Neuwirth a35f534235 Editor T4-pre W2: the glyph-asset contract, and three parallelism claims get finer
The W2 contract charters the shared typed glyph-asset seam Ruling A names as a
T4 prerequisite. Scoping it turned up the same shape W1 had: the seam is
already designed and merely unpopulated. PathCommand, GlyphRenderData, and
GlyphCatalog::render_data all exist in layout-ir, and BravuraCatalog returns
None by deliberate documented honesty -- reporting Some would claim render data
that does not exist. So the packet fills a seam rather than building one.

Two findings reshape it from the sketch carried in the W1 contract. Bravura.otf
is not in the tree -- tools/ holds only the extractor script and OFL.txt, and
the generated header pins source hashes verified at extraction time -- so
"have the generator emit typed paths alongside the d strings" cannot be
executed here. The contract replaces it with a dependency-free in-crate parser
over exactly the grammar the generator emits, and proves equivalence by
round-trip: parse every bundled d, re-emit, compare byte-for-byte, with a
sanctioned coordinate-sequence fallback that must be reported if used. And the
metrics table is conformance identity -- metrics_hash hashes (name, metrics)
pairs with values participating, and GlyphCatalogIdentity is encoded into the
resolved layout's canonical bytes -- so it is out of bounds entirely.

The test worth watching is the cross-table one. glyph.rs claims the metrics and
the outlines agree because both came from the same Bravura release; that is
asserted in prose and tested nowhere. The contract requires comparing each
glyph's real outline extent against its declared bbox, reporting the worst-case
deviation, and treating a failure as a finding rather than a reason to widen
the tolerance -- it would mean engraving reserves the wrong space for that
glyph, which is the bug class that twice bit the vertical metric.

Three parallelism claims are corrected in the same pass, all mine and all too
coarse. The plan and the ruling both said T1b's lease/save/single-writer
machinery could be contracted in parallel with the genesis work. That was
written before the ladder existed, and it is now per-rung rather than
unconditional: G1 needs no accept-set raise and never enters epiphany-bundle,
so T1b's bundle work runs beside it, while G2 spends the raise in bundle.rs
where T1b's single-writer enforcement also lands, so those two must not fly
together. Ruling B's blocker note also still described the identity disposition
as blocking; it is ruled, and now points at where.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 17:50:02 -04:00
Levi Neuwirth 439e1e2e14 Scope the genesis operation tranche: a ladder, and one very short first rung
The ruling names nine surfaces. Each op touches roughly thirteen places -- the
kind variant, four hand-written matches, the decode arm and its validation, the
reduction arm, both text-projection arms, validate, vectors, the fuzz
generators, and the catalog. Nine at thirteen is not one dispatch, so this plans
a ladder rather than a contract.

The load-bearing finding is that G1 is one operation. Score::empty gives
Canvas::default() and empty vectors, and the chain to a note is
CreateInstrument -> CreateStaff -> CreateRegion -> CreateStaffInstance ->
CreateVoice -> InsertEvent, of which only the first is missing. Every later
arrow already exists with graph-aware preconditions. So a single new kind
satisfies the ruling's acceptance criteria 1 and 3 and unblocks T1b.

It is also cheap. Instrument's major-2 appends are mandatory rather than
Option-hidden, so CreateInstrument is unconditionally born at major 2 -- exactly
where CreateStaff already sits, and the op-block accept-set is already 2. No
raise, no wire change. Checked every carried type's fields for the same
property: eight of the nine are major 0 or 2, and SetTuningContext alone drags
OperationEnvelopeBlock to 3, so the raise is spent once in G2, where S13 closes.

The tranche also designs no wire layouts. All nine carried types already have
Codec impls and already ship inside Score; CanonicalValue delegates to them
without introducing a layout, and its generated decode_canonical already
rejects non-canonical encodings, so each payload inherits strict-form
enforcement on the same seam the decode-vector corpus uses.

Traps recorded, including one that would bite quietly: OperationKind's
discriminant match is hand-written and is a DIFFERENT space from the
macro-guarded tag space -- they are not aligned, and Push 4a's four-green-lists
failure is why the tag half is a macro now. Also that bundle.rs documents the
accept-set cap with the tuning-context rationale in prose, so the comment must
move with the number.

Four questions need ratification before a G1 contract: delete/modify coverage
per family, the decomposition_attachments derived-not-authored call, the
measure/meter invariant, and the ladder shape itself.

All fifteen line citations verified against the working tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-24 15:25:25 -04:00
Levi Neuwirth ec17d06e77 The identity cursor is ruled, and three records stop contradicting each other
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
2026-07-24 15:17:55 -04:00
Levi Neuwirth 011c68a831 The operation set absorbs genesis
Ratified 2026-07-24, resolving Ruling B blocker (i). Pass-12 K8 is reversed:
every mutable field of Score becomes operation-authored, and a document is
Score::empty(identity) plus its envelope log. The instrument, staff, staff
instance, voice, event chain is authorable end to end.

The decision removes machinery rather than adding it. Every alternative kept
genesis outside the operation set and then had to pay for that: a new chunk
role, a manifest field, an immutability rule, and a merge or fail-closed rule
for a canonical payload with no CRDT semantics. Genesis state is edited —
instruments get added, page geometry changes, temperaments are chosen — and
each alternative made those edits single-writer, unmergeable, or impossible.
Concurrency is a first-order product commitment, so the exception was not
worth institutionalising in the format.

Scope is nine surfaces over two templates already proven in reduce.rs: three
LWW settings setters on the SetMetadata pattern (canvas.layout_defaults,
tuning_context, spelling_precedence) and six entity mint families on the
CreateStaff pattern (instruments, staff_groups, parts, analysis_layers, views,
and StaffInstance.measures), each with graph-aware referential preconditions.
Delete and modify coverage is left to the tranche contract rather than assumed,
since CreateStaff itself ships today with no DeleteStaff.

Measures are ruled authored rather than derived. TimeAnchor::Measure carries a
measure id that cross-cutting structures anchor to, so deriving measures from
the metric grid would make their identity a function of the meter and every
time-signature change would orphan the anchors pointing into them. The cost
accepted is that measure/meter consistency becomes an authoring obligation
backed by a graph invariant.

Three constraints are written in rather than left implicit. Pruning may not be
implemented until the canonical base carries graph values: a prune installs a
MaterializedState base whose effects are outcomes, not payloads, so nothing
rebuilds the score afterward — silent and total, and free to prohibit now
because no prune exists to break. The from-empty path must reduce through
new_onto with an empty Score rather than base-free, because the base-free mode
skips graph-aware preconditions by design and would silently lose referential
enforcement from the first operation. And the OperationEnvelopeBlock accept-set
raise 2 to 3 is spent once, so the new kinds land as one batch — this is a
different major from Push 4b's schema major 3, the Score and Snapshot role wire
that tranche 3b-i froze, and there is no free ride between them.

The analysis is corrected in place rather than rewritten, so the evidence the
ruling rests on stays readable. Two amendments: Measure is a ninth uncovered
surface the original table missed by scoring canvas.regions at container
granularity, and identity is promoted from a stated question to a blocking one
— IdentityContext is replica-scoped yet lives on Score and is encoded, so under
from-empty reduction two replicas with an identical log produce Scores
differing in an encoded field while the music is identical. That disposition
blocks specification of the tranche and is deliberately not ruled here.

Execution belongs to the Push-4b-class coordinated track; the editor track
consumes it. T1b's lease, save, and single-writer machinery does not depend on
the tranche landing and may be contracted in parallel.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 15:11:36 -04:00
Levi Neuwirth dd33b34f08 Editor T4-pre W1: the resolved layout stops discarding its own partition
ResolvedLayoutIR carried a page/system tree and three flat primitive arrays
with nothing joining them, so a canvas wanting to re-tessellate one damaged
system had to infer ownership spatially — ambiguous exactly where it matters,
at cross-system slurs and boundary-straddling strokes. The partition was
never missing, though: casting-off computes it (system_of_slot, stroke_system,
curve_system) and the fold into ResolvedLayoutIR dropped it. This publishes
it instead of inferring it, so the answer is exact.

ResolvedSystem gains PrimitiveIndices — u32 index lists into the layout's flat
glyphs/strokes/curves — and ResolvedLayoutIR gains an unowned bucket of the
same shape. No primitive is split, merged, reordered, or renumbered; the flat
arrays are exactly what they were. The partition is total and disjoint: for
each array, every system's list plus the unowned bucket covers 0..len exactly
once, tested directly rather than assumed from construction.

Unowned is a first-class bucket with a real producer. The stub solver resolves
no per-system geometry — one default-rect system per region — so it publishes
every primitive unowned rather than fabricating an attribution it never
computed. Cross-system primitives need no special case: casting-off already
splits a spanning stroke or curve into per-system segments carrying
SYSTEM_CONTINUATION_SYNTHESIS provenance, and each segment is owned by the
system it was split into, not by the source's.

Ownership is excluded from the canonical encoding, for the reason vertical_band
already is: it draws nothing, so two layouts differing only in it are the same
rendered layout and hash alike. Encoding it would make the fingerprint more
fragile than the rendering it fingerprints — a casting-off refactor that
re-partitions without moving a pixel would become a byte-level break for
something no renderer and no conformance claim can observe. Verified against
main: all eight reference fixtures produce byte-identical canonical layouts,
and all five GUI goldens are untouched.

One derivation, structurally. The quality census consumed its own copy of the
glyph-to-system rule, and so did vertical_raw's system_of_glyph closure; both
now read the published glyph_system. That left CastLayout::system_of_slot with
no consumer outside the casting pass, so it is removed from the published
struct — a future consumer cannot grow a third copy of the rule, because the
raw material is no longer there. The per-glyph answer travels; the derivation
does not.

Six mutations, each killed and coordinator-re-verified independently: dropping
an index from a system's list, coercing unowned onto system 0, an off-by-one
system index, publishing a constant attribution, attributing a continuation to
its source's system, and encoding ownership into the canonical bytes. The
fourth is the one that proves the single-derivation claim rather than asserting
it — it kills three quality tests, two of which survived it before
vertical_raw was migrated.

Gate: 34 suites / 1341 tests / 0 failed, conformance 9/9 with golden-gate and
8/8 without, requirement labels 6/6 at 212/282/282, clippy 0, five goldens
byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:47:31 -04:00
Levi Neuwirth 962eab3ac5 Editor T4-pre: the IR readiness contract, and the genesis-persistence table
Two documents, both prerequisites rather than implementations.

CONTRACT_EDITOR_T4PRE_IR.md charters the layout-IR readiness tranche Ruling A
names as its prerequisite: per-system primitive ownership (W1), the shared
typed glyph-asset seam (W2, contracted but not dispatched), and the text-run
primitive decision (W3, an analysis packet). Its central finding reframes the
work — casting-off already computes the ownership partition and the fold into
ResolvedLayoutIR drops it on the floor, so W1 is "stop discarding it", exact
rather than heuristic. Eight design pins, six named mutations, and a
byte-neutrality gate that keeps the tranche compatible with the Push-4b
freeze.

The contract survived a source-level review that found one real defect: the
blast radius audited ResolvedLayoutIR literals but not ResolvedSystem's, and
pin 1 adds a field to both — editor-core's two-system hit-test fixture is a
full literal with no spread, so the packet could not compile the workspace
without it. Amended, along with the SystemId claim (no *production*
constructor; testkit's random cache generator has one), the stub solver's
total provenance aliasing, and the stroke-continuation citation.

ANALYSIS_GENESIS_PERSISTENCE.md is the field-by-field Score table Ruling B
blocker (i) requires before it can be resolved. It decides nothing; it
establishes what is true. Eight of Score's twenty fields have no operation
that can produce them — canvas.layout_defaults (the printed page geometry),
instruments, staff_groups, parts, tuning_context, spelling_precedence,
analysis_layers, views, plus identity — and decomposition_attachments can be
pruned 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.

Three findings follow: the genesis root is unreachable from an empty base
(CreateStaff preconditions a live instrument; nothing creates an instrument),
the canonical base cannot carry what the base-only fields hold, and there is
no canonical wire path from bundle bytes to a Score at all. They are one
problem from three sides — the format has no representation for graph state
that no operation authors.

Four dispositions are laid out with costs: a canonical genesis block, closing
the op-coverage gap, promoting the canonical base to carry graph values, or
scope-limiting T1b. The recommendation is the last now and the third as the
destination, because only that one makes pruning safe for every field rather
than for an enumerated list, and T4b needs the same checkpoint mechanism
regardless.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 14:47:08 -04:00
Levi Neuwirth 91d44dbe65 P13-S13 defers to the genesis-persistence ruling
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
2026-07-24 14:23:36 -04:00
Levi Neuwirth 6d37f39826 Text-projection parity: smufl and overrides project, and a persistence gap surfaces
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
2026-07-24 14:02:31 -04:00
Levi Neuwirth 85d8af612f Editor T3: the note-entry caret [W1+W2]
The seam (editor-core): a session-local caret — (voice, position, entry
duration), never in the op log, undo leaves it advanced, a deleted voice
clears it — with enter_nominal/enter_pitch/enter_rest funneling to one
insertion core that reuses the pencil's make-room verbatim and advances
only on success. Octave inference is nearest-in-staff-steps to the
preceding note with a downward tie-break — proven unreachable for real
letter pairs (candidate octaves sit an odd 7 steps apart), so the
comparator is tested directly. midi_note_to_pitch (sharps-spelled v1
policy) is the testable core of MIDI entry; x_at_position mirrors
invert_x for the GUI's caret line, extrapolation-tested on non-uniform
anchors after the uniform fixture proved blind to the wrong-segment
mutation.

The GUI (editor-gui): N-toggled entry mode, mutually exclusive with
pencil via a pure toggle_exclusive; mode-gated letters A-G, R for rests,
arrow-key caret movement, the duration palette doing double duty; the
caret drawn full staff height at x_at_position from the staff's own
rendered strokes. G5 locks the entry loop: C-D-E-F entered at the caret,
each advancing position value-asserted as an exact rational before any
pixel, baseline user-reviewed. Twelve mutations killed across the pair,
coordinator re-verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 12:55:05 -04:00
Levi Neuwirth ca0dcee10c Editor T3: contract for the note-entry caret
The input-method-agnostic entry seam: session-local caret, naturals-only
letters with nearest-octave inference (downward tie-break), the pencil's
make-room semantics, the x_at_position forward map, and the testable core
of MIDI entry — the device listener stays T4.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 12:55:05 -04:00
Levi Neuwirth d96d79b8ba The decode vector corpus reaches the core score wire
The cross-implementation corpus (req:binfmt:decode-vectors) covered ops and
bundle but nothing from epiphany-core — the oldest and most load-bearing wire
in the repo had no vectors at all. 65 -> 93 vectors, 5 -> 19 surfaces.

This delivers a revision the companion had already ordered. binary_format.tex
asked for the corpus to extend to the representative layouts "which remain
round-trip locked rather than literal-byte locked" — a description, written in
advance, of the exact defect found in tranche 3b-i: swapping smufl and
overrides in BOTH halves of ScoreTuningContext's codec silently moved a
permanently frozen layout and passed all 1283 tests and 8/8 conformance. An
encoder and decoder that agree on the wrong order round-trip perfectly. Only
literal bytes can see it.

That mutation is now the mandatory regression vector
(core.score_tuning_context/overrides_before_smufl), and re-applying the swap
was verified to fail three ways: the corpus reports "declared reject, but was
ACCEPTED", the committed corpus goes stale, and the conformance suite fails.
Both the full workspace suite and conformance now catch what previously passed.

Almost no new machinery was needed: CanonicalValue was already public and
already did decode + finish() + re-encode + reject-on-mismatch, and already
covered four of the five representative layouts. RationalTime joins it (the
fifth, and the exemplar of the spec's own warning that an outer guard can mask
a lenient inner codec — it reduces to lowest terms on decode, so only
hand-written bytes reach that path), along with the five schema-major-3 types.
check() therefore calls real production API, not a harness-side wrapper: a
leaf reject vector exercises the code an external implementer must match.
Codec and Reader stay pub(crate); the only new public item is `pub mod
vectors`.

Two things deliberately shaped: core is appended LAST in the testkit chain, so
the regenerated corpus diff is purely additive — 56 insertions, 0 deletions,
which PROVES no existing vector's bytes moved and therefore that no existing
wire form changed. And the per-major Score vectors assert canonical_bytes()
equality only at major 3: majors 0-2 migrate by design, rewriting the bytes,
and their canonicality at their own major is already established by the frozen
per-major encoder's re-encode guard. Comparing there would fail on every
vector and invite "fixing" it by gutting the vector's meaning.

Also rebuilds binary_format.pdf, which commit 68b08ad should have carried and
did not — every other binary_format.tex commit includes it. The rebuild picks
up both that tranche's Schema Major 3 section and this one. latexmk -xelatex
twice: 0 errors, 0 undefined references, 60 pages.

Gate: fmt clean, clippy 0, 1321 passed / 0 failed, doc 0, conformance 8/8 with
gate [7d] reporting 93 vectors, requirement labels 6/6 at 212/282/282 (no new
req: labels).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 11:38:15 -04:00
Levi Neuwirth 62117d3426 Editor T2 W4b: slur/tie replay on paste, and the clipboard reaches the GUI
Paste now re-mints contained slurs and ties as fresh CreateCrossCutting
ops — fragment ordinals mapped to freshly-minted destination EventIds,
ids from new mint_slur_id/mint_tie_id under the same three-source
high-water discipline, inserts ordered before cross-cuttings because the
reducer preconditions live anchors — all inside the single paste
transaction, with the emitted-stream test tightened to the exact member
count and its kill re-proven against the per-op-commit mutation. Tie
replay is unconditional: pitch_pairing None is the model's own
ascending-PitchId default, not a gap (investigated, not assumed). The GUI
gains Ctrl/Cmd+C / Ctrl/Cmd+V and toolbar Copy/Paste over egui 0.29's
actual clipboard surfaces (copied_text out, Event::Paste in — verified
against vendored source), pasting over the selection anchor. Goldens
byte-identical; all mutations killed and coordinator re-verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 10:27:08 -04:00
Levi Neuwirth 56950f8d7c Push 4b: ji-adaptive-5limit resolves, and the blocker was never HarmonicContext
The last fail-closed entry in the built-in tuning catalog now resolves, leaving
only the compatibility-mapping registry open in Push 4b.

The recorded blocker was wrong. Five places in tuning.rs claimed adaptive
"needs HarmonicContext, which does not exist in Rust". But
req:tuning:adaptive-default-version makes version 1 a pure function of
(position, anchor pitch class) — it ignores concurrent, recent, hints,
parameters, and mode. And two of HarmonicContext's four specified fields are
UNIMPLEMENTABLE: key_context and hints are typed on KeyContext / ContextHint,
which core_spec.tex:4111 leaves undefined deliberately, "so defining them now
would freeze a type surface on a chapter with no consumer". The minimal
one-field shape was not a preference; it was the only implementable one. Each
remaining field arrives with the first function that consumes it.

The real blocker was structural and small: locate_voice returned
(RegionId, StaffId) while key_sequence lives on StaffInstance, so the resolver
threw away the only object that could answer "what key is prevailing here".

No new pitch math. ji_static_5limit_ratios already took a runtime anchor — the
three ji-static-5limit-{C,G,D} built-ins are that one function at anchors
0, 7, 2 — so adaptive v1 is that same call with a derived anchor. Verified as
an identity: adaptive at anchor 0 is bit-identical to ji-static-5limit-C and at
anchor 7 to ji-static-5limit-G, across all seven naturals.

A missing tonal centre is NOT an error. core_spec.tex:3452 mandates C
(chromatic position 0) when none is supplied, so that is a defined default, not
a fail-closed case; a test pins it against a future "fail closed" refactor.
Fail-closed applies to exactly two things: an unregistered
AdaptiveTuningFunctionId (hard error, no fallback) and a TimeAnchor that cannot
be unambiguously ordered against the onset (AnchorNotOrderable, naming the kind
that defeated it). If any KeySignatureChange in a sequence is unorderable the
whole derivation fails, rather than risk skipping the true prevailing signature.

Anchor arithmetic is (7 * fifths).rem_euclid(12), never %: fifths reaches -7 and
% yields a negative pitch class. ChromaticPitchClass is a checked 0..=11 newtype,
so the sign bug cannot degrade into a silently wrong anchor — under mutation it
panics outright. Verified by hand across the whole -7..=7 range, including the
enharmonic collisions (6 and -6 both F#/Gb = 6).

Nothing reaches the wire: TuningResolution is catalog-computed and appears in
neither codec.rs nor textvalue_graph.rs, so TuningResolution::Adaptive and
HarmonicContext are in-memory only and schema major 3 is untouched. Zero Codec
impls added; no vector or golden moved.

Gate: fmt clean, clippy 0, 1311 passed / 0 failed, doc 0, conformance 8/8,
requirement labels 6/6 at 212/282/282. Independently verified: the C default,
static systems ignoring context, statelessness across reordered resolution, and
the 0..=11 invariant. Two mutations killed — rem_euclid -> % and a silent
fallback for an unregistered id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-24 10:17:46 -04:00
Levi Neuwirth 1dfa8b4f3a Editor T2 W4a: the clipboard fragment projection and copy/paste
Ruling E made concrete: a versioned s-expression fragment — values only,
reusing epiphany-core's own TextValue productions, no ids, no envelopes —
with named untrusted-input caps (1 MiB / 4096 events / depth 64) checked
before parsing and clean unknown-major rejection. copy_selection reports
closure drops (boundary slurs/ties) on CopyOutcome; a partially-selected
tuplet refuses; incidental rubber-band geometry (staff lines, stems) is
skipped, mirroring alter_selection's precedent. paste_at and
paste_over_selection mint fresh operations — InsertEvent + carried
RespellPitch — per-voice-ordinal lanes, make-room overwrite, one atomic
transaction; a second lane's refusal rolls the whole paste back
byte-identically. Contained slur/tie capture lands in the format; replay
on paste is W4b. Six worker mutations killed; coordinator review added a
seventh test pinning the emitted stream (one DeclareTransaction descriptor
plus members — descriptor-precedence at peers is canonical surface) after
its absence let a per-op-commit mutation survive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:36:13 -04:00
Levi Neuwirth f50f0ee52d Editor T2 W2: rubber-band selection in the GUI
Drag-to-select over the score view: a pure DragRect tracks the gesture, a
4.0pt movement threshold (under egui's 6.0pt click tolerance) keeps still
long-holds resolving as the clicks users intend — egui reclassifies them as
drags by elapsed time alone — and resolve_release dispatches rubber-band
vs click/toggle as a pure, unit-tested decision. Ctrl/Cmd-click toggles
membership; every member draws the existing highlight with the anchor
accented on top; pencil mode takes precedence and never starts a drag. The
T1a baselines are byte-identical: selection is overlay-side and never
touches the raster. Both mutations killed and coordinator re-verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:35:54 -04:00
Levi Neuwirth 51f7ac9f61 Push 4b tranche 3b-ii: one SmuflVersion, and a live ordering bug dies with the homonym
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>
2026-07-23 19:30:52 -04:00
Levi Neuwirth 68b08adb5e Push 4b tranche 3b-i: the score wire opens schema major 3
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>
2026-07-23 17:35:55 -04:00
Levi Neuwirth 456792debd Editor T2 W3: the goldens become conformance gate [9/9]
A golden-gate feature on epiphany-testkit: resvg + epiphany-render-svg as
optional regular dependencies (dev-dependencies cannot be optional; the
MSRV test job builds examples with --all-targets, so the raster stack must
stay out of the featureless closure — proven by cargo tree, 0 vs 1). Gate 9
re-derives the three T1a golden states headlessly and compares decoded RGBA
against the committed baselines; compare-only, failing loudly with a pointer
to the editor-gui diagnostic tests. Without the feature the suite's output
is byte-identical to before; with it, 9/9. The conformance CI job's
invocation gains the feature; CONFORMANCE.md documents gate [9] and the
split. Two mutations killed and coordinator re-verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 17:20:14 -04:00
Levi Neuwirth d27ea23ae1 Editor T2 W1: the selection set with an anchor
Option<Selection> becomes a paint-ordered set with an anchor member.
click replaces, toggle_at adds/removes (anchor = last added), select_within
takes the hit-test within(rect) in paint order deduped by score object —
a ledgered note's synthesized ledger strokes collapse into its notehead's
member, the representative id preferring the non-synthesized occurrence so
relayout cannot wrongly drop it. Single-target intents retarget to the
anchor, byte-identical at N=1. delete_selection batches atomically via one
transaction (a refused member rolls back the whole batch);
alter_selection instead mints ONE TransposeIntervalOp naming every target —
the format's native atomic multi-target primitive
(req:opcat:transpose-interval-atomic) — preserving N=1 op-stream
compatibility outright. Anchor fallback across relayout: next surviving
member after the anchor's original position, wrapping. Four mutations
killed and coordinator re-verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 17:08:38 -04:00
Levi Neuwirth 35bba767d2 Editor T2: contract for selection v2, the golden gate, and copy/paste
Four work packets: the selection set with an anchor (W1), GUI rubber-band
select (W2), promotion of the T1a goldens to conformance gate [9/9] behind
a golden-gate feature that keeps resvg out of the MSRV closure (W3), and
copy/paste over the newly granted Ruling E fragment projection (W4) —
values-only, paste-as-minting, fail-closed closure, untrusted-input caps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 17:07:18 -04:00
Levi Neuwirth 6be14d2122 Editor T1a: pixel goldens over the score raster the GUI displays
The repo's last unverified surface gets a gate. A decoded-RGBA golden
comparator (dimensions first, then raw pixels — never encoded PNG bytes,
proven by a re-encode-invariance test) with three-artifact failure output
and a reviewed-bless mechanism; four golden states over the real Engraver —
as opened, after a scripted pencil insert (target derived through the
session's own inverses, pitch/beat value-asserted exactly), after undo
(asserted equal to the as-opened baseline byte-for-byte, bypassing the
bless path so a broken undo can never bless itself green), and the slurred
casting-off fixture (system count value-asserted). Every state renders
twice and must match itself. Three user-reviewed baselines; rasterize()
split into a pixmap core so the goldens lock the exact surface the GUI
displays; one additive CI step uploads failure artifacts; DECISIONS.md
records the calls, including two engraving gaps locked knowingly (no clef
restatement on later systems; uneven system spacing).

Seven mutations executed and independently re-verified by the coordinator;
full gate green (fmt, clippy 0, workspace 0 failed, docs, conformance 8/8,
requirement counts unchanged).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:58:28 -04:00
Levi Neuwirth 0e59f545ba Editor track: the plan, and the T1a golden-harness contract
PLAN_EDITOR_APP.md charters the editor product track: rulings A/C granted,
B blocked behind graph-state-persistence + versioned-decode, D conditional
on the document-bound session API; hardened by three source-level reviews
(14 + 11 + 9 findings, all dispositioned in its ledgers).
CONTRACT_EDITOR_T1A_GOLDENS.md dispatches the first tranche: pixel goldens
over the score raster, subagent work packets, coordinator review, user
deep-dive points.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:57:43 -04:00
Levi Neuwirth b25b8debed Push 4b tranche 3a: the accidental vocabulary, in memory
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>
2026-07-23 14:48:29 -04:00
Levi Neuwirth 6c9d84f6f6 Push 4b tranche 3a: contract for the accidental vocabulary, and S10-S12 filed
Tranche 3 completes ScoreTuningContext onto the wire -- schema major 3, the only
irreversible tranche. Split reversible-first: 3a builds the accidental / glyph /
engraving type surface in memory with a real consumer (so the shapes are
exercised while free to change), 3b freezes them. 3a adds no Codec, no wire
movement; the hand codec keeps encoding exactly three wire fields while
accidental_extensions and smufl join overrides as in-memory-only.

Scoping the subtree surfaced three types that freeze forever and the spec does
not pin -- filed as S10-S12, ratified before dispatch (the S5/S7-before-tranche-1
pattern):

- S10: PitchSpaceModification::Cents(f64) is a raw f64 in canonical state, which
  the byte layer cannot encode -- serialize.rs decodes floats only through
  CanonicalF64::from_le_bytes. Ratified Cents(CanonicalF64), the same maneuver
  Ruling D applied to the bounding box.
- S11: AnchorPoint is referenced (:3166) and defined nowhere; core cannot depend
  on layout-ir. Ratified core-native { x, y: SpaceUnit } plus a normative frame
  sentence, since the box it anchors is "relative to the glyph's anchor point".
- S12: the catch. SmuflVersion is NOT undefined -- it exists in layout-ir with
  LITERAL minor ({1,4}) and derived Ord, so 1.3 sorts before 1.12 today, and it
  is a field of GlyphCatalogIdentity (conformance identity). Ratified
  fraction-normalized minor_centi (1.4 -> 40); core defines it in 3a, and 3b
  unifies layout-ir onto it and moves GlyphCatalogIdentity deliberately.

Every citation and struct line-ref verified against core_spec.tex before commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:55:19 -04:00
Levi Neuwirth c68f317e47 Push 4b tranche 2b: the ten historical temperaments resolve
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>
2026-07-23 12:31:29 -04:00
Levi Neuwirth e06b7e6f28 Push 4b tranche 2b: contract for the ten historical temperaments
Makes the ten temperaments deferred by tranche 2 resolve, via
TuningResolution::Function with reserved built-in ids, each computed from its
construction (which fifths tempered, by what fraction of which comma) rather
than a pasted cents table -- the constructions are normative in core_spec.tex
as of 5e465a1.

Its own pass, separate from the resolver plumbing, because this is the
verify-heavy part: the S6 draft shipped two arithmetically impossible
temperaments and one false ambiguity, every one properly cited, caught only by
the closure invariant. So the contract's deliverable is the closure recomputed
in code -- the six circulating temperaments' temperings summing to one
Pythagorean comma, the four non-circulating ones' wolves matching the ratified
residue -- and two construction-level mutations required to prove it bites:
dropping Kirnberger's schisma fifth (the classic omission) and swapping
Werckmeister's Pythagorean comma for syntonic (the classic confusion). The comma
type is the load-bearing distinction no test outside the closure check can see.

Verified before dispatch: all three cited labels resolve, every line ref points
where claimed, and the closure arithmetic checked independently -- syntonic +
schisma = Pythagorean exactly, which is why Kirnberger closes and why omitting
the schisma lands one schisma short.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:38:14 -04:00
Levi Neuwirth 6fa14c76e3 Push 4b tranche 2: tuning becomes resolvable, in memory
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>
2026-07-23 11:19:21 -04:00
Levi Neuwirth b0acacb951 Push 4b tranche 2: contract for the in-memory tuning resolver
The reversible half of the remaining tuning work: make tuning resolvable --
(position, tuning system, reference) -> frequency, walking the five resolution
scopes -- entirely in memory, no Codec, canonical bytes byte-identical. The
schema-major-3 wire bump is a separate later tranche, so the resolution logic is
exercised and proven before anything freezes, the same slice discipline tranche
1 used.

Scope is nine of twenty systems: the six tet-* (EqualTemperament) and the three
ji-static-5limit-* (PerPositionRatios computed from the lattice block, not a
pasted table). The ten historical temperaments are tranche 2b -- their own
verify-heavy pass, because that is where S6's two impossible-but-cited
temperaments hid and the code re-derivation deserves the same closure-invariant
scrutiny. ji-adaptive-5limit and the compatibility-mapping registry are deferred
fail-closed, matching the established honest-gap pattern (user ratified both).

Names the struct_codec! trap concretely: adding an in-memory `overrides` field
to ScoreTuningContext breaks the macro (its dec constructs a literal of exactly
the named fields), so the contract requires a hand-written Codec that encodes
the three wire fields and defaults overrides -- with a round-trip test proving
overrides never reaches the wire.

Citations verified line-by-line before dispatch; two were wrong on first draft
(tuning-resolution-order and -compatibility cited by the numbers of unrelated
requirements, one left hedged) and are fixed -- P13-S9's own discipline applied
to the contract that cites P13-S9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 10:06:25 -04:00
Levi Neuwirth 2cf2dae31b Push 4b tranche 1: pitch spaces become structure, and cmn-24 transposes end-to-end
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>
2026-07-23 09:41:18 -04:00
Levi Neuwirth 4a2ee312f7 Push 4b tranche 1: contract for the pitch-space slice
Types, the catalog data that fills them, and the consumer that reads them --
dispatched as one vertical slice rather than the plan's three steps. A Chapter 4
type surface with no consumer is the shape that produced Staff::default_clef and
NOTEHEAD_ANCHORS, so the acceptance test is behavioural: a cmn-24 pitch
transposes end-to-end.

Six of the thirteen built-in pitch spaces are underdetermined by the
specification -- the three ji-* generators and all three of maqam-base,
gamelan-slendro and gamelan-pelog. The contract requires they be represented as
unresolved and reported, never inferred.

Writing it also caught an incomplete paraphrase of
req:tuning:diatonic-chromatic-mapping in the contract's own text: it has three
clauses, not two, and both built-in mappings satisfy all three -- so a
constructor enforcing only the first two would pass every test built from the
catalog. That is P13-S9's discipline applied to the contract that cites it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 17:08:00 -04:00
Levi Neuwirth 600cc31be4 Pass 13: five ledger movements, one of them a repair to the ledger itself
S5 and S6 close, which closes the spec half of Push 4b: the JI basis is
full-register (a JiVector is an absolute position -- without the prime-2
exponent, ji-5limit cannot tell C4 from C5), and all twenty tuning systems now
carry constructions rather than names.

S7 is filed retroactively. Its id was minted in PLAN_PUSH4B_TUNING.md marked
"new; file it", ratified as that plan's Ruling C, and implemented in Wave 1a --
but it never entered this ledger, so an id lived outside the index whose whole
job is to be the index. The candidate itself: the specification required every
score to *define* a pitch space, and the data model gives a score no way to do
it. Both "define"s are now "select".

S8 is new and was measured, not argued. TempoShape::Constant legalizes
end_tempo as None *or* equal to start_tempo -- two spellings of one musical
fact -- and TempoSegment encodes the field positionally into canonical state.
Encoding both forms of an otherwise identical score gives 336 bytes and 363.
Nothing normalizes; invariants.rs merely permits both. Everywhere else this
project looked at redundant encodings it ruled the other way: NFC for text, a
total order for collections. Tempo got no equivalent, so two musically
identical scores hash differently. Found sideways -- the branch is untested,
which is why the map_or rewrite could not be mutation-verified there.

S9 is the pattern behind three failures in one day: the citation checker
proves cited-to-defined and never cited-to-relevant, so a citation that
resolves cleanly while supporting nothing passes a fully green gate. An agent
justified "resolved layout is non-canonical" with a requirement about f32; a
dispatch contract of mine put KeySignature in Chapter 10 when the Score Graph
is Chapter 5. And S4 is itself an instance, which is what turned this from
incident into candidate.

The asymmetry is the dangerous part and is recorded with it: a dangling
citation is loud, a wrong one is silent, so repairing the loud kind without
reading the target converts a caught defect into an uncaught one. That is
exactly how S4 happened.

Every line citation in the new entries was re-read against its target before
filing, S9's own discipline applied to S9.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 15:24:43 -04:00
Levi Neuwirth a421b95edf CI: lint policy belongs on one toolchain
The MSRV bump got the floor building, and the job then failed on clippy 1.85
demanding `.and_then(seg_pos)` where clippy 1.95 is silent -- the exact mirror
of the failure fixed one commit ago, where 1.97 rejected a bare `2.0` that
1.95 accepts. Two clippy versions are not a stricter gate than one. They are a
different gate, whose contents are the union of two lint sets that can point in
opposite directions, and which no developer can reproduce without installing
every toolchain in the matrix.

So lints run once, on the pinned stable, over the whole workspace including the
GUI crate. The MSRV job proves the floor compiles and passes its tests, which
is the only claim an MSRV makes. Nothing is lost by not linting at 1.85:
clippy::incompatible_msrv runs in the lint job against the declared
rust-version and is the check that actually catches "you used an API newer than
you promised" -- it reads the floor rather than standing on it.

The borrow is dropped anyway, since it is correct under both versions;
rustfmt then pulled the expression onto one line.

Also fixed by this restructure, though it never got the chance to fail: the
MSRV job had been running `cargo fmt --check` too, and rustfmt output drifts
across versions in the same way.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 15:14:14 -04:00
Levi Neuwirth e6a118e1c6 CI: an MSRV that could not build, and a gate that floated
First remote CI run went red twice, neither from the change that triggered it.

The declared MSRV was false. Cargo.toml claimed 1.77 and all ten crates
inherited it, but the committed lockfile carries blake3 1.8.5, which ships
edition 2024 and cannot be parsed by any Cargo before 1.85. The job died in
fifteen seconds, before compiling a line. The claim had simply never been
tested -- there was no remote to test it.

Raised to 1.85, which is measured rather than picked: over the Linux-target
dependency closure minus the GUI crate, the highest declared rust-version is
constant_time_eq at 1.85.0, pulled in by blake3 itself. The comment on the
field says so, and says not to raise it again to make a build pass.

Raising it woke six clippy lints that MSRV-gating had suppressed: map_or(true,
f) is is_none_or(f) as of 1.82. Rewritten. Five of the six sites are covered --
verified by flipping each to is_some_and and confirming a named test dies. The
sixth, invariants.rs:1335, survives the flip: nothing constructs a Constant
tempo segment with end_tempo None, so that branch has no test that can see it.
The rewrite is safe by the standard library's own equivalence, but the gap is
real and is not created by this commit.

The blocking jobs no longer float on stable. CI ran 1.97.1; this machine has
1.95.0 with no rustup, so the second failure -- float_literal_f32_fallback on
a 2.0 literal in the GUI crate -- was invisible and unreproducible here. With
-D warnings and a floating toolchain, a Rust release turns the gate red with
no repo change, and a gate that fails for reasons the developer cannot
reproduce stops being a gate. All four blocking jobs now pin PINNED_STABLE;
the nightly soak keeps floating, so drift surfaces on a schedule instead of in
an unrelated push. The GUI literal is suffixed anyway, so the eventual
deliberate bump is clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 15:05:04 -04:00
Levi Neuwirth 5e465a1067 P13-S6: all twenty tuning systems now say what they mean
Ten temperaments and the static 5-limit construction were bare names in a
catalog whose own requirement calls the surrounding semantics normative.
They now carry generative rules: which fifths are tempered, by what fraction
of which comma, where the wolf sits when the construction does not force it,
exact ratios, and the closure sum that lets a reader check the whole thing
without leaving the page.

The comma distinction is the load-bearing part and no test in this repo can
see it. Pythagorean for pythagorean, werckmeister-iii and -iv, vallotti and
young-ii; syntonic for the three meantones and both Kirnberger sets, each of
which also carries the schisma-tempered F-sharp--D-flat closing fifth whose
absence made two of these temperaments arithmetically impossible in the first
draft. Verified by recomputation rather than by re-reading: 2 x 10.753 +
1.9537 and 4 x 5.377 + 1.9537 both land on 23.4600 cents exactly.

ji-adaptive-5limit gets version 1 as the key-anchored static scale, identity
"default-v1" with the version inside the machine-visible string, hard error on
anything unregistered. The anchor derivation is pinned to (7 * fifths) mod 12
off the prevailing key signature -- and pinned twice over, because
key_sequence is per-staff and time-anchored, so the staff and the moment both
had to be named or two conforming implementations would disagree on a
modulating score.

The Forward References block stops claiming KeyContext is partially defined
somewhere it is not. It is defined nowhere, stays out of scope, and now
carries the one obligation that matters: whatever completes it must expose a
tonal-centre pitch class the anchor rule can use.

Three new requirements, 209 -> 212. Verified independently of the agent that
did the work: the count by grep, the count constant by mutation, the ten
comma types against the draft, the twelve lattice ratios against a generator,
and the ten deleted lines against the four sites they were supposed to come
from.

Closes P13-S6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:54:25 -04:00
Levi Neuwirth 80193855bb P13-S6 ratified: the lattice block, and an argument that did not survive arithmetic
All four open ratifications are decided, and the draft is corrected at both
places where it would have misled the agent that reads it next.

The 12-note 5-limit scale is the contiguous lattice block {3^a 5^b | a in
[-1,2], b in [-1,1]}, octave-reduced and assigned in ascending order from the
anchor. The draft called this one "asymmetric" and offered it third; that name
is an artifact of describing it as a 5x3 grid minus three discarded cells.
Generated from its bounds it discards nothing, fills all twelve chromatic
positions exactly once, and states in one sentence -- which is the property
Ruling B exists to get. The name is now banned normatively.

Two of the three candidates fell to computation rather than preference.
"Symmetric scale 2" makes G-D a 40/27 wolf, so a scale the catalog calls
"anchored to C tonic" would have an unusable dominant. And the draft's own
argument for "symmetric scale 1" -- that its D and Bb match C-based
Pythagorean and quarter-comma meantone -- is false: meantone's are 193.157
and 1006.843 cents, each half a syntonic comma away. It can only hold of
circle-of-fifths position, and even then covers 2 notes of 12 while the other
ten shift a comma between systems. Struck at its site and at the head of the
file, with the numbers, so it cannot be quietly reinstated.

The block also wins on merit: 23 pure intervals of 36 against 22 and 21, and
it gains pure Eb major and G minor for the price of Bb minor.

The three ji-static identifiers are one construction at three anchors -- which
the catalog already said, in the same phrase three times, more strongly than
the vallotti/young-ii naming parallel the draft reasoned from.

ji-adaptive-5limit version 1 is the key-anchored static scale, identity
"default-v1" with the version inside the string. Checking the prerequisite
turned up why that is affordable: KeyContext is not defined anywhere and the
spec puts its completion out of scope, but KeySignature { fifths } already
exists, is canonical, and is encoded -- so the anchor is (7 * fifths) mod 12
today, mode-blind exactly as ruled. C, G and D are fifths 0, 1 and 2; the
catalog's three anchors were never arbitrary.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:26:08 -04:00
Levi Neuwirth 986b90aef3 P13-S6: the tuning constructions, drafted -- and why sourcing was not enough
`req:tuning:builtin-tuning-catalog` MUST-resolves 20 tuning identifiers while
specifying only the six `tet-*`. Ruling B keeps all 20 and pins them by
**construction** rather than by cents table, because published sources agree on
constructions and differ mainly in rounding. This lands the draft of the other
14 as a **non-normative** artifact for review; nothing enters `core_spec.tex`
until the four open ratifications are decided.

Drafting rather than editing was deliberate, and it earned its keep. An agent
writing Werckmeister III from memory into a normative document is the
`NOTEHEAD_ANCHORS` failure exactly: hand-written, authoritative-looking,
unverifiable in-tree, and load-bearing.

**The first draft was well-sourced and wrong.** Ten entries, ten live citations,
zero recalled, zero unknown -- and three of them could not exist. A twelve-fifth
circle must absorb exactly one Pythagorean comma (23.4600 c). Checked:

  kirnberger-ii   2 x 1/2 syntonic, "remaining ten pure"  -> 21.5063 c
  kirnberger-iii  4 x 1/4 syntonic, "remaining eight pure" -> 21.5063 c

Both short by 1.9537 c -- exactly one schisma, the signature of a dropped
schisma-tempered fifth. The draft said of the first, in terms, "This closes the
circle." It does not. And `werckmeister-iii` carried the syntonic-vs-Pythagorean
comma forward as a "genuinely ambiguous" hedge into its summary table, when
closure settles it: only the Pythagorean reading closes, and a well temperament
that does not close is not one.

The supporting reasoning had been *correct* -- the derivation that C-E, G-B and
D-F# come out pure is right. The error was inferring from it that every
remaining fifth is pure. Making those thirds just does not discharge the full
comma. That is precisely the failure a citation count cannot detect and an
invariant catches in one line.

**Corrected and re-verified by recomputation, not by re-reading.** All six
circulating temperaments now close to exactly 23.4600 c; all four
non-circulating residues reproduce independently (`pythagorean` 678.495 c;
meantone 1/4, 1/5, 1/6 wolves 737.637 / 725.809 / 717.923 c). The schisma fifth
is sourced at F#-Db, and Kirnberger III's four fifths are now a direct quotation
rather than a reconstruction.

**The best result is a disagreement.** Recomputing Kirnberger II's thirds from
its own chain, the agent found its new source's prose claims four pure thirds
where the construction yields three -- F-A comes out at 397.067 c, not 386.314.
It reported the conflict instead of deferring to the source or dropping it.
Independently confirmed to the cent.

The count is still 10 verified / 0 recalled / 0 unknown, but `verified` now
means sourced **and** invariant-checked. The number did not move; its meaning
did. A stable metric hiding a changed reality is this session's recurring lesson
in another costume.

`CONTRACT_P13S6_TEMPERAMENTS.md` gains a permanent section, "Check the
arithmetic, not just the source", stating the general principle first so it
transfers: a citation proves the source said it, not that it is right, that you
read it correctly, or that you transcribed it completely. Five obligations --
closure; closure as a decision procedure for vague sources; non-circulating
temperaments must *not* close, with the wolf computed; twelve fifths each
exactly once; recompute every derived claim and treat disagreement as a finding.
Plus the guard that matters most: never adjust a construction to make the
arithmetic work and present it as sourced -- that manufactures a temperament
nobody published. An unresolved entry is the correct output.

Still open, surfaced not decided: the three `ji-static-5limit-*` scales and
`ji-adaptive-5limit`'s algorithm, the latter recommended to follow
`req:pitch:spelling-algorithm`'s versioned-identifier pattern.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:50:06 -04:00
Levi Neuwirth 5dcaa58fd0 Push 4b Wave 1a: three ratified corrections, and a citation that pointed the wrong way
Scoping Push 4b turned up three Chapter 4 defects and four rulings; this lands
the spec half of the first three. `PLAN_PUSH4B_TUNING.md` carries the full
scoping and all four rulings.

**P13-S5, Ruling A -- full register.** `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 catalog table called
`ji-5limit` "Two-dimensional (prime axes 3, 5)", `ji-7limit` three- and
`ji-11limit` four-dimensional -- each exactly one short, the table being
octave-reduced and the requirement full-register. The table moves, the
requirement does not: a `JiVector` is an absolute position, and without the
prime-2 exponent `ji-5limit` cannot distinguish C4 from C5. Each row now states
its basis explicitly so two readers cannot derive different ones.

**P13-S7, Ruling C -- a score selects, it does not define.** `ScalePosition`
pointed at "the score's pitch-space registry", which does not exist: `Score` has
thirteen fields and none is one, and `ScoreTuningContext` carries ids and
accidental extensions, never definitions. So `req:pitch:default-pitch-space`'s
"MUST define / MAY define" was unsatisfiable except by reading define as select.
The requirement moves to *select*, the comment names the built-in catalog, and
score-local definition is recorded as a deferred major with its reason: the
Chapter 4 type surface has never had a consumer, and freezing ~20
never-constructed types under `req:binfmt:frozen-layout` is permanent.

**Ruling D -- `AccidentalEngraving` could not be canonical.** It borrowed
Chapter 7's `BoundingBox`, built on `StaffSpace(f32)` -- correct for the
non-canonical resolved-layout cache, but this field hangs off
`ScoreTuningContext`, which *is* canonical, and
`req:determinism:canonical-floating-point` requires canonical stored floats to
be binary64. Chapter 4 now has `EngravingBoundingBox` over `SpaceUnit`, the type
`advance_width` already used. Chapter 7's `BoundingBox` is untouched.

That defect only surfaced because the first scoping was wrong and got checked.
It claimed `GlyphReference` and `BoundingBox` "exist but live in
epiphany-layout-ir" and recommended moving them down. They are **homonyms**:
Chapter 4's `GlyphReference` is `enum { Smufl(u32), Custom, Composite }`;
layout-ir's is `struct GlyphReference(Cow<'static, str>)`, a glyph *name*. A
move would have relocated the wrong types. The plan is corrected and there is no
crate move.

**Review finding.** The rationale as first written read "Resolved layout is
non-canonical (Requirement req:layoutir:staff-space-coordinates)". That
requirement mandates staff-space units and single precision and says nothing
about canonicality -- a true sentence resting on the wrong authority, the P13-S4
pattern again. No labelled requirement asserts resolved-layout non-canonicality;
it is prose in the Binary Format companion. Each claim now rests on its real
source.

`requirement_labels.rs` is untouched: no requirement was added or removed, so
209/279/279 held, which was the contract's invariant against scope creep. The
checker cannot catch a citation that resolves but does not support its sentence
-- it enforces cited-to-defined, not cited-to-relevant. Second instance today.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 11:49:40 -04:00
Levi Neuwirth b3449256f9 P13-S5 and P13-S6: the two inherited claims were both true
`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>
2026-07-22 09:03:39 -04:00
Levi Neuwirth e7ebd8ac9c P13-S2: the alteration is space-relative, and the core fails closed
`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>
2026-07-22 09:00:09 -04:00
Levi Neuwirth 043c18cabf P13-S1: every requirement is named, and now it is also numbered
168 of core_spec's 207 requirement blocks carried no `\label`, so no conformance
claim could cite them. All 207 are labelled now; the suite is 277/277.

Labelling alone would not have delivered a citable requirement. **No document in
the suite numbered its requirements.** `\newtcolorbox{requirement}` had no
counter, so a `\label` inside one bound to the enclosing sectioning unit and
`\ref` rendered a *section* number: core_spec said "see Requirement 2.5.4" where
2.5.4 is a subsubsection, and 61 of its 207 requirements shared a rendered number
with another -- one number, 5.6.3, was shared by six. Adding 168 labels to that
scheme would have produced 168 citable-but-ambiguous references. All six documents
now carry a real counter, numbered within chapter, and the box title shows it, so
a reader can see which requirement they are looking at. 277 labels, zero
collisions.

The counter is stepped with a `code=` key rather than tcolorbox's own
`auto counter`, and that is not a style choice. `auto counter` steps its counter
for `\label` purposes inside an internal `\sbox`, and `\refstepcounter`'s effect
on `\@currentlabel` is a local assignment discarded when that box closes --
before a `\label` written in the box body ever runs, which is how every
requirement in this suite is labelled. Measured on a three-box test document:
titles rendered `1.1 1.2 1.3` while the three refs resolved to `1.1 1.1 1.2`. The
idiomatic form would have shipped 207 silently wrong cross-references under
correct-looking numbers.

`requirement_labels.rs` locks all of it: every requirement block in every
`spec/*.tex` carries a label; labels match `req:<area>:<slug>`; the area matches
the chapter; labels are unique suite-wide; no `req:*` string cited anywhere in the
repository is undefined; and the counter is stepped where the label can see it --
a regression lock, because reverting to `auto counter` leaves every other check
green while the references break.

The citation check needed an escape. It cannot tell "cite this requirement" from
"name a label that does not exist", and documenting a dangling label is a
legitimate thing to do -- it had already rewritten a scoping plan's prose into a
euphemism to make itself pass. `DISCUSSED_NOT_CITED` carries the one such string
with its reason.

That string was the pass's other finding. `req:layoutir:vertical-bands` was cited
twice in the Pass-12 log and never existed. It should not be repointed at the two
*ownership* requirements: those govern which band a primitive belongs to, while
both entries describe the inter-staff solve realizing a band's declared *height*,
which no requirement governs at all. That is why the log invented a name. Both
citations now say so, and the gap is filed as P13-S4 -- shipped behaviour with no
governing requirement.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 18:27:52 -04:00
Levi Neuwirth a9a57120e9 Text Projection: whole-document conformance, and a corpus with teeth
Thirteen drift-locked document vectors -- four accept, nine reject, one per
implemented rejection class -- gated as conformance step [7e] beside [7d]'s 65
decode vectors.

The checker distinguishes three outcomes, and that distinction is the whole
point. `Rejected`, `AcceptedCanonical`, and `AcceptedNonCanonical` are separate
verdicts, and every one of the four wrong pairings is a failure: an accept vector
that merely normalizes fails, and a reject vector that is accepted -- whether as
canonical or after normalization -- fails. The decode corpus learned this the
expensive way, where collapsing "rejected" with "accepted but normalized" let a
corpus pass against the exact decoders it was written to catch.

Accept vectors assert the byte-checkable equation `project(serialize(parse(T)))
== T`, which quantifies over TEXTS. The comments say plainly that it must not be
replaced with bundle identity, and name the three deliberate binary losses --
duplicate blobs collapsing under derived-ordering, regenerated physical layout,
and dropped accelerators -- so the next reader does not "fix" it into a bundle
comparison and then chase a phantom.

Reach is asserted exactly, not merely non-zero: two documents each carrying an
extension, a canonical base, a custom profile, a lineage, and more than one
envelope; and every reject class must have exactly one vector the parser really
refused. A mislabelled reject drops its class to zero rather than collecting
credit from its label.

The corpus was verified by breaking the implementation, not the checker: making
`parse_document` silently tolerate a missing trailing LF is caught three ways,
and the suite reports "declared reject, but was ACCEPTED and normalized".

Review fixed one defect in the semantic harness. It claimed to check
`semantics(parse(project(B))) == semantics(B)` but compared `parse(project(B))`
against `document_from_bundle(B)` -- and `project_bundle` *is*
`project_text_document(document_from_bundle(..))`, so both sides flowed through
one function and any bug in it cancelled out. Dropping an envelope there passed.
It now compares against the envelopes the test itself serialized in, which is
independent of the projection path; the same mutation fails.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:44:25 -04:00
Levi Neuwirth 244bd6bd96 Text Projection: the document layer, and the equation closes
`epiphany-textproj` projects a bundle to its canonical text and reads it back:
the header, identity, profile, extension, canonical-base and blob productions,
plus the pipeline that joins them to the operation layer already in place.

Free functions, not `TextValue` impls. `epiphany-bundle` does not depend on
`epiphany-core`, so the trait and the manifest types are both foreign to this
crate and the orphan rule forbids the impls. That looked like it would force a new
dependency edge until the productions were read properly: no document line
contains a `value` position, so none is needed. The bundle stays independent of
the music model, as designed.

Blobs are handled on both sides and neither is the obvious one. The emit side
filters `blob_roots` through a real reachability predicate that today provably
returns empty -- never the field wholesale, which is the plausible wrong answer.
The parse side rejects any blob line outright, while still parsing the production
in isolation so both halves are ready together. A trip-wire source-scans
epiphany-core and epiphany-ops for `BlobId` and names both obligations when it
fires; it asserts it actually scanned files, so it cannot pass vacuously.

Two rejections were added on the agents' own reading of the spec rather than the
brief, and both are right. Envelopes must already be in canonical reduction order
(text_projection.tex:458) -- without that, two texts differing only in envelope
order would denote one document. And a hand-built `TextDocument` carrying blobs is
refused rather than silently dropped, since serializing it would produce a bundle
whose next projection loses those bytes.

Section order is enforced as a sequence, not a set: a repeated or out-of-order
section is a rejection, never something the parser sorts back into place.
`derived-ordering` sorts and de-duplicates blob lines and extension chunk roots by
*rendered* form, proved against fixtures that are unsorted in memory rather than
already canonical -- the blind spot that let five outbound normalizations ship
untested in the operation layer.

Serialization stages payloads and lets `commit` assign every offset, hash and
chunk id, which is exactly what `req:textproj:derive-or-carry` assumes; nothing is
hashed by hand. The emitted bundle deliberately carries no accelerators.

And the three directions compose: `project(serialize(parse(T))) == T` holds
end-to-end over the companion's own worked example. Eight checks re-mutated
independently of the agents' reports, each killed by its own named test; all eight
document productions diffed against the grammar.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:05:28 -04:00
Levi Neuwirth 50ad97a31b Text Projection 0.7.0: single-version headers, and blob lines are rejected
Scoping the document layer found that no blob can be canonical. A blob is
canonical iff referenced by a canonical operation or by canonical reduced state
(`req:textproj:canonical-blobs`; core_spec §"Canonical and Non-Canonical Manifest
Roots"), and nothing in epiphany-core or epiphany-ops references a `BlobId` --
there is no mechanism by which one can be reached. So today every real bundle
projects to zero blob lines.

The first instinct was to let a parser accept blob lines anyway, for forward
compatibility. That is wrong twice over. Forward compatibility is owned by header
gating -- a future writer's text carries a future version, which this parser
rejects at line one -- and a blob line accepted today would be staged into a
bundle that the next projection silently drops, losing data *and* falsifying
`project(serialize(parse(T))) == T` for that text.
`req:textproj:reject-unreferenced-blobs` therefore requires rejection, and the
conformance equation holds unconditionally over parse-accepted texts rather than
only over texts in the image of `project`.

`req:textproj:header-version` pins the other half: a parser accepts exactly one
header version, the companion's own. Multi-version acceptance and text
migrate-on-read are deferred in the same posture as op-payload migrate-on-read
rather than improvised.

Auditing the worked example against its own grammar found three defects in it: it
carried a blob line, which is now by construction an example of an *invalid*
document; its byte strings used literal ellipses, which `bytes ::= "#x" hexdigit*`
cannot derive; and its header still claimed 0.3.0. All three fixed, and the
preamble no longer promises elisions it does not contain -- an example that cannot
be parsed teaches the wrong lesson.

The version now appears in six places here. Two were locked; the two dangerous
ones were not, because they are *normative* -- a bump that updated the title and
missed them would leave the companion requiring parsers to accept a version it no
longer is. `requirements_name_only_this_companion_version` scans every requirement
block and holds any version literal to the title, deliberately exempting the
revision history, where old versions are the point.

Twelve grammar-gate tests, all mutation-verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:04:43 -04:00
Levi Neuwirth fdb4a57885 Text Projection: the operation layer, and the tests that can see it fail
Projection and strict parse for the whole Chapter-6 operation vocabulary: the 31
kind productions, the envelope with its stamp and causal context, the four
payload variants, and the sub-vocabularies for actions, undo policies, tuplet
compensation, cross-cutting values and position remapping. Written against the
grammar, per `req:textproj:operation-vocabulary`, calling `TextValue` only where a
production says `value`.

Names are generated, never spelled: `OperationKindTag::catalog_name()` comes from
the same vocabulary macro as the wire discriminant and the decoder, and `parse`
dispatches through an exhaustive match, so a kind added to the vocabulary and not
to the projector fails to compile.

Six sequences are order-constrained because their encoders normalize. Both halves
of each are enforced and both halves are tested, which turned out to matter. The
rejecting half was straightforward -- `TransposeOp.targets` mirrors the frozen
multiset exactly, rejecting a strict decrease while accepting a duplicate, and
getting that backwards would silently break a frozen operation's replay. The
*normalizing* half was written correctly by every agent and tested by none: every
fixture was already sorted, so all five outbound sorts survived deletion with the
suite green. The consequence was real -- with one removed, the projector emits
descending targets that its own parser then rejects, and that disagree with the
canonical bytes.

`textproj_conformance.rs` closes that. Each of the five builds a value unsorted in
memory, asserts the fixture pair really is descending before relying on it, then
checks the projection sorts, parses, and matches what encode-then-decode produces
-- pinning text and bytes to one normalization rather than merely to each other.

It also carries a structural injectivity sweep: 3319 mutants of projected
envelopes, of which 259 parse, all re-projecting byte-identically. That is
`req:textproj:roundtrip`'s second equation, and it is the evidence that no
whole-line re-project guard is needed -- every path that could normalize is
pre-empted by a per-site check. The sweep asserts its own reach, because one that
rejected everything would prove nothing. Deleting any per-site order check makes
it fail independently of the dedicated test.

And it locks the companion's worked example byte-for-byte. It was correct and
nothing kept it so, which is how a "machine-checked" claim became true of one run
and of nothing durable at 0.3.0.

All seven checks mutation-verified, each killed by exactly its own named test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:43:09 -04:00
Levi Neuwirth 4ded63a583 Text Projection 0.6.0: the operation vocabulary is grammar-directed
Scoping the operation layer surfaced a contradiction the companion had never
resolved. `req:textproj:value-projection` is a mechanical rule over structs,
enums and newtypes; the Grammar chapter separately spells out `envelope`,
`stamp`, `causal`, `payload`, `kind` and their sub-vocabularies. The two disagree.
Under the value rule an operation would read
`(insert-event (insert-event-op #x0a <event>))`, because an `*Op` record is a
named-field struct and so not a transparent newtype.

`req:textproj:operation-vocabulary` rules for the grammar, which is what the
companion already implied: the value rule's own preamble scopes itself to the
values an operation *embeds*, and the `value` nonterminal marks where it applies.
If the rule governed the vocabulary too, the grammar's productions would be
redundant and partly wrong.

An operation kind therefore inlines its payload record, and the requirement says
why rather than asserting it: the record exists so each variant can name a type,
and the binary form adds no bytes for it -- `OperationKind`'s encoding writes the
tag and delegates. A wrapper that adds no bytes adds no text. That is the same
argument clause 2 makes for newtype transparency, applied one level out.

Also removes a second name for one type. `transpose-interval` inlined its interval
as `(interval <d> <c>)` while `TranspositionInterval` projects as
`(transposition-interval <d> <c>)` at every `value` position. The production now
delegates to `value` and the special case is gone.

Two new checker tests lock both, bringing that gate to nine.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:42:21 -04:00
Levi Neuwirth b6728da5c6 Text Projection: every graph id projects, because graph_id! says so
`TransactionId` had no `TextValue`, which blocked the operation layer: it is a
core type, so epiphany-ops cannot implement a core trait for it.

The fix is not the missing line. `bytes_text_value!` was a hand-maintained list of
thirty ids sitting parallel to the `graph_id!` invocations that declare them --
the same shape that has cost this project four bugs -- and `TransactionId`'s
absence was that latent bug already biting. `graph_id!` now generates the
`TextValue` impl beside the id itself: every graph id is a byte-string leaf by
definition, so the list that declares them is the list that projects them.

`bytes_text_value!` keeps only the leaves that are genuinely not graph ids --
`ContentHash`, `TypedObjectId`, `ReplicaId`, `OperationId` -- shrinking from
thirty entries to four. A graph id added tomorrow gets its projection for free
and cannot be forgotten.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:41:40 -04:00
Levi Neuwirth cf81074ca0 Text Projection: the Chapter-5 value layer, from one field list
`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>
2026-07-21 12:17:49 -04:00