You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
renameRepositoryIdentity (src/db/repo-identity-rename.ts:77) exists so a GitHub repo rename moves every
repo-identity-bearing row forward instead of orphaning it. It deliberately covers both Drizzle-schema
tables and raw-SQL-only tables — review_audit, contributor_gate_history, submitter_stats, orb_pr_outcomes, orb_webhook_events, override_audit, tunables_overrides, tunables_overrides_shadow, predicted_gate_calibration_ledger and predicted_gate_calls are all folded by
raw env.DB.prepare(...) blocks between src/db/repo-identity-rename.ts:446 and src/db/repo-identity-rename.ts:578.
The completeness drift guard added alongside RENAME_OUT_OF_SCOPE_TABLES only enumerates Drizzle tables:
A raw-SQL-only table never appears in Object.entries(schema), so the guard at test/unit/repo-identity-rename.test.ts:1306 ("every schema.ts table with a repo-identity column is renamed
here or explicitly exempt") structurally cannot see the exact class of table the module already spends ten
blocks handling.
Replaying every migrations/*.sql into a fresh SQLite database and diffing the tables that are absent from src/db/schema.ts but carry a repo_full_name / repository_full_name / project / repo column against
what src/db/repo-identity-rename.ts references yields five unhandled, unexempt tables:
table
identity column(s)
migration
live writer
decision_records
repo_full_name
migrations/0179_decision_records.sql:10
src/review/decision-record.ts:317
decision_audit_labels
project, target_id, id
migrations/0178_decision_audit_labels.sql:8
src/review/close-audit-holdout.ts:179
submitter_outcome_log
project
migrations/0189_submitter_outcome_log.sql:9
src/review/submitter-reputation.ts:289
ai_review_verdict_flips
repo_full_name
migrations/0183_ai_review_verdict_flips.sql:8
src/review/verdict-flip-store.ts:25
alert_dedup_claims
project
migrations/0181_alert_dedup_claims.sql:9
src/review/alerts.ts:227
Three of these break a named consumer, not just "history":
submitter_outcome_log vs. its already-folded sibling.submitter_statsis renamed
(src/db/repo-identity-rename.ts:494–src/db/repo-identity-rename.ts:506). But the gate-facing signal is
read from the LOG, not the stats table:
-- src/review/submitter-reputation.ts:348SELECTCOUNT(*) AS submissions, ...
FROM submitter_outcome_log
WHERE project = ? AND submitter = ? AND recorded_at >= datetime('now', ?)
decision_audit_labels breaks the calibration join.loadCalibrationPairs
(src/review/risk-control-wire.ts:98) joins the two tables on a reconstructed identity string:
JOIN decision_records dr ONdr.id= (
SELECTdr2.idFROM decision_records dr2
WHEREdr2.repo_full_name||'#'||dr2.pull_number=dal.target_id
dal.target_id is owner/repo#N and dal.id is audit:<target_id>
(migrations/0178_decision_audit_labels.sql:9–migrations/0178_decision_audit_labels.sql:11). Rename decision_records.repo_full_name without rewriting decision_audit_labels.target_id — or vice versa —
and every adjudicated human label for that repo silently drops out of the Clopper–Pearson calibration.
The two tables have to move together and consistently.
decision_records is the contributor's evidentiary trail, kept 180 days by RETENTION_POLICY
(src/db/retention.ts:71) specifically so a dispute raised weeks after a close still has its record.
Every read path is scoped by repo_full_name (index decision_records_target, migrations/0179_decision_records.sql:21), so a rename orphans the whole trail.
ai_review_verdict_flips is PRIMARY KEY (repo_full_name, pull_number)
(migrations/0183_ai_review_verdict_flips.sql:14), so a stray new-name row can genuinely collide. alert_dedup_claims has id TEXT PRIMARY KEY plus UNIQUE (project, target_id, notification_key)
(migrations/0181_alert_dedup_claims.sql:18), so it can collide too.
Requirements
renameRepositoryIdentity must move all five tables' rows from oldFullName to newFullName, each using
the fold shape dictated by that table's REAL constraint (verified against its own migration), not by column
name:
decision_records — rename repo_full_name only, with a plain UPDATE decision_records SET repo_full_name = ? WHERE repo_full_name = ? (the orb_webhook_events shape, src/db/repo-identity-rename.ts:527). Its id (record:<repo>#<pr>@<head sha>, src/review/decision-record.ts:301) must NOT be rewritten: decision_ledger.record_id commits to it
inside a hash chain (src/review/decision-record.ts:490), and decision_replay_inputs.record_id / decision_replay_prompts.record_id key off it (src/review/decision-replay.ts:251, src/review/decision-replay.ts:264) — rewriting it would break the chain and orphan both replay tables.
This exception must be stated in a code comment at the new block.
decision_audit_labels — substring-replace the old name inside id and target_id, set project
to the new name, with a collision fold first (its UNIQUE (target_id) and id primary key can both
already hold a new-name row): exactly the review_audit shape at src/db/repo-identity-rename.ts:459–src/db/repo-identity-rename.ts:472.
submitter_outcome_log — PRIMARY KEY (project, submitter, pull_number, outcome)
(migrations/0189_submitter_outcome_log.sql:15): delete any already-existing new-name rows whose (submitter, pull_number, outcome) triple is present under the old name, favouring the pre-existing
old-name row, then rename — the composite-key fold shape used for submitter_stats
(src/db/repo-identity-rename.ts:494).
ai_review_verdict_flips — fold on pull_number, then rename, mirroring orb_pr_outcomes
(src/db/repo-identity-rename.ts:512).
alert_dedup_claims — fold on (target_id, notification_key), then rename project.
The drift guard in test/unit/repo-identity-rename.test.ts must additionally enumerate raw-SQL-only
tables. It must derive them by replaying migrations/*.sql into an in-memory node:sqlite database (the
approach replayMigrations in scripts/check-schema-drift.ts:96 already uses), listing every table whose PRAGMA table_info includes repo_full_name, repository_full_name, project, or repo, and asserting
each is either referenced by src/db/repo-identity-rename.ts or present in RENAME_OUT_OF_SCOPE_TABLES.
With this in place, adding a sixth such table without handling it must fail the test.
RENAME_OUT_OF_SCOPE_TABLES (src/db/repo-identity-rename.ts:639) already lists review_targets and repo_chunks, which the widened guard now sees for the first time — verify they still satisfy it and do
not start failing. Anything else the widened guard newly surfaces must be either folded or added to that
set with its reason in the existing prose block, never silently omitted.
Must NOT change: any existing table's fold, the RENAME_OUT_OF_SCOPE_TABLES entries for the rebuildable
AI/LLM caches, or renameRepositoryIdentity's no-op-on-equal-names early return
(src/db/repo-identity-rename.ts:77).
No migrations/*.sql file may be added or edited by this issue. A shipped migration is immutable in
this repo; a new contiguous migrations/NNNN_*.sql is the only legal path for any schema change, and this
issue needs no schema change at all — it is a pure source + test change.
⚠️ Required pattern: mirror the existing raw-SQL fold blocks in the same file — src/db/repo-identity-rename.ts:459 (substring-replace + PK-collision fold, review_audit), src/db/repo-identity-rename.ts:494 (composite-key fold, submitter_stats), src/db/repo-identity-rename.ts:512 (orb_pr_outcomes), and src/db/repo-identity-rename.ts:527
(plain rename, orb_webhook_events). What does NOT satisfy this issue: (a) replacing the explicit
per-table blocks with a generic cross-table helper — the module header at src/db/repo-identity-rename.ts:22 states the per-table shape is deliberate; (b) adding only the table
renames and leaving the drift guard Drizzle-only, so the next raw-SQL table repeats this; (c) adding the
five tables to RENAME_OUT_OF_SCOPE_TABLES instead of folding them — none of the five is a rebuildable
cache; (d) a test-only PR that widens the guard and marks the five as exempt to make it pass;
(e) rewriting decision_records.id, which breaks the ledger hash chain.
Deliverables
renameRepositoryIdentity (src/db/repo-identity-rename.ts) folds decision_records, decision_audit_labels, submitter_outcome_log, ai_review_verdict_flips and alert_dedup_claims,
each with the fold shape named above, and carries a code comment recording why decision_records.id is deliberately left alone.
A per-table assertion in test/unit/repo-identity-rename.test.ts for each of the five: seed a row under OLD, call renameRepositoryIdentity(env, OLD, NEW), assert zero rows remain under OLD and the row
is present under NEW with its non-identity columns intact. For decision_records, additionally assert id is byte-identical before and after.
A collision test for decision_audit_labels, submitter_outcome_log, ai_review_verdict_flips and alert_dedup_claims: seed a row under OLDand a colliding row under NEW sharing the same
secondary key, rename, and assert exactly one row survives and it is the one carrying the pre-existing OLD row's payload column (adjudication / outcome / flip_count / status respectively).
A test that loadCalibrationPairs(env, "close", NEW.toLowerCase())
(src/review/risk-control-wire.ts:98) returns the adjudicated pair that was joinable under OLD
before the rename — proving decision_records.repo_full_name and decision_audit_labels.target_id
moved together.
The drift guard in test/unit/repo-identity-rename.test.ts additionally enumerates raw-SQL-only
migration tables (via an in-memory replay of migrations/*.sql, matching scripts/check-schema-drift.ts:96) and asserts each is folded or exempt. The same test must assert the
derived table list is non-empty and includes at least review_audit and submitter_outcome_log, so a
broken derivation cannot make the guard vacuously pass.
A regression test named for this bug at test/unit/repo-identity-rename.test.ts asserting that after a
rename, getSubmitterReputation(env, NEW, submitter) (src/review/submitter-reputation.ts:334)
returns the same windowed counts it returned for (OLD, submitter) before the rename.
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example one
that adds the five folds but leaves the drift guard reading only src/db/schema.ts — does not resolve this
issue.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted. vitest.config.ts's coverage.include
covers src/**/*.ts and packages/loopover-engine/src/**/*.ts; src/db/repo-identity-rename.ts is measured.
Each collision fold introduces an if (colliding*.length > 0) branch and both arms need a test: a rename
with no colliding new-name row (guard false) and one with a collision (guard true). The two plain-rename
blocks (decision_records, and decision_audit_labels's trailing UPDATE) introduce no new branch but must
still be executed by a test. The test file itself is not measured, but the derived-table-list assertion is
what keeps the widened guard honest.
Expected Outcome
A GitHub repo rename moves the contributor's decision records, the human adjudication labels, the submitter
outcome log, the verdict-flip history and the alert dedup claims forward with everything else, so getSubmitterReputation and loadCalibrationPairs keep working across a rename instead of silently going
blank. The completeness guard covers raw-SQL-only tables as well as Drizzle ones, so the next such table
cannot ship unhandled — closing the blind spot that let this recur after #8380 and #9650.
Context
renameRepositoryIdentity(src/db/repo-identity-rename.ts:77) exists so a GitHub repo rename moves everyrepo-identity-bearing row forward instead of orphaning it. It deliberately covers both Drizzle-schema
tables and raw-SQL-only tables —
review_audit,contributor_gate_history,submitter_stats,orb_pr_outcomes,orb_webhook_events,override_audit,tunables_overrides,tunables_overrides_shadow,predicted_gate_calibration_ledgerandpredicted_gate_callsare all folded byraw
env.DB.prepare(...)blocks betweensrc/db/repo-identity-rename.ts:446andsrc/db/repo-identity-rename.ts:578.The completeness drift guard added alongside
RENAME_OUT_OF_SCOPE_TABLESonly enumerates Drizzle tables:A raw-SQL-only table never appears in
Object.entries(schema), so the guard attest/unit/repo-identity-rename.test.ts:1306("every schema.ts table with a repo-identity column is renamedhere or explicitly exempt") structurally cannot see the exact class of table the module already spends ten
blocks handling.
Replaying every
migrations/*.sqlinto a fresh SQLite database and diffing the tables that are absent fromsrc/db/schema.tsbut carry arepo_full_name/repository_full_name/project/repocolumn againstwhat
src/db/repo-identity-rename.tsreferences yields five unhandled, unexempt tables:decision_recordsrepo_full_namemigrations/0179_decision_records.sql:10src/review/decision-record.ts:317decision_audit_labelsproject,target_id,idmigrations/0178_decision_audit_labels.sql:8src/review/close-audit-holdout.ts:179submitter_outcome_logprojectmigrations/0189_submitter_outcome_log.sql:9src/review/submitter-reputation.ts:289ai_review_verdict_flipsrepo_full_namemigrations/0183_ai_review_verdict_flips.sql:8src/review/verdict-flip-store.ts:25alert_dedup_claimsprojectmigrations/0181_alert_dedup_claims.sql:9src/review/alerts.ts:227Three of these break a named consumer, not just "history":
submitter_outcome_logvs. its already-folded sibling.submitter_statsis renamed(
src/db/repo-identity-rename.ts:494–src/db/repo-identity-rename.ts:506). But the gate-facing signal isread from the LOG, not the stats table:
After a rename,
getSubmitterReputationreturnsneutralfor every submitter on that repo while theoperator
/statsview (which readssubmitter_stats,src/review/contributor-trust-profile.ts) stillshows the real totals. The two disagree permanently with no repair path — the same
"sibling already fixed, this one wasn't" shape renameRepositoryIdentity omits 7 raw-SQL identity tables, including two structurally identical to already-fixed siblings review_audit/contributor_gate_history #8380 and db: renameRepositoryIdentity omits four more repo-identity tables, and nothing guards the list #9650 were filed for.
decision_audit_labelsbreaks the calibration join.loadCalibrationPairs(
src/review/risk-control-wire.ts:98) joins the two tables on a reconstructed identity string:dal.target_idisowner/repo#Nanddal.idisaudit:<target_id>(
migrations/0178_decision_audit_labels.sql:9–migrations/0178_decision_audit_labels.sql:11). Renamedecision_records.repo_full_namewithout rewritingdecision_audit_labels.target_id— or vice versa —and every adjudicated human label for that repo silently drops out of the Clopper–Pearson calibration.
The two tables have to move together and consistently.
decision_recordsis the contributor's evidentiary trail, kept 180 days byRETENTION_POLICY(
src/db/retention.ts:71) specifically so a dispute raised weeks after a close still has its record.Every read path is scoped by
repo_full_name(indexdecision_records_target,migrations/0179_decision_records.sql:21), so a rename orphans the whole trail.ai_review_verdict_flipsisPRIMARY KEY (repo_full_name, pull_number)(
migrations/0183_ai_review_verdict_flips.sql:14), so a stray new-name row can genuinely collide.alert_dedup_claimshasid TEXT PRIMARY KEYplusUNIQUE (project, target_id, notification_key)(
migrations/0181_alert_dedup_claims.sql:18), so it can collide too.Requirements
renameRepositoryIdentitymust move all five tables' rows fromoldFullNametonewFullName, each usingthe fold shape dictated by that table's REAL constraint (verified against its own migration), not by column
name:
decision_records— renamerepo_full_nameonly, with a plainUPDATE decision_records SET repo_full_name = ? WHERE repo_full_name = ?(theorb_webhook_eventsshape,src/db/repo-identity-rename.ts:527). Itsid(record:<repo>#<pr>@<head sha>,src/review/decision-record.ts:301) must NOT be rewritten:decision_ledger.record_idcommits to itinside a hash chain (
src/review/decision-record.ts:490), anddecision_replay_inputs.record_id/decision_replay_prompts.record_idkey off it (src/review/decision-replay.ts:251,src/review/decision-replay.ts:264) — rewriting it would break the chain and orphan both replay tables.This exception must be stated in a code comment at the new block.
decision_audit_labels— substring-replace the old name insideidandtarget_id, setprojectto the new name, with a collision fold first (its
UNIQUE (target_id)andidprimary key can bothalready hold a new-name row): exactly the
review_auditshape atsrc/db/repo-identity-rename.ts:459–src/db/repo-identity-rename.ts:472.submitter_outcome_log—PRIMARY KEY (project, submitter, pull_number, outcome)(
migrations/0189_submitter_outcome_log.sql:15): delete any already-existing new-name rows whose(submitter, pull_number, outcome)triple is present under the old name, favouring the pre-existingold-name row, then rename — the composite-key fold shape used for
submitter_stats(
src/db/repo-identity-rename.ts:494).ai_review_verdict_flips— fold onpull_number, then rename, mirroringorb_pr_outcomes(
src/db/repo-identity-rename.ts:512).alert_dedup_claims— fold on(target_id, notification_key), then renameproject.test/unit/repo-identity-rename.test.tsmust additionally enumerate raw-SQL-onlytables. It must derive them by replaying
migrations/*.sqlinto an in-memorynode:sqlitedatabase (theapproach
replayMigrationsinscripts/check-schema-drift.ts:96already uses), listing every table whosePRAGMA table_infoincludesrepo_full_name,repository_full_name,project, orrepo, and assertingeach is either referenced by
src/db/repo-identity-rename.tsor present inRENAME_OUT_OF_SCOPE_TABLES.With this in place, adding a sixth such table without handling it must fail the test.
RENAME_OUT_OF_SCOPE_TABLES(src/db/repo-identity-rename.ts:639) already listsreview_targetsandrepo_chunks, which the widened guard now sees for the first time — verify they still satisfy it and donot start failing. Anything else the widened guard newly surfaces must be either folded or added to that
set with its reason in the existing prose block, never silently omitted.
RENAME_OUT_OF_SCOPE_TABLESentries for the rebuildableAI/LLM caches, or
renameRepositoryIdentity's no-op-on-equal-names early return(
src/db/repo-identity-rename.ts:77).migrations/*.sqlfile may be added or edited by this issue. A shipped migration is immutable inthis repo; a new contiguous
migrations/NNNN_*.sqlis the only legal path for any schema change, and thisissue needs no schema change at all — it is a pure source + test change.
Deliverables
renameRepositoryIdentity(src/db/repo-identity-rename.ts) foldsdecision_records,decision_audit_labels,submitter_outcome_log,ai_review_verdict_flipsandalert_dedup_claims,each with the fold shape named above, and carries a code comment recording why
decision_records.idis deliberately left alone.test/unit/repo-identity-rename.test.tsfor each of the five: seed a row underOLD, callrenameRepositoryIdentity(env, OLD, NEW), assert zero rows remain underOLDand the rowis present under
NEWwith its non-identity columns intact. Fordecision_records, additionally assertidis byte-identical before and after.decision_audit_labels,submitter_outcome_log,ai_review_verdict_flipsandalert_dedup_claims: seed a row underOLDand a colliding row underNEWsharing the samesecondary key, rename, and assert exactly one row survives and it is the one carrying the pre-existing
OLDrow's payload column (adjudication/outcome/flip_count/statusrespectively).loadCalibrationPairs(env, "close", NEW.toLowerCase())(
src/review/risk-control-wire.ts:98) returns the adjudicated pair that was joinable underOLDbefore the rename — proving
decision_records.repo_full_nameanddecision_audit_labels.target_idmoved together.
test/unit/repo-identity-rename.test.tsadditionally enumerates raw-SQL-onlymigration tables (via an in-memory replay of
migrations/*.sql, matchingscripts/check-schema-drift.ts:96) and asserts each is folded or exempt. The same test must assert thederived table list is non-empty and includes at least
review_auditandsubmitter_outcome_log, so abroken derivation cannot make the guard vacuously pass.
test/unit/repo-identity-rename.test.tsasserting that after arename,
getSubmitterReputation(env, NEW, submitter)(src/review/submitter-reputation.ts:334)returns the same windowed counts it returned for
(OLD, submitter)before the rename.All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example one
that adds the five folds but leaves the drift guard reading only
src/db/schema.ts— does not resolve thisissue.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted.
vitest.config.ts'scoverage.includecovers
src/**/*.tsandpackages/loopover-engine/src/**/*.ts;src/db/repo-identity-rename.tsis measured.Each collision fold introduces an
if (colliding*.length > 0)branch and both arms need a test: a renamewith no colliding new-name row (guard false) and one with a collision (guard true). The two plain-rename
blocks (
decision_records, anddecision_audit_labels's trailingUPDATE) introduce no new branch but muststill be executed by a test. The test file itself is not measured, but the derived-table-list assertion is
what keeps the widened guard honest.
Expected Outcome
A GitHub repo rename moves the contributor's decision records, the human adjudication labels, the submitter
outcome log, the verdict-flip history and the alert dedup claims forward with everything else, so
getSubmitterReputationandloadCalibrationPairskeep working across a rename instead of silently goingblank. The completeness guard covers raw-SQL-only tables as well as Drizzle ones, so the next such table
cannot ship unhandled — closing the blind spot that let this recur after #8380 and #9650.
Links & Resources
src/db/repo-identity-rename.ts:77—renameRepositoryIdentitysrc/db/repo-identity-rename.ts:459—review_auditsubstring-replace + PK-collision foldsrc/db/repo-identity-rename.ts:494—submitter_statscomposite-key foldsrc/db/repo-identity-rename.ts:639—RENAME_OUT_OF_SCOPE_TABLEStest/unit/repo-identity-rename.test.ts:1293— the Drizzle-only drift guardscripts/check-schema-drift.ts:96—replayMigrations, the migration-replay helper to reusesrc/review/submitter-reputation.ts:348— the windowed read that goes blank after a renamesrc/review/risk-control-wire.ts:98— the calibration join that breaks if the two tables move apart