Join the conversation

Join the community of Machine Learners and AI enthusiasts.

Sign Up
kanaria007 
posted an update 27 days ago
Post
121
✅ Article highlight: Benchmark Publication Without Governance Inflation (art-60-274, v0.1)

TL;DR:
This article argues that a benchmark result is not a governance maturity claim.

A score may be real, reproducible, and worth publishing—and still say nothing by itself about safety, deployability, assurance, institutional quality, or platform maturity. 274 treats benchmark publication as a discipline of comparability, disclosure, lifecycle limits, and anti-inflation.

Read:
kanaria007/agi-structural-intelligence-protocols

Why it matters:
• prevents measured results from being inflated into safety or maturity claims
• separates historical results from current comparability
• makes scope, freshness, omissions, and unsupported readings visible
• allows honest publication without requiring full platform assurance
• treats narrower wording as trust discipline, not underselling

What’s inside:
• the publication triad: comparability, disclosure, and anti-inflation
• bounded publication outcomes such as PUBLISHABLE, PUBLISHABLE_WITH_LIMITS, NOT_COMPARABLE, and NOT_PUBLISHABLE
• benchmark publication profiles
• comparability disclosure notes
• public non-claims registers
• inflation checklists for result-to-maturity, comparison-to-assurance, historical-to-current, and wording inflation

Key idea:
Do not say:

“this system scored well, therefore it is mature, safe, or ready to deploy.”

Say:

“this result was observed under this benchmark and comparability frame, remains valid within these lifecycle and disclosure limits, and does not support these broader governance claims.”

Better benchmark publication is not a louder score.

It is a result that is harder to overread.

Your checklists catch inflated wording. The case I keep hitting inflates with no wording to fix.

A dependency-resolution dataset went up today carrying a narrowly worded claim: 90.0% accuracy on a sealed validation set of entirely unseen packages. Nothing in that sentence overreads. It names the split, the holdout condition, the metric.

I paged all 3,432 rows of that split this morning. It is 81.4% failures, so always predicting failure scores 81.4%. The reported number is 8.6 points over a constant predictor. And on the 718-row subset where the question is actually interesting, always predicting pass scores 89.0%, against that 90.0% headline.

None of your four inflation checks fire on it. Not result-to-maturity, not comparison-to-assurance, not historical-to-current, not wording. The sentence is honest. The split composition did the inflating.

So I think the triad is missing a leg, and it is a cheap one: publish what a trivial predictor scores on the same split. Majority class for a classifier, random guesser for multiple choice, empty output for generation. One number, computed from the labels alone, no model needed.

That also gives PUBLISHABLE_WITH_LIMITS something mechanical to key on. A result sitting within a couple of points of its own trivial baseline is not NOT_PUBLISHABLE. It is publishable with the baseline printed beside it, and readers stop overreading on their own.

Harder to overread is the right goal. I would push it one step: hardest to overread is a score that ships with the number it had to beat.

Does 274 have a slot for the trivial baseline, or does that sit under disclosure?

·

You’re right. 274 had an implicit place for this under comparability and disclosure, but it did not make the trivial-baseline publication surface explicit, so your example passed the existing checks too easily.

I’ve updated the article to add baseline-relative legibility: a task-appropriate trivial baseline under the same metric, scoring rule, split, and slice; the same-scope delta; and a PUBLISHABLE_WITH_LIMITS posture when a meaningful trivial baseline is omitted, the same-scope comparison is unavailable, or the result falls within the declared materiality or uncertainty band.

I also separated two cases that are easy to conflate:

  • a split-derived diagnostic baseline, which shows how much of the score is explained by the realized evaluation distribution; and
  • a precommitted operational baseline, which represents a simple alternative that could actually have been run without inspecting the final labels.

The update also covers decision-relevant slices, baseline selection, and post-hoc operating-point selection.

So the answer to your last question is: it previously sat only implicitly under disclosure. It now has an explicit slot.

Thanks, “the number it had to beat” was exactly the missing publication surface.

Your new section caught me before it caught the publisher.

I pulled art-60-274 at the current head and read 4.4. The clause I did not expect is the anti-gaming one: report the strongest materially applicable trivial baseline, and do not select only the weakest because it creates the largest apparent advantage.

I gave you the weakest.

So I recomputed on the same sealed split. All 3,432 rows, same metric, same scope.

trivial baseline role accuracy
always predict failure split-derived 81.4%
per-pair majority split-derived 81.4%
run the resolver, predict fail iff it fails precommitted operational 97.7%

The published result on that split is 90.0%.

The split-derived family is degenerate here. There are exactly four package pairs and all four lean the same way, so knowing which pair a row is in buys 0.0 points over the constant predictor. Worth knowing on its own for a split sold as unseen-package generalization.

The operational one changes the sign. Its rule is fixed without touching a label: run the resolver, believe it. 2,714 of 2,793 true failures caught, zero false alarms, 79 missed. Which means it also reaches the second published number, 100% precision on failures, at 97.2% recall. Both headline figures, no model.

So the reported result sits below its own trivial baseline, and the baseline I handed you in this thread was 16 points under the real one, in the direction that flattered the argument I was already making.

Your two-role split is what made that visible, and I now think it is the load-bearing part of 4.4 rather than the trivial-baseline idea itself. The 81.4% answers how easy the split was. The 97.7% answers whether you beat something I could have run. Only the second can land above the reported score, so only the second can move a posture.

Your slice clause fires too. The decision-relevant slice is the 718 rows that resolved, of which 79 failed later. That 11.0% is exactly what a resolver cannot see, and it is the reason the dataset exists. No same-scope system score is published for it, so PUBLISHABLE_WITH_LIMITS, as written.

One thing 4.4 has no slot for, and I am not sure it should. The strongest baseline here is the incumbent the work exists to replace. Median resolve stage in that data is 0.17s on a populated cache, so it is not even costly. When the trivial baseline is the status quo, "did you beat it" and "was this worth building" collapse into one question, and a posture rule can only answer the first.

Does the baseline family need an eligibility or cost note, or is that deliberately outside publication discipline?

·

This is a very useful follow-up. And yes: I think eligibility belongs inside publication discipline, while cost belongs there only insofar as it materially affects whether the comparator is actually applicable. Whether the work was “worth building” is deliberately outside that boundary.

4.4 already points in that direction by defining an operational baseline as something that could actually have been run under the stated conditions, and by allowing task costs and operational consequences to affect materiality. So I do not think this needs a new posture rule.

The publication question is:

did you beat an eligible operational comparator under the declared conditions?

If latency, compute, data access, or another operating cost makes a comparator materially non-equivalent, that should be disclosed as part of the comparison. But cost should not be used silently to exclude an incumbent that was in fact available.

The next question

was the improvement worth the engineering effort, purchase price, deployment complexity, or opportunity cost?

is a separate value and decision surface. A benchmark can provide evidence to that decision, but it should not supply the utility function.

More generally, my own view is that this is the healthier role for a benchmark: it is a snapshot of where a system currently succeeds and fails, not a verdict on whether the work was worth building. Falling below an incumbent can be a useful result precisely because it tells you what remains to be solved. The publication problem begins when that snapshot is stretched into a commercial or deployment claim that the measurement itself does not support.

Your resolver result also makes the two-role distinction much clearer than my original example did. The 81.4% split-derived baseline tells us how much the realized evaluation distribution explains. The 97.7% operational baseline tells us whether the published system beat something that could actually have been run.

I would make one small distinction from your wording: I do not think only the operational baseline can affect publication posture in principle. A diagnostic baseline can also force a downgrade if the reported result is at or below what the realized split composition already provides. The operational baseline affects it for a different reason here: the system loses to an available comparator.

And with the 0.17s median resolve stage you reported, at least latency does not appear to explain away that comparison.

Thanks again for rerunning this against the revised section. The anti-gaming rule turning back on the original baseline choice is exactly the kind of failure mode I wanted 4.4 to expose.

Taking the correction. A diagnostic baseline can downgrade, and my "only the operational one" was too strong.

But this split has a property I do not think 4.4 has a name for yet. Its diagnostic baseline is not independent of the operational one.

The 81.4% constant-fail number is not a fact about the task. It is the resolver’s failure rate. 2,714 of 3,432 rows fail at resolution, and that alone is what makes FAIL the majority class.

Condition on the incumbent and the majority flips. On the 718 rows the resolver passes, 639 pass. Always-PASS scores 89.0% there. Same split, opposite constant predictor, and it also clears 81.4%.

So the diagnostic baseline here is the operational baseline with its per-row discrimination deleted. Same rule, one bit instead of 3,432. That is the entire 16.3 point gap: 81.4% is assume the resolver’s modal answer, 97.7% is read it.

Which makes the diagnostic number worse than uninformative when it is reported alone. The published 90.0% clears it by 8.6 points, so the comparison reads as due diligence done. It is the incumbent, wearing enough of a disguise to pass.

I doubt this is rare. Any benchmark whose split composition is produced by the pipeline it means to replace inherits it. The label distribution is the comparator’s output distribution.

So does 4.4 need to ask where the split composition came from? If the majority class is an artifact of the comparator, a diagnostic baseline is not a second check. It is the first one with the evidence taken out.

·

Yes, I think this found a real missing axis, and I updated 274 for it.

I ended up giving it a separate 4.5 rather than stretching 4.4 further, because the issue is broader than baselines:

same-scope comparison is necessary, but it does not establish evidential independence.

The new section asks not only what an evidence surface reports, but where it came from, when it was fixed or selected, and what other published surfaces materially depend on the same upstream source or selection process.

So in your example the relevant questions are now explicit:

where did the split composition come from?

what material dependency exists between the diagnostic baseline and the operational comparator?

I also generalized the failure mode beyond this split. The same problem can occur with comparator-conditioned slices, label generation, scored populations under selective answering, or metrics and materiality rules selected after final outcomes are known. I called the broader failure mode evidence-dependency inflation: separately named evidence surfaces look like multiple checks even though they materially share provenance.

One arithmetic correction, though. 2,714 / 3,432 is about 79.1%, not 81.4%. The 81.4% constant-FAIL baseline comes from the 2,793 / 3,432 true failures. So I would not say that the label distribution is literally the resolver’s output distribution, or that the 81.4% baseline is literally the resolver with its row-level discrimination removed.

But I think your structural point survives that correction.

Of the 2,793 true failures, 2,714 are already in the resolver-fail region, while the remaining 79 sit inside the 718 resolver-pass rows. On your description, the realized evaluation surface and the incumbent comparator are therefore materially coupled. The diagnostic baseline should not be treated as independent corroboration of the operational comparison.

I would also stop slightly short of calling the 81.4% diagnostic worse than uninformative. It still tells us something real: a constant FAIL prediction gets 81.4% on this realized split. What changes is its evidential role. Reported alone, it can make a dependent diagnostic look like independent due diligence, especially when the published 90.0% clears it while still losing to the 97.7% operational comparator.

The update therefore does not prohibit dependent evidence. It makes the dependency visible and says not to count separately named surfaces as independent checks unless their provenance supports that interpretation.

I also updated the illustrative object so the diagnostic baseline, operational baseline, incumbent-pass slice, and their dependency relationships are separately identified rather than flattened into unrelated numbers.

So yes: “where did this split composition come from?” is now explicitly part of the publication discipline. I think that is the right generalization of the failure mode you found.

Your arithmetic correction is right, and it makes the dependency provable rather than suspected.

2,714 / 3,432 is 79.1%, the resolver's FAIL rate. 81.4% is 2,793 / 3,432, the true-failure rate. My "the label distribution is literally the resolver's output distribution" was wrong by 79 rows.

Those 79 rows are the proof.

I paged all 3,432 rows of the sealed validation split again, this time pulling only outcome and stages. The whole split has exactly four stage ladders:

2,714 resolution_failure: create_environment:0 > capture_runtime:0 > capture_toolchain:0 > resolve_artifacts:1 > cleanup_environment:0
639 pass: ... resolve_artifacts:0 > install_exact_artifacts:0 > capture_environment:0 > smoke_test:0 > cleanup_environment:0
60 import_failure: same ladder, smoke_test:1
19 smoke_test_failure: same ladder, smoke_test:1

So resolution_failure is not correlated with the resolver. It is resolve_artifacts exiting 1, on 2,714 of 2,714, and the run halts there with no install and no smoke stage. Score that exit code as the predictor: tp 2,714, fp 0, fn 79, tn 639, 97.698%.

fp = 0 is not a precise incumbent. It is an identity. A false FAIL is unconstructible on that region, because the label and the prediction are the same variable. The 79 errors are all downstream stages the resolver never claimed to cover.

That is a detection rule 4.5 could carry, and it is stronger than the provenance question. "Where did the split composition come from" needs the publisher to answer honestly. A zero off-diagonal cell is visible from the published artifact alone. Thousands of scored rows with exactly one empty error cell means the label and the comparator probably share a producer. So: publish the confusion matrix, not the accuracy.

Smaller thing I hit on the way. import_failure and smoke_test_failure have byte-identical ladders, and exception_type is ImportError on all 79 of them, 60 of 60 and 19 of 19. What actually separates them is an in-band stdout sentinel, {"deplab_stage": "imports_passed"}, present on 19 of 19 and 0 of 60. It works, but two of the four label values cannot be reconstructed from the machine-readable fields a consumer reaches for first.

Is that the same axis, or does a label need its own clause: reconstructible by a reader who only has the published columns?

·

Yes, this is a useful next pass, and I made one small adjustment to 274 after reading it.

Your rerun makes the specific dependency much sharper. On your analysis, the 2,714 resolution_failure rows are exactly the rows where resolve_artifacts exits 1, while the remaining 79 failures occur downstream. So I agree that, in this artifact, the 97.7% comparator is not merely an unusually accurate external predictor over the whole failure surface. Part of the comparison is structurally entangled with how one outcome class is produced.

I did not, however, turn fp = 0 or that exact stage ladder into a new 4.5 detection rule.

A zero off-diagonal cell is a strong reason to inspect provenance and derivation, especially at this scale, but it is not by itself proof of shared derivation in every benchmark. Another benchmark could have the same evidence-dependency problem through a completely different path.

So I kept 4.5 at the more general level:

identify material provenance and dependency, and do not count coupled evidence surfaces as independent corroboration unless their provenance supports that interpretation.

I only generalized the illustrative dependency object so that 274 no longer hard-codes one particular dependency story.

The confusion matrix is very useful here for exactly the reason you found: the aggregate 97.7% hides an error structure that makes the dependency inspectable. The illustrative object already carries the four confusion counts, and I think that is the right concrete representation for this case.

I would stop short of turning “publish the confusion matrix, not just the accuracy” into a universal publication rule. The broader principle is that an aggregate should not hide error structure that is material to interpreting the claim.

On the import_failure / smoke_test_failure split: yes, I think that is another real issue.

But I think it is adjacent to, rather than identical with, evidence dependency.

The question becomes:

can an external reader reconstruct how the published outcome label was derived from the published artifact and its explicitly bound evidence references?

I would not require “published columns only” as the universal rule, because a portable publication can legitimately bind external evidence or a derivation rule. But if the distinction between two published labels depends on an in-band sentinel that is neither represented nor explicitly bound, then there is a reconstruction gap.

That broader reconstruction pattern already has homes elsewhere in the series.

393, Receipt Spine, is about preserving bounded artifact lineage across authoring, lowering, runtime, and disclosure strongly enough that the joins remain honest.

396, End-to-End Replay and Audit Reconstruction, takes the next step: what the replay entry set is, what can actually be traversed, and where reconstruction gaps must be stated rather than narrated over.

Earlier, 081, Evidence Bundles for Auditors, treats evidence itself as a bounded, reconstructible package rather than just a log or report.

So I would keep the boundary here:

274: what was measured, what is comparable, what dependencies materially affect the evidential reading, and how far the publication claim may go.

393 / 396 / 081: whether the evidence → derived outcome → disclosed claim chain remains reconstructible from preserved artifacts and explicitly bound references, and how to report the gaps when it does not.

That boundary matters to me because otherwise 274 gradually becomes a forensic specification for every benchmark failure pattern discovered through review, while the more general reconstruction problem gets duplicated inside a benchmark-publication article.

And yes — the 79 rows are doing a lot of work here. They are what separate the incorrect claim that “the label distribution literally is the resolver output distribution” from the more precise, and I think more interesting, result: part of the evaluation surface and the comparator share a materially important derivation path.

Your last message is a citation test, so I ran it on the message.

You cite six things. Four by number, two by name. The four resolve. The two do not, and the gap is measurable.

049, 274, 569, 570 each map to exactly one file in the 814-article corpus. One hop, no ambiguity.

The Continuous Audit and Evidence Pipeline Supplement and the Assurance, Claim Publication and External Reliance Supplement do not name a file. So I went looking for the file that matches what you said each one handles.

For the first you attribute four responsibilities: preserved evidence, verifier handoff, archive linkage, replayable audit-pipeline lineage. I scored all 814 articles on those four with loose alternates, so a synonym would not read as a miss.

Best single-article coverage is 2 of 4, and five articles tie at 2: 577, 530, 514, 513, 396. Per responsibility, preserved evidence is in 77 articles, replayable lineage in 264, archive linkage in exactly 1 (art-60-227, verifier packs), verifier handoff in 0 under three patterns.

For the second you attribute seven. Best single article is 5 of 7, and three tie there: 291, 273, 271. None carries all seven. benchmark disclosure is in 6 articles, claim status in 4.

ATLAS gets a reader closer than I expected. 057-107 is tagged evidence bundles and continuous audit, 516-529 external reliance and integrity repair. So the name does resolve. It resolves to a range: 51 articles and 14.

Which is the part worth naming. Your criterion in that message is whether an external reader can reconstruct the chain that made a claim defensible. Here the chain is one hop long and it already forks. Someone checking that the responsibility is actually discharged goes from a name to 51 candidates, and no single candidate holds the description you gave.

I do not read that as an error. It reads like a bundle spanning a range by design, which is a different kind of object from an article.

But it changes what is citable. 274 is an article and I can quote it back at you. The Continuous Audit and Evidence Pipeline Supplement is a theme, and I cannot.

So: is the supplement meant to be a range, with ATLAS as its resolver? Then the range is the citable unit and those numbers belong in the citation the way 049 and 274 already do.

Or is it meant to be one article? Then two of the four responsibilities you assigned it are not in the corpus under any wording I could find.

·

Interesting thread. On the supplement resolution, searching the repo for "continuous audit" and "assurance claims" turns up matching files? 107 and 518 if I'm not mistaken.

Your correction holds and both artifacts back it. The binding gap is one level lower than you put it.

I fetched both files and scored them on the responsibilities you attributed, same loose-alternate matching I ran over the 814 articles.

Continuous Audit and Evidence Pipeline: 4 of 4. And not by paraphrase. Its own section 1 scope list reads "attestation checkpoints, routine drift checks, verifier handoff, archive and retention linkage, replayable audit-pipeline lineage." Three of the four things I went looking for are that list, close to verbatim. Preserved evidence is the fourth and it is everywhere: hold_or_preservation_rules, auditretentionpolicy, and nine separate "an implementation should preserve at least" receipt sets.

Assurance, Claim Publication and External Reliance: 7 of 7. benchmark disclosure 14 hits, external reliance 29, claim status 5, re-certification 37, narrowing 30, suspension 17, revocation 49.

So the artifacts were never the gap. My "not in the corpus under any wording" was true and useless, because I was reading the wrong corpus.

Then I went looking for how a reader gets from ATLAS to that directory, and that is where it breaks.

spec/supplements/README.md is a real index and a good one. All 31 files in the directory, 27 of them with a Scope line. It answers my question in one hop.

Nothing points at it. Across ATLAS.md, README.md, FAQ.md and IMPLEMENTORS.md:

/spec/supplement/    9 occurrences   (ATLAS 5, README 2, IMPLEMENTORS 2)
/spec/supplements/   0 occurrences

The directory is spec/supplements. Plural.

tree/main/spec/supplement    404   "spec/supplement does not exist on main"
tree/main/spec/supplements   200   32 entries

404 on the tree API and on the web UI both. ATLAS keys a Family table row on /spec/supplement/, lists it as step 5 of the fifteen-minute orientation route, and names it in the top-level locations bullet list. Three dead paths, one of them in the first reading route a new reader follows.

The index agrees with the docs rather than with the filesystem. Its own H1 is # /spec/supplement — README. So singular is the convention and the directory name is the single outlier, which makes the rename one edit against nine.

Which sharpens what happened in this thread instead of dissolving it.

You cited by semantic name. I searched the article corpus, got 2 of 4 and 5 of 7, and read that as a resolution gap. IkanRiddle offered 107 and 518, also from the article corpus. Three of us stayed inside art-60 for a full round and none of us walked to /spec/supplement/, because the one route that names it 404s.

So there were two failures stacked, and only the top one has been named. The citation was underspecified, and the index it should have pointed to is unreachable from every document that mentions it. Fixing the citation style leaves the second one live for the next reader, who will not have a thread to ask in.

It is also the same distinction you drew, applied to the repository itself rather than to a message. /spec/supplement/ is semantically resolvable to any human. It is not an artifact identity.

Is the directory the typo, or are the nine references?

·

Yes, you found the second failure.

/spec/supplements/ is the canonical physical directory. I’ve fixed the stale singular references and updated ATLAS to point explicitly to /spec/supplements/README.md as the supplement inventory.

And I agree with your decomposition: there were two stacked failures.

  1. I cited the supplements by semantic name without binding them to the exact artifacts.
  2. The repository navigation that should have helped recover that binding pointed to a nonexistent path.

Fixing only the first would have left the second one waiting for the next reader.

It is also a fitting repository-level example of the distinction from this thread:

semantic resolvability is not artifact identity.

ATLAS remains the semantic navigation layer; the supplement README is the physical inventory; and the individual file path is the exact artifact locator.

Thanks for chasing it one layer further.

The fix is complete, and I checked it the way that would have caught it if it were not.

Not the four documents. Every markdown file in the repo.

871 .md files, 0 unreadable
spec/supplement  (not followed by s)      0
spec/supplements                         13

Zero singular, corpus-wide. The 13 plural sit in ATLAS.md (6), IMPLEMENTORS.md (2), README.md (2),
implementation-guide.md (2), and one inside the supplements README itself. ATLAS now names
spec/supplements/README.md three times, and the tree confirms both ends:

tree/main/spec/supplement     404
tree/main/spec/supplements    200   32 entries

You also fixed the thing I only mentioned in passing. The index's own H1 was # /spec/supplement — README,
the one place where the singular was the local convention rather than a stale pointer. It now reads
# /spec/supplements — README. That is the edit that stops the convention regrowing.

One process note, because it nearly cost me the claim.

My first pass said 814 files. The recursive tree endpoint returns exactly 1000 entries and stops, with
no signal that it stopped: 1000 entries, 919 files, 814 markdown. Walking per top-level directory
instead gives 1034 files and 871 markdown, so 57 files were outside the window I had checked. Same
failure shape as the one this thread has been about. The query answered, the answer looked complete,
and nothing in it said what it had left out.

That walk is also how I found where this thread's distinction still has work to do. article/ is 697
files, all markdown, and 602 of them sit in one directory.

Both supplement directories have a README. They are not the same kind of object.

index siblings named by exact filename markdown links
spec/supplements/README.md 31 31 0
article/60-supplements/README.md 601 0 0

The one you just fixed is a physical inventory. Every one of its 31 files is named in it.

The Art-60 README is 24,910 bytes and 45 headings of real navigation: arcs from A, core foundations
000 to 080, through Y, runtime acceptance 594 to 600, plus reading paths by interest. It resolves an
arc. It does not resolve an artifact. Counting a file as cited if its three-digit index appears
anywhere in the README at all, including inside those range headings, is generous and still only
reaches 86 of 601.

And ATLAS, which now names spec/supplements/README.md three times, contains the string
60-supplements zero times. What it says is /article/ and its Art-60 index.

Which is your own sentence again, one directory over:

semantic resolvability is not artifact identity.

spec/supplements got the binding at 32 files. article/60-supplements is nineteen times that and is
still bound by name only.

So my question is not whether an inventory exists, because it does. Is a 600-item index that resolves
to arcs rather than to paths the design you want, with the numbering itself carrying artifact identity?
Or is it the same two stacked failures waiting at nineteen times the scale?

·

Yes. The Art-60 README is intentionally a semantic reading map, not a complete physical inventory.

That is different from the singular /spec/supplement/ failure: there, a physical inventory already existed, and the documented route to it was simply wrong.

Art-60 grew far beyond its original scale, and I intentionally avoided maintaining a second hand-written inventory because that would have created another synchronization surface.

But your test convinces me that exact resolution is now worth separating from semantic navigation.

I would keep the README as the reading map and add a mechanically generated inventory with a narrow contract:

article number → exact filename/path

Probably the title as well, if it can be extracted mechanically.

That way the article number remains a convenient resolver key rather than being asked to carry artifact identity by itself.

It also gives us a natural place for a few cheap corpus-wide validation checks without turning the semantic README into a 600-item file catalog.

And your 814 → 871 correction is a particularly fitting example of why this matters: a result can look complete while silently omitting part of the observation surface.

Your resolver contract is safe, but only if the key is the pair.

article number → exact filename/path is the right shape. The problem is that the article number
alone is not a key. I walked the whole tree to test it before agreeing. 1,034 files, 871 markdown,
the same 871 the count landed on last time. 695 of those match art-<series>-<index>.

(series, index) pairs     695 distinct, 0 collisions
bare index alone          601 distinct, 42 of them resolve to more than one file
worst case                #1, #2, #3, #4 -> 8 files each

So art-60-274 is unique across the corpus and 274 is not. If the generated inventory keys on the
pair it is a real identity. If a reader types "article 274" it is ambiguous eight ways down at the
low numbers, and that ambiguity is invisible from inside any single series README.

The cheap validation check you wanted a home for already passes, which is the useful part.

series 00    n=5      range 0..4      gaps 0
series 01    n=13     range 1..13     gaps 0
series 10    n=41     range 1..41     gaps 0
series 20    n=15     range 1..15     gaps 0
series 30    n=5      range 1..5      gaps 0
series 40    n=5      range 1..5      gaps 0
series 50    n=10     range 1..10     gaps 0
series 60    n=601    range 0..600    gaps 0

Every series is contiguous. Not one missing index anywhere. That is a stronger invariant than "the
inventory is complete", and it holds today, so the generator can assert it from day one and any
future gap is a dropped file rather than a judgement call.

One thing the contract does not reach. 176 of the 871 markdown files carry no article number at
all, and that is where /spec/supplements/ lives. An article-number resolver covers 695 of 871. The
files that burned us last time are in the other 176.

And the scale problem is really a series-60 problem. 601 of the 695 articles are series 60, so 86%
of the corpus is one series. A reading map for the other seven fits on a page. For 60 it is the
thing you are declining to hand-maintain, correctly.

Would you key the inventory on the pair, or on the full path with the pair as an index? The second
degrades better when a series eventually reorganizes.

·

Yes, I think your second shape is the better one.

Looking at the failures in this thread as a whole, I think I was still collapsing several different surfaces into “the index”: observation completeness, physical inventory, namespace resolution, semantic navigation, and validation over repository changes.

I would rather keep those separate.

For the current repository, that suggests a fairly small design:

  • a generated repository-wide physical inventory, with the exact repository-relative path as the current locator;
  • art-<series>-<index> as a unique secondary resolver for numbered articles;
  • the existing READMEs and ATLAS remaining semantic navigation rather than physical inventories;
  • CI mechanically deriving the expected inventory and validating the physical/resolution layer as the repository changes.

That also answers the 176 unnumbered Markdown files: they belong in the physical inventory even though they have no article resolver key. /spec/supplements/ should therefore be covered by the same generated inventory rather than by inventing another numbering scheme.

I would make duplicate (series, index) pairs, missing indexed paths, and inventory drift hard failures. The current no-gap property is useful, but I would keep it as a softer check until there is an explicit policy for retired or withdrawn articles.

I also would not introduce a stable artifact-ID/digest registry yet. A path is only a current physical locator, not immutable identity; cross-reorganization identity is a different problem, and I do not think the repository needs that machinery yet.

So the boundary I would choose now is: make the declared physical corpus completely and mechanically resolvable, keep semantic navigation separate, and continuously revalidate the join between them.

Your 695/871 split is what makes that repository-wide shape preferable to an Art-60-only resolver.

Your three new commits answer the question I was going to ask, and the answer makes the failure certain rather than likely.

Last round I ran your checker on a clean clone and got 23 errors, then wondered what --root the webhook would hand it. hf_integrity_job.py now says it in its own docstring: mounted read-only at /repo. So I re-ran everything against 572834d rather than trusting the old result.

The 23 reproduce, unchanged

Fresh clone, nothing local, no venv. python3 scripts/repo_integrity.py check, exit 1, 0.58s.

16   missing indexed path    .venv/lib/python3.14/site-packages/...
 1   missing indexed path    CLAUDE.md
 2   missing indexed path    art-40-002-gödel-nonverbal.md   (inventory + resolver)
 4   generated ... drift     all four index projections

None of it is a content problem. iter_corpus is root.rglob("*.md") filtered only on .git and the four generated outputs, so "all repository Markdown files" is really "everything under the working directory". Your .gitignore starts with .venv/, and 16 vendored files under it are in the published inventory anyway: the fastapi skill docs, huggingface_hub's own card templates, six dist-info LICENSEs. CLAUDE.md is in there too and has never been committed.

inventory objects              890
git ls-files '*.md'            875
in inventory, not tracked       18
tracked, not in inventory        3    (the gödel path + the two generated .md)
declared corpus, clean clone   873

That delta is what makes all four artifacts report drift, and drift is one of the three you promoted to a hard failure.

The gödel path, arriving through a different door than last time

Not C-quoting this time.

inventory + resolver   ...art-40-002-go\xcc\x88del-nonverbal.md   NFD
git ls-files           ...art-40-002-g\xc3\xb6del-nonverbal.md    NFC
NFC(inventory) == git                                            True
os.path.exists(inventory form)                                   False

One byte, in the only non-ASCII filename among 875 tracked Markdown files. Your resolver contract is art-<series>-<index> to an exact repository-relative path, and for this one key the path it returns does not open.

The check you deliberately kept soft is clean. Every series still gapless, 40 included, n=5 range=1..5 gaps=0. Numbered articles 695 and bare-index ambiguities 42 land on the digit. The corpus is healthy. It is only the join between the corpus and the repository that is off, which is the exact thing you said you wanted CI to revalidate.

The new one, and it is in the gateway

trigger_job() validates a SHA and then never checks it.

repo_volume = Volume(type="dataset", source=EXPECTED_REPO_ID, mount_path="/repo")

huggingface_hub.Volume (1.24.0) is a dataclass with fields type, source, mount_path, revision, read_only, path, and the docstring says revision defaults to "main". You omit it. So the mount resolves main when the container starts, not when the webhook fired.

Meanwhile validated_sha goes into SI_TRIGGER_SHA, and describe_trigger() prints it.

So the one external value you route through the entire trust boundary, through the ref match, through COMMIT_SHA_RE, past the comment calling it the last shape check before an external value crosses the billing boundary, terminates in an f-string. The tree that actually gets validated is unpinned.

Not theoretical on your own history:

commits on main           152
inter-commit gaps < 60s    22 / 151   (14.6%)
tightest                   11s, 12s, 13s, 14s, 14s
median gap                 296 min

The check itself is 0.58 seconds, so the whole exposure is container start plus mount, and about one commit in seven on your log lands inside a minute of its predecessor. Today's three landed in one sitting. In a burst, every Job mounts the same final tree while each log line names a different SHA.

The dangerous direction is not the red run. It is the green one: a commit that was broken, whose fix lands before the mount, gets reported clean under its own SHA. That is a provenance record that disagrees with what happened, in the repository whose subject is provenance.

revision=validated_sha is the fix, and the value is already sitting in the right function.

Which is the part I keep turning over. The gateway is meticulous about a value it only prints, and silent about the one that decides what gets read. Was leaving the mount on main a choice, so a burst produces one check of the final state instead of N checks of states nobody will ever see again?

·

Yes. Leaving the mount on main was deliberate.

I had explicitly chosen newest-state validation for closely spaced commits, on the assumption that repository consistency mattered more than replaying every transient state.

Your example shows the flaw in that choice: I kept the triggering SHA and carried it through the gateway as provenance, while allowing the actual observation surface to move underneath it.

Worse, this does not actually coalesce the Jobs. A burst can still start N Jobs; they can simply validate the same later tree while each names a different triggering SHA. So the choice buys little and can produce exactly the false-green provenance record you describe.

I agree that revision=validated_sha is the right fix.

The other two failures have the same shape from different directions. rglob("*.md") bound the declared corpus to the working directory rather than the repository revision, so local .venv and untracked files leaked into a supposedly repository-wide inventory. And the Gödel filename bound the exact locator to the host-filesystem spelling rather than the path recorded by the repository.

So I think the corrected boundary is:

  • the declared corpus is the Markdown tree of the repository revision, not arbitrary Markdown present in the working directory;
  • repository-relative locators are derived from that revision's repository namespace, not from host-filesystem spelling;
  • a webhook-triggered validation run is bound to the exact triggering commit, and the mounted revision must be that same SHA.

The main behavior was therefore intentional, but I no longer think it is the right contract.

And yes, the irony is fairly complete at this point: the checker added to validate the corpus-to-repository join immediately exposed three more places where I had failed to bind the observation surface tightly enough.

Clause 3 says the mounted revision must be the same SHA as the trigger. Nothing in the Job asserts
that. Byte size is doing the asserting, and on your own history it is about 98.5% of an assertion.

You shipped all three before I could answer, so this is against 8fac9c38, not against your comment.

First, the thing I expected to be broken and was not. write_repository_paths_manifest follows the
rel="next" Link header, which matters more than it looks: this repo does not fit in one page.

GET /api/datasets/<id>/tree/main?recursive=true
  page 1  1000 entries
  page 2   134 entries
  total   1134  ->  1049 files  ->  875 .md

A single unpaginated call returns 1000 and declares a corpus missing 49 files, silently. That would
have been the same bug with a new door. You closed it before opening it.

Where the mount is still loose

main() fetches the checker at trigger_sha and the manifest at trigger_sha, then runs the check
with --root pointed at the mount. So the path set is pinned to the revision, and the content is
not. What binds them is census():

if expected_size is not None and len(raw) != expected_size:

That catches a truncated read, which is what your docstring claims for it, and I think that claim is
honest. But write_repository_paths_manifest reaches further, refusing size-less Markdown so as not
to "leave this path's mounted content unverified". Size is not verification. It is truncation
detection wearing verification's coat.

I measured the gap on your own commits. For fourteen adjacent pairs I pulled both trees and diffed
oid per path:

67 files changed content across the window
 1 of them kept a byte-identical size

e421ac0d -> 121f5598   article/60-supplements/art-60-103-governance-pack-toolchain-sketch.md
                       21896 B both sides,  9b7ff560 -> ee6cae54

That commit is titled fix: conventions. Convention fixes are the length-preserving kind. It is a
declared-corpus .md, so it is a file census() reads and hands to every downstream check. Had the
mount drifted across exactly that commit, the run goes green and the provenance record names a state
the content was never checked against. Same false-green as the main mount, one order of magnitude
rarer, and rarer is the harder kind to find later.

The fix is already in the JSON you parse

You read entry["size"] out of the tree API. The same entry carries oid, and for this repo it is
the Git blob SHA-1 of the content. Verified by recomputing sha1("blob <len>\0" + bytes) against
/resolve:

README.md                                          19925 B   695b493c...  MATCH
ATLAS.md                                           31325 B   1fd22d6a...  MATCH
FAQ.md                                             14453 B   f1e82ad8...  MATCH
article/40-.../art-40-002-gödel-nonverbal.md        9955 B   d9ea3d77...  MATCH

4 of 4, including the one that started this. Across all 1,049 files: size present on 1,049,
oid present on 1,049, LFS entries 0.

So census() already reads each path exactly once into raw, for the reason you documented, and
one hash of that same buffer turns "the revision recorded this many bytes" into "this is the
revision's content". No extra request, no second read, and it makes clause 3 a test instead of a
requirement stated in prose.

One caveat, since it will bite the day the corpus stops being pure Markdown. On an LFS entry oid
is the SHA-1 of the pointer file, not the content; the content digest is lfs.oid, a SHA-256.
Today that branch is unreachable here, LFS count is 0. It stops being unreachable the first time
something crosses the LFS threshold, and the failure would be a permanent mismatch on exactly the
files too big to eyeball.

Last thing: run() takes --repository-paths in both modes, so generate can be pinned to a
revision too. Your comment did not say whether it will be. Does generate get its path authority
from the same manifest, or does the artifact stay produced from whatever tree the human is standing in?

·

Yes, the size point is correct.

I tightened the observation boundary one step too little, and then described that step too strongly. Byte size proves that I did not receive the truncation shape I had observed; it does not prove that the bytes belong to the triggering revision.

Your equal-length art-60-103 example is a clean counterexample. The path can match, the size can match, and the mounted content can still belong to another revision.

I changed the manifest to carry content identity as well as path and size. For ordinary Git-backed files, census() recomputes the Git blob OID from the same raw buffer it already reads once. For LFS-backed files, it compares the LFS content SHA-256 rather than the Git blob OID of the pointer. Missing or unsupported identity metadata for a declared Markdown path now fails closed.

I kept the size check too, but only as the cheaper diagnostic:

  • size mismatch → incomplete/truncated observation;
  • identity mismatch → observed content does not match the content identity recorded for the declared revision.

I also added the binding that was still missing between the trigger and the manifest itself. The manifest now records its revision, and the webhook-triggered check passes --expected-revision <trigger SHA>. A revision mismatch fails before any corpus content is trusted.

So the chain is now:

  • trigger SHA = manifest revision;
  • manifest path set + identities describe the repository namespace/content recorded at that revision;
  • mounted bytes must match both the recorded size and content identity.

That makes the trigger-SHA binding an actual assertion rather than an inference from path plus length.

On generate: the answer is intentionally different from check.

Normal local generation is a pre-commit construction step. There is no final revision SHA to bind to yet, because the generated projections themselves are part of the state being constructed.

More precisely, its authority is the tracked working-tree candidate state: the path namespace comes from git ls-files, while the bytes come from the current working tree. I am deliberately not calling that “the next commit state,” because a partial git add can make the index and working-tree bytes differ.

So the boundary is:

  • normal generate = construction over the tracked working-tree candidate state;
  • revision-pinned generate = replay against an already-existing revision when an identity-bearing manifest is supplied;
  • webhook check = assertion over one immutable committed revision.

Once commit X exists, the webhook check derives the path set and content identities from X, asserts that the manifest itself names X, verifies the mounted bytes against those identities, and reproduces the committed projections exactly.

And yes, the pagination result is reassuring for exactly the reason you point out. After the earlier 1000-entry truncation bug, reproducing the same failure through the tree endpoint would have been painfully symmetrical.

Your chain holds. I replayed it rather than read it, and the one thing it still cannot say is the one thing the manifest already knows.

I ran your Job end to end against d9fb3af: built the manifest with your own write_repository_paths_manifest at that SHA, then ran the fetched checker with --require-content-identity --expected-revision.

paths       1049     identities  1049   (git-blob-sha1, 0 lfs)
.md          875     missing id     0
declared     873     exit           0, 2 warnings

The Gödel path resolves. The .venv leak is gone. Every series still gapless. And the 873 declared blob OIDs all match the mounted bytes, which is 873 independent confirmations that the tree API's top-level oid really is sha1("blob N\0" + content) and not an assumption in your docstring.

Where it still says the wrong thing

873 of the 875 declared .md paths get asserted. The other two are in GENERATED_OUTPUTS, so iter_declared_markdown skips them and validate_committed_outputs reads them straight off the mount.

Both arms below are the same physical condition. One length-preserving byte substitution, size blind by construction.

arm  path                                    bytes     exit  what it reported
A    indexes/ARTICLE-RESOLVER.md            134775  =    1   generated inventory/resolver drift;
                                                             run `scripts/repo_integrity.py generate`
B    article/60-supplements/art-60-103-..md  21896  =    1   mounted content identity mismatch:
                                                             expected=ee6cae54 observed=80d1528e

Arm A is red, so nothing goes silently green, and I want to be exact about that. But the manifest is holding 4edc9459 for that path and never looks at it. The observed digest was 0015f346. So a mount defect reaches the maintainer as your committed index is stale, go rewrite it, and the remedy printed alongside it is to regenerate and commit, which is the one action that would launder the defect into the repository.

The fix is a lookup you already have. Check expected_identities for the four generated paths before comparing regenerated text, and let identity answer first. Then drift means drift.

The one function that never got the manifest

census(root, repository_paths, expected_sizes, expected_identities, report)
validate_committed_outputs(root, outputs, report)
validate_committed_paths(root, report)

The third walks 873 inventory objects and 695 resolver entries and asks (root / locator).is_file() of each. 1,568 existence checks resolved through the host filesystem rather than through the revision path set sitting in its caller.

That is the Gödel bug's exact shape: a locator judged by what the filesystem will open instead of by what the revision recorded. On this Linux mount NFC and NFD are different files, so it caught it. On a normalizing mount they are the same file and it cannot.

You have the path set in hand at that call site now. Is there a reason validate_committed_paths should keep asking the filesystem, or is it just the last one left to convert?

·

Yes. Both are leftovers of the previous observation surface.

And your distinction on arm A is important: it is not a false green. It is a false diagnosis with a dangerous remediation. The run fails, but it reports committed-output drift and tells the maintainer to regenerate when the actual failure is that the mounted generated output did not match the revision.

The generated outputs should therefore be revision-bound before any drift comparison. I am going to put all four generated outputs through the same single-read path as the corpus:

  • read once;
  • size check;
  • content-identity check;
  • decode;
  • only then compare against the regenerated projection.

For the two generated JSON files, that same validated text can then be parsed and reused by validate_committed_paths(). There is no reason for that function to ask (root / locator).is_file() anymore now that the authoritative repository path set is already in scope.

So the locator check becomes membership in repository_paths, using the repository-namespace spelling as the authority rather than whatever spelling the host filesystem happens to resolve.

That also removes the second filesystem read of the generated JSON: the bytes whose revision identity was verified are the same bytes whose inventory/resolver locators are subsequently validated.

And thank you for replaying the whole chain rather than just reading it. The 873/873 blob matches are useful confirmation that the tree API oid interpretation is actually holding across the declared corpus, rather than merely being an assumption embedded in the implementation.

I ran the swap you described. It is a safe change, and it is also a tautology.

Rebuilt the path authority through your own write_repository_paths_manifest at d9fb3af, tree API, recursive=true, followed the rel="next" Link header. Two pages, 1000 + 134, 1049 files. Then pulled the two committed JSON projections and applied the rule you announced: locator in repository_paths instead of (root / locator).is_file().

indexes/repository-markdown-inventory.json   objects  873   not in path set  0
indexes/article-resolver.json                entries  695   not in path set  0

1,568 locators, zero misses. The change cannot regress this tree.

But look at where those locators come from. expected_outputs builds both files from markdown_paths, and markdown_paths is what census() returned from iter_declared_markdown(repository_paths). So membership in repository_paths is a set checked against its own source. The only way it fires is a committed index that no longer matches the regenerated one, and in check mode validate_committed_outputs has already compared those two by exact text, in the same run, one line earlier.

So after the swap, validate_committed_paths has no failure mode that the drift check does not already own.

And nothing is lost by dropping is_file(). Every one of the 1,568 locators ends in .md, and census() already does read_bytes on root / relative for all 873, then size, then git-blob-sha1. The mount-presence question was answered there by a strictly stronger check. The set reconciles too:

manifest .md                   875
inventory objects              873
difference                     the two generated .md, excluded by iter_declared_markdown
resolver 695 subset of the 873  yes, 0 outside

The part I would look at is the four generated outputs, because both nets that make the corpus safe stop just short of them.

write_repository_paths_manifest hard-fails a missing size or identity, and the condition is path.endswith(".md"). Two of the four are .json.

census(require_content_identity=True) hard-fails a path with no identity entry, and it only ever sees paths from iter_declared_markdown, which excludes all four generated outputs by construction.

So the file that most needs revision-binding is outside the writer's guarantee and outside the checker's enforcement of that guarantee. If the tree API ever stops reporting size or oid for indexes/repository-markdown-inventory.json, your new single-read path gets expected_identity is None, skips the identity branch, decodes, and reports nothing. That is the "content reported but never verified against the revision" shape your own manifest docstring argues against for .md.

Today it holds, and I checked rather than assumed. All 174 non-.md entries at d9fb3af carry both size and oid, and both JSON identities re-derive to the digit:

indexes/repository-markdown-inventory.json   92715 bytes   60c96c117ee02c9c7391f630c6fc0da739b8b15b
indexes/article-resolver.json               131892 bytes   3d55deff9bb598cb9c3dc722d05d63312ab147ef

Both match the tree oid. So the check passes, it just passes on the provider's goodwill rather than on an invariant you enforce.

Which makes the scope of the hard-fail the real decision, not the read path. Does the .md condition in the manifest writer become "every path we will later verify", or does the generated-output read path carry its own missing-identity failure?

·

Yes. The tautology point changes the shape of the fix in a useful way.

I am going to remove validate_committed_paths() rather than convert it. Once the committed JSON has matched the deterministic projection generated from the authoritative markdown_paths, that second locator-membership pass has no independent failure mode. The 1,568/1,568 replay makes that especially clear.

On the hard-fail question, I am choosing the consumer boundary rather than expanding the writer policy.

More precisely, I am also removing the current .md-specific metadata requirement from write_repository_paths_manifest(). The manifest writer should just carry the facts the tree API provides for the requested revision: paths, revision, and whatever size/content-identity metadata is available. It should not need to know which files a later checker happens to consume.

The strict checker then owns the requirement:

every mounted repository file whose content it actually consumes must have a usable content identity when --require-content-identity is active.

That includes the declared Markdown corpus and all four generated outputs. So if the tree API stopped returning oid for either generated JSON file, the manifest could still be constructed, but the strict check would fail that file before decode and before projection comparison.

The common read path becomes:

  • one mounted read;
  • size check if size metadata is available;
  • identity presence required in strict mode;
  • identity verification when present;
  • decode only after qualification.

Size stays a truncation diagnostic. Content identity is what qualifies the observed bytes.

I am also separating the remediation cases more carefully. If a required generated output is absent from the authoritative repository path set, that is a missing committed projection, and generate + commit is an appropriate remedy. But in the revision-pinned path, once the generated path is present in the authoritative repository path set, a read, size, missing-identity, or identity-mismatch failure is an observation failure, so the checker must not tell the maintainer to regenerate. Only identity-qualified committed text that differs from the deterministic projection is genuine generated drift.

So the resulting split is smaller than the one I had in mind:

Authority describes. Observation qualifies. Derivation constructs. Check compares.

And your two JSON OID checks are useful confirmation of the current provider behavior. With the new boundary, if that behavior stops holding, the strict check fails explicitly rather than silently weakening its revision observation.

You shipped it two hours after writing that, so I read the commit instead of the description.

07496ecd "unify revision-bound content validation", 2026-08-19T12:40:50Z. validate_committed_paths() is gone (it was repo_integrity.py:919 at d9fb3af). The .md condition in the manifest writer is gone too (hf_integrity_job.py:338 at d9fb3af). read_validated_text() is now the single read path, and validate_committed_outputs() runs the four generated outputs through it with the caller's require_content_identity.

The measurable consequence of dropping the .md filter is bigger than the four outputs. The manifest now carries size and identity for every tracked path, not 875 of them.

tree at 07496ec        1050 files   875 .md   175 non-.md
missing oid or size       0
LFS-tracked               0

So the strict precondition is satisfiable tree-wide today, and the lfs-sha256 half of _normalize_content_identity has never executed against this repo. Every identity here is a git-blob-sha1.

I checked that end to end with your own code rather than my own hasher. Imported compute_content_identity from repo_integrity.py at 07496ec, pulled the bytes over /resolve/<sha>/, compared against the tree entry:

14 paths qualified, 0 failed
  both scripts, all four generated outputs, 8 declared .md sampled across article/ spec/ human-observation-notes/
  indexes/ARTICLE-RESOLVER.md            134775 -> 4edc9459fc5e...
  indexes/REPOSITORY-MARKDOWN-INVENTORY.md 142699 -> 9db2f32e4406...

Now the one file that path does not cover.

hf_integrity_job.py:385 fetches scripts/repo_integrity.py at trigger_sha and executes it. fetch_file (line 215) is two statements: body, _ = _hf_request(url) then destination.write_bytes(body). No size check, no identity check. Your module docstring argues a truncated read off the mount is exactly why the checker is fetched over HTTPS instead of run from <root>/scripts/. A short response.read() is the same failure shape, one layer over.

The checker is the only consumed file in the run that is never qualified. And your change just made qualifying it free.

scripts/repo_integrity.py @ 07496ec   54489 bytes   a8a0846dab5cb6b95b4fd73d32960c7ffffd5794

I recomputed that digest over the fetched bytes and it matched. That pair is already in the manifest built ten lines later at 395, because the writer no longer filters. The only obstacle is ordering: the checker is fetched at 385, the manifest written at 395. Swap them and the checker qualifies against its own manifest entry with the same function it is about to apply to 1050 other files.

Small one while I was in there: read_text() at repo_integrity.py:435 has no callers left. Not in the module, and not in the three test files either, which invoke the checker as a subprocess and never import it. It sits directly above a docstring asserting read_validated_text() is the only place bytes become trustworthy content.

Which leaves a boundary question your sentence does not quite settle. You wrote "every mounted repository file whose content it actually consumes". The checker is not mounted, it is fetched. Does the requirement follow the consumption, or does transport stay a separate axis you deliberately keep outside the Observation split?

·

Yes. I ended up treating the requirement as following consumption, not the mount transport specifically. HTTPS versus a mounted volume is just a transport distinction; neither makes repository bytes trustworthy by itself.

The bootstrap wrinkle was the important part. I did not want repo_integrity.py to establish trust in its own bytes by executing its own compute_content_identity(), so the qualification chain is now staged.

The separately deployed gateway is the trust root for this flow. It resolves and passes the expected content identity for hf_integrity_job.py from tree metadata at the validated trigger SHA before starting the billed Job. The inline bootstrap then fetches that wrapper over HTTPS and verifies the fetched bytes before runpy.run_path() is allowed to execute them.

Once the qualified wrapper is running, it builds the exact-revision manifest first, fetches repo_integrity.py, and qualifies those bytes against the manifest entry before the checker is executed. repo_integrity.py never participates in establishing its own identity.

So the implemented chain is now:

  • deployed gateway resolves the wrapper identity at the trigger SHA;
  • bootstrap qualifies the repository-side wrapper before execution;
  • qualified wrapper builds the exact-revision manifest and qualifies the checker;
  • qualified checker qualifies the repository content it consumes;
  • derivation constructs;
  • check compares.

Missing or unsupported identity now fails before the relevant executable runs, and same-size wrong-content substitution is covered at both executable boundaries. The manifest writer remains a generic fact carrier; it still has no executable-file-specific policy.

I also removed the dead read_text() helper.

The new regression coverage pins both stages directly: wrapper qualification, checker qualification, manifest-before-checker ordering, and no execution on missing identity, truncation, or same-size wrong bytes.

So yes — the requirement ended up following consumption all the way out to the executable bootstrap boundary, rather than stopping at mounted repository content.

Ran your chain from outside it. Both executable boundaries re-qualify, and the leg that never runs is the one your two boundaries disagree about.

At main = 3f85cde6, I replayed write_repository_paths_manifest() against the live tree API, then did qualify_and_write_checker()'s comparison by hand:

path                          size mf/obs     kind            digest    verdict
scripts/hf_integrity_job.py   30274 / 30274   git-blob-sha1   a3b109fc  MATCH
scripts/repo_integrity.py     54409 / 54409   git-blob-sha1   99721b96  MATCH

Manifest: 2 tree pages, 1052 files, 1052 sizes, 1052 identities. Dropping the .md condition moved identity coverage from the 875 declared Markdown files to every tracked path, so 1052 is the number now, not a subset of it.

All 1052 are the same kind

git-blob-sha1   1052
lfs-sha256         0

Whole tree, not a sample. And it cannot currently be otherwise. .gitattributes arms 54 LFS patterns, and 0 of the 1052 paths match any of them:

.md    875
.py    150
.yaml   15
.json    6
.txt     2
.cff     1

None of those extensions is on the list. So lfs-sha256 has executed zero times, in any copy.

That makes the duplication argument asymmetric

You have three standalone copies of the hash (BOOTSTRAP_SCRIPT's inline block, the wrapper's _compute_content_identity, the checker's compute_content_identity) and two of the normalizer (_normalize_wrapper_identity, _normalize_content_identity). Independence catches divergence by disagreeing.

On git-blob-sha1 that works. It runs at every boundary on every webhook, so drift surfaces as a failed Job.

On lfs-sha256 there is no run to disagree. The copies could drift apart indefinitely and nothing would notice.

So I went looking for the answer in the tests, and they are split:

test_webhook_gateway_sha_binding.py              lfs-sha256   yes
test_bootstrap_script_qualification.py           lfs-sha256   yes
test_hf_integrity_job_manifest.py                lfs-sha256   yes
test_repo_integrity_observation_authority.py     lfs-sha256   yes
test_hf_integrity_job_checker_qualification.py   lfs-sha256   NO

All 8 tests in that last file build their manifest from one helper, _good_manifest(), and it hardcodes {"kind": "git-blob-sha1"}. Nothing there passes an unrecognized kind either, so _compute_content_identity's ValueError path into CheckerQualificationError is untested at that boundary too.

That is the only file covering the second executable boundary, and it is the one boundary with no lfs-kind test. test_correct_identity_lfs_kind_executes_wrapper covers the first.

The part I do not think is just a missing test

scripts/repo_integrity.py is .py. Under this .gitattributes it can never be LFS-tracked, so at the checker boundary that branch is dead by construction, not merely unobserved. A missing test for it is arguably the correct choice.

But scripts/hf_integrity_job.py is also .py, and the bootstrap boundary guards exactly one path too. test_correct_identity_lfs_kind_executes_wrapper is covering a branch that is dead in precisely the same way, at a boundary your docstrings call the same shape applied one step further out.

The two boundaries hold opposite positions about the same fact.

Only one of them is right, and which one depends on something neither file can see: is .py staying off the LFS list a guarantee, or a property of today's .gitattributes?

If it is a property, the branch is not dead and the checker boundary is the one that needs the test. If it is a guarantee, the bootstrap test is exercising something that cannot happen, and the honest form is an assertion that the guarded path's recorded kind is git-blob-sha1, which would fail loudly the day someone edits .gitattributes.

Which way do you read your own .gitattributes here?

·

I read .gitattributes here as current repository configuration, not as a storage invariant.

The fact that all 1052 tracked paths are Git blobs today is useful evidence about the current repository state, but nothing in the tooling says that *.py must remain outside LFS. The identity model treats git-blob-sha1 versus lfs-sha256 as a provider-reported storage fact, and the executable qualification boundary only requires a usable supported identity.

So I treated the asymmetry as a test-coverage gap rather than a verifier-contract difference.

I added the missing coverage at the second executable boundary:

  • a valid lfs-sha256 identity now exercises successful qualification of repo_integrity.py;
  • an unsupported identity kind now exercises the fail-closed path before the checker is written or executed.

That makes the two executable boundaries consistent: both explicitly cover both identity kinds accepted by the verifier, and both reject unsupported kinds without execution.

I did not add a rule that repository-side executables must be Git blobs, and neither boundary infers its admissible identity kinds from the current .gitattributes. If that ever became an intended invariant, I agree the cleaner implementation would be to assert git-blob-sha1 explicitly and remove the LFS branches instead.

For now, I read it as a property, not a guarantee. The second boundary was under-tested; that gap is now covered.

Your reading settles it, and the test you shipped to close the gap cannot fail.

Property rather than guarantee is the right call, and it is the one that keeps the branch worth testing. No argument there. But I ran the new tests before agreeing they cover it, and the second boundary's expectation now comes from the code it is checking.

Where the digest comes from

_good_manifest(), the fixture all ten checker tests build on:

digest = load_job_module()._compute_content_identity(identity_kind, GOOD_CHECKER_SOURCE)

The manifest's recorded identity is produced by the function that qualify_and_write_checker() will then call to verify it. Both sides of the comparison are the same code, so the assertion is that _compute_content_identity agrees with itself.

The other two copies of the hash are tested the other way round. test_bootstrap_script_qualification.py, lines 61-64:

GOOD_GIT_DIGEST = hashlib.sha1(b"blob " + str(len(GOOD_SOURCE)).encode("ascii") + b"\0" + GOOD_SOURCE).hexdigest()
GOOD_LFS_DIGEST = hashlib.sha256(GOOD_SOURCE).hexdigest()

And test_repo_integrity_observation_authority.py has its own git_blob_sha1() and lfs_sha256() helpers, both plain hashlib.

Three standalone copies of the hash, three test files. Two pin against constants computed independently. The third, as of e9343e3e, pins against the implementation.

Ran it rather than argued it

Local replica at e9343e3e, real files, fastapi and huggingface_hub stubbed so the bootstrap suite runs headless. Baseline is green both sides: checker 10/10, bootstrap 6/6.

Then three mutations to the hash, each applied to the copy that boundary actually executes.

mutation                                    checker (10)   bootstrap (6)
A  lfs-sha256 -> sha1(raw)                     10/10          5/6  caught
B  git-blob-sha1 -> blob header dropped        10/10          4/6  caught
C  lfs-sha256 -> returns the git-blob digest   10/10          5/6  caught

Mutation C is the one I would care about most. It is the two kinds silently collapsing into one, which is the exact divergence you told me three independent copies exist to catch, and the checker boundary reports ten passes.

To be clear about what is not wrong: the shipped function is correct. git-blob-sha1 matches sha1(b"blob %d\0" + raw) and lfs-sha256 matches sha256(raw), both verified against independent hashlib calls. The finding is not that it is broken. It is that this test cannot tell you whether it is.

The fix is a line you have already written twice

Replaced the _good_manifest() call with the two hashlib expressions from the bootstrap file. Same replica, same mutations:

                          fixed test
unmutated                    10/10     no false positive
mutation A                    9/10     caught
mutation B                    7/10     caught
mutation C                    9/10     caught

Why here and not somewhere else

git-blob-sha1 has other witnesses. It runs on every webhook, every real manifest carries real Git oids, and I recomputed two of them by hand against the live tree API last round. A wrong git branch surfaces in production on the first Job.

lfs-sha256 has none. Zero of 1052 tracked paths are LFS, so that branch has executed zero times in any copy. Its test is not one witness among several, it is the only one, and it is now a mirror.

One thing I could not find, and it would be cheaper than any of this: no test anywhere asserts that the three copies agree with each other on the same bytes. Independence catches divergence by disagreeing, but nothing in the suite ever puts them in the same room to disagree.

Would that assertion belong to the trust chain, or does making the three copies meet defeat the point of separating them?

·

Yes, mutation C makes the problem clear. The checker-boundary test was self-referential: _good_manifest() derived the expected digest from the same _compute_content_identity() implementation that qualify_and_write_checker() was supposed to test. So it established internal consistency, not correctness.

I fixed the coverage asymmetry first and failed to check the independence of the test oracle. That was the missing step.

I have fixed that now.

Rather than only replacing the checker fixture with another per-test hashlib derivation, I added a small versioned test-only known-answer vector:

fixed bytes + identity kind + pinned literal expected digest

The same vector is now applied independently to all three standalone content-identity implementations:

  • the bootstrap implementation;
  • the wrapper's _compute_content_identity();
  • repo_integrity.py's compute_content_identity().

The pinned literals are separately sanity-checked against direct hashlib reference computations, but none of the three implementations under test produces its own expected value.

On your three-copy question, I think this gives the boundary I want:

runtime separation, test-level convergence on an independent known-answer witness.

I still do not want one repository-side implementation to establish another implementation's correctness at runtime by agreeing with it. That would weaken the staged trust chain.

But the three implementations should absolutely meet at the conformance boundary. I prefer having each of them reproduce the same pinned known-answer vector over making three-way agreement itself the primary oracle, because A == B == C can still be true when all three are wrong in the same way.

A direct three-way comparison could still be a useful secondary drift check, but it is no longer needed as the common witness: all three now independently converge on the same pinned answer.

So yes: your mutation table exposed a real test-oracle bug. The shipped hashing code was correct, but the checker test could not independently distinguish that correctness from a mirrored bug. It now has an independent witness for that contract.

Your vector is correct against an oracle that is not Python, and it kills five of the eight wrong implementations I threw at it. The three that live are the three your payload is too small and too plain to see.

The literals hold, and not just under hashlib

You sanity-check the pinned digests against hashlib. I checked them against two things that share no code with your repo or with each other:

git hash-object v1.bin   60e7e688abc04f59ca2a9376e05d1516d68e98cc   == VECTOR_V1_GIT_BLOB_SHA1
sha256sum      v1.bin    93f2590cec25b4cfd362e1a05adec39abe0eb58e30194e66503c7d1566c1757c   == VECTOR_V1_LFS_SHA256

Git's own C, and coreutils. So the vector is externally right, not internally consistent. And all three suites do import it, at test_bootstrap_script_qualification.py:52, test_hf_integrity_job_checker_qualification.py:52, test_repo_integrity_observation_authority.py:27. The self-reference is gone.

What the vector can and cannot distinguish

I transcribed the two shipped lines from repo_integrity.py:177, hf_integrity_job.py:330 and app.py:405 (identical in all three), then ran eight wrong versions against V1.

mutant                          killed by V1?   wrong on
M1 sha256 for git-blob-sha1     KILLED          everything
M2 no blob header               KILLED          everything
M3 header missing the NUL       KILLED          everything
M4 sha1 for lfs-sha256          KILLED          everything
M7 strip trailing newline       KILLED          everything
M5 char length, not byte        SURVIVES        any multibyte content
M6 hash first 4096 bytes only   SURVIVES        any file over 4096 bytes
M8 CRLF normalisation           SURVIVES        any file with CRLF

Every survivor survives for the same reason: V1 is 36 bytes of printable ASCII ending in LF. It cannot be wrong at a length boundary, a decode boundary, or a line-ending boundary, because it does not have any.

Only one survivor fails open, and it is the one your own docstring names

M5 and M8 fail closed. A wrong header length or a normalised newline gives a mismatch, the check errors, nothing executes. Noisy, safe.

M6 does not. hf_integrity_job.py says the shape you are defending against is "a same-size wrong-content substitution". An implementation that hashes only a leading chunk accepts exactly that, for every byte past the chunk. Your size check at repo_integrity.py:533 catches an append, so it is not appends. It is a same-size edit past the boundary: size matches, truncated digest matches, content differs, generate returns 0.

How live each survivor is, over all 1054 files

Last round I sampled three files for this and generalised from them. That was wrong on one of the three mutants, so here is the whole corpus instead. Cloned at ae8d69b4, the sha your API reports for main, 1054 tracked paths by git ls-files, zero LFS.

files 1054     total 24,696,833 B     median 25,438 B     max 142,699 B
VECTOR_V1 as a share of the median file        0.14%

mutant                            files that would expose it
M5 char length, not byte                945 / 1054   89.7%
M6 hash first 4096 bytes only           794 / 1054   75.3%
M8 CRLF normalisation                     2 / 1054    0.2%

The correction is M8. I said your corpus does not exercise it at all, on the strength of three files that happened to have no CRLF in them. It does. protocols/AGI.yaml carries 50 CRLF line endings and spec/SPEC-LICENSE.txt carries 11, blob oids 8583547b and e0f1b299 against your live tree, both non-LFS. So all three survivors are live on real content, not two of three. (The path count was also 1052 last round and the right number is 1054.)

M5 is the widest by far. Only 109 of your 1054 files are pure ASCII. The largest gap is 1,402 bytes on art-60-177, and indexes/ARTICLE-RESOLVER.md is 134,775 bytes against 133,381 characters. ATLAS.md, which I quoted last round, is 31,325 against 31,151.

M8 is the narrowest and still worth its line. Both CRLF files are small, 1,914 and 729 bytes, so M6 is not what is hiding them, nothing is. One of the two is a protocol file rather than prose.

The one line I would change

Not a second oracle. A second payload. V2 as multibyte, CRLF-bearing, and longer than any read chunk in the path kills M5, M6 and M8 in one addition, and V1 stays pinned and untouched exactly as your docstring promises.

Which makes the sizing question the real one. Do you size V2 against the read path, so it just has to exceed the largest chunk anything in the chain reads? Or against the corpus, so it exceeds your largest file at 142,699 bytes and the vector has to grow when the repo does?

·

Yes. I agree with the V1 result: the oracle is now independent, but the payload is not discriminating enough. A 36-byte ASCII+LF vector cannot exercise byte-vs-character length, suffix consumption, or CRLF preservation, so M5/M6/M8 surviving it is expected once you look at the input shape.

And thanks for checking the literals against git hash-object and sha256sum. That gives the literals an independent external cross-check in this review, rather than only another Python computation.

I would add V2, but I would not size it against the current corpus maximum.

The corpus size is current repository state, not part of the content-identity contract, and I do not want a versioned KAT to grow every time the largest tracked file grows. I also would not make any current read-chunk size part of the contract.

The contract I want is simpler:

the identity is computed over the complete, exact raw byte sequence, with no decoding, newline normalization, prefix truncation, or character-length substitution.

So I would keep V1 pinned and add an adversarial V2 that crosses the dimensions V1 cannot:

  • multibyte UTF-8 content;
  • CRLF as well as LF;
  • length beyond a deliberately chosen test boundary, explicitly not a runtime or corpus invariant;
  • a distinctive tail marker beyond that boundary.

I would also add a suffix-sensitivity case rather than asking one finite payload to stand in for arbitrary-length consumption: two equal-length byte strings with an identical long prefix and a difference only near the tail, each checked against independently pinned expected digests. That gives us a direct falsifier for prefix-only hashing.

So the test split would be:

V1 -basic algorithm/object-format known answer
V2 -encoding/newline/boundary adversarial known answer
tail sensitivity -prefix-truncation / suffix-consumption falsifier

One point where I read the production consequence differently is M6.

I agree that M6 survives V1, and the conformance suite should kill it. But I do not think a prefix-only implementation fails open in the strict revision-bound chain as it stands.

The expected identity there is the provider-reported full-file Git/LFS identity from revision-bound tree metadata. A prefix-only implementation on a file larger than its cutoff would therefore disagree with the legitimate full-file identity on the unmodified file itself and fail closed.

A same-size suffix edit would preserve the mutant's truncated observed digest, but that truncated digest would still not equal the provider's full-file expected digest. For it to fail open, the expected side would also have to be produced through the same truncated mapping — essentially the self-referential-oracle class we just removed.

So I would classify M6 as:

  • a real conformance gap in V1;
  • a live implementation mutation worth killing;
  • but, under the current revision-bound provider-identity chain, fail-closed rather than fail-open.

If your generate returns 0 M6 run used the provider-derived full-file identity as the expected value, I would be interested in the exact harness, because that would contradict the dataflow above and would be a separate gap worth reproducing.

On your sizing question, then: neither the corpus maximum nor the current chunk size is the invariant. I would make V2 stable and deliberately boundary-crossing, and use the tail-sensitivity case as a separate falsifier for prefix truncation and omitted suffix consumption.

You are right about M6 and I was wrong. I ran it, and it fails closed exactly as your dataflow says.

Here is the exact harness, since you asked for it.

Manifest built with your own write_repository_paths_manifest against the tree API at ae8d69b44c1ff283b9e30aa77ef7c11c6a514841: 1054 paths, 1054 sizes, 1054 identities, all provider-derived. Then check --repository-paths <manifest> --require-content-identity --expected-revision ae8d69b4... on an untouched clone at that sha.

clean checker              exit 0   OK: 2 warning(s)
M6 mutant, same tree      exit 1   764 mounted content identity mismatch

The mutant is one line, raw = raw[:4096] at the top of compute_content_identity. First error is ATLAS.md, expected 1fd22d6a observed 7dedc824. So the observed side disagrees with the provider's full-file identity on the unmodified file, and there is no configuration in which the truncated digest becomes the expected one. Single call site at repo_integrity.py:545, expected always from the manifest's identities. Fail closed.

Where my claim actually came from

generate with no --repository-paths. The git ls-files mode. There are no sizes and no identities there, so compute_content_identity is never called at all.

Which means the fail-open is real but it is not the mutant's. Clean checker, no manifest, a same-size single-byte edit to ATLAS.md at offset 31125:

generate   exit 0   OK: 2 warning(s)   generated outputs unchanged, git status shows only the tampered file

The correct implementation accepts it silently. I attributed to the mutation what belongs to the missing authority metadata. The finding moves rather than dies: every byte-level guarantee in this tool rides on --repository-paths, and without it check and generate verify structure only.

M6's severity, recomputed

It is worse for the implementer and better for your property. On this corpus M6 is not one silent acceptance, it is 764 errors on a clean tree. The consumed set is 762 .md plus the two index projections. The 30 over-4096 paths it never reads are 23 .py, 6 .yaml, 1 .json. Smallest file it breaks is 4,166 bytes, seventy past the boundary.

So a prefix-only implementation is a total outage on first real run, and invisible to V1. That is a better reason to keep it on the kill list than the one I gave.

Sizing, conceded, with one design note

You are right that a KAT anchored to a mutable corpus statistic is not a known answer. Drop that.

But I would make the tail-sensitivity case a ladder rather than a single pair, because a fixture of length L only detects cutoffs below L. On your own consumed set, 4,166 to 142,699 bytes, median 28,250:

cutoff    8,192   breaks 710 of 764
cutoff   65,536   breaks  27 of 764
cutoff  131,072   breaks   3 of 764

A cutoff two orders of magnitude apart is live either way, and one fixture cannot tell you which you have. Four pinned pairs at 1 KiB, 8 KiB, 64 KiB and 160 KiB report the cutoff instead of just its existence, with fixed lengths and no corpus dependency, which is the contract you asked for.

Does that ladder belong inside V2, or is it a third vector class beside tail sensitivity? And what is the largest prefix cutoff you would still call plausible enough to pin against?

·

Thanks for rerunning M6 and for separating the actual no-manifest result from the mutation. Your revised result matches the dataflow I had in mind: with provider-derived full-file identity on the expected side, a prefix-only implementation is a hard outage on the untouched revision-bound tree, not a silent acceptance.

I have implemented the follow-up.

I kept V1 unchanged and added V2 as a second pinned literal known-answer vector. V2 stays deliberately small and is focused on the representation dimensions V1 cannot exercise: multibyte UTF-8, mixed CRLF/LF, and preservation of the exact raw byte sequence.

I did not put the prefix-cutoff ladder inside V2, and I ended up making the distinction stronger than “third vector class.”

The split is now:

V1 -basic algorithm/object-format literal known answer
V2 -exact-byte representation adversarial literal known answer
tail ladder -deterministic metamorphic falsifier for prefix truncation / omitted suffix consumption

The ladder probes:

1 KiB -8 KiB -64 KiB -256 KiB

Each rung generates two equal-length inputs that share the entire prefix through the selected boundary and differ only in a distinctive tail beyond it. The same probes are applied to all three standalone content-identity implementations, under both git-blob-sha1 and lfs-sha256.

I deliberately did not pin a large table of ladder digests. The test-only reference calculator is itself checked against the pinned V1/V2 literals, and the generated tail pairs are then compared against that reference. So V1/V2 remain the concrete known-answer anchors, while the ladder acts as a bounded metamorphic falsifier for prefix truncation / omitted suffix consumption.

I also kept your logarithmic sizing idea but did not define a “largest plausible cutoff.” Every finite prefix-only cutoff is wrong under the contract. The 256 KiB top rung is only a finite diagnostic probe, not a semantic limit, runtime chunk size, or repository-size promise. I chose 256 KiB rather than something derived from the current 142,699-byte corpus maximum specifically so the fixture does not track mutable repository state.

The invariant remains:

identity is computed over the complete, exact raw byte sequence.

The finite probes are bounded falsifiers against known wrong implementation families; they do not define how much input an implementation is allowed to consume.

I also made the no-manifest boundary more explicit in the tooling documentation. Rather than treating assurance as a binary local/strict distinction, the documentation now separates three orthogonal dimensions: path authority, revision binding, and content-identity policy.

Ordinary local git ls-files operation remains a tracked working-tree-candidate check and makes no independent revision-qualified byte-identity claim; the webhook path is the strict revision-qualified profile that combines manifest authority, expected-revision binding, and mandatory content identity.

So I think the two findings now land in separate places:

  • M6 exposed a real conformance-test gap, now covered by the tail falsifier;
  • the no-manifest experiment exposed an assurance-posture boundary, now documented more explicitly rather than being turned into a self-derived local byte-identity check.

Thanks for pushing both far enough to distinguish those two.

Your ladder survived the attack I built for it, and it is stronger than your own docstring says.

I re-derived all four pinned literals independently before touching anything.

V1  36 bytes   blob-sha1  60e7e688...  OK    sha256  93f2590c...  OK
V2  88 bytes   blob-sha1  29819027...  OK    sha256  473015a1...  OK

A fourth confirmation, then. And V2's line endings are escapes inside a bytes literal rather than real CRLF in the source, so autocrlf on checkout cannot move that digest. That was the trap actually available here. The multibyte characters are literal, so the residual exposure is source re-encoding rather than line endings, and test_content_identity_vectors.py catches that one.

Full suite at 016765bf1 on a clean tree: 64/64 across five files (9 vectors, 8 probes, 9 bootstrap, 13 job checker, 25 observation authority).

The attack, and why it failed

The ladder looked blind to one family. make_tail_pair puts the difference deep in the tail, which is optimal against prefix truncation and maximally visible to anything that reads the tail at all. So I mutated all three implementations to a head+tail fingerprint, 64 KiB from each end, true length kept in the blob header, no-op below 128 KiB:

_M7 = lambda b: b if len(b) <= 131072 else b[:65536] + b[-65536:]

It reproduces V1 and V2 exactly. It reads neither the middle nor 67% of the top rung. I expected it through.

M7 vs shipped suite     3 FAIL of 64
caught at               boundary=262144, both identity kinds

Caught. And not by the metamorphic property: digest_a != digest_b holds fine for M7, because the differing bytes sit inside the tail it does read. What killed it is the second half of your test, where each payload is qualified against reference(payload) over the full bytes. That is a known-answer check at four input sizes, not a prefix falsifier, and it catches every non-full-consumption family rather than prefix-only ones.

So the docstring undersells the code. "No prefix-only shortcut observed at these sizes" is weaker than what the suite actually proves.

Where the edge actually is

Not 256 KiB. make_tail_pair builds boundary + boundary // 2, so the top rung feeds 393,216 bytes, and detection tracks payload length rather than the label.

elision engages above 142,699 B    CAUGHT      3 of 64
elision engages above 393,216 B    UNDETECTED  0 of 64
elision engages above 393,217 B    UNDETECTED  0 of 64

The edge is exactly the top rung's payload length. Against your corpus at that revision:

files                                     1,056
largest        142,699   indexes/REPOSITORY-MARKDOWN-INVENTORY.md
> 131,072                                     3
> 262,144                                     0
> 393,216                                     0

Headroom 2.76x, not the 1.5x you would read off the label. Every elision that could touch a byte in this repo has to engage at or below 142,699, and you cover to 393,216. On this corpus the ladder is a complete net for the family, not just a diagnostic.

The one thing still corpus-dependent

The headroom, not the fixture. You cut corpus dependence out of the fixtures deliberately and that was right. But nothing now notices if a path crosses 393,216 bytes and starts sailing over the top rung. Your three largest files are all index projections that grow with the corpus, and REPOSITORY-MARKDOWN-INVENTORY.md is the one nearest the edge.

The check that closes it is not a fixture, it is an assertion about the repo: no manifest path exceeds the largest probe payload. Fixtures stay corpus-free, and the headroom becomes checked rather than coincidental.

Would you take that assertion, or do you read a growing index projection crossing 384 KiB as the point where the ladder earns another rung instead?

·

Thanks, this was a useful attack, especially because it exposed that the fixed ladder was doing more than I had described.

You are right about what killed M7. The metamorphic half alone was not enough: digest_a != digest_b still held because your head+tail implementation consumed the differing tail. The failure came from the independent full-byte reference comparison on each generated payload.

I have updated the implementation and documentation around that distinction.

I kept V1, V2, and the fixed ladder unchanged:

V1 -literal algorithm/object-format KAT
V2 -literal representation-fidelity KAT
fixed ladder -stable bounded conformance/falsification evidence

The ladder is now described more accurately as combining two checks:

  • a metamorphic tail-sensitivity relation;
  • a full-reference comparison for each generated payload.

So it can falsify prefix truncation and other byte-elision strategies that produce a non-reference identity at those bounded probe sizes. I stopped short of describing that as a proof against every possible non-full-consumption implementation, because the fixture set is still finite.

You are also right that the top 256 KiB boundary produces a 393,216-byte payload, so the effective size of that particular fixed probe is 384 KiB rather than 256 KiB.

I did not, however, turn that number into either a repository limit or a permanent “add another rung here” threshold.

Instead I split stable diagnostic/conformance evidence from current live-domain size coverage.

I added a deterministic dynamic live-domain sentinel. The test measures the current maximum byte size among payloads covered by the live qualification surfaces of the three content-identity implementations — declared Markdown, generated projections, and the wrapper/checker source payloads themselves — and generates a synthetic sentinel at the next power of two strictly above that maximum.

That sentinel is then checked against the same independently anchored full-byte reference by all three independently maintained content-identity implementations, under both git-blob-sha1 and lfs-sha256.

This is a size-domain sentinel, not a representative sample of repository content.

So the structure is now:

unbounded contract
-identity is over the complete, exact raw byte sequence

stable evidence
-V1 / V2 / fixed 1-8-64-256 KiB ladder

current-domain size applicability evidence
-dynamically sized sentinel beyond the current live qualified-payload maximum

This means I did not add:

  • manifest path <= 393,216 as an admissibility rule;
  • a repository/runtime content-size ceiling;
  • a manual rule saying that crossing 384 KiB earns another fixed rung.

If the repository grows, the dynamic sentinel grows with it automatically. The fixed ladder remains corpus-independent and useful for stable diagnostics, while the live-domain size evidence follows the size domain whose coverage claim it is actually supporting.

The documentation also now explicitly says that neither the fixed ladder nor the dynamic sentinel proves correctness for arbitrary future input lengths. The contract is unbounded; the evidence is finite and scoped.

So I ended up taking a third route relative to your final question:

do not constrain the repository to the finite falsifier, and do not rely on remembering to extend the ladder when the corpus grows. Let a separate size-applicability probe scale with the live domain instead.

Thanks again — the M7 mutation was what made the distinction between the ladder's metamorphic role and its full-reference role visible enough to separate those two kinds of evidence cleanly.

The sentinel works, and it named itself when I attacked it.

Baseline first, at 5f717e9d: 121/121 across nine files.

Then I mutated all three implementations to prefix-truncate above 64 KiB. Caught, and the new file is one of the things that caught it:

test_content_identity_dynamic_live_domain_sentinel_matches_reference
  kind=git-blob-sha1 size=262144

Live max reads 142,699, so 1 << bit_length() lands on 262,144. The manual rung is gone and the size axis is closed.

The gap that is left is not size

Minimal pair. Same transform, one extra condition in front of it.

C2   replace(CRLF, LF)                          116/121   CAUGHT
M8   replace(CRLF, LF) if len(raw) > 88         121/121   clean

C2 fails in three places, all of them tests you wrote for exactly this: V2 representation fidelity in the bootstrap, in the wrapper, and in the observation authority. M8 is the same two lines with a size gate, and the whole suite passes.

The gate does not have to be 64 KiB. It has to reach 88.

gate     payloads above it    detected by
   0                    11    V2
  87                    10    V2
  88                     9    NOTHING
65536                    5    NOTHING

88 is len(VECTOR_V2_BYTES). Every newline-fidelity and multibyte-fidelity guarantee in the suite lives inside that one payload, and a gate one byte past it steps over all of them.

Second family, same cell

A chunked reader that decodes and re-encodes each chunk. Identity on pure ASCII at any length, identity below one chunk, wrong only where a multibyte sequence straddles a chunk boundary.

chunk B    detected by
      16   V2
      32   NOTHING
    4096   NOTHING
   65536   NOTHING

Both verified rather than argued: gate 88 is 121/121, chunk 4096 is 121/121.

Why they survive

payload             len   non-ASCII   CR   LF   distinct bytes
V1                   36           0    0    1               21
V2                   88          27    2    4               42
ladder 1 KiB       1536           0    0    0               28
ladder 256 KiB   393216           0    0    0               28
dynamic sentinel 262144           0    0    0               16

Every payload above 88 bytes is pure ASCII with no newline of either kind. So the suite exercises the large-input path only on ASCII, and the representation-sensitive path only at 36 and 88 bytes. A defect needing both is in neither cell.

The sentinel is also the narrowest payload in the suite, 16 distinct byte values against V2's 42, because hex. And it is the one that self-scales, so as the corpus grows the top probe gets narrower in byte class, not wider.

Your reason for hex is right about the constraint and maximal about the cost. census() requires valid UTF-8. Valid UTF-8 is not sixteen ASCII symbols.

What I am not claiming

Neither mutant corrupts anything you ship today.

tracked files over 64 KiB                  27
of those, containing CRLF                   0
of those, containing a non-ASCII byte      27
internal 64 KiB boundaries across them     30
continuation-byte density              0.2298%
expected straddles                      0.072
P(zero straddles)                         0.93

So the chunk family is silent right now at 93%. That is a coin, not a property, and it is the same shape as the headroom point from last round: the suite cannot see the transform, and the corpus happens not to trigger it.

The 27 of 27 is the line I would not leave alone. Every large file you ship is non-ASCII. Every large payload you test with is not.

The change

make_dynamic_probe already emits hex blocks. Threading a CRLF and a few 2/3/4-byte sequences through that stream keeps it valid UTF-8, keeps it deterministic, keeps the exact length, and moves the representation axis from 88 bytes to 262,144 and self-scaling from there. One payload, one reference comparison, no new rung.

Would you put the representation axis inside the dynamic probe? Or does folding two properties into one fixture cost you the diagnostic separation you just spent a round building, in which case the honest shape is a second sentinel rather than a wider one?

·

Yes, I think this attack found a real missing interaction cell rather than another weakness in either V2 or the size sentinel individually.

The suite had per-axis coverage on both dimensions:

representation fidelity -> V2
large-input consumption -> fixed ladder + dynamic size sentinel

but it did not have joint coverage of representation-sensitive behavior at large sizes. Your gated-CRLF mutant and chunked decode/re-encode mutant sit exactly in that empty interaction cell.

I kept the existing dynamic size sentinel unchanged and added a second dynamic representation-at-scale sentinel rather than folding the two responsibilities together.

The evidence split is now:

V1 -> literal algorithm/object-format KAT
V2 -> literal representation-fidelity KAT
fixed ladder -> stable bounded consumption/elision falsification
dynamic size sentinel -> current live-domain size applicability
dynamic representation-at-scale sentinel -> bounded representation × size × selected-boundary alignment interaction falsification

Both dynamic sentinels use the same live-domain-derived target size, so there is still only one size authority. At the current live maximum of 142,699 bytes, both are 262,144-byte probes.

The new probe is deterministic, exact-length, and valid UTF-8. It distributes mixed CRLF/bare-LF and recurring 2-, 3-, and 4-byte UTF-8 sequences across the payload rather than concentrating the representation-sensitive material in a small prefix.

It also deliberately places multibyte sequences across a bounded set of selected power-of-two internal boundaries. Those are adversarial test points, not a claim that an implementation uses those chunk sizes, and not a claim of arbitrary chunk-boundary coverage.

The probe generator itself is tested for the properties it claims: exact length, UTF-8 validity, line-ending diversity, 2/3/4-byte sequences, distribution across early/middle/late regions, and actual straddling at each selected boundary.

The new sentinel is then compared against the same independently anchored full-byte reference under all three independently maintained content-identity implementations and both git-blob-sha1 and lfs-sha256.

I also reran the two failure families you demonstrated.

For the gated CRLF transform:

replace(CRLF, LF) if len(raw) > 88

I now get the diagnostic shape I wanted:

V2 -> PASS
dynamic size sentinel -> PASS
representation-at-scale sentinel -> FAIL

The 4096-byte chunked decode/re-encode mutant produces the same separation:

V2 -> PASS
dynamic size sentinel -> PASS
representation-at-scale sentinel -> FAIL

So the new test is not just making the suite red somewhere; it localizes the missing evidence to the representation × scale/alignment interaction.

I deliberately stopped there.

I did not add arbitrary chunk-size enumeration, NFC/NFKC cases, BOM stripping, whitespace normalization, or a Cartesian product of every representation factor. The documentation now says explicitly that the selected boundaries and representation classes are finite adversarial evidence, not a proof over arbitrary future transforms or chunk boundaries.

So the contract remains:

identity is over the complete, exact raw byte sequence.

The evidence remains finite and factorized:

stable literal anchors + stable bounded falsifiers + live-domain size evidence + one independently diagnosable interaction sentinel.

Your 27/27 observation was the part that convinced me this was worth adding: every large tracked file currently contains non-ASCII bytes, while every large synthetic identity probe had been ASCII-only. That was a real mismatch between the live domain and the joint evidence surface, not merely a hypothetical mutant.

Thanks, this one moved the design from per-axis coverage to explicit interaction evidence without turning the size sentinel into a mega-fixture.

Both separations reproduce, and the new probe has exactly one CRLF in it.

I re-ran your two failure families against e8688aee before anything else.

mutant                                 caught by
replace(CRLF, LF) if len(raw) > 88     representation-at-scale sentinel, nothing else
chunked decode/re-encode, 4096         representation-at-scale sentinel, nothing else

So the diagnostic separation is real, and the second sentinel was the right call over a wider one.

The probe as built

make_dynamic_representation_probe(262144)
  exact length            yes
  valid UTF-8             yes
  4-byte straddles        14 of 14 selected boundaries, all present
  2-byte sequences         3
  3-byte sequences         3
  CRLF                     1     at offset 229376

One CRLF, in the last eighth.

place() returns silently on overlap, and the boundary markers go first by design so the region markers yield to them. At size = 262144, size // 8 = 32768 and size // 2 = 131072 are themselves selected boundaries already holding a 4-byte sequence at boundary - 2. So place(32768, b"\r\n") and place(131072, b"\r\n") both overlap and both return.

The live sentinel size is 1 << bit_length(), so it is always a power of two, so size // 8 and size // 2 are always in representation_probe_boundaries(size). This is not an edge case. It is the only case.

Why the generator test cannot see it

has_representation_material() is an OR over CRLF, 2-byte, 3-byte and 4-byte. The 4-byte boundary markers are in every region by construction, so the early and middle assertions pass on those alone and the CRLF skew is invisible.

Per third, at 262144:

region   CRLF   4-byte   2-byte
early       0       13        1
middle      0        1        1
late        1        0        1

The 4-byte axis is skewed the other way for the same reason. Powers of two below N pile up at the bottom, so 13 of 14 straddles are in the first third and none are in the last.

What it costs, verified

mutant                                                       caught by
if len(raw) > 88: normalise CRLF in raw[:65536] only         NOTHING
if len(raw) > 88: normalise CRLF in the first 7/8 of raw     NOTHING
if len(raw) > 88: normalise CRLF in the LAST 1/8 of raw      representation-at-scale sentinel

A prefix-scoped, size-gated newline normaliser is last round's M8 with one more condition in front of it, and the new sentinel does not see it. It sees the suffix-scoped one.

The change is three bytes

Nudge the two colliding relative positions off the powers of two:

for relative_position in (size // 8 + 3, size // 2 + 3, size - size // 8):
              CRLF total   per third    14 straddles   prefix-scoped mutant
as shipped             1   [0, 0, 1]    intact         escapes
with + 3               3   [1, 1, 1]    intact         caught

Exact length and UTF-8 validity unchanged, every boundary straddle still present. I ran it rather than argued it.

I would also make the distribution test per-class instead of an OR, since the OR is what let the skew through, and a per-class version would have caught this before I did.

Your chunk axis holds, and your scope note is honest about where it stops. Chunk 3000 escapes everything, which is exactly what you said it would do.

One question

The overlap guard skips silently. That is why the CRLF vanished, and it is the same shape as the size gate that started this whole line: a condition in front of a transform, doing nothing visible.

Would you rather it raise, so a collision is a build failure instead of a silent coverage loss? Or does "representation coverage across regions, not a promise that every specific marker exists" mean you want it to keep yielding, in which case the honest fix is to assert per-class per-region and let that assertion be the promise?

·

Yes, I ended up taking both.

The overlap is now a construction failure rather than a silent yield, and the fixture tests now assert the representation classes independently per region instead of collapsing them through the old OR predicate.

I also took your +3 adjustment for the two colliding CRLF positions. At the current 262,144-byte live-domain-derived probe size, that gives CRLF in all three regions while preserving all 14 selected boundary straddles.

There was one more version of the same problem hiding underneath that: the regional 4-byte markers and the selected-boundary 4-byte markers were initially the same code point, so a missing regional marker could still be masked by a boundary marker in the same region.

I split those as well:

regional 4-byte marker -> 🧪
selected-boundary straddle marker -> 😀

so the two claims are now independently observable rather than merely independently placed.

The generator contract is now effectively:

required placement collision -> construction error
regional representation -> CRLF / bare LF / 2-byte / 3-byte / 4-byte independently present in early, middle, and late regions
selected-boundary alignment -> 4-byte UTF-8 straddles every selected power-of-two boundary

I added a direct collision regression as well, rather than relying only on a too-small probe failing out of bounds. The probe fixture suite now passes 26/26, and removing only the regional 4-byte placements now correctly breaks the regional-distribution test instead of being masked by the alignment markers.

So I think your diagnosis was right: the problem was not another missing sentinel. It was that the sentinel generator could silently fail to realize part of the evidence surface it claimed to construct, and its own validation was coarser than that claim.

The useful rule I am taking from this round is:

For required test evidence, construction should fail closed, and fixture validation should be at least as fine-grained as the evidence claim it supports.

Thanks, this was a good catch. The second sentinel still keeps the diagnostic separation we wanted, but required evidence can no longer disappear silently during fixture construction.

Fail closed on construction is the right half. The half I would test next is whether the validation is actually as fine-grained as the claim now says it is.

Your contract has three clauses, and the middle one is the one that grew teeth:

regional representation -> CRLF / bare LF / 2-byte / 3-byte / 4-byte
                           independently present in early, middle, late

That is five classes across three regions. Fifteen required placements. You verified one class by removal, the regional 4-byte, and it correctly broke the regional-distribution test rather than being masked by the alignment markers. Splitting 🧪 from 😀 is what made that observable, and it is a better fix than the one I proposed, because it separates the claims rather than just separating the positions.

So the question is coverage of the removal test, not coverage of the fixture. And notice what the removal you ran actually proves. You took out the regional 4-byte placements together, which is three of the fifteen going out at once. That shows the class is load-bearing. It does not yet show that early, middle and late are each independently load-bearing, because one surviving region can carry the assertion for the other two, and that is precisely the failure you just fixed one level down.

The sharper version is one placement at a time. Does each of the fifteen break a test, and does each break a different one? If two can be removed and only one assertion goes red, the contract still reads finer than it verifies, and that is the same shape as the OR predicate you just retired.

I ran the generalized version of this today on an unrelated package, a control-plane evidence release with a four-test suite and a signed hash manifest. Seven mutants, each deleting one guarantee its own docs claim. Four survived with a green suite and every published number unchanged. Two reasons, both structural rather than sloppy: one field was written as a literal False at construction and never computed, so its counter could not move, and the headline metric was a set difference between two arms, so any failure that hit both arms equally cost nothing.

The second one is the one I keep meeting. A differential metric is blind to any fault that moves both arms the same way, by definition. Your per-region assertion is not differential, which is why I think it will hold. But the masking you found underneath it is the same family one level down: a single code point was satisfying two independent claims at once, so a failure of the regional claim got absorbed by the boundary claim. Evidence that is shared between two claims cannot discriminate between them.

Which brings the rule you stated somewhere sharper, if you want it: fixture validation should be at least as fine-grained as the claim, and the evidence that it is should be a red run, not a reading of the assertion.

So: have you run the removals one placement at a time rather than one class at a time, and did any single removal come back green against 26/26?

·

Yes, I took the sharper version and turned it into an explicit first-order mutation test.

The regional claim is now represented as exactly fifteen pinned cells:

5 representation classes × 3 regions

with the exact ordered set itself asserted separately, so the mutation matrix cannot silently shrink from 15/15 to 12/12 if a class disappears from the enumeration.

For each cell, one at a time, the test:

  1. starts from a genuine generated probe;
  2. locates that marker from the actual emitted bytes rather than re-deriving the generator's placement arithmetic;
  3. overwrites only that marker with the corresponding bytes from the plain ASCII dynamic probe;
  4. checks that length and UTF-8 validity are unchanged;
  5. runs the per-class/per-region validator;
  6. requires the validator to report exactly that one cell as missing.

So the acceptance condition is stronger than “some assertion went red”:

remove(cell_i) -> missing == [cell_i]

for all fifteen cells independently.

I also pinned the fifteen-cell inventory itself separately. That closes a smaller self-reference trap I noticed while implementing your suggestion: if the mutation test merely iterated whatever cells the validator currently knew about, deleting a class from both sides could reduce the matrix to 12/12 and still look healthy.

The current result is:

15 mutants
15 detected
15 correctly localized
0 survivors
0 mislocalized
0 impure mutants

The mutation harness does not reuse the generator's placement formulas. It scans the actual generated payload for the claimed marker in the claimed region, then removes that observed marker. That keeps “where the generator says it wrote something” separate from “what the fixture actually contains.”

So this is now red-run evidence at the same resolution as the regional claim, rather than an inference from reading the validator.

I also kept the scope narrow. This is first-order single-placement falsification of the fifteen regional claim cells, not pairwise or higher-order combinatorial mutation coverage. I do not think fifteen separate named test functions are the important property here; detection and localization at the fifteen-cell claim surface are.

Your unrelated-package example helped sharpen why.

A differential metric can be perfectly implemented and still have a common-mode fault direction that leaves the published value unchanged. The earlier shared 😀 witness had the same structural shape at a smaller scale: two claims intended to be independently falsifiable collapsed onto one observation, so one claim could disappear without changing what the validator saw.

The failure mode in both cases is not simply “insufficient test count.” It is an observationally null fault direction: a claim-relevant change exists that the evidence mapping does not distinguish.

Splitting 🧪 from 😀 removed one such ambiguity between the regional and alignment claims. The fifteen one-at-a-time removals now test the next level down: whether each declared regional cell is itself distinguishable under the selected single-removal fault family.

So I think the rule sharpens beyond just matching assertion structure:

A claim is only as fine-grained as the smallest claim-relevant fault its evidence system can distinguish.

Or operationally:

claim granularity, validation granularity, and falsification granularity should agree at the surface being asserted.

That still does not imply exhaustive combinatorial coverage. The fifteen removals establish first-order sensitivity for the fifteen declared cells; they do not prove arbitrary two-cell interactions, substitutions, relocations, or unrelated representation transforms.

Thanks — this was a good next test because it moved the question from “is the validator written at the claimed resolution?” to “can the evidence system actually observe a fault at that resolution?”

That is a much stronger distinction.

I was wrong about the drift, and your own harness is what showed me.

My worry was that multiplicity would creep in as the probe grew, because a one at a time removal has power exactly where a cell has one instance. _locate_regional_cell does region_bytes.find(marker_bytes), and _regional_representation_failures needs the count to reach zero. So detection requires count == 1.

I measured it instead of assuming. Your generator, seven sizes:

size        cells with exactly 1 instance
1,024                 15 / 15
4,096                 15 / 15
16,384                15 / 15
65,536                15 / 15
262,144               15 / 15
1,048,576             15 / 15
4,194,304             15 / 15

Singletons everywhere, and not by luck. make_dynamic_probe emits 0123456789abcdef and nothing else at every size I tried. That alphabet is disjoint from all five marker byte sets, so filler can never accidentally mint a CR, an LF, an é, a or a 🧪. Accidental recurrence is not merely unlikely here, it is impossible by construction. Your 15 of 15 is sound, and it is sound for a better reason than the fixture happening to be small.

The residue is that the property holding it up is asserted in prose. It lives in _make_single_cell_mutant's docstring, "always plain ASCII hex digits (0-9a-f)". The nearest thing to a test is len(set(payload)) > 1, which passes on any alphabet of two or more bytes.

So I ran your suite under a maintenance edit a reasonable person could make: keep the counter-indexed hexdigest construction, wrap it to 64 columns so the fixture diffs nicely in an editor.

exact requested length     still true
valid UTF-8                still true
len(set(payload)) > 1      still true (17 distinct bytes)

28 tests in the module     1 red
impure mutants             none
mislocalized               none
survivors                  bare-LF@early, bare-LF@middle, bare-LF@late

Your purity net, the one built to catch a second fault injected by the harness itself, stays green. Localization stays green. One test goes red, and it reports:

survivors (removed but undetected)

The validator was never undetecting anything. The fixture grew extra bare LFs, the removal took one of many, and the claim became over-witnessed. Opposite cause from what the label names, opposite fix.

So it is fail loud, not fail silent, and I should have said that. The cost is a wrong first hypothesis at 2am, on the one test whose whole job is telling you which cell broke.

The instance count fixes it for one integer per cell. bare-LF@early: removed 1 of 4096 reads as a fixture change instantly. removed 1 of 1 sends you to the validator, correctly.

Does anything currently pin make_dynamic_probe's alphabet, or is the hex property load-bearing for the mutation matrix in a place no test is watching?

·

Thanks, I think your reclassification is exactly right, and the measurement made the reason stronger than either of us had stated before.

The singleton property was not drifting with size. The current filler is plain hex and disjoint from all five representation-marker classes, so its growth cannot accidentally mint another CRLF, bare LF, or selected multibyte sequence.

The remaining issue was one layer higher: that property was load-bearing for the mutation experiment, but the harness was relying on it rather than independently proving that the experiment it was about to interpret was actually a single-cell removal.

I tightened that boundary.

The harness now checks the baseline witness counts before constructing any validator claim: every one of the 15 regional cells must have exactly one witness. For each mutant, it then verifies that the target changed from exactly 1 -> 0 and that every non-target cell count remained unchanged. Only after those checks pass is the validator allowed to produce a survivor or localization result.

I also pinned the 64-column wrapping case directly. A formatting change that introduces additional bare LFs now fails as a fixture precondition failure before the validator is evaluated, rather than surfacing as “removed but undetected.”

So the distinction is now explicit:

removed 1 of N, where N > 1
→ the falsifier itself is not admissible for that claim

removed 1 of 1, target becomes 0, everything else stays fixed
→ now the validator can legitimately be implicated if it misses the removal

I think there is a useful general rule hiding in this one:

Qualify the intervention before attributing the outcome.

A detector can only be blamed after the harness has independently established that the condition it was supposed to detect was actually instantiated. Otherwise a red test may still be useful, but its failure attribution is unsound.

That also sharpens the point you made about this being fail-loud rather than fail-silent. The suite was already telling us that something had gone wrong; what it did not yet know was which layer had gone wrong.

Thanks for going back and measuring the original drift hypothesis rather than carrying it forward. The correction itself exposed the more interesting issue.

Ran it at 31bc717. 31 green, up from 28, and the 64-column edit is now a named test with the presence-check negative control sitting inside it. test_64_column_wrapped_filler_fails_the_singleton_precondition_not_the_validator asserts both halves: the singleton precondition fires, and _regional_representation_failures() stays green on the same bytes. That second assertion is the one that makes it a regression test rather than a passing test.

You did answer the alphabet question, in test_dynamic_probe_filler_is_marker_free. So I went and measured which of its five checks actually does the work, across filler edits a maintainer might plausibly make.

                                isascii  no CR  no LF  no multibyte  hex-subset
today's filler (hex)              pass    pass   pass     pass          pass
wrapped every 64 cols             pass    pass  FIRES     pass         FIRES
tab-separated for readability     pass    pass   pass     pass         FIRES
NUL padding appended              pass    pass   pass     pass         FIRES
swapped to a base64 alphabet      pass    pass   pass     pass         FIRES
swapped to uppercase hex          pass    pass   pass     pass         FIRES
an 'é' slips in                  FIRES    pass   pass    FIRES         FIRES

hex-subset fires on 6 of the 6 changed fillers. The four checks the docstring calls load-bearing fire on 2 of 6 between them.

And the multibyte block cannot fire alone. Every one of those four markers is made of bytes at or above 0x80, so a marker present implies isascii() already False. It is strictly implied by the line above it. There is no filler that trips it and not isascii().

Which inverts the labels. The docstring says the CR/LF/multibyte checks are "the load-bearing property this test exists for" and that hex-subset is "implementation-choice-level. Not the load-bearing property above." Measured the other way round: the named-marker checks catch only classes already in the matrix, which are the ones you already know about. The alphabet check is the only line that catches a class you have not added yet.

The concrete case is tab. Suppose a representation class gets added that is an ASCII control byte, tab being the obvious neighbour to CRLF. A filler that grew tabs stays pure ASCII, has no CR, no LF, no multibyte marker, and passes four of the five checks. Only the one flagged optional fires. Row 3 above is exactly that payload.

The singleton precondition does not cover the gap either, and correctly so. _regional_representation_counts() counts the fifteen cells that exist. A contaminating byte that is not yet a cell is not something it can count.

None of this is a bug. The suite is green and it is right today. It is about which line survives a refactor, and the docstring currently tells a maintainer that the strongest line is the droppable one. Cheapest repair is a comment swap, not code.

Your rule generalizes further than the harness, by the way. Qualify the intervention before attributing the outcome is the same move as: check the fixture before blaming the detector. Both are refusing to read an outcome until you have established the condition was instantiated. That is also what my table is doing to your docstring, on a much smaller scale.

Is an ASCII control class ever a candidate for the matrix, tab in particular? If it is, the filler check and the cell list have to move together, and only the alphabet line would have caught the filler half on its own.