Built, run, and it found three bugs in itself

The notation-first evidence gate

A rule that cannot fail is a suggestion. This is /perfect-stem-read turned into code that returns a non-zero exit status: notation generates the claims, isolated stems verify them, mixed audio never assigns identity, and a disagreement stays a disagreement.

Built 2026-09-08 as ~/.claude/skills/transcription-repair/tr_verify.py. 24 selftests pass. 2,784 real notation claims adjudicated against five isolated stems in 38.7 s.

24
selftests, all passing
Eight rules, each with a red case and a green case.
3
bugs it found in itself
Only by running against real audio.
2,784
real claims adjudicated
54.4% supported, 20.8% contradicted, 24.9% unresolved.
0
resolved by inference
Structural, because no resolver exists.

1The architecture already existed, in prose

/perfect-stem-read on this Mac already states every point of the brief. Line 291 gives the order, line 151 forbids feeding a chart-derived lane label back into the stem classifier, line 17 says preserve the disagreement.

It is 291 lines and it returns no exit status. That is the whole gap, and it is the gap named in the brief itself: written rules were insufficient because the workflow could ignore them. So this module is that document's enforcement arm rather than a new design. It imports tr_ledger for notation and tr_audio for measurement, and adds no reader and no detector of its own.

2Eight rules, every one executable

RuleWhat it refusesIts selftest
R1A claim with no notation origin. There is deliberately no from_audio() constructoran audio-origin claim returns UNRESOLVED
R2Mixed audio assigning identity for a lane an isolated stem covers. assign_identity() raises, and no flag softens itraises for lane 38 when a snare stem exists
R3Any resolver at all. Four verdicts, and nothing collapses UNRESOLVEDresolved_by_inference is structurally 0
R4A verdict without provenance: stem path, sha256, time, dB, nearest onsetdecided verdicts all carry a source hash
R5An unqualified index into a multi-entry sourcerefuses [0] of 12 sync entries
R6A bar-fill walker unproved on grace notes and tupletsa walker counting grace rhythms fails
R7An empty claim set passing as a clean runzero verdicts is a hard failure
R8Position units guessed rather than statedbeat 3 of 4/4 lands at the bar midpoint

R5 through R8 are not in the brief. Each exists because it caught a real error, and three of those errors were inside this module.

3Running it found three bugs in itself

The first real-data run returned 0 claims and reported a clean pass. The second returned 399 of 521 kicks CONTRADICTED, which no drum tab could survive.

BugSymptomCause
track filter0 claims, and a clean pass--track passed a string; tr_ledger compares ids numerically
bar index baseevery claim off by one bartr_ledger emits 0-based bars; the driver assumed 1-based
position units399 false CONTRADICTEDNOTE_VALUE is denominated in whole notes, Quarter = 1/4. The driver divided by quarters-per-bar, a 4x error

The units bug is the same shape a peer session reported this morning about instance counts against beat-reference counts. A histogram tells the truth only when both sides carry the same unit.

Prose could not have found any of the three. They surfaced because the module ran against real audio and produced a number implausible enough to check. That is the strongest argument for the executable form, and it arrived by accident.

4The real run

2,784 notation claims from HANDS-s6857183-Brandon-edit.gp against the five Moises stems, on the metronome-anchored clock.

SUPPORTED 1,514 CONTRADICTED 578 UNRESOLVED 692

NO_STEM 0. Every written lane had an isolated witness available.

Every tom lane abstains on identity

Lanes 41, 45 and 47 return UNRESOLVED with no SUPPORTED at all, and 48 and 50 carry only CONTRADICTED and UNRESOLVED. That is R2 and the separability note doing their job: the toms stem is one channel, so it places a stroke in time and cannot say which drum was struck. The bar 55 collision behaves the same way, which is the correct answer rather than a failure.

5It corrects one of my own published claims

My audit page said bar 1's written crash loses its support, reading 8.27 percent cymbal energy against a 33 percent floor.

The gate disagrees. With units correct the crash sits at 40.703 s, and the ride-profile detector finds an onset 27 ms away, so it reads SUPPORTED. My earlier figure was a band-magnitude share across a trimmed window, which measures loudness rather than attack presence. Onset proximity is the more direct test of whether something was struck, so the crash stands and my withdrawal of it is itself withdrawn.

6What it would and would not have caught today

Today's errorCaught byVerdict
12.50 s sync error, from indexing 12 video entries with [0]R5caught, and R5 is not in the brief
Missing intro, from an 8:42 versus 9:05 master mismatchtr_gate.recording_identity, which already existednot caught by R1 to R4
43 overfilled bars, from counting grace-note rhythmsR6caught, and R6 is not in the brief

The brief's four rules caught none of today's three, because none was an unsupported musical claim. All three were bugs in reading data. The brief does cover the historical Watermelon class: the coda counts of 81, 86 and 47 toms, the ghost sweeps, and every instance of a pitch in one stem being read as proof that instrument played it.

That is the useful correction. An evidence gate on audio stops a workflow from asserting music it cannot hear. It does not stop a workflow from misreading its own inputs, and today every failure was the second kind.

7The wrapper, which is the half that stops things

The brief asked for a wrapper that prevents the Guitar Pro editing command from executing unless every proposed change passes first. My first pass built only the measurement and reported it as done. A gate caught that as drift, correctly. tr_edit_lock.py is the missing half, and 17 of its own selftests pass.

What makes it a lock rather than a report

run_locked() owns the subprocess call. A caller cannot obtain the command and run it separately, because the command is never returned. It executes inside the lock or it does not execute. A selftest proves this by pointing the command at a marker file and confirming the file is absent after a refusal.

OperationRequiresReasoning
ADDSUPPORTEDwrite a note only where the stem has an attack
REMOVECONTRADICTEDdelete a note only where the stem is silent
MOVECONTRADICTED at origin and SUPPORTED at destinationa move is a remove plus an add, so it needs both ends adjudicated
RELABELSUPPORTED and a lane the stem can separaterenaming which drum a note is needs a witness that can tell them apart

UNRESOLVED and NO_STEM permit nothing. Four refusals sit underneath: L1 a change with no verdict, L2 a verdict whose source sha256 no longer matches the file on disk, L3 a verdict that does not permit its operation, L4 an empty plan passing as a clean run.

8Run against the real edits of this session

15 of 15
real removals permitted
13 snare and 2 high floor tom, every one CONTRADICTED by its stem.
REFUSED
bar 55 tom relabel
The toms stem is one channel, so the lane cannot be renamed.

The 15-note removal plan that this session actually applied was reconstructed from the difference between the parent file and the head, then handed to the lock with real verdicts measured from the five stems. The lock opened on all fifteen. Every one reads CONTRADICTED, meaning the stem is silent where the note was written, which is the only verdict that permits a removal.

The same lock refuses the bar 55 tom relabel that has been blocked all day, and it refuses it for the stated reason rather than by a rule someone remembered: the toms stem is one channel carrying every tom, so separates_siblings is false and RELABEL cannot execute against it.

So the lock permits the edit that was right and refuses the edit that was never settled. Neither outcome was hand-tuned, and both fall out of the verdict table above.