verify-db-change — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited verify-db-change (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Prove a database change works end-to-end against real Postgres — not by asserting against mocks. The output is a markdown proof block, suitable for pasting into a PR description, that quotes actual captured output from each step. If a step did not run or did not pass, the proof block says so explicitly. No paraphrasing, no inferred outcomes.
This skill is designed for the case where the change is a typical projection-plane drop: one or more migration files, a writer / projector class, an entity or schema update, and (optionally) DI wiring in an app module. It generalizes cleanly to any INSERT … ON CONFLICT / UPDATE pattern and to migrations that touch existing tables.
manager.query() validate that the projector calls a query — they validate nothing about whether the query does what you think in Postgres. ON CONFLICT DO NOTHING, GREATEST(...), returning-row semantics, FK cascade, generated columns, partial indexes — these are SQL contracts. They must be tested against a real Postgres at least once before merge.down() method via a TypeORM DataSource against a throwaway DB and observe the SQL TypeORM emits. The CLI-equivalent (migration:revert) works too, but a one-off script is faster and lets you assert on the after-state in the same run.Nest can't resolve / UnknownDependencies is cheap and catches a class of bug that no unit test will.SELECT name FROM migrations WHERE name LIKE '%UserLogin%' returning the two expected rows is proof.Be terse but precise in the proof block. The reviewer should be able to verify each claim by running the same command themselves.
User-facing triggers:
Also use proactively when:
infra/migrations/ (or equivalent) and the description has no captured DB output.Do not use for:
proofshot / verify / dev-browser).Before running anything, list:
git diff --stat origin/<base>...HEADIdentify:
infra/migrations/**/*.ts or migrations/**/*.ts or prisma/migrations/. Note the timestamp / order.*.entity.ts, Prisma schema.prisma, etc.INSERT, UPDATE, manager.query, queryRunner.query.*.module.ts, etc.).docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}' usually reveals it.If you can't answer all five questions, stop and read the diff more carefully. Verification without the map of the change is theater.
Run, in parallel:
pnpm --filter <app> typecheck
pnpm --filter <app> lint
pnpm --filter <app> test -- --testPathPattern='<changed-file-names>'For each new or changed test, capture:
100 | 100 or note the gap)If coverage is < 100% on a new writer / migration, note which lines are uncovered — they're the candidates for the next step's edge cases.
If lint warnings exist in files you did not touch, note them as pre-existing. Do not bundle their fixes into this PR.
For each target DB (one DB or one per tenant):
docker exec <container> psql -U postgres -d <dbname> -c "\d <new_table>"
docker exec <container> psql -U postgres -d <dbname> -c "\d <touched_table>" | tail -25
docker exec <container> psql -U postgres -d <dbname> -c \
"SELECT name FROM migrations WHERE name LIKE '%<MigrationStem>%' ORDER BY id;"Capture:
\d <new_table> output (columns, indexes, FKs)\d <touched_table> showing the new columnsSELECT FROM migrations rows confirming both migrations recordedWatch for surprises:
varchar(64), entity says varchar(255)).(userId, loggedInAt) etc.).createForeignKeys: false or vice versa.For an app that registers the new writer in DI:
pnpm --filter <app> start > /tmp/<app>-boot.log 2>&1 &
BOOT_PID=$!
sleep 30
kill -9 $BOOT_PID 2>/dev/null
pkill -9 -f "dist/src/main" 2>/dev/null
wait $BOOT_PID 2>/dev/null(timeout is missing on macOS — use the background+sleep+kill pattern.)
Then grep:
grep -c "started successfully\|Application is listening" /tmp/<app>-boot.log
grep -iE "Nest can't resolve|UnknownDependencies|UnknownProvider|UnknownExports|Cannot find module" /tmp/<app>-boot.logA passing boot has:
Capture the "started successfully" line and one or two adjacent lines proving the relevant module initialized (e.g. EventListenerModule dependencies initialized).
BEGIN…ROLLBACKThis is the load-bearing step. For each behaviour the writer claims to implement, write a SQL statement that exercises it, wrap everything in BEGIN; … ROLLBACK;, and run it against the live dev DB.
The standard contract for an event-sourced projector is roughly:
GREATEST guard).Adapt to your writer's actual contract. Don't invent assertions the code doesn't make.
Write the SQL to a file (heredoc inline can mangle quoting):
cat > /tmp/<writer>-dryrun.sql <<'SQL'
\set USER_ID '''<existing-user-uuid>'''
\set EVT1 '''11111111-1111-1111-1111-111111111111'''
…
BEGIN;
\echo '===== Step 1: happy path ====='
INSERT INTO … ON CONFLICT … RETURNING id;
UPDATE … RETURNING id, "lastLoginAt", "loginCount";
\echo '===== Step 2: replay (same id) — expect ZERO rows from RETURNING ====='
INSERT …;
SELECT … FROM users WHERE id = :USER_ID;
…
ROLLBACK;
SQL
docker cp /tmp/<writer>-dryrun.sql <container>:/tmp/<writer>-dryrun.sql
docker exec <container> psql -U postgres -d <dbname> -f /tmp/<writer>-dryrun.sqlCapture the full output. Each \echo header makes the proof block readable. Build a small table mapping scenario → observed. Confirm each row of the table matches the writer's documented contract; if any doesn't, that's a real bug — stop and tell the user before continuing.
Do not skip the rollback. If a step fails partway and Postgres remains in a transaction, the next BEGIN will error.
Reverting migrations against the shared dev DB is destructive and forbidden. Instead:
TS=$(date +%s)
DBNAME="test_revert_<short-stem>_${TS}"
# 5a. Create the throwaway DB on the same container
docker exec <container> psql -U postgres -c "CREATE DATABASE ${DBNAME};"
# 5b. Clone schema only (no row data, faster + smaller)
docker exec <container> bash -c \
"pg_dump -U postgres --schema-only <source-dbname> | psql -U postgres -d ${DBNAME}"
# 5c. Confirm clone has the schema you intend to revert
docker exec <container> psql -U postgres -d ${DBNAME} \
-c "SELECT to_regclass('<new_table>') AS exists;"Then write a one-off TypeORM script that imports each migration class and calls .down(qr) in reverse order. Example skeleton:
// infra/scripts/_test-revert-<stem>.ts — temporary, delete after running
import { DataSource } from "typeorm";
import { CreateXxx1779360850300 } from "../migrations/.../1779360850300-CreateXxx";
import { AddYyy1779360850301 } from "../migrations/.../1779360850301-AddYyy";
async function main(): Promise<void> {
const ds = new DataSource({
type: "postgres",
host: "localhost",
port: <port>,
username: "postgres",
password: "postgres",
database: process.env.TEST_REVERT_DBNAME!,
logging: ["query", "error"],
});
await ds.initialize();
const qr = ds.createQueryRunner();
await qr.connect();
// BEFORE snapshot — capture concrete existence checks
const before = await qr.query(
"SELECT to_regclass('<new_table>') AS exists, " +
"ARRAY(SELECT column_name FROM information_schema.columns " +
"WHERE table_name='<touched_table>' AND column_name IN ('newCol1','newCol2')) AS cols"
);
await new AddYyy1779360850301().down(qr);
await new CreateXxx1779360850300().down(qr);
const after = await qr.query(/* same SELECT as above */);
console.log({ before, after });
await qr.release();
await ds.destroy();
}
main().catch((e) => { console.error(e); process.exit(2); });Run with TEST_REVERT_DBNAME=<dbname> pnpm exec ts-node infra/scripts/_test-revert-<stem>.ts. The --logging=["query","error"] option makes TypeORM print the actual DROP TABLE / DROP INDEX / ALTER TABLE statements it emits — capture those for the proof block.
Pass criteria:
after.user_logins is null (or whatever sentinel to_regclass returns for "gone")after.cols is [] (or contains only columns that existed before up())IDX_<table>_*If anything is left over (a stray index, a sequence, a check constraint), down() is incomplete. Surface this as a blocker.
Always:
docker exec <container> psql -U postgres -c "DROP DATABASE ${DBNAME};"
rm -f infra/scripts/_test-revert-<stem>.ts
rm -f /tmp/<writer>-dryrun.sql /tmp/<app>-boot.log
git status --short # must show only the files the change itself touchedIf git status --short shows the one-off script or any other artifact, delete it. The verification leaves zero trace.
Paste a section into the PR description that follows this exact structure. Each subsection is one of the steps above; each contains real captured output, not interpretation. If a step did not pass, the subsection says so and the verdict at the top reflects it.
## Verification
### Automated gates
- [x] `pnpm --filter <app> typecheck` — clean
- [x] `pnpm --filter <app> lint` — N errors / N warnings (note pre-existing in untouched files)
- [x] Unit specs — X/Y passing across <files>
- `<file>` — 100% lines / 100% branches (or note gap)
### Manual verification on the local dev stack (PG <version>, <tenants>)
**1. Migrations apply cleanly on <tenant(s)>**
\`\`\`text
$ docker exec <container> psql -U postgres -d <dbname> -c \
"SELECT name FROM migrations WHERE name LIKE '%<Stem>%' ORDER BY id;"
name
-------------------------------------
<Migration1>
<Migration2>
$ \d <new_table>
<key columns + indexes + FK summary>
$ \d <touched_table>
... <new columns> ...
\`\`\`
**2. Writer SQL works end-to-end against real Postgres**
Ran the writer's SQL inside `BEGIN…ROLLBACK` against the live <dbname>:
| Scenario | Observed |
|---|---|
| Happy path | <quote> |
| Replay (same id) | <quote> |
| Out-of-order older event | <quote> |
| <other contract bullet> | <quote> |
Validates <one-sentence statement of what the SQL contract is>.
**3. <App> boots with the new <Provider> registered**
\`\`\`text
[Nest] LOG [InstanceLoader] <Module> dependencies initialized
INFO: <App> application started successfully
\`\`\`
No `Nest can't resolve` / `UnknownDependencies` errors.
**4. Migration reversibility — `down()` cleanly reverses `up()`**
Ran both migrations' `down()` against a throwaway DB cloned via `pg_dump --schema-only`:
\`\`\`text
=== BEFORE revert ===
{ <new_table>: '<new_table>', <touched_table>_cols: ['newCol1', 'newCol2'] }
=== <Migration2>.down() ===
ALTER TABLE "<touched_table>" DROP COLUMN "newCol1"
ALTER TABLE "<touched_table>" DROP COLUMN "newCol2"
=== <Migration1>.down() ===
DROP INDEX "public"."IDX_<table>_*"
DROP TABLE "<new_table>"
=== AFTER revert ===
{ <new_table>: null, <touched_table>_cols: [], leftover_indexes: [] }
\`\`\`
Throwaway DB dropped after the test.Keep the proof block under ~80 lines. If the captured output is verbose, trim to the load-bearing lines (the \echo headers, the RETURNING rows, the DROP … statements TypeORM emitted). The reviewer wants to read it in 30 seconds, not 5 minutes.
pg_dump --schema-only excludes data. Either include --data-only for the affected tables, or hand-seed the throwaway with the minimum data needed for the migration to find what it expects.CONCURRENTLY index builds): BEGIN…ROLLBACK won't wrap them. Step 4 still applies to the writer's SQL, but you cannot test the migration itself inside a transaction. Use the throwaway-DB approach for that subset.PG <version> in the section header.\d <new_table> may show no FK even though one is intentional later. Don't flag this as a smell without checking the rest of the chain.CASCADE just to make the test pass.gh pr view <n> --json body --jq .body > /tmp/cur.md), preserve it, and replace only the Verification section. Never clobber the whole body.Direct. The reviewer should read the proof, not the prose. If something is missing, name what's missing; if something passed, quote the line that proves it. The PR description is not the place to hedge.
If during the verification you find a real bug (the writer's SQL doesn't behave as documented, a migration's down() leaves a stray index, the app fails to boot) — stop the skill and tell the user. The proof block exists to surface bugs, not paper over them. A failed verification is more valuable than a green one.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.