← back to Dw Marketing Reels

scripts/jewelry-sweep-pending.mjs

71 lines

#!/usr/bin/env node
/**
 * jewelry-sweep-pending.mjs — sweep the STAGED reel queue for jewelry-counter
 * content so nothing already-queued slips through when posting is re-armed.
 * TK-12012 / Steve 2026-09-22 (jewelry ONLY).
 *
 * For every reel in data/reels.json that is NOT already posted and whose mp4
 * exists locally, run the same frame-classify jewelry gate publish-social.mjs
 * uses. Any reel with a jewelry-counter frame is QUARANTINED in place —
 * publish.instagram/tiktok stamped {status:'held-jewelry-review'} — so it will
 * never auto-post. Room-setting reels are untouched (scope = jewelry only).
 *
 * Reversible: writes data/reels.json.jewelry-sweep-bak-<ts> before any change,
 * and only writes the manifest if something was actually quarantined.
 *   node jewelry-sweep-pending.mjs          # sweep + quarantine
 *   node jewelry-sweep-pending.mjs --dry     # report only, no writes
 */
import fs from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { gateReel } from './jewelry-gate.mjs';

const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const MAN = join(ROOT, 'data', 'reels.json');
const DRY = process.argv.includes('--dry');
const TERMINAL = new Set(['posted', 'already-posted', 'posted-unverified']);
const now = () => new Date().toISOString();

const reels = JSON.parse(fs.readFileSync(MAN, 'utf8'));
let scanned = 0, quarantined = 0, clean = 0, undecided = 0, skipped = 0;
const changes = [];

for (const reel of reels) {
  const st = reel.publish?.instagram?.status;
  if (TERMINAL.has(st)) { skipped++; continue; }
  if (st === 'held-jewelry-review') { quarantined++; continue; } // already held
  const local = join(ROOT, 'reels', reel.file);
  if (!fs.existsSync(local)) { skipped++; continue; }
  scanned++;
  const gate = await gateReel(local);
  if (gate.reason === 'jewelry') {
    quarantined++;
    const held = { status: 'held-jewelry-review', reason: 'jewelry',
      note: `swept: jewelry-counter frame (${gate.jewelryFrames}/${gate.sampled} sampled) — quarantined`, at: now() };
    reel.publish = reel.publish || {};
    reel.publish.instagram = { ...held };
    reel.publish.tiktok = { ...held };
    changes.push({ file: reel.file, jewelryFrames: gate.jewelryFrames, sampled: gate.sampled });
    console.log(`  QUARANTINE  ${reel.file}  (${gate.jewelryFrames}/${gate.sampled} jewelry frames)`);
  } else if (gate.reason === 'clean') {
    clean++;
    console.log(`  clean       ${reel.file}`);
  } else {
    undecided++;
    console.log(`  undecided   ${reel.file}  (${gate.note || gate.reason}) — will be held at publish (fail-closed)`);
  }
}

console.log(`\nswept ${scanned} staged reel(s): ${changes.length} newly quarantined, ${clean} clean, ${undecided} undecided(held-at-publish); ${quarantined} total held; ${skipped} skipped(posted/missing).`);

if (changes.length && !DRY) {
  const bak = `${MAN}.jewelry-sweep-bak-${Date.now()}`;
  fs.copyFileSync(MAN, bak);
  fs.writeFileSync(MAN, JSON.stringify(reels, null, 2));
  console.log(`wrote quarantine marks to reels.json (backup: ${bak})`);
} else if (changes.length) {
  console.log('--dry: no writes.');
} else {
  console.log('no jewelry reels found in the staged queue — nothing to quarantine.');
}