Check a driftNote on every run, and say what the audit does not evaluate - #568
Conversation
The registry freshness check was gated on the rest of the audit being clean, so one standing finding a repo could not clear exempted its whole driftNote list. Blog carries exactly that: the AGENTS.md template-reference finding (#552) held the gate shut, so its two stale hugo notes were never raised. The repo with open findings is where a stale note is most likely, which is the inverse of what the gate produced. Both note shapes are now evaluated unconditionally. A prose note asserting outstanding work is contradicted outright by a clean audit, and where findings are open it is raised as a question of which one it means. A note naming the check that would retire it is resolved against spec/project-types.json and surfaced for a hand decision. The check-id matcher is deliberately unanchored. An end-anchored pattern matches neither of the two notes the form was introduced for, because both end the sentence after the paren, and across 22 repos and 53 notes it matches none at all, which reads exactly like a fleet carrying no such note. The self-test covers the period case and fails on the anchored form. Drop Blog's two hugo notes. Both deviations closed in ptr727/Blog#30 and are on its ground-truth main: themes/README.md records the upstream repository, commit, and license, and the generator version and checksum are declared once in .github/actions/install-hugo/action.yml, consumed by both workflows. State that no per-type check is mechanized, where an agent reads a run. spec/audit.py never reads spec/project-types.json, so a clean run is evidence about the deterministic subset and about nothing in AUDIT.md section 4. The retirement sentence in #560 reads as automation and is not, and silence from a tool that was never looking reads exactly like a pass. Refs #563 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR adjusts the fleet audit tooling so driftNotes freshness is checked on every run (even when other findings exist), adds support for driftNotes that name a parenthesized check id, and clarifies (in tooling output + docs) what spec/audit.py does not evaluate.
Changes:
- Refactors driftNote freshness into
driftnote_findings()and runs it unconditionally, including check-id extraction viaCHECK_ID_RE. - Loads
spec/project-types.jsonto resolve whether a driftNote’s named check id exists and whether the repo declares the owning type. - Removes two now-stale Hugo driftNotes from the Blog registry entry and updates messaging/docs to emphasize that per-type checks are judged by hand.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| spec/audit.py | Adds check-id parsing + resolution for driftNotes, ungates driftNote freshness checks, and updates console/issue text about non-mechanized per-type checks. |
| registry/repos.json | Drops two retired Hugo driftNotes from the Blog entry. |
| OPERATIONS.md | Clarifies that a clean spec/audit.py run is not evidence about per-type checks in spec/project-types.json. |
| AUDIT.md | Expands the guidance that section-4 checks are hand-judged and explains driftNote freshness behavior. |
Uh oh!
There was an error while loading. Please reload this page.
…es not The previous sentence claimed spec/audit.py never reads spec/project-types.json at all, which this branch's own change falsified: the check-id resolver loads it. The claim the section needs is narrower and sharper anyway. The audit evaluates no check in that file, and reads it for one purpose, to resolve the id a driftNote names against the catalog and the repo's declared types. Resolving an id is not running the check it names. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (5)
spec/audit.py:1281
- The console clean-status line uses "no per-type check in spec/project-types.json is run here". Since cross-cutting checks also live in spec/project-types.json, this is ambiguous and can read as if cross-cutting checks are evaluated; the intent elsewhere is that none of them are.
print(" clean (deterministic checks only; no per-type check in spec/project-types.json is run here - AUDIT.md section 4 is judged by hand)")
spec/audit.py:1174
- This issue preamble says "No per-type check in spec/project-types.json is run here". Because spec/project-types.json also contains cross-cutting checks, this wording can be misread as implying cross-cutting checks are run; the audit runs none of them.
f"This lists what the audit mechanically detects. No per-type check in `spec/project-types.json` is run here, "
spec/audit.py:162
- The driftNote finding message says "no per-type check is mechanized", but driftNotes can also name cross-cutting check ids (from the same spec/project-types.json catalog). Using "per-type" here can be misread as implying cross-cutting checks are evaluated, even though the audit evaluates none of them.
This issue also appears in the following locations of the same file:
- line 1174
- line 1281
out.append(("DRIFT", f"registry: driftNote names check '{cid}', which this audit does not evaluate (no per-type check is mechanized, AUDIT.md section 4) - judge it by hand and delete the note once it passes: {quoted}"))
OPERATIONS.md:46
- This paragraph says "No per-type check in spec/project-types.json runs here". Because cross-cutting checks also live in spec/project-types.json, this phrasing can be read as if cross-cutting checks do run; the intent is that none of them do.
Findings are a point-in-time snapshot read live over the API. Re-run before acting on one, and quote the run stamp in any issue derived from it. The deterministic subset lives here, and the full letter-and-intent verdict is [AUDIT.md](./AUDIT.md). No per-type check in `spec/project-types.json` runs here, so a clean run is not evidence about any of them.
AUDIT.md:136
- The driftNotes section describes a check-id note as "a parenthesized id" but the example shows only the bare id in backticks. Since the matcher requires parentheses, the example should include them to avoid suggesting that
hugo.generator.pinned(without parens) will be detected.
**Reconcile `driftNotes` in the same pass.** A registry `driftNote` records a *current* deviation from the baseline. Once the deviation is resolved the note is deleted, not left describing finished work, since hand-maintained prose drifts silently otherwise. `spec/audit.py` flags two shapes of note, neither of them gated on the rest of the audit being clean. A note asserting outstanding work in prose ("pending", "not yet", "missing", "behind", ...) is contradicted outright by a clean audit, and where findings are open it is raised as a question of which one it means, because gating the check on a clean audit meant one standing finding a repo could not clear exempted its whole note list, and the repo carrying open findings is where a stale note is most likely. A note naming the check that would retire it, as a parenthesized id (`hugo.generator.pinned`), is the mechanically checkable shape and is surfaced on **every** run: the audit resolves the id against the catalog and confirms the repo declares its type, then hands the check itself to the auditor, since section 4 above is judged by hand. **So a note naming a check id is retired by a person, not by a run.** Write it that way anyway. The id says exactly what would close the note, and the surfaced finding puts that decision in front of whoever runs the audit rather than leaving the note to sit until someone rereads it.
…mplies Review read "no per-type check runs here" as ambiguous about the cross-cutting checks, which live in the same catalog. It is, and checking the catalog against the runner shows the ambiguity was hiding a false claim of mine rather than a merely loose one. Several crossCutting checks are mechanized. branch.both-protected, branch.ruleset.develop and .main, setup.secrets.present and .noforbidden, setup.dependabot.ecosystems, setup.driftnotes.current, parity.cspell, recurring.norepoxref and readme.sections all have a deterministic counterpart in the runner. So "every check below is judged by hand, and none of them is mechanized" was wrong for a third of section 4, and widening per-type to all checks, the reading the comment invites, would have entrenched it. The line falls between the halves rather than around the catalog: no check belonging to a project type is evaluated anywhere, and the deterministic subset lands on several cross-cutting checks without being organized by them. All four restating sites now say project-type and name the partial cross-cutting coverage, and the driftNote finding says the audit does not evaluate the named check by id, which holds whichever half the id came from. Also show the check-id example with its parentheses, since the matcher requires them and the bare id is not detected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727
commented
Aug 5, 2026
Answering round 2's five suppressed findings here, since a suppressed finding has no thread to pair an answer to. All five accepted, fixed in 792d7c6. Four are one class, and chasing it found a false claim of mine underneath. The class: "per-type" read as excluding cross-cutting
The ambiguity is real and all four sites are fixed. But the closing clause each finding shares, that the audit evaluates none of the cross-cutting checks either, is not correct, and taking it as the fix would have made things worse. I checked the catalog against the runner rather than adopting it:
So roughly a third of section 4 is mechanized, and my own sentence in Fixed in the other direction. The line falls between the two halves of the catalog, not around it: no check belonging to a project type is evaluated anywhere, while the deterministic subset lands on several cross-cutting checks without being organized by them. The instance
Accepted, and worth more than a formatting nit given what this PR is about: an example that does not match the matcher is how the anchored-pattern defect got written in the first place. The example is now Gates after the fix: |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (2)
spec/audit.py:879
- audit_repo() now always calls driftnote_findings(), which assumes spec includes the loaded project-types catalog at spec["types"]. _selftest() (and any other minimal-spec callsites) pass a reduced spec dict, so this can raise KeyError even when the repo entry has no driftNotes to check. Guard the call so driftNotes logic only runs when driftNotes exist, and emit a clear ERROR if the catalog is missing when needed.
findings.extend(driftnote_findings(entry, spec, len(findings)))
spec/audit.py:1175
- The issue preamble sentence ends with "AUDIT.md section 4's", which reads like a possessive and is grammatically unclear. Since this text is user-facing in generated issues, it would be clearer to say the verdict "lives in" (or "is in") AUDIT.md section 4.
f"This lists what the audit mechanically detects. No check belonging to a project type in `spec/project-types.json` is run "
f"here, and the cross-cutting dimensions are covered only in part, so the full letter and intent verdict is AUDIT.md section 4's.")
… a possessive
Review flagged that driftnote_findings assumes the catalog is loaded, and
reached for a guard that skips the work when it is absent. Measured first: the
pairing raises KeyError('types'), and no live caller produces it, since the
loop body is the only reader and main() always loads the catalog. So the
defect is the message rather than the reachability.
It now raises naming spec/project-types.json, and a self-test case drives that
path. Deliberately still an error rather than the suggested skip. Resolving
ids against an absent catalog would report every id undefined, which hands out
a work list whose remedy is to delete correct notes, and skipping silently
would make the check report clean over notes it never read, which is the exact
failure this whole change exists to remove.
The generated issue preamble ended on "AUDIT.md section 4's", which is
user-facing text in a filed issue. It now reads "lives in AUDIT.md section 4".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>ptr727
commented
Aug 5, 2026
Round 3's two suppressed findings, answered here since neither has a thread. Both accepted, fixed in d5cb17a. The earlier five are answered above and unchanged. The catalog dependency
Measured rather than taken on the description, and the two halves land differently. The reachability half does not hold. The existing The message half is a real defect, so it is fixed. It now raises naming Deliberately still an error, not the suggested skip, and worth saying why since the suggestion is the more natural-looking option. Both fallbacks are worse than the raise. Resolving ids against an absent catalog reports every id undefined, which hands out a work list whose remedy is to delete correct notes. Skipping the check when the catalog is missing makes it report clean over notes it never read, which is precisely the false clean this PR exists to remove, reintroduced one layer down. The possessive
Accepted. It now reads "the full letter and intent verdict lives in AUDIT.md section 4". The user-facing point is the right one: this string is rendered into a filed issue, not printed to a terminal. Gates: |
Uh oh!
There was an error while loading. Please reload this page.
…574) Promotes the nine commits `develop` has carried since the previous promotion (#555) to `main`. Merge commit only, no squash, and `develop` is not deleted. ## What lands - **#560** Standardize the static-site-deploy repo type and its destination, including the deploy-verification guarantee and a retention rule that records which side owns the prune. - **#561** Ask for a blocked decision instead of reporting it, and scope the clickable-link rule to a surface that renders markdown. - **#564** Read a suppressed section nested inside the review-details wrapper, so `scripts/pr_review.py` stops reporting a clean round over findings that reach no thread. - **#566** Settle Markdown capitalization in `CODESTYLE.md`, and fix the two `TODO.md` prose defects the digest defect had hidden. - **#568** Check a `driftNote` on every run rather than only on an otherwise clean audit, and state what the audit does not evaluate. - **#569** Name who trips the production ref gate, and state that a harness refusal is a different thing from the maintainer's permission and is not lifted by it. - **#571** Re-audit `ptr727/Blog` against the hugo type and bump the conformance matrix. - **#573** Scope the semicolon exemption to the sentence its list lives in, with the 44 newly reported occurrences recorded on #519 rather than swept here. - **#572** Exempt a verbatim section from the coordination-reference scan. ## Issues closed Fixes#562. Closes#565. The closing keywords sit on this promotion rather than on the feature pull requests, because GitHub fires them only on a merge into the default branch. #456, #558, #563, #567, and #570 are already closed against their merged fixes. ## Verification `git merge-tree` reports no conflict between `origin/main` and `origin/develop`, so this promotion needs no throwaway resolution branch. Every constituent pull request merged green with its review loop closed.
Resolves the audit-tooling half of #563, filed from
ptr727/Blogafter its re-integration. All four of that issue's asks, plus a defect in one of the fixes it proposes.The gate that suppressed the check
spec/audit.pywrapped the whole freshness check inif not findings:. The rationale was sound in isolation, in that a clean repo has no outstanding work for a pending-marker note to describe, but the consequence is that any repo carrying one finding it cannot clear has its entiredriftNoteslist exempted. Blog carries exactly that, thecarried: AGENTS.md references the template repofinding tracked in #552, so its notes were never checked however clean the rest of the audit ran. The repo with open findings is where a stale note is most likely, which is the inverse of what the gate produced.Both shapes are now evaluated on every run, worded by context rather than suppressed:
pending,still, ...)Measured live, this is one added advisory across the fleet. Of 53 notes on 22 repos, one carries a marker (MediaTools,
pending fleet-wide ratification), and it now readswhile 25 finding(s) are open - confirm it describes one of them rather than closed work. Under the old gate it was silent.The check-id matcher, and why it is not anchored
#563 proposes
\(([a-z]+\.[a-z.]+)\)$. That pattern matches neither of the two notes it was written for, because both end the sentence after the paren:Run over the whole registry it matches zero notes on zero repos, which is indistinguishable from a fleet carrying no such note. That is the silent-narrowing shape
GOVERNANCE.md"Verification Discipline" names: a pattern that matches less still exits zero.CHECK_ID_REis therefore unanchored, and the self-test covers the trailing-period case, the mid-sentence case, an id absent from the catalog, an id whose type the repo does not declare, and a parenthesized version string as the false positive the shape has to exclude. Watched failing: re-anchoring the pattern to$turns five cases red and the suite reportsSELFTEST FAIL.What the audit will not claim
Resolving the id is as far as a tool can honestly go here, because
spec/audit.pynever readsspec/project-types.jsonat all. So the audit checks that the id exists and that the repo declares its type, then hands the check itself to the auditor. The finding clears when the note is deleted, which is the retirement path #560 promised, made visible rather than left to a matcher that could never fire.That same fact is now stated where an agent reads a run rather than only in the issue:
AUDIT.mdsection 4, the console line printed for a clean repo, the--issuebody preamble, andOPERATIONS.md. Adding a check toproject-types.jsonchanges what an auditor must judge and changes no tool's output, and silence from a tool that was never looking reads exactly like a pass.Blog's two notes are dropped
Both deviations closed in ptr727/Blog#30 and are on Blog's ground-truth
main(2b132e4), verified by reading that branch rather than trusting the issue:hugo.vendored.provenance-themes/README.mdrecords the upstream repository, commit154d006e, its upstream date,git describe, the license location, and both local edits.hugo.generator.pinned- the version and SHA256 are declared once at.github/actions/install-hugo/action.yml:26-27, andvalidate-task.yml:76anddeploy-site-task.yml:79both consume that composite action.Verification
spec/audit.py --selftestspec/validate.pyspec/audit.py Blog MediaTools(live)scripts/prose_lint.pyRefsrather thanCloses, since a closing keyword cannot fire from adevelop-targeted pull request. #563 is closed by hand with evidence once this merges.Filed by an agent in
ptr727/Blog, resolved here.🤖 Generated with Claude Code