Skip to content

SWG-CORE-01: the three canonical index fields are u64 — H3 closed - #188

Merged
PhysShell merged 9 commits into
mainfrom
claude/swang-core01-fixed-width-index
Aug 19, 2026
Merged

PhysShell merged 9 commits into
mainfrom
claude/swang-core01-fixed-width-index

Conversation

@PhysShell

@PhysShell PhysShell commented Aug 18, 2026 •

Copy link
Copy Markdown
Owner

Closes H3. MasterBar.index, ImportWarning::TrackNameInvalidUtf8.track_index,
and ImportWarning::TempoApproximated.bar_index go from usize to u64, so a
score written on a 64-bit host means the same thing on a 32-bit one. Spec §1.2
forbids platform-sized integers in hashed or serialized state and Score
derives Hash; until now the portable exact format was not portable.

Please do not squash. Nine commits in three groups:

Commit What
5d618fe A — characterization GREEN, tests-only
65b35a6 B — fixed-width contract RED, tests-only
c5800f7 C — the migration
277364a falsification found a hole → guard
db5f6ac second hole from the same pass → witness
8aeacd3 D — docs closure
f02925e review RED — the acceptance witness misses three things
0e8fa6b review GREEN — witness repaired
5705ae8 review docs — count and description corrected

u64, and why not u32

Semantic, not defensive. The fields are public, values above u32::MAX were
already inhabited on a 64-bit host, and MasterBar.index is a stored exact
fact that need not agree with its vector position (H4) — not a bounded ordinal
that could be argued into 32 bits. u32 would not merely have removed a
platform dependence; it would have shrunk a canonical domain that already has
values in it.

Step A's >u32::MAX witnesses are what make that a check rather than a claim.
They sat behind cfg(target_pointer_width = "64") while the fields were
usize — the gate was the defect — and the migration removed the gate's
reason to exist, so the same assertions now run on every target.

index_from_ordinal

One new public function, the only sanctioned way to turn a vector position
into a canonical index. Total and lossless; deliberately no inverse, because
the inverse is not total. Twenty call sites, all constructors that already
numbered bars from an enumerate().

Three types pulled along

Not scope creep — the alternative at each is a truncating u64 → usize on a
32-bit target, which is the defect being fixed: dump::NormBar.index (a
Serialize type), SemanticPathSegment::MasterBar.index and ::Bar.index,
and ui_core::analysis::Section.bar_start / .bar_end. The ordinal beside
each path annotation stays usize; the two now sit in the same struct with
visibly different widths, which is the correct shape.

What did not change

No operational usize: Vec positions, lengths, slice indices, and MIDI's
MAX_MASTER_BARS resource bound are untouched. nearest_micros stays u32
and a test says so — "widen the integers near the ones being widened" is the
cheapest way for a mechanical change to grow a scope nobody gave it.

cli::bar_at_tick gains a comment rather than a fix: its two branches return
a stored index and a last ordinal respectively, a pre-existing conflation this
task only makes same-width, not correct. Display-only.

Two holes the falsification pass found

Ten of twelve mutations were caught immediately. Two were not, and both were
the same shape — a narrowing hidden at a seam no test walked:

  • index_from_ordinal could truncate through u32 and the whole suite
    stayed green. Reaching the truncation through a real Vec needs four
    billion elements; calling the conversion directly reaches it, because its
    input is a usize rather than a collection.
  • LossReport::add could clamp a payload index and the whole suite
    stayed green. Every test built an ImportWarning and read it straight back;
    none went through the one function every importer calls.

277364a and db5f6ac close them, each verified to fail under exactly the
mutation that motivated it.

The review round

An independent review then found the acceptance witness weaker than the
criterion it had been recorded against. Two defects, neither in the migration:

  • it scanned core/src/score.rs only, while the tree also reaches
    event.rs (TimeSignature, Tuning, NoteMarks, NotePosition,
    FretboardPosition, TechniqueEvidence, ConfidenceBps, Pitch,
    Velocity, Ticks) and slice.rs (TickRange);
  • its scanner could not read tuple forms at all — Other(String) was skipped
    for want of a :, and pub struct Pitch(pub u8); has no braced body for
    the header match to find. Its own doc comment claimed coverage "including
    inside an enum payload". The synthetic falsification used a named payload
    and so did not notice: a witness against vacuous witnesses that was itself
    a little vacuous.

f02925e reds all three, 0e8fa6b repairs them. The tree had no usize left
either way — what was missing was the ability to show it.

