Skip to content

Promote develop to main: the suppressed-findings fix and audit gates - #574

Merged
ptr727 merged 9 commits into
mainfrom
develop
Aug 6, 2026
Merged

Promote develop to main: the suppressed-findings fix and audit gates#574
ptr727 merged 9 commits into
mainfrom
develop

Conversation

@ptr727

@ptr727 ptr727 commented Aug 6, 2026

Copy link
Copy Markdown
Owner

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

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.

ptr727 and others added 9 commits August 5, 2026 06:10
Closes #456. Closes #558.

Blog has been running a Hugo build with an rsync-over-SSH deploy to a
host we own, and nothing governed it. `WORKFLOW.md`'s `Output Seam by
Destination` had no row for a filesystem on our own host,
`registry/repos.schema.json`'s `target` enum had no member for it, and
Blog stood cataloged as `source-only` behind two interim driftNotes
recording the deferral.

## What lands

**The `hugo` type** (`spec/project-types.json`), nine checks. Only
`hugo.build.strict` names the generator, where a generator-specific flag
*is* the letter. The rest are phrased generator-agnostically, so
promoting them to a shared type when a second generator arrives is a
registry edit rather than a rewrite. `spec/type-model.md` grows a
**Generators** section stating that rule and why there is no
`static-site` to `hugo` hierarchy at one member.

**The `self-hosted` target and the `deploy-ssh` mechanism.** What a repo
builds and where the result lands stay separate axes, so an SMB or S3
publisher later is a new *mechanism*, not a new type.

**D4.6 and D5.6.** D4.6 requires the deploy to assert *which release*
and *which environment* answered, polling to a bounded timeout, rather
than trusting the transport's exit status. D5.6 requires the prune of a
durable destination to be asserted on the host that was written to.

**A reference leaf pair** in `catalog/snippets/workflows/`, plus the 5A
addendum, scenarios S12/S13, and a section 6 walkthrough.

**Blog reclassified** to `["hugo", "source-only"]` with both publish
targets.

## Three questions a reviewer will ask

**Does D4.3 need extending?** No. It enumerates how a repo reaches the
tag-only release shape, and this destination adds no fourth route: the
deploy is a separate dispatch that touches no release, and Blog reaches
tag-only through the source-only route already listed.

**Why are `requires` and `stores` empty on `deploy-ssh`?** Not for want
of credentials. They are per-environment GitHub Environment secrets,
which neither `validate.py` nor `audit.py` can enumerate.
`validate.py:200` would force any `requires` name into the repo's
`requiredSecrets`, and `audit.py:615` unions that into the **actions**
store expectation, so listing them guarantees a false DEFECT on a
correctly configured repo. The new optional `environments` block records
the names as operator documentation and says plainly that it is not a
gate, so a clean audit is not evidence an environment is configured.
Extending the `stores` enum instead was measured and rejected:
`audit.py:606` seeds `required_by_store` with two keys and `:610`
indexes it unguarded, so an unknown store raises `KeyError` for every
repo whose `publish[]` maps to that mechanism. Follow-up folded into
`TODO.md`'s locally-required-secrets entry, since it is the same missing
axis.

**Why is the leaf concrete rather than parameterized?** An eleven-input
generalized transport leaf was proposed and rejected. The other twelve
leaves take `ref`/`branch`/`smoke` and nothing else; reuse is by
vendored copy, so a copier edits a `run:` line for free; the hub never
executes these files, so eleven parameters would be untested surface
presented as canonical; and naming it for the transport is the mistake
the type name avoids. The two outputs were kept, since Blog currently
has none and no caller can record what shipped.

## Two checks Blog fails today

Recorded as driftNotes naming their check id, so the next audit retires
them mechanically.

| Check | What is wrong | Filed |
| --- | --- | --- |
| `hugo.vendored.provenance` | The vendored theme is 125 tracked files
with no `.gitmodules`, no recorded upstream ref, and no Dependabot
ecosystem covering it. | ptr727/Blog#28 |
| `hugo.generator.pinned` | The generator version and checksum are
pinned in two workflows with nothing asserting they agree. |
ptr727/Blog#29 |

Both are fixed in ptr727/Blog#30.

## A third check was wrong, and the second commit fixes it

`hugo.deploy.retention` and D5.6 originally required the deploy to prune
the destination and assert the count on the host. **Blog cannot, and
should not be made to.** Its deploy credential is a forced `rsync`
command confined write-only, so the server never acts as sender and the
key can neither delete a release nor read the destination back to count
one. Blog's own ownership table already assigns release-prune timers to
the host, which is the correct resolution rather than a gap.

As first written the check was unsatisfiable for exactly the repos that
confine their credentials properly, and the only way to pass it was to
widen a deliberately narrow key. That trades a real confinement boundary
for a green check, so the check was wrong rather than the design.

Retention is now bounded by a **declared count with one side recorded as
owning the prune**: the deploy asserts it where its credential can
observe the destination, the host owns it where the credential cannot.
What the guarantee still rejects is a prune against a local scratch
tree, a best-effort prune, and neither side owning it, since each then
assumes the other prunes. The reference leaf keeps the
assert-in-pipeline shape and says when to delete the step. Blog passes
the corrected version, so its retention driftNote is dropped and
ptr727/Blog#27 is closed as invalid.

Worth noting as evidence for the type: a check written from the hub's
side alone, against a real repo, was wrong on first contact in a way
only the repo could reveal.

## The record was wrong, and is corrected rather than deleted

`TODO.md`'s intake entry predicted three things that are wrong against
what Blog actually runs, and writing the type from the prediction would
have encoded requirements the repo does not meet:

| Predicted | Measured |
| --- | --- |
| theme as a Dependabot-tracked submodule | vendored, no upstream ref
recorded |
| generator at `latest`, not pinned | pinned by version **and** SHA256 |
| tag cut last, after the live check | deploy is a separate dispatch;
the release is untouched |

## Drive-by fixes

- `spec/scope-model.md`'s project-type token table was missing `cpp`,
pre-existing.
- `STANDUP.md`'s new-type procedure never mentioned the registry
`target` enum, which is exactly what the first repo declaring a new
destination fails `validate.py` on. It also now warns that a leaf must
not be named `build-*-task.yml`, since `source-only.detect` is literally
that string.

## Verification

| Gate | Result |
| --- | --- |
| `spec/validate.py` | 22 cataloged, 0 backlog, clean |
| `spec/audit.py --selftest` | PASS |
| `scripts/repo_gate.py` | eol 0, sha-pin 0 |
| `scripts/prose_lint.py --diff develop` | clean (the 522-violation tree
backlog is #519's, untouched) |
| markdownlint-cli2 | 0 issues in 44 files |
| actionlint | clean |
| editorconfig-checker | only untracked `.artifacts/` and `Tests/obj/`
build output |

Selector resolution verified directly: Blog resolves to `{hugo,
source-only, release, dispatch-only, pull}`, the deploy leaf is
selected, `build-release-task.yml` is correctly **not** selected, the
`release` develop payload is selected over the operational one, and
`self-hosted` routes to `deploy-ssh` with an empty `requires` so no
secret finding is manufactured.

## Not in this PR

- The three Blog-side fixes, filed and fixed downstream.
- A fresh `reports/blog/audit.md`; the current one predates the deploy
and its staleness is noted in the conformance matrix.
- Real environment-scoped store support in `audit.py`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`GOVERNANCE.md` "Communicating with the User" governed the *form* of a
question (a numbered list) and said nothing about *where* it goes. So a
decision only the maintainer can make could satisfy the section while
sitting in the last paragraph of a summary, which is where it dies: a
summary reads as a report of finished work, the one line still waiting
on the user is the easiest in it to skim past, and the item ends up
owned by neither side while each believes the other has it.

## What lands

**One bullet in "Communicating with the User".** Work blocked on the
user is raised as a **direct interactive prompt with selectable
options**, at the point the work stops, never as prose in a summary. The
blocked item is the message rather than a closing remark on a message
about something else. It **narrows rather than replaces** the
numbered-list bullet above it: the list stays the form wherever the
interface offers no prompt mechanism, so an agent on a plain-text
surface is still covered.

**A second bullet-level clause: the options offered are the actions
themselves.** The first draft required the prompt and left its options
unspecified, which a prompt offering only ways to wait satisfies while
clearing nothing. The option that unblocks the work now names the action
it authorizes, so selecting it is the go-ahead, and where the agent may
not perform that action itself the option says who does.

**A third change, to the clickable-link bullet above them: the form
follows the surface.** That rule named one syntax, and a markdown link
is clickable only where markdown renders. An interactive prompt links
neither the markdown nor a bare URL, so following the rule as written
puts an unclickable string in front of the reader, which is the outcome
it exists to prevent. Its ban on a bare `#123` is now scoped to a
surface that renders markdown, and where none does, the reference is the
bare `#123` and the clickable link goes in the accompanying message.
Found by using the new prompt rule and watching the link fail in it.

