Commit Graph

197 Commits

Author SHA1 Message Date
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 acd7a7690f G3b packet 3a: the seven inbound surfaces a measure undo can strand
Undoing a CreateMeasure removes it from the owning StaffInstance, reached
through measure_values' carried parent rather than a search. The strand guard
covers seven surface classes, not the two earlier drafts assumed: spanner,
repeat, another measure's start, meter change, system break, page break, and
tempo segment -- the last at both its start and its optional end anchor.

It cannot use the structures index, which is event-only by construction and
would make the guard born green; rewriting it that way breaks fifteen tests.
RepeatStructure is not a CrossCuttingValue variant, so the modify chain
structurally cannot hold one and a repeat_values ledger carries it instead --
seven sites, no delete site. Deleted repeats stay in the map, matching the
ratified retention discipline and making the tombstoned-referencer row
reachable at all.

Owner rules are per surface. Six classes always have one; tempo segments split
by key, since a score-level segment has no owning object to test for liveness.

The five WriteChain surfaces are restoration-aware in both directions, and the
shared path now normalizes a value that does not reference the target to
absent. That is behaviour-preserving -- the guard only ever asks whether a
value names this target -- and it is what lets the two directions fail
independently: spanner chains have no explicit-removal form at all, and a
removed break is still Some((anchor, false)).

Also closes a hole that predates G3b: a minted TimeSignature still named by a
live Measure.time_signature now blocks its own undo.

Executed against spec/CONTRACT_GENESIS_G3B_MEASURE.md pin 10, mutations M48-M63.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-30 18:43:53 -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 b622498c19 G3b packet 1 repair 2: the real-write grid tie, on a fixture that cannot pass vacuously
The aggregate tie test added in b92023a never ran its own transaction. Envelope
counters jumped 3 to 10 to 11 to 12 to 20 while asserting seen_r1 continuity
across the gaps, so compute_pending's missing-vector-predecessor rule held both
transaction members and the undo permanently pending. The value it compared
against was the leftover pre-transaction baseline, not a restored one. It went
red under mutation only through its synthetic prediction half.

The tie-break was also too narrow. undo_transaction records the whole-grid and
per-key restorations under the same undo env.id, so after the undo commits both
chains carry Recency::Write with an identical stamp tuple. A prospective-only
tie-break left that real tie resolving to the whole grid while the graph held
the per-key value -- reachable today, since create_measure queries the oracle
with no overrides. Per-key now governs on any equal non-Base recency;
Base/Base still keeps whole-grid, because there the per-key chain has no write
and the overlay would delete the whole grid's entry.

The rebuilt fixture runs a contiguous 14-op sequence with the full prerequisite
chain and pre-mints every signature a SetMetricGrid names, since set_metric_grid
rejects a grid referencing an undeclared one. It asserts every op is exactly
Applied, that the effect count equals the accepted envelope count, and that no
conflicts or anomalies exist -- so a dropped op, a failed prerequisite, a
conflicted member or a skipped undo fails loudly instead of passing quietly.

Two proofs, deliberately distinct: the prospective tie via the aggregate
prediction, and the real-write tie via a CreateMeasure after the undo. Only the
second can see the widened rule.

Executed against spec/CONTRACT_GENESIS_G3B_MEASURE.md pins 6c, 7, 9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-30 13:49:39 -04:00
Levi Neuwirth b92023a032 G3b packet 1 repair: three defects the white-box tests could not see
Base-free c3 ordering was unsound three ways: it never checked that the two
measures share a StaffInstance, never checked they are live, and ordered by
OperationId, whose Ord is (replica, counter) authoring identity rather than
canonical reduction order. It now reads the parent from measure_values,
requires both Live, and compares the minters' stamp reduction tuples -- the
same tuple chain_recency uses. The old test discarded the parent, so it
encoded the bug.

An empty effective grid refused instead of abstaining. Pin 6c case 1 makes a
missing active signature vacuous for agreement and abstaining for boundary
distance, but agreement grouped GoverningElement::None with a disagreeing
Unique, and the boundary clause collapsed None into Indeterminate and then
failed closed. None and Indeterminate are now separate outcomes. The
white-box selector test passed throughout, so the new coverage is end-to-end.

Simultaneous prospective whole-grid and per-key restorations had no tie-break:
Recency::Prospective is a unit variant, so both compared equal and the strict
> handed it to the whole grid -- while real restoration records the meter
change after the whole grid under the same undo operation. The per-key write
now governs on equal recency, scoped to the prospective family only; a plain
>= would delete the whole grid's entry when both sides are Base.

The aggregate test asserts the oracle against the materialized post-undo
graph, not against itself.

Executed against spec/CONTRACT_GENESIS_G3B_MEASURE.md pins 6, 6c, 7, 9.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-30 13:12:04 -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 4b0abaf04b G3a undo repair: the tombstone branch was reachable all along
Undoing a transaction that minted a StaffGroup, PartDefinition,
AnalysisLayer, View, or Instrument tombstoned the object in the ledger and
left its value live in the Score. DECISIONS.md claimed ObjectState::Tombstoned
was unreachable through the public operation API; UndoTransaction over the
minting transaction reaches it with no delete operation required, and did so
for CreateStaff and CreateInstrument before G3a existed.

Adds five graph-removal arms to materialize_graph_tombstones and three
inbound-reference guards to undo_strand_block: StaffGroup blocked by a live
Staff.group, AnalysisLayer by a live ViewDefinition.active_layers, Instrument
by a live Staff.instrument. The guards read the carried-value maps rather than
the graph and are deliberately ungated, so base-free undo cannot strand a
reference the ledger can see; they require the referencer to be Live, and
exempt referencers minted in the same transaction. No restorations lookup:
none of those three fields has a modify operation, so there is no write chain.

Twenty-two mutation-signed rows across six families — removal, live-referencer
refusal, base-free guard behaviour, tombstoned-referencer permission,
same-transaction teardown, and objects-outranks-retained-map on re-create.

Executed against spec/CONTRACT_GENESIS_G3A_UNDO_REPAIR.md Packet A.
Records G3a's commit and corrects the kind/tag pair count to fifteen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-29 18:21:45 -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 25c473314b Sign t9 with a mutation that actually kills it
t9 attributed its mutation to t2 in both the contract row and the test comment.
The sweep disproved that attribution: t9 stayed green under t2's schema_major
change, because the from-empty spine reaches a note whatever the block stamps.
A mutation borrowed from another test signs nothing unless it is observed to
kill this one.

Signed with the dispatch-arm bypass instead:

    OperationKind::SetTuningContext(_) => OperationEffect::Applied

which skips tuning reduction without making the match non-exhaustive - so the
mutation isolates the reduction rather than being absorbed by a compile error.
Observed exactly as predicted: the spine operations stayed applied, the note
stayed reachable, and the test failed on the authored tuning-context
assertion, with the seeded default 440.0 surviving in place of the authored
441.0. Reversed by editing back; baseline green.

Both attribution sites now name that mutation and record why the previous one
was wrong, since a wrong mutation attribution is worse than a missing one: it
reads as signed.

Gate: 1410 tests, clippy 0, fmt clean, 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 10:18:57 -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 24f8c8099a Editor T4-pre W2: the glyph outlines become a shared typed seam
A canvas tessellator needs typed vector paths, and the outlines were private to
render-svg as SVG `d` strings. They move to a new epiphany-glyphs crate, which
implements layout-ir's GlyphCatalog with a real render_data.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-24 18:54:30 -04:00
Levi Neuwirth ec17d06e77 The identity cursor is ruled, and three records stop contradicting each other
RULING_GENESIS_PERSISTENCE.md §3 was the tranche's stated blocker. Ruled:
identity stays on Score and stays canonically encoded, byte-equality claims
confine to MaterializedState, and from-empty reduction derives next_counter
from the log.

Scoping it turned up that the section understated the problem. Verified in the
tree: epiphany-ops has no `.identity` reference at all, so reduction never
advances the cursor; invariant 11 checks only the reserved replica, never the
counter against ids present; and every mint from score.identity today is under
cfg(test). Score::identity is an authoring cursor reduction never touches, and
this tranche is what activates the hazard -- under from-empty the cursor sits at
the seed while the log already holds that replica's ids at 0..N. Divergent bytes
were the lesser problem, and none of the three options originally listed fixed
the larger one.

The manifest option, previously recommended, is rejected on evidence:
req:format:manifest-id promises two conforming writers derive identical
ManifestIds, which a replica-scoped field in a shipped content-addressed
structure cannot honour. The two wire options each cost schema major 4 on the
role 3b-i just froze at 3, and neither corrects the cursor.

Also reconciles two records against 011c68a. DECISIONS.md flatly prohibited a
SetTuningContext operation, which the ruling now requires; the prohibition is
marked superseded and re-scoped to what it was aimed at -- no tuning-only fix,
no wire widening to compensate -- both of which still hold. PASS13-S13 moves
from blocked-on to resolved-by, naming which of the four dispositions was taken.

Flagged for the tranche, not fixed here: bundle.rs documents the
OperationEnvelopeBlock cap of 2 with the tuning-context rationale in prose, so
that comment becomes false when the cap moves.

Gate: requirement_labels 6/6. No .md here is include_str'd or compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-24 15:17:55 -04:00
Levi Neuwirth 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 91d44dbe65 P13-S13 defers to the genesis-persistence ruling
Ratified 2026-07-24. S13 is re-pointed rather than resolved: the tuning context
is not a special case, it is field 10 of an eight-field gap that
spec/ANALYSIS_GENESIS_PERSISTENCE.md tables across the whole Score —
canvas.layout_defaults, instruments, staff_groups, parts, tuning_context,
spelling_precedence, analysis_layers, views, plus identity, with
decomposition_attachments prunable-but-never-authored. Each is reachable only
through a base the reducer is handed, and pruning is licensed to replace that
base with a MaterializedState carrying none of them.

Independently re-verified against the working tree before recording: every one
of those fields' sole reduce.rs mention is a read-only base-seed read, and
epiphany-ops has no tuning-context payload at all.

So the entry drops the operation-vs-canonical-base framing it was filed with.
That framing was right about the axis and wrong about the scope: it maps onto
the analysis's dispositions B and C at eight times the size, and a
tuning-specific fix is exactly the shape the analysis argues against — "any
option that enumerates fields must be re-audited against this table every time
a field is added to Score". S13 is now marked blocked-on rather than open, and
whichever disposition the genesis ruling picks resolves it as a side effect.

The accept-set cost this entry surfaced is kept, repositioned as evidence
against enumerated per-field fixes rather than as a tuning-only tradeoff:
blocks stamp minimally, and bundle.rs caps OperationEnvelopeBlock at major 2
precisely because no operation payload embeds the tuning context, so a
SetTuningContext-style operation would drag a role accept-set raise along with
the kind append — for one field of eight.

The wire layouts stay frozen and correct under every disposition. This was
never about how the tuning context encodes, only about which carrier embeds it,
so epiphany-core's DECISIONS entry now records the matching prohibition: no
SetTuningContext operation, and no widening of ScoreTuningContext's layout to
compensate.

Docs only; no code path touched. Gate unchanged: fmt clean, 1336 passed /
0 failed, requirement labels 6/6 at 212/282/282.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-24 14:23:36 -04:00
Levi Neuwirth 6d37f39826 Text-projection parity: smufl and overrides project, and a persistence gap surfaces
Schema major 3 put `smufl` and `overrides` on the binary wire and left the text
surface alone. This closes that: `TextValue for ScoreTuningContext` projects
five fields in `fn enc` order, with four new leaf impls (SmuflVersion,
SmuflVersionRequirement, TuningScope, TuningOverride) mirroring the four Codec
impls 3b-i froze.

The rule that moved them is the one this projection always followed — the text
projection is the same canonical surface the binary codec is. What changed was
not the rule but a premise: the old doc comment excluded all three fields
because "no schema major 3 has been opened", and 3b-i opened it.
`accidental_extensions` was staged out of that major and stays in-memory only,
so it is still correctly absent — the staging line now falls in the same place
on both surfaces, which is what the inverted tests assert.

No header-version bump and no document vector moved. This is a value-projection
change, not a document-format one: epiphany-textproj has no reference to a
tuning context anywhere, and the canonical base projects as a single opaque
byte atom (req:textproj:base-snapshot-inline), not structurally. So
req:textproj:header-version's sole accepted version (0 7 0) is untouched, all
13 document vectors are unchanged, and req:textproj:roundtrip was never
violated — the tuning context is not part of the projected document.

Both tests were renamed and inverted, following 3b-i's treatment of the binary
pair rather than rewriting the tranche-2/3a decision entries. Mutation-verified:
making `overrides` project as an always-empty vector — the exact regression this
fixes — fails both.

Two stale-pointer classes fixed, both of the kind this pass was hunting: the
comment in codec.rs naming the old text-test names and claiming the text
surface was unchanged; and ScoreTuningContext's per-field docs in graph.rs,
which still read "In memory only this tranche" for `smufl` and `overrides` —
stale since 3b-i, and contradicting the struct-level wire note they point at.
The struct-level doc was already correct.

