ci(dev): deterministic gates for doc/tree drift, test coverage, and a shared toolchain - #1164
Merged
Conversation
This session fixed a batch of documentation that had drifted from the tree: ten specs missing from both index files, per-family and per-mode skill counts stale in three places, and catalogue totals stale in a fourth. Every one was found by reading, not by a check. Left as is, they come back. check-doc-sync.py makes five of them mechanical: - every tools/spec-loop/specs/*.md is listed in BOTH overview.md and README.md; - README.md's family table "N skills" cells match live family: frontmatter; - docs/modes.md's Modes-at-a-glance Skill-count column matches live mode: frontmatter — the validator's modes-doc rule checks section membership but parses only the mode and status columns, so the counts were unguarded; - bare "N skills" totals in docs/setup/marketplaces.md match the catalogue; - every script in tools/dev/ is named in tools/dev/README.md. Each check was verified to fail on the exact defect it exists for, by reintroducing that defect and confirming the reported number matched what had actually been wrong. A check that cannot go red is worse than no check, since it reads as evidence — the lesson from apache#1161, where an unauthenticated CLI produced a fully green eval run. The fifth check found three undocumented scripts on its first run (agent-pre-commit.sh, check-placeholders.sh, run-workspace-check.sh), so tools/dev/README.md now carries a table saying what each of the seven guards rather than naming a couple in passing. The counts are deliberately narrow: the total-skills check reads an allowlist of files rather than sweeping the repo, because plenty of docs legitimately count a subset ("Nine skills cover the staged path") and a greedy scan would flag those as drift forever. Generated-by: Claude Code (Opus 5)
The scripts in tools/dev are the gates that guard everyone else's changes, and they were the one Python tree nothing checked: the workspace checks iterate `[tool.uv.workspace] members`, and tools/dev was not one. So ruff, mypy, and pytest all skipped it. Declares the tree as a workspace member, the same config-carrier pattern `skills/pyproject.toml` uses, and adds 20 tests for check-doc-sync.py. The tests are written around the red case. Each check builds a miniature repo in tmp_path and asserts the drift is reported with both numbers, then that the corrected tree is silent — because a gate that cannot fail is worse than no gate, it reads as evidence while measuring nothing. Also covered: a spec listed in only one of the two indexes still counts as missing, a README table row that names no family is not read as a family with a wrong count, a mode nothing declares must say 0 rather than be skipped, every stale total on a page is reported rather than just the first, and the totals check stays inside its allowlist. Script filenames are hyphenated, so the tests load the module through importlib rather than a plain import. Bringing the tree under the checks surfaced four lint findings in pre-existing scripts; three are fixed (an unused unpacked variable, two loops ruff can express directly) and RUF003 is ignored with a reason — the repo's prose style uses en dashes throughout, and rewriting comments to hyphens to satisfy a homoglyph check would make them worse. Four files were reformatted. Every script was re-run afterwards to confirm behaviour is unchanged. Generated-by: Claude Code (Opus 5)
Every workspace member repeated the same three pins in its own `[dependency-groups] dev`, under an instruction in the root pyproject to "keep the version bounds here and in every member in lockstep". They were not in lockstep: mypy >=2.1.0, >=2.3.0, >=1.10 (three floors) pytest >=9.1.1, >=8.0 (two) ruff >=0.16.1, >=0.16.2 (two) tools/skill-reconciler-diff declared only pytest, and tools/fossil had no dev group at all despite being ruff-checked — its checks ran against whatever happened to be in the environment. Nothing detected any of it. The duplication was the defect and the lockstep instruction was the workaround. tools/dev becomes `magpie-dev`, a metadata-only wheel whose dependencies are the toolchain. Every other member's dev group becomes `["magpie-dev"]`, with the workspace source declared once at the root. Members with extras keep them (tools/gmail/oauth-draft retains mcp). Bumping a version is now one edit and the whole workspace moves. Each member's environment stays self-contained: the checks still run `uv run --directory <member> --project . python -m <tool>`, so nothing leaks in from the root. Only the declaration is shared. Two properties checked rather than assumed. Check discovery keys off the config sections (`[tool.ruff]`, `[tool.mypy]`, `[tool.pytest.ini_options]`) and not the dev group, so giving a member more tools does not silently enrol it in more checks. And the whole battery was re-run after deleting every member `.venv`, not against a warm cache: ruff 30/30, ruff-format 30/30, mypy 27/27, pytest 33/33. One real behaviour change: tools/bitbucket moves from mypy>=1.10 / pytest>=8.0 to the current floors. It passes the fresh sweep, but that is a version bump rather than a no-op. Generated-by: Claude Code (Opus 5)
Both the CI pytest matrix and the workspace sweep are driven by the
presence of `[tool.pytest.ini_options]`. A project can therefore carry a
full tests/ directory and be executed by nothing: the job is simply never
emitted, so there is no red tick for anyone to notice. Nothing checked
this.
check-workspace-members.py now reports three shapes:
- tests on disk, no [tool.pytest.ini_options] → the tests never run;
- [tool.pytest.ini_options], no tests on disk → the CI job runs and
collects nothing, so its green tick proves nothing;
- neither → no tests at all.
A project that genuinely should not be tested declares it with
`[tool.magpie.checks] skip = ["pytest"]`, the same opt-out the sweep and
the CI matrix already honour — so an exemption is visible in the file
rather than being a silent absence from a matrix nobody reads.
All 33 members pass today, so this locks in the current state rather than
fixing a present gap. The hook now also re-fires on test files, since
adding or deleting tests changes the answer.
Ten tests cover it, including the false negative that would make the
whole check worthless: a test file vendored inside a member's .venv or
site-packages must not make an untested member look tested. Each of the
three failure shapes was also verified by hand against a real member,
and the declared opt-out confirmed to silence all three.
Generated-by: Claude Code (Opus 5)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four commits, each a step further into the same problem: things that should
track each other, don't, and nothing notices.
1.
check-doc-sync— deterministic gates for doc/tree drift#1159, #1162 and #1163 fixed documentation that had drifted from the tree. Every
instance was found by reading. This makes five of them mechanical, as one prek
hook:
tools/spec-loop/specs/*.mdlisted in bothoverview.mdandREADME.mdREADME.mdfamily table vs livefamily:frontmatter12→15, repo-health6→7docs/modes.mdModes at a glance vs livemode:frontmatter34→35, Drafting9→11docs/setup/marketplaces.md71→74, three occurrencestools/dev/named in its READMEThe validator's existing
modes-docrule looks like it covers the mode counts.It does not —
_read_mode_table()parses the mode and status columns only andnever reads the count, so those numbers had no guard at all.
2.
tools/devunder the workspace checks, with testsThe scripts in
tools/devguard everyone else's changes and were the one Pythontree nothing checked: the workspace checks iterate
[tool.uv.workspace] membersand
tools/devwas not one, so ruff, mypy and pytest all skipped it.20 tests for
check-doc-sync.py, written around the red case — each builds aminiature repo in
tmp_path, asserts the drift is reported with both numbers,then that the corrected tree is silent. A gate that cannot fail is worse than no
gate: it reads as evidence while measuring nothing. Also covered: a spec listed
in only one index still counts as missing, a README row naming no family is not
read as a family with a wrong count, a mode nothing declares must say
0ratherthan be skipped, every stale total on a page is reported rather than just the
first, and the totals check stays inside its allowlist.
Bringing the tree under the checks surfaced four lint findings in pre-existing
scripts. Three are fixed;
RUF003is ignored with a reason — the repo's prosestyle uses en dashes throughout, and rewriting comments to hyphens to satisfy a
homoglyph check would make them worse. Every script was re-run afterwards to
confirm behaviour is unchanged.
3. Shared dev toolchain via
magpie-devEvery member repeated the same three pins under an instruction in the root
pyproject to "keep the version bounds here and in every member in lockstep".
They were not in lockstep:
tools/skill-reconciler-diffdeclared only pytest, andtools/fossilhad nodev group at all despite being ruff-checked — its checks ran against whatever
was in the environment. The duplication was the defect; the lockstep instruction
was the workaround.
tools/devbecomesmagpie-dev, a metadata-only wheel whose dependencies arethe toolchain. Every other member's dev group becomes
["magpie-dev"], with theworkspace source declared once at the root. Extras are preserved
(
tools/gmail/oauth-draftkeepsmcp). Bumping a version is now one edit.Each member's environment stays self-contained — the checks still run
uv run --directory <member> --project ., so nothing leaks in from the root.Only the declaration is shared.
4. Verify every member's tests actually run
Both the CI pytest matrix and the workspace sweep are driven by the presence of
[tool.pytest.ini_options]. A project can therefore carry a fulltests/directory and be executed by nothing: the job is simply never emitted, so
there is no red tick for anyone to notice. Nothing checked this.
check-workspace-members.py— which already owns "the members list decides whichprojects get hooks and CI matrix entries" — now also reports three shapes:
[tool.pytest.ini_options][tool.pytest.ini_options], no tests[tool.magpie.checks] skip = ["pytest"]is the declared exemption — the sameopt-out the sweep and the matrix already honour, so a project that should not be
tested says so in its own file rather than being a silent absence from a matrix
nobody reads.
All 33 members pass today, so this locks in the current state rather than
fixing a present gap — which is the best moment to add it.
Ten tests cover it, including the one false negative that would make the whole
check worthless: a test file vendored inside a member's
.venvorsite-packagesmust not make an untested member look tested.Verification
prek run --all-filespasses (exit 0).venv, not against awarm cache: ruff 30/30, ruff-format 30/30, mypy 27/27, pytest 33/33
and pass once restored
member, and the declared opt-out confirmed to silence all three
tools/devscript re-run after reformatting to confirm unchangedbehaviour
[tool.ruff],[tool.mypy],[tool.pytest.ini_options]), not dev-group-based — so giving a member moretools does not silently enrol it in more checks
Notes for reviewers
One real behaviour change:
tools/bitbucketmoves frommypy>=1.10/pytest>=8.0to the current floors. It passes the fresh sweep, but that is aversion bump, not a no-op refactor.
Deliberate limits on the totals check: it reads an allowlist rather than
sweeping the repo. Plenty of docs legitimately count a subset ("Nine skills cover
the staged path"), and a greedy scan would flag those forever.
.last-syncstaleness is not a hook. It would fail nearly every PR, so itwould be noise rather than a gate; the human rule in
AGENTS.mdis its home.🤖 Generated with Claude Code
https://claude.ai/code/session_01So3JRGXrbqSGrohtZuHWKg