fix/dup-repair
needs attentionviewing older commit57eb1eb · incrementalPR #306reviewed 2026-07-14 02:41 UTC0H · 2M · 4L · 5I- 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
- 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+9−0scripts/lib/repair-dup-contracts-core.ts+3−2scripts/repair-dup-contracts.ts+20−6
- 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).
Findings · 10
correctness2
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.
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
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
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.
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
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.
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.
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
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.
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
- 80e0f62safeincremental0H · 0M · 1L2026-07-16 19:20
- d513853needs attentionincremental0H · 2M · 3L2026-07-16 19:13
- 3de8c23safeincremental0H · 1M · 3L2026-07-14 14:23
- 8af2fabsafeincremental0H · 0M · 2L2026-07-14 14:16
- 8e6343eneeds attentionincremental0H · 1M · 3L2026-07-14 04:45
- 57eb1ebneeds attentionincremental0H · 2M · 4L2026-07-14 02:41current
- a29f8f4needs attentionincremental0H · 4M · 6L2026-07-14 02:33
- 27179efneeds attentionfull5H · 4M · 3L2026-07-14 02:15