← all branches

fix/dup-repair

needs attentionviewing older commit
57eb1eb · incrementalPR #306reviewed 2026-07-14 02:41 UTC0H · 2M · 4L · 5I
The branch
Purpose
Repair June-2026 duplicate-contract damage caused by CFE cross-account contamination (#231) and a bills[0] stale-evidence inversion bug (fixed upstream in #298).
Goal
One-shot operational script: flip evidence-holder contracts to active, soft-delete contamination twins, sweep foreign bills, and re-point SUCs + subscriptions + entity_relationships.
Sub-goals
  • SG-1: Flip rule — newest-CFE-evidence holder becomes active row; contamination twins soft-deleted, inverted replacements terminated as history
  • SG-2: Foreign-bill sweep — delete 12 #231-residue xml bills with RPU mismatch + emit S3 manifest
  • SG-3: Safety — dry-run default, typed confirmation ('reparar'), 0600 snapshot, per-group FOR UPDATE transactions, idempotent re-runs, post-execution verify pass
The changes (whole branch)
What
Round-3 hardening: (1) classifier now rejects multi-active groups (actives.filter + length !== 1 guard + test); (2) REACTIVATE_NO_EVIDENCE evidence check correctly aborts if any evidence appeared since census; (3) entity_relationships dedup JOIN gains source_type predicate; (4) twinBillCount logged before purge for operator visibility; (5) dry-run demote label mirrors execution branch; (6) --limit tightened to integer-only.
Why
Prior loop-review round identified the original evidence-check logic was incorrect for the REACTIVATE_NO_EVIDENCE path (would have aborted valid flips) and the entity_relationships dedup was too broad (could over-delete rows with different source_types).
Areas
scripts/__tests__/repair-dup-contracts-core.test.ts+90scripts/lib/repair-dup-contracts-core.ts+32scripts/repair-dup-contracts.ts+206
Blast
3 files, +32/−8 lines. Script-only — no production code changed. Cumulative branch: 3 files, +874 lines (all new: script + pure core + unit tests).
data-remediation one-shot-script staging-rehearsed
ci· No CI checks registered on this PRcoderabbit· No .coderabbit.yaml in repo

Findings · 10

correctness2

info

Redundant !active check after actives.length !== 1 guard

scripts/lib/repair-dup-contracts-core.ts

When `actives.length !== 1` passes, `actives[0]` is guaranteed to be defined, making `!active` always false. Safe to drop — the length check is the real invariant. Harmless but slightly misleading.

info

REACTIVATE_NO_EVIDENCE evidence-check inversion: now correctly guards the no-evidence premise

scripts/repair-dup-contracts.ts

Old code ran the FLIP-side 'evidence changed' guard for REACTIVATE_NO_EVIDENCE too (wrong: it would have aborted when evidence IS on the holder, which is exactly where it should be). New code correctly checks `holderNewest || deposedNewest` (any evidence = premise violated, re-census). This was a latent bug — confirmed clean.

security1

low

twinBillCount: numeric coercion from raw DB row could suppress log

scripts/repair-dup-contracts.ts

PostgreSQL count may be serialized as a string by some adapters. The `as unknown as Array<{n: number}>` cast doesn't validate at runtime, so `twinBillCount` could be the string '3'. The `> 0` check still passes (coerced), but logging is the only purpose — the DELETE proceeds correctly regardless. Use `Number(...)` to make the coercion explicit.

conventions2

medium

twinBills dual-path cast lacks explanatory comment

scripts/repair-dup-contracts.ts

`(twinBills.rows ?? twinBills)` is a Drizzle/pg-driver shape uncertainty that appears multiple times in the script. A one-line comment (`// pg-js returns rows array directly; pooler wraps it`) prevents a future reader from deleting the `?? twinBills` fallback as dead code.

low

Dry-run demote label: else-arm silently covers future verdicts

scripts/repair-dup-contracts.ts

`p.verdict === 'FLIP_KEEP_HISTORY' ? 'terminate' : 'soft-delete + purge bills'` currently covers FLIP_DELETE_TWIN + REACTIVATE_NO_EVIDENCE in the else arm — correct today. A new verdict added later would silently display 'soft-delete + purge bills' regardless. An explicit map or exhaustive switch would break loudly at the right time.

tests3

medium

New two-active test only asserts verdict, not reason

scripts/__tests__/repair-dup-contracts-core.test.ts

The SKIP_MANUAL test for two active rows checks only `plan.verdict` — the `reason` field is the sole human-readable signal in dry-run output. Add `expect(plan.reason).toMatch(/1-active.*N-terminated/)` so a swapped reason string doesn't go undetected.

low

Active-only group (no terminated row) not tested

scripts/__tests__/repair-dup-contracts-core.test.ts

The guard `terminated.length === 0` fires for a group with one active + zero terminated rows. No test covers this arm independently (existing 'no active row' test hits `actives.length !== 1` first). Low risk for this one-shot script, but easy to add.

low

LIMIT integer validation untested

scripts/repair-dup-contracts.ts

The tightening from `Number.isFinite` to `Number.isInteger` (rejects `--limit 1.5`) is correct but untested. Extracting the guard to `parseLimit(raw)` would make it trivially unit-testable.

improvement2

info

twinBillCount SELECT adds a round-trip that DELETE rowCount could replace

scripts/repair-dup-contracts.ts

The count query exists only to drive a console.log. The subsequent DELETE's `rowCount` field (available on Drizzle execute results) would give the same number without the extra round-trip. Minor for a one-shot script.

info

entity_relationships source_type join fix: correct dedup scope tightening

scripts/repair-dup-contracts.ts

Adding `AND e2.source_type = er.source_type` prevents over-deletion if two entity_relationships rows share (org_id, source_id, target_type, target_id) but differ in source_type. This was a silent incorrect-delete risk for orgs with multiple relationship source types.

History · 8 commits

  1. 80e0f62safeincremental0H · 0M · 1L2026-07-16 19:20
  2. d513853needs attentionincremental0H · 2M · 3L2026-07-16 19:13
  3. 3de8c23safeincremental0H · 1M · 3L2026-07-14 14:23
  4. 8af2fabsafeincremental0H · 0M · 2L2026-07-14 14:16
  5. 8e6343eneeds attentionincremental0H · 1M · 3L2026-07-14 04:45
  6. 57eb1ebneeds attentionincremental0H · 2M · 4L2026-07-14 02:41current
  7. a29f8f4needs attentionincremental0H · 4M · 6L2026-07-14 02:33
  8. 27179efneeds attentionfull5H · 4M · 3L2026-07-14 02:15