Skip to content

fix: verify says whose the other files in the directory are - #80

Merged
donislawdev merged 1 commit into
mainfrom
fix/verify-attributes-other-runs
Sep 7, 2026
Merged

fix: verify says whose the other files in the directory are#80
donislawdev merged 1 commit into
mainfrom
fix/verify-attributes-other-runs

Conversation

@donislawdev

Copy link
Copy Markdown
Owner

Stacked on #79, which is merged. This branch is rebased onto it.

A directory is allowed to hold more than one run - output.manifest exists so a
second run can record itself beside the first instead of being refused - and
verify called every file the neighbour had written extra, the word for a
file nobody asked for, and then called the directory a mismatch.

What was measured

Two runs one after another into one directory, name templates that do not
collide, both ending 0:

verify shared/manifest-alpha.json   ->  3 differences, exit 7
  extra b_0001.txt   extra b_0002.txt   extra manifest-beta.json
verify shared/manifest-beta.json    ->  4 differences, exit 7
  extra a_0001.txt   extra a_0002.txt   extra a_0003.txt   extra manifest-alpha.json

The report described a directory somebody had polluted. Every one of those files
was the neighbour's, and which neighbour was written down in the same directory
the whole time. The original report described this as being about the second
manifest - it is about every file of that run plus its manifest, and the
count depends on the neighbour's count.

cleanup needed nothing. It walks the manifest's entries rather than the
directory, so it has never known the word.

The repair

A new kind, another-run, carrying the name of the record that lists the file.
It does not count towards the verdict: a shared directory ends 0 and matched
is true. A real disagreement is untouched - missing or changed is still a
mismatch and still 7 - and the heading counts only the disagreements, so the
list under it does not mix the two.

This is the third category cut out of Extra for this reason, after
Leftover and Respelled, so the shape is the one already in the file.

Attribution, not suppression

Every file stays in the report. One entry each in --json, and in the prose one
line per neighbouring record rather than one per file:

note: manifest-big.json is another run's record. 10000 files here belong to it:
      bulk_0001.txt, bulk_0002.txt, bulk_0003.txt, and 9997 files not named here.

Untouchable rule 6 is kept literally rather than on trust, and a manifest
somebody leaves in a directory can claim a file out loud and cannot hide one.
That neighbour's ten thousand files used to print 10 001 lines and exit 7.

Two limits, both pinned by a guard

  • A record is only opened when its name ends in .json. Reading the first bytes
    of every unlisted file was the first design and was measured out the same day.
    The factor is refused rather than reported: the canary runs of the
    unchanged binary disagreed with each other sevenfold while it was taken. What
    is not in doubt is that it was large enough to change the design.
  • A file no manifest in the directory lists is still extra.

A second sieve was written and removed the same day. It read half a kilobyte
of a candidate looking for the first key of a manifest. Nothing could redden it -
a document that got past it was refused by the schema check behind it anyway -
so it changed no answer and no test could tell.

Cost

Interleaved with canaries, directory of ten thousand unlisted files:

median range
canary before 188 ms
before 228 ms 133 - 362 ms
after 258 ms 190 - 266 ms
canary after 194 ms

Canaries agree to 3%, and the two variants overlap, so no factor is claimed.

Guards

Seven guards, seven mutations, all caught. Two mutations did not compile at
first and were rewritten to consume what they replaced. staleness.py then
found five existing mutations that lifting the classification out of the
loop had invalidated.

Also in here, because the crowding ratchet caught it

  • Three functions split out so the count of functions nesting three deep stays
    at 51 rather than being moved to 53. The ratchet only tightens.
  • The comment on worstComplexity named rawTarget.resolveSize, and the
    guard's own log has named engine.Run since the run lock went in. The number
    has not moved, the holder has, and a comment naming the wrong function sends
    the next reader to flatten something that is not at the ceiling. O185
    records that Run now sits at exactly 22 of 22.

Full suite green, preflight --quick green.

🤖 Generated with Claude Code

A directory is allowed to hold more than one run. output.manifest exists so
that a second run records itself beside the first rather than being refused,
and verify called every file the neighbour had written "extra" - the word for a
file nobody asked for - and then called the directory a mismatch.

Measured on 2026-09-07, two runs one after another into one directory with name
templates that do not collide, both ending 0:

  verify manifest-alpha.json   3 differences, exit 7
  verify manifest-beta.json    4 differences, exit 7

Every one of those was the other run's work, and which run had written it was
recorded in the same directory the whole time. The report described a directory
somebody had polluted.

They get their own kind now, another-run, carrying the name of the record that
lists them, and they do not count towards the verdict. A shared directory ends
0 and matched is true. A real disagreement is untouched: a missing or changed
file is still a mismatch, still exit 7, and the heading counts only the
disagreements so the list under it does not mix the two.

This is the third time a category has been cut out of Extra for this reason,
after Leftover and Respelled, so the shape is the one already in the file.

The rule it follows is attribution rather than suppression, and that is the
half worth stating. Every file stays in the report: one entry each in --json,
and in the prose one line per neighbouring record rather than one per file.
Untouchable rule 6 is kept literally rather than on trust, and a manifest
somebody leaves in a directory can claim a file out loud and cannot hide one.
Measured: a neighbour's ten thousand files used to print 10 001 lines and exit
7, and now print one line and exit 0. The grouping is the lesson
manifest.Notes() already carries.

cleanup needed nothing. It walks the manifest's entries rather than the
directory, so it has never known the word.

Two limits, both deliberate and both pinned by a guard rather than left to be
discovered:

  - a neighbour's record is only opened when its name ends in .json. Reading
    the first bytes of every unlisted file was the first design and was
    measured out the same day - opening a file on Windows is not free. The
    factor is REFUSED rather than reported, because the canary runs of the
    unchanged binary disagreed with each other sevenfold while it was taken.
    What is not in doubt is that it was large enough to change the design. A
    record under another name is reported exactly as it was before.
  - a file no manifest in the directory lists is still extra, so leaving a
    manifest somewhere does not account for everything beside it.

A second sieve was written and taken out again the same day. It read the first
half kilobyte of a candidate looking for the first key of a manifest, to avoid
reading a large JSON document in full. Nothing could redden it: a document that
got past it was refused by the schema check behind it anyway, so removing it
changed no answer and no test could tell.

Cost, interleaved with canaries on a directory of ten thousand unlisted files:
228 ms before against 258 ms after, canaries 188 and 194 ms. The canaries agree
to 3% and the two variants OVERLAP - 133-362 against 190-266 - so no factor is
claimed.

Seven guards, seven mutations. Two of the mutations did not compile at first
and had to be rewritten to consume what they replaced, and staleness.py then
found five EXISTING mutations that lifting the classification out of the loop
had invalidated.

Also in here, because the crowding ratchet caught it:

  - three functions split out so that the count of functions nesting three deep
    stays at 51 rather than being moved up to 53. The ratchet only tightens.
  - the comment on worstComplexity named rawTarget.resolveSize, and the guard's
    own log has named engine.Run since the run lock went in. The number has not
    moved. Who holds it has, and a comment naming the wrong function sends the
    next reader to flatten something that is not at the ceiling. O185 records
    that Run now sits at exactly 22 of 22.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@donislawdev
donislawdev merged commit 96c6b30 into main Sep 7, 2026
18 checks passed
@donislawdev
donislawdev deleted the fix/verify-attributes-other-runs branch September 7, 2026 13:08
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.

1 participant