Skip to content
Back to skills

Pre Pr Verification

ASecurity

The checks that must pass before committing, opening a PR, or telling the user a change is done. Use when wrapping up any change.

  • 2 stars
  • 0 votes
  • 0 copies
  • 5 views
  • Added October 15, 2026
documentationpythonrustgobashsqltestinggitdatabasedocumentation

Works with

  • cli

Security analysis

A100/100

Scanned October 15, 2026

npx -y skills add Spinnich/rommbat --skill pre-pr-verification --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pre Pr Verification?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Pre Pr Verification
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/spinnich-pre-pr-verification/badge)](https://www.skillsdirectory.com/skills/spinnich-pre-pr-verification)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: pre-pr-verification
description: The checks that must pass before committing, opening a PR, or telling the user a change is done. Use when wrapping up any change.
---

# Pre-PR verification

## Always

`pwsh -File tools/pre-pr.ps1` runs every gate below, plus the hook and agent publish CI does
so the process-level tests run and CI's `tools/publish.ps1` package, and prints which failed.
In a git worktree it skips trunk, which cannot read one from WSL; CI's trunk check covers it.
`-Fix` runs `trunk fmt` first. `-Quiet` prints only each gate's name and the last 80 lines of
a gate that fails; an agent session runs it that way, so the full output stays out of its
context. By hand:

```bash
dotnet build -c Release -warnaserror --no-incremental   # what CI builds
dotnet test -c Release --no-build                       # full suite green, only after that build
trunk fmt && trunk check        # never commit with --no-verify
python3 -m unittest discover -s tools/docs   # the docs checker's own tests
python3 tools/docs/check.py     # links, anchors, fact citations, size budgets
mkdocs build --strict           # the guide in wiki/: its links, anchors and nav
cd reference && python3 verify.py
python3 tools/build-platform-map.py --check     # bundled data is what its generator emits
python3 tools/build-bios-manifest.py --check
git ls-files --eol -- '*.sh'    # every line must start i/lf
```

**Build with `--no-incremental`, because an incremental build can hide an error the previous one
reported.** On PR #196 a nullable warning (CS8602) failed one build under `-warnaserror`; a second
incremental build finished in a second with "0 Error(s)", and the tests passed against stale
binaries. Nothing in the tree escalates CS8602, so without `-warnaserror` it is only a warning, in
Debug or Release. Grep the output for `error` rather than trusting the summary line.

**`dotnet test` here is Microsoft.Testing.Platform, not VSTest**, opted in through
`global.json`, and it takes a different set of options. An option it does not recognize is
forwarded to the test module, which refuses it and reports **`Zero tests ran` with exit code
5**, naming neither the option nor the problem. `--nologo` does exactly this. **Read a zero-test
run as a bad command line, not as a broken environment**, and never as a pass.

**A `--filter` matching nothing in one of the two test projects fails the whole run**, because a
module that runs zero tests is an error: `dotnet test --filter "FullyQualifiedName~LivePairingTests"`
exits 8 with 4 of 4 passed, since `RomMBat.Agent.Tests` matched none. Scope a filtered run with
`--project tests/RomMBat.Tests` as well, or a run where everything you aimed at passed still
reports failure.

`verify.py` drifting means an upstream fact moved. **Revisit the docs and skills citing it; do
not just update the expected number.**

## Test cost

**CI's Test step is the budget, not your local run.** A GitHub Windows runner ran this suite
more than ten times slower than a dev box, and the difference was disk: until #227 the step took
11 minutes for a suite that ran in a minute locally, almost all of it SQLite commits waiting on a
flush, and afterwards it took 50 seconds. A test's cost on your machine says little about its
cost there.

- **Profile before you cut anything.** Rank tests by duration from the CI log, where every
  `passed` line carries one, or locally with the test exe's `-xml` (`docs/contributing/testing.md`).
  An audit of 1,244 test methods found almost no duplication. The number of tests was never the
  cost; a few expensive patterns were.
- **A store-backed test opens its store through `LocalStore.Open` or `OpenAt`.**
  `Support/ThrowawayStores` makes both cheap for the whole suite: a new file starts as a copy of
  one migrated seed, and commits are not flushed. A raw `SqliteConnection` skips both, so use one
  only to write the old-version file a migration test starts from, and give it `Pooling=False`:
  a pooled connection keeps the file open after its `using` ends, the tree's teardown cannot
  delete it, and `TempTreeLeakCheck` fails the run.
- **Use `TestTimeProvider` rather than the wall clock.** A test that has to prove something
  never happened waits its full budget, so run those waits side by side rather than one after
  another. `HookSpawnTests` fires both no-spawn hooks and then waits once.
- **Compare the PR's Test step with main's** (`gh run list --workflow Build --branch main`) when
  the change adds process-level, live or waiting tests. If it grew by a minute or more, find out
  why before merging.

## Invariants worth re-checking by hand

- No absolute path reaches the database. Three layers enforce it (the `RelativePath` type,
  a `CHECK` on every path column, and `LocalStoreTests` binding the two to one table of bad
  values). A new path column needs its `CHECK` and a row in that test.
- No emulator INI was written. Configuration goes through `es_settings.cfg`.
- Nothing was written outside the RetroBat tree.
- No secret, token or instance URL is in the diff.
- Every new user-visible string is reachable without a mouse.

## When the change touches sync

- Re-run a sync with no changes on the agent tree ("Hands-on by change type"): zero uploads,
  zero downloads, no gamelist churn.
- Exercise the offline path: switch the stub to unreachable mid-operation and confirm work
  either completes locally or queues, and that a later flush is idempotent under replay.
- **If save logic changed**, a real emulator must have written a real save or state of the
  affected shape, and RomMBat must have handled it, through EmulationStation and back. Drive
  **every emulator the system offers and every save option RetroBat exposes within each** (PCSX2's
  memory card type, including folder cards), because the shape varies by both and one
  combination says nothing about the others. List the emulators and cores from `es_systems.cfg`
  and the save-affecting options from `es_features.cfg` before sitting down. That is **not** a
  certification and must not be recorded as one: it covers the changed shape, not the nine steps.

  A session that cannot do it, because it is non-interactive or has no permission to touch a
  real install, **says which claims are unproven for that reason** rather than letting the test
  suite stand in for evidence. Naming the gap is acceptable; implying it is closed is not.

## When the change touches a platform

Run the full `platform-certification` checklist. A platform is not done at eight of nine.

## Documentation parity

`docs/design/` is the design of record: the principles, and one record per standing decision
under `docs/design/decisions/`. It is not the only place the change can falsify. Work the table, and grep rather than remember: search the
docs for the terms the diff touches (the command name, the class, the table, the version).

| The diff contains                                            | Then re-read, and correct what it falsifies                                                                 |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| A new or changed subcommand, flag, or user-visible output    | The guide page in `wiki/` that names it                                                                     |
| A gamepad screen's rows, labels, buttons or wording          | The guide page in `wiki/` that walks through that screen                                                    |
| A new page in `wiki/`                                        | Its entry in `nav` in `mkdocs.yml`, which `mkdocs build --strict` fails without                             |
| A change to `rommbat-agent --help`                           | Regenerate `wiki/reference/cli.md` (`wiki/README.md`); `CliReferencePageTests` fails until you do           |
| A save shape, class or platform that now syncs, or stops     | `README.md`'s warning and feature list, and the guide's `wiki/saves/` page for it                           |
| A row certified, driven, or owed after a floor move          | Its row in `data/certification.json`, then regenerate `wiki/platforms/index.md` (`wiki/README.md`)          |
| A finding that changes which emulator to pick, BIOS or saves | The system's guide page, `wiki/platforms/<system>.md`, which a system's first certified row owes            |
| A migration, table or column                                 | `docs/architecture/local-store.md`: its table, and the out-of-`saves/` folders below it                     |
| Sync protocol, the save or state model, attribution, hashing | `docs/architecture/saves.md` and the `save-sync` skill                                                      |
| A rule that only exists because something was measured       | The skill for that area, plus its fact in `docs/upstream/`, and a `docs/design/decisions/` record it amends |
| A milestone or stage changing state                          | The GitHub milestone and issue that track it                                                                |
| A minimum RomM or RetroBat version                           | The `version-adoption` skill, "Adopt: a stable"                                                             |
| A new project, folder, probe set or bundled data file        | `docs/architecture/projects.md` and `reference-data.md`                                                     |
| A folder, type or trap a nested `CLAUDE.md` names            | That project's `CLAUDE.md`, and the routing table in the root `CLAUDE.md`                                   |

Three rules that keep this from becoming its own scope creep:

- **Correct the sentences the change falsifies.** Do not rewrite a document to sound current.
- **A stale claim is a defect at the same severity as the bug it describes.** "Directory saves
  do not sync yet" in a release that syncs them is wrong in the same way a wrong return value is.
- **Say what you checked.** The PR body names the docs that moved and the ones you read and
  found already correct. "Docs unchanged" with no statement is indistinguishable from not looking.

## When the change moves the supported RomM or RetroBat version

The `version-adoption` skill owns it: "Adopt: a stable" is the checklist, and a prerelease never
moves the floor. A PR that moves a floor and skips that checklist is not done, whatever the tests
say.

## Hands-on by change type

**A change a user can see or a server can receive is driven on a real install before it is
done**, not only tested. The tests prove the mechanism; a hands-on pass proves the shipped build
does it. The install is the agent tree, `ROMMBAT_AGENT_ROOT` in `.env`, which is the agent's to
deploy to and drive without asking, and the kit is `tools/handson/` (its README has the how).
Start with `Test-HandsOnEnv -Gui`, then `Publish-ToAgentTree`, so the pass runs this branch's build.

| The diff changes                     | Owed, on the agent tree with the deployed build                                                                                                   |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| A gamepad screen                     | Walk every changed state with `Send-Key`, screenshot each, and read the screenshots. Include the empty, offline and error states the change moves |
| A CLI command or its output          | Run it with `Invoke-Agent` and quote what it printed                                                                                              |
| Sync, hooks, the spool, ES           | `Start-ES`, launch a game, `Stop-ES`. The spool drains, `background.log` shows both passes, `status` shows the session, and a re-sync is a no-op  |
| Pairing or a call to the server      | `Connect-AgentTree -Repair`, or the changed call made against the real server                                                                     |
| Save logic                           | `/certify <system> --hands-on <PR>`, as "When the change touches sync" says                                                                       |
| Docs, tests, CI or dev tooling alone | Nothing                                                                                                                                           |

A diff in two rows owes both. **A pass that cannot run is named, never skipped silently**: a
disconnected session, ES or an emulator already running outside `/certify` (the kit refuses to
take the screen then, because the maintainer may be playing), or a server down. Say which claims that leaves
unproven. The PR body's **Hands-on** section says what was driven, on which build (the line in
`emulators/rommbat/deployed.txt`), and what each screenshot or output showed.

## Before claiming done

State plainly what was verified and what was not. Never claim a platform works without
having launched a game on it. If something was skipped, say so and why.

## PR description

- Base it on the repo's PR template.
- One type label, from CONTRIBUTING's "Labels and release notes" table. The release notes
  are grouped by it.
- **Disclose AI assistance and its extent.** RomM requires this and RomMBat inherits it.
  Non-negotiable.
- Link the issue: `Fixes #NNNN` for bugs, `Closes #NNNN` for features.
- Note any change to the minimum supported RomM or RetroBat version.
- One `semver:major`, `semver:minor` or `semver:patch` label, as
  `docs/design/decisions/versioning.md` defines them, so the release that collects the PR can
  pick its bump. A floor move or a store migration is a minor; a change that makes the user
  re-pair or redefine sets is a major; docs and CI are a patch. A major also says in the body
  what breaks and what the user does about it, for the release notes.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.