Filed P13-S13, found while scoping and deliberately not fixed here: the tuning
context has NO canonical persistence path. Its only *persisted* carrier is the
acceleration snapshot, which Chapter 8 makes non-canonical and regenerable; no
canonical carrier embeds it at all. No operation authors it (the ops
vocabulary's only `tuning` references are the per-pitch TuningReference::Inherit)
and MaterializedState does not carry it. So a per-voice tuning override survives
a snapshot round-trip but cannot be authored, replicated, or merged, which makes
req:tuning:tuning-resolution-order's scopes 2-4 unreachable from any exchangeable
document. Not a regression — nothing ever worked; major 3 made the gap visible
by giving the value a wire form and no way to reach it.

The candidate prices both routes. Ratification is about the carrier, not the
encoding — the layouts are frozen and correct either way — but the operation
route is not quite the free schema-minor it first looks: blocks stamp minimally,
and bundle.rs caps OperationEnvelopeBlock at major 2 precisely because no
operation payload embeds the tuning context. An authoring operation carrying a
TuningOverride would stamp v3, dragging a role accept-set raise along with the
kind append.

Gate: fmt clean, clippy 0, 1336 passed / 0 failed, doc 0, conformance 8/8,
requirement labels 6/6 at 212/282/282.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 09:35:54 -04:00
Levi Neuwirth 51f7ac9f61 Push 4b tranche 3b-ii: one SmuflVersion, and a live ordering bug dies with the homonym
Closes P13-S12. epiphany-layout-ir defined its own SmuflVersion storing the
minor LITERALLY ({1,4} for SMuFL 1.4), so derived Ord sorted 1.3 and 1.4
BEFORE 1.12 — backwards versus SMuFL's real release order — on a direct field
of GlyphCatalogIdentity, which is layout-conformance identity. That bug was
live. The crate graph fixes the direction: layout-ir depends on core, so
core's fraction-normalized minor_centi type is necessarily the survivor.
layout-ir deletes its own and re-exports core's, keeping
epiphany_layout_ir::SmuflVersion resolving for downstream at zero churn.

No wire change: no schema major, no Codec, no decode vector. LayoutCache is a
regenerable major-0 role. The one line that moves bytes is encode_catalog's
.minor -> .minor_centi (04 00 -> 28 00 at offset 2, verified by encoding a
default identity: 60 bytes, unchanged elsewhere).

The tranche also closes S12's OTHER half, which had gone unnoticed: the
ratified shape reached Rust and the binary companion but core_spec.tex never
defined SmuflVersion at all — the exact "undefined leaf" S12 was filed about.
It is now declared once, with the hundredths rule as normative prose and the
release table as a rationale note, cross-referenced from both Chapter 9 sites
so the glyph catalog's version and the tuning context's are visibly one type.
No new req: label; counts stay 212/282/282.

Two ledger claims corrected, both verified false before dispatch rather than
after: PASS13_CANDIDATES.md's S12 entry and epiphany-core/DECISIONS.md's S12
bullet both promised this move would land "with golden regen". Nothing is
pinned to the catalog identity — every ResolvedLayoutIR::canonical_bytes()
assertion is RELATIVE (stability, determinism, and a sensitivity check that
mutates metrics_hash, never smufl_version), and the committed SVG/PNG goldens
embed no identity, only a fixed comment string. There was nothing to
regenerate, and conformance gate [9] never moved.

The fix is locked by a test that names the bug: layout-ir asserts the real
release order through GlyphCatalogIdentity.smufl_version. Mutation-verified
independently by making from_decimal store one-digit minors literally — it
fails with "minor_centi: 20 did not sort before minor_centi: 3", which is
precisely the deleted type's behaviour.

Gate: fmt clean, clippy 0, 1283 passed / 0 failed (+1, the new test), doc 0,
conformance 8/8, requirement labels 6/6 at 212/282/282. No golden, vector,
baseline, or editor-track file changed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:30:52 -04:00
Levi Neuwirth 68b08adb5e Push 4b tranche 3b-i: the score wire opens schema major 3
ScoreTuningContext gains `smufl` and `overrides` on the canonical wire.
`accidental_extensions` is deliberately STAGED to a later major: under
req:binfmt:frozen-layout a field addition is a major, so freezing a large
subtree whose consumer (the engraver) does not exist yet buys nothing and
risks a major-4 to fix a mis-shaped field. The three shapes 3a ratified
(Cents/CanonicalF64, AnchorPoint, SmuflVersion) stay reversible until
something exercises them.

Scope discovery: no operation payload embeds the tuning context, anywhere.
So the wire reached is the acceleration full-Score snapshot ONLY — the
canonical operation layer is untouched, no payload is born at v3, no frozen
v2 op-payload decoder is needed, and OperationEnvelopeBlock's accept-set
stays at 2 while Snapshot rises to 3. Major 3 is the first data-model bump
under which a chunk role's max does not move in lockstep. The canonical
base embeds no tuning context and stays major 0, byte-identical.

Migration: the live codec becomes v3; the 3-field form is frozen as
enc/dec_tuning_context_v2 and consumed by decode_v2_score (new, with its
byte-exact inverse encode_v2_score) and by the v0/v1 decoders — AND by
encode_v0_score/encode_v1_score, which the contract missed and which would
have silently corrupted the frozen forms through the strict-canonicality
re-encode check.

The frozen bytes are now pinned by a golden, because nothing else pinned
them. The cross-implementation decode corpus covers only the operation and
bundle surfaces — there is no epiphany-core vectors module — so every
existing test round-trips enc against dec and stays green under a
SELF-CONSISTENT reordering: swapping smufl and overrides in both halves
passed the entire workspace suite and 8/8 conformance, silently moving a
permanently frozen layout. schema_major_3_tuning_context_wire_bytes_are_frozen
asserts the exact encoding of the default (48 B) and a loaded (82 B)
context; it kills that swap. The S12 normalization is visible in the
literal: 1.4 stores minor_centi 40 (0x28), 1.12 stores 12.

Gate: fmt clean, clippy 0, 1282 passed / 0 failed, doc 0, conformance 8/8,
requirement labels 6/6 at 212/282/282 (unchanged — no new req: labels).
Mutations verified independently: breaking the v0 reroute kills 6 tests;
the wire-order swap kills the new golden.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:35:55 -04:00
Levi Neuwirth 456792debd Editor T2 W3: the goldens become conformance gate [9/9]
A golden-gate feature on epiphany-testkit: resvg + epiphany-render-svg as
optional regular dependencies (dev-dependencies cannot be optional; the
MSRV test job builds examples with --all-targets, so the raster stack must
stay out of the featureless closure — proven by cargo tree, 0 vs 1). Gate 9
re-derives the three T1a golden states headlessly and compares decoded RGBA
against the committed baselines; compare-only, failing loudly with a pointer
to the editor-gui diagnostic tests. Without the feature the suite's output
is byte-identical to before; with it, 9/9. The conformance CI job's
invocation gains the feature; CONFORMANCE.md documents gate [9] and the
split. Two mutations killed and coordinator re-verified.

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:58:28 -04:00
Levi Neuwirth b25b8debed Push 4b tranche 3a: the accidental vocabulary, in memory
The accidental / glyph / engraving type surface Chapter 4 puts on ScoreTuningContext
now exists in epiphany-core (new accidental.rs): ScoreAccidentalExtensions,
AccidentalDefinition, GlyphReference (Chapter 4's, recursive -- not layout-ir's
homonym), PitchSpaceModification, AccidentalEngraving with EngravingBoundingBox
and AnchorPoint, AccidentalCombination, SmuflVersion(Requirement), plus three
catalog_id! newtypes. All in memory, no Codec, canonical bytes unmoved -- the
reversible half; 3b freezes them on the wire.

The three ratified corrections land as filed:
- S10 Cents(CanonicalF64), not raw f64 -- reverting it to f64 is 9 compile
  errors, the type system is the test.
- S11 AnchorPoint { x, y: SpaceUnit }, core-native, frame pinned in its doc.
- S12 SmuflVersion { major, minor_centi }, built only through from_decimal which
  normalizes 1-digit x10 / 2-digit as-is, so derived Ord orders SMuFL's real
  history right. layout-ir's SmuflVersion untouched; 3b unifies and moves
  GlyphCatalogIdentity.

accidental_extensions and smufl join overrides as in-memory-only fields; the hand
codec's enc is byte-for-byte unchanged (three wire fields), only dec defaults the
new ones. The consumer that keeps this off the NOTEHEAD_ANCHORS path is real:
resolve_accidental (override > addition > base) and the
accidental-modification-compatibility invariant wired into check_invariants.
Glyph/engraving metadata is carried but its deep consumer is the engraver, out of
core -- said honestly, not faked.

Verified independently of the agent. Through Score::canonical_bytes: a non-empty
accidental_extensions + non-default smufl encode byte-identically to all-default
(268 both) and decode back to empty -- all three fields off the wire. SmuflVersion
orders 1.12 < 1.18 < 1.20 < 1.3 < 1.4 (the trap: 1.12 before 1.3). CanonicalF64
rejects NaN/inf. And the compatibility invariant is non-vacuous: weakening the
predicate myself made the edo-31 reject test fail. No Codec, no golden moved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 14:48:29 -04:00
Levi Neuwirth c68f317e47 Push 4b tranche 2b: the ten historical temperaments resolve
The ten temperaments tranche 2 left Deferred now resolve, via
TuningResolution::Function with reserved built-in ids. Each is a
[FifthTempering; 12] descriptor over the fixed circle of fifths -- Pure,
NarrowPythagorean(f), WidePythagorean(f), NarrowSyntonic(f), NarrowSchisma, or
Residual (the closing wolf, computed as whatever brings the chain to seven
octaves). One walk places the notes and derives their ratios; the same code path
yields the wolf for the four non-circulating temperaments and full closure for
the six circulating ones. Comma sizes are 1200*log2(exact ratio), never
hardcoded cents. Still no Codec, no wire movement -- canonical bytes unmoved.

The closure invariant is recomputed in code, which is the whole reason this was
its own pass. Six circulating temperaments sum to one Pythagorean comma
(23.460010c); four non-circulating wolves match the ratified residues
(pythagorean 678.495, meantone 737.637/725.809/717.923) within 0.001c.

Verified independently of the agent. Through the public resolver, pythagorean,
kirnberger-ii and kirnberger-iii reproduce the spec's derived-cents tables to
0.01c -- and the Kirnberger tables only close WITH the schisma fifth, so the
match is end-to-end proof the schisma is present and the S6 trap avoided. The
kirnberger-ii/iii D discriminator holds (203.910 vs 193.157), meantone-1/4's
major third is the just 5/4 (386.314c), werckmeister-iii's C# is distinct from
tet-12's. And the closure test is non-vacuous: dropping kirnberger-ii's schisma
fifth myself made it fail -- the S6 defect reproduced as a mutation and caught.

TuningParameters is a documented zero-field marker (no built-in parameterizes
Function, its schema is unspecified); an unknown TuningFunctionId fails closed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:31:29 -04:00
Levi Neuwirth 6fa14c76e3 Push 4b tranche 2: tuning becomes resolvable, in memory
A pitch now resolves to a frequency. The tuning-resolution vocabulary
(TuningSystem, TuningResolution, TuningOverride, TuningScope), a partial built-in
catalog, and the five-scope resolver land in epiphany-core, entirely off the
wire -- the reversible half of the remaining tuning work, exercised and proven
before the schema-major-3 bump freezes anything.

Nine of twenty systems resolve: the six tet-* (EqualTemperament, each paired to
the pitch space of matching chromatic cardinality -- tet-12/cmn-12,
tet-N/edo-N), and the three ji-static-5limit-{C,G,D}, whose twelve ratios are
COMPUTED from the lattice block {3^a 5^b | a in [-1,2], b in [-1,1]} in exact
integer arithmetic, never a pasted table. The other eleven fail closed with a
distinct NotYetSupported (vs UnknownTuningSystem): the ten historical
temperaments await tranche 2b, ji-adaptive-5limit awaits HarmonicContext.

overrides is added to ScoreTuningContext as the one field the scope walk needs,
in memory only. Its struct_codec! -- whose dec constructs a literal of exactly
the named fields -- is replaced by a hand-written Codec that encodes the three
wire fields and defaults overrides on decode, with a matching hand-written
TextValue.

Verified independently of the agent that wrote it. Through the real
Score::canonical_bytes() path: a populated overrides encodes byte-identically to
an empty one (268 bytes both) and decodes back to empty -- the field never
reaches the wire. tet-12 A4=440 resolves C5 to 523.2511306011972 Hz by hand.
ji-static-5limit-C's major third is 386.3137c against tet-12's 400.0000c, the
5/4 just third distinct by the expected 13.69c. The wire-invariant test
mutation-killed by leaking the override count from the codec (the first attempt,
encoding the field itself, was a compile error since TuningOverride has no Codec
-- meaningless as a mutation, redone). No golden or fuzz digest moved.

Also corrected a doc comment on pitch.rs's sounding_equivalent that this tranche
falsified: it said frequency resolution is "not modeled in this crate", which is
now untrue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:19:21 -04:00
Levi Neuwirth 2cf2dae31b Push 4b tranche 1: pitch spaces become structure, and cmn-24 transposes end-to-end
The Chapter 4 pitch-space vocabulary lands in epiphany-core as in-memory types
with a real consumer, the built-in catalog as PitchSpaceId -> PositionStructure
data, and the P13-S2 "cmn-12" string guard is replaced -- not preserved beside --
by structural resolution over that catalog. Dispatched as one vertical slice
rather than the plan's three steps, because a Chapter 4 type surface with no
consumer is the shape that produced Staff::default_clef and NOTEHEAD_ANCHORS.

No Codec impls, no struct_codec! additions, no Score/ScoreTuningContext field:
these types stay in memory so Ruling C keeps them free to change. Canonical
bytes are byte-identical -- no golden or fuzz digest moved.

Six of the thirteen built-in pitch spaces are underdetermined by the spec (the
three ji-* generators, and all of maqam-base/gamelan-slendro/gamelan-pelog);
they resolve to None and fail closed at every consumer rather than being
invented. SpellingParameters, whose shape the spec calls an open question, is a
documented zero-field marker for the same reason.

The plan's proof of life -- cmn-24 transposing end-to-end -- held at the core
layer and FAILED one layer up, invisibly to the tranche's own tests.
resolve_transposed_spellings took the transposed 12-TET semitone with a ? before
checking whether any spelling needed it, so the 12-chromatic-only gate refused
every cmn-24 transpose, even one with nothing to spell (measured: NoOp
TranspositionOutOfRange, value untouched). P13-S3-shaped: latent in code, made
reachable the moment the space resolved. Moved the ? to point of use; a
spelling-less cmn-24 now transposes, one with an authored spelling still refuses
(the pre-pass cannot write a 24-chromatic accidental). Two tests, each killed by
a different mutation.

Verified independently of the agent that wrote the core half: the "cmn-12"
string survives only in doc comments and fixtures, not control flow; the
proof-of-life arithmetic (B4 -> natural C5 in cmn-24, chromatic 22, 118 - 120
carrying to octave 5) recomputed by hand; the strictly-increasing third clause
of req:tuning:diatonic-chromatic-mapping mutation-killed; full gate green (fmt,
clippy 0, workspace 0 failed, rustdoc 0, conformance 8/8, requirement counts
unmoved at 212/282/282).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes P13-S6.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:54:25 -04:00
Levi Neuwirth b3449256f9 P13-S5 and P13-S6: the two inherited claims were both true
`epiphany-core/DECISIONS.md` had carried two Push-4a audit claims as
**unverified** through two passes, with the standing note that they "should be
checked, not inherited". Checked. Both are real, and both are Chapter 4 defects
standing in front of Push 4b rather than inside it -- which is the point of
checking before scoping, not after.

**P13-S5 -- the JI prime basis is specified at two lengths.**
`req:pitch:ji-vector-basis` says the built-in JI spaces order primes ascending
*starting with 2*, and that `components.len()` MUST equal the basis size. The
built-in pitch-space table calls `ji-5limit` "Two-dimensional (prime axes 3,
5)", `ji-7limit` three-dimensional, `ji-11limit` four-dimensional -- each
exactly one short, consistently, because the table is octave-reduced and the
requirement is full-register. `req:tuning:builtin-tuning-catalog` makes the
table normative, so a 5-limit vector is required to be both length 2 and length
3. The requirement's own octave-reduction clause does not reconcile them: it
normalizes the first component to a canonical range, it does not remove it.
Same family as P13-S2 and P13-I1, and cheaper -- a one-side edit once ratified.

**P13-S6 -- no built-in tuning system's resolution is pinned to a versioned
definition, and 14 of 20 have no definition at all.** The claim as inherited
("the named historical tunings lack exact deterministic ratio data") understates
it. It is not a missing table: `TuningResolution::Function` delegates the
historical temperaments to a `TuningFunctionId`, which Chapter 10 lists as an
*extension point*, and no built-in is mapped to a function id nor any function
id pinned. Only the six `tet-*` entries are actually specified, by
`EqualTemperament`'s structural rule. The other 14 are names: three meantone
variants, `werckmeister-iii`/`iv`, `vallotti`, `kirnberger-ii`/`iii`,
`young-ii`, `pythagorean` (the 3:2 ratio is named, the fifth-chain construction
and wolf placement are not), three `ji-static-5limit-*`, and
`ji-adaptive-5limit`. Set against `req:tuning:tuning-resolution-determinism`,
which requires determinism *across platforms*, two conforming implementations
may each choose a different published Werckmeister III and both pass -- in a
project that runs a cross-implementation conformance step.

The fix pattern is already in-house and was found by looking for it:
`req:pitch:spelling-algorithm` pins `SpellingAlgorithmId "default"` at version 1
to a named algorithm and errors on any other identifier. Nothing equivalent
exists for tuning. Which published variant each built-in denotes is a
ratification question, not an implementation one.

