Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 154 additions & 11 deletions spec/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,9 @@ def extract_section(text, heading):
# Carried files scanned for a coordination reference (GOVERNANCE.md "Documentation Style Conventions").
TEMPLATE_REF_SCANNED = ("AGENTS.md", "GOVERNANCE.md", ".github/copilot-instructions.md")

# The undeclared-H2 scan reads the same set.
UNDECLARED_HEADING_SCANNED = TEMPLATE_REF_SCANNED


def strip_sections(text, names):
"""`text` with each named `## <heading>` region removed, located by position rather than by content.
Expand All @@ -444,6 +447,20 @@ def strip_sections(text, names):
return "\n".join(out)


def undeclared_h2_headings(text, declared):
"""Level-two headings in `text` that `declared` does not name, sorted.

`declared` is normalized here (stripped, lowercased), not trusted pre-normalized. The contract
then holds for any caller, regardless of how its own names are cased or spaced.
Scoped to `## ` only, the section model's unit. An H1 title or a nested H3 is not itself a
section this check judges.
Fence-aware via unfenced_text. A `## ` line inside a fenced code sample, or a `##`-prefixed
shell comment, is not misread as a real heading.
"""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
h2s = {ln[3:].strip().lower() for ln in unfenced_text(text).split("\n") if ln.startswith("## ")}
return sorted(h2s - {d.strip().lower() for d in declared})
Comment thread
coderabbitai[bot] marked this conversation as resolved.


def template_ref_outside_verbatim(text, verbatim_names, hub_name):
"""True when `hub_name` appears in `text` outside every one of its verbatim sections.

