docir — design documents Graph

Documents / Issues / issue-ec3819b1f13c

A code glob fingerprints gitignored build output, so a rebuild reports drift

Every file under a code: glob is hashed, ignored ones included, so one compile drifts every document governing a source tree that holds build output.

issue-ec3819b1f13cissueresolved#cli#integrity
View as Markdown◉ View in graph

What is wrong

RepositoryCodeMatcher fingerprints every file under a code: glob, including the ones .gitignore excludes. Touching only an ignored file moves the digest, so docir check reports code-drifted on a document whose governed source has not changed.

In a repository whose build writes output beside its sources — bin/ and obj/ for .NET, and the same shape for target/, dist/, build/ — one compile drifts every src/**-style glob at once.

_SKIPPED_DIRS already states the principle this misses: a directory that "moves differently on a teammate's machine" would "report drift against a baseline they never diverged from". Every entry in it is Python or git, and .gitignore is the repository's own statement of which the rest are.

Measured

Reported against 0.27.0 from a .NET repository where five documents reported code-drifted after a build in which no tracked file under any of their globs had changed. Reproduced on a scratch store: src/** with bin/ ignored, one write to src/bin/out.txt, git status clean, code-drifted on the document.

It was also live in docir's own corpus. Of the 116 distinct code: patterns here, one digest moves when ignored files are excluded: benchmarks/**, and what moves it is benchmarks/.coverage, rewritten by every test run under coverage. __pycache__ below it was already skipped; .coverage is a file, and a skip set made of directory names cannot reach it.

Why it matters

The finding is indistinguishable from a real one, so the queue stops being readable: the correct response to a false code-drifted is to re-read a document that nothing changed, and --verified clears it only until the next build.

The ! half, and a correction to the report

The report says a !-prefixed glob is "accepted and silently ignored", and that no unmatched-code is reported for it. The first half is right — code: globs are pathlib patterns, where ! is a literal — and the second is not: check does report unmatched-code on '!src/bin/**', verified on 0.27.0.

It is still a poor answer. The write is accepted, and the signal arrives later, as "matches nothing" rather than "exclusions are not written this way".

What would fix it

Consult the repository's own .gitignore files in the matcher, and refuse a ! entry at Tier 0 naming that mechanism. The reasoning, the alternatives refused and what upgrading costs are recorded as A code glob hashes what the repository tracks, not what it builds.

Reported

GitHub issue #20, against 0.27.0, reproduced with and without the daemon.

Resolution

FIXED 2026-09-18. RepositoryCodeMatcher consults the repository's own .gitignore files beside the hardcoded skip set: an ignored path is neither hashed into a glob's digest nor counted as a match. The reasoning, the alternatives refused and what upgrading costs are A code glob hashes what the repository tracks, not what it builds.

The reported reproduction, re-run against the fix — src/** with bin/ ignored:

text
write src/bin/out.txt (ignored)   no code finding
write src/keep.txt    (tracked)   code-drifted

The second line is what makes the first mean something: a matcher that ignored everything would pass the first and detect nothing.

On this repository, live. benchmarks/** was the one pattern of 116 whose digest moved, and benchmarks/.coverage was moving it on every test run under coverage. It is stable across a coverage run now, measured before and after. The document that governs it (issue-c6d184704682) stays code-drifted until somebody reads it, because its baseline was taken by the old algorithm — one document, which is what the upgrade costs here.

The ! half is refused at Tier 0, and the message names what replaces it rather than saying "unusable" — the author wrote an exclusion, and "matches nothing" is an answer to a question they did not ask. Patterns an older build already wrote are untouched: Tier 0 validates what a write sets, so an unrelated --set-owner on such a document still succeeds, verified against one the 0.27.0 wheel wrote.

Ten injections, each proven to fail its guard. Three against the matcher — dropping the ignore consultation, ignoring everything, dropping the always-skipped floor — and seven against the engine: no ancestor walk, first-match-wins inside a file, nothing anchored, ** as a literal segment, negation dropped, the shallowest ignore file winning, and an unreadable ignore file read as "exclude everything". The second matcher injection matters as much as the first: the cheap wrong fix here excludes too much, and a test suite that only checks "the build no longer drifts" passes on an engine that has stopped watching the code.

The grammar is parsed here rather than taken from a library, and it is checked against git check-ignore rather than against a reading of gitignore(5) — ten rule sets and forty paths, run twice, once as a recorded table and once re-derived from git. Doing that caught a real error in the first attempt, which used pathspec: a file under an excluded directory cannot be re-included, and a per-file matcher says it can.

Cross-build, measured on one store at one moment: this build reports nothing where 0.27.0 reports code-drifted. Neither refuses anything and doctor --strict exits 0 on both, so it clears Verify a change against the published build, not just this one; the ADR records it as the cost of the older build over-reporting until everyone upgrades.

To amend: Re-verify: