21 KiB
Contract — P13-S18: the invariant-20 outcome matrix
Status: DRAFT.
Rung type: diagnostic and bookkeeping. No behaviour change. No graph that violates invariant 20 today may stop violating it, and no graph that passes today may start violating it. If the work suggests otherwise, stop (§1 pin 6).
§0. What was verified before drafting
Every claim below was read out of the working tree at cc49533, not recalled,
and re-confirmed unaffected at f33673d (see §4).
-
check_measure_meter_consistencyhas nine non-success paths, not the five an earlier scoping note claimed. Enumerated with line numbers in pin 1. -
Only three are abstentions. The other six are one inapplicability, two delegations, two vacuities, and one separately-filed deferral (pin 2).
-
Invariant 10 really does own the two delegated paths.
invariants.rs:1220ff checks that time-signature references resolve "at every level aMeterChangecan appear" and its loops cover per-measure (si.measures, emitting"measure {:?} time signature {:?} is not declared"), instance-local grids (si.local_metric_grid), the region-default grid, and the metric time model's own meter sequence. -
P13-S18's "
Measureend anchor" claim is wrong only in its "any", and two successive scoping notes each overcorrected it. The first called the claim simply false; the second called it merely misattributed. The precise position has three parts, all read out ofmeasure20_comparable_order:- Same id, same position —
End↔Endis comparable when its offsets compare. c2 requiresia == ib && pa == pband then delegates tomeasure20_offset_order, which returnsNoneforMusical↔WallClock(:2427–:2428). So "anyMeasureend anchor" is false as written, but the counter-example is conditional and must be stated that way. - Distinct ids —
Endanchors remain incomparable, because c3 returnsNoneunless*pa == MeasurePosition::Startwith both offsetsZero. A vector index orders measure reference points, not arbitrary points near them. Distinct-idEndanchors therefore genuinely do reach A4, B4 and B5, and are a real residue shape. - The resolver citation describes the missing duration machinery, not
invariant 20's execution path.
resolve_anchor'sMeasurearm (invariants.rs:503–:516) returnsNonefor anyposition != MeasurePosition::Startbecause a measure's length "needs the deferred decomposition/tempo machinery" — a true statement about why the duration is unavailable. Invariant 20 never calls it: zero occurrences in:2636–:2725. The citation explains the underlying gap; it does not describe how invariant 20 reaches its abstentions.
The
invariants.rs:400line number is separately stale (§0.7). - Same id, same position —
-
P11-C5 is not P13-S18's gate.
PASS11_WORKLIST.md:159defines it as the "nearest surviving anchor" stand-in — a re-anchoring proximity metric that "resolves when the graph-mutation phase tracks resolved positions". The G3b contract cites it correctly but narrowly, for the two-distinct-Events case, viaPositionOutsideRegion's Reserved note (effect.rs:139–:142). The capability P13-S18 waits on — placing on a common timeline any pair c1–c5 cannot order, and any pair it orders without yielding a usable musical delta — is strictly broader and is owned by no filed candidate. Pin 10 is the authoritative scoping; this item must not restate it more narrowly. -
Step 0 is per measure reference.
measure20_governing_by_anchoris called once per measure with that measure's ownstart, andmeasure20_comparable_orderis a property of the pair. An incomparable meter change therefore disables agreement for every measure whose start is incomparable to it — not for every measure in the instance, when the instance's measures carry heterogeneous anchor shapes. An earlier scoping note said "every measure in that instance"; that was wrong and pin 7 locks the correction behaviourally rather than in prose. -
The G3b contract's
invariants.rs:400ff citation (:206) has drifted. The resolver it describes now begins atinvariants.rs:466, with theMeasurearm at:503–:516. The paragraph's own narrower claim — that cross-boundary comparison stays unverifiable without the boundary's duration — remains correct and is not touched (pin 9).
§1. Pins
Pin 1 — the nine paths, and their sites. These are exhaustive over
check_measure_meter_consistency (invariants.rs:2636). Any tenth found during
execution is a finding, reported before proceeding.
| Id | Clause | Path | Site |
|---|---|---|---|
| A1 | agreement | m.time_signature is None |
:2661 |
| A2 | agreement | declared signature does not resolve | :2662 |
| A3 | agreement | Governing20::None |
:2675 |
| A4 | agreement | Governing20::Indeterminate |
:2677 |
| B1 | boundary | first measure (i == 0) |
:2683 |
| B2 | boundary | governing signature does not resolve | :2689–:2693 |
| B3 | boundary | Governing20::None |
:2717 |
| B4 | boundary | Governing20::Indeterminate |
:2719 |
| B5 | boundary | musical delta not computable | :2713 |
Pin 2 — the classification, ratified. Six labels. Every path takes exactly one.
| Id | Class | Why |
|---|---|---|
| A1 | inapplicable | no declared signature can disagree with anything; pin 9b makes None avoid only this clause, never the boundary clause |
| A2 | delegated | invariant 10, per-measure arm (§0.3) |
| A3 | vacuous | no governing signature exists to disagree with (pin 6c case 1) |
| A4 | genuine abstention | the relation cannot place a candidate |
| B1 | P13-S19 | the pickup/anacrusis deferral, already filed |
| B2 | delegated | invariant 10, grid-level arms (§0.3) |
| B3 | vacuous | pin 6c case 1 |
| B4 | genuine abstention | as A4 |
| B5 | genuine abstention | order without distance |
Exactly three genuine abstentions: A4, B4, B5. This is the whole point of the rung — P13-S18 currently presents all nine as one undifferentiated residue, which both overstates the gap and hides which part of it is real.
Pin 3 — delegation must be verified, not asserted. A2 and B2 are classed
delegated only if some other invariant actually reports the condition. The
matrix must show, for each, that invariant 10 emits a violation on the same
graph — and M7/M8 must show that removing invariant 10's arm makes the condition
go unreported by the whole suite, not merely by invariant 20. A delegation
nobody discharges is an abstention wearing a better name.
Pin 4 — the outcome matrix. Rows are representative anchor shapes, columns are the two clauses, each cell names the path id that fired and its class. Minimum shapes:
| Shape | Description |
|---|---|
| S1 | WallClock start, WallClock-anchored meter changes |
| S2 | WallClock start, Region-anchored meter changes |
| S3 | Region same id, same edge, Musical offsets — fully decidable |
| S4 | Measure same id, pos: End on both sides, Musical offsets (c2) |
| S5 | Measure distinct ids, Start, Zero (c3) |
| S6 | Event same id, live event, Musical offsets (c1) |
| S7 | Event distinct ids, otherwise identical to S6 |
| S8 | matching referent, differing pos/edge selector |
| S9 | one instance, heterogeneous measure anchors (pin 7) |
S6 and S7 are a positive/negative pair and neither may be dropped.
Measure.start and MeterChange.anchor are unrestricted TimeAnchors, so
Event-anchored measures are valid rather than hypothetical. S6 proves such a
measure genuinely reaches c1. S7 changes only the referent identity and
isolates the distinct-Event fall-through — which is precisely the case the
retained P11-C5 citation at CONTRACT_GENESIS_G3B_MEASURE.md:223 exists for.
Both use live events and Musical offsets.
S4 is the row that falsifies "any End", and it must do so by observation.
It is deliberately an End↔End pair on the same measure id with Musical
offsets — c2's exact shape — and its meter changes carry the same anchor form,
so both clauses reach a decision. It is therefore D/D and, per the rule
below, deliberately wrong. S5 supplies the contrast on distinct ids. A contract
that only stated "same-id End is fine" would be repeating the mistake this
pin exists to correct.
Every cell holds either a path id (A1–B5) or D. A fully comparable shape
takes none of the nine paths — it decides — and a cell left blank because
"nothing fired" is indistinguishable from a cell nobody looked at. Every D
fixture must be deliberately wrong, so that the emitted violation is what
proves the clause decided. S3 and S6 are D in both columns; an S6 that emits
nothing has demonstrated nothing.
Every cell must be observed, never derived. A cell filled in by reading the match arms and reasoning about which one wins is unsigned.
Pin 5 — the WallClock finding, and the split an earlier draft got wrong.
valuegen::measure (ops/src/valuegen.rs:447) anchors starts to
TimeAnchor::WallClock. An earlier draft of this contract said that shape makes
both clauses abstain. It does not, and the difference is the meter changes'
anchors, not the measures':
- S1 —
WallClockmeasures,WallClockmeter changes. The anchors are mutually comparable under c5, so step 0 passes and a unique maximum is found: agreement decides. Butmeasure20_musical_deltanever returns aWallClockdelta (:2527,AnchorOffset::WallClock(_) => None), so the boundary clause takes B5. - S2 —
WallClockmeasures,Regionmeter changes.comparable_orderfalls through to_ => None, so step 0 returnsIndeterminate: agreement takes A4 and the boundary clause takes B4 — not B5, because theGoverning20match short-circuits before the delta is ever computed.
A WallClock start does not by itself disable both clauses. The report must
state the split, must not claim WallClock is the dominant shape in real
scores (measure starts are author-supplied), and may claim only that it is the
shape this repository's fixture generator emits.
Pin 6 — the hard stop. No widening of measure20_comparable_order or
measure20_musical_delta. No new violation. No change to which graphs violate
invariant 20. The G3b contract holds that defining a new comparability relation
"is a specification question this rung has no authority over", and that is
unchanged. If the classification work suggests a behaviour change — including
"A3 should really be a violation" or "B2 should be reported here rather than by
10" — stop, report, and let it be split into a separately ratified semantic
rung. Do not implement it here, and do not soften a class label to avoid
raising it.
Pin 7 — step-0 scope, locked behaviourally. S9 must contain one instance whose measures carry heterogeneous anchor shapes, such that one measure's agreement clause abstains via A4 while another measure's agreement clause in the same instance reaches a decision. Prose asserting this is not enough; the row is what forbids the "one incomparable change disables the whole instance" overstatement from reappearing.
Pin 8 — the P13-S18 correction. Five defects, all of them mine:
- three abstention shapes → nine paths, classified;
- correct, not delete, the "
Measureend anchor" claim, to all three parts of §0.4: same-idEnd↔Endis comparable under c2 when its offsets compare —measure20_offset_orderrejectsMusical↔WallClock— so "any" is false but conditionally so; distinct-idEndanchors are incomparable and do reach A4/B4/B5; and the resolver citation names the missing duration machinery rather than invariant 20's execution path; - re-gate: not P11-C5, but the broader capability filed as P13-S23 (pin 10);
- name A2/B2 as delegated to invariant 10 with its line, so the entry stops counting them as gap;
- state the S1 finding. The entry stays open — A4, B4 and B5 remain real — but open at its true size.
Pin 9 — the G3b contract corrections, two of them and both narrow.
:347says the residue's closure needs "P11-C5 resolved positions". That is the overbroad claim. Correct it to name P13-S23 as the owner of the general case.:206citesinvariants.rs:400ff for the prototype anchor resolver. The resolver now begins at:466with itsMeasurearm at:503–:516. Correct the line number only. The paragraph's claim — that cross-boundary comparison stays unverifiable without the boundary's own duration — is correct, is narrower than P13-S18's use of it, and must survive verbatim.
:223's citation is correct and stays. It cites P11-C5 for the
two-distinct-Events case specifically, which is genuinely what
PositionOutsideRegion's Reserved note (effect.rs:139–:142) covers.
The contract is RATIFIED and its pins are not reopened; both edits are
citation repairs, and neither changes a ratified claim.
Pin 10 — P13-S23. File the unowned capability: placing anchor pairs on a common timeline and measuring musical distance along it, wherever c1–c5 do not already yield both. Two disjoint deficiencies, and the candidate owns both:
- No ordering. The pair is not comparable at all. The failure may be in the
referent (distinct
Eventids, distinctMeasureids outside c3'sStart+Zerorestriction), the variant or selector (Event↔Measure,Measure↔Region, differingpos/edge), or the clock (Musical↔WallClock, including insidemeasure20_offset_order). This is what A4 and B4 are made of. - Ordering without a usable delta. The pair is comparable and still
yields no musical distance: c3 supplies a vector index, which is an order
and never a distance; c5 compares two
WallClocks, andmeasure20_musical_deltanever returns aWallClockdelta (:2527). This is what B5 is made of.
Scoping this as "not directly comparable under c1–c5" would have excluded B5
entirely — S5 is c3-comparable and S1 is c5-comparable, and both reach B5 — so
the candidate would have disowned a third of the residue it is filed to own. An
earlier draft also said "anchors of differing shapes", which additionally
excluded the same-variant cases (distinct Events, distinct Measures).
Explicitly broader than P11-C5, with the distinction stated: P11-C5 is a
re-anchoring proximity metric that waits on resolved positions; P13-S23 is the
timeline itself. Name its dependents: invariant 20's A4/B4/B5, and
PositionOutsideRegion's Reserved status. Filed open; no code owed.
§2. Touch table
| # | File | Change |
|---|---|---|
| 1 | crates/epiphany-core/src/invariants.rs |
tests only, plus a doc-comment classification on check_measure_meter_consistency naming each of the nine paths and its class. The doc comment is not normative and changes no behaviour. No production logic edit of any kind. |
| 2 | spec/PASS13_CANDIDATES.md |
P13-S18 corrected per pin 8; P13-S23 filed per pin 10 |
| 3 | spec/CONTRACT_GENESIS_G3B_MEASURE.md |
:347 (overbroad P11-C5 claim) and :206 (stale resolver line number), per pin 9. :223 untouched |
| 4 | this contract | DRAFT → RATIFIED |
No .tex change and no PDF. The classification describes existing
behaviour; it makes no normative claim. If the classification turns out to
contradict core_spec.tex's invariant-20 text, that is a finding — stop and
report it (pin 6), because a spec correction is a semantic rung.
No DECISIONS.md entry. Consistent with the P13-S15 ruling: test coverage
and ledger bookkeeping, not a semantic ruling.
§3. Mutation plan
Every mutation is a specific edit to a specific site, each observed red and
restored by hand-editing back. Never git checkout, never git stash.
| M | Edit | Row it signs |
|---|---|---|
| M1 | make A4's graph comparable (swap the incomparable meter change's anchor to the measure's own shape) | A4 is an abstention, not a pass: the agreement violation appears |
| M2 | as M1 for B4 | B4 is an abstention |
| M3 | swap S5's two measure starts to Region same-id Musical offsets |
B5 is an abstention: the delta becomes computable and the boundary violation appears |
| M4 | give A1's measure a resolving time_signature that disagrees |
A1 is inapplicable, not concealing: the violation appears |
| M5 | make S2's meter-change anchors WallClock, matching its measures |
pin 5's split: S2's cell pair moves A4/B4 → D/B5 — agreement stops abstaining and decides, and the boundary clause moves from indeterminate selection to incomputable delta. Writing this as "A4/B4 → B5" is wrong: A4 is an agreement path and cannot appear in the boundary column. S2 is the target, not S1 — S1's agreement clause already decides, so no mutation of S1 could show it begin to |
| M6 | in S9, give the second measure the first's incomparable anchor shape | pin 7: its agreement clause stops deciding — and only then |
| M7 | delete invariant 10's per-measure time-signature arm (:1250ff) |
A2 is delegated: the condition becomes unreported by the whole suite |
| M8 | delete invariant 10's instance-local-grid arm | B2 is delegated, same standard |
| M9 | make B1's measure a non-first measure | B1 is the P13-S19 deferral, not a silent pass |
| M10 | remove B3's emptying filter so a governing signature exists and disagrees | B3 is vacuity, not concealment |
Ten mutations. Report the observed count; any deviation is a finding.
Anti-traps, all earned in this session. A mutation that does not compile signs nothing. A mutation in an operation that runs before the asserted state cannot reach it. A guard written against an index that structurally cannot hold the referent is born green. Transaction-block members reduce atomically at the first member's canonical position. Envelope counter gaps make operations permanently pending, which makes fixtures silently vacuous. And the one this rung is most exposed to: a matrix cell that agrees with the prediction is the easiest place in the world to stop looking. Each cell must fail loudly if the path it names is not the path taken — assert the outcome, and construct the graph so no other path could have produced it.
§4. Gate
cargo test --workspace— baseline 1541 / 0, confirmed atf33673d(the commits betweencc49533and it arespikes/-only, a separate workspace, so the root count is unmoved); delta explained.cargo clippy --workspace --all-targets— zero warnings.cargo fmt -p epiphany-core -- --check— clean. Nevercargo fmt --all; it crosses into thespikes/workspace through path dependencies.- All 10 mutations observed red and hand-restored.
- A diff check that
check_measure_meter_consistency's executable body is byte-identical tocc49533— the doc comment may grow; not one line of logic may move. This is the rung's central claim and must be mechanically demonstrated, not asserted. git diff --cached --checkclean; nothing fromspikes/or.claude/staged.
§5. Whitespace and staging
- Stage the touch-table files explicitly. Never
git add -A. git diff --cached --check— catches staged and formerly-untracked files.- Commit.
git diff --check <parent>..HEAD -- crates/ spec/— path-scoped.
A concurrent session commits to this repository. Re-check HEAD before
committing, and never run git reset, git restore --staged, git checkout,
or git stash against the shared index.
§6. Boundary — unchanged and absolute
MUST NOT be read, written, or staged: spec/PLAN_EDITOR_APP.md,
spec/CONTRACT_EDITOR_*.md, spec/ANALYSIS_GENESIS_PERSISTENCE.md,
spec/ANALYSIS_TEXT_RUN_PRIMITIVES.md, spec/DRAFT_T4_FIXTURE_RECIPE.md,
crates/epiphany-editor-gui/goldens/*.png, crates/epiphany-render-svg/**,
crates/epiphany-glyphs/**,
crates/epiphany-testkit/benches/editor_pipeline.rs, the entire spikes/
tree, the root Cargo.toml change, .claude/worktrees/.
§7. Report requirements
- The full matrix — nine shapes × two clauses, 18 cells — every cell
observed, each holding a path id or
D, with the test name that observed it. - All 10 mutations with their observed failing test names.
- Explicit confirmation that
check_measure_meter_consistency's logic is byte-identical, with the command that showed it. - Explicit confirmation that no new violation is emitted anywhere, and that the workspace count moved only by the tests added.
- For A2 and B2: the invariant-10 violation text observed, and the M7/M8 result showing the condition otherwise goes unreported.
- Anything the contract did not anticipate — a tenth path, a cell that contradicts pin 2, or anything suggesting a behaviour change. Report it and stop; do not implement it.