**The closing-keyword rule moves from "Release Model" to "Branching
Model", unchanged.** That rule was queued for restatement in this PR,
and it was already law: closing keywords go on the `develop -> main`
promotion PR, because GitHub fires them only on a merge into the default
branch, with a hand-close citing the squash SHA as the fallback. Nothing
is added for it, since a second statement of a rule is the failure mode
rather than the fix. What was wrong was its address. It turns on
develop-versus-default-branch mechanics that "Branching Model" states
and "Release Model" does not, and an agent writing a feature PR body
reaches for the branch section, so it now follows the
promotion-execution traps beside the other `develop -> main` mechanics.
The move commit carries the line byte-identical, so it reads as a move
rather than as a rewrite. A later commit then formats its branch names
as code, matching the section it now sits in, which is a change to the
rule's text and is deliberately kept out of the move commit.

## For the reviewer

Both sections are `verbatim` fidelity in `spec/files.json`, so every
fleet repo carries the older copies until re-vendored, and an audit run
against a downstream repo before that reports both sections as
divergent.

A tree-wide grep for the moved rule's distinctive phrasing
(`issue-closing`, `closing keyword`, `Fixes #N`) finds no other site
restating or cross-referencing it, so no anchor or citation goes stale
with the move.

## Verification

`prose_lint` (both CI invocations), `repo_gate.py`, `spec/validate.py`,
`test_prose_lint.py`, markdownlint-cli2, and editorconfig-checker all
clean after each commit. The added line carries the file's CRLF endings.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
)

Fixes the false clean reported in #562: `scripts/pr_review.py` printed
`suppressed=0` over a review body carrying `### Suppressed comments
(2)`, so the gate that decides whether a review round is answered
reported a clean round over findings no thread will ever surface.

## Cause

The reviewer moved the section from its own `<details>` wrapper to a
markdown heading nested inside the `Review details` wrapper, and
`suppressed_blocks()` missed it twice over:

- The primary path matched the heading against the wrapper's
`<summary>`, which now reads `Review details`.
- The fallback that exists for exactly this case scanned
`DETAILS.sub('', body)`, which deletes every `<details>` block,
including the one the heading now sits in.

## Fix

Each region (every wrapper's contents, plus what is left outside them
all) is scanned line by line for the heading, so the section is found as
its own wrapper's `<summary>`, as a markdown heading nested inside
another wrapper, or bare in the body. Neither old shape is retargeted
away, since both appear across the rounds of a single pull request. A
block now starts at its own heading, so `finding_count()` reads the
heading's own `(N)` rather than the wrapper's, which would have floored
two findings to one.

## Verification

`python3 -m unittest discover -s scripts` passes (247 tests), with four
cases added for the nested shape: that it reports at all, that the count
is the heading's own, that both shapes report together in one run, and
that the neighbouring file-summary wrapper is still not read as a
finding.

Replayed over the reviewer's own bodies on the last seventeen pull
requests of this repository, the new parse recovers **6 findings across
5 rounds** that previously read as clean (on #546 and #561), and changes
no count that was already right.

The runbook section in `.github/copilot-instructions.md` gains the same
fact, since its rule to match more than one *phrasing* did not say the
section also *moves*, and a filter reading a wrapper's `<summary>`
reports zero on the nested shape.

Related: #562

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Answers the live half of #565, the six suppressed Copilot findings the
digest defect fixed in #562 hid. A suppressed finding reaches no thread,
so the digest was the only place it could have surfaced, and it reported
`suppressed=0` for five rounds across two pull requests.

## The two prose defects

Both are in `TODO.md`, both raised on #546 round 4, both still present
on `develop`.

| Entry | Was | Now |
| --- | --- | --- |
| downstream surface | `does not fail loudly, it quietly audits itself`
| two sentences |
| reading a local clone | `answers instead what that clone last saw` |
`answers a different question, which is what that clone last saw` |

## The capitalization convention

The reviewer asked for the proper noun on one bullet of #561, and that
bullet is now internally consistent in lowercase. So the live question
is not the line but the file-wide mix: `GOVERNANCE.md` alone carried
**17 lowercase against 7 capitalized**, and the same mix is vendored
fleet-wide, which is why settling it per file settles it nowhere.

**`CODESTYLE.md` "Markdown and Spelling" gains item 5.** Prose
capitalizes the format's name. Lowercase is for the strings a machine
reads and nothing else: a tool or package name (`markdownlint`,
`yzhang.markdown-all-in-one`), a settings key
(`markdown.extension.toc.levels`), a heading anchor
(`#markdown-and-spelling`), an identifier in code, and a file extension.
What it settles is the mix rather than either spelling, since a file
carrying both gives the next author no default and a reviewer a finding
to raise on whichever one it wrote last. It lands in `CODESTYLE.md`
because every repo carries that file, so the convention arrives with it
rather than being re-decided per repo.

**The sweep corrected 40 occurrences across 18 files** in documents,
code comments, docstrings, and two `spec/audit.py` output strings. No
identifier changed: the pattern skips a word adjacent to `.`, `-`, `_`,
`[`, `#`, or a paren, so `heading_texts(markdown)` and
`markdown.splitlines()` were left alone, and the result was read line by
line before applying.

## For the reviewer

Four `verbatim` regions changed, so every downstream repo is
byte-mismatched until re-vendored:

- Three `GOVERNANCE.md` sections by one word each, "Documentation Style
Conventions", "Communicating with the User", and "Repository Details".
- Two comment lines in `.markdownlint-cli2.jsonc`, which is `verbatim`
and `whole`, so a config nothing else changed about now reports as
drifted.

`CODESTYLE.md` is `intent`, so its new item reaches the fleet as a rule
each repo adopts in its own copy rather than as bytes to match.
`TODO.md`'s re-vendor entry records all of this and now names **five
files rather than three**, so the next sweep does not have to rediscover
it from a diff that reads cosmetic.

The three findings #565 records as already resolved were re-verified
against the tree and need no change here.

## Verification

| Gate | Result |
| --- | --- |
| `prose_lint.py` gating rules (charset, dupword, spelling) | clean
tree-wide |
| `test_prose_lint.py`, `test_repo_gate.py`, `test_pr_review.py` | pass
(157 prose tests) |
| `spec/audit.py --selftest` | PASS |
| `scripts/repo_gate.py` | eol 0, sha-pin 0 |
| `spec/validate.py` | 22 cataloged, 0 backlog, clean |
| markdownlint-cli2 | 0 issues in 44 files |
| cspell | 0 issues |
| editorconfig-checker | clean |

Every changed file kept its own line endings, which was checked after a
first attempt flattened CRLF to LF and was reverted before anything was
committed.

`prose_lint.py --diff` reports **five warn-only findings** (four
`comment-wrap`, one `dash`) on the three lines this diff touched in
`spec/audit.py`, `spec/validate.py`, and `spec/fidelity-model.md`. All
five predate this change and belong to the #519 backlog, where those
files carry 101 and 8 violations respectively. Correcting the one line
of each that this diff touches would leave the file no more conformant
while burying a one-word sweep in rewrapped comments.

## Not in this PR

- A `prose_lint` rule for the convention. The legitimate lowercase uses
are the whole difficulty, so a checker is a design question rather than
a follow-through, and stating the rule is what #565 asked for.
- The fleet re-vendor, which is `TODO.md`'s entry and needs the
maintainer to name the repos.

Related: #565

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ate (#568)

Resolves the audit-tooling half of #563, filed from `ptr727/Blog` after
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.py` wrapped the whole freshness check in `if 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 entire `driftNotes` list exempted. Blog carries exactly that,
the `carried: AGENTS.md references the template repo` finding 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:

| Note | Audit clean | Findings open |
| --- | --- | --- |
| prose marker (`pending`, `still`, ...) | contradicted outright, as
before | raised as which of the open findings it means |
| names a check id | surfaced for a hand decision | surfaced for a hand
decision |

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 reads `while 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:

> ... so it cannot be moved or diffed against upstream
(hugo.vendored.provenance)**.**

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_RE` is 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 reports `SELFTEST FAIL`.

## What the audit will not claim

Resolving the id is as far as a tool can honestly go here, because
`spec/audit.py` never reads `spec/project-types.json` at 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.md` section 4, the console line printed for a clean
repo, the `--issue` body preamble, and `OPERATIONS.md`. Adding a check
to `project-types.json` changes 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.md` records the upstream
repository, commit `154d006e`, 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`, and
`validate-task.yml:76` and `deploy-site-task.yml:79` both consume that
composite action.

## Verification

| Gate | Result |
| --- | --- |
| `spec/audit.py --selftest` | PASS, and FAIL on the anchored matcher |
| `spec/validate.py` | 22 cataloged, 0 backlog, clean |
| `spec/audit.py Blog MediaTools` (live) | behaves as described above |
| markdownlint-cli2 | 44 files, 0 issues |
| cspell (README, HISTORY) | 0 issues |
| editorconfig-checker | clean |
| `scripts/prose_lint.py` | no new violations on any touched file, net
-2 |

`Refs` rather than `Closes`, since a closing keyword cannot fire from a
`develop`-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](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…haped (#569)

Resolves #567, raised from ptr727/Blog#33 after the deploy transport
retest. Documentation only, no behavior change.

## The problem the contract created

The deploy contract guarantees a ref gate: a production deploy is
refused unless the dispatched ref is the default branch. **A gate is
shown to fail closed only by tripping it**, so the verification is one
`workflow_dispatch` of the production environment from a non-default
branch, expecting the run to fail at the gate job.

The agent that owns the pipeline is refused that dispatch by its
harness, and #567 records that conversational authorization did not lift
it: the maintainer authorized the call explicitly, in session, and the
identical call was refused again. The staging dispatch was allowed and
the same text inside an `echo` was allowed, so the refusal is a judgment
about the action rather than the subcommand or the ref. That judgment is
defensible and this change does not ask for it to be relaxed.

What the contract produced was the worst arrangement of it. The check is
required, the agent cannot perform it, and nothing said so. The two
predictable outcomes are an agent that drops the check and reports the
deploy verified, and an agent that re-expresses the refused call as a
raw `gh api -X POST .../dispatches`, which is a mutation fired as a
workaround.

## What changes

**`WORKFLOW.md` section 5C** gains the ref gate as a live probe, named
as the maintainer's step, with the evidence to capture. The evidence is
the point: the gate job's conclusion, its error text naming the expected
and the received ref, every downstream job recorded as **skipped**
rather than passed, and the deployment count against the production
environment unchanged. A gate that fails open and a gate nobody tripped
produce the same empty run history, so "we have never seen it fail" is
not evidence about the one control standing between a mis-dispatch and
the live site. The agent prepares the command and reads all four back
afterwards; it does not fire it. The same split covers a probe acting on
the deploy host directly, an outbound SSH exercising a forced command
among them, which #567 records failing the same way.

**`GOVERNANCE.md` "Repository Boundaries and Write Safety"** gains the
rule the re-shaping needs, and it is the natural home: that section
already bans a write fired as a probe and a write consuming a fabricated
id, and this is the third way a write goes somewhere nobody approved. It
states that a harness refusal and the maintainer's permission are
different things, that the second does not lift the first, that a
refused write is never re-attempted through a different API surface or
tool or rephrasing, and that writing oneself a permission rule is
self-authorization whatever was said. Two routes remain and both are the
maintainer's. The closing clause is the general form of #567's own
complaint: where a required verification can only be performed by a call
the agent is refused, the document requiring it says so and names who
runs it, since a check that is mandatory and unperformable is dropped
silently and then reported as done.

Grepped `GOVERNANCE.md`, `AGENTS.md`, and
`.github/copilot-instructions.md` for both claims before adding either.
Neither was stated anywhere, so this duplicates nothing and contradicts
nothing.

**`TODO.md`** adds the section to the fleet re-vendor entry. "Repository
Boundaries and Write Safety" is `verbatim` fidelity in
`spec/files.json`, so every downstream repo now carries an older copy
and an audit before the sweep reports it as stale. It joins the existing
list rather than opening a new item.

## Verification

| Gate | Result |
| --- | --- |
| markdownlint-cli2 | 44 files, 0 issues |
| cspell (README, HISTORY) | 0 issues |
| editorconfig-checker | clean |
| `scripts/prose_lint.py` | 0 violations on all three files, before and
after |
| `spec/validate.py` | clean |

`Refs` rather than `Closes`, since a closing keyword cannot fire from a
`develop`-targeted pull request. #567 is closed by hand with evidence
once this merges.

Filed by an agent in `ptr727/Blog`, resolved here.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…571)

The 2026-08-03 report graded Blog as source-only against a hub that had
no static-site type, and recorded that it was due a re-run once the
deploy existed. This is that run, replacing the file in whole per
AUDIT.md section 8.

Run stamp `audit run 2026-08-05T21:57:38Z | hub 01507a0`, read at Blog
`main@2b132e4`. Verdict operational, no defect.

All nine `hugo.*` checks are judged for the first time and pass, each
cited to file:line. The three deviations the first run recorded are
closed: ptr727/Blog#27 (the remote release tree was never pruned), #28
(the vendored theme recorded no upstream ref), and #29 (the generator
pin was duplicated across two workflows).

Two drift classes stay open, both the hub having advanced past what the
repo carries: seven stale verbatim units and 17 prose findings. One
needs sequencing, since the `Closes #N` rule moved out of Release Model
into Branching Model in #563, so a re-vendor taking the new Release
Model without the new Branching Model drops the rule rather than
leaving it stale.

The run also surfaced a hub defect. `spec/audit.py` flags a carried file
naming the hub anywhere in it, with no exemption for the byte-locked
AGENTS.md "Fleet Bootstrap" section, whose first sentence must name the
hub because saying where the canonical rules live is that section's
function. The finding is unclearable by construction and fires on any
repo carrying the current canonical, so it spreads with the carry.
Recorded as escalation 1 with the fix; not applied here, since it
changes the runner rather than a report.

The matrix takes the run's date and findings. Ten clause-joining
semicolons and three spaced hyphens elsewhere in the file are fixed in
the same pass. The 17 remaining `dash` findings are deliberately left:
every one is a `| - |` empty-table-cell placeholder, the notation the
file's own header documents, so they are a prose_lint.py exemption gap
rather than divergent content, systemic across 46 cells in 8 report
files.

Copilot review closed over two rounds, second covering the head.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Fixes #570.

## The defect

`scripts/prose_lint.py` judged the semicolon exemption over the whole
span, which for a Markdown bullet is the entire list item:

```python
listish = span.count(';') > 1 or ':' in span.split(';')[0]
if listish and ',' in span:
    continue
```

A colon anywhere before the first semicolon therefore marked the whole
bullet a list, and every semicolon later in it was exempt however
plainly it joined two independent clauses. The colon and the semicolon
did not have to be near each other, or related at all.

Measured over the 32 Markdown files in the gate's scope, the exemption
was covering **62 spans holding 120 semicolons across 9 files** while
the rule reported **0** of them, reproducing the issue's figures
exactly. That is the shape `GOVERNANCE.md` "Verification Discipline"
names: a pattern that silently matches less still exits zero.

## The fix

**The sentence is the unit**, because that is where a list lives.
`sentences()` splits a span at its sentence boundaries and each one is
judged on its own, so a genuine enumeration keeps its separators while
an independent clause later in the same bullet is still examined.

The boundary reuses the run-on rule's guards, so an initial, an
abbreviation, and a dotted identifier end nothing. It also accepts a
terminator that closes inside emphasis or a bracket: reading a bare `. `
left `.**` and `.)` joining a bullet's every sentence back into one
span, which would have left the defect in place for exactly the bullets
it appears in.

**The colon arm was measured before being kept.** The issue asked
whether a colon should confer the exemption at all. Dropping it reported
14 further lines, and reading them, they are genuine colon-introduced
lists whose items carry commas, which is the standard use
`GOVERNANCE.md` names, for example `Match the heading style: title case
with short bind words (a, an, the, of); hyphenated compounds capitalize
both parts.` So the arm is scoped rather than removed.

**Both spellings of the bullet-opener colon are now dropped.**
`LABEL_COLON` matched `**Label**:` only, so `**Label:**` still announced
a list it never announced. One line tree-wide was affected, and it is a
splice.

## Verdict diff, old against new on the same tree

| | before | after |
| --- | --- | --- |
| `semicolon` findings, whole tree | 20 | 64 |
| Previously reported, now silent | | **0** |
| Every other rule | | byte-identical |

The 44 newly reported sit in 33 lines across 7 files. Per
`GOVERNANCE.md`, existing prose is corrected as each file is next edited
rather than swept, and the issue itself asks that the exemption be fixed
and proven before anything acts on the work list, so no prose is changed
here. The measured list goes to #519.

## Tests

Six cases added to `TestSemicolon2`, each one proving a decision rather
than restating the code:

- a colon in an earlier sentence no longer exempts a later splice, the
issue's own example
- a sentence closing inside emphasis or a bracket still ends, so `.**`
and `.)` split
- a series in one sentence does not exempt the next
- a colon-introduced list whose items carry commas stays exempt, which
is why the arm was kept
- `- **D3:**` and `- **D3**:` are one construct
- an abbreviation does not end a sentence, since splitting at `e.g.`
would cut a list in half

All 253 script tests pass. `markdownlint`, `editorconfig-checker`,
`repo_gate.py`, `spec/validate.py` and the diff-scoped prose gate are
clean, and `ruff` reports nothing new against the pre-change baseline.

## Two corrections found while auditing the surfaces that describe this
gate

- `OPERATIONS.md` claimed `DEFAULT_RULES` omits `comment-wrap` and
`comment-case`, which it has not since they were added. The real
difference is the exit code, not the coverage.
- The `TODO.md` backlog figure read 534 against a measured 520 before
this change, so it is refreshed to 559 with a note that a gate fix moves
it as readily as a prose fix does.

## Fleet effect

`.github/actions/prose-gate` reads `prose_lint.py` from hub `develop`
for every non-`main` run, so once this merges, fleet PRs gate on the
corrected rule against their changed lines immediately.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found by the Blog re-audit in #571 and recorded there as escalation 1.

## The contradiction

The rule bans a reference to the template repo in any carried file,
because the coordination flow is machinery a consumer should not see.
`AGENTS.md` "Fleet Bootstrap" is byte-locked fleet-wide and its first
sentence names the hub by path, because saying where the canonical rules
live is that section's entire function. It is what an agent reads in a
repository whose carried copies are stale, partial, or absent, which is
precisely when nothing else present can say it.

The two rules therefore contradicted each other. A repository carrying
the current canonical was flagged for holding a section the hub requires
it to hold, byte for byte, and the only way to clear the finding was to
edit that section and fail the verbatim check instead.

**The finding was unclearable by construction, and it was going to
spread rather than stay a curiosity**, since every repository picks it
up as the carry propagates. Two repositories had reached it already.

## Three surfaces stated the rule, so three change

| Surface | Change |
| --- | --- |
| `spec/audit.py` | A new `template_ref_outside_verbatim` helper excises
a file's verbatim sections before looking for the name |
| `GOVERNANCE.md` "Documentation Style Conventions" | Carries the
exception, its reason, and its boundary |
| `recurring.norepoxref` in `spec/project-types.json` | The same, since
that is the check an auditor judges by hand |

A tool fix that left the written rule contradicting it would have fixed
nothing, which is why all three move together.

The exemption is **derived from the declared verbatim list rather than
hardcoded to one heading**, so a future verbatim section that must name
the hub is covered without a second fix.

**The exception stops at the region boundary.** A repository naming the
destination in prose it owns is still flagged, which is the point. A
reference that reaches a verbatim section is a defect in the canonical,
fixed once at the source rather than reported against every repository
carrying it.

## Verification

**Six offline cases in `--selftest`**, covering the exempt and
non-exempt positions, a document with nothing declared verbatim, a
re-cased heading, and a CRLF document. The last matters because
`extract_section` normalizes EOLs while carried files are CRLF on this
fleet, so excision has to survive that.

**Old versus new predicate across all 21 non-hub cataloged
repositories.** It clears exactly two findings, both `AGENTS.md`, on the
two repositories carrying the current Fleet Bootstrap, and keeps the
other eleven:

```text
HomeAutomation-Config   AGENTS.md                        True  False <== CLEARED
Blog                    AGENTS.md                        True  False <== CLEARED
Utilities               .github/copilot-instructions.md  True  True  kept
NxWitness               AGENTS.md                        True  True  kept
...                                                                  (11 kept)
```

Live runs agree. Blog loses the false positive with its seven genuine
verbatim drifts unchanged, and Utilities keeps its genuine one.

## Incidental

The scan moved into the file loop, so it reads content already fetched
for the section checks instead of re-fetching all three files per
repository, and reads the same selector-resolved verbatim list those
checks were judged against.

The conformance matrix note claiming the check has no exemption is
updated in the same pass rather than left to contradict the code. The
Blog report is deliberately **not** edited: it is a point-in-time run
snapshot, and the run-stamp discipline in AUDIT.md section 8 says a
later run supersedes the file rather than an edit revising it.

Note that `GOVERNANCE.md` "Documentation Style Conventions" is verbatim
fleet law, so every downstream repository's copy goes stale on merge and
is due a re-vendor.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 6, 2026 00:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Promotes the accumulated develop changes to main, updating the hub's governance/spec manifests and audit/review tooling so fleet repositories can be audited and reviewed correctly (notably: suppressed Copilot findings parsing, static-site deploy modeling, and driftNote/audit gating behavior).

Changes:

  • Extend the workflow/spec model to cover Hugo/static-site deploys to a self-hosted filesystem target (new target enum + deploy mechanism + interface workflow snippets + new hugo type checks).
  • Fix and harden review/audit tooling: correctly detect suppressed Copilot findings even when nested/moved; always evaluate driftNotes; and exempt verbatim regions from the template-reference scan.
  • Settle/standardize “Markdown” capitalization across docs, comments, and tool output strings.

Reviewed changes

Copilot reviewed 33 out of 33 changed files in this pull request and generated no comments.

Show a summary per file
File Description
WORKFLOW.md Documents the self-hosted filesystem deploy seam, deploy verification (D4.6), retention ownership (D5.6), scenarios (S12/S13), and probe guidance.
STANDUP.md Clarifies onboarding steps for new types/targets (including schema enum updates) and reinforces OPERATIONS.md/leaf naming constraints.
spec/validate.py Updates validation error strings to use “Markdown” consistently.
spec/type-model.md Adds “Generators” guidance for generator-vs-transport separation (supports hugo type design).
spec/section-model.md Aligns prose with “Markdown” capitalization and inline-link exception wording.
spec/secrets.schema.json Adds an environments documentation block shape to the secrets schema.
spec/secrets.json Adds deploy-ssh mechanism and maps self-hosted target to it; expands note to explain env-scoped secret limitations.
spec/scope-model.md Updates the project-type token table (adds cpp, hugo).
spec/readme-structure.md Updates prose to “Markdown” capitalization in readme-shape spec.
spec/project-types.json Introduces the hugo type with deploy/retention/verification expectations and updates cross-cutting “no repo xref” rule text.
spec/files.json Adds interface expectations for .github/workflows/deploy-site*.yml for hugo repos.
spec/fidelity-model.md Updates fidelity model prose to “Markdown” capitalization.
spec/fidelity_honesty.py Updates report-rendering docstring to “Markdown” capitalization.
spec/audit.py Fixes driftNote evaluation gating; adds check-id driftNote resolution; moves template-ref scan into the file loop with verbatim-section excision and new selftests; clarifies audit output scope.
scripts/test_prose_lint.py Adds regression coverage for sentence-scoped semicolon exemption and label-colon variants; updates “Markdown” capitalization in docstrings/comments.
scripts/test_pr_review.py Adds fixtures/tests for suppressed findings nested inside “Review details” wrapper and combined-shape handling.
scripts/README.md Updates the prose-lint documentation to reflect sentence-scoped semicolon exemptions and label-colon handling.
scripts/prose_lint.py Scopes semicolon list exemptions to sentences (not whole bullets), improves label-colon stripping, and adds sentence splitting helper.
scripts/pr_review.py Fixes suppressed-findings extraction to handle moved/nested headings and count extraction from the section heading itself.
reports/conformance-matrix.md Updates shape definitions and adds hugo + self-hosted target coverage notes.
reports/blog/audit.md Replaces the Blog audit report with the re-audit against the new hugo type and deploy model.
registry/repos.schema.json Extends the target enum to include self-hosted.
registry/repos.json Reclassifies Blog as hugo + source-only and adds self-hosted publish target; updates driftNotes accordingly.
OPERATIONS.md Clarifies gate invocation differences and audit-run interpretation (deterministic subset vs hand-judged checks).
GOVERNANCE.md Adds harness-refusal/non-reshaping write-safety rule; moves issue-closing keyword rule; adds coordination-reference exception for verbatim regions; scopes “clickable link” rule to Markdown-rendering surfaces; adds interactive prompt requirement.
CODESTYLE.md Establishes “Markdown” capitalization convention and applies it in the general rules.
catalog/snippets/workflows/README.md Registers the new deploy-site workflow snippets in the catalog index.
catalog/snippets/workflows/deploy-site.yml Adds the dispatch entry-point workflow for environment-scoped deploys with early ref gating and shared validation.
catalog/snippets/workflows/deploy-site-task.yml Adds the reusable deploy task workflow (generator pinning, env assertion, rsync deploy, optional prune step, live verification).
AUDIT.md Clarifies what spec/audit.py does/does not evaluate, adds hugo detection guidance, and expands driftNote reconciliation rules.
.markdownlint-cli2.jsonc Updates comments to use “Markdown” capitalization.
.github/copilot-instructions.md Updates runbook guidance to note suppressed-findings sections can move/nest and must be matched by heading location, not wrapper summary alone.

@ptr727
ptr727 merged commit 1b4a80e into main Aug 6, 2026
7 checks passed
ptr727 added a commit that referenced this pull request Aug 6, 2026
…e onboarding-feedback backlog (#582)

Promotes the two commits `develop` has carried since the previous
promotion (#574) to `main`. Merge commit only, no squash, and `develop`
is not deleted.

## What lands

- **#575** Require an explicit `--repo` on `scripts/pr_review.py`, since
the old default silently digested a pull request in the hub when a run
in another repository omitted it, and lead the digest summary with the
repository it read so a misdirected run is visible in its own output.
- **#576** Fold the two-day onboarding-feedback pass into `TODO.md`: ten
new entries, three amendments that each supply the second example their
entry was reasoning from one instance without, and two recurring cases
traced to underspecified wording in rules that already exist.

## Issues closed

None. Neither constituent pull request carried a closing keyword, and
the four issues #576 filed (#577, #578, #579, #580) record work that is
still open rather than work this promotion completes.

## Verification

`git merge-tree --write-tree origin/main origin/develop` reports no
conflict, so this promotion needs no throwaway resolution branch, and
the tree diff against `main` is exactly the five files the two commits
touch. Both constituent pull requests merged green with their review
loops closed.
ptr727 added a commit that referenced this pull request Aug 8, 2026
)

Closes #623.

**Disposition** `Amends "Find out which file a partial round skips, and
why re-requesting never clears it."` in the "A Programmatic Reading of a
Copilot Review" cluster. #623 was filed after this repository's last
`TODO.md` edit and appeared nowhere in the file, so selection step 1 was
false until this change.

## What was wrong

The `COVERAGE_IS_PARTIAL` caveat, `scripts/README.md` and the runbook
all said the reviewer names no file list in these rounds, so which file
went unread cannot be read from the API. Measured over 348 Copilot
review bodies on the newest 120 pull requests here and 121 on Blog, that
is false. 91 bodies carry a `| File | Description |` table, and every
table row in the corpus belongs to one of those tables.

## What the measurement says the table is worth

The issue proposed reading a table that names every changed file as
evidence that the count is a reporting artifact. It cannot carry that,
and the corpus is what says so.

| round | states | names | reading |
| --- | --- | --- | --- |
| #476 | 10 of 11 | all 11 | table contradicts the count |
| #592 | 2 of 3 | all 3 | table contradicts the count |
| #479 | 16 of 17 | 16, omitting `GOVERNANCE.md` | table corroborates
the count and names the file |
| #609 | 61 of 62 | 50 of 62 | short by 12 where the count is short by 1
|
| #574 | 33 of 33, full | 32, omitting `TODO.md` | a short table on a
fully covered round |
| #606 | 7 of 7 | names `GOVENANCE.md` | a path no diff carries |

On Blog every table names exactly the changed set, on all seven partial
rounds and on every full round alike. A reading identical under both
outcomes discriminates neither, so a full table is reported as
corroborating nothing rather than as a miscount. #574 kills the
converse, since a short table sits on a round that read everything.

## What ships

- `Q_FULL` selects the pull request's own `files`, windowed at 100 with
`hasNextPage` carried, since a path outside a short window reads exactly
like a path the reviewer left out and the record holds a pull request of
301 changed files.
- `file_table` reads the table, quotations dropped for the reason the
coverage line's are.
- `head_table` takes it from any round covering the current head and
never from a round before a push. Three of the four partials here carry
their table on the pre-push round, describing a diff that push replaced,
and comparing that against the current changed files would name a file
unreviewed on a stale list. Thirteen commits here carry more than one
round, and on one of them a round with a table sits beside a round
without, so which of the two the verdict reads must not decide whether a
table is found.
- `table_against_diff` prints one sentence and decides nothing. It names
an omitted file only where the table is short by exactly what the counts
leave unread **and** names nothing outside the diff, that second guard
existing because #606's typo would otherwise push the real
`GOVERNANCE.md` into the omissions and report it as the file nobody
reviewed.
- Exit `42` is unchanged in every arm, and the caveat keeps its
maintainer-decision framing, which is the issue's third suggestion.

## Verified against live data

#479 the reviewer's own file table omits exactly the 1 file the counts
leave unread, naming GOVERNANCE.md
Blog #60 the reviewer's own file table names all 6 changed files, ... so
it corroborates nothing
    #476/#592/#609   no round covering this head carries a file table

## Gates

228 `test_pr_review` cases, plus `test_prose_lint`, `test_repo_gate`,
`spec/audit.py --selftest`, `spec/validate.py`, both `prose_lint.py`
invocations, `markdownlint-cli2` and `editorconfig-checker`, all green
locally.

Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

Answer the six suppressed findings the digest never surfaced pr_review.py reports a clean round over suppressed findings it cannot see

2 participants