Repository navigation
Description
Follow-up to #82, which made the offline gate check absolute self-links in Markdown.
The gap #82 left
#82's own motivating example is still unchecked. Its text says:
a URL in a docstring rendered on PyPI has no checkout to be relative to, which is precisely how the
faststream-concurrent-aiokafkainstance arose. So the gate still needs to cover what remains.
The --remap fix covers what remains in Markdown. The gate's input glob is unchanged at '**/*.md', so nothing in .py is scanned — including the instance that motivated the issue.
Widening the glob would not reach it either. The URL is split across two adjacent string literals:
"https://github.com/modern-python/faststream-concurrent-aiokafka/blob/main/docs/adr/"
"0003-control-signals-not-honoured.md."A text-level checker sees a directory that exists and a filename fragment that is not a link, so it would report green while the interesting half goes unverified. This is the one that is user-visible on the package page.
It is wider than that one URL
Every repo's AGENTS.md already states the limitation as a known fact:
A relative link to an ADR is checked — CI runs lychee
--offlineover every.md— but a path named in a docstring or a comment is not.
Measured across all non-archived repos at HEAD, by extracting docs/adr/NNNN-*.md from tracked *.py and testing each against the working tree:
| Repo | References |
|---|---|
compose2pod |
5 |
db-retry |
1 |
faststream-concurrent-aiokafka |
1 |
modern-di |
1 |
modern-di-arq |
1 |
modern-di-starlette |
1 |
modern-di-typer |
1 |
.github |
2 |
13 ADR paths named in Python source across 8 repos, all resolving today, none checked by anything. Plus the one absolute URL above. Nothing is broken right now — which is exactly the position #82 described before the planning/ migration broke seven links at once.
These are load-bearing: INVARIANT: docstrings cite the ADR that explains why the invariant exists, so a renamed or pruned ADR silently strips the rationale from the test that depends on it.
Options, none costed yet
- Widen the glob to
'**/*.py'. Cheapest, and lychee extracts bare URLs from plain text. Catches nothing today: the only.pyURL is the split one, and baredocs/adr/...paths in prose are not links lychee resolves. Close to vacuous on its own. - Join the split literal, then widen the glob. Makes the PyPI-visible URL checkable by (1). Small, targeted, and fixes the specific instance The offline link gate never checks absolute self-links #82 cared about.
- A test per repo asserting cited ADR paths exist. Covers all 13 and the URL, in the repos' own idiom (
semvertag/tests/_descriptor_gate.pyis the precedent for a config-shape test). Cost: a test copied into 8 repos, which is the machinery The offline link gate never checks absolute self-links #82 avoided needing. - Accept and document. The
AGENTS.mdsentence already says it; make that the settled answer and close this.
(1) and (2) compose. (3) is the only one that covers the bare-path references.
Scope
The 8 repos above, or fewer depending on which option wins. Deciding the option is the work here; the edit is small either way.
Activity
lesnik512 commentedon Sep 7, 2026
Agent Brief
Category: enhancement
Summary: Add a per-repo test asserting every ADR path cited in Python source resolves, in all 26 non-template repos
Decision: option (3), a test per repo. Options (1), (2) and (4) are rejected below with measurements.
Why (1) and (2) lost
Both were measured against lychee 0.24.2, the version
lychee-action@v2pins, with the remap block exactly as it shipped in #82.Widening the glob to
'**/*.py'is not "close to vacuous". It is exactly vacuous.Across all 26 repos carrying the gate, widening the glob changes the checked-link count by +1, in one repo.
lychee --dumpover every Python file infaststream-concurrent-aiokafkaextracts exactly one link:The directory. Renaming the ADR out from under it leaves the gate green. Bare
docs/adr/…paths in prose are not extracted at all: a synthetic file citingdocs/adr/9999-does-not-exist.mdproduces zero errors.Widening the glob turns
db-retryred today, and no flag fixes it.Nine errors, from PostgreSQL DSN strings in its source and tests:
The failure happens at URL construction, before any filtering, so it cannot be suppressed:
--scheme https,--exclude 'postgresql',--exclude '^postgresql'and--exclude 'postgresql.*'each still report 9 errors. The only escape is--exclude-pathnaming those files, which puts a repo-specific parameter back into the gate block and revives exactly the drift argument #82's resolution dissolved. Option (2) inherits this, since it needs the same widened glob.Option (2)'s source edit is unnecessary. Python concatenates adjacent string literals at parse time, so an
astwalk already sees the joined URL that no text-level tool can:Option (2)'s only unique contribution is therefore free under (3), and its coverage is otherwise a strict subset.
Why not (4). The
AGENTS.mdsentence option (4) rests on no longer exists: it was removed from every repo in the #85 sweep, so there is now no written record of the gap at all. Even while it stood, it documented the gap without holding it closed. These citations are load-bearing: anINVARIANT:docstring names the ADR carrying the rationale, so a renamed or pruned ADR silently strips the reason from the test that depends on it, with nothing going red.Current behavior
Nothing verifies a
docs/adr/NNNN-<slug>.mdpath named in Python source. The offline link gate reads Markdown only, and its--remapcovers absolute self-links in Markdown.Measured across every non-archived repo at
HEAD: 14 citation sites in Python across 8 repos (compose2pod5,.github2,faststream-concurrent-aiokafka2, and one each indb-retry,modern-di,modern-di-arq,modern-di-starlette,modern-di-typer). All 14 resolve today. None is checked by anything.Desired behavior
Each in-scope repo carries a test that fails when a
docs/adr/NNNN-<slug>.mdpath cited anywhere in its own tracked Python source does not resolve against the repo root. The failure message names every unresolved path together with the file citing it, so one run reports the whole blast radius of a rename rather than the first casualty.A repo with no citations, or with no
docs/adr/directory, passes. That is the normal state for most repos and must not be an error or a skip.Key interfaces
Citation pattern.
docs/adr/followed by four digits, a hyphen, a lowercase-and-hyphen slug, and.md. Repo-root-relative; resolution is a filesystem check against the repo root, never a network call.Two extraction sources, unioned. Neither alone is sufficient, and both gaps are live today:
nackguard message infaststream-concurrent-aiokafka, where the directory and the filename sit in separate literals: the instance The offline link gate never checks absolute self-links #82 cited and left uncovered.#comments, which the AST discards.compose2podhas one.Taking either source alone drops one real citation. The union is the contract.
Input set. The repo's own tracked Python files. Vendored, generated, and virtualenv trees are not the repo's citations and must not be scanned.
Self-verification. The scanner must be exercised against a fixture whose expected result is known, including at least one deliberately unresolvable citation, so a regex that silently stops matching fails the suite rather than passing empty. The idiom to follow is
semvertag's descriptor gate: a validator module shared between the CI step and its own regression tests, precisely so the gate and its tests cannot drift.Test shape. Per
AGENTS.md: the test name is the claim, the docstring opensINVARIANT:, and a second paragraph names what breaks it: the design rationale, not a report of what this test catches.Acceptance criteria
HEAD(all 14 current citations resolve, so no repo goes red on adoption)faststream-concurrent-aiokafkanackguard message, which a text-level scan cannot see#comment is caught, verified against thecompose2podinstance, which an AST-only scan cannot seedocs/adr/directory pass; neither reports a skipmypyandpyreflyadditions inthat-dependsScope
In scope: 26 repos. Every non-archived repo in the org except
fastapi-sqlalchemy-templateandlitestar-sqlalchemy-template.The templates are excluded following #66, which kept the link gate out of them: a template's test suite is copied into downstream user projects, where an org-internal ADR-citation check is noise a user has to delete. Neither template has a
docs/adr/directory, so nothing is lost.that-dependshas nodocs/adr/directory either and is still in scope. So is every repo with zero citations. The test is deliberately inert where there is nothing to check. A citation added tomorrow is covered from the moment it lands, which is the whole point of a rename guard.Out of scope
'**/*.md'. Measured above: no coverage gained,db-retrybroken, no flag to fix it.faststream-concurrent-aiokafkanackguard. The AST reads it as it stands.--exclude-path,--schemeor--excludeto any repo's gate block. The block stays byte-identical across repos; The offline link gate never checks absolute self-links #82's resolution depends on it holding no parameter to copy wrong.One correction to the record
Both #82 and this issue describe the
faststream-concurrent-aiokafkaURL as user-visible on the package page. It is not. It lives in a runtime guard-message dict and surfaces in a traceback when a user callsmessage.nack()under the middleware. What appears on the PyPI page isREADME.md, which cites the same ADR relatively and is therefore already covered by the existing gate.The URL is still load-bearing, since a user who trips the guard gets a link they are meant to follow, so it belongs in the covered set either way. But the "package page" framing overstated the urgency in a way that argued for option (2), and option (2) is the one the measurements rule out.
lesnik512 commentedon Sep 7, 2026
The
AGENTS.mdsentence this issue quotes no longer exists#85's sweep removed it. The longer form quoted in the body:
existed in exactly two files,
modern-di-typer/AGENTS.mdand.github/AGENTS.md, and both aregone. The shorter
Nothing enforces that docstring shape; it is read at review time.was removedfrom 21 further repos.
The outcome does not change. Option (3) won on measurement, not on that sentence. But two things
here now cite a file that no longer says what they quote:
AGENTS.mdalready states the limitation as a known fact" is no longertrue. I have not edited the body.
AGENTS.mdsentence already says it;make that the settled answer and close this." There is now no written record of the gap anywhere
in the org.
The agent brief above has been edited to say so.
The gap is now undocumented as well as unchecked
Before the sweep, an agent reading
AGENTS.mdat least learned that a path named in a docstring isnot checked. That sentence is gone, and no test replaced it. This issue is the only remaining record
that the gap exists, which argues for doing the work rather than deferring it again.
A live instance occurred during #85's work
Renumbering
0004-flask-horn-historical-artwork-grant.mdto0003in.githubrequired updating acitation inside
brand/build/symbols.py, a bare backticked path in a docstring. Nothing in CI wouldhave caught it had it been missed. Its sibling citation in
AGENTS.mdwas a real Markdown link, andthe gate would have failed on that one.
Same rename, same ADR, two citations, one checked and one not. That is this issue's thesis met in the
course of a routine change, rather than argued from a hypothetical.
lesnik512 commentedon Sep 15, 2026
Landed in all 26 in-scope repos as
tests/test_adr_citations.py, byte-identical everywhere, one PR per repo. Verified against the brief before merging: AST constants and raw text are unioned, the split-literal and hash-comment cases each have a test, a fixture with a known-missing ADR fails the scanner, repos withoutdocs/adr/pass without a skip, and the failure message lists every unresolved citation with its file. Every PR was green at merge time.Two repos needed an unrelated lint fix first, because the
installrecipe runsuv lock --upgradeand CI resolved ty 0.0.81: faststream-outbox#177 (two unusedty: ignorecomments) and modern-di#491 (a slotted-attribute assignment ty now checks statically). Same drift as faststream-redis-timers#77 earlier today.Merged: .github#103, chat-app#31, compose2pod#99, db-retry#47, eof-fixer#43, faststream-concurrent-aiokafka#73, faststream-outbox#176, faststream-redis-timers#78, httpware#127, lite-bootstrap#219, modern-di#489, modern-di-aiogram#23, modern-di-aiohttp#27, modern-di-arq#21, modern-di-celery#22, modern-di-fastapi#50, modern-di-faststream#51, modern-di-flask#22, modern-di-grpc#20, modern-di-litestar#52, modern-di-pytest#49, modern-di-starlette#27, modern-di-taskiq#20, modern-di-typer#46, semvertag#71, that-depends#247.
lesnik512 commentedon Sep 15, 2026
Option (3) is implemented: one test module,
tests/test_adr_citations.py, byte-identical in all 26 in-scope repos. Merged in modern-python/faststream-concurrent-aiokafka#73; the other 25 are open:compose2pod#99, db-retry#47, eof-fixer#43, faststream-outbox#176, faststream-redis-timers#78, httpware#127, lite-bootstrap#219, modern-di#489, modern-di-aiogram#23, modern-di-aiohttp#27, modern-di-arq#21, modern-di-celery#22, modern-di-fastapi#50, modern-di-faststream#51, modern-di-flask#22, modern-di-grpc#20, modern-di-litestar#52, modern-di-pytest#49, modern-di-starlette#27, modern-di-taskiq#20, modern-di-typer#46, semvertag#71, that-depends#247, chat-app#31, .github#103.
Three corrections to the brief, all measured at
origin/mainmodern-dihas nodocs/adr/path in any tracked Python file. The other seven match the brief's counts.0004-4xx-does-not-trip-the-circuit-breaker.md,0004-keep-py310-vendored-assert-never.md,0013-httpx2-is-the-public-surface.md. A "lowercase-and-hyphen" regex would skip citations of those silently. The module usesdocs/adr/\d{4}-[a-z0-9-]+\.md.git ls-files. Five repos runjust testinside apython-slimcontainer with nogitbinary. A walk from the repo root that prunes dot-directories andnode_modulesyields exactly the tracked Python set in all 28 org repos today, so the walk is the input set. It also avoids asubprocesscall underselect = ["ALL"].What was verified before opening the PRs
git mvof a cited ADR turns the invariant test red naming the citing file and the path; restoring goes green. Thefaststream-concurrent-aiokafkaproof namesmiddleware.py, the split-literal instance this issue was opened on.faststream-redis-timersfailsty checkonorigin/mainalready, with two unusedty: ignorecomments inrouter.py; that is unrelated and left for its own fix.test-ciis green locally with the 100% floor intact in the 20 repos whose suite runs without Docker. The four docker-backed suites andmodern-di-aiogramrun in CI.tmp_pathfixtures with known results, including a missing ADR, a split literal, a#comment, a tree with nothing to check, and a dot-directory that must not be walked. Fixture paths are built through f-strings so the scanner cannot match its own test file.One deviation from the brief's "validator module shared between the CI step and its tests" idiom: no CI step other than pytest consumes this scanner, so it lives at the top of the test module rather than in a sibling
_module.py. Both forms are measured by--cov=.; the single file has one fewer thing to keep in step.All 25 open PRs are green in CI at the time of writing.