Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
30 changes: 9 additions & 21 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,29 +38,19 @@ One pull request clearing the prose findings the hub's own docs and spec still c
- **Settled** - The three largest files are not snippets and are Python comments rather than prose, being [`spec/audit.py`][audit] at 99, [`gh-write-guard.py`][write-guard] at 52, and [`spec/validate.py`][validate] at 41 when measured before the sweep.
- **Settled** - A comment opening on a lowercase identifier is the bulk of what `comment-case` still reports, and the rule intends those restructured rather than exempted. The exemptions the snippets sweep added cover a commented-out key and a definition label, and nothing wider.

### Two Checks That Read What a Claim Points At
### Giving the Fleet's Own Pins Something to Resolve Against

One pull request adding two gates of the same shape, each confirming that a reference in a committed artifact still resolves, since both failures are silent and both are caught by a reviewer or not at all.
One pull request pointing a hub `uses:` at a hub-owned action, so that the resolvability pass added beside it has a reference under this owner to read. It is separated from that pass because it changes what a workflow runs, where the pass only changes what a gate reports.

**State** `ready`. **Touches** [`scripts/repo_gate.py`][repo-gate] and [`scripts/pr_review.py`][pr-review]. **Cost** one hub edit, hub-only, no sweep.
**State** `decision`. **Touches** the hub's own workflows. **Cost** one hub edit, hub-only, and it changes a running workflow so it is not a paper change.

- **Teach the `sha-pin` check to verify a pin resolves rather than that it is shaped like a SHA.** Forty hex characters is a format any fabricated string satisfies.
- **Blocked by** - Nothing.
- **Issue** - None filed.
- **Checked** - `develop` at `1ed0cc8` on 2026-08-03, where the check reads the shape and never the ref.
- **Open** - Nothing.
- **Settled** - An agent hand-writing a plausible SHA into a workflow is a real failure mode rather than a hypothetical one.
- **Settled** - Resolvability also catches the neighboring case, a pin whose commit was reachable only from a branch since squashed and deleted, which breaks a downstream gate long after the change that caused it.
- **Settled** - The network call is scoped to same-owner repositories, where the fleet's own actions live, and skips rather than fails when the host is offline so the local gate stays usable.
- **Settled** - The `gh-write-guard` hook cannot cover this, since it watches Bash and an editor tool writing the same string into a file never reaches it.

- **Check that a pull request description does not contradict its own branch.** Extract the commits and `uses:` refs the body quotes and confirm each still appears in the head tree.
- **Blocked by** - Nothing.
- **Decide whether the hub consumes its own [`prose-gate`][prose-gate] action the way the fleet does.** Today it calls `prose_lint.py` directly, so every `uses:` in the tree is under another owner.
- **Blocked by** - Nothing, though it is only worth doing on its own merits rather than to give a gate something to read.
- **Issue** - None filed.
- **Checked** - `develop` at `1ed0cc8` on 2026-08-03, where no check reads a description at all.
- **Open** - Nothing.
- **Settled** - Three stale descriptions in one session generated six review findings between them, each a reviewer noticing that the body named a commit, a branch, or a behavior the branch no longer carried.
- **Settled** - Prose claims stay out of scope, since judging those needs a similarity heuristic, which [`spec/section-model.md`][section-model] rejects for the reason it would fail here.
- **Checked** - `develop` at `dbd1cdc` on 2026-08-07, where the tree carries 45 pinned `uses:` refs and not one of them names a `ptr727` repository.
- **Open** - Whether the hub gating itself through its own pinned action is desirable at all, given the action reads the rules from hub `develop` on a non-`main` target and the hub already has the script in its own checkout.
- **Settled** - The resolvability pass reports what it covered on every run, so the hub's zero is visible rather than silent, which is why this is a separate decision rather than a defect in that pass.
- **Settled** - The fleet's `ptr727` pins are live in the downstream repos that consume the action, and `repo_gate.py --root <repo>` from a hub checkout reads them there, so the pass is not idle fleet-wide.

### Three Rules That Leave the Recurring Case Unstated

Expand Down Expand Up @@ -482,12 +472,10 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i
[matrix]: ./reports/conformance-matrix.md
[merge-bot]: ./.github/workflows/merge-bot-pull-request.yml
[operations]: ./OPERATIONS.md
[pr-review]: ./scripts/pr_review.py
[project-types]: ./spec/project-types.json
[prose-gate]: ./.github/actions/prose-gate/action.yml
[readme]: ./README.md
[readme-structure]: ./spec/readme-structure.md
[repo-gate]: ./scripts/repo_gate.py
[reports]: ./reports/
[repos]: ./registry/repos.json
[scripts]: ./scripts/README.md
Expand Down
22 changes: 20 additions & 2 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,31 @@ A comment sentence also has to start with a capital, which `comment-case` checks