Both are filed open in the Batch-3 ledger. Neither was needed for 4a; both are
load-bearing for 4b, whose whole job is resolving a scale position to a
frequency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:03:39 -04:00
Levi Neuwirth e7ebd8ac9c P13-S2: the alteration is space-relative, and the core fails closed
`cmn-24` was declared in the built-in pitch-space table as "CMN extended with
24-EDO quarter-tone accidentals" while Chapter 2 fixed a `Cmn` position's
absolute semitone at `nominal.chromatic() + alteration + 12*octave` for every
space, unqualified. Two ratified MUSTs, each correct in its own chapter,
jointly unsatisfiable -- P13-I1's two-listings drift in another costume.

The tracker parked this as "a data-model major". It is not.
`req:binfmt:frozen-layout` names `PitchSpacePosition` as one of four open
value-layer vocabularies, so three of the four available options cost no schema
event at all. The one that *is* a major -- regrading `alteration` onto a finer
fixed unit -- is also the only one that rewrites the canonical bytes of every
pitch ever authored, and it buys a single grid while `edo-31`/`53`/`72` stay
unrepresentable. It is explicitly ruled out.

Ratified instead: a `Cmn` alteration and a `CmnChromatic` modification are
denominated in steps of the enclosing pitch space's chromatic layer. One rule,
not a special case for one catalog row -- it settles `cmn-24`, `maqam-base`,
and `PitchSpaceModification::CmnChromatic` together. `cmn-12`'s step is the
semitone, so every score in existence keeps its meaning and **not one canonical
byte moves**; `canonical_pitch_bytes` writes the space id before the position,
so a `cmn-24` E-half-flat and a `cmn-12` E-flat already derive different
`PitchId`s.

The defect was larger than `cmn-24`. Nothing bound a `Cmn` position to a
12-chromatic space: `ScalePosition { space: "edo-31", position: Cmn { .. } }`
constructed, validated, encoded and transposed, and meant nothing. Neither
`Pitch::transposed` nor `twelve_tet_semitone` ever read the space. Until Push
4b resolves `PitchSpaceId` to a `DiatonicOverChromatic` structure, both now
fail closed outside provable built-in `cmn-12` -- a capability check, not a
claim that the identifier defines the structure. False refusal for a
score-defined 12-chromatic space is accepted over silently wrong arithmetic.

No wire change. `TransposeRefusal::PitchSpaceUnavailable` is a new *diagnostic*
mapping to the existing `PreconditionFailureReason::PitchSpaceMismatch` (6);
appending a discriminant for a guard Push 4b must delete would have reserved a
permanent wire value for a temporary mechanism, in exactly the artifact the
fail-closed ruling exists to protect. Assignments 10-15 are untouched.
Operation Catalog 0.9.0 records the broadened case *and* amends the 0.8.0
rationale, which asserted that discriminant 6 never consults a pitch-space
registry -- true of the non-`Cmn` check, false of this one.

Two spec-side fixes found in review, neither visible to any test: the
`TranspositionInterval` listing had lost its `pub chromatic_steps: i32,`
declaration to a comment rewrite, leaving a one-field struct above the sentence
"Both components are load-bearing"; and `pitch.rs` still documented
`alteration` as "in semitones", contradicting the requirement landing beside
it. Both new requirements are now cited from the code that implements them --
the label checker catches cited-but-undefined, not the reverse.

Push 4b keeps the registry work and loses the contradiction. P13-S4 is the last
item open in Batch 3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:00:09 -04:00
Levi Neuwirth 043c18cabf P13-S1: every requirement is named, and now it is also numbered
168 of core_spec's 207 requirement blocks carried no `\label`, so no conformance
claim could cite them. All 207 are labelled now; the suite is 277/277.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Twelve grammar-gate tests, all mutation-verified.

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:43:09 -04:00