The module list is written out rather than discovered. Discovering it means
resolving use paths and type aliases, a half-compiler to check three files;
the cost is that the list can rot, so the coverage test asserts each listed
module contributes.

Falsification, in full

Seventeen mutations, each caught by the test that names it:

MasterBar.index / track_index / bar_index back to usize -> library will not build
all three narrowed to u32                               -> library will not build
projection derives the index                            -> projection characterization
index_from_ordinal truncates via u32                    -> NOT CAUGHT -> 277364a
writer clamps the stored index                          -> above_u32_max writer test
writer renumbers bars from the position                 -> golden + small-index test
LossReport::add clamps a payload index                  -> NOT CAUGHT -> db5f6ac
exact diff drops the stored-index annotation            -> six existing diff tests
slice keeps the source index                            -> slice_extract
add() drops an out-of-range payload                     -> importer-path witness
usize in a braced struct in event.rs                    -> no_platform_sized_integer
usize in a tuple enum payload in score.rs               -> no_platform_sized_integer
usize in a tuple struct in slice.rs                     -> no_platform_sized_integer
source list narrowed back to score.rs                   -> scans_every_module
a listed source that declares nothing                   -> scans_every_module

One caveat on method, since it bit three times: a batch harness reported "not
caught" when the truth was "did not compile" or "did not apply" —
grep '^error' matches cargo's own "error: test failed", and two probes broke
the crate by detaching a #[derive(…)] block rather than by anything to do
with the witness. Every result above is from a run with a diff guard on the
mutated file and a compile check that distinguishes error[E…] from a failing
assertion.

Verification

1344 tests green across core, swang, pattern, cli, and ui-core. Clippy
-D warnings and cargo fmt --check clean workspace-wide, cockpit, preview
and plugin included — type-checked with --all-targets against a stub
pkg-config entry, since libasound is absent in this environment. Rustdoc
produces the same 15 warnings as the base d7012f5; none are new.

fuzz/ pins nightly and could not be built locally. Its two targets that
construct a MasterBar are edited identically to sites that do compile, and
CI is the real gate for them.

🤖 Generated with Claude Code

https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE


Generated by Claude Code

claude added 9 commits August 18, 2026 16:54
…ex fields

Characterization, tests-only, green before the migration. The backlog exempts
characterization from the red phase; these must pass now and keep passing
after, and must not be edited to make the migration pass.

Thirteen tests across two crates pinning what the fields do today:

- a stored `MasterBar.index` is not derived from its vector position (H4),
  and the exact diff reports a change to it as exactly one difference;
- the normalized projection copies the stored index and does not renumber;
- warning payload indices are payload — a `track_index` of 7 in a score with
  no tracks stays 7, and nothing resolves it against the tree;
- the exact writer spells the stored index in full, with a byte-golden.

The load-bearing ones are the `>u32::MAX` witnesses. They are what turns the
width argument from prose into a check: a stored index above `u32::MAX` is
**already inhabited** on a 64-bit host, survives the exact diff whole,
survives the projection, and is written whole. So `u32` would not merely
remove a platform dependence — it would shrink a canonical domain that
already has values in it, and `MasterBar.index` is an exact fact of its own
rather than a bounded ordinal.

Those witnesses sit behind `cfg(target_pointer_width = "64")`, and the `cfg`
is the defect itself: the same score cannot be built on a 32-bit target while
the field is `usize`. That is precisely the portability claim H3 calls false.
Recording it before the migration means the change can be shown to have moved
the field's width without moving the writer's bytes for any value that
already fit.

Refs SWG-CORE-01

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
Tests-only. No `src/` change: the whole point is that the contract cannot be
satisfied by the current declarations.

## Two kinds of red, deliberately

`core/tests/canonical_index_contract.rs` is red by **failing to compile**,
which is the correct failure mode for a type contract. `usize` and `u64` are
distinct types in Rust even where they are the same size, so `require_u64` on
each of the three fields cannot pass on any target today and cannot be
accidentally satisfied on a 64-bit host tomorrow. Six errors, each naming a
field the migration must move.

It is a separate file from step A on purpose. A red file that also held the
green characterization would make "the characterization was green before and
after" unverifiable at exactly the moment it matters — and `canonical_index_width`
does still compile and still passes 9/9 in this commit.

The contract also states, twice and without a `cfg`, that a value above
`u32::MAX` is representable **on every target**. That absence of a `cfg` is
the deliverable: step A could only assert the same value behind
`target_pointer_width = "64"`, which is precisely the portability claim H3
called false.

