Skip to content

Scope the semicolon exemption to the sentence its list lives in - #573

Merged
ptr727 merged 1 commit into
developfrom
fix/prose-lint-semicolon-exemption-scope
Aug 6, 2026
Merged

Scope the semicolon exemption to the sentence its list lives in#573
ptr727 merged 1 commit into
developfrom
fix/prose-lint-semicolon-exemption-scope

Conversation

@ptr727

@ptr727 ptr727 commented Aug 5, 2026

Copy link
Copy Markdown
Owner

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:

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

A colon anywhere before a bullet's first semicolon marked the whole bullet a
list, so every semicolon after it was exempt however plainly it joined two
independent clauses, and the two did not have to be related at all. Measured
over this repo, that covered 62 spans holding 120 semicolons across 9 files
while the rule reported none of them, which is the silent-narrowing shape
GOVERNANCE.md "Verification Discipline" names.

Judge the exemption per sentence rather than per span, since that is where a
list lives. The boundary reuses the run-on rule's guards, so an initial or an
abbreviation ends nothing, and it accepts a terminator closing inside emphasis
or a bracket, since reading a bare `. ` left `.**` and `.)` joining a bullet's
every sentence into one span.

The colon arm was measured before being kept. Dropping it flagged 14 further
lines, and those were genuine colon-introduced lists whose items carry commas,
which is the standard use the rule names, so the arm is scoped rather than
removed. Both spellings of the bullet-opener colon are now dropped, since
matching `**Label**:` alone left `**Label:**` announcing a list it never
announced.

Old and new verdicts were diffed over the whole tree: 20 semicolon findings
become 64, nothing previously reported went silent, and every other rule is
byte-identical. The 44 newly reported are the existing backlog, corrected as
each file is next edited per GOVERNANCE.md, not swept here.

Also correct an OPERATIONS.md claim that `DEFAULT_RULES` omits `comment-wrap`
and `comment-case`, which it has not since they were added, and refresh the
TODO.md backlog figure, stale at 534 against a measured 520 before this change.

Fixes #570

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

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

This PR fixes an accuracy hole in the Markdown semicolon prose gate by scoping the “list exemption” to the sentence that contains the potential list, rather than applying it across an entire bullet/list-item span. This restores the intended behavior where a colon-introduced list in one sentence does not silently exempt unrelated clause-joining semicolons later in the same bullet.

Changes:

  • Update scripts/prose_lint.py to split Markdown spans into sentences (with Markdown-friendly sentence boundaries) and apply the list-exemption logic per sentence.
  • Expand the bullet label-colon stripping to treat both **Label**: and **Label:** as the same opener, preventing false “list announcement” detection.
  • Add targeted regression tests and update operational/docs text to reflect the corrected behavior and current gate semantics.

Reviewed changes

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

Show a summary per file
File Description
TODO.md Refreshes prose backlog metrics and adds a reference link for issue #570.
scripts/test_prose_lint.py Adds regression tests covering sentence-scoped exemptions and both label-colon spellings.
scripts/README.md Updates documentation of the semicolon rule to match the new sentence-scoped behavior and label-colon handling.
scripts/prose_lint.py Implements sentence-level splitting for semicolon exemptions and broadens LABEL_COLON matching.
OPERATIONS.md Corrects the description of DEFAULT_RULES vs CI invocation behavior and clarifies sentence-split invocation gaps.

@ptr727
ptr727 merged commit 34c3841 into develop Aug 6, 2026
7 checks passed
@ptr727
ptr727 deleted the fix/prose-lint-semicolon-exemption-scope branch August 6, 2026 00:13
ptr727 added a commit that referenced this pull request Aug 6, 2026
…574)

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

## What lands

- **#560** Standardize the static-site-deploy repo type and its
destination, including the deploy-verification guarantee and a retention
rule that records which side owns the prune.
- **#561** Ask for a blocked decision instead of reporting it, and scope
the clickable-link rule to a surface that renders markdown.
- **#564** Read a suppressed section nested inside the review-details
wrapper, so `scripts/pr_review.py` stops reporting a clean round over
findings that reach no thread.
- **#566** Settle Markdown capitalization in `CODESTYLE.md`, and fix the
two `TODO.md` prose defects the digest defect had hidden.
- **#568** Check a `driftNote` on every run rather than only on an
otherwise clean audit, and state what the audit does not evaluate.
- **#569** Name who trips the production ref gate, and state that a
harness refusal is a different thing from the maintainer's permission
and is not lifted by it.
- **#571** Re-audit `ptr727/Blog` against the hugo type and bump the
conformance matrix.
- **#573** Scope the semicolon exemption to the sentence its list lives
in, with the 44 newly reported occurrences recorded on #519 rather than
swept here.
- **#572** Exempt a verbatim section from the coordination-reference
scan.

## Issues closed

Fixes #562. Closes #565.

The closing keywords sit on this promotion rather than on the feature
pull requests, because GitHub fires them only on a merge into the
default branch. #456, #558, #563, #567, and #570 are already closed
against their merged fixes.

## Verification

`git merge-tree` reports no conflict between `origin/main` and
`origin/develop`, so this promotion needs no throwaway resolution
branch. Every constituent pull request merged green with its review loop
closed.
ptr727 added a commit that referenced this pull request Aug 8, 2026
Closes the last piece of
[#519](#519), which is
now closed with its evidence quoted on the issue.

## Why now

The prose backlog reached zero at `20916ad`. `semicolon` and `dash` were
warn-only because a whole-tree gate on them would have failed every run
while several hundred findings sat in the tree. That reason has expired,
and a warn-only tier over a clean tree is how the backlog grows back.

Measured on this branch before writing the change, and again after:

```text
python3 scripts/prose_lint.py . --check charset --check semicolon --check dash --check dupword \
  --check spelling --check comment-wrap --check comment-case --check home-path
0 violation(s) across 0 file(s)
```

## `home-path` gated nothing anywhere, which is the substantive fix

`home-path` has been in `DEFAULT_RULES` since it was written, so a bare
local run has always included it. It was named by neither CI step, so
the rule that catches an absolute home path naming a real account ran on
a developer's machine and gated nothing in CI. That is the
pattern-detectable sliver of
[`GOVERNANCE.md`](https://github.com/ptr727/ProjectTemplate/blob/develop/GOVERNANCE.md)
"Representative Data in Agent-Authored Text", the section that exists
because real paths carrying real names reached a public comment.


[`OPERATIONS.md`](https://github.com/ptr727/ProjectTemplate/blob/develop/OPERATIONS.md)
already recorded the hole, correctly and in detail. This closes it and
drops the record, rather than leaving a runbook describing a gap that no
longer exists.

## What stays warn-only, and why that is not an oversight

`charset-unknown` alone. A finding there names a character no tier
covers, and classifying one is a fleet-law edit rather than something
the change that happened to type it can fix. Blocking on it would make
an un-tiered character an unmergeable change instead of a question for
the maintainer. The step keeps `continue-on-error: true` and is renamed
from "Report prose backlog step" to "Report unclassified characters
step", since there is no backlog left for it to report.

`sentence-split` remains named by no invocation and stays that way here.
It is deliberately outside `DEFAULT_RULES`, so promoting it is a
separate decision rather than a consequence of this one, and
`OPERATIONS.md` still records it as a gap.

## The composite action is unaffected


[`.github/actions/prose-gate`](https://github.com/ptr727/ProjectTemplate/blob/develop/.github/actions/prose-gate/action.yml)
runs `python3 "$SCRIPT" --diff "$BASE"` with no `--check` list, so it
already gates the full default set, `home-path` included, over the lines
a change touches. A downstream caller sees no behavior change from this
pull request. Only the hub's own whole-tree steps move.

## Documentation kept level with the change

-
[`OPERATIONS.md`](https://github.com/ptr727/ProjectTemplate/blob/develop/OPERATIONS.md)
carries the two CI invocations verbatim so a local run matches CI rather
than exceeding it, so both lines are updated, the three-gap paragraph
becomes two, and the sentence describing the second invocation as a
backlog report is corrected.
-
[`scripts/README.md`](https://github.com/ptr727/ProjectTemplate/blob/develop/scripts/README.md)
claimed five rules gate and the rest report. It now states the tree is
zero and every default rule gates but one. It also names `home-path` for
the first time, including the limit that it closes a sliver of its
section and nothing more, because the exposure that section exists for
was name-shaped and no pattern finds a name.
- `TODO.md` loses the #519 entry and its link definition.

## Three corrections the closing comment carries rather than this diff

The `TODO.md` entry being removed held three wrong figures, each
re-measured before the issue was closed:

| Claim in the entry | Measured |
| --- | --- |
| Fixed by `f7a6a13` (snippets) | Not a valid object in this repository.
The snippets batch is `b002fac` (#600). |
| 557 across 45 to zero, 184 in snippets | 553 across 44 to zero,
batches 181, 241, 90 and 41, with today's checker run at every point |
| #573 accounts for 37 of the 38 carried findings | 38 of 38. Pre-#573
checker reports 0 and post-#573 reports 38 over the identical bytes at
`69688ec`. |

## Verification

| Check | Result |
| --- | --- |
| New gating invocation, whole tree | 0 violations across 0 files |
| `charset-unknown`, whole tree | 0 violations across 0 files |
| `python3 scripts/test_prose_lint.py` | 198 tests, OK |
| `python3 scripts/repo_gate.py` | `eol` 0, `sha-pin` 0 |
| `actionlint` | exit 0 |
| `editorconfig-checker` | exit 0 |
| `python3 spec/validate.py` | 22 cataloged, 0 backlog repos classify
cleanly |

The four edited files keep their declared line endings, CRLF for the
three Markdown files and LF for the workflow, which
`editorconfig-checker` confirms.

---------

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.

2 participants