Commit Graph

209 Commits

Author SHA1 Message Date
Levi Neuwirth be244df6a0 Pin 3b's projection refusal was asymmetric: close the reachable half
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
2026-08-07 13:06:40 -04:00
Levi Neuwirth bc06706e41 Container major 1: the format epoch carries base provenance
FORMAT_MAJOR becomes 1 and FORMAT_MINOR restarts at 0. The decoder
stops being exact-major-only: it classifies three ways through a named
FormatEpoch carried on FixedHeader, so major 0 is decoded deliberately
as legacy rather than refused. Old readers already fail closed on an
unknown major, so that half needed no mechanism — which is why the major
is the right carrier, and why the header's immutability, fatal to
FORMAT_MINOR as a provenance field, is what makes it sound as an epoch
field.

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

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

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

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

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

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

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

Workspace green at 1569.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-08-07 12:51:14 -04:00
Levi Neuwirth 973297e1a1 Format epoch amended: pin 3c bounds the interval's conformance cost
Reconnaissance before dispatch found that pin 3a's refusals close every
path to a base-bearing container, which the pins never said out loud.
create already rejects a base-bearing manifest, row 3 refuses committing
one into major 0 and row 6i into major 1, and rows 2 and 5i refuse
opening one in either epoch. For the whole S28 -> S27 interval no bundle
anywhere may carry a canonical base.

That reaches criterion 4's bookkeeping-projection counterpart,
roundtrip::assert_reduction_serialization_stable, which commits the
canonical state as the canonical base and reopens it. Both halves are
refused. Its file was not in the touch table, and staging is limited to
that table, so gate 1 could not have passed and the fix could not have
been staged.

The first reading of the finding said the assertion could only be
suspended. That was too strong and was checked before the pin was
written: read_chunk hash-verifies any ChunkRef, so the serialize -> load
-> decode -> reserialize cycle does not depend on the snapshot being the
canonical base. The harness keeps the cycle and loses exactly two
canonical-base-specific assertions — verify_canonical_chunks's base
branch with its hash cross-check, and the reopened manifest carrying the
base. Both are recorded as owed back by P13-S27.

Re-homing the snapshot to acceleration_snapshots is forbidden: that
field appears nowhere in bundle.rs, so the reference would verify
nothing while looking like preserved coverage. Pin 3c names it as the
tempting wrong repair, and names one more trap — the existing
mis-stamped-base test asserts read-only for a base fault on the
data-model-major axis, which must not be harmonized toward pin 4's three
errors, none of which may degrade to read-only.

Adds touch rows 10 and 11 (testkit roundtrip harness, bundle bench) and
gate 8, which classifies every surviving base-declaring site as a
crafted-image fixture, a refusal test, or the text corpus — a live
commit path among them is a failure.

Forward amendment of a ratified contract, with its own review round.
Documentation only. Still not implemented.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-08-07 11:41:51 -04:00
Levi Neuwirth 8bb917d7c3 Format epoch ratified: container major 1 owns the provenance boundary
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
2026-07-31 21:46:56 -04:00
Levi Neuwirth 818a16f1ef Pass 13 ledger: P13-S28 filed as the critical path
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
2026-07-31 21:04:22 -04:00
Levi Neuwirth 79a2507d47 Contract for P13-S27, tracked as DRAFT and blocked
The reduction-version check is a tautology: reduction_version_for seeds
a superblock from the canonical base's self-report, and open compares
the base against that superblock. Both operands descend from the same
source. It catches a corrupt base disagreeing with its superblock, and
necessarily passes for a conformingly propagated stale one.

Rulings: a typed BundleCapabilities required at both constructors and
carried on the Bundle, with no default; outright rejection on mismatch
via CanonicalBaseRequiresRebuild, not read-only and not an anomaly.
Storing the capability keeps all 57 commit sites unchanged; only open
(57) and create (32) move.

Pin 2a stays an open question. Baseline zero does not preserve the
corpus, and after S16 moves the authority to one, a pre-S27 base that
happens to carry one is indistinguishable from a legitimately rebuilt
S16 base. A raw u32 has no provenance. Four candidate dispositions are
recorded, including FORMAT_MINOR as a carrier, which is rejected here
for two independent reasons: the header never changes after creation
(core_spec.tex:10799) so a legacy bundle that commits a validated base
keeps its old minor forever, and a minor change may only append
append-safe discriminants (core_spec.tex:12258), not alter acceptance
semantics — while current readers ignore minor entirely, so the
boundary would bind only readers that already comply.

What survives: provenance must be carried by a container property old
readers cannot silently accept and a later commit cannot inherit
unchanged. That is a format-epoch design, filed next as P13-S28.

Three claims of mine died while scoping this, all recorded in the
contract: a constructor-grep that could not see a propagated value, a
head -14 truncation behind a universal negative, and reading
`format_major != FORMAT_MAJOR` for the half that supported the
proposal. A contract about unobservable defects should carry its own.

Docs only. Not dispatchable. S16 and S27 both remain blocked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-31 21:03:29 -04:00
Levi Neuwirth 381c49849e P13-S16 scoped and blocked; P13-S27 filed
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
2026-07-31 20:27:43 -04:00
Levi Neuwirth f876836454 G3a contract: ten source locators, corrected forward
The ratified reasoning is untouched — every changed token is a bare line
number. What the contract argues, pins, and disposes of is unchanged;
only its pointers move, so a reader following them lands on the code the
sentences describe.

Ten, not the two first found: reduce.rs:4119 and :4117 both name
CreateStaff's group precondition, now :4372; invariants.rs:1126/:1135
name invariant 10's two resolution arms, now :1188/:1196; graph.rs:819
and :1614 name the two fields, now :848 and :1650, cited twice each.

core_spec.tex:5578 appears twice and needed two different corrections —
at the prose it quotes the doc line "Visual grouping: which staff group",
now :5585; in the pin-4b table it parallels :4231's struct declaration,
so it wants `pub struct Staff {`, now :5564. A single substitution would
have been wrong in one of the two places.

Forward, not by amendment: the contract is ratified and pushed, and its
hash is cited by the ledger and the genesis plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-31 19:47:43 -04:00
Levi Neuwirth f90a5b1c29 Pass 13 ledger: two rows corrected against the tree, two filed
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
2026-07-31 19:44:41 -04:00
Levi Neuwirth a4d78471bb P13-S22 contract: M2's whole-test alternative was false
§3 offered "delete the new table's row loop (or the whole new test)" as
interchangeable forms of the paired control. They are not. Deleting the
test drops the suite to 1557, contradicting the 1558/0 result required
in the same sentence; only loop-deletion holds the test count fixed so
the passing count is comparable to the baseline.

Found by the executing agent, which took the loop reading — the only one
satisfying both conditions — and reported the tension rather than
silently picking. Corrected here as a forward commit: 6e4e455 already
sits on the contract, and the evidence chain reads better with the
defect and its repair both visible.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-31 19:30:21 -04:00
Levi Neuwirth 6e4e45587f P13-S22: the tag space gets a second witness
`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
2026-07-31 19:28:28 -04:00
Levi Neuwirth 3397c0573e Contract for P13-S22, tracked as DRAFT before dispatch
The tag space has one statement of its own discriminants: the
`operation_kind_tag_vocabulary!` macro. `PAYLOAD_FREE` carries variants
only, so every derived test obtains the pair from that same invocation
and asserts `$disc == $disc`. The kind side is safe for a reason the tag
side lacks — `OperationKind::discriminant()` is a hand-written match, so
two independent statements exist and the golden lock asserts they agree.

Disposition A: a hand-written literal table supplies the second witness.

The signing mutation, executed at 17c1d67: swapping tags 32 and 33 in
both the discriminant literals and the declaration lines leaves
`PAYLOAD_FREE` emitting ascending discriminants, so every derived
artifact stays byte-identical. Two operations exchanged wire
discriminants and the workspace suite returned 1558/0 — unchanged from
baseline. The three probes already on record swapped literals only,
which reorders the corpus and gets caught; they proved a lock exists,
not that one is missing.

Retires one test, not six: the table duplicates the tag→byte subclaim of
the three retained assertions but does not subsume the kind-and-tag
pairings that contain them, nor G3b's local mutation evidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-31 19:11:33 -04:00
Levi Neuwirth 17c1d67561 P13-S19: what a partial measure actually costs
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
2026-07-31 18:13:44 -04:00
Levi Neuwirth ab430ec52d Contract for P13-S19, tracked as DRAFT before dispatch
P13-S19 is filed as a deferral. It is closer to a live defect, and the tree
already contains the proof: m35_boundary_flags_wrong_distance places a first
measure at offset 0 and its successor half a whole note later, under a signature
whose measure_duration is a whole, and asserts invariant 20 fires. That is a
pickup, and the test has been labelled "wrong distance" since packet 2.
create_measure applies the same rule as a refusal. So authoring a pickup does
not merely leave it unmodelled -- it makes the rest of the instance unauthorable.

The rung corrects seven surfaces, three of them normative, one of them
outright false: ops/DECISIONS.md says all three CreateMeasure clauses are
vacuous for a first measure, and the agreement clause has no predecessor
dependency at all. It observes the reducer refusal rather than citing it, files
the Chapter 3 splitter deferral as its own candidate, and introduces no
partial-measure notion -- that is a semantic rung and pin 9 stops this one at
its edge.

Three over-readings committed while drafting are recorded inside the contract
rather than smoothed away, because text that is locally true and globally
misleading is the rung's own subject. It claimed a pickup is "neither refused
nor flagged" (the agreement clause runs on a first measure); it claimed that
again more weakly after the first fix (referential preconditions and invariant
10 still apply); and it listed core/DECISIONS.md as an understated surface by
matching "never flagged" without reading the qualifier that makes it correct.
That file is now explicitly out of bounds, with the reason, so a later reader
comparing it against the corrected ops entry does not "fix" the one site that
already draws the distinction.

The absence gate replaces a grep that could not fail. Two of its seven phrases
straddle a newline, so a line-oriented search would have returned nothing before
the correction as well as after; the gate normalizes whitespace, and all seven
anchors were confirmed present at 339269b so it fails today and can pass only
once the work lands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-31 17:50:02 -04:00
Levi Neuwirth 339269b3cf P13-S18: the invariant-20 outcome matrix, and what the residue actually is
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
2026-07-31 17:05:49 -04:00
Levi Neuwirth 35e34d948e Contract for P13-S18, tracked as DRAFT before dispatch
Filed ahead of execution so the contract is under version control before any
agent touches it. This session already learned the other way round: the G3b
contract sat untracked past a "clean" gate, because git diff --check cannot see
a file that never reached the index, which is why this contract's own section 5
mandates git diff --cached --check instead.

The rung it scopes is diagnostic. Invariant 20 has nine non-success paths, not
the three P13-S18 records; only three are abstentions. Two are delegated to
invariant 10, two are vacuous, one is inapplicable, one is P13-S19. The rung
builds an 18-cell outcome matrix, classifies every path, corrects the ledger and
two drifted citations in the G3b contract, and files P13-S23 for the capability
everything defers to. No behaviour change: the gate requires
check_measure_meter_consistency's executable body to stay byte-identical, and a
finding that suggests changing which graphs violate invariant 20 stops the rung
rather than being implemented in it.

Six review rounds are recorded in the pins as corrections, not smoothed away.
Successive drafts scoped the residue at five paths then nine; called the
Measure-end claim false, then misattributed, before landing on conditionally
false; conflated two distinct WallClock shapes whose clauses fail differently;
and scoped P13-S23 twice too narrowly, the second time excluding B5 -- comparable
pairs that yield no delta -- which is a third of what it is filed to own.

Baseline 1541/0 confirmed at f33673d. The commits since cc49533 are spikes/-only,
a separate workspace, so the root count is unmoved and the section 0 readings
stand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-31 14:51:10 -04:00
Levi Neuwirth cc49533c76 P13-S22: the fragment count follows the inventory it describes
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
2026-07-30 21:38:15 -04:00
Levi Neuwirth 6f790bca65 P13-S22: Registered = 16, and why the numbered tag rows do not count
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
2026-07-30 21:36:43 -04:00
Levi Neuwirth 02eb3aa32f P13-S15 ledger repair, and P13-S22 filed against the tag space
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
2026-07-30 21:32:12 -04:00
Levi Neuwirth dcb28f0c25 P13-S15: the golden lock catches up with ten appended kinds
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
2026-07-30 21:17:49 -04:00
Levi Neuwirth 4f141ce197 P13-S21: the two precondition reasons the wire spec never named
PreconditionFailureReason 14 (AcousticRealizationPinned) and 15
(TranspositionOutOfRange) entered the vocabulary at Push 4a and reached this
document at neither place that owed them. The bounded enumeration ran 13 straight
to G3b's 16, and Push 4a's own history row recorded only OperationKind 30 while
saying nothing about the two reasons it appended in the same epoch. The Operation
Catalog documented both at its 0.8.0 and effect.rs has carried both throughout;
only the wire specification was silent.

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-30 20:45:27 -04:00
Levi Neuwirth 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 2892e7b2cd Give §1.1's ruling a normative footprint, and fix two impossible mutations
1. The ruling had no normative/API surface. §1.1 assigns authority, but Pin 3
   still said "nothing else in core" and the touch table listed graph.rs under
   "Not touched" -- leaving the contract and P13-S16 as the only carriers of
   supposedly normative meaning while core_spec.tex remained ambiguous. A
   normative ruling that lives only in a contract is not normative.

   Added pin 4b requiring all four sites, and narrowed pin 3 to the codec
   surface ("no other CODE change in core"):

   - graph.rs:819 Staff.group -- sole authority. Note this field has NO doc
     comment today; it must be written, not amended.
   - graph.rs:1614 StaffGroup.members -- non-authoritative projection, must
     not be read for membership, may be stale in both directions. Also
     currently undocumented.
   - core_spec.tex at the Staff/StaffGroup declarations -- the same rule
     normatively. This is the document that declared both fields without
     ranking them, so it is where the ambiguity actually lives.
   - operation_catalog.tex -- explicit stale-form semantics in the new
     CreateStaffGroup section AND the existing CreateStaff section (:1104).
     CreateStaff needs it because it is the operation that creates the
     missing form, and its section currently promises nothing.

   Each cross-references P13-S16. graph.rs moves into the touch table
   (documentation only; no type changes anywhere in core). Added t14 to guard
   the two field doc comments, sliced per-field on t12's discipline since a
   file-wide grep cannot fail here.

2. t8b's two mutations were assigned to the reducer that runs FIRST in each
   order, so neither could reach the state its assertion names:

   - Missing form (CreateStaffGroup then CreateStaff): create_staff_group
     cannot append a staff that does not exist yet. Mutation moved to
     create_staff -- append the newly minted staff to g.members.
   - Spurious form (CreateStaff then CreateStaffGroup): create_staff has no
     later group to repair. Mutation moved to create_staff_group -- reject or
     normalize away the non-empty carried members.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-29 11:39:54 -04:00
Levi Neuwirth ec25a5e0c1 Ratify §1.1 as disposition B, and file the gap it defers
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
2026-07-29 11:34:54 -04:00
Levi Neuwirth 012a2de16d G3a contract repairs: the authorship cycle, and four maps the pin promised without
Five review findings, all confirmed against the tree before repair.

1. StaffGroup/Staff authorship cycle. CreateStaff requires its group live
   (reduce.rs:4117); CreateStaffGroup requires its members live. With mints
   only, NO authoring order yields a bidirectionally consistent group, and the
   inconsistency is unrepairable in-packet -- there is no ModifyStaffGroup and
   deletes are deferred. Invariant 10 checks resolution in both directions
   independently (invariants.rs:1126, :1135) and agreement in neither; the
   spec declares both fields (core_spec.tex:4231, :5578) without saying which
   is authoritative. Added as §1.1, UNRESOLVED, with two dispositions and a
   recommendation -- the shape G2b's accidental_extensions pin took. t8's
   claim is narrowed accordingly: what closes is that CreateStaff's group
   precondition becomes satisfiable, not that a consistent group becomes
   authorable. The unqualified "defect closes" framing was wrong.

2. Pin 4 promised byte-identical re-carry without the machinery that makes it
   work. Comparing "the same value" requires retaining it, and the object map
   holds only Live/Tombstoned. Exactly three carried-value maps exist
   (reduce.rs:999-1007), each threaded through seven sites; G3a needs four
   more, so 28 touch points the first draft omitted entirely. Added pin 4a
   with the seven sites enumerated. Site 4, base seeding, is the one that
   fails silently: a re-carry test that only reduces from empty cannot see a
   missing seed at all. Added t5b (base re-carry) and t13 (each of the four
   seeds killed separately), modelled on G1's documented instrument_values
   precedent at reduce.rs:13264.

3. The companion bump was stale. The tree is at 0.11.0 (G2b bumped it,
   textproj/src/lib.rs:47); the contract repeated G2b's 0.10.0 -> 0.11.0.
   Corrected to 0.11.0 -> 0.12.0 at both sites, with the negative vector
   rejecting 0.11.0.

4. Mutation table repairs:
   - t4's mutation was impossible. A create op's encode_canonical is a single
     push_lp_bytes line over one carried value, so there is nothing in the op
     to reorder. Moved to the carried type's struct_codec! declaration
     (codec.rs:1790), which is where the layout actually lives and is
     self-consistent across both halves -- exactly trap 4's shape.
   - t6 omitted PartDefinition.staves. Now three independent mutations, one
     per referential loop; an uncovered loop is a loop that deletes green.
   - t9 moved fixture and production together, which proves only that a bad
     score fails a checker. Fixture now constant and already dangling; only
     production mutates, one skipped check at a time.
   - t12 could not fail: grepping the whole file matches the implementation
     body, which contains the identifiers the doc comment is meant to gain.
     Now slices the invariant-10 doc block and searches only that.

5. Touch table missed testkit/src/layout_stub.rs:1373, whose 30..=34 range
   restates the coverage claim in prose. Added there and to the plan's trap 6,
   now five boundary-crossing literals rather than four.

Editor authorization recorded as granted narrowly: barriers.rs for the four
exhaustive subjects_of arms only, barrier.rs for the invalid-tag literal,
comment, and assertions 35 -> 39 only. No other change in either crate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-29 11:26:41 -04:00
Levi Neuwirth f1ce25f8f8 Ratify the G3 rulings and draft the G3a contract
Folds the three open PLAN_GENESIS_OPS.md §6 rulings, ratified 2026-07-29:

1. Deletes: mints only, all five deferred out of G3. The former "live
   dependents -> container-not-empty" sentence is superseded outright --
   ContainerNotEmpty concerns owned children (effect.rs:156), while the G3
   hazard is dangling inbound references from independently-live objects,
   which would need a new typed reason and its own epoch.
2. decomposition_attachments: derived, not authored.
3. Measure.time_signature: resolution-plus-agreement. An optional explicit
   declaration at the measure start, neither an override of the metric grid
   nor a cache of it. Invariant 20 (G3b) covers agreement and boundary
   consistency only, not reference resolution.

G3 splits: G3a (four root-level mints, kinds/tags 35-38, epoch 11, all
schema major 0) and G3b (CreateMeasure, kind/tag 39, epoch 12, carrying
invariant 20 and a new precondition reason at discriminant 16).

Correction of record: an earlier scoping claimed invariant 10 "covers
cross-cutting refs, not this". False -- read off the variant's doc comment
rather than the check body. Invariant 10 already resolves a staff's group, a
group's members, a part's staves, a view's active layers, and measure and
grid time-signature references (invariants.rs:1122-1156, :1180-1212), tested
at :3596. Its doc comment names none of them, so G3a owes a prose
reconciliation and no enum entry.

Also corrects five drifted citations that a touch table exists to prevent:
reduce.rs 3850->4075 and 2342->2559, barriers.rs 437->313, barrier.rs
1105->1156, text_projection_grammar.rs 307->315; and "five siblings" ->
"four siblings" now that CreateMeasure is its own packet.

Verified against the tree, not assumed: all four carried types are schema
major 0 (no versioned walk exists; both decode_v0_score and the live walk
read them through plain Codec::dec), TypedObjectId already carries all five
variants, and struct_codec! generates TextValue as well as Codec -- so G3a
moves no wire bound, appends no typed id, and needs no textvalue_graph.rs
work. It does close a live defect: every G3 object kind becomes Live only
through base ingest, so CreateStaff's group precondition is currently
unsatisfiable under from-empty reduction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-29 11:12:49 -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 039a8ddec8 G2b amendments: undo restores the seed, epoch 10 lands, and a real touch table
Documentation only. Ready for dispatch after these four repairs.

Replaces pin 5 and t7. The earlier draft required undo to distinguish "never
authored" from "authored to the default", which PLAN_GENESIS_OPS.md section 5
trap 5 explicitly withdrew - and SetMetadata is the disproof: Score::empty
seeds metadata exactly as it seeds tuning_context, and base ingest runs
metadata_chain.seed precisely so a value-restoring undo of the first
operational write restores the pre-operational state. Undo restores the seeded
base settings, default or not, and the distinction must stay UNOBSERVABLE -
code that can tell the two apart is a defect, not a safeguard. Predecessor::Base
vs ::Write earns its keep only for the bookkeeping families, where a base
predecessor returns a map key to absence; ScoreTuningContext is always-valued
like metadata and has no absent state to return to. t7 now asserts the two
cases are indistinguishable rather than that they differ.

Appends epoch 10 to the authoritative ladder with G2b as the event. It stays
monotonic and prefix-closed, and it is the first exercise of the ladder's own
growth path - G-minor's wildcard-free introduced_minor means kind 34 cannot
compile without one.

Adds the explicit non-goal the plan requires: G2b authorizes no pruning or
compaction of the canonical operation log. The rung sharpens that prohibition
rather than relaxing it, because after G2b pruning would discard authored
genesis state rather than merely re-derivable state.

Adds the touch table the gate already referred to but which did not exist,
derived from G2a's actual thirty-file footprint plus G1's one extra rather than
from recollection: the new core type and its export, full ops plumbing
including envdecode/migrate/v0/valuegen/fuzz/vectors, the bundle accept-set
raise and its falsified rationale, the four budgeted boundary crossings, text
projection with companion 0.11.0, all four .tex sources with all four PDFs, both
vector corpora, and P13-S13's closure.

Ratifications folded in: subset design, epiphany_core::TuningContextSettings,
epoch 10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 21:27:17 -04:00
Levi Neuwirth b724e94faf Contract revision 6: round 1 was testing the wrong property on the wrong glyphs
Building round 1's oracle exposed two defects in the round as written, both
mine, and both invisible until something tried to satisfy it.

The glyph set was chosen by subpath count. I wrote that 19 of the 37 bundled
outlines have more than one subpath and named gClef, fClef, timeSig8 and
accidentalFlat as the useful ones -- conflating multi-subpath with has-a-hole.
fClef is the counter-example: its three subpaths are a bowl and two SOLID,
disjoint dots, nested in nothing. Measured across all 37 by point-in-path,
exactly twelve carry a bounded hole; fClef, cClef, barlineFinal and every
repeat glyph carry none.

Round 1 now runs five glyphs in two classes testing two different properties.
Hole checks -- gClef, timeSig8, accidentalFlat and noteheadHalf, which is new
and earns its place by being frequently repeated and semantically consequential
(a filled counter renders half notes as quarter notes, a notation error rather
than an artifact). Disjoint-component check -- fClef alone, no background
requirement, instead requiring one ink point inside EACH of its three filled
subpaths, tagged with its subpath index. That second class catches a
tessellator that keeps only the largest contour, which would pass every hole
check ever written. Hard failure is now stated as either: a bounded hole
painted as ink, or a required filled subpath omitted.

The oracle's status model is now specified rather than inferred from an
absence. fClef passing with zero background points is a SATISFIED result under
its own requirement class; recording it only as background_satisfied = false
would make a correct outcome indistinguishable from a failed one.

The second defect was the criterion itself. Ruling A said epaint does not
implement even-odd/nonzero fill for paths with holes -- framing criterion 1
around the fill RULE. Bravura's contours are correctly oppositely wound
(signed ring areas gClef [8.702, -0.691, -1.803, -0.509]; fClef
[2.534, 0.153, 0.148], all positive, the same fact from the other side), so
even-odd and nonzero AGREE on every bundled hole. The rule is not load-bearing;
preserving every filled contour and every bounded counter is. The criterion is
amended to compound-path / inner-subpath fill correctness, with a more accurate
reason for excluding raw egui shapes than the one it replaces: PathShape is a
single point loop documenting "Fill is only supported for convex polygons", so
it cannot express compound-fill or subtractive-hole semantics -- a Shape::Vec
can group loops, but grouping paints them, it does not subtract a counter from
its enclosing contour.

Signed areas are recorded from the oracle's adaptive flattening rather than an
earlier coarse fixed-step measurement, with the note that magnitudes are
flattening-dependent and the SIGNS are the claim.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-28 21:24:47 -04:00
Levi Neuwirth 3af242d457 Draft the G2b contract: subset over normalization, and the raise this rung pays
Documentation only. Not dispatched - two items need ratification.

Resolves the open accidental_extensions pin as SUBSET, not normalization.
SetTuningContextOp carries a new type holding exactly the five wire-bearing
fields, and reduction leaves accidental_extensions untouched rather than
clearing it.

The deciding argument is that this costs no wire design at all:
ScoreTuningContext::enc already encodes exactly those five fields in exactly
that order, so the subset type's canonical encoding is byte-for-byte the
current one. It is a type-level narrowing, and canonical_value! still applies.
Normalization would instead make correctness depend on clearing a field at
every construction site, enforced by nothing the compiler or the codec can see
- canonical_value! compares bytes and never the originating value, so a field
that never reached the bytes is structurally invisible to it. That is the same
hand-maintained-discipline shape that went stale four times at Push 4a and six
times at G2a. Preserving is also semantically right: the operation carries no
information about the field, so clearing it would silently erase a live
registry extension the wire cannot even record.

Nothing in production populates accidental_extensions - every non-empty
reference in the tree is a test - and the text projection drops it too, so the
subset type ratifies the status quo rather than changing it.

Ten pins. The accept-set raise is charged here because minimal stamping is a
function of each payload's value and ScoreTuningContext is the only genesis
payload born at major 3. bundle.rs:56 does not merely document the cap, it
asserts that no operation payload embeds the tuning context - a sentence this
rung falsifies, so it must be rewritten rather than left beside a changed
number.

Awaiting ratification: the subset type's name, and epoch 10 for kind 34, which
is the first exercise of the G-minor ladder's own growth path. G-minor's
wildcard-free introduced_minor means kind 34 cannot compile without one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 21:15:07 -04:00
Levi Neuwirth b8601ec909 File P13-S15: the OperationKind golden lock stops at 29, and close P13-S14
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
2026-07-28 21:10:17 -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 0c8b8db1a5 Amend the G-minor contract: the [7f] oracle, and the PDFs the touch table missed
Documentation only. No implementation.

Rules pin 11 as (b) with (a) normative. textproj stays a preserving producer -
it carries the declared manifest version verbatim, never decodes barriers, and
the document author owns updating it when editing opaque barrier bytes.

On top of that, an independent testkit conformance oracle at gate [7f]. When
every barrier blob decodes it recomputes the exact aggregate and requires
equality, not >=: equality is what catches stale over-stamping after a
contributing barrier is removed, which >= cannot see. Both negative fixtures
are required - a tag-31 blob carrying baseline, and removal of the sole maximum
contributor. An undecodable blob is reported not-checkable and never as a pass,
with byte-and-version preservation left to s10/s13 rather than claimed here.
The gate must describe itself as validating known decodable in-tree artifacts,
not as evidence that textproj validates arbitrary edits.

Three new mutations. s16 is deliberately inverted: relaxing equality to >= makes
the over-stamp fixture go green, and that green is the evidence that equality is
load-bearing.

The touch table gains the testkit conformance files and the two tracked PDFs
whose .tex sources change. core_spec.pdf and operation_catalog.pdf stay
build-only for the undefined-references check; if either turns out byte-changed
that is a finding, since this contract changes neither source.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 18:59:00 -04:00
Levi Neuwirth f94102d839 Contract G-minor: ten pins, one open ruling, fourteen mutations
Documentation only. No implementation.

Drafts the implementation contract for the schema-minor rung against the
ratified epoch ladder and the vocabulary audit. Ten design pins covering the
derivation (envelope max over emitted, block max over envelopes, major and
minor derived independently), the five vocabularies that need introduced_minor,
the manifest seam, and the text-projection consequence.

Three pins exist to prevent specific wrong edits. The baseline sentinel must
not be 0, because 0 is a real baseline minor for V1-V3 and conflating them
makes the max read correctly by accident. OperationKindTag's epochs go inside
operation_kind_tag_vocabulary!, not beside it, because a sibling match is the
parallel list the macro exists to prevent. And bundle.rs:301 must stay a
major-only comparison - tightening it to the full version would be a
conformance regression, and it is the most tempting wrong edit in the packet.

One pin requires ratification before implementation: epiphany-textproj cannot
detect changed barrier bytes, having no prior state to diff and no layout-ir
dependency to decode with. Recommended disposition is a documented preserving
producer, with a testkit conformance check as the alternative. The two differ
by a test rather than a design.

Fourteen tests, each with the mutation that must kill it, including the
rejected highest-discriminant policy, the over-stamp after a contributing
barrier is removed, and the bundle.rs:301 tightening.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 18:50:45 -04:00
Levi Neuwirth 0def1563c8 Amend the G-minor plan: the seam ruling, the companion consequence, one wrong hash
Documentation only. No Rust, no .tex, no vectors, no implementation.

Corrects G2a's introducing commit to 7df5ca1 - where kinds/tags 32-33 enter
payload.rs. 55eff00 is the later review-fix and introduces no discriminant.
The hashes are the evidence for the ladder's monotonicity, so citing a
follow-up commit would make it unverifiable at exactly the rung a reader is
most likely to re-derive.

Records the ruled seam. epiphany-bundle stays opaque with no ops or layout-ir
dependency; producers supply the aggregate manifest SchemaVersion explicitly
and CommitContext exposes the previous one so unchanged barrier content
preserves it. That is new plumbing rather than a read-through: Manifest has no
schema-version field, it lives in the superblock, and it must not gain one -
a field addition is schema-major regardless of type, which would defeat the
rung. Changed barrier bytes require an aware producer that computes the exact
epoch or refuses, because blindly retaining the previous aggregate
over-stamps once a contributing barrier is removed.

Manifest::SCHEMA becomes a baseline constant rather than the universally
emitted version. Three sites silently select it, not two - bundle.rs:220,
bundle.rs:724, and the re-exported helper manifest_chunk_hash at bundle.rs:1298,
which is public API and so a signature change. The major-only check at
bundle.rs:301 stays as-is and stays correct.

Consequently G-minor does bump the text companion: 0.9.0 to 0.10.0 with corpus
regeneration, because TextDocument must carry the manifest SchemaVersion that
the projection cannot derive from deliberately opaque bytes. Not because of
op-block stamping, which remains projection-invisible - section 2's "no
companion bump" correction is still true about op blocks and now carries a
forward pointer so it does not read as the whole answer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 18:47:42 -04:00
Levi Neuwirth 2c7191482a Ratify the G-minor epoch ladder, and narrow the Manifest::SCHEMA rule
Documentation only. No Rust, no .tex, no vectors, no implementation.

Records the ratified schema-minor epochs (minors 2-9, one per additive event:
M2c, Push 3, Phase-3 tranche 1, Pass-12 G-pass, schema-major-2 repeat revision,
Push 4a, G1, G2a). These are schema-minor epochs, not companion semver.

The ladder is complete against spec/AUDIT_GMINOR_VOCABULARIES.md - every
post-baseline variant appears exactly once - and monotonic in the topological
order of the commits that introduced each variant, which is what makes a
declared minor prefix-closed. Verified against git rather than assumed,
including the two events sharing 2026-07-07.

Major baselines unchanged: V0 minor 1, V1-V3 minor 0. Baseline variants impose
no additive override; M2c's operation kinds stay baseline while its
PreconditionFailureReason append requires epoch 2.

Manifest::SCHEMA is narrowly superseded. Changed child ChunkRefs still never
raise the manifest minor, but emitted barrier tags do: a manifest naming a tag
in 24-33 takes that tag's epoch. Major stays 0. The derivation seam is left to
the implementation contract, with the case that must not be discovered late
written down - epiphany-bundle depends on neither ops nor layout-ir and
preserves barrier bytes verbatim, so a repack can carry a blob it cannot
decode, and "decode and inspect" does not cover it.

PLAN_GENESIS_OPS.md:257 no longer says "per-kind minor". That policy was
rejected; the paragraph now carries the ratified rule and records why.

Every gate in PLAN_GMINOR_SCHEMA_MINOR.md section 5 is discharged. The
implementation contract may now be drafted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 18:39:28 -04:00
Levi Neuwirth c90d662a0c Fold the vocabulary audit into the G-minor plan: the ladder is not ratifiable as written
Documentation only. No Rust, no .tex, no vectors, no implementation.

Records spec/AUDIT_GMINOR_VOCABULARIES.md (c63258d) as the governing
reachability inventory, superseding section 3's table where they differ. Section
3 stays as the reasoning that led there.

Section 4's tentative epoch ladder is marked incomplete rather than merely
unratified: it was drawn against OperationKind alone, and the audit placed
three families it never considered. OperationPayload 3 (Push 3) and
ReanchorReason 6 (Pass-12 G-pass) have no rung, and PreconditionFailureReason
10-15 spans four tranches of which M2c and the G-pass are unplaced.

Also records the distinction that makes the global epoch space and the
per-vocabulary baselines compatible: the epoch answers which revision
introduced a variant, the baseline answers whether it predated that
vocabulary's own lock, and only the second varies. M2c is the proof - baseline
for OperationKind, a genuine append for PreconditionFailureReason. So
introduced_minor is assigned per variant, per vocabulary.

The .tex correction at binary_format.tex:2373 is recorded as owed by the
implementation, not by this plan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 18:29:59 -04:00
Levi Neuwirth c63258d32a Audit the G-minor reachable vocabularies: a ledger, a matrix, one stale sentence
Read-only packet. Walks each of the nine ChunkKind roles' payload encoders
transitively and records every discriminant vocabulary actually emitted,
with every variant classified baseline / post-baseline native / escape.

Two vocabularies outside the OperationKind/Tag/Payload trio have real native
appends: ReanchorReason (SameCanvasNearer = 6, Pass-12 G-pass) and
PreconditionFailureReason (10-15, four separate tranches). Fourteen further
escape-carrying enums checked and clean, recorded as evidence.

Two findings the plan's section 3 table did not name. The canonical base
reaches RepairKind, ReanchorReason, and SpellingNominal, and ReanchorReason
is one of only two vocabularies with an append. And the manifest reaches
OperationKindTag with no operation envelope in it at all, through
ExtensionDeclaration::edit_barriers -> EditBarrier::prohibited_operation_kinds
- the disposition-(c) crossing, realized in a role the plan never mentions.

binary_format.tex:2373 is recorded as stale narrative: it says OperationKind
and OperationKindTag "append at >= 30" and its history names only 24-29,
while 30-33 are taken. The normative tables at :1443 and :1526 are current
and authoritative; the .tex correction is deferred to the G-minor
implementation, which must touch that paragraph regardless.

CompressionAlgorithm is ruled out of scope for schema-minor epochs: it is
ChunkRef transport metadata, not a discriminant emitted by the payload
SchemaVersion governs, and it is deliberately excluded from chunk identity.
Recorded so it is not reopened.

The epoch space stays global, but baseline classification and introduction
events are per variant per vocabulary. The tentative ladder is incomplete -
Push 3, the Pass-12 G-pass, and M2c are unplaced - and must not be ratified
as written.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 18:27:35 -04:00
Levi Neuwirth 9dc48ecb70 Audit contract amendments: a possible gate, and four scoping repairs
The gate was unsatisfiable. The tree is already dirty with the editor track's
Cargo.toml and spikes/ subtree -- which the contract itself acknowledged two
sections earlier -- so "exactly one new file" could only have been faked or
argued around. It is now a before/after status delta whose sole permitted
difference is the new audit file, with anything else, a cargo-touched
Cargo.lock included, reported rather than cleaned up.

Scope widened to every reachable discriminant vocabulary, open or append-only
or expanded by ratified revision. binary_format:2386 permits minor-additive
appends to the value unions it describes as closed, so "closed" was the wrong
exclusion boundary; only ChunkKind is genuinely out.

Role completeness now has four dispositions rather than two, because opacity
is a property of a layer and not of bytes. Normatively typed bytes opaque only
to the immediate layer -- operation envelopes, manifest barrier blobs -- must
be crossed into and their producer encoders walked; treating op blocks as
opaque would drop the one vocabulary G-minor exists for. Producer-owned opaque
bytes such as ExtensionData get an explicit "no core derivation possible,
schema carried from the producer", since cannot-see-inside and nothing-inside
are different findings.

The escape checklist is now sixteen named entries and the count is deleted
rather than corrected: BarrierScope and BarrierCondition are separate enums,
and a prose pairing of them is exactly how the miscount happened.

Baseline enumeration is mandatory, in a separate vocabulary ledger classifying
every variant as baseline, post-baseline native, or escape/reserved. The matrix
references it instead of repeating large baseline sets, and a disagreement
between the two parts is a defect in the audit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 18:08:55 -04:00
Levi Neuwirth 6145efecd9 Contract the G-minor vocabulary audit: a matrix, read-only
The gating work before G-minor can be contracted. Deliverable is one file,
spec/AUDIT_GMINOR_VOCABULARIES.md, holding a reachability matrix keyed
chunk role -> encoded payload type -> discriminant vocabulary ->
post-baseline variants -> introduction event -> derivation site.

Method is pinned to walking the encoder rather than the type, because a field
can exist and never reach the wire -- ScoreTuningContext::accidental_extensions
is the standing proof -- and the matrix's purpose is to say what is emitted.

Inclusions cover nested additive variants at any depth and, the sharpest edge,
later native additions to enums that also carry a Registered escape: the escape
is out of scope while a new native variant on the same enum is not, and
binary_format names fifteen such carriers. ChunkKind is excluded as closed.

The trap most likely to corrupt the matrix: the spec's "append at >= N"
phrasing names the next free slot, not the baseline boundary. OperationPayload
is documented as appending at >= 4 while discriminant 3 is itself an append,
and OperationKind at >= 30 while 24-29 were appends too. Post-baseline
membership must come from revision history, not from those numbers.

Epoch numbers are explicitly not ratified here; column 5 records the
introduction event. Completeness is enforced by requiring a per-role
accounting including roles that produce no rows, and negative results recorded
for every escape-carrying enum -- an unlisted role being indistinguishable
from an overlooked one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 18:02:32 -04:00
Levi Neuwirth 82ac636982 Revise the G-minor scope: the recommended policy could not work
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
2026-07-28 17:59:56 -04:00
Levi Neuwirth a229a977a2 Scope G-minor: the schema minor is content-addressed, and that changes the rung
The scoping question was whether the chunk header's minor reaches a pinned
corpus. It does, and worse: chunk_content_hash pushes SchemaVersion's canonical
bytes -- major AND minor -- into the preimage, so raising a minor changes the
ChunkId and propagates to the manifest that names it. Not a semantic break,
since readers gate on the major only, but a content-address-moving change
landing on the structure req:format:manifest-id makes a two-writer promise
about.

The text projection also projects the minor as a document surface, and the
committed corpus carries six literal (schema 0 1) occurrences, so op-block
minors rising means another companion bump on the G1/G2a precedent.

The policy is the real work and is left for ratification: one minor per tranche
(a retroactive judgement needing a hand-maintained table -- a seventh such site
on a track whose defining lesson is that they go stale), minor as the highest
discriminant emitted (derivable, self-describing, nothing to maintain, but
redefines the term and is per-vocabulary), or a per-major append counter. I
recommend the second and say why the recommendation is not a default.

Four further questions logged, including whether the canonical base is exempt:
its major is role-pinned to 0 and its byte-identity test pins the payload, not
the header, so a base whose minor rises keeps its payload and moves its id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 17:49:29 -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 61890a046f Editor T4 opens: the toolkit spike gets a contract that cannot be argued with
Ruling A grants the architecture -- direct vector canvas over ResolvedLayoutIR,
viewport-culled, per-system damage -- and deliberately leaves the toolkit open
for this spike to decide under six recorded criteria. All three prerequisites
are discharged (W1 dd33b34, W2 24f8c80, W3 f639919), so the tranche opens.

The contract's problem is not what to build but how to keep the answer honest.
Six criteria across three candidate stacks is unbounded work with a dozen
places for the verdict to be decided by whoever writes the report. So the
structure is an elimination ladder, cheapest disqualifier first, with every
deciding number pinned before it is measured: the fixture ladder F1-F4 and its
musical-content recipe, the deciding rung, the equivalence band, the tie-break
and its three operationally defined axes, and the oracles -- committed, with
their commit recorded, before any candidate renders against them.

Five review rounds shaped it, and most of what they caught was mine.

Measurement. Timed work must be completed GPU work, not submission, or a
candidate that returns after enqueueing looks fast. One common configuration
across all candidates: offscreen, 1920x1080, identical MSAA and format,
--release --locked, both adapters with the integrated one deciding. Freehand
latency runs from scheduled event injection to completion of the first frame
containing that event, backlog included; timing from renderer pickup would let
a candidate buffer the trace, render late, drop nothing, and report fast update
work while lagging visibly.

Damage. The first draft compared raw PrimitiveIndices, which are positions in
flat arrays -- so a pitch change that preserves primitive count moves geometry
while every vector stays identical, and an inserted accidental renumbers
indices so untouched systems look changed. It is now fingerprints over
dereferenced primitives, unowned included. And byte equality alone lets a full
redraw masquerade as per-system damage, so the oracle is paired with a reuse
assertion over the scene cache, at every engraving-valid rung, before the
ranking round rather than inside it.

Outcomes. A check that ran and got the wrong answer is FAIL, not NOT RUN;
conflating them let a broken implementation buy an escalation instead of
recording negative evidence. Capability and timing are separate cells.
Eligibility is a disqualifying set tracked apart from criterion cells, so a
user waiver actually reaches the tie-break instead of leaving a decision with
no effect. Both no-winner paths are total: disqualification, and ranking-loss
wipeout when nobody renders the deciding rung.

Maintenance inputs are reproducible rather than selectable: upstreams pinned
per candidate before any figure is fetched, composites taking the worse value
per axis, maintainer identity machine-observable, the issue snapshot archived,
and unanswered issues contributing infinity rather than their current age --
which would otherwise score twenty ignored issues opened yesterday as
excellent responsiveness.

What the spike is not: the app. No command registry, no editing, no document
layer, no goldens. Ruling D creates epiphany-editor-app fresh after this
verdict. And it does not reopen Ruling A -- a finding that no candidate can
implement the granted architecture escalates as an amendment request and does
not silently promote the SVG path back.

Docs only; no code, no crate, no gate movement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-28 17:26:23 -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 4043627898 Contract G2a round five: the sixth hand-maintained site, and locking Registered
The literal-site count said four and named three plus two generator bounds. The
omitted one is OperationKind::discriminant() -- the hand-written match that
Push 4a got wrong in the first place, and the one the contract elsewhere notes
the tag macro does not guard. All six are now enumerated, with the count after
row 29's derivation stated as five so the next tranche inherits an accurate
number rather than this one's.

s10's layout-stub half asserted kinds 30-33, which would have survived deleting
the required Registered append -- PAYLOAD_FREE excludes it by design and the
generator's doc promises every variant, so the gap between them is exactly
where the bug would live. It now asserts PAYLOAD_FREE union {Registered}, with
removal of Registered as the mutation. Future built-ins follow structurally
with no test change; the one payload-bearing variant that cannot stays locked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 15:47:17 -04:00
Levi Neuwirth 2b86e8033a Contract G2a round four: three generator sites, and a grep I read and ignored
All three confirmed. Two of them contain SetMetadata and appeared in the very
grep the touch table claimed to be derived from, so the derivation claim is
struck rather than repaired: the evidence was on screen and went unused, which
is worse than not having looked.

generators.rs:647 and layout_stub.rs:951 are both already stale by two
tranches -- neither emits TransposeInterval or CreateInstrument -- and both
feed corpora other suites treat as exhaustive, so a missing kind is untested
everywhere downstream while every suite stays green. layout_stub's doc comment
claims every variant while its bound omits two. Its fix is structural rather
than additive: derive the built-ins from PAYLOAD_FREE and append Registered,
which payload.rs:1991 already does. New test s10 asserts both generators emit
all four appended kinds.

textproj_kind.rs:597 is a fourth hand-maintained literal count.

The pattern is worth naming: operation_kind_tag_vocabulary! exists because
Push 4a added TransposeInterval to a hand-written match and nothing else, and
its own doc says four hand-maintained lists stayed green. The macro made the
compile-enforced half safe, and these three lists went stale at that same
append anyway, because nothing forces an rng bound or a literal count to move.
Hence: prefer deriving over extending wherever a list can be derived, and
report the sites that cannot be.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 15:43:18 -04:00
Levi Neuwirth 5eac1c9bc7 Contract G2a round three: Push-4a listing debt, and the ladder order
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
2026-07-28 15:29:32 -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 c39e58a755 Contract G2a round two: the schema-minor debt, and a test that could not fail
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
2026-07-28 14:48:10 -04:00
Levi Neuwirth f639919ee1 Editor T4-pre W3: the text run carries both its string and its ink
The last T4 prerequisite, and the one that had to be a ruling rather than a
packet: a canvas, an exporter, a hit test, and an accessibility tree must agree
about text, and what they agree on is decided by where the shaper runs relative
to the canonical boundary.

The census reframes the tranche. Three of the five categories the plan names --
lyrics, chord symbols, rehearsal marks -- carry no text in the model at all:
LyricLine holds only event references, ChordSymbol and Marker only an anchor.
They are blocked on a core-track schema major, not on this decision. What the
primitive does gate is the text the model already has: score metadata,
instrument and staff names, and text-line spanners. That is a smaller v1 than
the plan implied and a real one, and bidi and fallback are exercised through
synthetic fixtures that need no model work.

The ruling is a fourth resolved primitive carrying the source string and the
canonical shaped result together. The alternative that discards the string
renders a title as anonymous outlines and is unreadable to a screen reader; the
alternative that discards the shaped result lets two consumers draw the same
bytes differently, which contradicts the definition of canonical_bytes as the
rendering fingerprint. Both halves stay, and the apparent trade between
deterministic geometry and accessibility turns out not to exist.

Two drafts were wrong in opposite directions and the errors are recorded rather
than quietly fixed, because each came from asserting a constraint instead of
reading the requirement that governs it. Draft 1 held that shaping before the
canonical boundary poisons cross-implementation byte equality -- but layout
determinism is byte-equal only within one implementation at a fixed version;
across implementations it is reference-suite thresholds, and the spec says so in
both the determinism table and req:solver:cross-implementation-conformance.
Draft 1 had imported the score layer's guarantee into the layout layer, where
the spec deliberately weakens it. Revision 2 then over-corrected, banning host
fonts outright on the grounds that an OS font update breaks fixed-version
stability -- but that requirement defines identical inputs to include font
metrics referenced by version and content hash, so an updated font is a changed
input. The rule that survives is narrower than either: no ambient or unresolved
lookup, and a host face may participate only once resolved to an exact
content-hashed asset every consumer can obtain.

The identity is specified rather than gestured at, because bytes that do not
determine ink are worse than bytes that admit they don't. A face is pinned by a
hash over the font file, not its metrics -- GlyphCatalogIdentity's metrics_hash
covers bounding boxes, advances and anchors, which pins spacing and not shape --
together with face index, variation coordinates and synthetic weight/slant.
Segments carry font-internal glyph ids, source ranges, direction, script,
language and em size; glyph offsets have alignment already applied, so a
consumer places by origin alone; positions quantize on the same 1/1024 grid as
every other primitive. The cluster map indexes UTF-8 byte offsets with caret
stops at grapheme boundaries carrying bidi affinity, and the Unicode
segmentation version is always part of the identity -- otherwise two
implementations could agree on every pixel and still differ inside the
fingerprint, where no visual test would ever see it.

One consequence lands on the exporter: SVG cannot honour "no consumer reshapes"
with <text>, which carries characters and lets the viewer's shaper choose the
glyphs, so a ligature or positional form silently draws something the layout did
not resolve. Conformant text export emits explicit glyphs as paths through the
same face, reusing the mode render-svg already has for music.

The reservation is re-ordered to follow shaping rather than precede it -- with a
canonical shaper in the pipeline, reserved_box becomes a solver policy over
measured bounds, not an estimate of them. Paint-time re-spacing stays forbidden.

Two findings for the core track, named so their absence is a decision. Score
text authored through operations is not NFC-validated: the envelope's NFC-
checked string reader covers only directly encoded strings such as transaction
labels, while SetMetadataOp, CreateStaffOp, CreateInstrumentOp and the cross-
cutting values embed the core codec's bytes, which preserve non-NFC strings by
design. And the .tex amendment adding the primitive changes the layout
fingerprint but needs no bundle or wire schema-major move, following strokes and
curves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RSX4zSLgKvtiXaPjnMqLGz
2026-07-28 14:38:06 -04:00
Levi Neuwirth 4a4a766d4b Contract G2a review fixes: the normative surfaces, and a G1 debt
All four findings verified independently against the tree before fixing; all
four hold, and the first is worse than filed.

P1 (normative wire docs) is not a G2a omission but a G1 regression. 3b09595
touched operation_catalog.tex and text_projection.tex and no other .tex, so it
shipped four falsehoods: binary_format's payload-layout and tag tables stop at
kind 30 while 31 is assigned in code, and req:binfmt:kind-discriminants says
each row pins the payload's byte layout; binary_format:2432 still asserts there
is no CreateInstrument; core_spec:12186 still says Canvas.layout_defaults and
Instrument.range reach only the non-canonical snapshot; and the catalog gained
a CreateInstrument section with no version bump against its own convention.
G2a now repairs all four and adds its own two surfaces, with explicit version
and changelog dispositions for binary_format (0.11.0 -> 0.12.0), the catalog
(0.9.0 -> 0.10.0, covering G1 retroactively), and two narrow core_spec edits.

The core_spec:5114 amendment is pinned rather than left to a subagent. The
K8 paragraph has a load-bearing half the ruling does not touch -- root and
canvas remain structural givens no operation mints -- and only the
outside-the-operation-set clause is superseded.

P1 (source) adds ops/src/lib.rs re-exports and the textproj parse.rs literal
HEADER fixture, whose tracking test is a tripwire that fires by design.

P2: s8 and s9 gain the mutations they lacked, and s2's was fabricated --
set_metadata contains no comparison to reverse, it records and overwrites
unconditionally, so the mutation is now setter-local first-write-wins. s7's
framing was backwards: WorkingSnapshot is transaction rollback, so omitting a
chain retains a failed transaction's write rather than losing history, and the
wrong framing would have sent the test hunting the wrong symptom.

Records the G2b holdout: ScoreTuningContext's Codec deliberately drops
accidental_extensions while OperationSet::accept stores envelopes as objects,
so a naive full-value SetTuningContext diverges between a live session and the
same document reloaded -- and canonical_value! cannot see it, because its
check compares bytes and never the originating value.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-28 14:35:53 -04:00
Levi Neuwirth be40eb2d7d Contract G2a: the split, and the two setters that move no wire bound
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
2026-07-28 14:18:49 -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 4e30fe8475 Contract G1: one operation, and the two things that make from-empty correct
The ladder is ratified, so this contracts its first rung. CreateInstrument is
the single missing link between Score::empty and a note -- CreateStaff already
demands a live Instrument and nothing can create one -- so one kind satisfies
the ruling's acceptance criteria 1 and 3. Instrument holds no outbound entity
references, so the operation needs no referential preconditions at all, only
mint and idempotence.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

All fifteen line citations verified against the working tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-24 15:25:25 -04:00
Levi Neuwirth ec17d06e77 The identity cursor is ruled, and three records stop contradicting each other
RULING_GENESIS_PERSISTENCE.md §3 was the tranche's stated blocker. Ruled:
identity stays on Score and stays canonically encoded, byte-equality claims
confine to MaterializedState, and from-empty reduction derives next_counter
from the log.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ANALYSIS_GENESIS_PERSISTENCE.md is the field-by-field Score table Ruling B
blocker (i) requires before it can be resolved. It decides nothing; it
establishes what is true. Eight of Score's twenty fields have no operation
that can produce them — canvas.layout_defaults (the printed page geometry),
instruments, staff_groups, parts, tuning_context, spelling_precedence,
analysis_layers, views, plus identity — and decomposition_attachments can be
pruned but never authored. Each is reachable only through a base the reducer
is handed, and pruning is licensed to replace that base with a
MaterializedState carrying none of them.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QjsEnYhm1gPpf6ii2iFxFV
2026-07-24 14:02:31 -04:00
Levi Neuwirth ca0dcee10c Editor T3: contract for the note-entry caret
The input-method-agnostic entry seam: session-local caret, naturals-only
letters with nearest-octave inference (downward tie-break), the pencil's
make-room semantics, the x_at_position forward map, and the testable core
of MIDI entry — the device listener stays T4.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Closes P13-S6.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

**P13-S5, Ruling A -- full register.** `req:pitch:ji-vector-basis` says the
built-in JI spaces order primes ascending *starting with 2* and that
`components.len()` MUST equal the basis size; the catalog table called
`ji-5limit` "Two-dimensional (prime axes 3, 5)", `ji-7limit` three- and
`ji-11limit` four-dimensional -- each exactly one short, the table being
octave-reduced and the requirement full-register. The table moves, the
requirement does not: a `JiVector` is an absolute position, and without the
prime-2 exponent `ji-5limit` cannot distinguish C4 from C5. Each row now states
its basis explicitly so two readers cannot derive different ones.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Twelve grammar-gate tests, all mutation-verified.

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

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:42:21 -04:00
Levi Neuwirth 7b96c2d131 Text Projection 0.5.0: the projection is schema-directed
Found by starting the implementation: the grammar could not derive an ordinary
pitched note.

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

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

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

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

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

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 22:27:32 -04:00
Levi Neuwirth 2868f8c4b3 Text Projection 0.3.0: every production expanded, and one rule for values
0.1.0 left kind, action, policy, constraints and barrier derived-but-unwritten
and admitted it. All are now written. The grammar has no undefined nonterminal
(machine-checked) and 31 operation-kind productions in exact discriminant order,
cross-checked against envdecode.rs.

The one real decision was how embedded Chapter-5 values appear. An operation
payload carries an Event, a Pitch, a Region, a TimeSignature, and there were
three ways to write them:

  Forty hand-written productions would restate the entire Chapter-5 data model in
  a second normative document -- two normative listings of one struct, which is
  the exact drift P13-I1 was opened to close.

  Opaque canonical-value byte strings would be lossless and zero-drift, but a
  pitch would be unreadable without binary tooling, failing the core spec's own
  "format inspection and debugging" use case.

  One mechanical rule (req:textproj:value-projection, ratified): a struct is
  (<type-name> <field>...) with fields positional in the ratified declaration
  order; a newtype is transparent, exactly as in the binary form; a tagged union
  is (<variant> <field>...); an option is () or (some v); a sequence keeps the
  binary form's order. A rule cannot drift from what it reads.

Two leaf decisions follow from canonicality rather than taste. A rational is
(ratio n d), lowest terms, sign on the numerator. A CanonicalF64 is the byte
string of its eight canonical IEEE-754 bytes and never a decimal: decimal float
text is not canonically unique -- shortest-round-trip and 17-significant-digit
forms both round-trip, and -0.0 has two spellings -- so a decimal tempo would
break req:textproj:canonical-text at the first tempo mark.

Operation-kind names follow the Operation Catalog's sections (create-region,
create-staff), not OperationKindTag's (InsertRegion, InsertStaff), which renamed
three pairs for reasons internal to the tag space. The projection follows the
semantics.

One deferral, stated as such rather than left to inference:
affected_object_kinds and edit_barriers have ratified structured shapes AND
canonical byte encodings, and the bundle stores them opaquely. At 0.3.0 the
projection does the same, on the principle that it interprets nothing the bundle
does not. A later revision may project them structurally; their canonical bytes
are unchanged by that, so it changes the text and not the document.

Also corrected: the extension line now writes its fields in the ratified
declaration order, which had preserved_chunk_roots before affected_object_kinds
and edit_barriers.

Gate: clippy 0, 31 targets / 1031 passed / 0 failed, conformance 8/8, zero golden
churn; core_spec, binary_format and text_projection all build with no undefined
references. Grammar closure machine-checked: no undefined nonterminal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 21:55:49 -04:00
Levi Neuwirth ee18b71aca Text Projection 0.2.0: the gate was lossy, and the cause was one missing rule
A review found the 0.1.0 companion lossy for documents that are valid today. Its
claim to preserve the manifest's canonical roots was false three ways:

  A canonical blob had no representation at all. blob_roots referenced by
  canonical operations or reduced state ARE canonical roots, and the document
  structure had no blob line. An embedded image, font or recording would vanish
  from a projection silently -- the operations referencing it still there,
  pointing at a blob id the text no longer contained.

  An ExtensionDeclaration lost its semantic version and its affected_object_kinds
  outright, and left its preserved_chunk_roots undefined.

  ProfileId::Custom(ProfileRegistryId) was unrepresentable: a symbol was required
  where sixteen registry bytes are carried.

All three share one cause I had not named. A ChunkRef and a BlobRef are PHYSICAL
references -- offset, compressed length, compression -- which the projection may
not preserve; and they carry DERIVABLE identities -- ChunkId, ContentHash, BlobId
-- which it may not duplicate. Having no rule for either, I dropped the
references and took their contents with them.

req:textproj:derive-or-carry states it, and it is the same rule
req:textproj:reduced-state-derived already applied one level up: carry exactly
what the document does not determine, and nothing it does. Physical attributes
never appear; derivable identities never appear; content and semantic attributes
always do. The one non-derivable identity in schema major 0 is SnapshotId, which
the Binary Format companion pins as opaque and forbids readers to derive -- an
exception for a stated reason rather than an oversight. The grammar now visibly
contains no offset, no length, no compression, no chunk id, no hash, no blob id.

So: req:textproj:canonical-blobs (canonical blobs projected, non-canonical ones
not), req:textproj:profile-id ((custom #x...)), req:textproj:extension-declaration
(every field; chunks as kind + schema + payload, never as a ChunkRef -- the
projection has no file to point into), and req:textproj:base-snapshot-inline
extended to say what the inlined payload IS and that the root ChunkRef and the
SnapshotRef hash are re-derived from hash(Snapshot, schema, payload), never read.

The gap started upstream. core_spec's own list of what the projection preserves
omitted canonical blobs while classifying blob_roots as canonical roots -- an
inconsistency inside one document. Corrected there, along with withdrawing the
permission to reference a base snapshot "externally", which the inline
ratification had already made untenable.

Also: the generated PDF metadata and page header still said Operation Catalog,
inherited from the copied preamble.

The four 0.1.0 ratifications stand unchanged. Implementation stays deferred: a
gate that is lossy is not a gate.

Gate: clippy 0, 31 targets / 1031 passed / 0 failed, conformance 8/8, zero golden
churn; core_spec, binary_format, operation_catalog and text_projection all build
with no undefined references.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 21:34:36 -04:00
Levi Neuwirth 0ba9bb2dd8 Text Projection design gate: the companion the core spec delegates to
core_spec Chapter 8 declares the text projection normative and leaves the form
unwritten; binary_format excludes it as "the Text Projection companion's". That
companion did not exist. spec/text_projection.tex v0.1.0 supplies it. No
implementation -- this is the gate.

It was blocked on P5. The projection's normative requirement is bidirectionality
WITH THE BINARY FORM, which needs bytes -> OperationEnvelope, and that decoder
did not exist until 3baf8d0.

Four ratified calls:

Reduced state is preserved by DETERMINING it, never by a second literal copy. It
is a deterministic function of the operation set and the canonical base; a text
carrying both would hold two sources of truth for one fact and nothing could stop
them disagreeing. core_spec's "all canonical reduced state" now carries that
reading inline rather than being silently reinterpreted.

A canonical base snapshot is INLINED as one opaque byte string. This is the call
with teeth. A base exists precisely so prior operations need not be retained, and
where they are pruned the base is derivable from nothing else -- so a
reference-only projection of a compacted document would be LOSSY, and the text
would not determine the document it claims to project. core_spec permits "encoded
compactly or referenced externally"; inline is the choice that keeps archival
honest, and core_spec now says why.

Lowercase hex everywhere: one rule, no alphabet or padding to canonicalize,
greppable. Base64 would buy a quarter of the bytes of the one body nobody reads,
at the price of a second encoding and a rule for which applies where.

One envelope per line: the stated use case is that merge conflicts surface at the
envelope level, and one line per envelope makes a three-way merge conflict
exactly an envelope conflict -- never a conflict inside one, yielding an operation
neither side wrote. It also removes all indentation, so canonicality has nothing
to hide in. Readability is a pretty-printer's job; what a pretty-printer must not
do is write its output back and call it a projection.

Strict parsing is stated in the terms P2-P5 taught: normalizing non-canonical
text IS accepting it. The rationale names both hazards this repo hit in binary --
a re-encode guard is blind to order-preserving sequences, and a guard on an outer
value can mask a lenient inner codec -- and prescribes the same total defence.
Conformance requires both directions: a projector alone cannot be checked.

One gap is stated in the document rather than left to be discovered: the atom
productions and line shapes are normative, while kind/action/policy/constraints/
barrier are derived from the Operation Catalog and the wire table rather than
spelled out. That is the difference between a design gate and a finished
companion.

Gate: clippy 0, 31 targets / 1031 passed / 0 failed, conformance 8/8, zero golden
churn; core_spec, binary_format and the new companion all build with no undefined
references.

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