One negative clause: `nearest_micros` is not an index and stays `u32`.
Written down because the migration passes right by it, and "widen the
integers near the ones being widened" is the cheapest way for a mechanical
change to grow a scope nobody gave it.

## The backlog's "no `usize` returns" witness

Extends the existing `exact_text_census` rather than adding a second scanner.
`declared_field_types` discovers every `pub struct` / `pub enum` in
`core/src/score.rs` instead of taking a hand-written list, so a canonical type
added later is covered without anyone remembering to add it — and it reads
enum payload lines, which no struct-field scan reaches.

Three tests: no field in the canonical tree declares `usize`; the three
migrated fields are specifically `u64` (the general witness would stay green
if one were widened to `u128`); and the scanner itself is falsified against a
synthetic source, because a scanner that silently found nothing would make
both witnesses vacuously green — the failure this suite has already hit twice.
`mentions_type` matches whole tokens, so `MyUsizeAlias` is not a `usize`.

Refs SWG-CORE-01

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
… u64

Closes H3. `MasterBar.index`, `ImportWarning::TrackNameInvalidUtf8.track_index`,
and `ImportWarning::TempoApproximated.bar_index` go from `usize` to `u64`, so a
score written on a 64-bit host means the same thing on a 32-bit one. Spec §1.2
forbids platform-sized integers in hashed or serialized state and `Score`
derives `Hash`; until now the portable exact format was not portable.

**u64, not u32**, and the reason is semantic rather than defensive. The fields
are public, values above `u32::MAX` are already inhabited on a 64-bit host, and
`MasterBar.index` is a stored exact fact that need not agree with its vector
position (H4). `u32` would not merely have removed a platform dependence — it
would have shrunk a canonical domain that already has values in it. Step A's
witnesses are what make that a check rather than a claim.

## `index_from_ordinal`

One new public function in `core::score`, and the only sanctioned way to turn a
vector position into a canonical index. Importers legitimately derive the
second from the first; the point is that every such crossing is now named and
greppable rather than implicit. It is total and lossless — `usize` is at most
64 bits on every target Rust supports — and there is deliberately no inverse,
because the inverse is not total and must not be written by accident. Twenty
call sites, all of them constructors that were already numbering bars from an
`enumerate()`.

## Three types the migration had to pull along

Not scope creep — the alternative at each is a truncating `u64` → `usize`
conversion on a 32-bit target, which is the defect being fixed:

- `dump::NormBar.index` copies the stored index into a `Serialize` type;
- `SemanticPathSegment::MasterBar.index` and `::Bar.index` annotate diff paths
  with the stored index. The `ordinal` beside each is an operational position
  and stays `usize` — the two sit in the same struct and now have visibly
  different widths, which is the correct shape;
- `ui_core::analysis::Section.bar_start` / `.bar_end` are copied straight from
  `MasterBar.index`; `bar_count()` follows them to `u64`.

## What did not change

No operational `usize` was touched: `Vec` positions, lengths, and slice
indices are the same as before, including MIDI's `MAX_MASTER_BARS` resource
bound, which is an operational limit and not an argument about canonical
width. `nearest_micros` stays `u32` and step B has a test saying so. No index
became derived from a position, no warning payload became a lookup, no range
invariant was added, and nothing truncates, saturates, or clamps.

`cli::bar_at_tick` gains a comment rather than a fix: its two branches return
a stored index and a last ordinal respectively, a pre-existing conflation that
this task only makes same-width, not correct. It is display-only.

## Steps A and B

Step A's characterization is green, and the only edits to it are fixture
parameter types — every assertion is byte-identical to the pre-migration
commit. Its `#[cfg(target_pointer_width = "64")]` gates are gone, because the
migration is exactly what removed their reason to exist: those four assertions
now run on every target rather than on the one where `usize` happened to be
wide enough. Step B's contract compiles and passes, including the two
`>u32::MAX` cases that never had a `cfg`.

Verified: 1339 tests green across core, swang, pattern, cli, and ui-core;
clippy `-D warnings` and `cargo fmt --check` clean workspace-wide, cockpit,
preview, and plugin included (type-checked with `--all-targets` against a stub
pkg-config entry, since libasound is absent here). The two `fuzz/` targets that
build a `MasterBar` are edited identically to sites that do compile, but
`fuzz/` pins nightly and could not be built locally — CI covers it.

Refs SWG-CORE-01

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
…tion

