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
3 changes: 2 additions & 1 deletion .agents/skills/python-codestyle/references/code-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@

## Naming

- `snake_case` for functions, methods, variables, modules, package directories.
- `snake_case` for functions, methods, variables, modules, package directories. A module's file
name follows CODESTYLE.md's "Script and Utility File Names".
- `PascalCase` for classes, type aliases, type vars, enum members.
- `UPPER_SNAKE_CASE` for module-level constants.
- Single leading underscore for module-private, double leading underscore for name-mangled (rare,
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
047e89b805ed646f
bba583635a70530a
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@

## Naming

- `snake_case` for functions, methods, variables, modules, package directories.
- `snake_case` for functions, methods, variables, modules, package directories. A module's file
name follows CODESTYLE.md's "Script and Utility File Names".
- `PascalCase` for classes, type aliases, type vars, enum members.
- `UPPER_SNAKE_CASE` for module-level constants.
- Single leading underscore for module-private, double leading underscore for name-mangled (rare,
Expand Down
3 changes: 2 additions & 1 deletion .github/skills/python-codestyle/references/code-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@

## Naming

- `snake_case` for functions, methods, variables, modules, package directories.
- `snake_case` for functions, methods, variables, modules, package directories. A module's file
name follows CODESTYLE.md's "Script and Utility File Names".
- `PascalCase` for classes, type aliases, type vars, enum members.
- `UPPER_SNAKE_CASE` for module-level constants.
- Single leading underscore for module-private, double leading underscore for name-mangled (rare,
Expand Down
27 changes: 27 additions & 0 deletions CODESTYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ A command-line script is one a person or an agent runs by name. A hook its host
- **Every command-line script answers `--help` and `-h`** with the same text on stdout, then exits 0, changing nothing. That text opens on a usage line naming the script. An agent learns a script's interface by asking it, so a script that reads `--help` as an input acts in a way nobody predicted.
- **An argument the script does not recognize is refused**, with a non-zero exit before any read or write. The refusal names it as an unknown or unrecognized option, argument, flag, or parameter, and PowerShell's own refusal of a parameter it cannot bind counts. An unrecognized first argument is never taken as a default mode or as a target.

### Script and Utility File Names

| Type | Rule | Basis |
| --- | --- | --- |
| Python `.py` | lowercase snake_case | [PEP 8][pep8-names-link] module naming. A hyphenated module also cannot be named in an `import` statement |
| Shell `.sh` and PowerShell `.ps1` | lowercase-hyphenated | The Linux command convention. A cross-platform pair keeps one name, `sync-tools.sh` beside `sync-tools.ps1` |
| Windows batch `.cmd` and `.bat` | lowercase-hyphenated | The same convention as shell and PowerShell. A shim that puts a command on `PATH` takes the name of the command it provides, `sync-tools.cmd` launching `sync-tools.ps1` |
| PowerShell functions inside a `.ps1` | Verb-Noun with an approved verb (`Get-Verb`) | PSScriptAnalyzer `PSUseApprovedVerbs`. This is a command rule, not a file rule |
| Env and secret files | lowercase snake_case stem | `mail_relay.env`, `db_root_password` |
| Env variable names and CI secret names | UPPER_SNAKE_CASE | The POSIX and GitHub convention |

- **Installed commands.** An executable or launcher on `PATH` or in a system slot (systemd, cron, run-parts, motd) is named as a command, whatever its language. That name is lowercase-hyphenated. It carries no extension unless the platform needs one to run it, such as `.cmd` or `.ps1` on Windows.
- **Wrapper exemption.** A thin launcher whose only job is to run a Python tool keeps that tool's stem, so `fetch_report.py`, `fetch_report.sh`, and `fetch_report.ps1` stay grouped together. It covers only a launcher run by its path. A launcher installed onto `PATH` or into a system slot is an installed command instead, such as `fetch-report` rather than `fetch_report`.
- **Externally fixed names are exempt.** That covers Git hook names, `__init__.py`, `conftest.py`, and Pester's `.Tests.ps1` suffix, whose stem mirrors the script under test. It covers a PowerShell data file (`.psd1`) whose name a tool fixes, such as a module manifest named for its module. `PSScriptAnalyzerSettings.psd1` is another, since PSScriptAnalyzer finds it by that name. It also covers any name a platform defines.
- **Directories are out of scope.**

Departures, stated on purpose so a reader does not "fix" them back:

- The [Google Shell Style Guide][google-shell-link] asks for underscores in shell file names. The fleet uses hyphens instead, matching installed Linux commands.
- Microsoft's [approved verbs][ps-approved-verbs-link] govern command names. A `.ps1` run by path is a file rather than an exported command, so the file takes the shell convention and its functions take Verb-Noun.

### Markdown and Spelling

These apply repo-wide, in every directory: Markdown lints clean via `markdownlint-cli2` against the shared config, spelling is US English via CSpell against the shared `cspell.json`, the CI spelling gate covers `README.md` and `HISTORY.md` only, `HISTORY.md` mirrors the README's opening, and "Markdown" is a proper noun in prose. A repo excluding a subtree of its own that it does not treat as authored prose, a committed data archive, a vendored theme, or a hand-maintained record, puts a `.markdownlint-cli2.jsonc` carrying its own `ignores` beside that content rather than editing the shared root config, whose contents are fleet-fixed. Those `ignores` patterns resolve against the directory holding them rather than against the repo root, so a repo-root-relative entry there matches nothing and reports no error saying so, and excluding through the CI workflow's own negated Markdown glob input instead is a CI-only fix that leaves the same files flagged for anyone running the linter locally. The full rules are in the `comment-and-doc-style` Skill referenced above.
Expand Down Expand Up @@ -71,3 +92,9 @@ Bash, and only where a program cannot be Python: a bootstrap that installs the i
[governance-verification-discipline]: ./GOVERNANCE.md#verification-discipline
[readme]: ./README.md
[root]: ./.editorconfig

<!-- External -->

[google-shell-link]: https://google.github.io/styleguide/shellguide.html
[pep8-names-link]: https://peps.python.org/pep-0008/#package-and-module-names
[ps-approved-verbs-link]: https://learn.microsoft.com/en-us/powershell/scripting/developer/cmdlet/approved-verbs-for-windows-powershell-commands
1 change: 1 addition & 0 deletions GOVERNANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Line-ending governance for an operational repo is in [Line Endings](#line-ending
A repo whose own stacks or scripts read local runtime credentials from disk, most commonly an operational repo's Docker Compose stack, keeps them in a dotted `.secrets/` directory at the repo root. This is the repo-scoped counterpart to the host-scoped `~/.secrets/` convention a repo's own `OPERATIONS.md` may document, and it is a different thing from `spec/secrets.json`, the CI/GitHub Actions secret-name registry `spec/audit.py` cross-checks. `spec/secrets.json` governs what a workflow reads from GitHub Actions. This convention governs what a repo's own process reads from its own checkout.

- **The directory is named `.secrets/`, dotted, never a bare `secrets/`.**
- **A secret file's stem is lowercase snake_case** (`homeassistant_db_password`, `mail_relay.env`), per [`CODESTYLE.md` "Script and Utility File Names"](./CODESTYLE.md#script-and-utility-file-names).
- **A single opaque credential file carries no extension** (`homeassistant_db_password`, not `homeassistant_db_password.txt`), the same reason `README` and `LICENSE` carry none. It is a security property, read but never sourced, not a formatting preference.
- **A structured credential keeps its format's extension** (`.json` for structured config).
- **The shared env file is named for what it configures**, not a bare `.env` (`docker.env` for a repo whose stacks are Docker Compose), so a second env-shaped file added later stays unambiguous.
Expand Down
43 changes: 23 additions & 20 deletions scripts/pr_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -734,6 +734,7 @@ def strip_fences(
"(N) previously missed issues",
"(N) previously missed issue",
}
SUPPRESSED_SUMMARIES = {"Suppressed comments (N)", "Comments suppressed due to low confidence (N)"}
VETTED_SUMMARIES = (
set(OVERVIEW_SECTIONS)
| {
Expand All @@ -742,9 +743,8 @@ def strip_fences(
"Show a summary per file",
"File summaries",
"Review details",
"Suppressed comments (N)",
"Comments suppressed due to low confidence (N)",
}
| SUPPRESSED_SUMMARIES
| PREVIOUSLY_MISSED_SUMMARIES
)
VETTED_LABELS = {"Files reviewed", "Comments generated", "Review effort level", "Review effort"}
Expand Down Expand Up @@ -3734,11 +3734,13 @@ def unrecognized_in(body: str) -> list[str]:
quoting a heading is not a review carrying one, and this script's own pull requests quote
these lists in full.

A previously-missed summary that no reader finds as a section is reported too. Its spelling is
vetted wherever it appears, while the reader keys on how its line opens. A layout the reader
misses would otherwise hand its findings to nobody under `shapes=ok`. The check compares which
summaries the blocks hold rather than how many blocks there are. Compared by count, a bare
`(N)` line read in one region stood in for a summary missed in another.
A previously-missed or suppressed summary that its reader does not find as a section is
reported too. Its spelling is vetted wherever it appears, while the reader keys on how its line
opens and where the section around it ends. A layout the reader misses would otherwise hand its
findings to nobody under `shapes=ok`. The check compares which summaries open the blocks rather
than how many blocks there are. Compared by count, a bare `(N)` line read in one region stood in
for a summary missed in another. Compared by every summary a block holds, a block that ran on
into the next section stood in for that section.

A body carrying no heading at all is reported rather than passed, since every one of the 333
measured opens on a heading and a body with none is a format nothing here has seen. A refusal
Expand All @@ -3762,19 +3764,20 @@ def unrecognized_in(body: str) -> list[str]:
for marker in (normal(s) for s in dict.fromkeys(SUMMARY.findall(plain)))
if unvetted(marker, VETTED_SUMMARIES) and not finding_entry(marker)
]
missed = Counter(
marker
for marker in (normal(s) for s in SUMMARY.findall(plain))
if not unvetted(marker, PREVIOUSLY_MISSED_SUMMARIES)
)
read = Counter(
normal(s)
for block in previously_missed_blocks(body)
for s in SUMMARY.findall(CODE_SPAN.sub(" ", strip_fences(block)))
)
unread = missed - read
if unread:
found.append(f"summary: {next(iter(unread))}, vetted while no reader found its section")
summaries = [normal(s) for s in SUMMARY.findall(plain)]
for vetted, reader in (
(PREVIOUSLY_MISSED_SUMMARIES, previously_missed_blocks),
(SUPPRESSED_SUMMARIES, suppressed_blocks),
):
expected = Counter(marker for marker in summaries if not unvetted(marker, vetted))
read = Counter(
normal(s)
for block in reader(body)
for s in SUMMARY.findall(CODE_SPAN.sub(" ", strip_fences(block)).split("\n", 1)[0])[:1]
)
unread = expected - read
if unread:
found.append(f"summary: {next(iter(unread))}, vetted while no reader found its section")
found += [
f"metadata label: {la}" for la in dict.fromkeys(labels) if unvetted(la, VETTED_LABELS)
]
Expand Down
52 changes: 52 additions & 0 deletions tests/test_pr_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -1705,6 +1705,58 @@ def test_the_vetted_but_unread_check_compares_which_summaries_were_read(self) ->
pr_review.unrecognized_in(stand_in),
)

def test_a_vetted_suppressed_summary_its_reader_misses_is_reported(self) -> None:
"""The suppressed spellings are vetted wherever they appear, as the previously missed are.

A layout that `suppressed_blocks` misses therefore reported `shapes=ok` while `suppressed=`
Comment thread
ptr727 marked this conversation as resolved.
came out short. Each case reads correctly first, so the stub dropping a block is what
changes the answer.
"""
both = (
f"{OVERVIEW}\n{COVERED}\n\n<details>\n<summary>Review details</summary>\n\n"
+ entries_section("Suppressed comments (1)", "Constructed finding 1")
+ entries_section("Previously missed (1)", "Constructed finding 2")
+ entries_section("Suppressed comments (2)", "Constructed finding 3", "Constructed 4")
+ "</details>\n"
)
cases = {
"low confidence": collapsed(),
"suppressed": collapsed(heading="Suppressed comments (2)"),
"alternating in one wrapper": both,
}
for name, body in cases.items():
with self.subTest(name):
blocks = pr_review.suppressed_blocks(body)
self.assertEqual([], pr_review.unrecognized_in(body))
with mock.patch.object(pr_review, "suppressed_blocks", return_value=blocks[:-1]):
found = pr_review.unrecognized_in(body)
self.assertEqual(1, len(found))
self.assertIn("vetted while no reader found its section", found[0])
self.assertTrue(found[0].startswith("summary: "))
self.assertIn("uppressed", found[0])

def test_a_block_running_into_the_next_section_does_not_stand_in_for_it(self) -> None:
"""Unbounded, the first section's block holds every section after it in the wrapper.

Counted by every summary a block holds, the sections it ran into read as found, so an
unbounded block reported `shapes=ok`. Only the summary opening a block counts.
"""
body = (
f"{OVERVIEW}\n{COVERED}\n\n<details>\n<summary>Review details</summary>\n\n"
+ entries_section("Suppressed comments (1)", "Constructed finding 1")
+ entries_section("Suppressed comments (2)", "Constructed finding 2", "Constructed 3")
+ "</details>\n"
)
self.assertEqual([], pr_review.unrecognized_in(body))
with mock.patch.object(
pr_review, "sibling_heading", side_effect=lambda lines, _: len(lines)
):
self.assertEqual(1, len(pr_review.suppressed_blocks(body)))
found = pr_review.unrecognized_in(body)
self.assertEqual(
["summary: Suppressed comments (N), vetted while no reader found its section"], found
)

def test_every_no_thread_render_drops_the_same_presentation_markup(self) -> None:
"""All of these sections come from one format, so a badge in a suppressed block buries its
title exactly as one in a previously-missed block does.
Expand Down
Loading