Skip to content

Restructure the Prose Gate into Extract, Normalize, and Evaluate Layers - #3411

Merged
ptr727 merged 13 commits into
developfrom
refactor/prose-gate-layers
Oct 23, 2026
Merged

ptr727 merged 13 commits into
developfrom
refactor/prose-gate-layers

Conversation

@ptr727

@ptr727 ptr727 commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Summary

Restructures the prose gate into three layers and moves its data out of code, as decided for the prose follow-ups.

  • Extract, shared. Code and config go through the Pygments lexer the file name or shebang selects, reading comment, documentation, and docstring tokens. Markdown goes through markdown-it-py and its block line maps. A file whose lexer the data declares no language for is read as plain text, and the scope line names it.
  • Normalize, per language, from .github/actions/prose-gate/prose-gate.json. Comment markers, documentation and help forms, trailing checker directives, docstring section names, and nested lexes: a YAML run: block is lexed as bash, and a lambda: key or !lambda tag scalar as C++, each numbered by the file's own lines.
  • Evaluate, shared. Each rule is one function of a text unit in RULE_TABLE. The rules that lived inline in check_file moved out, and the sentence boundaries share one terminator definition.
  • Data. Rule names, defaults, and messages, the CLI, the charset tiers, the British spelling families, hub_hosted, the instruction documents, service accounts, dupword_allow, generated trees, the runbook exemption, and the not-prose and tool-directive pattern lists. The ledger test for hub_hosted reads the JSON.
  • Override. A repository may carry .prose-gate.json at its root with additions to the exemption lists only. Any other key, a non-list value, or unreadable JSON refuses the run.
  • Dependencies. PEP 723 block, requires-python = ">=3.13" (the spec/host-tools.json floor), markdown-it-py>=4.2.0 and pygments>=2.21.0, the versions tested against as a lower bound only, per the maintainer's ruling. A missing package or a missing prose-gate.json beside the script prints one message naming uv run --no-project --script and the stale hub_fetch_run.py snippet to resync, then exits 1, so a downstream hook still running the gate in-process fails with the remedy rather than a traceback. mypy ignores their missing stubs in the generic validator leg, and the gate type-checks clean against types-Pygments with the script's own interpreter.
  • Two Pygments rules corrected in subclasses rather than hand parsing: bash's heredoc ended at any later occurrence of its label (a $STUB_DIR ended a STUB heredoc) and read no double-quoted label, and C#'s attribute rule swallowed a collection expression opening on its own line, comments included.
  • Run the Prose Gate Through uv in hub_fetch_run.py and the Local Shim #3408. catalog/snippets/hub_fetch_run.py and scripts/prose_lint.py run the gate through uv run --no-project --script instead of runpy. The snippet fetches prose-gate.json from the same ref beside the script. The shim answers --help and refuses an unknown argument from the same data, with no uv start, and a test holds its help equal to the gate's.
  • Tests. One class per rule, one per language, one for Markdown, one for the override merge, single-incident classes folded in, cases that only exercised the replaced parser dropped, and a size ceiling of 1800 lines on prose_lint.py.

prose_lint.py is 1799 lines (from 3519), prose-gate.json 172.

Languages the gate reads comments in: Python (comments and docstrings), bash and sh (with the corrected heredoc), TOML, Terraform, Makefile, Ruby, Caddyfile, C, C++, Arduino, C#, JavaScript, TypeScript, JSON, JSON5, JSONC and .code-workspace (as JSON), CSS and SCSS (as CSS), HTML, XML and the MSBuild project formats (as XML), PowerShell, INI, .editorconfig, .conf, YAML (with the nested bash and C++ lexes), Dockerfile, git pattern files and extensionless scripts (as bash), and Markdown through markdown-it-py.

Closes #3408

Verdicts

Run develop before this change This branch
python3 scripts/prose_lint.py . --summary on develop at 4a77a1d1 0 violations across 0 files, 380 files read
develop's gate, python3 <develop prose_lint.py> . --summary, on this branch's tree 0 violations across 0 files, 382 files read
uv run --no-project --script .github/actions/prose-gate/prose_lint.py . --summary 0 violations across 0 files, 382 files read, 1 named as plain text (PSScriptAnalyzerSettings.psd1, no lexer, as before)

A wider comparison ran the old and the new check_file over every rule, opt-in ones included, on the committed trees of 23 local fleet clones, 6112 files. Every difference falls in one of these classes:

  • The old parser misread, the new one reads correctly (fewer findings): comments inside a C# raw string literal, CSS and script inside HTML files read as HTML comments, and a minified JavaScript bundle's template literals.
  • The old parser missed comments a lexer finds (more findings): a shell script where the old quote tracking lost its place, and ESPHome lambda C++ comments, which the nested lex now reads by design.
  • Heredoc bodies in four extensionless shell scripts (fewer findings): their bodies are remote scripts whose # lines the old parser read because its heredoc detection failed on them. Bash reads a heredoc body as data, and so does the gate now. In a YAML run: block the body is still read as code, which keeps the workflow verdicts the old YAML reading gave.
  • Opt-in sentence-length and sentence-split only: markdown-it-py's CommonMark blocks differ from the old approximation at a setext underline under a multi-line paragraph, a table needing its delimiter row, and the <search> and <source> block names. The shared sentence boundary now carries the ellipsis guard the comment rules already had.

Verification

Each command was run from the worktree, with the gate's packages on PYTHONPATH for the in-process test imports.

Command Result
uvx ruff@latest format --check . 265 files already formatted
uvx ruff@latest check . All checks passed
uvx mypy@latest Success: no issues found in 71 source files
uvx --with types-Pygments mypy@latest --python-executable <script env python> .github/actions/prose-gate/prose_lint.py Success: no issues found in 1 source file
python -m unittest discover -s tests, in a venv holding tests/requirements.txt, after merging develop Ran 2742 tests, OK (skipped=8)
uv run --no-project --script .github/actions/prose-gate/prose_lint.py . --summary 0 violations across 0 files
PROSE_ALLOW_COMMENTS=1 uv run --no-project --script .github/actions/prose-gate/prose_lint.py . --diff origin/develop 0 violations, 15 of 382 files read
python3 scripts/repo_gate.py composite-actions, eol, eol-coverage, sha-pin: 0 issues each
python3 spec/validate.py Spec validation OK

The comments label applies: this change writes code comment lines throughout the restructured script and tests.

Local review

Seven recorded local-strict-review passes, the last three clean. A hosted review raised five findings on 0caa9ad, each now answered: two fixed in bd12813 (a blank override entry is refused, and the help test turns color off on both sides), two fixed by the merge of develop (the test requirements), and one declined under the maintainer's transition ruling. Fixed from the first two: a comment after a block indicator emptying the script under it, a shift << 2 read as a heredoc, multi-line HTML comment lines read as paragraphs, inline comment line numbers, the generated_trees wording, an unused constant, a shebang assignment read as the interpreter, the line-rule-only sweep lexing every file (the warn-only charset-unknown sweep went from about 9 s to 1.4 s), stale statements in spec/host-tools.json, tests/test_repo_gate.py, and the snippet, and the pr_review.py block-name cross-check, restored against markdown-it-py's list.

Introduced findings left after the local edit budget, each with its outcome:

  • A run-block heredoc body with an apostrophe (didn't) opens a bash single-quoted string that runs to the next ' in the script, hiding the comments between. The old YAML reading did not. No current fleet file shows it in the comparison above. Reading the body as data instead would lose the inline-script comments workflows carry today. Deferred to Read a Run-Block Heredoc Body Without Letting Its Apostrophes Hide Later Comments #3413.
  • An inline comment after a code span holding its own text is numbered on the code span's line, so a comment rule's finding on it moves one line up and a diff touching only its real line drops it. The fix is skipping code_inline children in the line search. Deferred to Skip Inline Code Spans When Numbering an Inline Markdown Comment #3414.
  • The shim help test compared colored output under FORCE_COLOR on Python 3.14. Fixed in bd12813.

The test suite's packages come from tests/requirements.txt, which this branch extends with pygments and raises markdown-it-py to the tested floor.

Ruled by the maintainer: the downstream transition window is accepted, the fleet snippet being resynced right after the promotion, and the packages carry lower bounds only, the two Pygments patches refusing to load loudly if a release respells either stock rule.

Declined: a shared copy of the parser-building loop between the gate and the shim. The shim exists to answer --help without the gate's packages, so it cannot import the gate, and the new help-equality test holds the two copies together.

Pre-existing findings, not filed

  • sentence-length (opt-in) reports 152 sentences over the cap across the files this branch touches, most of them in text the branch moved rather than wrote.

🤖 Generated with Claude Code

ptr727 and others added 9 commits October 23, 2026 12:16
The rule names, defaults, and messages, the charset tiers, the British
spelling families, the exemption lists, the per-language comment data,
and the file-to-lexer mappings become data beside the script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pygments replaces the hand-written comment, string, and docstring
parsing, with its bash heredoc rule corrected to end only on the
terminator line. markdown-it-py replaces the Markdown block
approximation. A YAML run block and an ESPHome lambda are lexed again
as the script or C++ they hold. Each rule is a function of one text
unit, registered by name, and the sentence boundaries share one
definition. A repository may add exemptions in .prose-gate.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One class per rule, one per language for extraction and
normalization, one for Markdown, and one for the override merge, with
the single-incident classes folded into them. Cases that exercised the
replaced parser are dropped, and the size ceiling is asserted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pygments read a C# collection expression opening on its own line as an
attribute and lost the comments inside it, so that rule is patched the
way the bash heredoc rule is. A comment line's sentence boundary takes
no closing quote or bracket, which kept a quoted `'.'` from reading as
two sentences. The whole command line moves into prose-gate.json, so
the local shim can answer --help from it without starting the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both ran the gate in-process through runpy, which ignores its PEP 723
block, so its first dependency raised ImportError in every downstream
hook. Each now runs it as `uv run --no-project --script`, and
hub_fetch_run.py fetches prose-gate.json from the same commit beside it.
The shim answers --help and refuses an unknown argument itself.

Closes #3408

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A comment after a block indicator no longer empties the script under
it, a heredoc label opens on a letter so a shift hides nothing, a
multi-line HTML comment is no Markdown paragraph, an inline comment
keeps its line past a wrapped code span, and a shebang's assignment is
not its interpreter. A run reading only line rules skips the lexers.
The stale statements on scripts needing no packages are corrected, and
pr_review's block names are held to markdown-it-py's list again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The line search for an inline HTML comment resumes after the previous
one, so two identical comments keep their own lines. The shim help test
turns argparse color off, and the host tools entry says the tests need
the gate's packages where the gate itself does not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings October 23, 2026 20:09
@ptr727 ptr727 added the comments Permits the comment lines the pull request adds or edits, which the prose gate otherwise refuses label Oct 23, 2026
@coderabbitai

coderabbitai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ptr727

ptr727 commented Oct 23, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head bdcf136956ce6ababc9749241bb1bca436944c9c, the content this pull request carries at that commit against develop, and it recorded 2 findings.

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.

🟡 Changes recommended

Unresolved critical findings affect dependency installation, override validation, and downstream runtime compatibility.

5 open issues
What changed in this PR

This pull request restructures the prose gate into extraction, normalization, and evaluation layers, moving configuration into prose-gate.json and updating runners to use uv.

Changes:

  • Adds Pygments and markdown-it-py extraction, nested-language handling, repository overrides, and data-driven rules.
  • Updates the hub fetcher, local shim, tests, hook fixtures, specifications, and documentation for the companion data file and new runtime.
  • Expands rule, language, Markdown, CLI, override, and runner test coverage.

Outstanding findings remain in the final review:

  • .github/actions/prose-gate/prose_lint.py: unresolved critical findings cover empty tool-directive handling (1 vote) and missing runtime packages in the lint-only test leg (2 votes). Moderate findings cover Pygments compatibility, apostrophes and indentation in YAML run blocks, instruction HTML comments, empty spelling configuration, and EOF block scalars (1 vote each). A nit remains for CLI abbreviation handling (1 vote).
  • tests/test_prose_lint.py: a moderate finding (4 votes) requires normalizing color settings in the help comparison.
  • catalog/snippets/hub_fetch_run.py: a critical downstream resync finding and a nit concerning the loader contract remain (1 vote each).
  • pyproject.toml: a critical test-environment dependency finding remains (2 votes).
  • scripts/prose_lint.py: a nit remains for disabling CLI abbreviation (1 vote).
File Description
tests/​test_repo_gate.py Updates the gate-copy documentation test.
tests/​test_pre_commit_hook.py Includes the companion gate data file in hook fixtures.
tests/​test_pr_review.py Aligns CommonMark block-name validation with markdown-it-py.
tests/​test_hub_fetch_run.py Tests companion fetching, uv delegation, provenance, and failure handling.
spec/​host-tools.json Documents the prose gate’s dependency and interpreter requirements.
scripts/​README.md Documents the layered gate, data file, overrides, and uv usage.
scripts/​prose_lint.py Provides the dependency-free local shim and delegates execution through uv.
RESYNC.md Updates resynchronization guidance for the data-driven exemption file.
pyproject.toml Configures mypy handling for the gate’s external packages.
GOVERNANCE.md Documents companion fetching and uv-based gate execution.
docs/​fleet-map.md Updates the renamed dead-path test reference.
catalog/​snippets/​hub_fetch_run.py Fetches companion data and runs the fetched gate through uv.
.github/​actions/​prose-gate/​prose-gate.json Stores the data-driven rules, mappings, messages, exemptions, and CLI metadata.

Review effort: Lite

Comment thread catalog/snippets/hub_fetch_run.py Resolved
Comment thread pyproject.toml Resolved
Comment thread tests/test_prose_lint.py Resolved
ptr727 and others added 2 commits October 23, 2026 13:18
A hook that still imports the gate in-process gets one message naming
`uv run --no-project --script` and the stale snippet to resync, rather
than a traceback. Both packages carry the versions tested against as a
lower bound.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The old in-process snippet catches SystemExit and drops its message,
so the remedy is printed before the exit rather than carried by it. A
missing prose-gate.json beside the script names the same remedy, and
the test runs the gate the way that snippet did.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727

ptr727 commented Oct 23, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 647ec3d4ea830b6f9d6c69beafa9cf0405ac7ad2, the content this pull request carries at that commit against develop, and it recorded 0 findings.

…e-layers

# Conflicts:
#	pyproject.toml
#	spec/host-tools.json
#	tests/test_pr_review.py
@ptr727

ptr727 commented Oct 23, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head 0caa9ad0cb93a333c4505173260ab4b8dbf4a468, the content this pull request carries at that commit against develop, and it recorded 0 findings.

@codecov

codecov Bot commented Oct 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 20 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (develop@7862c48). Learn more about missing BASE report.

Files with missing lines Patch % Lines
scripts/prose_lint.py 0.00% 20 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             develop    #3411   +/-   ##
==========================================
  Coverage           ?   80.13%           
==========================================
  Files              ?       21           
  Lines              ?     9661           
  Branches           ?        0           
==========================================
  Hits               ?     7742           
  Misses             ?     1919           
  Partials           ?        0           
Flag Coverage Δ
python-3.13 80.13% <0.00%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

An empty directive name escaped to an empty pattern that matched every
comment and stood the comment rules down, so a blank entry under any
override key refuses the run. The shim help test sets NO_COLOR in the
child too, since FORCE_COLOR otherwise colors only that side.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ptr727

ptr727 commented Oct 23, 2026

Copy link
Copy Markdown
Owner Author

A recorded local strict-review pass covers head bd128136b6665a40757a89ff6dd7d78b3fe2062f, the content this pull request carries at that commit against develop, and it recorded 0 findings.

@ptr727
ptr727 merged commit f646430 into develop Oct 23, 2026
13 checks passed
@ptr727
ptr727 deleted the refactor/prose-gate-layers branch October 23, 2026 21:54
ptr727 added a commit that referenced this pull request Oct 24, 2026
…ry (#3416)

Implements the maintainer's decision of 2026-10-23 on #3391's follow-ups
for the audit, in his words: "audit, agree on moving tests to tests and
that should be consistent for the fleet to have tests and keep them in
tests, no hand written parser when a field tested and maintained
versions readily exists, so switch it and proceed".

## What Changes

### Tests Move to tests/

- `_selftest_cases`, the inline matrix in `spec/audit.py`, is now
`tests/test_audit_helpers.py`, a unittest module with one `subTest` per
case.
- The temporary git repositories are built by helpers on the test
classes.
  - The `globals()` swaps are now `unittest.mock.patch` calls.
- The invalid-byte probe tests move out of `tests/test_audit.py` along
with the probe.
- `--selftest` stays, as a smoke check of each helper family that runs
in under a second.

### Parsers

- `spec/audit.py` declares `pyyaml` and `markdown-it-py` in PEP 723
inline metadata.
- **YAML:** workflow jobs, contract tokens, the `python-directories`
input, the concurrency group, the schedule gate, and `dependabot.yml`'s
ecosystems are now read from the parsed document.
  - A job's verbatim region is cut at the parser's own marks.
- Contract tokens are matched against a view of the parsed job, indented
two spaces per level, with block-scalar bodies left out except an `if:`
condition.
- **Markdown:** headings, fenced blocks, the README title and intro,
link stripping, and the License column are now read from markdown-it-py
tokens.
- **Text scans that stay:** link definitions, image references, and tool
rows are still read as text. A parsed link keeps its target but drops
the reference name and position those checks read, and a comment beside
each scan says so.
- **`pyproject.toml`:** the audit reads only whether the file is
present, never its content, so there was no read to move to `tomllib`.

### The One Hand-Written Reader That Stays

The fence step and the `## ` region reading are still hand-written. They
now live in `spec/carried_text.py`, which uses the standard library
only.

- **What shares it:** the audit and all three scripts below use
`fence_step`. The audit and `scripts/carry.py` also use `normalize`, and
the audit uses `extract_section` and `strip_sections`.
- **Why it stays hand-written:** `scripts/carry.py`,
`scripts/build_dist.py`, and `scripts/canonical_review.py` run under a
plain `python3`, in CI and from the host menus, and carry.py writes the
verbatim regions the audit then hashes. The four tools must split a
document at the same boundaries, so moving the audit alone onto
markdown-it-py would let carry.py write a region the audit reads as
drift.
- **Follow-up:** a later lane moves the region reading of all four tools
onto markdown-it-py together, along with the switch of their callers to
`uv run --no-project --script`.

### Registry

- `spec/checks.json` registers one entry per check: its id, family, a
one-line description, the spec file it reads, its scope, and whether it
runs on the hub itself.
- `audit_repo` walks the registry in order and calls `check_<id>` for
each entry. It runs the per-file checks file by file, so findings keep
their order.
- Hub special cases that apply to a single check are now the registry's
`hub` flag. The hub's verbatim-section excision and the hub argument to
the baseline read stay in code, since neither belongs to one check.
- The suppression ids and the driftNote id parse now resolve against the
registry. `CHECK_ID_RE` moves into `spec/validate.py`.
- `spec/validate.py` now checks:
  - the registry's shape;
- that every file check has a baseline check before it in its run, on
the hub's filtered run too;
- that no check carries an unknown key, and that only a repo check sets
`ground: false`;
  - that every id a driftNote in `registry/repos.json` names is defined.

### Size

Trimming the docstrings reached the ceiling only by dropping what a
caller needs, so the Markdown reading and README checks move to
`spec/audit_markdown.py`. That family depends on nothing else in the
audit. Each docstring keeps what a caller needs: what the function
returns, what None or an empty result means, and what it raises.

| File | Lines | Ceiling in `tests/test_audit_size.py` |
| --- | --- | --- |
| `spec/audit.py` | 2442 | 3000 |
| `spec/audit_markdown.py` | 665 | 1000 |

`spec/checks.json` registers 33 checks.

### Callers

- `spec/workflow_reuse.py` and `spec/fidelity_honesty.py` import the
audit, so they declare the same libraries. Their usage lines, RESYNC.md,
and docs/reusable-workflows.md now run them through `uv run --no-project
--script`.
- `tests/requirements.txt` gains `pyyaml>=6`. The merge with develop
keeps #3411's lines beside it.
- mypy's missing-import override adds `yaml`.

### Validate Action

The coverage step now runs the helper suite, with the suite's
requirements installed, in place of `spec/audit.py --selftest`:

```sh
uvx --with-requirements tests/requirements.txt coverage@latest run --source=scripts,spec,host-setup -m unittest discover -s tests -p test_audit_helpers.py
```

`uvx` alone reads no script's inline metadata, so
`spec/workflow_reuse.py --selftest` gets the same `--with-requirements`.
OPERATIONS.md's local commands match.

## Before and After Audit Comparison

The full sweep, covering every cataloged repository plus the fleet
membership check, ran once at develop f646430 and once at this branch's
head. The finding lists match line for line, apart from the run stamp.
Before the parser commit, the hub plus PlexCleaner, ESPHome-Config, and
Blog also matched at 7862c48.

Where a parser now reads a shape the hand-written reader refused, the
helper suite asserts the new reading. None of these shapes occurs in the
audited repositories.

- A folded or continued concurrency group, or one written as a flow
mapping, is now read rather than reported as unreadable. A sequence is
still refused.
- A folded `python-directories` value, or a double-quoted one with
escapes, is now read the way GitHub passes it, rather than refused.
- A workflow that does not parse as YAML produces one `interface` DRIFT
saying its contract is unread, rather than one finding per missing job.
- A `[text][ref]` in the README tagline counts as a link only where
`ref` is defined, which is how GitHub renders it.

## Verification

Each command below ran in the worktree after the last commit.

- `uvx ruff@latest format --check .` -> `269 files already formatted`
- `uvx ruff@latest check .` -> `All checks passed!`
- `uvx mypy@latest` -> `Success: no issues found in 75 source files`
- `npx markdownlint-cli2` over the changed Markdown -> `Summary: 0
error(s)`
- `python -m unittest discover -s tests`, with tests/requirements.txt
installed -> `Ran 2837 tests ... OK (skipped=7)`
- `uv run --no-project --script spec/audit.py --selftest` -> `SELFTEST
PASS`
- `python3 scripts/prose_lint.py . --allow-comments --diff
origin/develop` -> no violations. Without `--allow-comments`, every
reported line is `comment-added`, which is why this pull request carries
the `comments` label.
- `python3 scripts/repo_gate.py` -> `composite-actions`, `eol`,
`eol-coverage`, and `sha-pin` all report 0 issues, exit 0.
- `python3 spec/validate.py` -> `Spec validation OK: 22 cataloged repos
classify cleanly. ...`
- `python3 scripts/build_dist.py --check` -> `Generated skill
distributions are current.` It ran under a plain `python3`, as did
`scripts/canonical_review.py report`.

## Local Review

`local-strict-review` passes ran at the `frontier` tier over the whole
branch diff. Each pass was recorded before the push it covers, and each
push is attested on this pull request. The fixes they drove:

- **Workflow reading:**
  - Contract tokens anchored on a newline match a job's first key.
  - A workflow is parsed once.
  - An unparseable workflow produces one `interface` finding.
  - An unparseable `dependabot.yml` produces one `dependabot` finding.
- The concurrency refusal names the shapes the parser actually refuses.
- **driftNote ids:** an id the catalog defines keeps its catalog
reading, and only an id spec/checks.json alone defines reads as
evaluated. This keeps the fleet sweep identical to develop.
- **Taglines:** a link-free tagline is returned unchanged, an image
counts as markup, and a code span is taken from the source as written.
- **Registry validation:** `spec/validate.py` refuses:
  - an unknown key;
  - a `ground: false` outside a repo check;
- a file check with no baseline check before it, on the hub's filtered
run too;
- a malformed id or suppression type. It reports that one rather than
raising on it.
- **Tests:**
- New tests pin which checks skip the hub, the hub's own run, and the
baseline slot each file check reads.
- Others pin `registry.driftnotes` running last, `split_jobs` at a
sibling key and at a file with no final newline, and the schedule gate
reading every `on:` shape.
- A declared Dependabot ecosystem is tested, and the smoke self-test is
gated by a test.
- `test_strip_md_links_stays_linear` checks a ratio rather than
wall-clock time, since CI's coverage-traced run took 1.08 s against the
old 1 s bound. It holds 20000 brackets under 8x the time for 5000, plus
0.05 s of slack. A quadratic reader fails that check unless it takes
under about 6 ms at 5000 brackets, and the old wall-clock bound passed
such a reader too.
- **Pointers:** every line that invokes the audit, or a script that
imports it, now spells `uv run --no-project --script`. Every pointer to
a moved function names its new home. The trim's dropped rationales are
restored as one line each. OPERATIONS.md says which tools share which
`spec/carried_text.py` primitive.

Declined with evidence:

- The validate action's helper-suite run duplicates the Python leg, and
`--selftest` stays. The brief fixes both ("runs the test module under
coverage instead", "`--selftest` stays as a smoke check").
- `spec/validate.py` refuses a driftNote whose parenthesized dotted
token names no check, and spec/checks.json carries `family`,
`description`, and `spec`. The brief requires both.
- The following stay as they read today, since a full sweep of every
cataloged repository matches develop and GitHub renders each case the
same way:
- markdown-it reads a heading inside an HTML block, a fence, or a
container differently from the old line regexes;
- a `## X ##` closing sequence reads differently for presence than for
verbatim extraction;
  - an undefined `[x][ref]` in a tagline reads as literal text.

The follow-up lane that moves all region and link reading onto
markdown-it-py removes the remaining split.
- `split_jobs` returns `{}` for a flow-style `jobs:` mapping, exactly as
develop's reader did, and no contract declares `verbatimJobs`.

## Pre-Existing Findings, Not Filed

- `--check sentence-length` reports pre-existing sentences that this
branch only touched to rename a path: docs/reusable-workflows.md:40,
scripts/build_dist.py:198, :202, :321, scripts/canonical_review.py:157,
scripts/carry.py:418, :492, and tests/test_build_dist.py:949.
- `spec/validate.py`'s `contains_description_markdown_link` still
matches brackets by hand.
- `CHECK_ID_RE` reads any parenthesized lowercase dotted token as a
check id, a file name such as `(cspell.json)` included.
- `scripts/canonical_review.py` and `scripts/carry.py` each write the
unfenced `## ` predicate inline rather than calling
`spec/carried_text.py`. The follow-up lane moves them.
- `tests/test_audit_size.py` follows the shape of the two size tests
already in tests/. Those two disagree on `assertLess` versus
`assertLessEqual`.
- `tests/workflow_steps.py` and `tests/test_docker_task_workflow.py`
read workflows by indentation, where PyYAML is now available to the
suite.
- The `reports/*.md` headers still print `python3` for their generators.
The generator strings now say `uv run`, so the next regeneration
rewrites the headers.
- `scripts/carry.py` keeps its own region reading (`section_span`,
`comparable_region`) beside `spec/carried_text.py`. The follow-up lane
covers it.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
ptr727 added a commit that referenced this pull request Oct 24, 2026
…#3417)

## Summary

Promotes the pull requests merged into develop since release 2.0.1148.
Each carries a maintainer decision of 2026-10-23 recorded on #3391.

- [#3407](#3407): rewrites
the write guard as a small lexer of 1,653 lines. It fails open under its
stated threat model and fails closed only on requirement 4. A test holds
it at 2,500 lines or fewer (item 1).
- [#3409](#3409): adds
CODESTYLE rules. Tests live in `tests/`, and a maintained parser
replaces a hand-written one. A script declares its dependencies in PEP
723 metadata and runs as `uv run --no-project --script`. It switches the
callers to that spelling and adds a managed allow rule to the
agent-safety installer (item 1 follow-ups).
- [#3410](#3410): splits
`pr_review.py` into the gate, a Copilot adapter reading its vocabulary
from JSON, and `pr_cleanup.py`. It collapses the exit codes and adds
`tests/requirements.txt` and the CI runners.
- [#3411](#3411): rebuilds
the prose gate on Pygments and markdown-it-py, with its vocabulary and
exemptions in `prose-gate.json` and a repo-local override.
- [#3415](#3415): holds
the Merge Gate on a part of a Copilot review body the tooling cannot
place. The hold lasts until an owner acknowledges it with outcomes (item
4 follow-up).
- [#3416](#3416): moves
the audit self-test to `tests/` and reads YAML and Markdown with PyYAML
and markdown-it-py. It adds the check registry `spec/checks.json` and
splits out `spec/audit_markdown.py`. It shares `spec/carried_text.py`
with carry, `build_dist`, and `canonical_review`.
- [#3418](#3418): fixes
the findings this promotion's first review round raised on the content
above. It aligns the drive-pr escalation line with pr-review-conduct and
names `tests/requirements.txt` where the prose said the tests need
nothing installed. It also restores the `uvx` launcher in a
release-guard assertion.
- [#3419](#3419): fixes
the finding the second review round raised. The Copilot adapter's prose
path reads a coverage count longer than `COUNT_DIGITS` as `unknown`,
sharing `count_state` with the marker path.
- [#3420](#3420): fixes
the finding the third review round raised, under the maintainer's
ruling. `hub_fetch_run.py` falls back to `git ls-remote` where the API
lookup of `main` fails, so the prose gate and its data are fetched at
one commit. Where neither lookup resolves, the documented fail-open
against `main` stays.
- [#3421](#3421): fixes
the findings the fourth review round raised. The Copilot adapter reads a
finding count longer than `COUNT_DIGITS` as unknown rather than zero,
through one length check and one None-aware sum. The host-setup
gates-and-tests row says `uv` installs the test requirements.

Closes #3408

## Constituent coverage

The Copilot rounds on this promotion ran at Lite effort and state no
full coverage, so Merge Gate item 2 rests on the promotion case. Each
change this promotion carries had full coverage on its own pull request
into develop, at its merged head:

| Pull request | Merged head | Coverage |
| --- | --- | --- |
| [#3407](#3407) |
`9efc1002` | attested local pass, `coverage=local` |
| [#3409](#3409) |
`bcc7a9e1` | attested local pass, `coverage=local` |
| [#3410](#3410) |
`00cec726` | attested local pass, `coverage=local` |
| [#3411](#3411) |
`bd128136` | attested local pass, `coverage=local` |
| [#3415](#3415) |
`5be9db87` | attested local pass, `coverage=local` |
| [#3416](#3416) |
`b914f083` | attested local pass, `coverage=local` |
| [#3418](#3418) |
`3a86cb51` | attested local pass, and a Copilot table naming exactly the
changed files, `coverage=table` |
| [#3419](#3419) |
`6e7b868d` | attested local pass, and a Copilot table naming exactly the
changed files, `coverage=table` |
| [#3420](#3420) |
`9b492290` | attested local pass, and a Copilot table naming exactly the
changed files, `coverage=table` |
| [#3421](#3421) |
`743010e6` | attested local pass, and a Copilot table naming exactly the
changed files, `coverage=table` |

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comments Permits the comment lines the pull request adds or edits, which the prose gate otherwise refuses

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants