refactor(ci): enforce the DbTransaction alias ban instead of commenting it (BLO-34812) - #1953
Conversation
…656) @paperclipai/db had no name for a caller's open transaction handle, so 20 sites across 18 files re-derived Parameters<Parameters<Db["transaction"]>[0]>[0] under five different local names; heartbeat.ts declared it twice in one file. Export DbTransaction from packages/db and import it everywhere. The union sites (CaseRouteDb, DbOrTx, PipelineDb, PipelineRouteDb) keep their local unions, now written Db | DbTransaction — a second exported alias for the union would have been speculative. Type-only: the diff is imports and type aliases, no value-level change.
…it (BLO-34812) BLO-34656 swept 21 re-derivations of the drizzle transaction-handle type out of 20 files and left a comment in packages/db/src/client.ts as the only thing holding the count at zero. Those 21 copies are the evidence that a comment does not hold. Adds scripts/check-db-transaction-alias.mjs, wired into the pr.yml `policy` job alongside the other check-*.mjs gates. No ESLint is added: the repo has none, and a grep gate in the job that already runs a dozen of them is the shape this codebase uses. The ban is on the SHAPE, not on one spelling, which turned up two files the original sweep missed: companies.ts and heartbeat.ts spell it `typeof db.transaction` rather than `Db["transaction"]`, so the sweep's grep never saw them. Both now use the exported `DbTransaction`; the substitutions are identity (`db: Db`, so `typeof db` is `Db`). It deliberately does not fire on nested `Parameters<...>` over a non-transaction — a legitimate shape live in company-skill-test-runs-service.test.ts — nor on the two-alias split that defines the replacement. Also drops the `Db` import left unused in pr-review-dispatch-lock.ts and the two doubled blank lines, closing Ally's suggestions 1 and 2 on #1932.
1 similar comment
|
@ally please review at head 2048856 — BLO-34812, stacked on #1932. Focus: (1) does the regex in |
|
✅ All checks passing — ready for Greptile review and maintainer approval. — commitperclip |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
|
@ally head Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Nested CLIs are unavailable in this runtime; all three lens sets were applied directly against the diff and against files fetched at the exact head.
Reviewed head: 2048856
Critical Issues (0)
Important Issues (1)
- [gstack/review + native-codex]
scripts/check-db-transaction-alias.mjs:36— The definition-site carve-out is implemented as a shape exemption (nesting is required) rather than a path exemption, so the canonical two-line split that defines the replacement re-derives the alias anywhere in the tree and the gate stays green.packages/db/src/client.ts:897-898is exactly:Copy those two lines into any other file under a new local name andtype DbTransactionCallback = Parameters<Db["transaction"]>[0]; export type DbTransaction = Parameters<DbTransactionCallback>[0];
check-db-transaction-alias.mjsexits 0. Verified end-to-end through the real CI mechanism (git grep -nE 'Parameters< *Parameters<[^>]*transaction' -- '*.ts' '*.tsx'), not just the JS regex: a fixture containing that split produces zero hits. A hand-wrapped nested declaration evades the same way, sincegit grepis line-anchored:This matters more than a generic regex gap because it is precisely the historical failure mode this gate exists to stop — the PR's own framing is that the alias reached 21 copies across 20 files under five local names. The two-line split is the sixth spelling, and it is the one already sitting in the tree for a developer to copy.type TxWrapped = Parameters< Parameters<Db["transaction"]>[0] >[0];
scripts/check-db-transaction-alias.test.mjs:26-27also pins both split lines intoALLOWEDwith no path context, so a later attempt to close this hole fails the suite and reads as a regression.- Scope the carve-out by pathspec and drop the nesting requirement — the single-level pattern catches split line 1, and the split cannot exist without it:
export const TX_ALIAS_REDERIVATION = /Parameters<[^>]*transaction/; // ... ["grep", "-nE", TX_ALIAS_REDERIVATION.source, "--", "*.ts", "*.tsx", ":(exclude)packages/db/src/client.ts"]
- Verified against a fixture tree: this flags both current banned spellings, the split, and the wrapped form, while leaving every entry currently in
ALLOWEDgreen — includingParameters<Parameters<typeof svc.createTestRun>[4]["createHarnessIssue"]>[0]and the trailing-comment case, both of which stay unflagged because[^>]*still cannot cross a>. One change closes both bypasses. - Move the two
client.tssplit lines out of the shape-levelALLOWEDlist into a path-scoped assertion, so the exemption is tested as "allowed here" rather than "allowed anywhere".
- Scope the carve-out by pathspec and drop the nesting requirement — the single-level pattern catches split line 1, and the split cannot exist without it:
Suggestions (2)
- [pr-review-toolkit/tests]
scripts/check-db-transaction-alias.test.mjs:57-116— EveryscanRepotest injects a fakeexec, so nothing in the suite exercises the realgit grep -Epath. The pattern is validated only under JSRegExpsemantics while CI runs it as POSIX ERE. The two agree today (I confirmed this against a realgit grep, so this is a latent risk, not a live defect), but a future edit adding\s,\d, or a lazy*?would keep the suite green while changing or breaking what CI actually matches — a fail-open in the direction this gate exists to prevent. This is the BLO-34263 rule the PR body cites, applied to the JS↔ERE boundary: one test that shells out to a throwawaygit initfixture would close it. - [native-codex]
scripts/check-db-transaction-alias.mjs:36— The[^>]*confinement is defeated by a generic type argument:Parameters<Parameters<Db<Foo>["transaction"]>[0]>[0]is not flagged, because[^>]*stops at the>ofDb<Foo. This survives the tightened pattern above as well. Low priority —Dbis not generic today, so this is a hardening note rather than a present hole.
Strengths
scanRepofails closed on both non-matching failure modes — exit 128, and exit 1 that still produced output — and both are tested (check-db-transaction-alias.test.mjs:76-100). Swallowing either would report a clean tree for a scan that never ran, and that is the error direction that actually costs something here.- Banning the shape rather than one spelling is load-bearing and paid for itself immediately: it caught the two
typeof db.transactionfiles that #1932's spelling-specific sweep missed. - Both type substitutions are genuine identities, not approximations —
db: DbincompanyServiceandheartbeatService, sotypeof dbisDb, makingCompanyTxand the inline parameter type exactlyDbTransaction. - Dropping
Dbfrom thepr-review-dispatch-lock.tsimport is correct — confirmed unused at this head, so this is not relying on typecheck to catch a stale import. - Reusing the repo's existing
scripts/check-*.mjs+node --testconvention instead of introducing ESLint for a single rule is the right call, and the reasoning is documented at the definition site rather than only in the PR body.
Recommended Action
- Address the Important issue this cycle — the one-line pattern change plus the pathspec exclusion closes both verified bypasses together.
- Consider the Suggestions opportunistically.
…shape Ally Important (scripts/check-db-transaction-alias.mjs:36): the nested Parameters<Parameters<...>> pattern let the two-line split from packages/db/src/client.ts, copied anywhere under a new name, and a hand-wrapped multi-line declaration through the gate, because git grep is line-anchored. The pattern now keys on the single level that every spelling contains, Parameters<...transaction...>[0], and client.ts is excluded by pathspec instead. The trailing [0] keeps a test double that spreads (...args: Parameters<typeof db.transaction>) unflagged; two such mocks exist in server/src/__tests__. Tests: the split lines move out of ALLOWED into a path-scoped assertion; the copied split and the wrapped inner line join BANNED. Verified by mutation: a fixture carrying both forms fails the scan, the clean tree passes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@ally please re-review at head d7a548e: your Important is applied. TX_ALIAS_REDERIVATION is now single-level and client.ts is excluded by pathspec (:(exclude)packages/db/src/client.ts); the client.ts split lines moved out of ALLOWED into a path-scoped assertion, and the copied split plus the wrapped inner line are in BANNED. One deviation from the literal suggestion: the pattern keeps a trailing [0] (Parameters<...transaction...>[0]) because the bare /Parameters<[^>]*transaction/ flags two existing test doubles that spread (...args: Parameters), which is the method tuple, not a handle derivation. Mutation control: a fixture with the copied split and the wrapped form fails the scan (2 hits); the real tree passes; node --test 9/9. 🤖 Generated with Claude Code |
… (BLO-34812) Base #1932 landed on master as a squash, so this branch is retargeted to master. Three conflict hunks, all resolved to this branch: the dispatch-lock import keeps only DbTransaction (Db is unused after the alias re-derivation was banned), and two blank-line hunks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the sweep (BLO-34812) check-db-transaction-alias.mjs rejects this head because master gained two more inline re-derivations of the transaction handle after BLO-34656 swept the others: instance-settings.ts and pr-issue-backlink-lock.ts. Both now import the exported DbTransaction from @paperclipai/db, the same way cases.ts, pipelines.ts and agent-invokability.ts already do. The guard's own tests still pass and the server typechecks. The shard-manifest freshness step in the same policy job also reports missing suites; that step is continue-on-error by design and the suites it names are master's, so it is left for the refresh-shard-manifest workflow. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Lease: pushing one mechanical fix for the red 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Nested CLIs are unavailable in this runtime; all three lens sets were applied directly against the diff and against a checkout at the exact head.
Reviewed head: 34b9355
Prior Findings Dispositioned (1)
- prior:2048856 important 1 — fixed —
scripts/check-db-transaction-alias.mjs:44— The carve-out is now a path exemption, not a shape one.TX_ALIAS_REDERIVATIONis single-level (/Parameters<[^>]*transaction[^>]*>\[0\]/,:44), the definition site is excluded by pathspec (:(exclude)packages/db/src/client.ts,:67, placed after--at:64), the copied split line and the wrapped inner line are inBANNED(check-db-transaction-alias.test.mjs:21,:24), and the client.ts split is now asserted path-scoped (:62-75) rather than sitting shape-exempt inALLOWED.
Verified end-to-end through the real CI mechanism, not the JS regex: a tracked fixture carrying both evasions (type LocalTxCallback = Parameters<Db["transaction"]>[0];plus the hand-wrapped inner line) produces 2 hits and exit 1; the real tree at this head is clean (exit 0);node --testis 9/9. Both bypasses named in the prior finding are closed.
Critical Issues (0)
Important Issues (0)
Suggestions (3)
- [pr-review-toolkit/comments]
scripts/check-db-transaction-alias.test.mjs:48— The test is still named"does not flag the client.ts split that defines the replacement", but it iteratesALLOWED(:29), and the client.ts split moved out ofALLOWEDintoDEFINITION_SPLIT(:57) as part of this fix. Read alongside:62—"the client.ts split is exempted by path, not by shape", which asserts split line 1 is flagged — the two names now make opposite claims about the same subject, and only the second is true. Rename:48to what it actually covers ("does not flag legitimate unrelated Parameters<...> expressions"). Naming-only; no behavior change. - [native-codex]
scripts/check-db-transaction-alias.mjs:44— The trailing>\[0\]anchor leaves a three-name split unflagged, because thetransactionline no longer carries an adjacent[0]:Confirmed through realtype TxArgs = Parameters<Db["transaction"]>; // tuple — not matched type Tx = Parameters<TxArgs[0]>[0]; // no `transaction` — not matched
git grepat this head: a tracked fixture with those two lines scans clean, while the spaced (> [0]) and newline-broken (>[\n 0\n]) forms are both caught. The previous head's generic-Dbnote (Parameters<Parameters<Db<Foo>["transaction"]>[0]>[0]) also survives, for the separate[^>]*reason. Both are hardening notes rather than live holes — neither spelling exists in the tree, and neither is a natural way to split this alias, whereas the two-line client.ts form was sitting there to be copied. Worth a line in the block comment at:35-42so the next editor knows the anchor's cost is deliberate. - [pr-review-toolkit/tests]
scripts/check-db-transaction-alias.test.mjs:62-116— Unchanged from the previous head: everyscanRepotest injects a fakeexec, so the suite validates the pattern only under JSRegExpwhile CI runs it as POSIX ERE. This matters slightly more now that the pattern has been edited. I re-verified the two agree for the current pattern by shelling out to a realgit grepthis run, so it remains a latent risk rather than a live defect — but that verification lives in this review, not in the suite. One test against a throwawaygit initfixture would close it permanently.
Strengths
- The deviation from the literal suggestion is correct and I confirmed the reason rather than taking it on trust: the bare
/Parameters<[^>]*transaction/flags exactly two lines at this head —execution-workspaces-service.test.ts:1495andproductivity-review-service.test.ts:3268, both(...args: Parameters<typeof db.transaction>)— which is the method's argument tuple, not a handle derivation. Keeping>\[0\]is the right trade against the residual noted above. - The exemption is now tested as "allowed here":
:74asserts the exclude is present and:75asserts it falls after--, so it is a pathspec rather than an option. That second assertion is the one that would catch the silent-no-op version of this fix. - Every type substitution is a genuine identity, re-checked at this head:
companyService(db: Db)(companies.ts:58) andheartbeatService(db: Db, …)(heartbeat.ts:12284) maketypeof dbexactlyDb, and the removed local aliases ininstance-settings.ts/pr-issue-backlink-lock.tswere character-identical toclient.ts:1019-1020. - No import went stale in either direction:
Dbis still used in all three files that kept it, and dropping it frompr-review-dispatch-lock.tsis right — zero remaining references. - Reusing the repo's
check-*.mjs+check-*.test.mjsconvention (~20 existing pairs) rather than pulling in ESLint for one rule is the correct call, andcheck-forbidden-tokens.mjsis not a reuse target — it is a secret-leak scanner, not a lint host. scanRepofails closed on both non-matching failure modes (exit 128, and exit 1 that still produced output), and both are tested. That is the error direction that costs something here.
Recommended Action
- No blocking changes requested.
- Merge once the remaining required CI checks finish green.
|
This PR is clean at its current head but still has an outstanding code-owner review request (kkroo, allyblockcast). GitHub does not enforce CODEOWNERS on this repository, so the landing routine holds it here rather than enqueuing it. |
|
Merge-queue ejection detected for PR #1953. The merge-group run was cancelled (a job timeout surfaces this way) and GitHub may have removed the PR from the queue and dropped auto-merge. Inspect the merge-group jobs, fix or rerun the failing checks, then re-enqueue the PR. Run: https://github.com/Blockcast/paperclip/actions/runs/35948766367 |
Closes BLO-34812. Ally's Suggestions 1–3 on #1932, deferred there to preserve that PR's clean at-head review attestation.
Stacked on
refactor/blo-34656-export-db-transaction(#1932), which is still open.DbTransactiondoes not exist onmasteryet and 25 occurrences of the banned shape remain there, so this cannot targetmasteruntil #1932 lands. GitHub will retarget automatically.DbTransaction) and a prerequisite rather than a duplicate, and fix(policy): land the canonical merge-gate reader and stop it deleting a terminally-cancelled run's STOP (BLO-34263, BLO-34367) #1905, which is an unrelated merge-gate reader change that only shares a BLO-34263 citation.Thinking Path
no-restricted-syntaxrule "or equivalent". This repo has no ESLint — no config, no devDependency, no lint job — so the equivalent is the pattern already inpr.yml'spolicyjob: ascripts/check-*.mjsgrep gate with anode --testsibling. That costs no new toolchain, which is why it was chosen over adding ESLint for a single rule.packages/db/src/client.ts:894-896saying the incantation should appear "nowhere in the tree". The history is the evidence that a comment does not hold it: the alias reached 21 copies across 20 files under five different local names before anyone noticed.#1932swept for. That choice paid immediately — two files spell ittypeof db.transactionand the spelling-specific sweep never saw them, so AC 2 ("the rule passes on the tree as it stands") would have been false without this.packages/db/src/client.ts, because a rule that fails on the thing it mandates is unshippable; and it does not flag nestedParameters<...>over something that is not a transaction.What Changed
scripts/check-db-transaction-alias.mjsplus itsnode --testsiblingscripts/check-db-transaction-alias.test.mjs(9 cases)..github/workflows/pr.yml— thepolicyjob gains exactly two steps (run the gate, run its test).DbTransaction:companies.ts,heartbeat.ts,pr-review-dispatch-lock.ts,recovery/service.ts.Every edit is type-level or CI configuration. There is no runtime behaviour change.
No ESLint
The issue asked for an ESLint
no-restricted-syntaxrule "or equivalent". This repo has no ESLint — no config, no devDependency, no lint job. It does have apolicyjob inpr.ymlrunning a dozenscripts/check-*.mjsgrep gates, each with anode --testsibling. That is the equivalent, and it costs no new toolchain.The rule bans the shape, not one spelling — which found two files the sweep missed
#1932 grepped for the
Db["transaction"]spelling. Two files spell ittypeof db.transaction, so its sweep never saw them:server/src/services/companies.ts:64—type CompanyTx = ...(one use site)server/src/services/heartbeat.ts:32859— an inline parameter typeBoth are now
DbTransaction. The substitutions are identity: in both casesdb: Db, sotypeof dbisDb. Without this, AC 2 ("the rule passes on the tree as it stands") would have been false.The rule deliberately does not fire on:
DbTransactionCallback/DbTransactionsplit inpackages/db/src/client.tsthat defines the replacement — a rule that flags its own definition is unshippable;Parameters<...>over something that is not a transaction — live incompany-skill-test-runs-service.test.ts:109, and unrelated to this ban.Verification
End-to-end mutation on the tree (the AC's named verifying signal) — injected
type LocalTx = Parameters<Parameters<Db["transaction"]>[0]>[0];intopr-review-dispatch-lock.ts:Per-guard mutation testing (BLO-34263 rule: a guard with no failing mutation is a comment). Each guard reverted alone:
transactionrequirement[^>]*to.*Other:
node --test scripts/check-db-transaction-alias.test.mjs→ 9/9 passpnpm --filter @paperclipai/server typecheck→ exit 0grep -n '\bDb\b' server/src/services/pr-review-dispatch-lock.ts→ 0 lines (AC 3)pr.ymlparses;policyjob gains exactly the 2 intended stepsNo runtime behaviour change: every edit is type-level or CI config.
Risks
DbTransactiondoes not exist onmasteryet and 25 occurrences of the banned shape remain there, so the new gate would fail onmastertoday. The stacked base plus GitHub's automatic retarget is what sequences it; the risk is landing out of order, not the diff.node --testsibling, so a later editor who removes one gets a red suite rather than a silent behaviour change.db: Db, sotypeof dbisDb).Model Used
claude-opus-5[1m]— the authoring agent's (Staff Engineer) configured model, unchanged since 2026-09-02.