From 67e1c20bd0018d837dd9494b553f00aaff3f86e4 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 21 Aug 2026 16:41:07 -0700 Subject: [PATCH 1/5] Widen undeclared-heading advisory to copilot-instructions.md The undeclared-H2 advisory (spec/section-model.md) only scanned AGENTS.md and GOVERNANCE.md, so a repo's own content sitting in .github/copilot-instructions.md was invisible to it, which is how ptr727/PhotoCleaner's local sections went undetected and duplicated a later OPERATIONS.md (#523). Extend UNDECLARED_HEADING_SCANNED to include copilot-instructions.md, which already carries a declared section list in files.json. Do not attempt to name a destination file for an undeclared heading: neither OPERATIONS.md's six headings nor ARCHITECTURE.md's are declared as data anywhere, and PhotoCleaner's actual headings ("Development Workflow", "Command Line Usage") matched neither, so a heading-name match would have missed the case that motivated this. The finding stays structural, naming the heading as undeclared and pointing at section-model.md's destinations for a human to judge, per #523's own warning against a content-similarity heuristic. Also make the H2 scan fence-aware via the existing unfenced_text helper, extracted the scan into undeclared_h2_headings() so it is unit-tested, and added 6 selftest cases covering the new scope, the fence fix, and the existing AGENTS.md/GOVERNANCE.md behavior. --- spec/audit.py | 81 +++++++++++++++++++++++++++++++++++++++---- spec/section-model.md | 2 ++ 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/spec/audit.py b/spec/audit.py index 1fb60454..50123f2a 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -419,6 +419,11 @@ 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") +# Carried files scanned for an undeclared H2 heading (spec/section-model.md). +# Started as AGENTS.md and GOVERNANCE.md, the two files section-model.md's split governs. +# #523 added .github/copilot-instructions.md, after a repo's local content sat there undetected, duplicating a later OPERATIONS.md. +UNDECLARED_HEADING_SCANNED = ("AGENTS.md", "GOVERNANCE.md", ".github/copilot-instructions.md") + def strip_sections(text, names): """`text` with each named `## ` region removed, located by position rather than by content. @@ -444,6 +449,22 @@ def strip_sections(text, names): return "\n".join(out) +def undeclared_h2_headings(text, declared): + """Level-two headings in `text` that lowercased `declared` does not name, sorted. + + Scoped to `## ` only: the section model's unit is the H2, and an H1 title or a nested H3 is not itself a + section this check judges. Fence-aware via unfenced_text, so a `## ` line inside a fenced code sample - + documenting the heading syntax itself, or a `##`-prefixed shell comment - is not misread as a real + heading; per unfenced_text's own docstring, a checker left fence-blind is a document read two ways. + """ + h2s = { + ln[3:].strip().lower() + for ln in unfenced_text(text).split("\n") + if ln.startswith("## ") + } + return sorted(h2s - declared) + + def template_ref_outside_verbatim(text, verbatim_names, hub_name): """True when `hub_name` appears in `text` outside every one of its verbatim sections. @@ -1869,15 +1890,13 @@ 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: AGENTS.md and GOVERNANCE.md, whose section structure section-model.md governs directly, plus .github/copilot-instructions.md, which carries its own declared sections in files.json and is where repo-specific content has actually accumulated undetected (#523). + # It does not name a destination file, only that the heading is undeclared: neither OPERATIONS.md's six headings nor ARCHITECTURE.md's are declared as data anywhere, and the repo that motivated this used headings that matched neither, so a name-match would have missed the case it exists to catch. # 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", @@ -2865,6 +2884,56 @@ def _selftest(): f" ok template-ref: {len(tref)} cases, verbatim regions excised before the hub-name scan" ) + # Undeclared-heading advisory: an H2 the manifest does not declare, scoped to AGENTS.md, GOVERNANCE.md, and .github/copilot-instructions.md (#523), fence-aware so a documented heading syntax 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 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 - the copilot-instructions.md case #523 added", + "# 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") + # Issue generator: findings land in the right buckets and the title carries the count. fe = {"name": "Widget", "types": ["python"]} it, ib = render_issue( diff --git a/spec/section-model.md b/spec/section-model.md index 68a31206..cebcb7a7 100644 --- a/spec/section-model.md +++ b/spec/section-model.md @@ -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 above also runs against `.github/copilot-instructions.md`, not only `AGENTS.md` and `GOVERNANCE.md`, since that file has its own declared sections in `files.json` and is where repo-specific content has accumulated undetected before. It names the heading as undeclared and points at this doc's destinations, and it does not attempt to name which destination a given heading belongs in, since neither `OPERATIONS.md`'s six headings nor `ARCHITECTURE.md`'s are declared anywhere as data, and matching by heading name would miss content filed under a differently worded heading regardless. + [agents]: ../AGENTS.md From e5d8f565dadf16d5e32de82dcefe21061e192243 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 21 Aug 2026 16:45:54 -0700 Subject: [PATCH 2/5] Normalize declared inside undeclared_h2_headings, fix ISC004 Copilot review: the docstring said the function reads lowercased declared, but it trusted the caller to have already normalized it. Normalize inside the helper so the contract holds for any caller, plus a selftest case with an un-normalized declared set. Also parenthesize the implicitly-concatenated string literal in the new selftest fixture (ruff ISC004), caught by CI, not by the local ruff-less environment this was authored in. --- spec/audit.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/spec/audit.py b/spec/audit.py index 50123f2a..17851222 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -450,8 +450,10 @@ def strip_sections(text, names): def undeclared_h2_headings(text, declared): - """Level-two headings in `text` that lowercased `declared` does not name, sorted. + """Level-two headings in `text` that `declared` does not name, sorted. + `declared` is normalized here (stripped, case-folded) rather than trusted pre-normalized, so the + contract holds for any caller regardless of how its own section names are cased or spaced. Scoped to `## ` only: the section model's unit is the H2, and an H1 title or a nested H3 is not itself a section this check judges. Fence-aware via unfenced_text, so a `## ` line inside a fenced code sample - documenting the heading syntax itself, or a `##`-prefixed shell comment - is not misread as a real @@ -462,7 +464,7 @@ def undeclared_h2_headings(text, declared): for ln in unfenced_text(text).split("\n") if ln.startswith("## ") } - return sorted(h2s - declared) + return sorted(h2s - {d.strip().lower() for d in declared}) def template_ref_outside_verbatim(text, verbatim_names, hub_name): @@ -2904,6 +2906,12 @@ def _selftest(): {"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", @@ -2918,9 +2926,11 @@ def _selftest(): ), ( "a repo's own local content, undeclared, is flagged - the copilot-instructions.md case #523 added", - "# 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", + ( + "# 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"], ), From 9d710aefd0f541fbfb170df21b6fc00acf65c9c9 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 21 Aug 2026 16:48:54 -0700 Subject: [PATCH 3/5] Apply ruff format to undeclared_h2_headings CI's format-check job caught this, not the check job: the multi-line set comprehension fits ruff's line-length on one line. --- spec/audit.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/audit.py b/spec/audit.py index 17851222..0c5c41d6 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -459,11 +459,7 @@ def undeclared_h2_headings(text, declared): documenting the heading syntax itself, or a `##`-prefixed shell comment - is not misread as a real heading; per unfenced_text's own docstring, a checker left fence-blind is a document read two ways. """ - h2s = { - ln[3:].strip().lower() - for ln in unfenced_text(text).split("\n") - if ln.startswith("## ") - } + 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}) From 3eed1079ad3e048ce1c781caca5b871c049423d4 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 21 Aug 2026 16:52:34 -0700 Subject: [PATCH 4/5] Fix docstring wording: lowercased, not case-folded Copilot suppressed finding: the docstring said case-folded but the code uses .lower(), and every other case-insensitive comparison in this file (heading_texts, hub-name matching) already says lowercased in its own docstring. Match that vocabulary rather than switch this one call site to .casefold() and diverge from the rest of the file. --- spec/audit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/audit.py b/spec/audit.py index 0c5c41d6..ca063f01 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -452,7 +452,7 @@ def strip_sections(text, names): def undeclared_h2_headings(text, declared): """Level-two headings in `text` that `declared` does not name, sorted. - `declared` is normalized here (stripped, case-folded) rather than trusted pre-normalized, so the + `declared` is normalized here (stripped, lowercased) rather than trusted pre-normalized, so the contract holds for any caller regardless of how its own section names are cased or spaced. Scoped to `## ` only: the section model's unit is the H2, and an H1 title or a nested H3 is not itself a section this check judges. Fence-aware via unfenced_text, so a `## ` line inside a fenced code sample - From 76f6b8de39c11c28d13a461b2aa388b9ed4a81d2 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 21 Aug 2026 16:59:29 -0700 Subject: [PATCH 5/5] Trim comments per comment-and-doc-style: no task refs, no growth qodo-code-review (advisory, under evaluation per docs/pr-reviewer-evaluation.md) flagged real violations: - Comments referencing #523: task context belongs in the PR description and commit history, not in long-lived code comments. - The undeclared-section advisory's existing 5-line comment block grew by 2 lines instead of staying the same length; restored it to 5 lines with the scope note folded into the existing line. - A new 3-line comment where the sibling TEMPLATE_REF_SCANNED constant sets a 1-line precedent; matched it. - A spaced hyphen and a semicolon in the new docstring, both banned in agent-authored prose regardless of the syntax carrying them. --- spec/audit.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/spec/audit.py b/spec/audit.py index ca063f01..e477b99b 100755 --- a/spec/audit.py +++ b/spec/audit.py @@ -420,8 +420,6 @@ def extract_section(text, heading): TEMPLATE_REF_SCANNED = ("AGENTS.md", "GOVERNANCE.md", ".github/copilot-instructions.md") # Carried files scanned for an undeclared H2 heading (spec/section-model.md). -# Started as AGENTS.md and GOVERNANCE.md, the two files section-model.md's split governs. -# #523 added .github/copilot-instructions.md, after a repo's local content sat there undetected, duplicating a later OPERATIONS.md. UNDECLARED_HEADING_SCANNED = ("AGENTS.md", "GOVERNANCE.md", ".github/copilot-instructions.md") @@ -455,9 +453,9 @@ def undeclared_h2_headings(text, declared): `declared` is normalized here (stripped, lowercased) rather than trusted pre-normalized, so the contract holds for any caller regardless of how its own section names are cased or spaced. Scoped to `## ` only: the section model's unit is the H2, and an H1 title or a nested H3 is not itself a - section this check judges. Fence-aware via unfenced_text, so a `## ` line inside a fenced code sample - - documenting the heading syntax itself, or a `##`-prefixed shell comment - is not misread as a real - heading; per unfenced_text's own docstring, a checker left fence-blind is a document read two ways. + section this check judges. Fence-aware via unfenced_text, so a `## ` line inside a fenced code sample + (documenting the heading syntax itself) or a `##`-prefixed shell comment is not misread as a real + heading. Per unfenced_text's own docstring, a checker left fence-blind is a document read two ways. """ 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}) @@ -1888,8 +1886,7 @@ 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 UNDECLARED_HEADING_SCANNED: AGENTS.md and GOVERNANCE.md, whose section structure section-model.md governs directly, plus .github/copilot-instructions.md, which carries its own declared sections in files.json and is where repo-specific content has actually accumulated undetected (#523). - # It does not name a destination file, only that the heading is undeclared: neither OPERATIONS.md's six headings nor ARCHITECTURE.md's are declared as data anywhere, and the repo that motivated this used headings that matched neither, so a name-match would have missed the case it exists to catch. + # 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 UNDECLARED_HEADING_SCANNED and entry.get("name") != HUB_NAME: @@ -2882,7 +2879,7 @@ def _selftest(): f" ok template-ref: {len(tref)} cases, verbatim regions excised before the hub-name scan" ) - # Undeclared-heading advisory: an H2 the manifest does not declare, scoped to AGENTS.md, GOVERNANCE.md, and .github/copilot-instructions.md (#523), fence-aware so a documented heading syntax or a shell comment inside a code sample is not misread as a real section. + # Undeclared-heading advisory: an H2 the manifest does not declare, scoped to AGENTS.md, GOVERNANCE.md, and .github/copilot-instructions.md, fence-aware so a documented heading syntax or a shell comment inside a code sample is not misread as a real section. uh = [ ( "a declared H2 is not flagged", @@ -2921,7 +2918,7 @@ def _selftest(): [], ), ( - "a repo's own local content, undeclared, is flagged - the copilot-instructions.md case #523 added", + "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"