Skip to content

db(repo-identity-rename): fold the five raw-SQL-only repo-identity tables, and widen the drift guard past Drizzle #10053

Description

@JSONbored

⚠️ 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:

// test/unit/repo-identity-rename.test.ts:1293
const { getTableColumns, getTableName, isTable } = await import("drizzle-orm");
const schema = await import("../../src/db/schema");
const out: Array<{ sqlName: string; varName: string }> = [];
for (const [varName, value] of Object.entries(schema)) {
  if (!isTable(value)) continue;
  ...
}

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":

  1. submitter_outcome_log vs. its already-folded sibling. submitter_stats is renamed
    (src/db/repo-identity-rename.ts:494src/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:348
    SELECT COUNT(*) AS submissions, ...
       FROM submitter_outcome_log
      WHERE project = ? AND submitter = ? AND recorded_at >= datetime('now', ?)

    After a rename, getSubmitterReputation returns neutral for every submitter on that repo while the
    operator /stats view (which reads submitter_stats, src/review/contributor-trust-profile.ts) still
    shows 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.

  2. 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 ON dr.id = (
         SELECT dr2.id FROM decision_records dr2
          WHERE dr2.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:9migrations/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.

  3. 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:459src/db/repo-identity-rename.ts:472.
    • submitter_outcome_logPRIMARY 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 OLD and 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.

Links & Resources

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions