replay: corpus replay pattern — buzz history through pg-sprite, generalizable to any project - #58
replay: corpus replay pattern — buzz history through pg-sprite, generalizable to any project#58Kiran01bm wants to merge 6 commits into
Conversation
Throwaway postgres:17-alpine harness that replays a real public schema-change corpus (block/buzz's SQLx history, pinned to one commit) against pg-sprite. This change ships the corpus fetch and the baseline/reset lifecycle; the tier-by-tier replay assessment builds on it next.
Walk a pinned schema-change history in order, asserting per statement that pg-sprite either really executes the change (exit 0, executed-natively) or refuses with exactly the expected typed reason (exit 2); refusals and out-of-scope content advance via psql so later steps replay against true state. The machinery is project-agnostic: fetch/harness/replay read replay/<project>/project.conf (repository, pinned commit, migrations path, corpus list), and init.sh scaffolds a new project from exactly those inputs — buzz becomes the first project directory. fetch.sh refresh reports corpus drift for deliberate pin-bumps. Curating buzz against real verdicts surfaced two boundary facts, kept as manifest comments: CREATE INDEX IF NOT EXISTS is refused by design, and unnamed ADD FOREIGN KEY refuses while the named form runs the safer NOT VALID + VALIDATE sequence.
Walk a pinned schema-change history in order, asserting per statement that pg-sprite either really executes the change (exit 0, executed-natively) or refuses with exactly the expected typed reason (exit 2); refusals and out-of-scope content advance via psql so later steps replay against true state. The machinery is project-agnostic: fetch/harness/replay read replay/<project>/project.conf (repository, pinned commit, migrations path, corpus list), and init.sh scaffolds a new project from exactly those inputs — buzz becomes the first project directory. fetch.sh refresh reports corpus drift for deliberate pin-bumps. make replay / replay-refresh / replay-down (REPLAY_PROJECT, default buzz) wrap the whole flow one-shot, with replay.sh starting or resetting the harness itself. Curating buzz against real verdicts surfaced two boundary facts, kept as manifest comments: CREATE INDEX IF NOT EXISTS is refused by design, and unnamed ADD FOREIGN KEY refuses while the named form runs the safer NOT VALID + VALIDATE sequence.
Review findings on the replay pattern: recreate_database now quotes the database name via psql's :"var" identifier interpolation instead of splicing it into raw SQL (hyphenated project names broke reset while up succeeded); the initial harness reset/up is guarded so a partially applied baseline stops the run instead of assessing the wrong starting state; the refuse-branch psql advance narrows to exit 2, mirroring the execute branch — an exit-1 failed execution may have committed a prefix that must not be re-applied. Project names are validated early to one documented charset, python3 preflights match across entry points, reset distinguishes a stopped container from a missing one, and the 0027 manifest comment names the statement as written in the corpus.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…roblem vs by-design A flat refusal count made bootstrap CREATE TABLE look like missing capability. The manifest now carries an optional class per refuse row so the bucket summary separates what pg-sprite will eventually handle (T2) from what has no concurrent-access problem to solve and what is refused deliberately because a safer form exists.
|
🤖 Adversarial correctness review, requested by @aparajon and performed by their agent. Reviewed at head Verdict: the assertions are genuinely strict — a wrong reason fails, a refusal advances state only on exit 2, and a mid-plan failure never gets blindly re-applied — and the buzz curation is meticulous (I verified programmatically that its 94 ranges tile every substantive line of 0002–0032 with no overlaps and no gaps). Two things to fix, both in the machinery rather than the assessment. The pin does not identify the corpus on disk: I bumped Findings1. This is exactly the failure the README says is prevented by design — "an assessment must never silently apply to a corpus it was not written against" (README line 99) and "the pin in 2. Nothing checks that the manifest tiles the corpus, and the uncovered case is silent. 3. A divergence mid-run keeps producing verdicts, and the engine's own explanation is thrown away. The baseline path already refuses to replay a wrong world — "a results table that looks legitimate but assesses the wrong world" (replay.sh line 55) — and One root cause, seven mismatches, a cascade reaching a migration twenty-three files later — and a complete-looking bucket summary of 33 / 30 / 24 computed against a world where The second half of that output is its own defect: six rows say 4. (nit) 5. (nit) Action items
Verified (tried to break, couldn't)The headline reproduces exactly: from a cold This review was generated by Claude Code (claude-opus-5). |
|
🤖 Second pass, same head ( Lens 1 — OSS adoptionThis is the first artifact in the repo that answers an evaluator's actual first question — "will it handle my schema?" — with a number instead of a promise, and that changes what pg-sprite can claim in public. Every other form of evidence here is the project grading its own homework: the Go suite tests the engine against schemas written for the tests, and the demo tours a schema chosen to tour well. Replaying somebody else's real history, curated against observed verdicts rather than predictions, is the one form of evidence a skeptical reader can't discount — and pinning it to a public commit means they can re-run it and get the same table. The design choices that make it credible are the unglamorous ones: real execution rather than dry-run, an exact reason match so a refusal that lands for the wrong cause is a failure, and psql-advancing after each refusal so statement 90 is assessed against a database that actually has statements 1–89 in it. The one change I'd make before this becomes the number people quote: the bucket summary undersells the engine by about four times, and the fix is regrouping, not engineering. The headline reads 37 executed against 32 typed refusals, which invites the conclusion that pg-sprite handles a bit over half of a real workload. I broke down what those 32 refusals actually are:
So of the 69 table-shape statements buzz put in front of the engine, 62 are either executed online today or bootstrap DDL that is safe by definition, and six are genuine capability gaps — three of which are one feature. That is a far stronger and equally honest story than "32 refusals", and the current grouping actively hides it, because the biggest bucket in the refusal column is the one that isn't a limitation at all. Splitting the summary into executed / safe by definition, outside intake / capability gap / out of scope would make the table say what the data says. The README already explains this distinction in prose (lines 23–29) — the summary just doesn't reflect it. Second adoption note: the fastest way for another team to reject this is the curation cost, and nothing sets that expectation. The scripts scaffold in one command and the docs are genuinely good, but the actual work is hand-writing 94 line ranges against a corpus and iterating until green — and the ranges are line numbers, so they are re-derived by hand on every pin bump. That is fine for a flagship exemplar and worth every hour it took here; it is a surprise to somebody who reads "assess your own project" and expects to spend an afternoon. Either say so in Lens 2 — the seam an orchestrator consumesWhether or not it was meant as one, this is now the repo's only end-to-end test of the CLI contract an embedder actually integrates against — exit codes and The deeper seam question is which front door this measures. The replay drives One thing worth keeping exactly as it is: the choice to curate against observed verdicts rather than predicted ones, and to write the two surprises down in This review was generated by Claude Code (claude-opus-5). |
aparajon
left a comment
There was a problem hiding this comment.
🤖 Approved. I ran the replay end to end at head a30c984 — cold container pull through 94 steps, 37 / 32 / 25 with 0 mismatches, matching the manifest's own expectation counts field-for-field — and independently re-derived both boundary facts in buzz/README.md. The assertions are strict in the ways that matter, and the buzz curation tiles the corpus exactly. Findings are non-blocking and both concern the machinery rather than the assessment: the pin does not identify the corpus on disk (fetch.sh caches by filename), and nothing checks that the manifest covers the corpus. Details: #58 (comment)
This review was generated by Claude Code (claude-opus-5).
Summary
Adds
replay/— a throwaway-Docker pattern that replays a real project's schema-change history through pg-sprite from scratch, asserting per statement that pg-sprite either executes the change for real or produces exactly the expected typed refusal (a reason mismatch is a failure, not a pass). The machinery is project-agnostic; the first project is the SQLx migration history of block/buzz (range partitioning, composite FKs, enums, generated TSVECTOR columns, GIN/partial indexes, PL/pgSQL triggers), replayed 0002 → 0032 with zero mismatches.What
A replay project is defined by three inputs — repository, pinned commit, migrations path — captured in
replay/<project>/project.confalongside the corpus file list and a curated manifest:init.sh <project> <repo> <ref> <path>— scaffolds a new project from those inputs: resolves the ref to a commit, captures the corpus file list at that pin, picks a free host port, writesproject.conf,.gitignore(corpus is fetched, never vendored), and a skeletonassessment.tsv.fetch.sh <project>— downloads the corpus at the pinned commit (explicit file list, never a directory scrape — the corpus stays synchronized with the assessment written against it).fetch.sh <project> refresh [ref]reports files beyond the pin via the GitHub API without touching anything; picking up new history is a deliberate pin-bump + re-curation.harness.sh <project> up|reset|psql|dsn|down— the project's pinned postgres image with state confined to the container;upapplies the project baseline via psql (bootstrap DDL on an empty database has no online-safety problem),resetreturns to exactly that state. Per-project ports let harnesses coexist.assessment.tsv— one row per replay step,<migration> <line-range> <execute|refuse:<reason>|psql> [class]. Line ranges index into the pinned corpus, so a pin bump forces re-curation — an assessment never silently applies to a corpus it was not written against. The optional class on refuse rows separates three very different refusals: capability boundary (default — pg-sprite is expected to handle this eventually), no-online-safety-problem (bootstrapCREATE TABLE— nothing reads the object yet, so there is no concurrent-access problem for an online engine to solve), and by-design (refused deliberately because a safer form exists).make replay [REPLAY_PROJECT=<project>]— the one-shot: build the binary, fetch the pinned corpus, start (or reset) the project's container, replay.make replay-refreshreports corpus drift;make replay-downremoves the container. Default project: buzz.replay.sh <project>— starts or resets the harness to the pristine baseline, then walks the manifest in strict corpus order:pg-sprite migrate --alter '…' --url … --jsonand require exit 0 with outcomeexecuted-natively; pg-sprite itself mutates the database — real execution, not dry-run.DOblocks, sessionLOCK/SET LOCAL) are applied via psql in one transaction and never assessed.Why
The Go suite tests the engine against schemas written for the tests. Replaying an independent project's full history measures something different: how much of a real service's schema-change workload lands in each support tier, with the refusals firing for the right reasons. Buzz is public, so the whole harness lives in the open — and any other project can be assessed the same way from its repo URL, a commit, and a migrations path.
Buzz replay result at the pinned corpus (94 steps, 0 mismatches)
Two findings surfaced by curating against real verdicts (kept as manifest comments):
CREATE INDEX IF NOT EXISTSis refused by design — a name-only no-op cannot prove the existing index is valid or even the requested one.ADD FOREIGN KEYis refused (not-native-safe-rewrite-required) while the same constraint written withCONSTRAINT <name>executes via the saferNOT VALID+VALIDATEsequence — the rewrite needs a name to reference.Known follow-up: the refusal class lives in the manifest, not the verdict
The class split above is curated — the engine returns the same machine-readable reason (
unsupported-statement) for a data backfill, a bootstrapCREATE TABLE, andCREATE INDEX IF NOT EXISTS, so a machine consumer parsing--jsoncannot distinguish "wrong tool class" from "missing capability". A verdict-taxonomy change will follow as its own PR (additiveclassfield on the refusal verdict, exit codes unchanged, capabilities/limitations/README/demo updated together per the capability-statement rule); once the engine emits the class, the replay will assert it from the verdict instead of carrying its own column.Before / after