Skip to content

feat(gate): resolve every cited CLAUDE.md section number, and list the four missing subpackages - #356

Merged
wshallwshall merged 4 commits into
mainfrom
claude/claude-md-section-guard
Aug 13, 2026
Merged

feat(gate): resolve every cited CLAUDE.md section number, and list the four missing subpackages#356
wshallwshall merged 4 commits into
mainfrom
claude/claude-md-section-guard

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

Replaces #355, which carried the same commits on a branch whose name referenced an unrelated research topic. Identical content -- the trees match at aa4933a395a8. Nothing about this change derives from that research; only the branch name did, and it is gone.

What it closes

CLAUDE.md's numbered sections are a de facto API -- 653 citations across 260 tracked files -- and nothing validated a section number. link_check.py resolves the PATH and skips the fragment; the doc guards that read CLAUDE.md check SDS identifiers and link paths. A renumber landed entirely green.

5 files, +366/-2. CLAUDE.md is insertion-only, no section renumbered. Its 4 added lines list security/, support/, verify/ and tray/ -- subpackages that exist in the tree and were missing from the section 3 listing. ci.yml adds the new module to the existing DOC_GUARDS list, beside test_link_resolution.py and test_dast_claims.py.

Verified here, not accepted on report

Reproduced against a tree including main's newest CLAUDE.md change:

guard on the merged tree     rc=0    "sections [0..12]; scanned 1523 tracked files;
                                      653 section citations across 260 files"
renumber section 11 -> 13    rc=1    names 61 broken citations, by file and line
after revert                 rc=0    CLAUDE.md byte-identical

The guard goes red on the defect, names the citers, and leaves no residue -- it is not green because it checks nothing.

A caution about that measurement, recorded because it nearly fooled me: my first attempt piped the guard through tail and read $?, capturing tail's exit code and reporting rc=0 on a run that had plainly failed. Re-measured with no pipe. That is SDS-3.8, and this PR's own subject -- a check that silently does not check -- is the same family one level up.

Why it belongs in the repo

It is the same category as the doc guards already here, and it is a contributor-facing control rather than a runtime one: it protects the accuracy of the file every session loads as context. The 15th module on a list whose 14th caught a broken link on a PR earlier today, while a guard that was not on that list reached main -- same blind mode, two outcomes, decided purely by list membership.

Provenance

Authored by another session and routed here for push and PR. Verified before pushing: commits present, 5 files +366/-2 exactly as stated, still merge-tree rc=0 against current main, and a leak scan of all 366 added lines clean with a negative control.

Unblocks a third session: the ASVS tracking lane is recording its section-citation exposure as closed pending merge of e7e825ed rather than closed -- correctly, since a control on an unpushed branch is not a shipped control.

The repository-layout tree listed 10 subpackages while messagefoundry/ has
14. security/, support/, verify/ and tray/ were absent, so CLAUDE.md
contained ZERO occurrences of "tray" despite ADR 0113 and a 17-module
package shipping at HEAD.

Cause: the tray, verify and support packages landed in the private lineage
between 2026-07-13 and 07-25, and that content never reached the repo of
record at publication -- five commits dropped by omission, absent from HEAD.
The tree has been stale since.

Descriptions taken from each package's own __init__ docstring. The tray line
records what it wraps, verified rather than assumed: tray/ imports only
messagefoundry.service, messagefoundry.service_status and its own submodules
-- no api/, no engine internals -- so section 4's dependency rule needs no
carve-out for it.

Insertion-only, no section renumbered. That is deliberate: 281 tracked files
cite this file by section number, nothing validates a section number
(link_check.py:21 validates the path and skips the fragment), and the same
rot has already landed once -- test_sds_rule_ids_are_stable.py:7-11 records
four security citations that still resolve to the wrong section today.

Verified: tests/test_link_resolution.py and
tests/test_sds_rule_ids_are_stable.py, 34 passed. The second was confirmed to
read this worktree's file by injecting a bogus SDS-9.9 citation and watching
it go red, then removing it.
…nested anchor escaping the SDS gate

CLAUDE.md's numbered sections are a de facto API and NOTHING validated a
section number. Measured: 281 tracked files name the file, 646 citations name
one of its sections, and tests/test_dependency_boundaries.py cites section 4
in its own docstring, so this reaches code and not only prose.

Every existing gate stays green through a renumber. link_check.py:21 says so
in its own header -- it resolves the PATH and skips the #fragment -- and the
two doc guards that already read CLAUDE.md check other things: one checks
SDS-N.N identifiers, one checks link paths. Path resolves, identifiers
resolve, only the meaning moves. The checkable half stays green.

The rot is not hypothetical. The sibling guard
tests/test_sds_rule_ids_are_stable.py:5-11 records it landing on the Secure
Development Standards -- a new section 5 pushed 5-9 to 6-10, and citations
still resolve to the wrong section today. The instance was fixed and the
class left open. This closes it for the anchor.

MEASURED BLAST RADIUS: renumbering section 11 alone breaks 61 citations.

scripts/docs/claude_section_check.py, stdlib only, beside its siblings:
  - parses the anchor's own `## N.` headings as the source of truth
  - scans tracked .md/.py/.ps1/.yml via git ls-files
  - requires the anchor's NAME on the same line within a bounded window, so a
    section number belonging to another document is never attributed to it --
    false positives are how a gate gets disabled
  - ALWAYS prints what it scanned; a finding count alone cannot distinguish a
    clean scan from one that examined nothing
  - REFUSES to run when zero headings parse or the anchor is missing, rather
    than reporting every citation broken or the corpus clean

ADDED TO DOC_GUARDS in ci.yml. Without that it would be born unable to fire:
a markdown-only PR is classified non-code and runs only that list, so a check
landed outside it goes green by absence on exactly the PR shape it exists to
catch. That enumeration hole has already been fixed twice for other paths
(BACKLOG #327, #1200) and left open both times; ci.yml:1050 documents its own
recurrence.

Also widens _citation_sources() in the SDS guard to include nested CLAUDE.md
files. Root is a literal path and the globs are rooted at docs/,
messagefoundry/ and scripts/, so a subpackage anchor -- which CLAUDE.md
section 3 invites -- would take its SDS citations out of coverage silently.
Resolved through git ls-files, not rglob, because this repository nests
worktrees under .claude/worktrees/ and an rglob would scan sibling checkouts.
There are no nested anchors today; this is the prerequisite, not a response.

Verified. Both guards were made to FAIL ON PURPOSE and the injected defect
confirmed to land:
  - renumber section 11 to 13        -> rc=1, 61 citations; revert -> rc=0
  - nested CLAUDE.md citing SDS-9.9  -> SDS guard red; remove  -> green
  - 8 self-bite tests, including both refuse-to-run guards and a
    false-positive case (another document's section 9 is not claimed)
  - ruff check, ruff format, mypy strict clean
  - full DOC_GUARDS set, 15 modules: 219 passed, 89 skipped
@wshallwshall
wshallwshall merged commit 2d11dac into main Aug 13, 2026
37 of 38 checks passed
@wshallwshall
wshallwshall deleted the claude/claude-md-section-guard branch August 13, 2026 12:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant