← back to Wallco Ai

scripts/dedupe-flagged-jsonl.js

93 lines

#!/usr/bin/env node
// dedupe-flagged-jsonl — clean up data/ghost-scan-flagged.jsonl after the
// post-c4ac1f0 scan finishes.
//
// Two sources of pollution can land in this file:
//   (a) rogue old-pipeline scans (no `unanimous` field) — these are SINGLE-call
//       Gemini verdicts before the analyzeGhostLayerStable fix landed
//   (b) duplicate IDs from overlapping scan processes
//
// Keep policy:
//   - keep only entries WITH `unanimous` field (= new-pipeline 3-vote majority)
//   - last-wins per id (the most recent verdict supersedes earlier ones)
//   - preserve the pre-c4ac1f0 backup intact
//
// Default is dry-run. Pass --apply to actually overwrite the file.
//
// SAFETY: refuses to run if a scan-ghost-layer process is still active —
// don't want to truncate a file the scan is appending to.

const fs   = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

const ROOT    = path.join(__dirname, '..');
const FLAGGED = path.join(ROOT, 'data', 'ghost-scan-flagged.jsonl');
const APPLY   = process.argv.includes('--apply');
const FORCE   = process.argv.includes('--force');

function scanRunning() {
  const r = spawnSync('pgrep', ['-f', 'node scripts/scan-ghost-layer.js'], { encoding: 'utf8' });
  return r.stdout.trim().length > 0;
}

if (!fs.existsSync(FLAGGED)) {
  console.error(`MISSING: ${FLAGGED}`);
  process.exit(1);
}

if (APPLY && scanRunning() && !FORCE) {
  console.error('REFUSING: a scan-ghost-layer.js process is still running.');
  console.error('Wait for the scan to finish, or pass --force to truncate anyway.');
  process.exit(2);
}

console.log(`[dedupe] reading ${FLAGGED}`);
const lines = fs.readFileSync(FLAGGED, 'utf8').split('\n').filter(Boolean);
console.log(`[dedupe] ${lines.length} total lines`);

// Two-pass: count format buckets first
let newFmt = 0, oldFmt = 0, badJson = 0;
for (const ln of lines) {
  try {
    const j = JSON.parse(ln);
    if ('unanimous' in j) newFmt++;
    else                  oldFmt++;
  } catch { badJson++; }
}
console.log(`[dedupe] new-format (with unanimous): ${newFmt}`);
console.log(`[dedupe] old-format (no unanimous):   ${oldFmt}`);
console.log(`[dedupe] bad JSON:                    ${badJson}`);

// Build last-wins map of new-format entries
const latestById = new Map();
let lineNo = 0;
for (const ln of lines) {
  lineNo++;
  try {
    const j = JSON.parse(ln);
    if (!('unanimous' in j)) continue;        // drop old-format
    if (!Number.isFinite(j.id)) continue;     // drop malformed
    // Preserve insertion order via Map; reinserting moves to end
    latestById.set(j.id, ln);
  } catch { /* skip */ }
}
console.log(`[dedupe] unique new-format IDs: ${latestById.size}`);
console.log(`[dedupe] would write ${latestById.size} lines (was ${lines.length})`);

if (!APPLY) {
  console.log(`\nDRY RUN — pass --apply to overwrite ${FLAGGED}`);
  process.exit(0);
}

// Backup + atomic write
const ts = new Date().toISOString().replace(/[:.]/g, '-');
const backupPath = FLAGGED + '.pre-dedupe-' + ts;
fs.copyFileSync(FLAGGED, backupPath);
console.log(`[dedupe] backup → ${backupPath}`);

const tmp = FLAGGED + '.tmp-dedupe';
fs.writeFileSync(tmp, [...latestById.values()].join('\n') + '\n');
fs.renameSync(tmp, FLAGGED);
console.log(`[dedupe] wrote ${FLAGGED} (${latestById.size} lines)`);