docir — design documents Graph

Documents / Issues / issue-325742d96896

docir query --code finds nothing for a dot-prefixed path, so the CI notice step is blind to workflow changes

_normalize strips a leading dot as a character class, so a document governing .github/workflows/** can never be returned by the reverse query — while check's own matcher, which does not share the function, reports its drift correctly.

issue-325742d96896issueresolved#cli#retrieval
View as Markdown◉ View in graph

What is wrong

code_globs._normalize is one line:

python
return path.strip().lstrip("./").strip("/")

str.lstrip takes a character set, not a prefix. It is there to drop a leading ./, and it does — but it also eats the leading dot of any dotted path:

text
_normalize(".github/workflows/ci.yml") == "github/workflows/ci.yml"

The pattern side is not normalized the same way (_compiled strips / only), so the regex still expects the dot and the two can never meet:

text
matches(".github/workflows/ci.yml", ".github/workflows/ci.yml")  -> False
matches(".github/workflows/**",     ".github/workflows/ci.yml")  -> False
matches("docs/tags.yaml",           "docs/tags.yaml")            -> True

What it costs

Four documents in this store declare a workflow glob and none of them can be found from the code they govern: adr-b2cfed9d5888, issue-82b01d7f80d0, issue-b323a5b2ba18 and run-30aceb4eacc6.

text
docir query --include-inactive --code .github/workflows/ci.yml   # []
docir query --include-inactive --code src/docir/entry_points/cli/app.py   # 7

The live cost is in this repository's own CI. The "Decisions this branch touches" step runs docir query --code over the changed files, so a pull request that edits a workflow is told no decision governs it. The document written to be found exactly there — adr-b2cfed9d5888, which governs .github/workflows/ci.yml because the decision it records is enforced by a CI step — is the one the step cannot return. A notice that answers "nothing" is indistinguishable from one that has nothing to say.

.docir/** and every other dotted path are affected identically.

Why nothing reported it

There are two matchers over one grammar. unmatched-code, code-drifted and code-changed resolve patterns through platform/filesystem/code_matcher.py, which walks the tree with Path.glob and never calls _normalize — so check reports those four documents' drift correctly, and only the query is blind. The split is why the defect survived: every guard that would have caught it is on the other matcher, and the query's own tests use src/ paths.

The rule this breaks is the one in the read-path note — --code matches globs as text, before the limit, never by walking the tree — which is right, and is exactly why the text path needs the grammar the walking path gets for free.

The fix

Strip the prefix as a prefix, not as a character class, and run the pattern and the path through the same normalizer so the two sides cannot disagree again.

Verify by injection: restore the lstrip and assert the four documents above are no longer returned for .github/workflows/ci.yml. A count alone will not do it — assert which ids come back, or the guard cannot tell "nothing governs this" from "the matcher is blind".

Resolution

FIXED 2026-09-18. _normalize now strips a leading ./ as a prefix rather than as a character class, and both sides go through it: matches normalizes the pattern as well as the path, so the two cannot drift apart again, and _compiled is cached on the one spelling rather than on however a pattern was typed.

Against this store, .github/workflows/ci.yml now returns the seven documents that govern it, where it returned none.

Guarded in two places, each proven by restoring the lstrip. The grammar table in test_domain_services.py gains the dotted cases, including src/auth/** against .src/auth/login.py, which must stay false — the dot is part of the name, not noise to be stripped, and a fix that over-stripped would pass every other case. TestQueryByPath asserts which id comes back for a dotted path, because an empty list cannot distinguish "nothing governs this" from "the matcher is blind". A third injection — normalizing the path but not the pattern — is caught by the same table.

The forward check is untouched. RepositoryCodeMatcher still answers "does this pattern still name anything" by walking the tree, which handled a dotted directory correctly all along. Only the grammars had to agree, and the shared normalizer is what makes that structural rather than remembered.

To amend: Re-verify: