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?