Commit Graph

83 Commits

Author SHA1 Message Date
Levi Neuwirth 4df8e257f3
P13-S27: accept reduction authority implementation 2026-08-09 15:05:58 +02:00
Levi Neuwirth bc06706e41 Container major 1: the format epoch carries base provenance
FORMAT_MAJOR becomes 1 and FORMAT_MINOR restarts at 0. The decoder
stops being exact-major-only: it classifies three ways through a named
FormatEpoch carried on FixedHeader, so major 0 is decoded deliberately
as legacy rather than refused. Old readers already fail closed on an
unknown major, so that half needed no mechanism — which is why the major
is the right carrier, and why the header's immutability, fatal to
FORMAT_MINOR as a provenance field, is what makes it sound as an epoch
field.

The matrix: a major-0 bundle with no base may open; one carrying a base
is refused; one attempting to add a base is refused and told to repack.
That last row is the non-inheritance rule. Three errors, none of which
degrades to read-only: two permanent legacy/repack errors, and
ReductionAuthorityUnavailable, which is temporary, names P13-S27, and
must not say repack — a major-1 container is already the right epoch.

Until P13-S27 lands, both major-1 base boundaries are closed: opening a
major-1 bundle that already carries a base, and committing one into it.
Neither may be left open while the epoch asserts a validation that never
ran.

Text projection cannot mint a base. serialize_document staged a carried
base into a fresh bundle and build_manifest wrote it, so an old or
hand-authored document could be laundered straight through the boundary.
All three sides now refuse: projection, parsing, and a new dedicated
SerializeError variant — none of which existed to be "retained".
COMPANION_VERSION moves to 0.14.0 and the corpus is rebuilt to 20
vectors and ten rejection classes, with canonical_bases reach dropping
2 -> 0. That is a real capability loss and is recorded as one.

Corruption keeps precedence in both epochs: a corrupt major-1 base fails
as malformed, never as the temporary authority error a user would
reasonably retry.

All 11 mutations were run and observed, not reasoned about. M4 is the
signing one — with the legacy commit refusal removed, a legacy container
gains a base in place, which is exactly the counterexample that killed
FORMAT_MINOR. M11 confirms the third error is distinct while test 3
stays green, proving the mutation stayed inside the major-1 branches. M7
fails on both epoch halves. M8 falls through to
SerializeError::Bundle(ReductionAuthorityUnavailable), confirming the
text layer's own refusal is what the test asserts.

Two touch-table gaps surfaced during execution, both the same shape: a
.tex requirement addition moves hardcoded counts in
requirement_labels.rs, and the companion bump moves a second normative
version literal spelled version~0.13.0 rather than (0 13 0). Neither
file was in any touch table; the second was caught only because a test
exists for exactly that failure.

P13-S27 is unblocked — its pin 2a is resolved from outside, as its own
prohibition required — and inherits three obligations: both interim
refusals converted to validation, M8's deferred laundering
demonstration, and pin 3c's two suspended conformance assertions.
P13-S16 remains blocked on S27.

Workspace green at 1569.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-08-07 12:51:14 -04:00
Levi Neuwirth 4f141ce197 P13-S21: the two precondition reasons the wire spec never named
PreconditionFailureReason 14 (AcousticRealizationPinned) and 15
(TranspositionOutOfRange) entered the vocabulary at Push 4a and reached this
document at neither place that owed them. The bounded enumeration ran 13 straight
to G3b's 16, and Push 4a's own history row recorded only OperationKind 30 while
saying nothing about the two reasons it appended in the same epoch. The Operation
Catalog documented both at its 0.8.0 and effect.rs has carried both throughout;
only the wire specification was silent.

This is P13-S20's specification-side twin, and it is why that decoder could stop
at 13 unchallenged: an implementer reading only the wire specification would have
built exactly that decoder and been right. No version bump and no new history
row -- this records an assignment normative since Push 4a rather than making one.

The regression test checks both sites, each bounded to its own longtable row,
because either alone is satisfiable by the wrong thing: the G3b row and the
enumeration both discuss PreconditionFailureReason at length, and an unbounded
search would go green the moment any row mentioned the names. Each half was
observed red alone while the other stayed green. The Push 4a row is located by a
version-free separator marker, per the file's standing rule against encoding a
document version number anywhere in it.

The epoch-12 evidence hash is e64a4b7, not this rung's parent. G3b landed across
six commits, and the chain records introducing commits -- the commit where
kind/tag 39 enters payload.rs and reasons 16-18 enter effect.rs -- exactly the
distinction the 2026-07-28 correction draws between 7df5ca1 and 55eff00 for G2a.
d58eee8 completes the rung and introduces no discriminant; both are named, with
their roles stated, in the plan and in the contract's pin 15.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-30 20:57:59 -04:00
Levi Neuwirth d58eee8063 G3b packet 3b: the documentation half, and the ladder closes
Kind 39 CreateMeasure and precondition reasons 16-18 reach the Binary Format's
kind table, tag table, payload layout, and reason table; graph invariant 20
reaches the core specification's enumeration, which now says twenty in all three
places it counts. Both normative listings in that document gain CreateMeasure --
earlier drafts of the contract named only the invariant, and prose fails silently.

Every version surface moves in pairs. Binary Format 0.15.0 -> 0.16.0, Operation
Catalog 0.12.0 -> 0.13.0, each with a changelog entry beside the title bump. The
Text Projection companion needed only the changelog: packet 1 bumped its header to
0.13.0 and stopped there, leaving the document claiming a version its own history
did not record. That was live from e64a4b7 until now, and no gate could see it.

Two public hooks exist that would otherwise look like leaks. epiphany-ops depends
on epiphany-core and never the reverse, so invariant 20 implements pin 6/6b's
comparable relation and musical delta a second time over the graph alone. Both
DECISIONS records name the divergence hazard that forces the duplication, name the
cross-crate agreement test as the hooks' only sanctioned use, and say so from each
side.

The monotonicity evidence chain gains only vocabulary-introducing events -- G2b
13c3d2f, G3a 6c5e69f, G3b -- and excludes G-minor and P13-S17 with the reason
stated: neither introduced an additive variant. The 2026-07-29 tie between G2b and
G3a is broken by ancestry, not timestamp.

P13-S18 (invariant 20's abstention residue) and P13-S19 (the pickup deferral) are
filed open by design. P13-S20 is recorded RESOLVED.

The genesis ladder G1 -> G2a -> G-minor -> G2b -> G3a -> G3b is CLOSED.

Executed against spec/CONTRACT_GENESIS_G3B_MEASURE.md rows 14a and 26-36,
mutation M71, which is now the contract's own guard: deleting the G3b Revision
History row fails the history test even though "genesis tranche G3b" still appears
twice in neighbouring prose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-30 20:45:27 -04:00
Levi Neuwirth 314cd7a80d G3b packet 2: graph invariant 20, and the operations that must preserve it
Invariant 20 checks measure/meter agreement and boundary consistency, and
nothing else: invariant 10 already checks that a measure's signature reference
resolves, so 20 never re-checks it. A None signature avoids only the agreement
clause -- inherited meter still governs distance. Where agreement or delta is
not computable the invariant ABSTAINS rather than failing closed, the deliberate
opposite of create_measure, because base-ingested data may predate the rule.
Pickup first measures are exempt.

epiphany-ops depends on epiphany-core and never the reverse, so invariant 20
cannot reuse packet 1's reducer predicates and implements the comparable
relation and delta a second time over the graph alone. Two implementations of
one normative relation is a divergence hazard, so a cross-crate agreement test
drives a table of anchor pairs through both and a one-sided perturbation signs
it.

SetMetricGrid and SetTimeSignature now refuse writes that would break either
clause for a live measure, and every check precedes the mint -- set_time_signature
previously minted its carried signature before writing the chain, so a refusal
appended afterward would leak a TimeSignature from a non-transactional operation
with no undo to reclaim it.

Undo restoration safety is evaluated in aggregate, because individually-unsafe
restorations can be jointly safe and the reverse. StrictInverse conflicts on the
whole set; BestEffort applies the maximal safe subset under a documented
canonical-order greedy. These are the first callers to pass overlapping
overrides into the grid oracle, so they meet packet 1's tie-break for real.

Both aggregate paths are signed end-to-end as well as by unit test: the measures
are created after the transaction commits, so nothing disagrees on the forward
path and only the restoration of the older grid conflicts. Deleting either call
site was previously invisible to the whole suite.

Executed against spec/CONTRACT_GENESIS_G3B_MEASURE.md, mutations M34-M47.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-30 15:13:07 -04:00
Levi Neuwirth e64a4b7103 G3b packet 1: CreateMeasure reaches the wire, the mint, and the grid oracle
Kind and tag 39 at schema-minor epoch 12, in both unaligned discriminant
spaces. Measure is schema major 0 -- struct_codec! is a plain walk and
TimeAnchor's Codec has no version branching -- so schema_major() gains no arm
and OperationEnvelopeBlock stays at 3.

CreateMeasure is a nested-container mint following CreateStaffInstance, not
G3a's root-level shape: it carries the owning StaffInstanceId beside the
value, and measure_values carries that parent because Measure has no
back-pointer and the graph-removal arm will need it. Append-only, with
referential preconditions on the parent, the signature, and every non-wall-clock
start referent.

The comparable relation is five exact shapes with an identical boundary
selector; ordering across Start/End is unsound while measure length is
unresolved, so it stays unverifiable. Boundary distance needs a musical delta,
which only same-referent same-selector Musical offsets supply. The effective-grid
oracle reconstructs inheritance from metric_grid_chain and meter_change_chain by
write recency -- not by always overlaying per-key on whole-grid -- folds in
prospective overrides, and runs identically in both reduction modes, so an
instance_grid ledger keeps base-free reduction honest.

Three precondition reasons at 16-18. Repairs a live bug found in review: the
materialized-effect decoder stopped at 13, so reasons 14 and 15 already encoded
without decoding, and the generator drawing below(14) could not see them.

Executed against spec/CONTRACT_GENESIS_G3B_MEASURE.md, mutations M1-M33,
M64-M66, M67-M70. text_projection.tex moved into this packet: four tests read
it live, so the companion bump cannot be split from the grammar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-30 12:19:32 -04:00
Levi Neuwirth 6170015a8c P13-S17: restore G2b to the Binary Format revision history
The Revision History chapter ran G2a straight to G-minor to G3a, with no row
for genesis tranche G2b anywhere -- so the accept-set raise
OperationEnvelopeBlock 2 to 3, the first accept-set move since G2a explicitly
recorded staying at 2, reached the normative tables and never the history.
G2b's own contract required that row; 13c3d2f edited 99 lines of
binary_format.tex and added none of it.

The stack is unpublished, so the chronology is restored rather than patched:
G2b lands as its own row between G-minor and G3a, G3a renumbers up, and the
PDF is regenerated.

The new epiphany-testkit guard makes recurrence detectable. Bare name-presence
would not have: with the G2b row deleted, "G2b" still occurs inside the chapter
in G3a's prose, so a substring guard would have been born green. The guard
requires a principal marker -- the rung name preceded by the row's separator --
strictly ordered across the four standalone-row rungs G2a, G-minor, G2b, G3a,
with G2b's content anchored inside its own row segment so G3a's row cannot
satisfy it. G1 is deliberately unguarded: it has no standalone row, being
recorded retroactively inside G2a's. No document version number appears in the
test, in its comments, or in this message.

Executed against spec/CONTRACT_GENESIS_G3A_UNDO_REPAIR.md Packet B.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-29 19:09:25 -04:00
Levi Neuwirth 6c5e69f19d Genesis G3a: the four root-level entity mints
CreateStaffGroup (35), CreatePartDefinition (36), CreateAnalysisLayer (37),
CreateView (38) -- all schema major 0, all schema-minor epoch 11, on
CreateStaff's set-union mint pattern with byte-identical re-carry
idempotence. Completes the four remaining root-level Score entity vectors.

No epiphany-bundle change of any kind: schema_major() gained NO arm (all
four fall through the catch-all _ => 0), so the op-block accept-set stays at
3 where G2b left it. The OperationKind golden lock stays at [(OperationKind,
u8); 30] -- kinds 35-38 remain outside it by design, per P13-S15's own rung.

Closes the satisfiability half of the from-empty defect: every G3 object
kind previously became Live only through base ingest, so CreateStaff's group
precondition was unsatisfiable from empty. It does NOT make a consistent
group authorable -- §1.1 disposition B rules Staff.group the sole authority
and StaffGroup.members a non-authoritative projection G3a stores without
maintaining, with both stale forms permitted. That ruling now reaches all
four normative sites: both field doc comments (neither field was documented
before), core_spec.tex, and both the new CreateStaffGroup and the existing
CreateStaff sections of operation_catalog.tex.

Carried-value maps for all four families thread through seven sites each,
including the base seed that fails silently -- a re-carry test reducing only
from empty cannot see a missing seed, which is why t5b reduces onto a
populated base.

Verified independently, not relayed. Full gate re-run: 1429 tests / 0
failed, clippy 0, fmt clean, git diff --check clean. Two mutations the agent
had not observed were run here and observed: reverting COMPANION_VERSION to
0.11.0 kills t11 (the agent had only reasoned this), and skipping the
staff_group_values base seed kills t5b. Both reversed by editing back, with
the baseline green.

Two findings recorded rather than silently absorbed:

- valuegen::analysis_layer's name format was accidentally exactly 16 bytes,
  the same width as AnalysisLayerId's encoding, which made a field-swap
  mutation on the two-field AnalysisLayer struct byte-invisible. Shortened
  and documented -- a fixture that makes a mutation undetectable is the same
  failure class as a guard that cannot fail.
- binary_format.tex has NO Revision History row for G2b. G2b's own contract
  required one (touch row 27: "version, Revision History row"); it changed
  the payload-layout row, tag row, accept-set text and §2373 history but
  never bumped the version or added the row. The document went 0.12.0 (G2a)
  -> 0.13.0 (G-minor) -> 0.14.0 (G3a) with the accept-set raise unrecorded.
  G3a adds its own row without retroactively backfilling G2b's.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-29 14:07:35 -04:00
Levi Neuwirth 1a7df12b0c G2b: unconflate the corpus version domains, unscope t10, and sign the mutations
The corpus fixture stamped the manifest at {0,10} because SetTuningContext
needs an operation block at {3,10}. Those are separate domains: the document
line carries the manifest's aggregate SchemaVersion and projection discards the
block's schema by design. With no edit barriers the manifest stays baseline V0,
so the fixture was locking an over-stamped manifest into the corpus while
appearing to prove the operation epoch - the exact inference text_projection.tex
tells readers not to make. The block's stamp is proven where it lives, by the
staged-and-reopened roundtrip test.

t10 still read only bundle.rs. Correcting ids.rs and adding a prose
cross-reference did not make the pair travel together; sharing one guard does.
It now iterates both sources. Two incidental discoveries while extending it:
include_str! pulls in the test's own text, so both the needle and the assertion
message must avoid the phrase they search for - which is why the original split
its needles with concat().

Four stale comments: manifest.rs's barrier-tag range 24-33, payload.rs's "ten
events"/"thirty-four payloads", and the 30..=33 ranges in generators.rs and
layout_stub.rs.

Mutation sweep, each run and observed:
- t1 kind space 34->35, and tag space 34->35, separately. Both kill t1.
- t2/t3 schema_major 3->0: kills t3 and the staged/reopened test ({0,10} vs
  {3,10}).
- t6 predecessor restore dropped: kills t6 (442 vs 441).
- t8 kind epoch 10->9 and tag epoch 10->9, separately. Both kill t8; the tag
  mutation additionally kills s1's tag table, the kind mutation does not,
  which is why both tables needed the entry.
- t10 stale claim reinjected into bundle.rs and into ids.rs, separately. Each
  kills the guard, naming the offending file.

t5 and t7 were signed earlier; t4 by the cap-to-2 run.

Gate: 1410 tests, clippy 0, fmt clean, git diff --check clean, 14 textproj
vectors, 105 decode vectors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-29 10:09:49 -04:00
Levi Neuwirth d685b641cd G2b coverage repairs: the second stale rationale, and three guards that could not fail
Closes four coverage gaps found in review. No behavioral change to the reducer,
codec, or wire.

ids.rs carried the same falsehood bundle.rs did - major 3 is snapshot-only, no
operation payload embeds the tuning context, the op-block accept-set is
untouched. t10 grepped only bundle.rs, so it could not see it. Rewritten, with
a pointer to its sibling so the pair stays together.

The text-projection corpus covered no set-tuning-context document: every
accepted document built DeleteRegion envelopes, and G2b only regenerated
headers to 0.11.0. Adds a real one, carried at minor 10. The typed all-kind
round-trip proves the production parses; only a committed vector proves the
emitted text is stable across implementations, which is what that corpus is
for.

Fixing it exposed a latent fragility worth more than the fix: the negative
vectors bound their source documents by positional index into the accept list,
so inserting a document silently repointed every reject builder at the wrong
text. Converted to by-name binding, which is what made the insertion safe.

The generator guard stopped at kind 33 - the very test written because
TransposeInterval and CreateInstrument had gone missing from every corpus while
downstream suites stayed green. It now records and asserts SetTuningContext, so
removing or rerouting the new arm cannot leave it green.

t3 and t4 did not test block behavior. t3 computed max(schema_major()) over
bare OperationKinds without building a block; t4 asserted only the ceiling
constant. Neither would notice a mis-derived stamp or a gate that rejected the
block it now admits. Adds one integration test that stages a real envelope
through stage_operation_block and reopens the bundle, asserting {3, 10} and
read-write. Verified by mutation: reverting the accept-set to 2 fails it.

Also three stale counts, each the hand-maintained-parallel-list shape: the
textproj production count said 31 and had been stale since the original
text-projection commit (now 35), the G-minor epoch table omitted kind 34, and
the corpus count was pinned at 13.

Gate: 1410 tests, clippy 0, fmt clean, conformance 8/8, 14 textproj vectors,
105 decode vectors, git diff --check clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-29 09:48:41 -04:00
Levi Neuwirth 13c3d2fd46 Genesis G2b: SetTuningContext, and the accept-set raise it pays for
Kind and tag 34, schema major 3, minor epoch 10. The last rung before G3, and
the one that closes P13-S13: the tuning context becomes operation-authored, so
it finally has a canonical carrier. The closure argument is the metadata
precedent, not the canonical base - the base embeds no graph values for any
field, and metadata has been durable purely through its operations since M2d.

The payload carries epiphany_core::TuningContextSettings, a five-field subset
of ScoreTuningContext, not the full graph type. ScoreTuningContext's codec
deliberately drops accidental_extensions, so a full-value payload would have
diverged between a live session, where accept stores the envelope as a value,
and the same document reloaded, where the field decodes empty. canonical_value!
could not have caught that: it compares bytes and never the originating value,
so a field that never reached the bytes is structurally invisible to it. The
subset makes the divergence unrepresentable instead of relying on a
normalization step nothing can enforce, and it costs no wire design - the
encoding is byte-identical to the existing five-field walk, which
tuning_context_settings_canonical_bytes_match_score_tuning_context asserts
directly. Reduction leaves accidental_extensions untouched.

SetTuningContext is the sole genesis payload born at major 3, because minimal
stamping is a function of each payload's value, so the accept-set raise is
charged to this one surface: OperationEnvelopeBlock 2 to 3. The doc comment
above it did not merely record the cap, it asserted that no operation payload
embeds the tuning context - a sentence this rung falsifies - so it is rewritten
rather than left beside a corrected constant.

Undo restores the seeded base settings, default or not, and the
never-authored versus authored-to-default distinction stays unobservable. An
earlier draft of the contract had that backwards; PLAN_GENESIS_OPS section 5
trap 5 withdrew it, and SetMetadata is the disproof.

Fixes two undefined references the interrupted run had not yet reached:
operation_catalog.tex referenced sec:evolution:major3, a label defined in
binary_format.tex, which LaTeX cannot resolve across documents. Replaced with
the sectionsc convention already used for every other cross-companion citation
in that file.

Gate: 1409 tests, clippy 0, fmt clean, conformance 8/8 including [7f], both
vector corpora regenerated, all four PDFs at 0 undefined references. The t5 and
t7 mutations were re-run independently and observed to fail as specified; the
remaining eight are not signed off, because the implementing run was stopped
before it reported them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-29 09:30:32 -04:00
Levi Neuwirth ff9bd0fd06 G-minor: the chunk schema minor becomes a derived record
Implements the schema-minor MUST (binary_format.tex, Schema Versioning) that no
writer had ever honoured: a writer raises the chunk minor when it emits a
discriminant appended after the minor it otherwise declares, so an
unknown-discriminant decode failure is attributable to version skew rather than
corruption.

introduced_minor() lands on the five vocabularies with post-baseline variants -
OperationKind, OperationKindTag, OperationPayload, ReanchorReason, and
PreconditionFailureReason - each exhaustive with no wildcard arm, so a future
variant cannot compile without being assigned an epoch. The tag epochs live
inside operation_kind_tag_vocabulary! rather than beside it, because a sibling
match is the parallel list that macro exists to prevent. The sentinel is
Option<u16>, not 0, since 0 is a real baseline minor for V1-V3 and conflating
them would make the max read correctly only by accident.

An envelope's required minor is the max over every discriminant it actually
emits; a block's is the max over its envelopes; major and minor derive
independently. Baselines are not normalised - V0 keeps minor 1.

The manifest seam keeps epiphany-bundle opaque: no ops or layout-ir dependency,
and the aggregate version is supplied by the producer rather than derived, with
CommitContext carrying the previous one so unchanged barrier content preserves
it. The version rides the superblock slot that already exists; Manifest gains no
field, which would have been schema-major and would have defeated the rung.
bundle.rs's superblock check stays major-only - tightening it to full-version
equality is a conformance regression, and s11 locks that.

textproj carries the manifest SchemaVersion and never derives it, so
COMPANION_VERSION moves 0.9.0 to 0.10.0 with the corpus regenerated. Not because
of op-block stamping, which remains projection-invisible. A new normative
requirement records the carry-never-derive rule in the companion itself; its
rationale names layout-ir, which textproj genuinely lacks, rather than the
operation vocabulary, which it has.

Gate [7f] adds an independent oracle over decodable in-tree barrier fixtures,
requiring exact equality rather than >=. Equality is load-bearing: >= catches
under-stamping but not stale over-stamping after the sole maximum contributor is
removed. Undecodable blobs are reported not-checkable, never as a pass.

Also repairs binary_format.tex's stale claim that OperationKind and
OperationKindTag append at 30 with a history stopping at 29, while 30-33 are
taken and the normative tables already carry them.

Gate: 1399 tests, clippy 0, fmt clean, conformance 8/8 with [7f] at four
fixtures checked and one not-checkable, 102 decode vectors byte-identical
(verified, not assumed), 13 text-projection vectors.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 21:04:34 -04:00
Levi Neuwirth 55eff00778 G2a review fixes: a false deviation, and two tests that proved less than claimed
Four findings, all confirmed against the tree.

The accept-set "deviation" was not real, and I propagated it. bundle.rs:1322
has asserted max_supported_major(OperationEnvelopeBlock) == 2 since before
this packet, so the gate was always satisfiable; the privacy observation about
the symbol not being re-exported is true but irrelevant, because the assertion
never needed to live outside the crate. I checked the premise and not the
conclusion, then reported it as verified. No re-export is needed here or by
G-minor. The test now also asserts what the contract actually asked for -- that
a staged block carrying either kind stamps major 0, exercising the writer-side
derivation rather than the bare kind.

s3 asserted effects only, though the contract required chain growth too. A
mutant returning Applied while skipping WriteChain::record for an unchanged
value passed it, and the damage would surface only later as an undo restoring
the base instead of reporting supersession. The two identical writes now sit in
different transactions and a strict undo of the first must report the second as
superseding; mutation (b) kills exactly that half while the effects half stays
green.

s9 claimed no mutation was needed because the test is itself a reject-path
exercise. That reasoning was wrong: being a reject-path test does not show the
rejection is caused by the mislabeling rather than something incidental. The
mutation -- a parse arm that silently accepts a mismatched shape -- was both
performable and killing.

core_spec grouped SetCanvasLayoutDefaults and CreateInstrument as leaves with a
single layout. True only of the first. Instrument has distinct major-0/1/2
layouts and stamps major 2 unconditionally because its major-2 appends are
mandatory, so it is not major 1 for the opposite reason. Split.

All ten contract mutations now have observed kill evidence: s1, s2, s4, s8 and
s10's row-28 half were run here alongside the four recorded earlier. s8 killed
only the two pinned literal-byte vectors while 217 round-trip tests stayed
green -- the 3b-i property, demonstrated rather than asserted.

Gate: fmt clean, clippy 0, 1371 passed / 0 failed, conformance 8/8 and 9/9,
labels 6/6, core_spec PDF 0 undefined refs, goldens byte-identical,
epiphany-bundle diff empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 17:37:43 -04:00
Levi Neuwirth 7df5ca17ce Genesis G2a: two settings setters, and the vocabulary's documentation catches up
SetCanvasLayoutDefaults and SetSpellingPrecedence, kinds and tags 32/33, on the
SetMetadata LWW pattern: advisory last-writer-wins, no conflict, no idempotence
short-circuit, seeded from the base so a value-restoring undo of the first
operational write reaches the pre-operational value. Both carried types already
had Codec impls, so the packet designs no wire layout -- canonical_value!
delegation and one push_lp_bytes each, inheriting strict canonical-form
rejection rather than writing it.

Both stamp schema major 0 unconditionally, staying in the catch-all arm. The
op-block accept-set is untouched at 2; epiphany-bundle has an empty diff. The
raise to 3 belongs to G2b, and the chunk schema minor -- a MUST no writer has
ever honoured -- belongs to G-minor, filed as P13-S14 and deliberately not
fixed here.

Most of this commit is documentation the vocabulary had outgrown. G1 shipped
five normative falsehoods because its contract declared the wire surfaces out
of scope, and Push 4a left TransposeInterval out of both of core_spec's
normative listings. So binary_format's payload-layout and tag tables now reach
33 rather than 30, its snapshot-only bullet records that it is empty and why,
two spelled-out payload counts move from eight to nine, and core_spec's
listings gain all four missing kinds. The Pass-12 K8 doctrine is narrowed
rather than reversed: the root and canvas remain structural givens no operation
mints, and only the claim that the score's contents arrive outside the
operation set is superseded.

Two generators were stale by two tranches and fed corpora other suites treat as
exhaustive. layout_stub's is now derived from PAYLOAD_FREE with an explicit
Registered append instead of a hand-written match, so future built-ins follow
structurally; s10 asserts PAYLOAD_FREE union {Registered} so deleting that
append cannot pass.

Companion 0.8.0 -> 0.9.0, binary format 0.11.0 -> 0.12.0, catalog 0.9.0 ->
0.10.0 (covering G1 retroactively). 102 decode vectors, regenerated.

Coordinator verification: 1371 passed / 0 failed, clippy 0 warnings, fmt clean,
conformance 8/8 and 9/9, requirement_labels 6/6, four PDFs at 0 undefined
references, goldens byte-identical, accept-set confirmed still 2. Mutations
s3/s5/s7/s10 re-run independently and each observed to kill its test.

Two findings beyond the contract. max_supported_major is not reachable outside
epiphany-bundle -- mod bundle is private and the symbol is absent from its
pub use -- so the gate's "assert it in code" was unsatisfiable without touching
a crate the contract forbids; verified by reading the unedited source instead.
And core_spec carries two P12-K8 references, not one; the contract named only
the first, so the second is annotated here for consistency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 17:07:46 -04:00
Levi Neuwirth 4a4988ce88 The latency wall is document-lifetime, not one sitting
Review found the mitigation attached to the last landing reading the wrong
code path. EditorSession::open does start with an empty applied log, but that
is the score-only probe constructor. The savable-document path is Ruling B's:
reopen is full replay, stored envelopes load as a committed partition, and
materialization reduces committed plus session operations together. Nothing
resets the depth this bench varies until the checkpoint and pruning machinery
assigned to T4b can write a new canonical_base.

So consequence (e) is withdrawn rather than corrected in place. The ~4,500-edit
wall is a budget on a document's whole accumulated history, and there is no
session reset to lean on -- neither as reassurance about the number nor as
support for the sequencing argument, which rests on paint dominance and does
not need it. T4b's trigger is correspondingly firmer than it read yesterday.

Also: two comments still described the gated core portion as reduce plus
engrave, from before envelope construction was added as a third stage. The
sum they document has included it since the last landing.

Verified in an isolated worktree at HEAD rather than in the working tree,
which still carries the genesis tranche's in-flight work: fmt clean, clippy 0
with and without golden-gate, gate OK with every verdict unchanged (the edits
are documentation only).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-28 16:27:59 -04:00
Levi Neuwirth 986c9cc5a1 The edit-latency bench measures the log a session actually writes
Review found the first version generating every envelope with an empty causal
context. Production gives only the root that shape: each later edit carries
active_prior_context(), the head's context extended by the head, so it covers
the whole active prefix. Reduction orders on those edges, so a context-free log
exercises a different algorithm -- and a cheaper one. Counters were wrong the
same way: EditorSession mints at authored.len(), so the root is counter zero,
which is also what extend_context recognises as the start of a contiguous run.

Remeasured on a session-shaped log, reduce is roughly three times its former
self at depth ten thousand -- 54 ms, not 17 -- and the wall moves from about ten
thousand edits to between three and five thousand. That is the number T4b is
sequenced against, so the first table would have mis-sequenced it. Depths three
and five thousand now bracket the crossing; sampling only decades hid it.

Two findings survive the correction and one is weakened. Reduce is still the
only depth-scaling stage, and is superlinear at about n^1.4 -- which does not
contradict the reduction bench's subquadratic result at fifty thousand
envelopes, because that log is generated across three replicas with a different
causal shape, and two logs of equal length are not equal work. Engrave is still
flat, at 260 to 327 microseconds, and is still the larger half of the core's
portion at shallow depth, so criterion 2's "uninformative while reduction
dominates" holds only past roughly depth five hundred. But "render dominates at
realistic depths" is now bounded: paint leads by four and a half times at depth
one hundred, is level by one thousand, and is left behind after. T4 before T4b
still stands -- the canvas removes what dominates a session's first thousand-odd
edits -- but the two are no longer comfortably separated.

The gate now includes envelope construction, which the requirement names first
and the first version silently dropped. It is forty nanoseconds and never moves
a verdict; a gate that omits a named component is a proxy for the requirement
rather than the requirement.

The 98% claim is replaced by both figures with their denominators named: what a
direct-IR canvas avoids is 83% of the full measured per-edit pipeline, and 99.8%
of the render path alone. The unqualified number was supported by neither.

One row changed marking for a reason worth recording. Depth four thousand passes
clean at 12.99 ms, but that is 78% of budget, and a load-contaminated run
measured it at 22.77 ms -- above the five thousand row, which is impossible
clean. A Pass row that fails whenever the machine is busy teaches people to
ignore the gate, so the last gated Pass is three thousand and four thousand's
clean number is kept as data in the table instead.

Also recorded: depth is per session, not per document. EditorSession::open
starts with an empty applied log, so reopening resets it and the reduced score
becomes the new pristine base. That is what keeps a four-figure wall from being
catastrophic -- though note entry mints one operation per note, so it is
reachable in a sitting.

Verified in an isolated worktree at HEAD rather than in the working tree, which
currently carries the genesis tranche's in-flight G2 work: fmt clean, clippy 0
with and without golden-gate, workspace tests green, gate OK across all five
rows.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-28 16:03:27 -04:00
Levi Neuwirth 47fb4266c4 Ruling A criterion 2 stops being an assumption
The staged interactive-edit latency bench: reduce / engrave / scene-build /
paint measured separately, gating the core's portion against
req:perf:single-system-edit-latency's 16.7 ms frame. Criterion 2 asserts that a
toolkit verdict is uninformative while reduction dominates, and that sentence
had never been measured; the sequencing question it governs -- T4's spike now,
or T4b's incrementality first -- was resting on it.

The stage split is not invented here. It is the seam EditorSession::materialize
already walks, read off its private render_score and reproduced stage for
stage, so the bench measures the pipeline rather than a model of it. Only reduce
and engrave are gated: the requirement bounds "the core's portion" and says in
its own words that edit-to-pixel latency is a product-layer obligation, so
charging the SVG serializer and resvg against a core budget would be a category
error. They are measured and printed because the ruling asks for the stages
separately, and because today's is the path Ruling A demotes -- the number is
the baseline a canvas must beat, not a budget to defend.

Four findings, in the order they matter. Reduce is the only stage that scales
with log depth, near-linearly, and it breaks the frame at roughly ten thousand
edits -- 17.26 ms against 16.7, a three percent miss, so an order of magnitude
rather than a threshold. Engrave is flat and small at ~280 microseconds, and at
shallow depth it is the larger half of the core's portion, which qualifies
criterion 2 rather than confirming it: reduction does not dominate until about
depth five hundred. Paint is the largest single cost at every realistic depth --
2.12 ms at depth one hundred is four and a half times the entire core portion.
And scene-build is 3.5 microseconds of IR work plus about 130 of SVG
serialization, which the no-feature run separates: a canvas consuming the IR
directly skips some ninety-eight percent of today's per-edit cost, none of it in
the core.

The sequencing answer is therefore that T4 before T4b stands, for the opposite
reason to the one assumed. The dominant cost at the depths real sessions reach
is the render path Ruling A already demoted, not reduction. T4b's trigger is a
session ten thousand edits deep, and the bench now watches for it as the one
Xfail row.

Two things the bench had to survive being wrong about, both mine. The depth-1000
row was drafted Xfail on the assumption Fact 8 would already bite; it passes
with eightfold margin, the gate's XPASS notice said so, and the row is promoted
here rather than left stale -- which is the whole point of that mechanism. And
the first edit log alternated transposition direction per operation, which is
degenerate when the pitch-list length is even: every edit to a given pitch
pushed the same way, drifting it twenty-five semitones by depth 1000 and would
have been two hundred and fifty by depth 10000. That inflated engrave by a
factor of two and paint by nearly three -- a score-content change wearing a
log-depth costume. Alternating per pass instead bounds drift to one semitone.
The residual content effect is documented rather than hidden: paint is
non-monotonic in depth because pass-count parity decides how many accidentals
the score carries, and reading its dip at depth 10000 as a scaling win would be
a mistake.

Stated limitation: the testkit's largest fixture is three staves by ten
measures, so the engrave and scene-build columns are lower bounds and this
cannot prove the budget holds on the hundred-page orchestral score the
requirement contemplates. It shows where the time goes at the scale we can
build, and a row that misses at this size misses by more at a real one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-28 15:04:36 -04:00
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 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 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 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 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 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 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 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 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
Levi Neuwirth 7b96c2d131 Text Projection 0.5.0: the projection is schema-directed
Found by starting the implementation: the grammar could not derive an ordinary
pitched note.

`value` had no alternative for a sequence at all, though value-projection clause 5
required one. Adding it exposed why it was missing. A sequence whose first element
is a fieldless variant is shape-identical to a struct, and `()` is both the empty
sequence and the absent option. Both collisions are reachable from the first
pitched note in any score: `PitchedEvent` carries `articulations` and `ornaments`,
sequences over the zero-field `ArticulationMark` and `OrnamentMark`, beside an
optional `DynamicMark`. One `insert-event` line holds both.

The collision is irreducible without new syntax, and new syntax buys nothing --
`req:textproj:strict-parse` already obliges a parser to reject a duplicate in a
set-typed field, which it cannot do without knowing the field is set-typed. The
parser consults the schema either way. So `req:textproj:schema-directed` states
what the ratified rules already required, `value` collapses to `"(" value* ")"` or
a leaf -- all shape can honestly say -- and the requirement assigns meaning by the
expected type. The binary form is schema-directed for the same reason and pays the
same price: its bytes do not say what they are either.

Three consequences stated: a struct with no fields is the bare symbol, as a
fieldless variant is; a byte string is not a sequence, so an opaque extension
payload and a `SoundConfiguration` project as byte strings, never as lists of
integers; and the grammar's repetitions now carry a notation rule -- adjacent
elements separated by exactly one space -- without which `"(transpose (" bytes* ")"`
spelled two targets as one undelimited run of hex.

Checker gains a seventh test, mutation-verified by restoring the 0.4.0 `value`
production and by stripping the schema-directed citations. It asserts the
symbol-headed struct alternative is *absent*: a grammar claiming to tell a struct
from a sequence by shape would be lying.

Gate green -- clippy 0, 1038 tests, doc 0, conformance 8/8, no golden churn, three
spec documents build clean with no undefined references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 22:40:27 -04:00
Levi Neuwirth 7face56ab4 Text Projection 0.4.0: two normative corrections, and a checker that can fail
The 0.3.0 audit raised two normative issues and one process issue. All three
land here, plus a fourth defect that reviewing the fix turned up.

The grammar contradicted its own escape requirement. `req:textproj:string-escapes`
obliges a writer to escape the backslash and a parser to reject a bare one, while
`unescaped` admitted it. Escapes are now four two-character sequences and
`unescaped` excludes U+0022, U+005C, U+000A, U+0009 by codepoint.

"Keep the binary order" was not available for every sequence. It holds only where
the binary order reads data the projection preserves, and two sequences fail that
test: `blob_roots` sorts by the full `BlobRef` encoding (offset, compressed
length, compression), and an extension's preserved chunk roots sort by
`ChunkRef`'s order, keyed on kind, then content hash, then *offset*. Under the
blanket rule, relocating a chunk -- which changes no semantics -- would change the
text, and two entries indistinguishable after erasure would produce duplicate
lines. `req:textproj:derived-ordering` orders and de-duplicates exactly those two
by projected form, and states that every other sequence keeps the binary order:
profile and extension declarations sort on semantic `(id, version)` keys, and
envelopes on canonical operation order.

The "machine-checked" grammar was checked by a throwaway script -- true of one run
and of nothing durable, the same evidence gap P2-P4 kept exposing.
`text_projection_grammar.rs` is the committed form: no nonterminal undefined or
unreachable, the escape rule admits exactly its four sequences, and the operation
and chunk productions are *derived* from `OperationKindTag::PAYLOAD_FREE` and
`ChunkKind` through an exhaustive match, so a kind added to the enum and not to
the grammar fails to compile. Every locator finds its production by name; writing
it exposed four bugs in itself, three of them column-anchored checks that a reflow
would have silently switched off.

Reviewing the escape fix found it reintroduced the audited defect in disguise: a
quoted terminal `"\\"` reads as two backslashes, so every escape became three
characters long. Both characters are now codepoints. Relatedly the mono font's
`Ligatures={TeX}` rendered U+0022 as a right curly quote and `--` as an en dash,
so the grammar misprinted the very delimiters it defines; `core_spec.tex` already
omitted it. Both are asserted against.

All six tests mutation-verified: the anchor asserted present before substitution,
then the named test observed to fail. Gate green -- clippy 0, 1037 tests, doc 0,
conformance 8/8, no golden churn, three spec documents build clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 22:27:32 -04:00
Levi Neuwirth 3baf8d050d Push 5 / P5: the missing inverse -- bytes back to an operation envelope
Text Projection requires decoding operation envelopes. No such decoder existed,
and the hole was bigger than the task: the format was WRITE-ONLY for operations.
A bundle's envelope blocks decoded to opaque byte strings, OperationKind had an
encoder and no decoder, nothing outside epiphany-bundle even called decode_block,
and nothing anywhere reconstructed an OperationEnvelope. Chapter 6 holds that a
score's canonical state IS the set of operations committed to it -- so a bundle
could be written and its score never reopened. The envelope's byte layout was
fully pinned in the Binary Format companion. Nobody wrote the inverse.

epiphany_ops::decode_envelope is that inverse. The first thing built on it is
testkit/tests/bundle_reopen.rs: create a bundle from 400 generated envelopes,
commit, take the bytes, reopen from nothing but bytes, decode every envelope,
rebuild the OperationSet, reduce -- and get the same canonical state. That test
could not have been written before this commit.

Strict in two layers, per the P2 lesson. A whole-envelope re-encode-and-compare
guard, sound here because every sequence in this encoding is normalized by its
encoder. Plus per-site checks where the rule deserves its own error and a future
encoder change must not silently relax it: TransposeInterval.targets is a SET
(seq-strictly-increasing; a duplicate is rejected, never absorbed by the BTreeSet
it collects into), and the frozen Transpose.targets is a MULTISET (non-decreasing,
duplicates preserved). That is the rule Push 4a wrote into the wire table and left
for whoever built this decoder. And a bounded count(): a declared count past the
bytes remaining is rejected before it can drive an allocation.

Coverage measured, not assumed -- again. The obvious oracle (gen_envelope_set,
4000 envelopes) reaches only 28 of 31 kinds and 1 of 4 payload variants.
ChangeRegionTimeModel, DeclareTransaction, Registered and all three meta payloads
were untouched, and they hold the trickiest decoders: PositionRemapping, NFC
strings, ResolutionAction, EnvelopeHash. So the exhaustive test drives a match on
OperationKindTag, and the compiler forces a sample for every future kind.

Two mutations verified. Removing the seq-strictly-increasing check still rejects
-- the guard is a real backstop there -- but with the wrong error, so the per-site
check earns its place on the error rather than the verdict. Removing the
whole-envelope guard leaves every round-trip test green, because round-trips only
ever feed canonical bytes; an_unsorted_sequence_is_rejected_by_the_whole_envelope_guard
is the test that locks it, and it fails under that mutation.

A trap worth remembering: PitchId::new(ReplicaId(7), 1) and
OperationId::new(ReplicaId(7), 1) have identical canonical bytes -- typed ids
share their byte form -- so a byte-patching test that searches for an id finds the
envelope's own leading id first.

Gate: fmt clean, clippy 0, 31 targets / 1031 passed / 0 failed, docs 0 under
-D warnings, conformance 8/8, zero golden churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:59:43 -04:00
Levi Neuwirth 567e8214a4 One tag vocabulary the compiler owns, and ratify the corpus in the companion
Two review findings after P4. Both real.

The tag-omission failure could recur. After the P4 fix there were STILL four
hand-maintained lists -- a test-local all_tags(), a barrier test spelling
0u8..=30, a fuzz corpus naming five tags, a vector corpus naming four -- plus a
malformed-bytes test asserting that 31 rejects. A future tag 31 added to
discriminant() and omitted from decode_canonical() would have left every one of
them green, and the malformed test would have LOCKED it, exactly as the 30
version did two commits ago.

operation_kind_tag_vocabulary! is now the single source. It generates
discriminant, from_discriminant, and OperationKindTag::PAYLOAD_FREE from one
list, and the generated discriminant match is exhaustive over the enum -- so a
variant added to the enum and not to the macro fails to COMPILE. Everything
downstream reads PAYLOAD_FREE: the decoder, the fuzz corpus (all 31 tags, not
five), the conformance vectors (an accept vector per tag: 65 vectors, not 37),
and the edit-barrier round-trip. Every "one past the vocabulary" constant is
computed, never spelled; a spelled constant is the trap that springs on whoever
appends the next tag.

Verified end to end with a hypothetical tag 31. Added to the enum alone: compile
error. Added to the enum and the macro: it compiles, decodes, and every derived
check passes because they read PAYLOAD_FREE -- while the committed corpus's
drift lock AND its now-stale "one past the vocabulary" reject vector both fail,
forcing the new vectors into the diff. There is no path where a new tag leaves
everything green.

Second finding: the corpus called itself normative while the spec said it was
deferred. Binary Format's "About This Companion" listed the cross-implementation
decoder test among things the document does not cover, and the Golden Anchor
Registry called it "the deferred conformance harness" whose literal-byte vectors
a future test "should add". Both now ratify it. New req:binfmt:decode-vectors
and a "The Decode Vector Corpus" section: a conforming decoder MUST accept every
accept vector for a surface it implements, MUST reject every reject vector, and
MUST re-encode an accepted value to exactly its bytes -- and accepting a reject
vector and then normalizing it IS accepting it. Binary Format 0.8.0 -> 0.9.0.
The wire-format fuzzer stays an implementation deliverable. The corpus header
now cites the requirement instead of asserting one.

Gate: fmt clean, clippy 0, 30 targets / 1024 passed / 0 failed, docs 0 under
-D warnings, conformance 8/8 with [7d] at 65 vectors, zero golden churn,
binary_format rebuilds with no undefined references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:32:35 -04:00
Levi Neuwirth a41596d329 Push 5 / P4: the decode conformance corpus, and the tag it could not read back
spec/vectors/decode_vectors.txt -- 37 committed byte strings across five
surfaces, each with its normative accept/reject verdict. The reference
implementation's fuzzers prove its own decoders self-consistent, which says
nothing about whether a foreign decoder agrees with the format. This is what one
is checked against. Gated in the conformance suite as [7d], and drift-locked:
the committed file must equal vectors::render(), so a wire-format change lands
in the diff.

It found a real defect on its first run. OperationKindTag::TransposeInterval
encoded to [30] and its own decoder REJECTED it -- Push 4a added the variant to
discriminant() and never to decode_canonical. OperationKindTag is what edit
barriers persist, so a barrier prohibiting TransposeInterval could be written and
never read back. Silent data loss on reopen.

Four things should have caught it. None did, and two made it worse:

  The round-trip test enumerated DISCRIMINANTS -- (0u8..30).map(decode_canonical)
  -- starting from bytes the decoder already knew, so it structurally could not
  notice a variant the decoder was missing. It now enumerates VARIANTS from one
  all_tags() list, with a completeness check in both directions.

  The distinctness test's hand-written variant list omitted it too. Same list now.

  operation_kind_tag_decode_rejects_malformed_bytes asserted that tag 30 is
  REJECTED, and layout-ir's decode_rejects_unknown_discriminants asserted the
  same at the barrier surface. Both were locking the bug in place and made it
  look deliberate. Both now name 31, and a new barrier test round-trips a barrier
  prohibiting every tag -- the persistence surface where this actually bites.

  The P2 decode fuzzer fed valid corpus bytes to the tag decoder and tallied the
  failure as a REJECTION, like any garbage input. It never asserted that an
  unmutated corpus entry decodes. Both fuzzers now do, as a pre-pass.

The harness had the same disease as the code. `check` collapsed "rejected" with
"accepted but does not re-encode", so a decoder that silently normalizes
non-canonical bytes PASSED the reject vectors it was written to catch. Verified:
removing the whole-state guard, and restoring the lenient compression codec, both
left the corpus green. `check` now returns Ok(injective) for accept and Err for
reject and never conflates them -- silently normalizing non-canonical bytes IS
accepting them. With that fixed, all four defect mutations fail the corpus, each
naming its class.

The corpus pins one vector per class this repo has shipped a bug in:
non-canonical-map-order (a guard catches it; no per-site check exists),
non-canonical-vec-order (only a per-site check catches it; a guard is blind),
lenient-sub-codec (a guard masked it in the manifest; the index had none), plus
trailing-bytes, truncated, unknown-discriminant, count-exceeds-remaining. A test
fails if one goes missing.

Gate: fmt clean, clippy 0, 30 targets / 1024 passed / 0 failed, docs 0 under
-D warnings, conformance 8/8 (now including [7d]), zero golden churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 20:16:15 -04:00
Levi Neuwirth 91a99b2093 Stop the conformance suite from authoring the frozen operation
req:opcat:transpose-frozen says a conforming implementation MUST NOT emit
Transpose from new authoring. testkit's editloop::sharpen -- the [7c] UI-seam
gate's editing action -- was emitting exactly that, so the conformance suite
itself would have violated the requirement the same push introduced. It now
authors TransposeInterval with (0, +1): the same alteration shift, the same
staff line, the same rendered result.

The random-kind corpora in generators.rs keep producing the frozen Transpose,
and should: it must reduce correctly forever, and a generator is now the only
thing that will ever produce one. The distinction is authoring versus replay.

This is not inert. TransposeInterval also writes the Propagated spelling
attachment the frozen operation never did, so the edited score genuinely
changes shape. the_sharpen_records_the_spelling_it_propagated pins that, and
was mutation-verified by removing graph_propagate_spelling.

Gate: fmt clean, clippy 0, 30 targets / 983 passed / 0 failed, docs 0 under
-D warnings, conformance 8/8, zero golden churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:50:22 -04:00
Levi Neuwirth afb487c24d P13-I2: Staff::default_clef is the fallback, not decoration
to_constrained took the active clef from the staff instance's clef_sequence and
fell back to Clef::default() -- treble. A staff that declared its clef only on the
Staff, with no ClefChange, therefore drew a treble clef and placed every note
against it. The field was decorative in the projection.

It is the fallback. StaffContent now carries default_clef -- the clef belongs to
the Staff, the sequence to the StaffInstance, and resolving "the clef at time t"
needs both -- and active_clef_or(clefs, at, default) resolves against it.
active_clef remains as that with the treble default, for callers with no staff to
hand, so the public API is intact.

The part worth pausing on: epiphany-editor-core reads the same function for
hit-test pitch resolution. Fixing only the projection would have left a click on a
bass staff resolving its pitch as treble -- the engraved clef and the editor
disagreeing about what note is where. Both now go through active_clef_or.

Removal was rejected: the field is named for its purpose, is encoded on the wire,
and dropping it would be schema-major.

Zero golden churn: every fixture and generator declares treble, which is also
Clef::default(), so nothing that exists today moves. Locked by
a_staff_declaring_only_a_default_clef_engraves_in_it and mutation-verified by
restoring the Clef::default() fallback. Scoping that test by provenance was
necessary -- valid_score_rich has three staves and only one was re-clefed, so the
first assertion I wrote ("no gClef anywhere") failed against a correct fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 14:47:42 -04:00
Levi Neuwirth 029039bf35 Straighten the QMC version story in the docs that assert it
Review follow-up on 5b016f3. Bumping the catalog to 0.3.0, I ran a mechanical
find-and-replace over the version string and did not reread the sentences around
it. Result: layout-ir's quality.rs credited the spacing_distortion refinement to
v0.3.0 when that was v0.2.0, and engrave's quality.rs still announced v0.2.0
while implementing the v0.3.0 unit set.

Each version now says what it actually did: v0.2.0 narrowed spacing_distortion's
measurement DOMAIN to rhythmic columns; v0.3.0 narrowed vertical_density_penalty's
CONTRIBUTING UNITS to one per realization of a gap band. Both narrowed what is
measured over, not what it is normalized against -- so the transcribed constants
(anchors, thresholds, warning fraction) have not moved since v0.1.0, which is the
invariant worth stating and the reason the two revisions were safe.

Swept for the same rot rather than fixing only the two reported. Three more
present-tense claims pinned a version that will keep going stale: engrave and
layout-ir DECISIONS both said "the companion (v0.2.0) ratifies/pins the nine
axes", and the reference suite claimed RS-1 passes "under engrave v3, QMC v0.2.0
anchors" -- engrave is at 11. All three now cite the current version and name what
is actually invariant across revisions. The remaining "QMC 0.1.0 -> 0.2.0"
mentions are historical records of what that revision did and stay as they are.

Docs only. Gate green; zero golden churn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 11:54:25 -04:00
Levi Neuwirth 5b016f33b7 Two more glyph-members assumptions in the vertical-density axis (QMC 0.3.0)
Review follow-up on 43215c7. Both findings were right, and the second falsified a
comment I wrote in that very commit.

1. Region staff bands were still identified by glyph `members`. vertical_raw
   measured content over all primitives but decided WHICH staff bands belong to a
   region by glyph membership -- reintroducing the assumption the change exists to
   shed. A staff band is allowed to own no glyphs: to_constrained emits one per
   staff of the region regardless, and a percussion-clef staff (no bundled glyph,
   so it engraves to a traced anchor stroke) with no notes owns only staff-line
   strokes. Membership now comes from content presence in one of the region's
   systems, which identifies the band exactly -- a staff band is per-(staff,
   region), so its content can land nowhere else.

2. Only the first realizing system was measured, justified by a comment claiming
   rigid system translation makes every realization agree. The inter-staff solve
   had just falsified that: it sizes each system's gaps from that system's own
   content. req:qmc:vertical now counts ONE UNIT PER REALIZATION, matching how
   realized inter-system gaps were already counted. That is a contributing-unit
   change, so unlike 43215c7's clarification the catalog moves: QMC 0.2.0 -> 0.3.0
   (the P12-I12 precedent). Raw formula, anchor, orientation, thresholds unchanged.

Two new fixtures, because an unexercised fix is what I criticised last round:
percussion_placeholder_staff (a valid, invariant-clean score whose lower band owns
zero glyphs and six strokes) and two_staff_wrapping_pressure (one region, two
systems, staff-line gap 15.93 where it collides and 7.87 where it is slack). Both
mutation-verified: the members filter scores 4.8e-7, first-system-only scores
1.3e-7 -- each ~0 where the corrected axis reports real deviation.

What the per-realization count exposes is not comfortable, and is recorded rather
than smoothed over: two_staff_wrapping_pressure now scores 0.739. Its pressured
system solves to the declared gap exactly; its slack system sits at ~5 staff
spaces against a preferred 2.0. The axis is symmetric -- a gap wider than
preferred is sprawl exactly as a narrower one is crowding -- and this solve only
expands, never compresses. The deferral "compressing an OVER-wide fixed gap toward
preferred ... rarely wanted" is promoted to measurably wrong. Named, not fixed:
compression is a layout change (golden churn, ENGRAVER_VERSION move), not a
measurement one.

Adjacent finding, parked: Staff::default_clef is never consulted -- to_constrained
takes the active clef from the instance's clef_sequence and falls back to
Clef::default() (treble), so a staff declaring its clef only on Staff engraves as
treble. Verified (no layout-ir consumer reads the field). Filed in layout-ir
DECISIONS with the ConstrainedLayoutIR listing gap, pending a >=3-candidate batch.

Measurement-only: no layout change, ENGRAVER_VERSION stays 11, zero golden churn.
Gate green; QMC PDF rebuilds clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 11:39:28 -04:00
Levi Neuwirth efaebb9acd Let the band model carry staff attribution instead of inferring it
Stroke and Curve gain `vertical_band: VerticalBandId`, the field GlyphObject has
always carried. Curve's own doc comment used to call it "a *free* primitive (no
vertical band, no spring slot)" -- but the projection computed each primitive's
band, used it for the glyphs, and threw it away for the strokes and curves.

The engraver then reconstructed it geometrically, and got it wrong twice: nearest
glyph by x handed a lower-staff stem to the upper staff (4132a7a), and nearest
glyph to a slur's start endpoint handed a bottom-staff slur to the top staff
(b1bfe04). Both tore primitives off their own notes, both reached a committed
golden, both were caught by review rather than by the gate. A slur is the proof
the inference can never be made safe: its endpoints are deliberately lifted clear
of its own staff, into the zone where the nearest notehead belongs to the
neighbour. No distance metric recovers the owner.

Both fixes were correct and both were the wrong shape -- reconstructing by
inference a fact the projection had in hand and discarded. So: the projection
declares it (a slur's staff is its notes' staff), the engraver's attribution
becomes three map lookups, and ~60 lines of geometric rules, epsilons, and
fallbacks are deleted.

Two bands had to become unconditional, since strokes could otherwise name bands
that no glyph had caused to exist -- validation now rejects that as UnknownBand:

  - a staff band per staff of the region, in the region's own staff order (the
    order y_origin stacks by), not only for staves that emitted a glyph. A staff
    whose clef is unbundled engraves to an anchor *stroke* and no glyph.
  - the margin band unconditionally, because a region's own traced anchor is a
    stroke that names it whether or not a margin glyph puts a member in it.

Both may carry zero members, as an inter-staff gap band already did: membership
realizes the spring solve over glyphs; existence is what attribution needs.
Strokes and curves are deliberately NOT added to VerticalBand::members -- their
band reference is one-way.

vertical_band is not part of ResolvedLayoutIR::canonical_bytes (primitives are
encoded field-by-field), so this is layout metadata outside the canonical
encoding: no companion-version bump, and ENGRAVER_VERSION stays at 11 because the
output is unchanged. Zero goldens churn -- which is the evidence that the declared
owner agrees with the inferred one across the entire corpus.

Locked by every_stroke_and_curve_names_a_band_that_exists, verified by mutation:
making the margin band conditional again fails it on valid_score_rich. The two
tear-off regressions are kept -- they now assert an outcome the data model
guarantees, which is where a dropped declaration would surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 10:37:27 -04:00
Levi Neuwirth f7350362a3 Close the inter-staff residual risk: a three-staff cascade fixture
The reviewer signed off on the inter-staff slice with one named residual risk:
the 3+-staff cumulative shift cascade was documented and traced correct but had
no fixture behind it -- valid_score_rich's "three staves" are three separate
single-staff regions, so each lands in its own system and the cascade never runs.

three_staff_close_content puts three staves in ONE region with deliberately
asymmetric pressure: the upper pair collides hard (C1 against C7), the lower pair
only gently. That asymmetry is what makes the fixture discriminating. Sizing each
pair independently -- the plausible wrong implementation -- measures the lower
pair against the middle staff's ORIGINAL position, hands the bottom staff only
its own small correction, and drags it back up through the middle staff.

Verified by mutation, not by assertion alone: with the cascade removed the bottom
staff's shift collapses from 34.68 to 4.56 against the middle staff's 15.06, and
both the shift ordering and the staff-line-gap assertions fail. two_staff_close_content
still passes under that same mutation, which is precisely why the new fixture was
needed.

Writing the test also corrected a wrong mental model, now recorded in DECISIONS.md:
a shift INCREMENT generally exceeds the lower pair's own raw correction, because
the upper staff's descent has itself eaten into that pair's gap and must be undone.
The first version of the test asserted the opposite and failed against a correct
solve.

The fixture additionally pins curve attribution against a three-band choice -- the
slur must still find the bottom staff, not merely the nearer of two -- and carries
its own render golden. No existing golden churns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 10:20:35 -04:00
Levi Neuwirth f103641013 Inter-staff slice 1/2: a two-staff pressure fixture
Groundwork for inter-staff band-height renegotiation (the last vertical-spring
piece): a fixture whose two staves' content nearly collides at the engraver's
fixed staff pitch, so the pending solve has something meaningful to separate.

two_staff_close_content: a one-measure, two-staff (treble/treble) metric score.
The top staff descends into low ledger notes (down to C2); the bottom staff
climbs into high ledger notes (up to C6) under a slur that arcs further above
them. In the constrained frame the top staff's content occupies y ≈ [-8, +4]
and the bottom staff's ≈ [-12, -5], overlapping in [-8, -5] — the C2 ledgers
collide with the C6 + slur. Invariant-clean; 8 notes, 1 slur, 41 strokes
(the heavy ledgering), 1 curve.

Registered in the render-svg acceptance suite; its goldens (this commit) show
the CURRENT overlapping layout — slice 2 (the solve) will update the engrave
golden to show the staves separated. Invariant test added. 946 tests, clippy 0,
docs -D warnings, conformance 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 22:48:39 -04:00
Levi Neuwirth c5173d275e Push 3 (slur quality) 1/3: render dashed/dotted slurs faithfully
E2 drew every slur solid and surfaced a SlurLineStyleNotRendered diagnostic for
a non-Solid authored line. That deferral is now lifted: the Curve primitive
gains a `line: LineStyle` (from the slur's SpanStyle.line), threaded through the
resolved canonical encode (a per-curve style byte), the engrave remap, and
casting; render-svg emits stroke-dasharray (dashed = "0.5 0.35"; dotted =
round-capped "0 0.28"). The diagnostic variant is removed — the style is
rendered, not deferred, so `req:layoutir:slur-curve`'s "never silently rendered
solid" is satisfied by faithful rendering rather than a surfaced gap.

ENGRAVER_VERSION 5→6 (a dashed/dotted slur's resolved bytes and SVG differ).
Zero churn beyond the one dashed slur: the slur fixture's two solid curves are
byte-identical; only the editorial slur's <path> gained the dasharray. Solid
slurs and slur-free scores are unchanged. 932 tests, conformance 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:13:32 -04:00
Levi Neuwirth 28f210e16f E2 follow-up: surface slur style deferral, conservative curve hit capsule
Three source-audit findings on 81b7f42, fixed:

1. Slur kind and line style were dropped at projection and every slur drew
   solid with no signal. SlurContent now carries `kind` (SlurKind) and `line`
   (LineStyle) through the projection, and a non-Solid authored line style
   emits a `LayoutDiagnosticKind::SlurLineStyleNotRendered` — the curve still
   draws (solid, ink + provenance kept), but the ignored dash/dotted intent is
   surfaced, not papered over. The Minimal tier still draws one canonical arc
   per kind (kind-aware rendering is higher-tier); the kind is now preserved
   for it. The slur fixture's editorial slur is authored Dashed to exercise it.

2. Curve hit-testing flattened to a fixed 16 chords and tested capsules at
   exactly half_width, so a thin/high-curvature slur's true ink between samples
   could miss a click. contains/intersects_rect now inflate the capsule by a
   proven flattening-error bound (h²/8·max‖B''‖ = (3/4N²)·max second-difference)
   — provably conservative, so a click on the drawn arc never misses. The AABB
   (control hull ± half_width) already bounds the true ink, unchanged.

3. The engrave crate-level doc still said "no drawn slur geometry exists yet";
   updated to slur_shape 0.0 by construction (Minimal draws the ideal arc),
   beam_slope still vacuous.

931 tests, docs clean, conformance 8/8. Zero golden churn (dashed slur draws
solid; layout diagnostics don't reach the render output).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 16:08:05 -04:00
Levi Neuwirth 81b7f42f0a Schema major 2 Phase E2: slur curves + cubic-bézier primitive
The third pipeline primitive kind, review-hardened (5 verified findings fixed
pre-commit). A `Curve` (four control points, mirroring `Stroke`) threads
through all three IR stages, the canonical-encode fingerprint (a 5th u32 count
prefix; width-lock 4→5), the round-trip provenance chains and count identity,
`to_render`, the stub solver, engrave remap + casting, SVG path emission, and
hit-testing.

Slurs draw as one cubic bézier per slur carrying the slur's exact provenance
(no synthesis). LayoutContent::Slur resolves each endpoint event to a Note
column at to_logical (SlurEndpoint At/Unresolved — E1's honest-placement
discipline); a symmetric arc whose apex sits `height` from the endpoint line;
curvature_override direction+height honored, style.line (dashed) deferred to
Push 3. Honest non-drawing (traced anchor kept) for an unresolved endpoint, a
non-left-to-right span, or a cross-staff slur (no single staff — would float
at yo=0). Authored height/thickness sanitized to defaults when non-positive
(a negative thickness would else fail validation and blank the layout).

Hit-test: HitShape::Curve (Copy) flattens the cubic to a 16-segment capsule
inside contains/intersects (one region per curve); a slur click resolves to
Slur generically (no editor arm; edit ops refuse it). render-svg: stroked
unfilled <path C> after strokes/before glyphs + curve_count + content-hull
bounds. engrave: HorizontalRemap::curves; casting curve_system = Rigid-to-
start-system (no de Casteljau split — deferred; break-spanning slur draws
whole in its start system, kept for the source surjection); ENGRAVER_VERSION
4→5; slur_shape_penalty now 0.0 by construction (Minimal draws the ideal arc).

Existing SVG goldens changed only in the provenance-note comment (curves now
enumerated) — geometry byte-identical; 6 snapshots gained curve_count; new
ten_measure_with_slurs goldens. 929 tests, conformance 8/8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 15:33:00 -04:00
Levi Neuwirth 7a9bf42119 Schema major 2 Phase E1: repeat barlines + volta brackets render
The first repeat-structure ink, review-hardened (19 verified findings fixed
pre-commit). Zero golden churn for repeat-free scores; four new goldens for
the ten_measure_with_repeats fixture.

layout-ir: repeats project LayoutContent::Repeat resolved at to_logical
(RepeatPlacement::At/RegionEnd/Unresolved — honest, no origin fallback;
zero-offsets judged by value; bare wall-clock boundaries draw no ink); deps
now from anchor_sites(). Constrained: coinciding measure barlines morph into
precomposed repeatLeft/Right/RightLeft (name-only, exact provenance kept);
standalone signs synthesize under a semantic (site, staff) instance key;
end repeats at the region close draw repeatDots beside a final barline (or
the full sign on continuing staves); start marks there draw nothing;
end-facing signs reserve their left reach via column overhang; time
signatures clear morphed signs; volta brackets = 3 strokes above the top
staff + timeSig digit numerals, kind-independent.

engrave: ENGRAVER_VERSION 3 -> 4; casting classifies barline columns via
is_barline_glyph AND direct Measure source, so standalone signs are never
phantom break candidates or measure records; criterion-6 round-trip covers
the repeat fixture.

render-svg: repeatLeft/Right/RightLeft/Dots outlines + metrics + font subset
regenerated from the SHA-pinned Bravura (fontTools 4.63.0); GlyphClass::Repeat.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:51:11 -04:00
Levi Neuwirth 9b5339f8f3 Schema major 2 Phase D: the repeat-authoring pair (code tranche)
CreateRepeatStructure / DeleteRepeatStructure land end-to-end,
review-hardened (five-dimension workflow review; 19 findings triaged —
12 verifier agents lost to a session limit were verified by hand).

epiphany-ops:
- CreateRepeatStructureOp { repeat } (lp(RepeatStructure), wire 28,
  BORN AT V2: schema_major always 2 — kind/voltas are unconditional
  fields) and DeleteRepeatStructureOp { repeat } (bare id, wire 29,
  major 0: minimal stamping over a major-0 layout; the discriminant a
  schema-minor append). OperationKindTag 28/29 name-verbatim with
  validating decode; golden tables grown to 30; migrate/v0 identity
  arms; lib re-exports.
- Reduction: set-union mint (live-id => AlreadyApplied without value
  comparison — the cross-cutting discipline; RecreateContentMismatch
  scope unchanged) with the ALL-anchor-sites-live precondition;
  delete-wins tombstone; the "Repeat structure / Anchor" rule-table
  row wired into BOTH the ledger (reanchor_for_tombstone arm, min-id
  survivor, cascade when dry) and the graph (every dead site rewrites
  to the same survivor), agreement asserted by tests incl. the
  DalSegno jump-target case.
- Undo: materialize_graph_tombstones gains RepeatStructure — and the
  pre-existing SPANNER GHOST fix (an undone spanner mint never left
  the graph), regression-locked.
- validate.rs: VoltaEndingsIllFormed advisory (non-empty, 1-based,
  strictly ascending; interactive-only).
- Canonical-base honesty: blake3 re-pinned for the corpus stream
  shift, its comment corrected (the seeded repeat creates all no-op),
  and the property covered surgically by
  the_canonical_base_embeds_no_repeat_values (two reductions
  differing only in repeat v2 content => byte-identical bases).

epiphany-core:
- RepeatStructure::anchor_sites()/anchor_sites_mut(): THE single
  site-set walk (start/end, jump targets, volta spans), consumed by
  reduction, the editor barrier seam, the invariant walk, and the
  cross-reference index — replacing five hand-rolled copies and
  fixing a SIXTH, silently-stale one (indexes.rs had never learned
  the Phase-B kind/volta anchors; regression-locked).
- RepeatStructure joins the CanonicalValue seam (strict per-value
  decode); decode-fuzz corpus gains valid_score_rich_with_repeats
  (DalSegno + voltas) CORPUS-LOCAL — shared render fixtures stay
  repeat-free until E1 (zero golden churn).

editor-core / gui / testkit / layout-ir:
- Barrier seam: repeat_event_refs + subjects_of arms +
  graph_structure_endpoints arm, gated end-to-end by
  repeat_authoring_kinds_derive_subjects_and_gate_on_barriers.
- Generators: operation_payload -> below(30); graph-session gen ->
  below(14); ops fuzz gen_payload -> below(27); layout_stub
  gen_operation_kind_tag unstaled 24..=29 (the Phase-3 tags were
  already missing); GUI name table.

Spec touch-ups (review findings): catalog says "strictly ascending";
the survivor tie-break is now explicit in the core rule-table row and
catalog (identifier-order minimum among multiple survivors;
proximity-aware four-key selection deferred, as for spanners).

Trail: ops + core DECISIONS entries; spec/PASS13_CANDIDATES.md opens
the Pass-13 ledger at three candidates (P13-K1; P13-D1 undo-driven
tombstones bypass ledger re-anchoring — pre-existing class; P13-D2
cue-cascade repair-trail ordering).

Full gate: fmt, clippy -D warnings, rustdoc -D warnings, 30 workspace
suites, conformance scale 1 (8/8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
2026-07-07 20:06:18 -04:00
Levi Neuwirth 437e0758a0 Phase B follow-up: real acceleration-snapshot path + base role bound
Review findings on 794400c. The criterion-4 harness stored current
(v2) Score bytes stamped V0 in the canonical_base slot and decoded
them with the unversioned decoder — bypassing the schema-major
snapshot contract Phase B built, and misusing the base role. Fixed
substantively: the harness stages a properly-roled ACCELERATION
snapshot (ChunkKind::Snapshot stamped for_major(2), referenced from
Manifest::acceleration_snapshots) and decodes through
Score::decode_canonical_versioned keyed by the stamped major — the
versioned seam now exercised end-to-end through the bundle, giving
the snapshot role its first real producer/consumer.

Consequences: max_supported_major(Snapshot) -> 2; and since the
per-kind gate no longer implies it, the canonical-base-stays-major-0
rule is enforced per ROLE (mis_stamped_canonical_base, consulted at
open and commit -> read-only + UnsupportedCanonicalChunkMajor;
regression test covers commit and reopen).

Also: three stale-doc fixes (decode_canonical now "current (schema
major 2)"; CreateRegionOp + StagedChunk::operation_block +
stage_operation_block docs describe minimal stamping, not just "v1
CreateRegion").

Full gate green: fmt, clippy -D warnings, rustdoc -D warnings, 30
workspace suites, conformance scale 1 (8/8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
2026-07-07 16:54:06 -04:00
Levi Neuwirth 794400c4c7 Schema major 2 Phase B: snapshot side + honest stamps (data-model fills)
The nine type bodies fill to the ratified Ch5 shapes and the wire moves
to the Binary Format §Schema Major 2 layouts, review-hardened (high-
effort review; 8 findings, 7 fixed + 1 deferral sharpened).

epiphany-core:
- 19 new leaf types in graph.rs (SlurKind/CurveDirection/
  CurvatureOverride/SpanStyle/LineStyle/SpaceUnit/SubBeam/
  BeamGeometryOverride/SpannerKind+5 payload types/RepeatKind/Volta/
  StaffBracketKind/MetadataEntry/MetadataValue/Timestamp/
  SoundConfiguration/TranspositionInterval/UnpitchedMember) with the
  ratified discriminants (cstyle_enum_codec! reused; tagged unions
  hand-written); nine struct fills appended in wire order.
- The frozen wire forms generalized to a SHARED sub-codec layer
  (enc_/dec_*_v1, v0==v1 for every type major 2 changed; vec framing
  through enc_/dec_vec_v1) used by the new decode_v1_score/
  encode_v1_score AND the rerouted v0 pair — major 2 touched types the
  v0 walk had treated as unchanged (metadata, staves, cross_cutting,
  staff instances transitively). Strict-canonical guards on every
  versioned path; decode_canonical_versioned dispatches {0,1,2} with
  composed default-fill migration.
- Invariants extended to the new reference-bearing fields (REVIEW
  FIX): Beam.sub_beams events, RepeatKind DaCapo/DalSegno anchors,
  Volta spans now covered by CrossCuttingRefsResolve + the anchor
  model walk, with negative tests.
- Tests: v1 migration size-anchor (v1 omits exactly the appended
  default bytes — the frozen encoder cannot drift), a non-default
  round-trip covering every new field and every SpannerKind/RepeatKind
  wire arm, fuzzer corpus gains genuine-v1 forms + the major-2 seam
  with enforced must-decode-Ok on unmutated frozen forms.

epiphany-ops + epiphany-bundle (Phase C's semantic core, landed here
deliberately — the live codec flip makes CrossCutting/Staff/Metadata
payload bytes v2 immediately; shimming nine transitively-embedded
types the major-1 D1 way was throwaway):
- Minimal-stamping OperationKind::schema_major per the ratified rule
  (CrossCutting/CreateStaff/SetMetadata always 2; CreateRegion 2 iff a
  carried instance bears Some(staff_lines_override) else 1;
  CreateStaffInstance/SetStaffLayout 2 iff Some else 0), unit-locked.
- Bundle op-block accept-set [0,2]; SchemaVersion::V2;
  beyond-accept-set tests moved to major 3; testkit V2
  stamp-derivation test.
- the_canonical_base_is_byte_identical_across_data_model_majors:
  pinned blake3 of a seeded reduction — the companion's SHOULD that
  the canonical base never moves across data-model majors.
- The op-payload migrate-on-read deferral restated precisely in
  DECISIONS (no consumer byte-reconstructs op payloads today; the
  first one must bring per-type frozen payload decoders).

Zero golden churn (fixtures deliberately carry v2 defaults).
Instrument::new consolidates the sweep's default fills. Full gate:
fmt, clippy -D warnings, rustdoc -D warnings, 30 workspace suites,
conformance scale 1 (8/8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
2026-07-07 16:30:07 -04:00
Levi Neuwirth 93f3dfde93 G-pass follow-up: unsupported pre-pass algorithm ids error
Post-commit review finding: req:pitch:spelling-algorithm /
req:time:decomposition-algorithm ratified MUST-error for unregistered
algorithm ids, but derive_annotations kept the pre-ratification
derive-nothing-under-honest-profile behavior (and a test locked it).
The spec text stands; the code moves: derive_annotations returns
Result<DerivedAnnotations, PrePassError>, rejecting unregistered ids
up front — a silently-empty derivation is indistinguishable from a
legitimately empty score, and would silently disagree with an
implementation that does support the requested id. All production
callers use the default profile (.expect); the stale lock test is
rewritten as unknown_algorithm_ids_error; PrePassError re-exported.

Full gate green: fmt, clippy -D warnings, rustdoc -D warnings, 30
workspace suites, conformance scale 1 (8/8).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
2026-07-07 11:32:05 -04:00