spec/HANDOFF_2026-08-07.md: state of the spec / Pass-13 / format-epoch
thread at be244df, the live constraint that no bundle may carry a
canonical base until P13-S27 lands, the S28 -> S27 -> S16 chain with
S27's three inherited obligations, the working agreements that are not
derivable from the code, and the environment notes the other machine
needs (xelatex not pdflatex, the 1.95.0/1.85 toolchain pins, the
cargo fmt --all trap and why its --check form is safe).
Section 2 covers the parallel editor/T4 thread and is explicitly
bounded: those files were out of bounds for this session all along, so
it records only what shared git history shows plus leads to verify, and
says plainly that it is not a substitute for that session's own handoff.
It does name the one place the threads can collide — the canonical-base
interval — which neither side can see from its own side.
Ledger: P13-S27 and P13-S28 both still opened with "open" while their
resolutions sat further down the cell. S27 is UNBLOCKED and
dispatchable; S28 is IMPLEMENTED. The cells are appended to rather than
rewritten, so an opener can lag the truth by several rungs; the handoff
records that as a reading hazard.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
document_from_bundle refused a base-bearing bundle, but the public
project_text_document did not. A caller holding a directly constructed
TextDocument could therefore emit a (canonical-base ...) line that
parse_document then rejects — a projector able to produce what the
parser refuses, which is precisely the asymmetry pin 3b exists to close
and which req:textproj:roundtrip's second equation quantifies over.
The guard had been placed on the path the pin happened to name rather
than on every path a caller can reach, and the unguarded one was the
only reachable half: no live Bundle can carry a canonical base during
the S28 -> P13-S27 interval, so the bundle-side refusal cannot fire
today, while the document-side path is one public call away. The new
corpus vector proved the hole existed rather than closing it — it is
built by projecting a base-bearing document.
project_text_document now returns Result and refuses. A crate-private
render_text_document keeps the unchecked formatter for its one
legitimate caller, the canonical_base_present negative vector: a
negative vector still has to contain the spelling it asserts is
refused, and producing those bytes is not the same as permitting them.
Every other vector goes through the checked projector.
projecting_a_base_bearing_text_document_is_refused locks both halves —
that the public projector refuses, and that the private renderer still
emits the section, since the reject vector silently stops carrying its
spelling otherwise. Mutation-verified: removing the refusal fails that
test and nothing else. Restored by hand.
The corpus is byte-identical, so no vector regenerated. Workspace green
at 1570.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
FORMAT_MAJOR becomes 1 and FORMAT_MINOR restarts at 0. The decoder
stops being exact-major-only: it classifies three ways through a named
FormatEpoch carried on FixedHeader, so major 0 is decoded deliberately
as legacy rather than refused. Old readers already fail closed on an
unknown major, so that half needed no mechanism — which is why the major
is the right carrier, and why the header's immutability, fatal to
FORMAT_MINOR as a provenance field, is what makes it sound as an epoch
field.
The matrix: a major-0 bundle with no base may open; one carrying a base
is refused; one attempting to add a base is refused and told to repack.
That last row is the non-inheritance rule. Three errors, none of which
degrades to read-only: two permanent legacy/repack errors, and
ReductionAuthorityUnavailable, which is temporary, names P13-S27, and
must not say repack — a major-1 container is already the right epoch.
Until P13-S27 lands, both major-1 base boundaries are closed: opening a
major-1 bundle that already carries a base, and committing one into it.
Neither may be left open while the epoch asserts a validation that never
ran.
Text projection cannot mint a base. serialize_document staged a carried
base into a fresh bundle and build_manifest wrote it, so an old or
hand-authored document could be laundered straight through the boundary.
All three sides now refuse: projection, parsing, and a new dedicated
SerializeError variant — none of which existed to be "retained".
COMPANION_VERSION moves to 0.14.0 and the corpus is rebuilt to 20
vectors and ten rejection classes, with canonical_bases reach dropping
2 -> 0. That is a real capability loss and is recorded as one.
Corruption keeps precedence in both epochs: a corrupt major-1 base fails
as malformed, never as the temporary authority error a user would
reasonably retry.
All 11 mutations were run and observed, not reasoned about. M4 is the
signing one — with the legacy commit refusal removed, a legacy container
gains a base in place, which is exactly the counterexample that killed
FORMAT_MINOR. M11 confirms the third error is distinct while test 3
stays green, proving the mutation stayed inside the major-1 branches. M7
fails on both epoch halves. M8 falls through to
SerializeError::Bundle(ReductionAuthorityUnavailable), confirming the
text layer's own refusal is what the test asserts.
Two touch-table gaps surfaced during execution, both the same shape: a
.tex requirement addition moves hardcoded counts in
requirement_labels.rs, and the companion bump moves a second normative
version literal spelled version~0.13.0 rather than (0 13 0). Neither
file was in any touch table; the second was caught only because a test
exists for exactly that failure.
P13-S27 is unblocked — its pin 2a is resolved from outside, as its own
prohibition required — and inherits three obligations: both interim
refusals converted to validation, M8's deferred laundering
demonstration, and pin 3c's two suspended conformance assertions.
P13-S16 remains blocked on S27.
Workspace green at 1569.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
P13-S28 asked for a container property old readers cannot silently
accept and a later commit cannot inherit unchanged. The format major is
that property. FORMAT_MAJOR moves 0 -> 1, FORMAT_MINOR resets to 0, and
the decoder stops being exact-major-only: it classifies three ways
through a named FormatEpoch, deliberately decoding major 0 as legacy.
Old readers already fail closed on an unknown major, so that half needs
no new mechanism — which is why the major is the right carrier and the
header's immutability, fatal to FORMAT_MINOR as a provenance field, is
exactly what makes it sound as an epoch field.
The eight-row matrix carries the rule: a major-0 bundle with no base may
open, one carrying a base is rejected, and one attempting to add a base
is rejected and told to repack. That last row is the non-inheritance
rule. Legacy resolves to hard rejection, never read-only — a
pre-authority base is not a restricted-but-correct view.
Three things the review rounds found, none visible at filing:
It cannot stamp major 1 before S27's writer enforcement exists. Pin 3a
therefore closes both boundaries temporarily — opening a major-1 bundle
already carrying a base, and committing one into it — through a third,
temporary error that names P13-S27 and must not name repack, since a
major-1 container is already the right epoch.
Text projection launders provenance straight through the boundary:
serialize_document stages a carried base into a fresh bundle and
build_manifest writes it. Resolved as symmetric document-level refusal —
projection, parsing, and a new dedicated SerializeError variant. None of
the three existed to be "retained"; an earlier draft claimed otherwise
and was wrong. This forces COMPANION_VERSION to 0.14.0 and rebuilds the
committed corpus to 20 vectors and ten rejection classes, with
canonical_bases reach dropping 2 -> 0. That is a real capability loss
and is stated as one.
Corruption precedence binds in both epochs. A corrupt major-1 base must
still fail as malformed, never as the temporary authority error a user
would reasonably retry on a container that is in fact tampered with.
11 pins, 11 tests, 11 mutations, 15 touch rows, 7 gate items. S27's
contract is a mandatory touch: pin 8 resolves its open pin 2a — legacy
bases are refused by container epoch, never by version arithmetic.
Documentation only. Not implemented, not dispatched. S27 and S16 stay
blocked until this rung lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
S27 installs a reduction authority but cannot say what to do with a
base that predates it. A raw ReductionAlgorithmVersion is a bare u32
with no provenance, and the text-projection parser accepts an unbounded
one from a document, so no numeric convention — including a high epoch —
is safe from a hand-authored file declaring it.
FORMAT_MINOR fails too. The header never changes after creation and
commit publishes only a superblock, so a legacy bundle that commits a
freshly validated base keeps its old minor forever: rejecting minor-<=1
bases rejects one the authority just accepted, and accepting them leaves
S16's version ambiguous. Separately, a minor change may only append
append-safe discriminants, and current readers ignore minor entirely, so
the boundary would bind only readers that already comply.
What survives is the requirement: provenance must ride a container
property old readers cannot silently accept and a later commit cannot
inherit unchanged. S28 owns that, plus the old-reader rejection
boundary, legacy rebuild/repack behaviour, every writer path including
text projection, and the exact format-version consequences.
S16 and S27 both blocked on it; the chain is recorded in all three rows.
Ledger only.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Scoping disposition A found two things that move the rung's size in
opposite directions.
Cheaper: the refusal needs no new PreconditionFailureReason and no
schema-minor epoch. reduce.rs:1236's container_not_empty() helper
already covers "a create carrying children" by its own doc, and three
creates already call it for exactly this shape. create_staff_group is
the sole outlier.
More expensive: this is a canonical reduction-semantics change, not
merely a behaviour change. The same operation set now reduces to a
different Score, so core_spec.tex:11614 applies — canonical bases
materialized beforehand cannot be reused without rebuilding. That
requirement is currently unenforceable, so the contract is complete and
ratifiable as a plan but explicitly not dispatchable.
S27 is why. The version machinery is self-referential:
reduction_version_for sources a new superblock's value from the
canonical base's own self-report, and open compares it only against the
superblock that value seeded. The check is not vacuous — it catches a
corrupt base disagreeing with its superblock — but it necessarily
passes for a conformingly propagated stale base, which is the case the
requirement exists to prevent.
An earlier draft of pin 0 claimed no writer path existed at all. That
was false, and the way it was false is recorded in both the contract and
the S27 row: the search behind it looked for constructor calls, which
cannot find a path that propagates an existing value without
constructing one. The instrument could not observe the thing it was used
to rule out.
Docs only. No code, no spec sources, no implementation authorized.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
S8 carried a false mutation claim. It said flipping `is_none_or` to
`is_some_and` at the constant-tempo check killed no test; three tests
kill it — two in graph_reduction, one in convergence reporting the
witness verbatim. Executed and restored by hand. The original was
reached by reading only invariants.rs's own tests, which assert
`fires(...)` and survive the flip; the kill sites live in another crate.
The genuinely unconstructed spelling is the opposite one: `Constant`
with `Some(equal)` has no construction site anywhere.
S8's ratification also stops presenting normalize-on-encode as the
default. Folding one of two accepted byte forms violates
req:binfmt:decode-vectors' injectivity rule, and
req:binfmt:compression-none-parameter is the ratified precedent for
refusing exactly that leniency. A repair must reject one spelling or
keep both as distinct canonical values.
S16 had drifted in every code citation, some by hundreds of lines, and
never named what makes its fix expensive: t8b_both_permitted_stale_forms_hold
pins both stale forms as passing and documents disposition A's rules as
mutations that must break it. The fix is the mutation an existing test
exists to detect.
S25 files disposition B of S22 — corpus rows named for their variant.
Complementary, not a replacement: it reaches other implementations, but
its failure still reads as corpus staleness.
S26 files a doc comment claiming a core_spec repair that never landed.
A P13-S9 instance, and the sharpest: the side that is wrong about the
other is the grep-guarded side. Its evidence must not be repaired alone.
Locators only in this commit; no code, no spec, no contract.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
`operation_kind_tag_vocabulary!` generates `discriminant()`,
`from_discriminant()`, `catalog_name()`, `introduced_minor()` and
`PAYLOAD_FREE` from one list, and `PAYLOAD_FREE` carries variants only.
Every derived test therefore obtains a tag and its byte from the same
invocation and asserts `$disc == $disc`. It cannot disagree with the
macro, because it is the macro.
The kind side never had this problem: `OperationKind::discriminant()` is
a hand-written match, so two independent statements exist and
`operation_kind_wire_discriminants_are_golden` asserts they agree. This
adds the tag side's second statement — `tag_wire_discriminants_are_golden`,
a hand-typed 40-row literal table transcribed by reading the macro
invocation rather than derived from its output.
Coverage is computed, association is not: a 40-long array does not prove
forty distinct tags, so the table's totality over the vocabulary is
asserted separately, and the comment says why that is not circular.
Payload-free tags assert the whole canonical byte vector — which also
proves the length-1 property the retired test had and the kind-side
idiom lacks — while `Registered`, the one tag carrying a payload,
asserts `[0]` alone.
Retires `phase3_tag_discriminants_are_golden`, whose whole subject was
tag→byte for 24–29. Keeps the three assertions at payload.rs:3086,
reduce.rs:12744 and reduce.rs:15941: the table duplicates their tag→byte
subclaim but not the kind-and-tag pairings that contain them, nor G3b's
local mutation evidence.
Signed by the coordinated 32↔33 permutation — swapping the discriminant
literals *and* the declaration lines, so `PAYLOAD_FREE` still emits
ascending discriminants and every derived artifact stays byte-identical.
Before: 1558/0, silent. After: fails naming SetCanvasLayoutDefaults.
No wire, schema, specification or corpus change. Suite 1558/0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Filed as a deferral -- pickups unmodelled -- it is closer to a live defect, and
the tree already held the proof. m35 placed a first measure at offset 0 and its
successor half a whole note later under a whole-note signature and asserted
invariant 20 fires. That is a pickup. The test has been labelled "wrong distance"
since packet 2. create_measure applies the same rule as a refusal, now observed
end to end rather than cited: the successor comes back NoOp with
MeasureMeterMismatch. Authoring a pickup does not leave it unmodelled; it makes
the rest of the instance unauthorable.
Both refusals carry the same reason code, so the fixture is the only thing
separating them. Pickup and successor both declare None, which keeps clause 2
from running on either side and makes the observed refusal provably clause 3's.
The pickup's own mint is asserted Applied before the successor's NoOp, because a
fixture whose operations never execute produces a non-Applied result
indistinguishable from a refusal.
The exemption is narrower than every document said. A first measure escapes only
the predecessor-dependent checks -- invariant 20's boundary clause, and
create_measure's clauses 1 and 3 -- plus agreement when it declares None or a
matching signature, and only when its other preconditions hold. It can still be
refused for a dead parent or an unresolving anchor referent, and invariant 10 can
still flag it. Seven surfaces carried the loose form; one had hardened into
falsehood, claiming all three clauses are vacuous for a first measure when
clause 2 has no predecessor dependency at all.
core/DECISIONS.md is deliberately untouched. It already said "never flagged by
the boundary clause" -- the one site that drew the distinction correctly -- and
an earlier contract draft listed it as defective by matching the phrase without
reading its qualifier. The corrected ops entry now quotes that qualifier, and a
positive gate check protects it.
A mid-score partial enters successfully and its successor fails, so the scope is
boundaries following any partial measure, not partial measures. The root cause is
a missing quantity rather than a missing exemption: both rules compare the
start-to-start distance against the governing signature's full measure_duration
when it actually equals the predecessor's own content duration. Introducing that
quantity is a semantic rung; this one stops at its edge, with both function
bodies byte-identical.
P13-S24 is filed for the Chapter 3 splitter deferral, which shares the missing
partial-duration concept and is otherwise independent.
Executed against spec/CONTRACT_P13S19_PARTIAL.md, four mutations.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Invariant 20 has nine non-success paths, not the three P13-S18 recorded. Only
three are abstentions: agreement-Indeterminate, boundary-Indeterminate, and an
incomputable boundary delta. Two are delegated to invariant 10, two are vacuous,
one is inapplicable, one is P13-S19's pickup deferral. The entry had been
counting all of it as gap.
Delegation is proved, not asserted. Deleting invariant 10's per-measure arm
leaves the condition unreported by the entire workspace suite except by the two
tests that name it; the same holds for the instance-local-grid arm. A delegation
nobody discharges would have been an abstention with a better name.
Every abstention cell carries a paired positive control, because silence is the
same observation for all nine paths. Each test asserts zero violations on the
fixture that takes the claimed path, then changes only that path's dependency
and asserts the clause decides with the expected witness. The control has to
observe the clause the cell names: S8's first version restored the governing
search by moving prev, which broke prev<->x comparability and left the boundary
silent for a second reason, signing the cell by inference. Moving the grid edge
instead keeps both measures c4-comparable and the boundary clause itself fires.
S2 has no such option -- a WallClock delta is never computable -- so its control
legitimately observes prev's agreement, and that exception is S2's alone.
Three shapes claimed a clause pair no single measure exhibited: m0 carried a
resolving signature at index 0 and m1 carried None, so the pair was really
A4+B1 on one measure and A1+B4 on the other. A boolean over the whole invariant
cannot see that, which is how it survived the first pass.
No behaviour change. check_measure_meter_consistency's executable body is
byte-identical to f33673d at 4871 bytes, verified by brace-matching from the
signature rather than a sentinel; every red observation came from fixture data or
from invariant 10, never from invariant 20's own logic.
P11-C5 was never this residue's gate -- it is a re-anchoring proximity metric.
P13-S23 is filed for the real dependency: placing anchor pairs on a common
timeline and measuring musical distance wherever c1-c5 do not already yield
both. It owns two disjoint deficiencies, since c3 and c5 order without
supplying any delta.
Executed against spec/CONTRACT_P13S18_MATRIX.md, 18 cells and 10 mutations.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Adding Registered = 16 made the inventory six fragments -- 24-29, 34, 35-38, 39,
16, and 1 -- but the repair sentence still said "superseding the five scattered
fragments rather than adding a sixth". The count was right when written and went
stale in the same commit that lengthened the list it counts, which is the entry's
own subject matter arriving one row early.
Ledger-only. No code, wire, or specification change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Registered joins the semantic-lock inventory. Its corpus row is emitted under the
variant name at ops/src/vectors.rs:210 and its committed literal leads with 0x10
at spec/vectors/decode_vectors.txt:80, so the drift comparison binds the
association. The uncovered set narrows to 0, 2-15, 17-23, and 30-33.
Verifying that turned up a false clause of my own. The entry said the corpus's
tag coverage is unchosen and unmaintained. It is neither: vectors.rs:201-204
emits one row per tag straight from the vocabulary and states the reason -- a
hand-picked subset is how TransposeInterval shipped encoding to a byte its own
decoder rejected.
But the rows lock byte-to-byte, not variant-to-byte. Each is named
tag_{discriminant} and carries [discriminant], both derived from the value alone,
so tag_32 asserts that 0x20 round-trips and never that SetCanvasLayoutDefaults is
32. All forty rows are identical under a permutation; what moves is their order,
since PAYLOAD_FREE is declaration order. That is why the 32<->33 probe failed,
established from the committed file's ascending tag_NN rows rather than inferred.
Which sharpens the entry rather than weakening it: Registered's row has exactly
the property the numbered rows lack, because it is named for its variant. The gap
is that one row's discipline is not the vocabulary's.
Ledger-only. No code, wire, or specification change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Three citations in P13-S15 had drifted when the golden-lock comment was added:
the table (:2220 -> :2233), the_tag_vocabulary_is_complete (:2569 -> :2652), and
phase3_tag_discriminants_are_golden (:2645 -> :2728). The tag residue moves out
of P13-S15's closing sentence and into an open row of its own -- a residue
recorded only inside a resolved entry is a residue that gets lost.
P13-S22 was drafted claiming a tag permutation is invisible, with 32<->33 as the
demonstration. Running it falsified the claim, so the entry says what was
observed instead. Three permutations, three catches: 32<->33 and 2<->3 by the
frozen decode-vector corpus, 1<->2 by layout-ir's edit-barrier golden blob. Each
restored by hand; suite back to 1541/0 with payload.rs byte-identical to dcb28f0.
So the gap is narrower and different from the draft. Semantic tag-to-byte locks
cover 24-29, 34, 35-38, 39, and -- incidentally, in a blob comment -- 1. Tags 0,
2-23, and 30-33 have none. What defends them is byte-level goldens that embed the
tag by accident and report a permutation as corpus drift or a moved blob, never
as a moved wire discriminant. The coverage is real but unchosen and unmaintained
as coverage, which is the hand-maintained-table failure mode wearing a costume.
The entry carries a probe-design note, because the obvious next probes are now
known to fail and a probe that fails proves the lock exists rather than that it
is missing.
Ledger-only. No code, wire, or specification change. No DECISIONS.md entry: this
is test-coverage bookkeeping, not a semantic ruling.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
The OperationKind wire golden lock stopped at discriminant 29 while ten kinds
were appended past it -- TransposeInterval (30, Push 4a) through CreateMeasure
(39, G3b). Every one sat with no byte-level lock, and this is the one guard
written to catch exactly that class of stale hand-maintained table, so its own
staleness was the worst place for it. The table goes to 40 entries and locks
30-39 individually, each row asserting both that discriminant() has not moved
and that the byte leads the canonical encoding.
The mutation is the finding reproduced rather than argued for. Editing
discriminant()'s SetTuningContext arm 34 -> 44 fails the extended lock; with
that same mutation still applied, restricting the loop to &table[..30] -- the
exact pre-repair coverage -- passes. That is P13-S15, executed.
The sibling tag half needs no extension and did not get one.
the_tag_vocabulary_is_complete is derived, not hand-written: it computes the
bound from PAYLOAD_FREE's maximum instead of spelling it, so it already covers
30-39, and the vocabulary macro makes a tag without a discriminant a compile
error. One residue is stated in the ledger rather than papered over: density
plus round-trip does not pin which tag holds which byte, so a permutation
inside the dense range survives both tag tests. The same permutation on the
kind side is now caught. Closing the tag-side permutation gap is a separate
question and is not part of this rung.
No wire, schema-version, or specification change: this adds a guard over
assignments that were already normative. P13-S18 and P13-S19 remain open by
design.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
PreconditionFailureReason 14 (AcousticRealizationPinned) and 15
(TranspositionOutOfRange) entered the vocabulary at Push 4a and reached this
document at neither place that owed them. The bounded enumeration ran 13 straight
to G3b's 16, and Push 4a's own history row recorded only OperationKind 30 while
saying nothing about the two reasons it appended in the same epoch. The Operation
Catalog documented both at its 0.8.0 and effect.rs has carried both throughout;
only the wire specification was silent.
This is P13-S20's specification-side twin, and it is why that decoder could stop
at 13 unchallenged: an implementer reading only the wire specification would have
built exactly that decoder and been right. No version bump and no new history
row -- this records an assignment normative since Push 4a rather than making one.
The regression test checks both sites, each bounded to its own longtable row,
because either alone is satisfiable by the wrong thing: the G3b row and the
enumeration both discuss PreconditionFailureReason at length, and an unbounded
search would go green the moment any row mentioned the names. Each half was
observed red alone while the other stayed green. The Push 4a row is located by a
version-free separator marker, per the file's standing rule against encoding a
document version number anywhere in it.
The epoch-12 evidence hash is e64a4b7, not this rung's parent. G3b landed across
six commits, and the chain records introducing commits -- the commit where
kind/tag 39 enters payload.rs and reasons 16-18 enter effect.rs -- exactly the
distinction the 2026-07-28 correction draws between 7df5ca1 and 55eff00 for G2a.
d58eee8 completes the rung and introduces no discriminant; both are named, with
their roles stated, in the plan and in the contract's pin 15.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
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
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
Ruling, ratified 2026-07-29:
- Staff.group is the SOLE authority for membership.
- StaffGroup.members is a non-authoritative denormalized projection.
- G3a stores it but neither maintains nor trusts it.
- BOTH stale forms are permitted, equally: a missing member (s.group ==
Some(g) while g.members omits s) and a spurious member (g.members contains
s while s.group is None or names a different group). The earlier draft
named only the first, which left the spurious form reading as a bug rather
than a permitted state.
Withdraws the earlier draft's argument for B. "B adds no semantics while A
does" was wrong: B assigns authority to a field the specification left
unranked, which IS a semantic change. What B defers is enforcement, not
meaning. The honest advantage is narrower -- B adds semantics without adding
machinery, leaving the mint a mint.
Files P13-S16, which makes the contract's "filed gap" claim true; it was
false when written, since no such entry existed. The entry records both
stale forms, the disposition-A fix, candidate invariant 21, the re-carry
comparison question A must answer, and the standing instruction that
consumers read Staff.group and never StaffGroup.members for membership.
Remaining repairs:
- PLAN_GENESIS_OPS.md still claimed G3a "closes the staff-group half".
Narrowed to satisfiability, matching the contract.
- Boundary accounting normalized to six crossings across both documents: one
exhaustive-match site plus five literal/prose sentinels. The two classes
are counted together but named apart because they fail differently -- the
match site refuses to compile, while every sentinel stays green while
meaning something narrower than it says.
- t4 now runs four independent mutations, one per struct_codec! layout;
these are four separate layouts and one mutation signs one of them.
Collapsing is permitted only if the implementation consolidates them
behind a shared mechanism, and must be stated if it does.
- t8 split: t8 asserts satisfiability only; new t8b pins BOTH asymmetric
authoring orders as states the ruling permits, with a mutation each. A
test pinning one order leaves the other free to change silently.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Kind and tag 34, schema major 3, minor epoch 10. The last rung before G3, and
the one that closes P13-S13: the tuning context becomes operation-authored, so
it finally has a canonical carrier. The closure argument is the metadata
precedent, not the canonical base - the base embeds no graph values for any
field, and metadata has been durable purely through its operations since M2d.
The payload carries epiphany_core::TuningContextSettings, a five-field subset
of ScoreTuningContext, not the full graph type. ScoreTuningContext's codec
deliberately drops accidental_extensions, so a full-value payload would have
diverged between a live session, where accept stores the envelope as a value,
and the same document reloaded, where the field decodes empty. canonical_value!
could not have caught that: it compares bytes and never the originating value,
so a field that never reached the bytes is structurally invisible to it. The
subset makes the divergence unrepresentable instead of relying on a
normalization step nothing can enforce, and it costs no wire design - the
encoding is byte-identical to the existing five-field walk, which
tuning_context_settings_canonical_bytes_match_score_tuning_context asserts
directly. Reduction leaves accidental_extensions untouched.
SetTuningContext is the sole genesis payload born at major 3, because minimal
stamping is a function of each payload's value, so the accept-set raise is
charged to this one surface: OperationEnvelopeBlock 2 to 3. The doc comment
above it did not merely record the cap, it asserted that no operation payload
embeds the tuning context - a sentence this rung falsifies - so it is rewritten
rather than left beside a corrected constant.
Undo restores the seeded base settings, default or not, and the
never-authored versus authored-to-default distinction stays unobservable. An
earlier draft of the contract had that backwards; PLAN_GENESIS_OPS section 5
trap 5 withdrew it, and SetMetadata is the disproof.
Fixes two undefined references the interrupted run had not yet reached:
operation_catalog.tex referenced sec:evolution:major3, a label defined in
binary_format.tex, which LaTeX cannot resolve across documents. Replaced with
the sectionsc convention already used for every other cross-companion citation
in that file.
Gate: 1409 tests, clippy 0, fmt clean, conformance 8/8 including [7f], both
vector corpora regenerated, all four PDFs at 0 undefined references. The t5 and
t7 mutations were re-run independently and observed to fail as specified; the
remaining eight are not signed off, because the implementing run was stopped
before it reported them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Documentation only. No code change.
S15 records that operation_kind_wire_discriminants_are_golden declares
[(OperationKind, u8); 30] at payload.rs:1959, covering 0..=29 - so
TransposeInterval, CreateInstrument, SetCanvasLayoutDefaults and
SetSpellingPrecedence have no byte-level lock. Not a live incorrectness: all
four discriminants are currently correct and the .tex tables carry them
normatively. The gap is the absence of a guard.
Left open deliberately with no code change. The fix is mechanical, but a
golden-lock extension should land with its mutation evidence and nothing else
in the diff - and the mutation is to move one of the four and watch the
extended lock fail where it previously stayed green. The macro-guarded
OperationKindTag half is unaffected; this is the hand-written match, which is
the site Push 4a got wrong. Its sibling phase3_tag_discriminants_are_golden
wants the same check.
Closes S14 at ff9bd0f, and records the two things its filing did not
anticipate. The scope was never just OperationKind - OperationPayload,
ReanchorReason and PreconditionFailureReason all append - and the manifest
reaches OperationKindTag through edit_barriers with no envelope involved, which
supersedes the filing's "no companion bump" note. Op-block stamping did stay
projection-invisible as scoped; the bump came from the manifest attribute.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
All five findings verified against the tree; the recommended policy is
rejected and replaced with the ratified one.
Policy (b), minor = highest discriminant emitted, cannot represent an
operation block. An envelope also emits the outer OperationPayload
discriminant, and ResolveEquivocation is appended at 3 while carrying no
OperationKind at all -- so the ambiguity is inside one role and one block, not
between roles. Generalizing to "highest from any vocabulary" is worse: an old
kind 23 would numerically mask a new payload 3. binary_format itself enumerates
four independent minor-additive vocabularies. The rung's gating work is
therefore an audit of every append-only discriminant reachable from each
affected payload, which the first draft never scoped.
Ratified instead: a global additive epoch with content-minimal stamping, an
envelope's minor being the max across outer payload, primitive kind, and every
nested additive variant actually emitted. The maintenance objection is
answered by co-locating introduced_minor with each discriminant in an
exhaustive match with no wildcard, so an unassigned variant cannot compile --
the operation_kind_tag_vocabulary! reasoning. Per-major counters are rejected
too: mixed blocks do not compose after max_major.
Two of my conclusions were wrong. Op-block minors do not reach the text
projection -- block schemas are discarded there, and all seven accepted-corpus
schema forms belong to extension chunks or canonical bases -- so no companion
bump. I had also miscounted them as six by grepping lines rather than
occurrences. And the manifest-ID promise is not threatened: it is conditional
on the same manifest body, and a changed ChunkRef is a different body. Real
address churn, not a broken guarantee.
"Is the canonical base exempt" was the wrong binary question. It never emits
the op-kind discriminant, so it holds its minor until MaterializedState's own
bytes emit a later-added variant. Manifest::SCHEMA stays put; existing bundles
need no migration. Construction-site count corrected 62 -> 66.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
TransposeInterval is missing from both Core normative listings, which carry the
older Transpose and never gained its successor at kind 30. So the vocabulary
has drifted from its normative listings for two tranches, not one, and both
listings need four additions rather than two. The contract's grep list gains
TransposeInterval, and names the signature worth hunting: a spelled-out count
that disagrees with the enumeration beside it.
for_major does not return {major, 0} unconditionally -- V0 is {0, 1}, and only
V1 through V3 carry minor 0. Corrected in the contract, the plan, and P13-S14.
The finding is unchanged: the function accepts only a major, so no per-kind
additive minor can reach it.
Ladder order is now explicit as G2a -> G-minor -> G2b, with the reason. The
sweep is scoped to kinds 24-33, which is what exists once G2a lands; running
G2b first appends kind 34 and would either grow the sweep mid-flight or ship 34
carrying the defect the rung exists to retire.
Core is five live-text edits plus one historical annotation, not five edits.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
All four findings verified against the tree before fixing; all four hold.
The schema-minor MUST (binary_format.tex:2330) has never been implemented.
SchemaVersion::for_major accepts no minor and returns {major, 0}, and both
staging paths derive only the major, so kinds 24-27, 28-29, 30, and 31 already
carry no additive-version record -- and the requirement's own rationale is
exactly what the gap defeats: a reader meeting an appended discriminant cannot
tell a stale vocabulary from damaged bytes. Filed as P13-S14 and ruled a
separate rung after G2a, sweeping 24-33 in one retroactive pass rather than
blocking G2a on a debt eight kinds deep or paying for two partial sweeps. G2a
now says explicitly that it extends the violation by two, knowingly, and
forbids working around the absence.
s7 could not fail. WorkingSnapshot::restore reassigns the whole graph
independently of every write chain, so omitting a chain leaves stale history
while the field still rolls back -- the prescribed assertion passed under its
own mutation. Two framings of this test were wrong; the third asserts against
a later undo's predecessor, and the contract now requires the mutation be run
rather than reasoned about. s8's mutation was impossible as written: each
payload has one field, so there are no adjacent fields to swap. Swapping
discriminants 32/33 in both halves is the self-consistent mutation that leaves
round-trips green and kills correctly-named literal vectors.
The normative repair surface doubles: eleven sites across four documents, five
of them G1 debt. Core's normative OperationKind and OperationKindTag listings
are missing CreateInstrument as well as both new kinds; the catalog's
value-restoration family list is normative for undo and omitting a family is a
silent semantic gap; two spelled-out payload counts move. Since two independent
reviews each found sites the other missed, the list is a floor and the contract
now prescribes grepping the load-bearing phrases.
Also: core_spec said "two edits" and prescribed more, and the split-cost
accounting counted only Text Projection when G2b repeats the Binary Format and
Operation Catalog work too.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
G2 was scoped as one packet of three LWW setters. Reading the frozen codec
walks rather than the type labels shows they do not sit at the same major:
SpellingPrecedence has never been versioned, CanvasLayoutDefaults is versioned
in the containing Canvas walk and not the leaf, and only ScoreTuningContext is
born at v3. So the accept-set raise — a one-way door — is charged to one
surface, not amortised across nine as the ruling's framing implied. G2 splits:
G2a is the two major-0 setters and touches epiphany-bundle not at all; G2b is
SetTuningContext alone, carrying the raise and the S13 close.
Withdraws plan trap 5. SetMetadata already answers it: Score::empty seeds
metadata exactly as it seeds tuning_context, the base ingest seeds the LWW
chain from it, and restoring that seed is correct for both never-authored and
authored-to-default. These are always-valued fields, not map keys, so the
Predecessor::Base/::Write distinction that matters for spellings and breaks
does not apply here.
Records the G1 lesson as a trap in its own right: an OperationKind variant is
not containable to core+ops, and the G2a contract budgets all three downstream
literal sites up front instead of discovering them mid-dispatch.
Corrects S13's amortisation claim, and states the closure argument properly —
the canonical base is a MaterializedState that embeds no graph values for any
field, so S13 closes on the metadata precedent, not on the base. Notes the
consequence: after G2b, pruning would discard authored state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
RULING_GENESIS_PERSISTENCE.md §3 was the tranche's stated blocker. Ruled:
identity stays on Score and stays canonically encoded, byte-equality claims
confine to MaterializedState, and from-empty reduction derives next_counter
from the log.
Scoping it turned up that the section understated the problem. Verified in the
tree: epiphany-ops has no `.identity` reference at all, so reduction never
advances the cursor; invariant 11 checks only the reserved replica, never the
counter against ids present; and every mint from score.identity today is under
cfg(test). Score::identity is an authoring cursor reduction never touches, and
this tranche is what activates the hazard -- under from-empty the cursor sits at
the seed while the log already holds that replica's ids at 0..N. Divergent bytes
were the lesser problem, and none of the three options originally listed fixed
the larger one.
The manifest option, previously recommended, is rejected on evidence:
req:format:manifest-id promises two conforming writers derive identical
ManifestIds, which a replica-scoped field in a shipped content-addressed
structure cannot honour. The two wire options each cost schema major 4 on the
role 3b-i just froze at 3, and neither corrects the cursor.
Also reconciles two records against 011c68a. DECISIONS.md flatly prohibited a
SetTuningContext operation, which the ruling now requires; the prohibition is
marked superseded and re-scoped to what it was aimed at -- no tuning-only fix,
no wire widening to compensate -- both of which still hold. PASS13-S13 moves
from blocked-on to resolved-by, naming which of the four dispositions was taken.
Flagged for the tranche, not fixed here: bundle.rs documents the
OperationEnvelopeBlock cap of 2 with the tuning-context rationale in prose, so
that comment becomes false when the cap moves.
Gate: requirement_labels 6/6. No .md here is include_str'd or compiled.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Ratified 2026-07-24. S13 is re-pointed rather than resolved: the tuning context
is not a special case, it is field 10 of an eight-field gap that
spec/ANALYSIS_GENESIS_PERSISTENCE.md tables across the whole Score —
canvas.layout_defaults, instruments, staff_groups, parts, tuning_context,
spelling_precedence, analysis_layers, views, plus identity, with
decomposition_attachments prunable-but-never-authored. Each is reachable only
through a base the reducer is handed, and pruning is licensed to replace that
base with a MaterializedState carrying none of them.
Independently re-verified against the working tree before recording: every one
of those fields' sole reduce.rs mention is a read-only base-seed read, and
epiphany-ops has no tuning-context payload at all.
So the entry drops the operation-vs-canonical-base framing it was filed with.
That framing was right about the axis and wrong about the scope: it maps onto
the analysis's dispositions B and C at eight times the size, and a
tuning-specific fix is exactly the shape the analysis argues against — "any
option that enumerates fields must be re-audited against this table every time
a field is added to Score". S13 is now marked blocked-on rather than open, and
whichever disposition the genesis ruling picks resolves it as a side effect.
The accept-set cost this entry surfaced is kept, repositioned as evidence
against enumerated per-field fixes rather than as a tuning-only tradeoff:
blocks stamp minimally, and bundle.rs caps OperationEnvelopeBlock at major 2
precisely because no operation payload embeds the tuning context, so a
SetTuningContext-style operation would drag a role accept-set raise along with
the kind append — for one field of eight.
The wire layouts stay frozen and correct under every disposition. This was
never about how the tuning context encodes, only about which carrier embeds it,
so epiphany-core's DECISIONS entry now records the matching prohibition: no
SetTuningContext operation, and no widening of ScoreTuningContext's layout to
compensate.
Docs only; no code path touched. Gate unchanged: fmt clean, 1336 passed /
0 failed, requirement labels 6/6 at 212/282/282.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Schema major 3 put `smufl` and `overrides` on the binary wire and left the text
surface alone. This closes that: `TextValue for ScoreTuningContext` projects
five fields in `fn enc` order, with four new leaf impls (SmuflVersion,
SmuflVersionRequirement, TuningScope, TuningOverride) mirroring the four Codec
impls 3b-i froze.
The rule that moved them is the one this projection always followed — the text
projection is the same canonical surface the binary codec is. What changed was
not the rule but a premise: the old doc comment excluded all three fields
because "no schema major 3 has been opened", and 3b-i opened it.
`accidental_extensions` was staged out of that major and stays in-memory only,
so it is still correctly absent — the staging line now falls in the same place
on both surfaces, which is what the inverted tests assert.
No header-version bump and no document vector moved. This is a value-projection
change, not a document-format one: epiphany-textproj has no reference to a
tuning context anywhere, and the canonical base projects as a single opaque
byte atom (req:textproj:base-snapshot-inline), not structurally. So
req:textproj:header-version's sole accepted version (0 7 0) is untouched, all
13 document vectors are unchanged, and req:textproj:roundtrip was never
violated — the tuning context is not part of the projected document.
Both tests were renamed and inverted, following 3b-i's treatment of the binary
pair rather than rewriting the tranche-2/3a decision entries. Mutation-verified:
making `overrides` project as an always-empty vector — the exact regression this
fixes — fails both.
Two stale-pointer classes fixed, both of the kind this pass was hunting: the
comment in codec.rs naming the old text-test names and claiming the text
surface was unchanged; and ScoreTuningContext's per-field docs in graph.rs,
which still read "In memory only this tranche" for `smufl` and `overrides` —
stale since 3b-i, and contradicting the struct-level wire note they point at.
The struct-level doc was already correct.
Filed P13-S13, found while scoping and deliberately not fixed here: the tuning
context has NO canonical persistence path. Its only *persisted* carrier is the
acceleration snapshot, which Chapter 8 makes non-canonical and regenerable; no
canonical carrier embeds it at all. No operation authors it (the ops
vocabulary's only `tuning` references are the per-pitch TuningReference::Inherit)
and MaterializedState does not carry it. So a per-voice tuning override survives
a snapshot round-trip but cannot be authored, replicated, or merged, which makes
req:tuning:tuning-resolution-order's scopes 2-4 unreachable from any exchangeable
document. Not a regression — nothing ever worked; major 3 made the gap visible
by giving the value a wire form and no way to reach it.
The candidate prices both routes. Ratification is about the carrier, not the
encoding — the layouts are frozen and correct either way — but the operation
route is not quite the free schema-minor it first looks: blocks stamp minimally,
and bundle.rs caps OperationEnvelopeBlock at major 2 precisely because no
operation payload embeds the tuning context. An authoring operation carrying a
TuningOverride would stamp v3, dragging a role accept-set raise along with the
kind append.
Gate: fmt clean, clippy 0, 1336 passed / 0 failed, doc 0, conformance 8/8,
requirement labels 6/6 at 212/282/282.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
Closes P13-S12. epiphany-layout-ir defined its own SmuflVersion storing the
minor LITERALLY ({1,4} for SMuFL 1.4), so derived Ord sorted 1.3 and 1.4
BEFORE 1.12 — backwards versus SMuFL's real release order — on a direct field
of GlyphCatalogIdentity, which is layout-conformance identity. That bug was
live. The crate graph fixes the direction: layout-ir depends on core, so
core's fraction-normalized minor_centi type is necessarily the survivor.
layout-ir deletes its own and re-exports core's, keeping
epiphany_layout_ir::SmuflVersion resolving for downstream at zero churn.
No wire change: no schema major, no Codec, no decode vector. LayoutCache is a
regenerable major-0 role. The one line that moves bytes is encode_catalog's
.minor -> .minor_centi (04 00 -> 28 00 at offset 2, verified by encoding a
default identity: 60 bytes, unchanged elsewhere).
The tranche also closes S12's OTHER half, which had gone unnoticed: the
ratified shape reached Rust and the binary companion but core_spec.tex never
defined SmuflVersion at all — the exact "undefined leaf" S12 was filed about.
It is now declared once, with the hundredths rule as normative prose and the
release table as a rationale note, cross-referenced from both Chapter 9 sites
so the glyph catalog's version and the tuning context's are visibly one type.
No new req: label; counts stay 212/282/282.
Two ledger claims corrected, both verified false before dispatch rather than
after: PASS13_CANDIDATES.md's S12 entry and epiphany-core/DECISIONS.md's S12
bullet both promised this move would land "with golden regen". Nothing is
pinned to the catalog identity — every ResolvedLayoutIR::canonical_bytes()
assertion is RELATIVE (stability, determinism, and a sensitivity check that
mutates metrics_hash, never smufl_version), and the committed SVG/PNG goldens
embed no identity, only a fixed comment string. There was nothing to
regenerate, and conformance gate [9] never moved.
The fix is locked by a test that names the bug: layout-ir asserts the real
release order through GlyphCatalogIdentity.smufl_version. Mutation-verified
independently by making from_decimal store one-digit minors literally — it
fails with "minor_centi: 20 did not sort before minor_centi: 3", which is
precisely the deleted type's behaviour.
Gate: fmt clean, clippy 0, 1283 passed / 0 failed (+1, the new test), doc 0,
conformance 8/8, requirement labels 6/6 at 212/282/282. No golden, vector,
baseline, or editor-track file changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Tranche 3 completes ScoreTuningContext onto the wire -- schema major 3, the only
irreversible tranche. Split reversible-first: 3a builds the accidental / glyph /
engraving type surface in memory with a real consumer (so the shapes are
exercised while free to change), 3b freezes them. 3a adds no Codec, no wire
movement; the hand codec keeps encoding exactly three wire fields while
accidental_extensions and smufl join overrides as in-memory-only.
Scoping the subtree surfaced three types that freeze forever and the spec does
not pin -- filed as S10-S12, ratified before dispatch (the S5/S7-before-tranche-1
pattern):
- S10: PitchSpaceModification::Cents(f64) is a raw f64 in canonical state, which
the byte layer cannot encode -- serialize.rs decodes floats only through
CanonicalF64::from_le_bytes. Ratified Cents(CanonicalF64), the same maneuver
Ruling D applied to the bounding box.
- S11: AnchorPoint is referenced (:3166) and defined nowhere; core cannot depend
on layout-ir. Ratified core-native { x, y: SpaceUnit } plus a normative frame
sentence, since the box it anchors is "relative to the glyph's anchor point".
- S12: the catch. SmuflVersion is NOT undefined -- it exists in layout-ir with
LITERAL minor ({1,4}) and derived Ord, so 1.3 sorts before 1.12 today, and it
is a field of GlyphCatalogIdentity (conformance identity). Ratified
fraction-normalized minor_centi (1.4 -> 40); core defines it in 3a, and 3b
unifies layout-ir onto it and moves GlyphCatalogIdentity deliberately.
Every citation and struct line-ref verified against core_spec.tex before commit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
S5 and S6 close, which closes the spec half of Push 4b: the JI basis is
full-register (a JiVector is an absolute position -- without the prime-2
exponent, ji-5limit cannot tell C4 from C5), and all twenty tuning systems now
carry constructions rather than names.
S7 is filed retroactively. Its id was minted in PLAN_PUSH4B_TUNING.md marked
"new; file it", ratified as that plan's Ruling C, and implemented in Wave 1a --
but it never entered this ledger, so an id lived outside the index whose whole
job is to be the index. The candidate itself: the specification required every
score to *define* a pitch space, and the data model gives a score no way to do
it. Both "define"s are now "select".
S8 is new and was measured, not argued. TempoShape::Constant legalizes
end_tempo as None *or* equal to start_tempo -- two spellings of one musical
fact -- and TempoSegment encodes the field positionally into canonical state.
Encoding both forms of an otherwise identical score gives 336 bytes and 363.
Nothing normalizes; invariants.rs merely permits both. Everywhere else this
project looked at redundant encodings it ruled the other way: NFC for text, a
total order for collections. Tempo got no equivalent, so two musically
identical scores hash differently. Found sideways -- the branch is untested,
which is why the map_or rewrite could not be mutation-verified there.
S9 is the pattern behind three failures in one day: the citation checker
proves cited-to-defined and never cited-to-relevant, so a citation that
resolves cleanly while supporting nothing passes a fully green gate. An agent
justified "resolved layout is non-canonical" with a requirement about f32; a
dispatch contract of mine put KeySignature in Chapter 10 when the Score Graph
is Chapter 5. And S4 is itself an instance, which is what turned this from
incident into candidate.
The asymmetry is the dangerous part and is recorded with it: a dangling
citation is loud, a wrong one is silent, so repairing the loud kind without
reading the target converts a caught defect into an uncaught one. That is
exactly how S4 happened.
Every line citation in the new entries was re-read against its target before
filing, S9's own discipline applied to S9.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`epiphany-core/DECISIONS.md` had carried two Push-4a audit claims as
**unverified** through two passes, with the standing note that they "should be
checked, not inherited". Checked. Both are real, and both are Chapter 4 defects
standing in front of Push 4b rather than inside it -- which is the point of
checking before scoping, not after.
**P13-S5 -- the JI prime basis is specified at two lengths.**
`req:pitch:ji-vector-basis` says the built-in JI spaces order primes ascending
*starting with 2*, and that `components.len()` MUST equal the basis size. The
built-in pitch-space table calls `ji-5limit` "Two-dimensional (prime axes 3,
5)", `ji-7limit` three-dimensional, `ji-11limit` four-dimensional -- each
exactly one short, consistently, because the table is octave-reduced and the
requirement is full-register. `req:tuning:builtin-tuning-catalog` makes the
table normative, so a 5-limit vector is required to be both length 2 and length
3. The requirement's own octave-reduction clause does not reconcile them: it
normalizes the first component to a canonical range, it does not remove it.
Same family as P13-S2 and P13-I1, and cheaper -- a one-side edit once ratified.
**P13-S6 -- no built-in tuning system's resolution is pinned to a versioned
definition, and 14 of 20 have no definition at all.** The claim as inherited
("the named historical tunings lack exact deterministic ratio data") understates
it. It is not a missing table: `TuningResolution::Function` delegates the
historical temperaments to a `TuningFunctionId`, which Chapter 10 lists as an
*extension point*, and no built-in is mapped to a function id nor any function
id pinned. Only the six `tet-*` entries are actually specified, by
`EqualTemperament`'s structural rule. The other 14 are names: three meantone
variants, `werckmeister-iii`/`iv`, `vallotti`, `kirnberger-ii`/`iii`,
`young-ii`, `pythagorean` (the 3:2 ratio is named, the fifth-chain construction
and wolf placement are not), three `ji-static-5limit-*`, and
`ji-adaptive-5limit`. Set against `req:tuning:tuning-resolution-determinism`,
which requires determinism *across platforms*, two conforming implementations
may each choose a different published Werckmeister III and both pass -- in a
project that runs a cross-implementation conformance step.
The fix pattern is already in-house and was found by looking for it:
`req:pitch:spelling-algorithm` pins `SpellingAlgorithmId "default"` at version 1
to a named algorithm and errors on any other identifier. Nothing equivalent
exists for tuning. Which published variant each built-in denotes is a
ratification question, not an implementation one.
Both are filed open in the Batch-3 ledger. Neither was needed for 4a; both are
load-bearing for 4b, whose whole job is resolving a scale position to a
frequency.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cmn-24` was declared in the built-in pitch-space table as "CMN extended with
24-EDO quarter-tone accidentals" while Chapter 2 fixed a `Cmn` position's
absolute semitone at `nominal.chromatic() + alteration + 12*octave` for every
space, unqualified. Two ratified MUSTs, each correct in its own chapter,
jointly unsatisfiable -- P13-I1's two-listings drift in another costume.
The tracker parked this as "a data-model major". It is not.
`req:binfmt:frozen-layout` names `PitchSpacePosition` as one of four open
value-layer vocabularies, so three of the four available options cost no schema
event at all. The one that *is* a major -- regrading `alteration` onto a finer
fixed unit -- is also the only one that rewrites the canonical bytes of every
pitch ever authored, and it buys a single grid while `edo-31`/`53`/`72` stay
unrepresentable. It is explicitly ruled out.
Ratified instead: a `Cmn` alteration and a `CmnChromatic` modification are
denominated in steps of the enclosing pitch space's chromatic layer. One rule,
not a special case for one catalog row -- it settles `cmn-24`, `maqam-base`,
and `PitchSpaceModification::CmnChromatic` together. `cmn-12`'s step is the
semitone, so every score in existence keeps its meaning and **not one canonical
byte moves**; `canonical_pitch_bytes` writes the space id before the position,
so a `cmn-24` E-half-flat and a `cmn-12` E-flat already derive different
`PitchId`s.
The defect was larger than `cmn-24`. Nothing bound a `Cmn` position to a
12-chromatic space: `ScalePosition { space: "edo-31", position: Cmn { .. } }`
constructed, validated, encoded and transposed, and meant nothing. Neither
`Pitch::transposed` nor `twelve_tet_semitone` ever read the space. Until Push
4b resolves `PitchSpaceId` to a `DiatonicOverChromatic` structure, both now
fail closed outside provable built-in `cmn-12` -- a capability check, not a
claim that the identifier defines the structure. False refusal for a
score-defined 12-chromatic space is accepted over silently wrong arithmetic.
No wire change. `TransposeRefusal::PitchSpaceUnavailable` is a new *diagnostic*
mapping to the existing `PreconditionFailureReason::PitchSpaceMismatch` (6);
appending a discriminant for a guard Push 4b must delete would have reserved a
permanent wire value for a temporary mechanism, in exactly the artifact the
fail-closed ruling exists to protect. Assignments 10-15 are untouched.
Operation Catalog 0.9.0 records the broadened case *and* amends the 0.8.0
rationale, which asserted that discriminant 6 never consults a pitch-space
registry -- true of the non-`Cmn` check, false of this one.
Two spec-side fixes found in review, neither visible to any test: the
`TranspositionInterval` listing had lost its `pub chromatic_steps: i32,`
declaration to a comment rewrite, leaving a one-field struct above the sentence
"Both components are load-bearing"; and `pitch.rs` still documented
`alteration` as "in semitones", contradicting the requirement landing beside
it. Both new requirements are now cited from the code that implements them --
the label checker catches cited-but-undefined, not the reverse.
Push 4b keeps the registry work and loses the contradiction. P13-S4 is the last
item open in Batch 3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
My design note was half right, and the audit caught the other half. Keeping
engraved_spelling_chain physically separate from respell_chain was correct --
respell_chain is RespellPitch's LWW working state, read by its
concurrent-differing conflict detection, and folding transposes into it would
make a concurrent respell conflict with a transpose and move the canonical
bytes of every existing history. What that did NOT license was letting one
operation own the key.
RespellPitch mutates the same graph attachments and recorded nowhere on the
attachment chain, so a chain with a single writer was wrong both ways. Both
reproduced:
respell -> [tx: transpose] -> StrictInverse undo
Applied; pitch restored to C4; the UserChosen(C) attachment ERASED.
The respell was an operation, not part of the base, so the transpose's
chain had never seen it and its predecessor was absence.
[tx: transpose] -> respell -> StrictInverse undo
Applied; the newer UserChosen(D) authoring WIPED. The respell was
invisible to the chain, so it never registered as a superseding writer,
contradicting the catalog's rule that a later canonical writer supersedes
a strict undo.
And BestEffort could restore the pre-transpose pitch while leaving a spelling
authored against the transposed one attached to it.
Fix: every writer of the attachments records on the attachment chain
(record_engraved_spellings), and a pitch's value and its engraved spelling set
undo as one unit -- if either half is superseded, neither is restored.
StrictInverse already refuses on any supersession, so the coupling only bites
for BestEffort. Two physical chains, two responsibilities: respell_chain owns
the ledger spelling and the LWW verdict, engraved_spelling_chain owns the graph
attachments.
Recording is gated on graph presence, so base-free reduction is byte-unchanged
and the seeded corpus's canonical-base digest does not move.
Four tests, two mutations verified: removing the respell's record fails all
three undo tests; removing the coupling fails the best-effort one with the
pitch back at C4 and its spelling still at C-sharp. The fourth test locks
convergence -- both permutations of a concurrent respell/transpose reduce to
identical canonical bytes.
Spec: new req:opcat:spelling-set-chain. Batch 3 of the Pass-13 ledger reopens
(P13-S1, S2 open; S3 resolved here).
Also: PitchSpelling::transposed's doc said B-sharp 3 becomes F-double-sharp 3.
The code, spec, and tests all correctly produce F-double-sharp 4 -- B to F
carries the octave. Doc only.
Gate: fmt clean, clippy 0, 30 targets / 998 passed / 0 failed, docs 0 under
-D warnings, conformance 8/8, zero golden churn, canonical-base digest unmoved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two open candidates, staged until a third reopens the pass.
P13-S1: 169 of core_spec's 207 requirement blocks carry no \label, so no
conformance claim can cite them. The audit that surfaced this scoped it to
Chapter 4 (9/9 unlabeled). It is not a Chapter 4 problem: Determinism Contract
is 15/15, Semantic Operations 24/27, The Score Graph 22/28, Pitch 10/13. Every
req:* label this repo cites was added ad hoc by whichever pass needed it.
P13-S2: cmn-24 is in the built-in pitch-space table as "CMN extended with
24-EDO quarter-tone accidentals" and cannot be represented -- Cmn.alteration is
an i8 of whole semitones, and a quarter-tone is half of one. Blocks Push 4b.
Also parked: the Push 4b prerequisites. Push 4a proved transposition needs no
tuning catalog; what the catalog is still for -- resolving a scale position to
a frequency, applying an instrument's transposition at the written/sounding
boundary -- remains, and Chapter 4 is not implementable as written.
Two further claims from the audit are recorded as UNVERIFIED rather than
inherited: that the JI dimension convention conflicts with its own prime-2
requirement, and that the named historical tunings lack exact ratio data.
Neither was needed for 4a; neither was confirmed. An audit finding that has not
been executed is a hypothesis.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
BRAVURA_METRICS' NOTEHEAD_ANCHORS were hand-derived where every neighbouring
number in the table is machine-extracted from the SHA-pinned font -- the same
mistake as inferring band ownership downstream instead of reading it from the
source that had it. And they were wrong under any reading: they named
stemUpNW/stemDownSE, the corners a normal notehead's stems do not attach to and a
pair Bravura's noteheadBlack does not define, with an x of 1180 that reads as 1.18
staff spaces written in thousandths rather than the table's 1/1024 units (1.18 sp
= 1208). Nothing consumed them.
Deleted rather than corrected. The font is not vendored, so the true values cannot
be verified in-tree, and shipping data we cannot stand behind into a hash every
conformance claim declares is worse than shipping none. Verified empirically
before deciding, not reasoned about: changing the anchors breaks nothing in-tree
-- 30/30 targets, zero golden churn, no pinned literal hash -- so
GlyphCatalogIdentity moves once, now, while no claim declares the old one.
extract_bravura_outlines.py gains --anchors, emitting them from the pinned
bravura_metadata.json (anchors live in the SMuFL metadata, not the glyf bounds,
which is why the outline extraction never covered them). SMuFL anchors are points,
so they round to nearest -- unlike a bbox, which rounds outward so the metric box
contains the ink. The metadata's SHA-256 is deliberately left UNPINNED and
verify() now refuses an unpinned source, printing the digest to paste: the script
cannot regenerate anchors until an operator with the font pins it in a reviewable
commit, which is the discipline the other two sources already have.
The test guarding the anchors proved nothing. anchors_participate_in_the_hash
compared noteheadBlack (anchored) against noteheadWhole (not) and asserted they
hash differently -- but their advance and bbox differ too, so it would have passed
with the anchors ignored entirely. It now varies the anchors while holding every
other field fixed (presence, one coordinate, one name), against synthetic metrics
through a factored-out metrics_hash_of.
Pass-13 Batch 2 CLOSED. All three candidates resolved; two grew when examined.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
to_constrained took the active clef from the staff instance's clef_sequence and
fell back to Clef::default() -- treble. A staff that declared its clef only on the
Staff, with no ClefChange, therefore drew a treble clef and placed every note
against it. The field was decorative in the projection.
It is the fallback. StaffContent now carries default_clef -- the clef belongs to
the Staff, the sequence to the StaffInstance, and resolving "the clef at time t"
needs both -- and active_clef_or(clefs, at, default) resolves against it.
active_clef remains as that with the treble default, for callers with no staff to
hand, so the public API is intact.
The part worth pausing on: epiphany-editor-core reads the same function for
hit-test pitch resolution. Fixing only the projection would have left a click on a
bass staff resolving its pitch as treble -- the engraved clef and the editor
disagreeing about what note is where. Both now go through active_clef_or.
Removal was rejected: the field is named for its purpose, is encoded on the wire,
and dropping it would be schema-major.
Zero golden churn: every fixture and generator declares treble, which is also
Clef::default(), so nothing that exists today moves. Locked by
a_staff_declaring_only_a_default_clef_engraves_in_it and mutation-verified by
restoring the Clef::default() fallback. Scoping that test by provenance was
necessary -- valid_score_rich has three staves and only one was re-clefed, so the
first assertion I wrote ("no gClef anywhere") failed against a correct fix.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Three parked candidates accumulated while the Standard-tier solver track closed
and the notation-quality pass landed, which is the threshold at which the house
rule opens a batch pass. PASS13_CANDIDATES.md reopens as Batch 2 with P13-I1
(this), P13-I2 (Staff::default_clef never consulted), P13-I3 (the notehead stem
anchors).
P13-I1 was filed as "two elided fields". Checking before writing turned up a
third, and it is the one that matters: `diagnostics: Vec<LayoutDiagnostic>`
appears NOWHERE in core_spec, though it is how the projection's honesty rule
manifests. So Chapter 7's listing gains break_origins, diagnostics, and catalog;
BreakOrigin and LayoutDiagnostic gain their shapes.
And it gains req:layoutir:coverage-diagnostics, ratified as implemented: an
object the projection cannot engrave faithfully -- a pitch with no resolved
spelling, a glyph the bound catalog does not carry -- MUST be recorded as a
LayoutDiagnostic AND still placed, as a fallback notehead or a zero-extent traced
anchor. It MUST NOT silently substitute a plausible shape, and MUST NOT drop the
object. Both halves matter: dropping it breaks the round-trip surjection, so a
hit-test can no longer find what the author wrote; guessing produces a score that
looks engraved and is wrong, with nothing in the IR to say so.
Locked by an_unengravable_object_is_recorded_and_still_placed, which drives a
percussion clef (no bundled glyph) and asserts all three halves: the diagnostic
names the object, no glyph stands in for it, and a zero-extent anchor keeps its
provenance addressable.
Spec rebuilds clean, no undefined references, new label registered. No code
change beyond the test; zero golden churn.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
delete_event tombstoned the event in objects AFTER materialize_graph_delete —
but that graph pass cascades a cue among the event's referents, running
reanchor_for_tombstone over the cue's referents while the source event is still
Live. A slur bridging {X, cue-of-X} therefore re-anchored onto X
(Reanchored{to: X}) and then cascade-deleted when X's tombstone landed a line
later: a contradictory same-effect trail (candidate was "plausible by code
trace, unexecuted" — now executed; reverting the fix reproduces exactly that
two-record trail).
Fix: tombstone the event in objects BEFORE the graph delete, matching the
conventions cascade_cue and tombstone_undo_targets already follow (both
tombstone before their graph delete — which is why the undo path never had this
bug). The bridging slur now sees X already dead during the cue cascade and
cascades once.
Regression: deleting_a_cue_source_does_not_leave_a_contradictory_repair_for_a_
bridging_slur_p13_d2 (built on a cue-bearing base with a bridging slur; asserts
exactly one CascadeDeleted, no Reanchored). Verified to fail without the fix.
940 tests, convergence/conformance green.
PASS13_CANDIDATES.md: P13-D2 resolved — batch CLOSED (all four candidates done).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
An undo of an event-minting transaction tombstoned the events graph-side
(materialize_graph_tombstones → materialize_graph_delete re-anchors/cascades a
structure whose anchor died) but never ran the ledger-side
reanchor_for_tombstone — so a structure orphaned by the undo left the graph
while staying Live in objects, with no RepairRecord. Ch6's same-step
RepairRecord MUST was unmet for undo-driven tombstones (slurs/spanners/repeats).
Fix: tombstone_undo_targets now captures each event target's voice (before the
graph half clears voice_occupancy) and runs reanchor_for_tombstone per event
target after the graph half. The orphaned structure now cascades or re-anchors
in objects with a same-step RepairRecord, agreeing with the already-updated
graph — both use the same min-survivor rule, so they converge on existence and
target. reanchor_for_tombstone gains a liveness guard (skip a non-Live
structure) so the undo's own tombstoned mints — whose stale structures-index
entries linger — aren't re-processed into duplicate repairs; the direct-delete
path already drops tombstoned structures from the index, so the guard is a
no-op there.
canonical_bytes embeds both objects and the effect log, so this corrects the
reduced state (an inconsistency never previously exercised — no existing test
broke). Regression: undo_orphaning_a_pre_existing_slur_cascades_it_in_the_
ledger_p13_d1 (cascade + recorded repair + order-independent convergence).
939 tests, convergence/equivocation/conformance green. PASS13: P13-D1 resolved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The K3 verdict for a system pitch introduced by a ModifyEvent replacement
(never minted — the collision pre-walk excludes ModifyEvent) differed across a
snapshot cut: in-session it slipped through (system_mints had no entry, so the
P12-K3 identity check saw nothing), yet post-snapshot it read
SystemDerivedContentImmutable once the pitch was re-seeded as a system mint.
Per the user's call ("reject the introduction"): modify_event now refuses a
replacement carrying a never-minted SYSTEM-DERIVED pitch id (replica
SYSTEM_DERIVED, not Live in objects) with TargetMissing, before the identity
check. The verdict no longer depends on the registry — the pitch is not live in
objects in either frame — so both refuse identically, closing the asymmetry.
Scoped to the system namespace, where the asymmetry lives (only system pitches
re-seed as system mints): a user-replica pitch carries no namespace claim and
has no snapshot asymmetry, so ModifyEvent may still introduce user pitch
content (the concurrent-modify tests rely on it).
Regression: a_modify_event_introducing_a_never_minted_system_pitch_is_refused_
p13_k1. 938 tests, convergence/conformance green. PASS13: P13-K1 resolved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A spanner anchored to a missing region/measure minted dangling: the
CreateCrossCutting mint checked only CrossCuttingValue::endpoints() (events),
so a region/measure TimeAnchor with no live target slipped past — the exact
sibling of the repeat mint gap fixed in Phase D.
Per the user's call ("fix the mint only"): CrossCuttingValue::anchor_object_refs()
returns the full anchor object set (events + a spanner's measure/region anchors;
wall-clock references nothing), and create_cross_cutting's liveness precondition
now checks it, so such a spanner is refused (TargetMissing) rather than minted
dangling. Deterministic across both reduction modes (the base seed registers
regions/measures in objects). endpoints() stays event-only — it feeds the
re-anchoring referent index, and non-event referent re-anchoring stays deferred,
ratified events-only (the spanner discipline).
Regression: create_cross_cutting_spanner_preconditions_region_measure_anchors
(missing region → refused; live measure/region → mints, invariant-clean).
937 tests, convergence/equivocation/conformance green. PASS13_CANDIDATES.md:
P13-D3 resolved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User-review findings on 9b5339f — the Phase-D site-set unification was
incomplete in exactly two consumers that still collapsed anchor_sites()
to events:
- High: the mint precondition validated only TimeAnchor::Event targets,
so CreateRepeatStructure with start naming a missing REGION (or a
volta span a missing MEASURE) minted a dangling repeat straight past
CrossCuttingRefsResolve. Fixed: anchor_object_refs (events + measures
+ regions; wall-clock references nothing) drives the precondition —
deterministic across reduce()/reduce_onto(), since the base seed
registers regions and measures in `objects`. Regression covers a
missing region in start (base-free), a ghost measure inside a volta
span (graph-aware), and the positive measure/region-anchored mint
with invariants green. The referent INDEX stays event-only by design
(the rule table repairs event tombstones — the spanner discipline).
- Medium: editor barrier containment derived only from event locations,
so a repeat anchored solely to a protected region carried a default
context and bypassed a region-scoped barrier. Fixed: repeat_context
walks all anchor objects in anchor_sites order — event/measure sites
bind (region, staff instance) via event_location/measure_location, a
bare region anchor binds the region — used by both Create and Delete
subject arms. Regression: a region-scoped barrier fires for a
region-anchored repeat create and stays quiet for another region.
P13-D3 filed (spec/PASS13_CANDIDATES.md + ops DECISIONS): the SPANNER
family has the same mint-time shape (CrossCuttingValue::endpoints() is
events-only while anchor_target_exists checks all three kinds) plus the
non-event-referent-tombstone gap — pre-existing,
ratified-as-implemented; a catalog-semantics decision, not a Phase-D
fix.
Full gate: fmt, clippy -D warnings, rustdoc -D warnings, 30 workspace
suites, conformance scale 1 (8/8).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd
CreateRepeatStructure / DeleteRepeatStructure land end-to-end,
review-hardened (five-dimension workflow review; 19 findings triaged —
12 verifier agents lost to a session limit were verified by hand).
epiphany-ops:
- CreateRepeatStructureOp { repeat } (lp(RepeatStructure), wire 28,
BORN AT V2: schema_major always 2 — kind/voltas are unconditional
fields) and DeleteRepeatStructureOp { repeat } (bare id, wire 29,
major 0: minimal stamping over a major-0 layout; the discriminant a
schema-minor append). OperationKindTag 28/29 name-verbatim with
validating decode; golden tables grown to 30; migrate/v0 identity
arms; lib re-exports.
- Reduction: set-union mint (live-id => AlreadyApplied without value
comparison — the cross-cutting discipline; RecreateContentMismatch
scope unchanged) with the ALL-anchor-sites-live precondition;
delete-wins tombstone; the "Repeat structure / Anchor" rule-table
row wired into BOTH the ledger (reanchor_for_tombstone arm, min-id
survivor, cascade when dry) and the graph (every dead site rewrites
to the same survivor), agreement asserted by tests incl. the
DalSegno jump-target case.
- Undo: materialize_graph_tombstones gains RepeatStructure — and the
pre-existing SPANNER GHOST fix (an undone spanner mint never left
the graph), regression-locked.
- validate.rs: VoltaEndingsIllFormed advisory (non-empty, 1-based,
strictly ascending; interactive-only).
- Canonical-base honesty: blake3 re-pinned for the corpus stream
shift, its comment corrected (the seeded repeat creates all no-op),
and the property covered surgically by
the_canonical_base_embeds_no_repeat_values (two reductions
differing only in repeat v2 content => byte-identical bases).
epiphany-core:
- RepeatStructure::anchor_sites()/anchor_sites_mut(): THE single
site-set walk (start/end, jump targets, volta spans), consumed by
reduction, the editor barrier seam, the invariant walk, and the
cross-reference index — replacing five hand-rolled copies and
fixing a SIXTH, silently-stale one (indexes.rs had never learned
the Phase-B kind/volta anchors; regression-locked).
- RepeatStructure joins the CanonicalValue seam (strict per-value
decode); decode-fuzz corpus gains valid_score_rich_with_repeats
(DalSegno + voltas) CORPUS-LOCAL — shared render fixtures stay
repeat-free until E1 (zero golden churn).
editor-core / gui / testkit / layout-ir:
- Barrier seam: repeat_event_refs + subjects_of arms +
graph_structure_endpoints arm, gated end-to-end by
repeat_authoring_kinds_derive_subjects_and_gate_on_barriers.
- Generators: operation_payload -> below(30); graph-session gen ->
below(14); ops fuzz gen_payload -> below(27); layout_stub
gen_operation_kind_tag unstaled 24..=29 (the Phase-3 tags were
already missing); GUI name table.
Spec touch-ups (review findings): catalog says "strictly ascending";
the survivor tie-break is now explicit in the core rule-table row and
catalog (identifier-order minimum among multiple survivors;
proximity-aware four-key selection deferred, as for spanners).
Trail: ops + core DECISIONS entries; spec/PASS13_CANDIDATES.md opens
the Pass-13 ledger at three candidates (P13-K1; P13-D1 undo-driven
tombstones bypass ledger re-anchoring — pre-existing class; P13-D2
cue-cascade repair-trail ordering).
Full gate: fmt, clippy -D warnings, rustdoc -D warnings, 30 workspace
suites, conformance scale 1 (8/8).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NEs4aYiu8MXjdYdMxw8PTd