Two deterministic checks:

- `sha-pin`: every workflow `uses:` is a 40-hex commit SHA, with the one documented `dotnet/nbgv@master` exception allowed.
- `sha-pin`: every workflow `uses:` naming an action is a 40-hex commit SHA that resolves, with the one documented `dotnet/nbgv@master` exception allowed. A local reusable workflow (`./.github/workflows/...`) names no action and carries no ref to pin, so it is skipped.
- `eol`: every path pinned LF in [`.gitattributes`][gitattributes] has the matching [`.editorconfig`][editorconfig] override the line-ending rule requires, with EditorConfig brace syntax expanded. One direction only: an `.editorconfig` LF glob with no git pin is legitimate, since `.editorconfig` governs what the editor writes where git enforces a class it must not guess at.

```sh
python3 scripts/repo_gate.py
python3 scripts/repo_gate.py --check sha-pin
```

**`sha-pin` resolves the pin as well as reading its shape**, because forty hex characters is a format any fabricated string satisfies, and an agent hand-writing a plausible SHA into a workflow is a failure this repo has seen rather than a hypothetical one. The `gh-write-guard` hook cannot cover it: the hook watches Bash, and an editor tool writing the same string into a file never reaches it. Resolving also catches the neighboring case, a pin whose commit was reachable only from a branch since squashed and deleted, which breaks a downstream gate long after the change that caused it.

The resolution pass is **scoped to the scanned repository's own owner**, which is where the fleet's actions live and where that decay comes from, since a squash merge deletes the branch a pin was taken from and the pin outlives the commit. A third-party action's tag is stable by comparison, and reading one would make every local run of this gate depend on a stranger's repository answering. The cost is real and is stated rather than left to be found: a fabricated pin on a third-party action is still only shape-checked. Every run therefore prints what the pass actually covered, because a scope that resolves nothing prints the same `0 issue(s)` a full sweep does. On the hub today it covers nothing, since this repo's own `uses:` refs are all under other owners and the fleet's `ptr727` pins live in the downstream repos that consume [`prose-gate`][prose-gate-action]. Those are reached by running this gate from a hub checkout with `--root`, per the hosted-and-reached model above.

A pin is a finding only where GitHub **answered** that the commit is absent, meaning a 404 or a 422. An offline host, a missing `gh`, a rate limit and a narrow token all report as unread and leave the pin on its shape, so the gate stays usable with no network instead of failing a correct tree. A 404 is confirmed against the repository itself before it becomes a finding, since an absent commit and a repository the credentials cannot see are the same answer from here, and a repository-scoped CI token is narrower than the fleet. That second read runs only on the failing path, and each distinct pin is read once however often it repeats.

A `note:` line is how a check says it did less than its name. It prints under the check's own result, outside the issue count, and never changes the exit code, because nothing is wrong with the tree when the network is what is missing. It carries one fixed shape every run so a zero is as visible as a count, and it separates a pin under a known other owner from one whose owner could not be compared at all, since a checkout with no readable `origin` skips every pin including this owner's own and reporting those as somebody else's is the same false clean the note was added to prevent.

A stale-backticked-path check was built and **rejected**: a template repo legitimately references paths that live in downstream repos, so it produced 34 false positives on a clean tree with no way to separate those from real drift. Doc-to-doc drift is a review lens, not a regex.

## `pr_review.py`

One compact digest of a pull request's Copilot review state, replacing a sequence of one-`gh`-call-per-turn polls. `status` prints the digest, `wait` runs the backoff in-process so a long review wait costs one agent turn instead of one per poll, and `reply` answers one thread and resolves it. Re-requesting a review stays out and its runbook is in [`.github/copilot-instructions.md`][copilot-instructions].
One compact digest of a pull request's Copilot review state, replacing a sequence of one-`gh`-call-per-turn polls. `status` prints the digest, `wait` runs the backoff in-process so a long review wait costs one agent turn instead of one per poll, `reply` answers one thread and resolves it, and `claims` reads the description against the branch it describes. Re-requesting a review stays out and its runbook is in [`.github/copilot-instructions.md`][copilot-instructions].

```sh
python3 scripts/pr_review.py status 452 --repo ptr727/ProjectTemplate
python3 scripts/pr_review.py claims 452 --repo ptr727/ProjectTemplate
python3 scripts/pr_review.py wait 452 --repo ptr727/ProjectTemplate --timeout 2700
python3 scripts/pr_review.py reply 452 --repo ptr727/ProjectTemplate \
--match "retry count is off by one" --body "Fixed in abc1234: the loop now stops at n." --resolve
Expand All @@ -119,6 +128,14 @@ Every failure is a stop rather than a fallback, because each alternative closes

What this trades away is stated rather than glossed. A mutation spelled as a `gh` command in a shell is read by the `gh-write-guard` hook and one this script performs is not, since the hook sees `python3 pr_review.py reply` and no `gh` write. That is a real loss of a second pair of eyes, and it is taken because what the hook guards against there is a fabricated id, which this removes at the source instead of catching after the fact. The guard's other rule is re-implemented here rather than assumed: the owner check above is the same scope rule, enforced in-process, and it is honest that it stops a mistake rather than a determined caller. The whole-source guard against every other state-changing call stays and was narrowed to these two documents rather than dropped when the first of them arrived.

`claims` checks that a description does not contradict its own branch, and it exists for the same reason `sha-pin` now resolves: a reference that points at nothing is a silent failure caught by a reviewer or not at all. Three stale descriptions in one session generated six review findings between them, each a reviewer noticing that the body named a commit or a behavior the branch no longer carried. It reads the commits a body **claims** the branch carries and the `uses:` refs it quotes, confirms the head tree still carries each, and exits `70` where one does not. Prose claims stay **out of scope**, since judging those needs a similarity heuristic, which [`spec/section-model.md`][section-model] rejects for exactly the reason it would fail here.

**Scanning for bare SHAs was built first and the corpus rejected it.** Over the 25 most recent merged pull requests it raised four findings, and every one was correct prose: a `develop` commit named as history, a SHA inside a pasted digest, and two commits in `ptr727/Blog` written without a URL. Nothing in the *form* of a bare SHA separates those from a real claim, and separating them by meaning is the heuristic already ruled out above. A path arm was measured on the same corpus and is worse, flagging 54 of 215 backticked candidates, nearly all of them bare basenames, `origin/develop`, and other repositories. What survives is the verb: a commit counts only where the body says it was fixed, landed, shipped, added, introduced, corrected, resolved, carried or amended in it. That alternation raises exactly one reference over the same 25, and that one is true. It is an **inclusion** list, so a phrasing nobody thought of costs a detection rather than producing a finding, which is the direction to be incomplete in, and a claimed SHA still has to carry a digit as a backstop on the list growing later, since `accede` and `defaced` inflect into all-hex English words.

A commit passes on **ancestry** rather than on membership of the branch's own commits, so a description may cite a commit it inherited from the base branch. A commit the repository does not carry at all and a commit this head does not descend from are both findings, and each names which it is, since the first is an amended-away SHA and the second is a branch cut elsewhere. The `uses:` refs are checked against the whole tree at head rather than a guessed set of workflow paths, because this repo carries `uses:` lines in catalog snippets and in documentation as well as under `.github/`, and a narrower surface would report a ref absent because it looked in the wrong place. One archive is also one request, where walking a listing costs a request per file and grows with the repository, and the match is on bytes so a file that does not decode is searched rather than skipped.

Undecided is a third answer here for the reason it is one in `repo_gate.py`. A reference GitHub did not answer for is left undecided rather than reported stale, and where **every** reference is undecided the run exits `71` and says so, because `stale=0` from a check that read nothing renders exactly like `stale=0` from one that did.

The match is on the block's heading rather than anywhere in the body, and on the runbook's alternation rather than on one phrasing, since the wording has already appeared two ways. A case asserts the script's pattern is the one the runbook publishes rather than a copy of it that can drift. Reading the whole body was the first implementation and its own review caught it: a review whose overview prose discusses suppressed findings carries none, and reporting that as a finding trains the reader to skim the field. A heading outside any `<details>` wrapper is still read, because reporting zero when the markup moves is the same false clean one level up, and that fallback takes a count so ordinary prose does not become one.

<!-- Internal -->
Expand All @@ -131,3 +148,4 @@ The match is on the block's heading rather than anywhere in the body, and on the
[governance]: ../GOVERNANCE.md
[governance-hub-hosted-tooling]: ../GOVERNANCE.md#hub-hosted-tooling
[prose-gate-action]: ../.github/actions/prose-gate/action.yml
[section-model]: ../spec/section-model.md
Loading
Loading