Expand Down Expand Up @@ -536,14 +553,28 @@ def unfenced_text(text):
`<!-- Shields -->` or an `![alt][ref]` in one is not a definition, a group, or a rendered badge. Kept as
one helper because the checkers were fence-aware in some places and blind in others, which is the state
that lets a document be read two ways by one audit.
Fence matching follows CommonMark. A fence marker needs at most 3 leading spaces, more reads as
content, not a boundary. A closing fence needs the same character, a run at least as long as the
opener's, and nothing but whitespace after that run - a mismatched or shorter marker (a ~~~ example
inside a ``` block, a ``` inside a longer ````) or trailing text (an opening fence's language tag has
no closing counterpart) does not close it.
"""
out, fenced = [], False
out, marker, marker_len = [], None, 0
for ln in normalize(text).split("\n"):
s = ln.strip()
if s.startswith(("```", "~~~")):
fenced = not fenced
stripped = ln.lstrip(" ")
indent = len(ln) - len(stripped)
char = stripped[:1]
run = (
len(stripped) - len(stripped.lstrip(char)) if indent <= 3 and char in ("`", "~") else 0
)
if marker is None:
if run >= 3:
marker, marker_len = char, run
continue
elif char == marker and run >= marker_len and not stripped[run:].strip():
marker = None
continue
if not fenced:
if marker is None:
out.append(ln)
return "\n".join(out)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Expand Down Expand Up @@ -1869,15 +1900,12 @@ def audit_repo(entry, spec, branch=None):
)
# The undeclared-section advisory, per spec/section-model.md, treats an H2 the manifest does not declare as a candidate duplicate of a verbatim section, or as repo-specific content to relocate.
# It is advisory only, since a repo may legitimately carry its own project-specific sections, which the AGENTS.md preamble allows, so it points at the reconciliation and never fails.
# It covers AGENTS.md and GOVERNANCE.md only, the two files whose section structure is governed by section-model.md.
# It covers UNDECLARED_HEADING_SCANNED, not only AGENTS.md and GOVERNANCE.md, and never names which destination file an undeclared heading belongs in.
# Skip the hub itself, since its copies are the source and legitimately hold hub-only sections, Repository Onboarding and Conformance being one, that are deliberately not carried.
# A downstream repo carrying such a section is still flagged, which is the point.
if path in ("AGENTS.md", "GOVERNANCE.md") and entry.get("name") != HUB_NAME:
if path in UNDECLARED_HEADING_SCANNED and entry.get("name") != HUB_NAME:
declared = {n.strip().lower() for n in (needed | verbatim_needed)}
h2s = {
ln[3:].strip().lower() for ln in text.splitlines() if ln.startswith("## ")
}
for h in sorted(h2s - declared):
for h in undeclared_h2_headings(text, declared):
findings.append(
(
"DRIFT",
Expand Down Expand Up @@ -2865,6 +2893,121 @@ def _selftest():
f" ok template-ref: {len(tref)} cases, verbatim regions excised before the hub-name scan"
)

# An H2 the manifest does not declare, in AGENTS.md, GOVERNANCE.md, or .github/copilot-instructions.md.
# Fence-aware, so a heading syntax example or a shell comment inside a code sample is not misread as a real section.
uh = [
(
"a declared H2 is not flagged",
"# AGENTS\n\n## Fleet Bootstrap\n\nText.\n",
{"fleet bootstrap"},
[],
),
(
"an undeclared H2 is flagged",
"# AGENTS\n\n## Fleet Bootstrap\n\nText.\n\n## Local Notes\n\nRepo-specific.\n",
{"fleet bootstrap"},
["local notes"],
),
(
"declared-name match is case-insensitive",
"# AGENTS\n\n## fleet BOOTSTRAP\n\nText.\n",
{"fleet bootstrap"},
[],
),
(
"an un-normalized declared set (mixed case, untrimmed) is normalized here, not trusted",
"# AGENTS\n\n## Fleet Bootstrap\n\nText.\n",
{" Fleet Bootstrap "},
[],
),
(
"an H1 title and a nested H3 are not judged, only H2",
"# Local Notes\n\n## Fleet Bootstrap\n\n### Local Notes\n\nText.\n",
{"fleet bootstrap"},
[],
),
(
"a fenced sample showing heading syntax is not a real heading",
"# AGENTS\n\n## Fleet Bootstrap\n\n```\n## Local Notes\n```\n",
{"fleet bootstrap"},
[],
),
(
"a repo's own local content, undeclared, is flagged even in a file with its own declared sections",
(
"# Copilot Instructions\n\n## GitHub Copilot Review Runbook\n\nText.\n\n"
"## Development Workflow\n\nLocal build and test steps.\n\n"
"## Command Line Usage\n\nLocal CLI reference.\n"
),
{"github copilot review runbook"},
["command line usage", "development workflow"],
),
]
uh_ok = True
for label, doc, declared, want in uh:
got = undeclared_h2_headings(doc, declared)
if got != want:
ok = uh_ok = False
print(f" FAIL undeclared-heading: {label} (expected {want}, got {got})")
if uh_ok:
print(f" ok undeclared-heading: {len(uh)} cases, H2-only, case-insensitive, fence-aware")

# A fence closes only on a same-family marker at least as long as the opener.
uf = [
("a simple ``` fence excludes its content", "```\n## Phantom\n```\n## Real\n", "## Real\n"),
("a simple ~~~ fence excludes its content", "~~~\n## Phantom\n~~~\n## Real\n", "## Real\n"),
(
"a ~~~ line nested inside a ``` fence does not close it",
"```md\n~~~\n## Phantom\n```\n## Real\n",
"## Real\n",
),
(
"a shorter ``` cannot close a longer ```` fence",
"````md\n## Phantom\n```\n## Real\n````\n",
"",
),
(
"a longer closing fence than the opener still closes",
"```\n## Phantom\n````\n## Real\n",
"## Real\n",
),
(
"a marker run followed by trailing text does not close",
"```\n## Phantom\n```not-a-fence\n## Real\n```\n## Real2\n",
"## Real2\n",
),
(
"trailing whitespace after the marker run still closes",
"```\n## Phantom\n``` \n## Real\n",
"## Real\n",
),
(
"a 3-space-indented closing fence still closes",
"```\n## Phantom\n ```\n## Real\n",
"## Real\n",
),
(
"a 4-space-indented closing fence remains content, not a boundary",
"```\n## Phantom\n ```\n## Still Phantom\n```\n## Real\n",
"## Real\n",
),
(
"a 4-space-indented opener never opens a fence",
" ```\n## Real\n",
" ```\n## Real\n",
),
]
uf_ok = True
for label, doc, want in uf:
got = unfenced_text(doc)
if got != want:
ok = uf_ok = False
print(f" FAIL unfenced-text: {label} (expected {want!r}, got {got!r})")
if uf_ok:
print(
f" ok unfenced-text: {len(uf)} cases, closing fence matches the opener's marker and length"
)

# Issue generator: findings land in the right buckets and the title carries the count.
fe = {"name": "Widget", "types": ["python"]}
it, ib = render_issue(
Expand Down
2 changes: 2 additions & 0 deletions spec/section-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ A repo that carried its governance inside `AGENTS.md` before the router split ho

`files.json` declares each section's fidelity. [validate.py][validate] proves every declared section resolves to a real level-two heading in the hub's own copy of the file that declares it, so a renamed or mistyped section cannot silently stop being checked. [audit.py][audit] checks each repo's copy (presence for `intent`, byte-match for `verbatim`) and classifies a mismatch as stale (re-vendor) or modified (review).

The undeclared-heading advisory also runs against `.github/copilot-instructions.md`, not only `AGENTS.md` and `GOVERNANCE.md`. That file carries its own declared sections in `files.json`, and repo-specific content can accumulate there unseen. The advisory names the heading as undeclared and points at this doc's destinations. It does not name which destination a heading belongs in. Neither `OPERATIONS.md`'s six headings nor `ARCHITECTURE.md`'s are declared anywhere as data, and matching by heading name misses content filed under a differently worded heading.

<!-- Internal -->

[agents]: ../AGENTS.md
Expand Down