Found by the migration's own falsification pass, not by review. Replacing
`index_from_ordinal`'s body with `(ordinal as u32) as u64` left all 1339
tests green: the function is the one place a silent narrowing could hide, and
nothing reached it, because reaching the truncation through a real `Vec`
needs four billion elements and no test can build one.

Calling the conversion directly does reach it — its input is a `usize`, not a
collection — so the guard passes an ordinal above `u32::MAX` and checks the
value survives whole. Verified to fail under exactly that mutation and to
pass without it.

Gated on `target_pointer_width = "64"`, and this gate is not the kind step A
removed: on a 32-bit target the offending input does not exist, so there is
nothing to truncate and nothing to check. The gate marks where the hazard
lives rather than where the model happens to fit.

Refs SWG-CORE-01

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
The second hole from the same falsification pass, and the same shape as the
first: a narrowing hidden somewhere the tests never walked.

Every other contract test builds an `ImportWarning` and reads it straight
back. None went through `LossReport::add`. So clamping `bar_index` to
`u32::MAX` inside `add` — the one function every importer calls, and none of
the tests did — left all 1339 green.

This checks the importer's path: both payload variants go in through `add`,
come back out whole, and survive `absorb` as well, since that is the other
door into a report.

Verified to fail under exactly that clamp and to pass without it.

Two holes from one pass, both of the same family: the migration itself was
consistent, and what the suite lacked was a witness at the *seam* — one for
the conversion no `Vec` can reach, one for the constructor path the fixtures
bypassed. Worth stating plainly, because "the tests were green" was true both
times and meant nothing either time.

Refs SWG-CORE-01

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
Docs-only. Records what the migration settled, in the three places that were
still describing the world before it.

## `exact-score-text.md` (normative)

- Inventory B: `MasterBar.index` is `u64`, and its "platform-sized (H3)"
  malformed note is gone because the condition it named no longer exists.
  Both `ImportWarning` payload shapes lose their `← H3` markers and gain
  `u64`;
- §2.8: `<index>` is a `u64` written in decimal, fixed-width, so the same
  document means the same thing on a 32-bit target. It is still not an
  ordinal — the pointer to H4 stays, because that is the fact people get
  wrong;
- H3 becomes **CLOSED by SWG-CORE-01**, kept rather than deleted: the width
  is a normative fact and the reasoning behind it constrains what a later
  change may do. It now states why `u64` and not `u32` — values above
  `u32::MAX` were already inhabited, and a stored index is an exact fact
  rather than a bounded ordinal, so `u32` would have shrunk the model rather
  than merely making it portable — and names the two things easiest to undo:
  `index_from_ordinal` is the one sanctioned widening with no inverse, and
  operational `usize` was never part of the argument;
- the "until then the portability claim is false" clause is gone, because it
  is no longer true. Level 2 stays *allocated*: closing H3 removed the
  portability objection to freezing, not the phase-acceptance requirement.

H4 is unchanged in meaning, deliberately. The stored index still need not
equal its position, and that is now the only surprising thing about it.

## `foundation-backlog.md` (non-normative)

CORE-01 marked done with its outcome and its acceptance criteria checked off,
including the witness that fails if a `usize` reappears — named, because
"a witness test" is not a criterion until you can open it. 4A-05 is recorded
as unblocked and as the next task on the writer lane.

The width itself is stated here only as a back-reference; the normative
record lives in H3, so the two cannot drift into disagreeing.

## `decisions.log.md`

One Y-statement, because the backlog defined the width as this scope's
decision rather than the grammar's. It carries the falsification result too:
two of nine mutations survived the original suite, and saying so is more use
to a later reader than a clean number would be.

Refs SWG-CORE-01

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
…ee things

Tests-only, all three red at runtime, and the 13 existing census tests keep
passing beside them. Found by independent review of the closure, not by the
migration's own falsification pass — which is why these land as continuation
evidence rather than as edits to the six commits already made.

The production migration is not implicated. What is wrong is the *acceptance
witness* for it, and the backlog has already recorded that criterion as met
in its strong form.

## 1 and 2 — the scanner cannot see tuple forms

`declared_field_types` requires a `:` on the line, so a tuple enum payload is
skipped entirely: `Other(String)` today, and a `Tuple(usize)` tomorrow. One
level up, a tuple *struct* has no braced body, so the header match never
fires at all — `pub struct Pitch(pub u8);` is invisible, and §2.9 lists
exactly those newtypes as canonical leaves.

Meanwhile the witness's own doc comment claims coverage "including inside an
enum payload". For tuple payloads that is false.

The synthetic falsification did not catch it because it used only a named
payload. A witness written against vacuous witnesses turned out to be a
little vacuous itself, which is at least consistent.

## 3 — the witness reads one file

It scans `core/src/score.rs`. `Score` also reaches `TimeSignature`, `Tuning`,
`NoteMarks`, `NotePosition`, `FretboardPosition`, `TechniqueEvidence`,
`ConfidenceBps`, `Pitch`, `Velocity`, and `Ticks` in `core/src/event.rs`, and
`TickRange` in `core/src/slice.rs`. "No `usize` remains in any type reachable
from `Score`" may well be true; this witness does not show it.

The failing test names the defect in the only place it can be named — the
input expression. It is written as the exact call the witness makes today, so
the repair replaces that one line with the aggregate and leaves the
assertions untouched.

Refs SWG-CORE-01

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
…t claims

Tests-only. All three review-red tests pass; 16/16 census tests green.

## The scanner reads tuple forms

`declared_field_types` gains two branches: a one-line `pub struct Name(T, U);`
header, which had no braced body for the old scan to find, and a
`Variant(T, U)` line inside an enum body, which carried no `:` and so was
skipped element by element. Both key positionally — `Other.0`, `Pair.1` — the
way `enum_payload_fields` already keys tuple payloads, because
`Other(String, usize)` is two facts and a scanner that reports one is the
whole problem.

`tuple_arity` is now `tuple_elements(..).len()`. One splitter serves both the
count and the types, so they cannot disagree about where an element ends, and
`the_payload_parser_counts_tuple_elements` keeps guarding it unchanged.

`without_visibility` strips `pub` and `pub(crate)` so a width is one
declaration rather than three.

## The witness reads every module the tree reaches

`CANONICAL_SOURCES` lists `score.rs`, `event.rs`, and `slice.rs`, and
`canonical_field_types()` aggregates them. Both the no-`usize` witness and
the three-fields witness now run over that set instead of `score.rs` alone.

The list is written out rather than discovered. Discovering it means
resolving `use` paths and type aliases — a half-compiler to check three
modules, which is the temple-of-checkers this suite was told not to build.
The cost of writing it is that an entry could rot, so the coverage test
asserts each listed module actually contributes.

## Falsification

Five mutations, each caught by the test that names it:

```
usize in a braced struct in event.rs      -> no_platform_sized_integer
usize in a tuple enum payload, score.rs   -> no_platform_sized_integer
usize in a tuple struct in slice.rs       -> no_platform_sized_integer
source list narrowed back to score.rs     -> scans_every_module
a listed source that declares nothing     -> scans_every_module
```

The first two attempts at the event.rs and slice.rs probes were inconclusive
and I nearly recorded them as such: inserting a type *before* an existing one
detached its `#[derive(…)]` block, so the crate failed to compile for a
reason that had nothing to do with the witness. Appending before the test
module instead makes them conclusive.

The mutation harness also reported "not caught" three times today when the
truth was "did not compile" or "did not apply" — `grep '^error'` matches
cargo's own "error: test failed". Every result above comes from a run with a
diff guard on the mutated file and a compile check that distinguishes
`error[E…]` from a failing assertion.

Refs SWG-CORE-01

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
Two things the closure recorded were not true.

## The mutation count

`decisions.log.md` said "nine mutations". The standalone-verified figure is
**twelve**. Corrected in place rather than left to propagate — which the
entry immediately above it in the same file is about, so getting it wrong
here would have been a particularly poor joke.

## The witness description

The backlog said the acceptance witness "discovers the declarations rather
than taking a list". It did discover declarations, but only inside one file,
and it could not see tuple forms at all. Both halves are repaired in the two
commits before this one; the description now says what the witness does:
three named modules, named fields plus tuple enum payloads plus tuple
structs, with a coverage test that fails if a listed module stops
contributing.

## The review round itself

A second Y-statement records it, because "acceptance criterion met" was
written down on evidence that could not carry it. The distinction matters and
is stated plainly: the gap was in the witness, not in the migration — the
tree had no `usize` left either way, and the witness simply could not have
shown it. Five further mutations confirm the repair, and the reason the
module list is written out rather than discovered is recorded with its cost,
so the next person can weigh it instead of rediscovering the argument.

Refs SWG-CORE-01

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vdRzztKXy8bA16tEzwgLE
@coderabbitai

coderabbitai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1114dec5-314d-4bfd-96c4-b191596876b2


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@PhysShell
PhysShell merged commit 04327ba into main Aug 19, 2026
16 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants