Skip to content

Tier the charset rule, invert the prose rules, and gate comments in every language - #459

Merged
ptr727 merged 6 commits into
developfrom
docs/tiered-charset-and-prose-rules
Jul 30, 2026
Merged

Tier the charset rule, invert the prose rules, and gate comments in every language#459
ptr727 merged 6 commits into
developfrom
docs/tiered-charset-and-prose-rules

Conversation

@ptr727

@ptr727 ptr727 commented Jul 30, 2026

Copy link
Copy Markdown
Owner

The flat non-ASCII ban could not express context. reports/homeassistant-purpleair/audit.md classified U+2264 and U+2265 as the scientific carve-out while the rule named both must-replace. That is not a wrong reading so much as a rule with no vocabulary for the distinction.

Three tiers

Tier Meaning Behavior
1 Typography carrying no meaning its ASCII form loses always flags
2 Relational and arithmetic operators kept beside a number, unit, or another operator; replaced between words
3 Unit and scientific symbols never flags
none Unclassified reported, not passed

Measured behavior:

Range <=2264> 50 <B5>g/m<B3> at 25 <B0>C <B1> 2.   -> clean
0 <=2264> x <=2264> 100                            -> clean
the check <=2264> the threshold                    -> flag, use `<=`
a <=2014> clause break                             -> flag, restructure
a <=2603> appeared                                 -> charset-unknown, classify it

Clean tree-wide, so charset gates. The audit report is corrected in the same pass.

Two prose rules inverted

The pronoun-keyed splice pattern found 170 of 493 and missed every imperative one, so any prose semicolon now flags and the exception is the list the rule already names. The em-dash replacement becomes "restructure the sentence" rather than a spaced hyphen, and that hyphen is banned in its own right: allowing the ASCII spelling would keep the shape and change only the character.

Both are markdown-only until a comment can be told from code. repo-config/configure.sh alone carries 78 statement separators that are not prose. Both are warn-only.

Comments, in every language the fleet declares

Driven by the declared project types (csharp, cpp, python, docker, homeassistant, eda, docs, nuget, pypi) rather than by whichever file was open:

Marker Languages
//, /* */ C#, C, C++, JSONC, JSON, .code-workspace, JS/TS, CSS
# Python via tokenize, shell, YAML, TOML, .gitattributes
<!-- --> XML, .csproj, .props, markdown
<# #> PowerShell
;, # INI, .editorconfig
///, /**, docstrings exempt, CODESTYLE governs those

JSON is read as JSONC, because that is what ships. Four tracked files were invisible before, including catalog/snippets/configs/vscode-tasks.json and both devcontainer.json snippets, which downstream repos copy. Their 37 malformed comments are fixed here so the shape stops propagating.

A marker inside a string literal is not a comment, verified in C#, shell, JSON, and Python.

Two rules come out of it: one sentence per comment line, never wrapped and never two on a line, and a sentence that starts on a line starts with a capital. Where the first word is a tool whose own casing is lowercase, the fix is to restructure rather than to capitalize against CODESTYLE tooling-casing.

Verification Discipline

The mechanisms behind the above land as fleet law: a gate has to be watched failing, driving the real table rather than a restatement of it, and a heredoc consumes a backslash escape and leaves an invisible control character behind.

Watched failing

  • Drop U+2264 from tier 2 -> 2 cases fail, naming the codepoint and tier.
  • Drop U+2126 from tier 3 -> 2 cases fail.
  • Drop the .json mapping -> 5 cases fail, naming the file type.
  • Disable the capitalization check -> 1 case fails.
  • Break the workflow glob -> repo_gate prints 0 issue(s) and reads clean, while 3 cases object.

51 self-test cases. Whole gate green: mypy, markdownlint, actionlint, editorconfig-checker, cspell.

Posture

Gating: self-tests, both repo_gate checks, charset, dupword.
Warn-only in a continue-on-error step so the backlog is visible without blocking: charset-unknown, semicolon 323, dash 963, comment-wrap 432, comment-case 56. Going green needs the sweep the prose rules themselves forbid, so each is corrected as its file is next edited.

The 247-instance GOVERNANCE.md dash sweep is deliberately a separate PR, where the diff is reviewable as prose rather than buried under tooling.

🤖 Generated with Claude Code

…very language

The flat non-ASCII ban could not express context. An audit report classified U+2264 and U+2265 as
the scientific carve-out while the rule named both must-replace, which is not a wrong reading so
much as a rule with no vocabulary for "the range it describes next to a figure, agent typography
between two words".

Three tiers give it that vocabulary. Tier 1 carries no meaning its ASCII form loses and always
flags. Tier 2 is an operator, kept beside a number or another operator and replaced between
words. Tier 3 is a unit symbol whose ASCII form would be a lie and never flags. A character in no
tier is reported rather than passed, because a gate that allows whatever it does not recognize
stops gating as the character set grows.

The charset rule is clean tree-wide, so it gates.

Two prose rules invert from detecting a subset to banning the construction. The pronoun-keyed
splice pattern found 170 of 493 and missed every imperative one, so any prose semicolon now flags
and the exception is the list the rule already names. The em-dash replacement becomes "restructure
the sentence" rather than a spaced hyphen, and that hyphen is banned in its own right, since
allowing the ASCII spelling would keep the shape and change only the character. Both are
markdown-only until a comment can be told from code, and both are warn-only.

Comments get a gate that reaches every syntax the fleet's types carry, not only the hash ones:
`//` and `/* */`, `<!-- -->`, `<# #>`, `;`, and `#`. JSON is read as JSONC because that is what
ships, and the VS Code task, devcontainer, and workspace files carrying comments under a plain
`.json` name were invisible before. A documentation comment is left to CODESTYLE, which permits
the paragraphs this rule forbids. Python uses `tokenize` so a trailing comment is seen exactly,
and every other language masks quoted spans so a marker inside a string is not read as a comment.

Two comment rules come out of it. One sentence per comment line, never wrapped and never two on a
line, and a sentence that starts on a line starts with a capital. Where the first word is a tool
whose own casing is lowercase, the fix is to restructure rather than to capitalize the name
against the tooling-casing rule.

The Verification Discipline mechanisms behind all of this land too: a gate has to be watched
failing, driving the real table rather than a restatement of it, and a heredoc consumes a
backslash escape and leaves an invisible control character behind.

Fixed in passing: 37 malformed comments across eight JSON-family files, the task snippets among
them, so the shape stops propagating to every repo that copies them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 30, 2026 17:00

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 updates the repo’s governance-driven prose linting to support tiered charset rules, invert/reshape prose punctuation checks, and expand comment-format checks across the fleet’s declared language/file types, with CI updated to gate only the now-clean checks and surface the rest as warn-only backlog.

Changes:

  • Replaces the prior flat ASCII/typography rule with a 3-tier charset policy (charset, charset-unknown) and corresponding self-tests and governance coupling.
  • Makes semicolon and dash checks “construction-banning” (with named exceptions) and scopes them to markdown-only until code-comments can be cleanly separated.
  • Adds cross-language comment extraction and comment style checks (comment-wrap, comment-case), plus CI reporting for backlog rules.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
scripts/test_prose_lint.py Expands self-tests to cover charset tiers, punctuation rules, and multi-language comment extraction behavior.
scripts/README.md Updates documentation of prose_lint.py behavior, including tiering and new comment rules.
scripts/prose_lint.py Implements tiered charset scanning, markdown-only punctuation rules, and multi-syntax comment extraction with wrap/case checks.
host-setup/agent-safety/.markdownlint-cli2.jsonc Reflows/clarifies markdownlint configuration comments (no functional change).
GOVENANCE.md Updates governance text for comment capitalization, tiered charset policy, and punctuation rules.
docs/token-cost.md Updates analysis text to reflect which rules gate vs. warn.
catalog/snippets/vscode/docker.jsonc Reflows comments to match comment style rules.
catalog/snippets/vscode/base.jsonc Reflows comments to match comment style rules.
catalog/snippets/devcontainer/python/devcontainer.json Reflows comments to match comment style rules.
catalog/snippets/devcontainer/dotnet/devcontainer.json Reflows comments to match comment style rules.
catalog/snippets/configs/vscode-tasks.json Reflows comments to match comment style rules.
catalog/snippets/configs/vscode-tasks-python.json Reflows comments to match comment style rules.
.markdownlint-cli2.jsonc Reflows/clarifies markdownlint configuration comments (no functional change).
.github/workflows/validate-task.yml Updates CI to gate charset+dupword and add warn-only reporting for backlog rules.

Comment thread scripts/prose_lint.py
Comment thread scripts/prose_lint.py
Comment thread scripts/prose_lint.py
Comment thread scripts/prose_lint.py
Comment thread docs/token-cost.md Outdated
Comment thread scripts/README.md Outdated
… than memory

The comment rule took four corrections to cover what it always governed. It started at hash
comments only, then Python only, then missed JSON because the specification says JSON carries no
comments, then missed capitalization. Each time the cause was the same: the check was scoped to the
file being edited rather than to what the project declares it ships.

The durable form is a rule beside the other verification mechanisms, since host memory is the wrong
home for something every agent on every machine needs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 30, 2026 17:08
…e fifth

Every finding was reproduced before it was fixed, and each carries a case.

CSS was mapped to the C-like syntax, so `//` inside a URL read as a comment marker and
`url(http://x/y. Z)` reported a run-on. CSS has block comments only.

The numeric-context test skipped literal spaces, so a tab between an operator and its figure made
a legitimate range read as prose. Any whitespace separates them.

The list exception let a comma anywhere earlier on the line excuse a semicolon, so
"It runs on push, always; it gates the merge" went unreported. A list announces itself with an
introducing colon or with more than one separator, and only then does a comma excuse it. The
semicolon count rises from 323 to 388, which is the under-reporting this removes.

Extending the capitalization rule to trailing comments is declined, with the rule text tightened
instead. Of the 105 trailing lowercase comments in the tree, 104 are the `# vX.Y.Z` version pins
the action-pinning rule requires on every SHA-pinned action, and exactly one ends in a period.
Capitalizing a version pin would satisfy one rule by breaking another, so the rule now says it
governs a line that opens prose, and a bare version pin is classified as machinery.

Two doc corrections: the warn-first sentence now names several rules and takes plural pronouns, and
the script README described the semicolon rule as it was before the inversion.

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

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

Comments suppressed due to low confidence (4)

docs/token-cost.md:92

  • This bullet refers to multiple rules, but the follow-up sentence uses the singular pronouns "its" and "it", which reads ambiguously and is grammatically incorrect in context.
- **The semicolon and dash rules are warn-first, and the charset and duplicate-word rules gate, being clean tree-wide.** Compare their hits against the next batch of review findings before enforcing them, and do not claim the mechanical share until that comparison exists.

scripts/prose_lint.py:452

  • Comment scanning runs before the markdown code-fence logic, so comment-wrap / comment-case will treat <!-- --> sequences inside fenced code blocks as real markdown comments. That can produce false positives when a fenced snippet contains HTML/XML (or literal <!-- ... -->) that is not a prose comment.
        raw = path.read_bytes().decode('utf-8')
    except (UnicodeDecodeError, OSError):
        return out
    lines = raw.split('\n')

scripts/prose_lint.py:477

  • The semicolon exception currently skips any semicolon if there is any comma earlier on the line. That can miss real semicolon splices in sentences that happen to contain a comma before the splice (e.g. "When X, do Y; it gates the merge"). If the intent is to only exempt list-style semicolons, the heuristic should be constrained to list contexts (for example, after a colon that introduces the list).
        # Both prose rules are markdown-only until a comment can be told from code.
        # A shell script carries 78 statement separators that are not prose at all.
        if path.suffix == '.md':
            if 'semicolon' in rules:
                listish = prose.count(';') > 1 or ':' in prose.split(';')[0]

scripts/prose_lint.py:404

  • extracted_comments scans for block-comment openers before it considers line comments. In C-like syntaxes this can mis-parse a /* ... */ sequence that appears inside a // ... line comment as the start of a real block comment, causing subsequent lines to be treated as "inside a block comment" until a closing token is found.
                out.append((n, body, True))
            closing = '' if end >= 0 else closing
            continue
        masked = strip_strings(line, spec['quotes'])
        cut = len(line)
        leading = True
        for opener, closer in spec['block']:
            at = masked.find(opener)
            if 0 <= at < cut:

Copilot AI review requested due to automatic review settings July 30, 2026 17:13

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

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

Comments suppressed due to low confidence (4)

scripts/README.md:48

  • This list of warn-only checks is incomplete: the workflow's warn-only step also runs comment-wrap and comment-case, so the README should include them to match CI behavior.
`charset` and `dupword` are clean tree-wide and gate CI. `charset-unknown`, `semicolon`, and `dash` run as a warn-only CI step so their backlog is visible without blocking, and are corrected as each file is next edited.

scripts/prose_lint.py:455

  • comment_wrap_findings() runs before fenced code blocks are detected, so comment-wrap / comment-case findings can be reported from inside ```/~~~ fenced regions even though the rest of check_file() skips fenced blocks. This makes markdown code examples and other fenced text eligible for comment findings while other rules intentionally ignore them.
    lines = raw.split('\n')
    if {'comment-wrap', 'comment-case'} & rules:
        out.extend(f for f in comment_wrap_findings(path, raw, lines) if f[1] in rules)
    in_fence = False

scripts/README.md:42

  • This sentence says CSS uses // line comments, but the implementation explicitly treats CSS as block-comment-only (see scripts/prose_lint.py CSS: Syntax = {'line': (), ...}) and the tests assert that // in CSS is a URL scheme separator, not a comment marker.
The `comment-wrap` rule covers comments in every syntax the fleet's project types carry, not only the hash ones: `//` and `/* */` for C#, C, C++, JSONC and CSS, `<!-- -->` for XML, csproj and markdown, `<# #>` for PowerShell, `;` for INI, and `#` for Python, shell, YAML and TOML.

scripts/README.md:40

  • The earlier paragraph about whole-tree runs still refers to the old ascii rule and CI gating ascii + dupword. With this PR, CI gates charset + dupword and reports the other checks warn-only, so that paragraph should be updated to avoid misleading local runs.

This issue also appears on line 48 of the same file.

The `semicolon` and `dash` rules ban a construction rather than a detectable subset of it, so each flags by default and the exceptions are the ones the rule names: a semicolon inside a list that already carries commas, and for the dash a compound word, a leading list marker, a range, and the `- **Label** - explanation` separator that opens a governed bullet.

**Both are markdown-only for now.** A shell script carries 78 statement separators that are not prose at all, so telling a comment from code is a precondition for reaching source files. Until then a semicolon or dash in a code comment is missed, which reading the diff by eye still catches.

…of the loop

The review body carries a collapsed block of findings that appear in no review thread, so polling
threads reports a clean pass while they stand. Four were waiting there, and all four held.

The comment rules ran before the fence loop, so an HTML comment inside a fenced markdown block was
reported while every other rule skipped fences. A fenced example is quoted code, and its comments
belong to whatever is being shown. Reproduced, fixed, and pinned by a case.

Three were stale claims in the script README: it still said CSS uses line comments, which the
previous round disproved, it named the pre-tier rule in the whole-tree paragraph, and its warn-only
list omitted the two comment rules the CI step actually runs.

The loop itself is the durable fix. The runbook now requires reading the body of every review and
investigating each suppressed finding on the same footing as an inline one, answering it in the PR
conversation because there is no thread to resolve. The merge gate names them too, since closing out
findings meant threads and issue-level comments and silently excluded these.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 30, 2026 17:27
@ptr727

ptr727 commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Worked the low-confidence findings from the review body, which I had not opened. All four held.

1. Comment rules ignored fenced blocks (prose_lint.py:455). The real bug of the four. comment_wrap_findings() ran before the fence loop, so a comment inside a fenced markdown block was reported while every other rule skipped fences. Reproduced first:

Prose.

```html
<!-- One thing. Another thing. -->

-> flagged comment-wrap


A fenced example is quoted code, so its comments belong to whatever is being shown. Fixed in `cc1446d`, with a case asserting the fenced form is clean and the unfenced form still flags.

Worth noting my first attempt to reproduce this was wrong. I put a `//` comment in a markdown fence and saw nothing, and nearly declined the finding on that basis. Markdown maps to `<!-- -->` only, so `//` was never a comment there. The finding was right and my test was measuring nothing.

**2, 3, 4. Three stale claims in `scripts/README.md`.** All mine. It said CSS uses `//` line comments, which the previous round disproved when it turned out `//` in a URL was reading as a comment marker. The whole-tree paragraph still named the pre-tier `ascii` rule. The warn-only list omitted `comment-wrap` and `comment-case`, which the CI step does run, so the doc understated what CI reports.

## The loop is the durable fix

These findings have landed several times now, and a loop that polls `reviewThreads` never sees them. Two changes:

- The **runbook** now requires reading the body of every review and investigating each suppressed finding on the same footing as an inline one, answering it in the PR conversation because there is no thread to reply on or resolve. It carries the query.
- The **merge gate** named "all review threads resolved" and "issue-level comments triaged", which silently excluded these. Condition 3 now names them.

So the gap was in the contract, not just in my attention. 56 self-test cases, whole gate green.

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

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

Comments suppressed due to low confidence (1)

scripts/prose_lint.py:461

  • comment-wrap continuation detection only tracks prev_body for comments that start the line. That misses wrapped sentences that begin in a trailing inline comment and continue on the next (leading) comment line (still a cross-line wrapped sentence per the rule), because prev_body is cleared when leading is false.
        # A continuation is the very next line: two comments with code between them are separate.
        adjacent = n == prev_no + 1
        continuation = (adjacent and leading and prev_body
                        and not SENT_END.search(prev_body) and body[:1].islower())
        if continuation:
            out.append((prev_no, 'comment-wrap',
                        'comment sentence wraps into the next line -> one sentence per line'))
        # A lowercase opening that is not a continuation is a sentence that failed to start.
        elif leading and body[:1].islower():
            out.append((n, 'comment-case',
                        'comment sentence opens in lowercase -> capitalize, or restructure so it '
                        'does not open on a lowercase name'))
        prev_body = body if leading else ''
        prev_no = n

The predecessor was cleared whenever a comment did not start its line, so a sentence beginning in a
trailing comment and continuing on the next full-line comment was not read as wrapped. It reported
a capitalization error instead, whose advice was to capitalize the continuation rather than to
un-wrap the sentence. Reporting the wrong rule is worse than reporting nothing, because the fix it
asks for is wrong.

A trailing comment is now remembered. The continuation still has to be a full-line comment, since a
trailing one annotates its own line rather than continuing the line above, so two trailing
annotations on consecutive lines stay separate.

Found in the low-confidence block of the review, which is now part of the loop.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 30, 2026 17:34
@ptr727

ptr727 commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

The one low-confidence finding on the previous head held, and it was worse than a miss.

prev_body was cleared whenever a comment did not start its line, so this pair was not read as wrapped:

x = 1  # a sentence that keeps
# going onto the next line.

It reported comment-case instead, advising to capitalize the continuation, when the correct fix is to un-wrap the sentence. Reporting the wrong rule is worse than reporting nothing, because the fix it asks for is wrong.

Fixed in abcfe8c. A trailing comment is now remembered as a possible sentence start. The continuation still has to be a full-line comment, since a trailing one annotates its own line rather than continuing the line above, so two trailing annotations on consecutive lines stay separate. Verified across all six combinations:

Input Result
trailing starts, leading continues comment-wrap
leading starts, leading continues comment-wrap
trailing label, then a new sentence clean
two trailing annotations clean
labels split by code clean
lowercase leading, no predecessor comment-case

Reverting the fix fails the new case, so the behavior is pinned rather than incidental.

58 self-test cases. Whole gate green.

That is two rounds in a row where the only real finding was in the suppressed block and the inline threads were empty, which is why the runbook and merge gate now name it.

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

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

Comments suppressed due to low confidence (2)

scripts/prose_lint.py:279

  • comment-wrap is meant to catch any two sentences on one comment line, but RUN_ON only triggers when the second sentence starts with an uppercase letter and also suppresses matches when the sentence-ending punctuation is preceded by any uppercase letter. This misses real run-ons like One thing. another thing. and CI. Another thing.
# Two sentences on one line, guarded against an abbreviation or a dotted identifier.
RUN_ON = re.compile(r'(?<![A-Z])(?<!\be\.g)(?<!\bi\.e)(?<!\bvs)(?<!\betc)[.!?]\s+(?=[A-Z])')

.github/workflows/validate-task.yml:66

  • The workflow comment says only the semicolon rule is warn-only, but this job also runs other warn-only prose rules (charset-unknown, dash, comment-wrap, comment-case) in the backlog step right below. Updating the comment avoids implying that only semicolon is non-gating.
      # The charset and duplicate-word rules are clean tree-wide, so they gate.
      # The semicolon rule stays warn-only, since going green needs a sweep the rule forbids.

… opens in lowercase

The initial guard suppressed a match whenever any capital preceded the sentence-ending period, which
was meant to keep `J. Smith` as one name. It also silenced every sentence ending in an acronym, and
this codebase ends them in CI, PR, SHA, and JSON constantly. The guard now anchors on a word
boundary, so an initial is still one name and an acronym ending is two sentences.

The second sentence also had to open with a capital to count, so a lowercase opening escaped both
rules: the run-on missed it for being lowercase, and the capitalization rule missed it for not
starting a line. Either case now counts.

The workflow comment named only the semicolon rule as warn-only while the step below reports four
more.

Found in the low-confidence block, which is two rounds running as the only place a real finding
appeared.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 30, 2026 17:48
@ptr727

ptr727 commented Jul 30, 2026

Copy link
Copy Markdown
Owner Author

Both low-confidence findings on abcfe8c held. Fixed in f401f9c.

1. A sentence ending in an acronym was invisible. The initial guard was (?<![A-Z]), meant to keep J. Smith as one name. It suppressed a match whenever any capital preceded the period, and this codebase ends sentences in CI, PR, SHA, and JSON constantly.

# The check runs in CI. Another thing happens.   -> MISSED
# Pinned by SHA. Dependabot still bumps it.      -> MISSED

The guard now anchors on a word boundary, (?<!\b[A-Z]), so J. is still one name (the J opens a word) while CI. is two sentences (the I does not).

2. A lowercase second sentence escaped both rules. The run-on required (?=[A-Z]) and missed it for being lowercase; the capitalization rule missed it for not starting a line. # One thing happens. another thing happens. fell through the gap. Either case now counts.

3. The workflow comment named only the semicolon rule as warn-only while the step below reports four more.

Every guard re-verified after widening:

Input Result
CI. Another thing. flag
One thing. another thing. flag
Reviewed by J. Smith today. clean
Uses e.g. Docker and i.e. Podman. clean
Bump to 3.13 for the runner. clean
See audit.py and validate.py. clean
Formats, then lints, etc. for each file. clean

Reverting the guard fails 3 cases. comment-wrap rises 436 to 454, which is the under-reporting removed.

61 self-test cases. Whole gate green.

That is three rounds running where the inline threads were empty and the suppressed block held the only real findings, five of five across them. Worth saying plainly: had I merged on the first clean-looking round, the gate would have shipped silently under-reporting on exactly the sentence shapes this repo writes most.

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

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

@ptr727
ptr727 merged commit d4c4085 into develop Jul 30, 2026
7 checks passed
@ptr727
ptr727 deleted the docs/tiered-charset-and-prose-rules branch July 30, 2026 17:54
ptr727 added a commit that referenced this pull request Jul 30, 2026
Copilot's finding on promotion PR #460, fixed at the source. That PR's
head is `develop`, so it picks this up when this squashes in.

## The contradiction

Two adjacent bullets in "Documentation Style Conventions" disagreed on
the emoji case:

> **Unicode the developer deliberately typed** stays regardless of tier
[...] Never strip the developer's own characters.

> **An unrecognized non-ASCII character is reported, not allowed.**

An emoji is both developer-typed and un-tiered, so the two bullets
pointed opposite ways. The implementation already had a coherent answer:

```
un-tiered emoji: [(1, 'charset-unknown', 'SNOWMAN (U+2603) is in no tier - classify it in GOVERNANCE.md')]
```

It is preserved in place and never rewritten, and it is still reported
until classified. The doc simply never said the second half, so the
first bullet read as an exemption from the gate rather than from
rewriting.

## The fix

The carve-out is about what an agent may rewrite, not about what the
gate reports. Saying that leaves both bullets true and needs no change
to either rule. `scripts/README.md` restated the same claim in one
sentence, so it takes the same clause.

The spaced hyphen in the edited bullet is recast at the same time, under
the rule three bullets below it that existing prose is corrected as each
file is next edited rather than swept.

## Why it blocks a promotion

This is fleet law on its way to `main`, and `main` is what the audit
reads as ground truth. Shipping a self-contradicting rule is what leaves
downstream repositories arguing the point, and the contradiction already
produced one real misreading: the audit report that classified U+2264
and U+2265 as the scientific carve-out, which is what motivated tiering
the rule in #459 to begin with.

No behavior change, so no test accompanies it. 62 self-tests and 19
repo_gate tests pass, `repo_gate` is clean, and `charset` plus `dupword`
exit 0.

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

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Jul 31, 2026
Promotes `d4c4085` (#459), `adaa068` (#461), `e52c68a` (#463), `7bf8a12`
(#464), `269629a` (#465), and `036e468` (#467). Conflict-free, six
commits ahead.

## What lands

The charset rule gets three tiers instead of a flat non-ASCII ban, which
could not express context - an audit report classified U+2264 and U+2265
as the scientific carve-out while the rule named both must-replace. Tier
1 never survives ASCII, tier 2 is an operator kept beside a number and
replaced between words, tier 3 is a unit symbol whose ASCII form would
be a lie. A character in no tier is reported rather than passed, and the
rule is clean tree-wide, so it gates.

Two prose rules invert from detecting a subset to banning the
construction. The pronoun-keyed splice pattern found 170 of 493 and
missed every imperative one; the em-dash rule now says restructure the
sentence, and the spaced hyphen is banned in its own right.

Comments get a gate covering every syntax the fleet types carry - `//`,
`/* */`, `<!-- -->`, `<# #>`, `;`, `#` - with JSON read as JSONC because
that is what ships. That recovered four files the old discovery never
saw, including the VS Code task and devcontainer snippets downstream
repos copy; their 37 malformed comments are fixed here.

The Verification Discipline mechanisms land too: a gate has to be
watched failing, and a check is scoped by what the project declares
rather than by the file that prompted it.

## The comment-extractor fix (#461)

Copilot's review of this promotion found a defect in the extractor #459
adds, so it was fixed at the source and this PR now carries it.

Block openers were searched before line markers against a ceiling of the
whole line, so a `/*` inside a `//` comment opened a real block: the
line's own comment was truncated at the opener, the closer carried into
the lines below, and the code there was handed to the comment rules as
prose. PowerShell failed identically with `<#` inside a `#` comment.
Those are the two fleet syntaxes carrying both marker kinds.

Tree-wide counts are unchanged - dash 963, comment-wrap 454, semicolon
388, comment-case 56 - because nothing in this repository nests the
markers that way. The exposure is downstream, in the C# and PowerShell
repositories the extractor is aimed at, and it would surface the moment
the comment rules gate rather than warn. That is the argument for fixing
before promotion rather than after.

## The carve-out contradiction (#463)

Copilot's second round found the same review's own subject matter
contradicting itself. Two adjacent bullets disagreed on whether a
developer-typed but un-tiered character is exempt from the gate or
reported by it. The implementation already reported it as
`charset-unknown` while the doc read as an exemption. The carve-out now
states that it governs what an agent may rewrite rather than what the
gate reports, which leaves both bullets true and changes neither rule.

Worth blocking a promotion for, because `main` is what the audit reads
as ground truth, and this contradiction had already produced a real
misreading - the audit report that classified U+2264 and U+2265 as the
scientific carve-out, which is what motivated tiering the rule in #459
to begin with.

## The scanner rewrite (#464)

The third round found a fourth defect in the same function, so the
structure went rather than the instance. The extractor searched each
marker kind from column 0 against a ceiling, and a ceiling can only
describe the first comment on a line. That one shape produced every
extractor defect in this promotion: a marker quoted inside the first
comment read as real, a doc marker excluded from the ceiling unbounded
it, and anything after the first comment was unreachable.

A single left-to-right pass replaces it. Several blocks on one line, a
comment trailing the line where a block closes, and a multi-line
documentation block that no longer leaks its prose into the scan all
come with it.

#462 is fixed there too rather than deferred, since a defect left in the
promoted diff keeps being found and the loop cannot reach a clean round
while it stands. C# verbatim strings read correctly in both directions
and in all three spellings, and C# took its own syntax entry so the rest
of the C-like family does not inherit a form it lacks.

## Multi-line strings (#465)

The fourth round found that masking runs per line while a C# verbatim
string spans them, so a marker on any later line of one was reported as
a comment. A false positive, and the direction that is worse than a
miss, since it asks a reader to edit text that is data.

Reviewing that fix found two more in the same area: a quote in comment
text opened a phantom string that blanked the markers after it, within a
line and then across lines, and a line-skip meant as an optimization
dropped a string that closed and reopened around real code. Masking now
runs from the scan position and only code advances the string state.

#466 records what is still not carried. C# is the only syntax whose
strings are tracked across lines, and the README says so rather than
leaving a reader to find out.

## The continuation asterisk (#467)

The fifth round found the extractor damaging the prose it then judged. A
leading `*` was taken off every block comment body, which is the `/* */`
convention for continuing a line and ordinary text everywhere else, so
`<!-- *emphasis* leads here -->` became `emphasis* leads here` and
`comment-case` reported the lowercase opening it had just created. Worse
than a plain false positive, since the prose reported is not what the
file holds.

Stripping is now one `*` against whitespace, on a line continuing a `/*
*/` block. This defect dates to `d4c4085` rather than to any fix made
during this review, so the promotion is where it would first reach
`main`.

## Why promote now

The snippets fixed here are the ones a new or realigning repo copies,
and the reviewer-facing rules - the merge gate and the runbook now
require investigating the low-confidence block - only bind downstream
once `main` carries them.

That requirement earned itself twice more on #461. Round one's single
inline comment read as a wording nit and was a live defect; round two
had no inline comments at all and both findings sat in the suppressed
block, one of them the reopened bug. A loop polling `reviewThreads`
would have reported a clean pass on both rounds.

## Fidelity note

`GOVERNANCE.md` and `.github/copilot-instructions.md` sections declared
verbatim changed, so downstream copies are **stale** until re-vendored.
`spec/fidelity_honesty.py` separates stale from modified by hash, so the
audit reports it correctly.

Warn-only backlog, reported but not gating: dash 963, comment-wrap 454,
semicolon 388, comment-case 56.

🤖 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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants