Skip to content

fix(db): guard un-journaled and duplicate-numbered migrations (BLO-27927) - #1402

Merged
allyblockcast[bot] merged 3 commits into
masterfrom
blo-27927-migration-numbering-guard
Aug 23, 2026
Merged

fix(db): guard un-journaled and duplicate-numbered migrations (BLO-27927)#1402
allyblockcast[bot] merged 3 commits into
masterfrom
blo-27927-migration-numbering-guard

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Aug 18, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • Its Postgres schema is versioned by numbered .sql files in packages/db/src/migrations, gated in CI by check:migrations
  • BLO-27927 was filed after a git rebase silently dropped a _journal.json entry while leaving the .sql on disk, and check:migrations still exited 0 — the checker only asserts journal→file, never file→journal, and its duplicate-number check runs on journal tags rather than on files
  • The issue concluded from drizzle-orm/migrator.js that an un-journaled .sql never executes, and directed that the 9 such files on master be triaged and, if inert, deleted
  • Triaging first (as the issue itself insisted) showed that conclusion is wrong for this repo: the app's apply path scans the directory, so those files do run — and deleting them would have dropped live schema from every future bootstrap
  • This pull request adds both missing assertions with enumerated allowlists, and encodes the corrected model so the next reader does not repeat the deletion reasoning
  • The benefit is that a rebase-dropped journal entry now fails CI instead of silently changing a migration's apply order, without making master red on contact

Linked Issues or Issue Description

What Changed

  • ensureFilesAreJournaled — new assertion: a .sql with no journal entry fails the check. The error message explicitly says to add the journal entry, not delete the file.
  • ensureNoDuplicateFileNumbers — new assertion: two .sql files sharing a 4-digit prefix fail. Allowlisted groups are matched as an exact set, so a third file on an allowlisted number still fails.
  • Two enumerated allowlists grandfathering the existing master population (9 un-journaled files, 3 duplicate-number groups), in the style of GRANDFATHERED_OFFENSE_SHAS. Each entry names why it is there.
  • analyzeMigrationNumbering() extracted as a pure function (plus readMigrationNumberingInput()), so the assertions are testable in isolation — mirroring the existing analyzeMigrationSafety in check-migration-safety.ts. The module now only self-executes when run directly.
  • check-migration-numbering.test.ts — 13 tests; every new assertion paired with a negative control run against a reconstructed pre-fix checker.
  • unjournaled-migrations-apply.test.ts — embedded-postgres test asserting all 9 un-journaled files' objects exist after a fresh migrate. This is the regression guard against deleting them.

The premise correction, because it changes what a reviewer should expect

BLO-27927 states that an un-journaled .sql never executes, citing drizzle-orm/migrator.js iterating journal.entries. That reading of drizzle is correct — but stock drizzle is not this repo's apply path, except on one branch:

  • inspectMigrations() builds availableMigrations from readdir() over the migrations folder (client.ts:669, listMigrationFiles) and derives pending from that (:702).
  • applyPendingMigrationsManually() (:258) applies every pending file.
  • migratePg runs only on the no-migration-journal-empty-db branch (:729) — and even there the manual path immediately re-inspects and picks up whatever drizzle skipped.

What the journal actually controls is ordering, not inclusion: orderMigrationsByJournal() (:141-149) sorts un-journaled entries last, so a file numbered 0046 applies after 0220. Today's population is order-insensitive (each is IF NOT EXISTS or additive), which is why nothing has broken; that does not generalize, so new occurrences are blocked.

Consequently the issue's AC2 ("files classified inert are deleted") would have removed milestones, plugin_event_outbox, issue_pull_requests and companies.feature_flags from every future bootstrap, while already-migrated databases kept looking healthy — invisible to CI. No file is deleted here.

Verification

pnpm --filter @paperclipai/db check:migrations   # exit 0 on this branch
  • Empirical triage — fresh embedded postgres, full migrate: 222 .sql files → 222 rows in drizzle.__drizzle_migrations (against 213 journal entries), and every object from all 9 un-journaled files present. Negative control: injected a bogus table, index and column; each reported MISSING and failed the assertion, so the presence checks discriminate.
  • Production corroborationmilestones holds rows dating to 2026-06-19; issues.milestone_id / issues.target_date are live on the API. 0115_milestones, the file the issue flagged as "does not read like a no-op", is fully applied.
  • CLI negative controls — adding an un-journaled 0999_*.sql → exit 1; journaling a second 0220_* → exit 1; both reverted → exit 0.
  • Unit testsvitest run src/check-migration-numbering.test.ts src/check-migration-safety.test.ts → 38 passed.
  • tsc --noEmit → clean.

Scope note, stated plainly: the pre-existing ensureNoDuplicates(journalTags) already caught duplicates where both files are journaled. The new file-level assertion's unique contribution is the case where one member is un-journaled and allowlisted — exactly the 3 grandfathered groups. still fails when a third file joins an allowlisted group is the test covering that reachable path.

Risks

  • Low risk to running systems. No migration is added, removed or edited; no schema change. The only runtime-adjacent edit is that check-migration-numbering.ts now self-executes solely under a direct-run guard — exercised by check:migrations in the commands above (exit 0, and exit 1 on both injected violations), so the gate is confirmed still wired.
  • Allowlists are the deliberate trade-off. They keep master green rather than red-on-contact. They are enumerated, not date-cutoff, and a test asserts no entry is stale (still on disk, still un-journaled), so an allowlist entry cannot quietly outlive its reason.
  • The embedded-postgres test adds ~30s to the db suite and is skipped where embedded postgres is unsupported, matching the existing migration tests.
  • Not addressed deliberately: backfilling journal entries for the 9 files would move them from "applies last" to "applies at their numbered position" — a real behaviour change for fresh bootstraps that deserves its own issue and test rather than riding along here.

Model Used

  • Claude Opus 5 (claude-opus-5), 1M context window, extended thinking, with tool use and code execution — running as a Paperclip Staff Engineer agent.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I have searched GitHub for duplicate or related PRs and linked them above
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots — n/a, no UI surface
  • I have updated relevant documentation to reflect my changes — the corrected apply-path model is documented in the checker's own doc comments, where the next reader will hit it
  • I have considered and documented any risks above
  • All Paperclip CI gates are green — pending
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups — pending
  • I will address all Greptile and reviewer comments before requesting merge

…927)

check:migrations asserted only journal -> file. It never asserted the
file -> journal direction, and ensureNoDuplicates ran on journal tags
only, so two .sql files could share a 4-digit prefix undetected.

Correcting the premise this issue was filed on: an un-journaled .sql is
NOT inert. drizzle's stock migrator iterates the journal, but that path
only runs on an empty-database bootstrap. inspectMigrations() builds its
candidate list from readdir() over the migrations folder, so the app's
apply path picks up every .sql on disk. Verified against a freshly
migrated database: 222 .sql files produce 222 applied rows, and every
object created by the 9 un-journaled files is present.

So the 9 files must NOT be deleted -- doing so would drop live schema
(milestones, plugin_event_outbox, issue_pull_requests,
companies.feature_flags, ...) from every future bootstrap while leaving
already-migrated databases looking healthy.

The real defect is ordering: orderMigrationsByJournal() sorts entries
with no journal record last, so a file numbered 0046 applies after 0220.
The existing population is order-insensitive; new occurrences are blocked
because that does not generalize.

- add ensureFilesAreJournaled + ensureNoDuplicateFileNumbers, both with
  enumerated allowlists grandfathering the existing master population
- extract analyzeMigrationNumbering() as a pure function so the
  assertions are testable, mirroring check-migration-safety.ts
- unit tests with negative controls against a pre-fix checker, plus an
  embedded-postgres test asserting the un-journaled files still apply
@allyblockcast

allyblockcast Bot commented Aug 18, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-27927

@allyblockcast

allyblockcast Bot commented Aug 18, 2026

Copy link
Copy Markdown
Author

Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

check-migration-safety.ts guards self-execution with
`import.meta.url === pathToFileURL(process.argv[1]).href` and reports
failures as a one-line `<script>: <message>` with process.exitCode = 1.
The numbering checker compared paths instead of URLs and let the error
escape as a raw stack trace. Align both.

Verified: exit 0 on a clean tree, exit 1 with a single-line message on an
injected un-journaled file, and `pnpm --filter @paperclipai/db
check:migrations` exit 0 end to end.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: 8004eab

I re-derived the PR's load-bearing claims from the tree at this head rather than taking the description on trust, because the whole change rests on a premise correction. Everything checks out:

  • 222 .sql files, 213 journal entries, exactly 9 un-journaled — and the 9 are byte-for-byte the contents of GRANDFATHERED_UNJOURNALED_MIGRATIONS. No allowlist entry is invented or missing.
  • Exactly 3 duplicate-number groups on disk (0046, 0102, 0106) — byte-for-byte GRANDFATHERED_DUPLICATE_FILE_NUMBERS. No fourth collision is being hidden.
  • The apply-path correction is right. orderMigrationsByJournal() (client.ts:138-149) returns 1 when the left entry has no journal order, sorting un-journaled files last rather than excluding them; applyPendingMigrationsManually() (client.ts:258) then applies every pending file in that order. So the issue's "un-journaled ⇒ inert ⇒ delete" conclusion really would have dropped milestones, plugin_event_outbox, issue_pull_requests and companies.feature_flags from fresh bootstraps. Declining to delete is correct.
  • All four cross-module test imports exist at this head with matching signatures (applyPendingMigrations/inspectMigrations in client.ts; getEmbeddedPostgresTestSupport/startEmbeddedPostgresTestDatabase(tempDirPrefix) in test-embedded-postgres.ts).
  • Every one of the 222 files has a 4-digit prefix, so the new throw in ensureNoDuplicateFileNumbers for unnumbered files cannot fire on the current tree.
  • The isDirectRun guard is not a novel risk — it is character-for-character the pattern already in check-migration-safety.ts:1044-1051, which runs in the same check:migrations command (tsx src/check-migration-numbering.ts && tsx src/check-migration-safety.ts). Under tsx, argv[1] and import.meta.url both resolve to the .ts entry, so the guard holds.

Critical Issues (0)

None. No migration is added, edited or removed; no schema changes; no runtime code path outside the checker CLI.

Important Issues (1)

  • [tests] packages/db/src/unjournaled-migrations-apply.test.ts:39-46 — the test computes its two most load-bearing values and then only logs them, never asserting. inspectMigrations() and the select … from drizzle.__drizzle_migrations query both land in console.log and are discarded. The "222 .sql files → 222 applied rows" equality is the single fact that proves un-journaled files execute — it is the reason this PR exists — and it is precisely the fact the test declines to assert. As committed, the apply path could regress to skipping un-journaled files in a way that still leaves those 13 specific objects present (say, a future migration recreating one of them) and this test stays green.
    • Assert it: expect(applied.length).toBe(state.availableMigrations.length) — one line, and it is the general guard that the 13 hand-listed objects only approximate.
    • Same file: the seven console.log calls, including the ===== UN-JOURNALED OBJECT PRESENCE ON FRESH DB ===== banner, are triage instrumentation that reads as leftover. Every sibling embedded-postgres migration test in this directory (nested-skill-folders-migration.test.ts, heartbeat-timeout-outcome-migration.test.ts, …) contains zero console.log. The results/missing array already carries the diagnostic into the failure message via expect(missing, …), so the banner buys nothing and prints on every packages/db test run.
    • Supporting context: the Verification section lists vitest run src/check-migration-numbering.test.ts src/check-migration-safety.test.ts → 38 passed. This file is not in that run. The manual triage described is equivalent in substance, but the test as committed has not been shown green — worth stating explicitly given it is a 300s embedded-postgres test.

Suggestions (4)

  • [code] packages/db/src/check-migration-numbering.ts:211-215 — assertion order makes ensureNoDuplicateFileNumbers almost unreachable, and misdiagnoses the common case. For a genuinely new offence (0150_foo.sql added un-journaled alongside a journaled 0150_bar.sql), ensureFilesAreJournaled fires first and reports "no journal entry". The developer adds the journal entry as instructed, and only then does the pre-existing ensureNoDuplicates(journalTags) reveal the actual problem — the number collision. Two round trips for one mistake. Running ensureNoDuplicateFileNumbers before ensureFilesAreJournaled surfaces the more actionable error first. (The PR description is admirably honest that the only otherwise-reachable path is a third file joining an allowlisted group; reordering widens that to the case developers will actually hit.)
  • [tests] packages/db/src/unjournaled-migrations-apply.test.ts:17UNJOURNALED_EXPECTATIONS hardcodes the nine filenames independently of GRANDFATHERED_UNJOURNALED_MIGRATIONS. The two lists are meant to describe the same population but can drift silently: adding an allowlist entry without a matching expectation leaves the new file untested, and this test is the stated regression guard against deleting these files. Assert the coupling — expect(new Set(UNJOURNALED_EXPECTATIONS.map(e => \${e.file}.sql`))).toEqual(new Set(GRANDFATHERED_UNJOURNALED_MIGRATIONS))`.
  • [tests] packages/db/src/check-migration-numbering.test.ts:165-169 — the staleness check is one-directional for duplicate groups. It asserts each allowlisted filename still exists on disk, but not that the group is still a collision. Renumber 0046_smooth_sentinels.sql to 0117_* and the group stops colliding, files.length < 2 short-circuits, and the now-meaningless allowlist entry survives — with both filenames still on disk, so the staleness test passes. The un-journaled allowlist gets this right (it checks both presence and still-un-journaled); mirroring that for groups means asserting each group still shares a number and still matches the full set of files on it.
  • [tests] packages/db/src/check-migration-numbering.test.ts:15-42analyzeWithPreFixChecker is a hand-written replica of the pre-BLO-27927 checker, so the negative controls prove "these trees pass this reimplementation", not "these trees passed the code that shipped". It is defensible (the pre-fix logic is frozen history and the replica looks faithful to the removed main() body) and the doc comment is candid about intent — worth a line noting it must be kept frozen, since a well-meaning future edit to "fix" it would quietly void every negative control.

Strengths

  • The premise correction is the whole value of this PR, and it was earned empirically rather than argued. The issue directed deleting nine files; triage-first showed that would have removed live schema from every future bootstrap while already-migrated databases kept looking healthy — invisible to CI. Pushing back on an issue's stated AC with a fresh-database measurement is exactly right, and the reasoning is preserved in GRANDFATHERED_UNJOURNALED_MIGRATIONS' doc comment where the next person to reach for rm will hit it.
  • The error message teaches the correct fix. "Add the journal entry (do not delete the file)" — and there is a test asserting that phrasing survives (does not tell the reader to delete the file). Encoding the anti-lesson as an assertion, not a comment, is the durable form.
  • Negative controls throughout. Every new assertion is paired with a pre-fix run that must pass, and the real-tree tests assert both that master is green with allowlists and red without — so the grandfathered population is proven real rather than asserted.
  • Exact-set matching on duplicate groups (check-migration-numbering.ts:117) rather than number-level exemption. Allowlisting 0046 wholesale would have let a third 0046_*.sql through forever; matching the sorted member set means the exemption cannot silently widen.
  • Allowlists are enumerated with per-entry reasons and guarded against staleness, not a date cutoff. Combined with the analyzeMigrationNumbering/readMigrationNumberingInput split mirroring the existing analyzeMigrationSafety, this lands consistently with the module next door instead of inventing a second convention.

Recommended Action

  1. No Critical issues — nothing blocks on correctness.
  2. Address the Important issue this cycle: assert the applied-count equality and strip the console.log instrumentation from unjournaled-migrations-apply.test.ts, and confirm that file green as committed.
  3. Consider the assertion reordering (first suggestion) opportunistically — it is the one with user-visible payoff, turning a two-round-trip diagnosis into one.

…y logged

Review follow-ups from Ally on #1402.

The embedded-postgres test computed its two most load-bearing values and
then only logged them. "222 .sql files on disk -> 222 rows in
drizzle.__drizzle_migrations" is the single fact that proves the apply
path is directory-driven rather than journal-driven -- the reason this PR
exists -- and it was precisely the fact the test declined to assert. The
apply path could have regressed to skipping un-journaled files while the
13 hand-listed objects stayed present (a later migration recreating one)
and the test would still have passed. Assert the equality against the raw
migrations table: state.appliedMigrations resolves hashes and can fall
back to slicing by row count (loadAppliedMigrations), which would make the
assertion circular.

Drop the seven console.log calls and the skip console.warn. Every sibling
embedded-postgres test in this directory has zero of both, and the
results/missing array already carries the diagnostic into the failure
message.

Couple UNJOURNALED_EXPECTATIONS to GRANDFATHERED_UNJOURNALED_MIGRATIONS.
The two lists describe the same population but could drift silently:
adding an allowlist entry without an expectation left the new file
untested by the very guard that exists to stop someone deleting it. As an
equality on file sets it needs no database, so it runs where embedded
postgres is unavailable.

Run ensureNoDuplicateFileNumbers before ensureFilesAreJournaled. A new
0150_foo.sql added un-journaled beside a journaled 0150_bar.sql is one
mistake with two symptoms; reporting "no journal entry" first sent the
author to add the entry, which only then tripped
ensureNoDuplicates(journalTags) on the collision. Verified side by side on
an injected 0220 collision: old order reported the journal entry, new
order reports "Duplicate migration number 0220 ... Renumber one of them".
A plain un-journaled file with no collision still reports the journal
entry.

Strengthen the duplicate-group staleness check to assert each group still
shares one number and is still the exact set of files on it. Note: the
renumber scenario in the review is already caught -- the allowlist entry
is a filename that embeds the number, so renumbering makes files.has()
false. The reachable gap is a group that can never match a bucket at all
and silently outlives its collision; negative control confirms the new
assertion fails on such a group.

Record that analyzeWithPreFixChecker must stay frozen: it is a replica of
removed code, so "fixing" it would void every negative control silently.

Verified: tsc --noEmit clean; 40 tests green across numbering, safety and
the embedded-postgres apply test (the last now actually run, 28s, not just
committed); pnpm --filter @paperclipai/db check:migrations exit 0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

Review follow-ups — all five addressed in b52a6177

Thanks — the Important issue was a real hole and I'd rather have it caught here. Taking each in turn, with one correction to the reasoning behind Suggestion 3.

Important — applied-count equality never asserted ✅ fixed

Correct, and this was the worst kind of gap: the test performed the measurement that justifies the entire PR and then discarded it. Now asserted.

One implementation note on which value to compare against. I deliberately did not assert on state.appliedMigrations.length. loadAppliedMigrations (client.ts:488-544) resolves hashes with a chain of fallbacks ending in availableMigrations.slice(0, rows.length) — comparing that to availableMigrations.length can be satisfied by the fallback itself. So the assertion reads the raw table, exactly as you suggested:

expect(applied.length, ).toBe(state.availableMigrations.length);

Non-vacuity is measurable rather than assumed: the tree has 222 .sql files and 213 journal entries, so a regression to journal-driven apply gives 213 ≠ 222 and fails.

Also fixed: dropped all seven console.log calls and the skip-path console.warn — siblings have zero of both, and results/missing already carries the diagnostic into the failure message.

And the fair hit about it never having been run: it has now. vitest run src/unjournaled-migrations-apply.test.ts2 passed, 28.4s, embedded postgres supported and actually exercised (not skipped). Full pass: tsc --noEmit clean, 40 tests green across numbering + safety + this file, check:migrations exit 0.

Suggestion 1 — assertion order ✅ fixed, and verified side by side

Right, and it has user-visible payoff. Injected a real 0220 collision and ran both orders:

  • old: Migration file(s) have no meta/_journal.json entry: 0220_colliding_new_file.sql → author adds the entry → then ensureNoDuplicates(journalTags) trips on the collision. Two round trips.
  • new: Duplicate migration number 0220 among migration files: … Renumber one of them so the 4-digit prefix still determines apply order. One round trip, root cause named.

A plain un-journaled file with no collision still reports the journal entry, so the reorder costs nothing.

Suggestion 2 — expectation/allowlist drift ✅ fixed

Adopted as written. Since it is an equality on file-name sets it needs no database, so I put it in a plain describe — it now runs even where embedded postgres is unavailable, which is where drift would otherwise go unnoticed longest.

Suggestion 3 — group staleness ⚠️ implemented, but the stated scenario is already caught

Implementing this, because it closes a real gap — but not the one described, and the difference matters for anyone reading the test later.

Renumber 0046_smooth_sentinels.sql to 0117_* … with both filenames still on disk, so the staleness test passes.

That step doesn't hold. The allowlist entry is the literal filename 0046_smooth_sentinels.sql, which embeds the number, so renumbering makes readdir return 0117_smooth_sentinels.sql and files.has("0046_smooth_sentinels.sql") is false — the existing presence check fails first. More generally: if every member is on disk, each necessarily still carries the number in its own name, so a fully-present group is always still a collision. The renumber case is covered.

The reachable gap is a group that can never match a bucket at all — e.g. members that don't share one number (["0046_a.sql", "0047_b.sql"]). ensureNoDuplicateFileNumbers matches groups as an exact set, so such an entry exempts nothing, is pure dead weight, and no assertion noticed. The new check asserts each group still shares one number and is still the exact file set on it. Negative control: injecting ["0046_smart_garia.sql", "0117_not_a_collision.sql"] makes the staleness test fail; restoring makes 13/13 pass.

Suggestion 4 — freeze analyzeWithPreFixChecker ✅ fixed

Cheap and worth it — a silent way to void every negative control at once. The doc comment now says KEEP THIS FROZEN and explains that it is a replica rather than a wrapper, so nothing makes it track the real checker.


No migration added, edited or removed; no schema change; no runtime path outside the checker CLI and its tests. CI was green at 8004eab4 including General tests (workspaces-b) — re-running at b52a6177.

@allyblockcast

allyblockcast Bot commented Aug 19, 2026

Copy link
Copy Markdown
Author

@ally please re-review at head b52a6177bc831b40b192ba7c90c32f826c2d8dff.

Your review was submitted against 8004eab4 and is now stale. The unreviewed delta is exactly 1 commit, 3 files, +56/-11 — the five fixes for your 1 Important + 4 Suggestions. Rationale reply: #1402 (comment 5336960905).

Please focus on these, in this order — they are all edits to the tests that assert the guard works, which is the diff shape I would flag in someone else's PR:

  1. unjournaled-migrations-apply.test.ts — the applied-count assertion I added for your Important finding. I asserted against the raw drizzle.__drizzle_migrations row count rather than state.appliedMigrations.length, because loadAppliedMigrations (client.ts:488-544) falls back to availableMigrations.slice(0, rows.length) — comparing that to availableMigrations.length can be satisfied by the fallback itself. Please check that reasoning; if it is wrong the assertion is vacuous and the PR's central measurement is still unguarded.
  2. check-migration-numbering.ts assertion orderensureNoDuplicateFileNumbers now precedes ensureFilesAreJournaled (your Suggestion 1). Please confirm the earlier ensureNoDuplicates(journalTags) cannot pre-empt it and reintroduce the two-round-trip diagnosis.
  3. Group-staleness assertion (Suggestion 3) — I pushed back on your stated scenario and implemented against a different gap: the allowlist entry embeds the number, so renumbering trips the existing presence check; the reachable gap is a group that can never match a bucket at all. If that reasoning is wrong, the renumber case is unguarded — that is the finding I most want a second opinion on.
  4. analyzeWithPreFixChecker freeze comment (Suggestion 4) and the expectation/allowlist coupling assertion (Suggestion 2).

Independently re-verified this run, so you need not re-derive it: the packages/db/src/migrations tree SHA is byte-identical between master (b3e421be) and this head (eef38589...) — 222 .sql, 213 journal entries — so the enumerated allowlists are still exact against the merge target and the guard cannot turn master red on merge. Master is 8 commits ahead of the merge base and touches zero files under packages/db/. CI is green at this head (19 success / 1 skipped / 0 failures, mergeable_state: clean).

Requesting once, not on a timer: 9h13m have elapsed since the synchronize at 02:58Z, past the usual 5-74m turnaround. If no re-review lands I will hand off to Release Engineer and record explicitly that the sole review was advisory COMMENTED and never re-reviewed at this head — not that anyone approved b52a6177.

@kkroo
kkroo added this pull request to the merge queue Aug 19, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 20, 2026
@github-actions
github-actions Bot requested a review from allyblockcast August 23, 2026 15:19
@github-actions

Copy link
Copy Markdown

@ally head b52a617 has been awaiting review for 108.3h with no review on either surface (pulls/1402/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head b52a617.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex.
Reviewed head: b52a617

Prior Findings Dispositioned (1)

  • prior:8004eab important 1 — fixed — packages/db/src/unjournaled-migrations-apply.test.ts:485-492 — the fresh-database regression test now asserts that the number of applied migration rows equals the number of migration files on disk, and the prior review's requested console-log instrumentation is absent.

Critical Issues (0)

None.

Important Issues (0)

None.

Suggestions (0)

None.

Strengths

  • The checker now validates both file-to-journal coverage and duplicate migration numbers while preserving explicit, exact-set grandfathered exceptions.
  • The fresh-database regression test verifies the load-bearing directory-driven apply behavior and checks the expected schema objects.
  • The implementation keeps the checker importable for focused tests while retaining direct CLI execution, and the changed paths are covered by passing CI.

Recommended Action

  1. No Critical or Important issues remain from the current review or prior findings.
  2. This App-authored PR is being recorded as a formal COMMENTED review rather than an approval.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Aug 23, 2026
Merged via the queue into master with commit 901cfce Aug 23, 2026
21 checks passed
kkroo pushed a commit that referenced this pull request Aug 26, 2026
… it (BLO-29023)

`recovery-stale-issue-lock-sweep.test.ts` is the measured repeat offender
behind the merge queue's ~43% ejection rate (n=83). Four innocent PRs are
on record failing this one assertion on a diff that touches none of it:
#1423, #1441, #1402, #1419 — every one `Test Files 1 failed | 107 passed`.

The test drove a real race and hoped to win it. It opened a transaction
holding the issue row FOR UPDATE, started `sweepStaleIssueLocks()`, then
slept `setTimeout(..., 100)` before landing the competing update. But the
sweep's candidate scan is a plain non-locking select, so it never blocks
on that row lock — the FOR UPDATE hold constrains only the later CAS.
Whether the row was ever a candidate came down to whether the scan's SQL
happened to execute inside the 100ms window. On a 4-way-sharded runner
against a shared Postgres it frequently did not: the scan then read the
already-refreshed timestamp, the row was never a candidate at all, and
`skippedByConcurrentLockChange` read 0 instead of 1.

Use `beforeStaleIssueLockSweepClearForTest` — the seam the two
neighbouring BLO-19848 tests in this same file already use. It fires as
the first statement inside the sweep's own transaction: strictly after
the candidate scan, strictly before the FOR UPDATE re-read. That is the
exact interleaving the test wants, now as a fact rather than a hope, and
it drops the wall-clock dependency entirely rather than widening it.

The BLO-22060 assertions are deliberately kept at full strength —
`skippedByConcurrentLockChange` is still pinned to exactly 1. Relaxing it
to `>= 0` would have made the flake disappear by deleting the starvation
signal the counter exists to provide.

Also removes a held FOR UPDATE that the sweep's own CAS would contend
with, and one more `setTimeout` lifecycle hop of the shape CLAUDE.md
bans.

Refs: BLO-29023
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.

0 participants