[object Object]

← back to Mfr Review Viewer Corruption

mfr-master-sweep: gated executor (dry-run default, snapshot-first, re-verify, never-to-zero, blast cap, ledger) + read-only batch analyzer

23d36ffde13b8d557d7b13399080b548b39161e6 · 2026-08-27 09:19:31 -0700 · vp-dw-commerce

Files touched

Diff

commit 23d36ffde13b8d557d7b13399080b548b39161e6
Author: vp-dw-commerce <steve@designerwallcoverings.com>
Date:   Thu Aug 27 09:19:31 2026 -0700

    mfr-master-sweep: gated executor (dry-run default, snapshot-first, re-verify, never-to-zero, blast cap, ledger) + read-only batch analyzer
---
 scripts/execute-sweep.mjs        | 309 +++++++++++++++++++++++++++++++++++++++
 scripts/vendor-sweep-analyze.mjs | 142 ++++++++++++++++++
 2 files changed, 451 insertions(+)

diff --git a/scripts/execute-sweep.mjs b/scripts/execute-sweep.mjs
new file mode 100644
index 0000000..64025a1
--- /dev/null
+++ b/scripts/execute-sweep.mjs
@@ -0,0 +1,309 @@
+#!/usr/bin/env node
+// execute-sweep.mjs — GATED EXECUTOR for the "DW# == Mfr SKU" master-cleanup sweep.
+//
+// ============================ HARD SAFETY RAILS ============================
+//  1. DRY-RUN by default. Nothing is written unless --apply is passed.
+//  2. Every DELETE re-snapshots the FULL record to data/restore/<rid>-<ISO>.json
+//     FIRST and REFUSES to delete without a saved snapshot on disk.
+//  3. RE-VERIFY at execute time (re-read each record via FileMaker): a record is
+//     deleted ONLY if it STILL has no sample history AND its mfr is STILL a
+//     placeholder (== the dw_sku numeric tail). Any record that no longer matches
+//     is SKIPPED + reported (skipped-mismatch).
+//  4. NEVER delete a SKU to zero — at least one KEEP record must survive per SKU.
+//  5. Each KEEP: write the real mfr onto `Mfr Pattern` ONLY if the current value
+//     is a placeholder; the OLD value is captured first (reversible).
+//  6. Blast cap (default 500 deletes) — refuses to exceed without --max.
+//  7. Every executed action is ledgered to
+//     ~/.claude/yolo-queue/executed-reversible/ledger.jsonl via log-exec.mjs.
+//
+// This script performs DESTRUCTIVE canonical FileMaker writes and must only be
+// run on an explicit approved plan + --apply. Default is dry-run.
+//
+// Usage:
+//   node scripts/execute-sweep.mjs --plan data/plans/<vendor>-<ISO>.json          # DRY-RUN
+//   node scripts/execute-sweep.mjs --plan data/plans/<vendor>-<ISO>.json --apply   # EXECUTE
+//   node scripts/execute-sweep.mjs --memo ~/.claude/yolo-queue/pending-approval/<memo>.md --apply
+//   node scripts/execute-sweep.mjs --plan <file> --apply --max 800 --ticket TK-10909
+//
+// Plan JSON shape (array of per-SKU entries):
+//   [ { "dw_sku": "HSW-51526", "keepRecordId": "446079", "realMfr": "gz127",
+//       "deleteRecordIds": ["391954","446080"] }, ... ]
+
+import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
+import { execFileSync } from 'node:child_process';
+import path from 'node:path';
+import os from 'node:os';
+import { fileURLToPath } from 'node:url';
+
+const __dir = path.dirname(fileURLToPath(import.meta.url));
+const ROOT = path.join(__dir, '..');
+const RESTORE_DIR = path.join(ROOT, 'data', 'restore');
+const PENDING_DIR = path.join(os.homedir(), '.claude', 'yolo-queue', 'pending-approval');
+const LOG_EXEC = path.join(os.homedir(), '.claude', 'yolo-queue', 'executed-reversible', 'log-exec.mjs');
+
+const FM_PROJECT = path.join(os.homedir(), 'Projects', 'filemaker-mcp');
+const FM_ENV = path.join(FM_PROJECT, '.env');
+const FM_CLIENT = path.join(FM_PROJECT, 'src', 'fm-client.js');
+const FM_DB = 'WALLPAPER';
+const FM_LAYOUT = '*List Wallpapers - Full View';
+
+// ---- CLI args --------------------------------------------------------------
+function arg(k) { const i = process.argv.indexOf('--' + k); return i >= 0 ? process.argv[i + 1] : undefined; }
+const HAS = (k) => process.argv.includes('--' + k);
+const APPLY = HAS('apply');
+const PLAN_FILE = arg('plan');
+const MEMO_FILE = arg('memo');
+const MAX = arg('max') ? Number(arg('max')) : 500;
+const TICKET = arg('ticket') || process.env.TK_TICKET || 'TK-10909';
+const AGENT = arg('agent') || process.env.TK_AGENT || 'vp-dw-commerce';
+
+// ---- load filemaker-mcp .env (read-only) so FM Cloud creds are present -----
+function loadFmEnv() {
+  if (!existsSync(FM_ENV)) return;
+  for (const line of readFileSync(FM_ENV, 'utf8').split('\n')) {
+    const m = line.match(/^([A-Z_]+)=(.*)$/);
+    if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
+  }
+}
+loadFmEnv();
+
+let _fm = null;
+async function fm() {
+  if (_fm) return _fm;
+  _fm = await import('file://' + FM_CLIENT);
+  return _fm;
+}
+
+// ---- placeholder / sample predicate helpers (mirror candidates.mjs) --------
+function numericTail(s) { const m = String(s || '').match(/(\d[\d]*)\s*$/); return m ? m[1] : ''; }
+
+// Parse the real mfr code out of the Mfr Pattern / note text.
+// "#gz127 - $44.10 Net - 1/14" -> "gz127"; "AMW10042-6; ..." -> "AMW10042-6"; "15712" -> "15712".
+function parseNoteMfr(note) {
+  let t = String(note || '').trim();
+  if (!t) return '';
+  t = t.split(/[\r\n]/)[0].trim();
+  const hash = t.match(/#\s*([^\s;,-][^\s;,]*)/);
+  if (hash) return hash[1].replace(/[.,]$/, '').trim();
+  const m = t.match(/^([^\s;]+?)(?:\s*;|\s+-\s+|\s+\$|\s+net\b|$)/i);
+  if (m) return m[1].replace(/[.,]$/, '').trim();
+  return t;
+}
+
+// Given a live fieldData object + the dw_sku, compute (sampleOrdered, mfrPlaceholder).
+// This IS the execute-time re-verification predicate.
+function classifyRecord(fd, dwSku) {
+  const tail = numericTail(String(dwSku || '').trim().replace(/[-_ ]?sample$/i, ''));
+  const mfrPattern = String(fd['Mfr Pattern'] || '').trim();
+  const chaseMemo = String(fd['Vendor Sample - Where is Memo Send 2nd day'] || '').trim();
+  const mfrNote = mfrPattern || (/#/.test(chaseMemo) ? chaseMemo : '');
+  const noteMfr = parseNoteMfr(mfrNote) || parseNoteMfr(chaseMemo);
+
+  const sampleReq = String(fd['today for client'] || '').trim();
+  const sampleSent = String(fd['Date WP Sample Sent'] || '').trim();
+  const sampleOrdered = !!(sampleReq || sampleSent);
+
+  const codeHasAlpha = /[A-Za-z]/.test(noteMfr || '');
+  const codeDigits = String(noteMfr || mfrPattern || '').replace(/[^0-9]/g, '');
+  const mfrPlaceholder = !codeHasAlpha && !!tail && codeDigits === tail;
+  return { sampleOrdered, mfrPlaceholder, mfrPattern, noteMfr };
+}
+
+// ---- plan loading ----------------------------------------------------------
+// Accepts either a plan JSON file, or an approved memo (parses the KEEP/DELETE
+// blocks that vendor-sweep-analyze / the viewer wrote into the pending-approval memo).
+function loadPlanFromJson(file) {
+  const raw = JSON.parse(readFileSync(file, 'utf8'));
+  // Support both a bare array and { plan: [...] } / { skus: [...] }.
+  const arr = Array.isArray(raw) ? raw : (raw.plan || raw.skus || raw.deletions || []);
+  return arr.map((e) => ({
+    dw_sku: String(e.dw_sku || e.sku || '').trim(),
+    keepRecordId: String(e.keepRecordId || e.keepRid || '').trim(),
+    realMfr: String(e.realMfr || e.keepMfr || e.confirmed_real_mfr || '').trim(),
+    deleteRecordIds: (e.deleteRecordIds || e.deleteRids || []).map(String),
+  })).filter((e) => e.dw_sku);
+}
+
+// Parse an approved memo's "### <dw_sku>" / "KEEP ... master <id> ... confirmed mfr: <x>"
+// / "DELETE masters: a, b" blocks (the format server.js /api/stage writes).
+function loadPlanFromMemo(file) {
+  const text = readFileSync(file, 'utf8');
+  const out = [];
+  const lines = text.split('\n');
+  let cur = null;
+  for (const ln of lines) {
+    const h = ln.match(/^###\s+(.+?)\s*$/);
+    if (h) { if (cur) out.push(cur); cur = { dw_sku: h[1].trim(), keepRecordId: '', realMfr: '', deleteRecordIds: [] }; continue; }
+    if (!cur) continue;
+    const keep = ln.match(/master\s+(\d+)/i); if (keep && !cur.keepRecordId) cur.keepRecordId = keep[1];
+    const mfr = ln.match(/confirmed mfr:\s*([^\s]+)/i); if (mfr && !cur.realMfr) cur.realMfr = mfr[1];
+    const del = ln.match(/DELETE masters?:\s*(.+?)\s*(?:\(|$)/i);
+    if (del) { for (const m of del[1].split(/[,\s]+/)) { const d = m.replace(/[^0-9]/g, ''); if (d) cur.deleteRecordIds.push(d); } }
+  }
+  if (cur) out.push(cur);
+  return out.filter((e) => e.dw_sku && e.deleteRecordIds.length);
+}
+
+// ---- snapshot before delete (REFUSE without a saved snapshot) --------------
+async function snapshotRecord(rid, iso) {
+  const { getRecord } = await fm();
+  const rec = await getRecord(FM_DB, FM_LAYOUT, rid);
+  if (!rec) throw new Error(`record ${rid} not found`);
+  if (!existsSync(RESTORE_DIR)) mkdirSync(RESTORE_DIR, { recursive: true });
+  const file = path.join(RESTORE_DIR, `${rid}-${iso}.json`);
+  const snap = {
+    recordId: rec.recordId || rid, db: FM_DB, layout: FM_LAYOUT,
+    modId: rec.modId || null, captured_at: new Date().toISOString(),
+    fieldData: rec.fieldData || {},
+  };
+  writeFileSync(file, JSON.stringify(snap, null, 2));
+  if (!existsSync(file)) throw new Error(`snapshot write failed for ${rid}`);
+  return { file, fieldData: rec.fieldData || {} };
+}
+
+function ledger(action, extra) {
+  try {
+    execFileSync('node', [LOG_EXEC,
+      '--agent', AGENT, '--ticket', TICKET, '--action', action,
+      '--blast', String(extra.blast || 1),
+      '--undo', extra.undo || 'recreate WALLPAPER record from data/restore snapshot',
+      '--verify', extra.verify || 'candidatesForSku(dw_sku) no longer lists the deleted rid',
+    ], { stdio: 'pipe' });
+  } catch (e) { console.error('  ! ledger failed:', e.message); }
+}
+
+// ---------------------------------------------------------------------------
+async function main() {
+  if (!PLAN_FILE && !MEMO_FILE) {
+    console.error('ERROR: pass --plan <file.json> or --memo <memo.md>');
+    process.exit(2);
+  }
+  const plan = PLAN_FILE ? loadPlanFromJson(PLAN_FILE) : loadPlanFromMemo(MEMO_FILE);
+  if (!plan.length) { console.error('ERROR: plan is empty — nothing to do.'); process.exit(2); }
+
+  const totalDeletes = plan.reduce((n, e) => n + e.deleteRecordIds.length, 0);
+  console.log(`\n=== execute-sweep ${APPLY ? 'APPLY (LIVE DESTRUCTIVE)' : 'DRY-RUN (no writes)'} ===`);
+  console.log(`plan source : ${PLAN_FILE || MEMO_FILE}`);
+  console.log(`SKUs        : ${plan.length}`);
+  console.log(`deletes     : ${totalDeletes}   (blast cap: ${MAX})`);
+  console.log(`mfr-fixes   : ${plan.filter((e) => e.keepRecordId && e.realMfr).length}`);
+  console.log(`ticket      : ${TICKET}   agent: ${AGENT}\n`);
+
+  if (totalDeletes > MAX) {
+    console.error(`>>> BLAST CAP: ${totalDeletes} deletes exceeds --max ${MAX}. Re-run with --max ${totalDeletes} to proceed.`);
+    process.exit(3);
+  }
+  if (totalDeletes > 500) {
+    console.error(`>>> HEADS-UP: ${totalDeletes} deletes exceeds the standing 500-item bound. Proceeding because --max=${MAX} was set explicitly.`);
+  }
+
+  const iso = new Date().toISOString().replace(/[:.]/g, '-');
+  let deleted = 0, mfrFixed = 0, skippedGuard = 0, skippedMismatch = 0, snapFail = 0;
+
+  for (const e of plan) {
+    console.log(`--- ${e.dw_sku} ---`);
+    const { getRecord } = await fm();
+
+    // Re-read every record in the plan (keep + deletes) to re-verify at execute time.
+    const allRids = [...new Set([e.keepRecordId, ...e.deleteRecordIds].filter(Boolean))];
+    const live = {};
+    for (const rid of allRids) {
+      try { live[rid] = await getRecord(FM_DB, FM_LAYOUT, rid); }
+      catch { live[rid] = null; }
+    }
+
+    // never-delete-to-zero: how many records for this SKU will REMAIN?
+    // A record survives if it is NOT in the confirmed delete set (after re-verify).
+    const confirmedDeletes = [];
+    for (const rid of e.deleteRecordIds) {
+      const rec = live[rid];
+      if (!rec) { console.log(`  SKIP delete ${rid}: record no longer exists`); skippedMismatch++; continue; }
+      const c = classifyRecord(rec.fieldData || {}, e.dw_sku);
+      if (c.sampleOrdered) { console.log(`  SKIP delete ${rid}: now has SAMPLE history (sampleOrdered) — KEEP`); skippedMismatch++; continue; }
+      if (!c.mfrPlaceholder) { console.log(`  SKIP delete ${rid}: mfr no longer a placeholder ("${c.mfrPattern}") — KEEP`); skippedMismatch++; continue; }
+      confirmedDeletes.push(rid);
+    }
+
+    // never-to-zero guard: the keep record must exist & survive, OR at least one
+    // non-deleted record for this SKU must remain.
+    const keepAlive = e.keepRecordId && live[e.keepRecordId];
+    if (!confirmedDeletes.length) {
+      console.log(`  (no confirmed deletes after re-verify)`);
+    }
+    if (confirmedDeletes.length && !keepAlive) {
+      // Would we be deleting every re-read record? Guard: refuse if nothing survives.
+      const survivors = allRids.filter((r) => live[r] && !confirmedDeletes.includes(r));
+      if (!survivors.length) {
+        console.log(`  GUARD: would delete SKU to zero (no keep survives) — SKIPPING all deletes for ${e.dw_sku}`);
+        skippedGuard += confirmedDeletes.length;
+        confirmedDeletes.length = 0;
+      }
+    }
+
+    // ---- KEEP: write real mfr onto Mfr Pattern iff current is placeholder ----
+    if (e.keepRecordId && e.realMfr && keepAlive) {
+      const c = classifyRecord(live[e.keepRecordId].fieldData || {}, e.dw_sku);
+      const oldVal = String(live[e.keepRecordId].fieldData['Mfr Pattern'] || '');
+      if (c.mfrPlaceholder) {
+        if (APPLY) {
+          try {
+            const { updateRecord } = await fm();
+            const r = await updateRecord(FM_DB, FM_LAYOUT, e.keepRecordId, { 'Mfr Pattern': e.realMfr }, { dryRun: false });
+            if (r.committed) {
+              console.log(`  KEEP ${e.keepRecordId}: Mfr Pattern "${oldVal}" -> "${e.realMfr}" [committed]`);
+              mfrFixed++;
+              ledger(`mfr-master-sweep set Mfr Pattern on kept master ${e.keepRecordId} (${e.dw_sku}): "${oldVal}" -> "${e.realMfr}"`, {
+                blast: 1,
+                undo: `fm updateRecord WALLPAPER '${FM_LAYOUT}' ${e.keepRecordId} {"Mfr Pattern":"${oldVal}"}`,
+                verify: `getRecord ${e.keepRecordId}.Mfr Pattern == "${e.realMfr}"`,
+              });
+            } else {
+              console.log(`  KEEP ${e.keepRecordId}: no change (${r.note || 'values matched'})`);
+            }
+          } catch (err) { console.log(`  ! KEEP mfr-write failed for ${e.keepRecordId}: ${err.message}`); }
+        } else {
+          console.log(`  KEEP ${e.keepRecordId}: WOULD set Mfr Pattern "${oldVal}" -> "${e.realMfr}" (dry-run)`);
+        }
+      } else {
+        console.log(`  KEEP ${e.keepRecordId}: Mfr Pattern "${oldVal}" is not a placeholder — leaving as-is`);
+      }
+    } else if (e.keepRecordId && e.realMfr && !keepAlive) {
+      console.log(`  ! keep record ${e.keepRecordId} not found live — mfr-write skipped`);
+    }
+
+    // ---- DELETE confirmed records (snapshot-first, refuse w/o snapshot) ------
+    for (const rid of confirmedDeletes) {
+      let snap;
+      try { snap = await snapshotRecord(rid, iso); }
+      catch (err) { console.log(`  ! SNAPSHOT FAILED for ${rid} (${err.message}) — REFUSING to delete`); snapFail++; continue; }
+
+      if (!APPLY) {
+        console.log(`  DELETE ${rid}: WOULD delete (snapshot saved: ${path.relative(ROOT, snap.file)}) (dry-run)`);
+        continue;
+      }
+      // snapshot exists on disk — safe to delete.
+      try {
+        const { deleteRecord } = await fm();
+        await deleteRecord(FM_DB, FM_LAYOUT, rid);
+        console.log(`  DELETE ${rid}: deleted [committed]  (restore: ${path.relative(ROOT, snap.file)})`);
+        deleted++;
+        ledger(`mfr-master-sweep DELETE broken no-sample master ${rid} (${e.dw_sku})`, {
+          blast: 1,
+          undo: `node -e "recreate WALLPAPER record from ${path.relative(ROOT, snap.file)} via fm createRecord --allowDuplicate"`,
+          verify: `candidatesForSku('${e.dw_sku}') no longer lists rid ${rid}`,
+        });
+      } catch (err) { console.log(`  ! DELETE failed for ${rid}: ${err.message}`); }
+    }
+    console.log('');
+  }
+
+  console.log('=== SUMMARY ===');
+  console.log(`  deleted          : ${deleted}${APPLY ? '' : ' (dry-run: 0 actually deleted)'}`);
+  console.log(`  mfr-fixed        : ${mfrFixed}${APPLY ? '' : ' (dry-run: 0 actually written)'}`);
+  console.log(`  skipped-guard    : ${skippedGuard}   (would delete-to-zero)`);
+  console.log(`  skipped-mismatch : ${skippedMismatch}   (predicate no longer holds at execute time)`);
+  console.log(`  snapshot-failed  : ${snapFail}   (delete refused — no reversibility record)`);
+  console.log(APPLY ? '\n[APPLIED — LIVE writes executed + ledgered]' : '\n[DRY-RUN — nothing was written. Add --apply to execute.]');
+}
+
+main().catch((e) => { console.error('FATAL:', e.message); process.exit(1); });
diff --git a/scripts/vendor-sweep-analyze.mjs b/scripts/vendor-sweep-analyze.mjs
new file mode 100644
index 0000000..9117b0c
--- /dev/null
+++ b/scripts/vendor-sweep-analyze.mjs
@@ -0,0 +1,142 @@
+#!/usr/bin/env node
+// vendor-sweep-analyze.mjs — READ-ONLY batch analyzer for the "DW# == Mfr SKU" sweep.
+//
+// For ONE vendor, enumerate the DW#==mfr placeholder SKUs from dw_unified
+// (mfr_sku == numeric tail of dw_sku, numeric-only i.e. no real alpha code), then
+// per SKU reuse lib/candidates.mjs candidatesForSku() to resolve the FileMaker
+// masters + the delete/keep suggestion, and aggregate a plan JSON.
+//
+// PURELY READ-ONLY: psql SELECTs + FileMaker _find reads via candidatesForSku.
+// Writes ONLY its own plan JSON to data/plans/<vendor>-<ISO>.json. Never touches
+// dw_unified, Shopify, or FileMaker records. Paces FM reads gently.
+//
+// Usage:
+//   node scripts/vendor-sweep-analyze.mjs "Glitter Walls"
+//   node scripts/vendor-sweep-analyze.mjs "Glitter Walls" --limit 20   # sample run
+
+import { execFile } from 'node:child_process';
+import { writeFileSync, existsSync, mkdirSync } from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dir = path.dirname(fileURLToPath(import.meta.url));
+const ROOT = path.join(__dir, '..');
+const PLANS_DIR = path.join(ROOT, 'data', 'plans');
+const PSQL = process.env.PSQL_BIN || '/opt/homebrew/opt/postgresql@14/bin/psql';
+
+function arg(k) { const i = process.argv.indexOf('--' + k); return i >= 0 ? process.argv[i + 1] : undefined; }
+const VENDOR = process.argv.slice(2).find((a) => !a.startsWith('--'));
+const LIMIT = arg('limit') ? Number(arg('limit')) : null;
+const PACE_MS = arg('pace') ? Number(arg('pace')) : 250; // gentle FM pacing
+
+if (!VENDOR) { console.error('Usage: vendor-sweep-analyze.mjs "<vendor>" [--limit N]'); process.exit(2); }
+
+function psql(sql) {
+  return new Promise((resolve) => {
+    execFile(PSQL, ['dw_unified', '-tAc', sql], { maxBuffer: 1024 * 1024 * 64 }, (err, stdout) => {
+      if (err) return resolve([]);
+      resolve((stdout || '').trim().split('\n').filter(Boolean));
+    });
+  });
+}
+
+// Enumerate the vendor's DW#==mfr placeholder base SKUs (strip -Sample, dedupe).
+async function enumerateSkus(vendor) {
+  const esc = vendor.replace(/'/g, "''");
+  const sql = `
+    SELECT DISTINCT regexp_replace(dw_sku, '[-_ ]?[Ss]ample$', '') AS base_sku
+    FROM shopify_products
+    WHERE vendor ILIKE '%${esc}%'
+      AND mfr_sku IS NOT NULL AND mfr_sku <> ''
+      AND mfr_sku !~ '[A-Za-z]'
+      AND regexp_replace(coalesce(mfr_sku,''),'[^0-9]','','g') <> ''
+      AND regexp_replace(coalesce(mfr_sku,''),'[^0-9]','','g')
+        = regexp_replace(coalesce(dw_sku,''),'[^0-9]','','g')
+    ORDER BY base_sku`;
+  const rows = await psql(sql);
+  return rows.map((r) => r.trim()).filter(Boolean);
+}
+
+async function main() {
+  console.log(`\n=== vendor-sweep-analyze (READ-ONLY): "${VENDOR}" ===`);
+  let skus = await enumerateSkus(VENDOR);
+  const totalFound = skus.length;
+  if (LIMIT) skus = skus.slice(0, LIMIT);
+  console.log(`DW#==mfr placeholder SKUs found: ${totalFound}${LIMIT ? ` (analyzing first ${skus.length})` : ''}\n`);
+
+  const { candidatesForSku } = await import('file://' + path.join(ROOT, 'lib', 'candidates.mjs'));
+
+  const plan = [];
+  let recordsToDelete = 0, mastersToFix = 0, skippedGuard = 0, noFmMatch = 0;
+  const sampleReport = [];
+
+  for (let i = 0; i < skus.length; i++) {
+    const sku = skus[i];
+    let out;
+    try { out = await candidatesForSku(sku); }
+    catch (e) { console.log(`  ${sku}: candidatesForSku error — ${e.message}`); noFmMatch++; continue; }
+
+    const fmRecs = out.filemaker || [];
+    const sug = out.suggestion || { realMfr: '', keepRid: '', deleteRids: [] };
+
+    if (!fmRecs.length) { noFmMatch++; if (i < skus.length) process.stdout.write(`.`); continue; }
+
+    const deleteRids = (sug.deleteRids || []).map(String);
+    if (!deleteRids.length) {
+      // no delete suggested — either nothing to prune or the guard blocked it
+      // (guard = no keeper survives). Detect the guard case: placeholders exist but canPrune=false.
+      const placeholders = fmRecs.filter((r) => r.mfrPlaceholder && !r.sampleOrdered);
+      const keepers = fmRecs.filter((r) => r.sampleOrdered || (!r.mfrPlaceholder && /[A-Za-z]/.test(r.noteMfr || r.mfrPattern || '')));
+      if (placeholders.length && !keepers.length) { skippedGuard++; }
+      if (i < skus.length) process.stdout.write(`.`);
+      await new Promise((r) => setTimeout(r, PACE_MS));
+      continue;
+    }
+
+    plan.push({
+      dw_sku: sku,
+      keepRecordId: String(sug.keepRid || ''),
+      realMfr: String(sug.realMfr || ''),
+      deleteRecordIds: deleteRids,
+    });
+    recordsToDelete += deleteRids.length;
+    if (sug.keepRid && sug.realMfr) mastersToFix++;
+    if (sampleReport.length < 6) {
+      sampleReport.push({ dw_sku: sku, keep: sug.keepRid, realMfr: sug.realMfr, delete: deleteRids });
+    }
+    process.stdout.write(`+`);
+    await new Promise((r) => setTimeout(r, PACE_MS));
+  }
+  console.log('\n');
+
+  // ---- write the plan JSON ----
+  if (!existsSync(PLANS_DIR)) mkdirSync(PLANS_DIR, { recursive: true });
+  const iso = new Date().toISOString().replace(/[:.]/g, '-');
+  const slug = VENDOR.replace(/[^A-Za-z0-9]+/g, '-').replace(/(^-|-$)/g, '');
+  const planFile = path.join(PLANS_DIR, `${slug}-${iso}.json`);
+  const counts = {
+    vendor: VENDOR,
+    generated_at: new Date().toISOString(),
+    skus_analyzed: skus.length,
+    skus_found_total: totalFound,
+    skus_with_deletes: plan.length,
+    records_to_delete: recordsToDelete,
+    masters_to_fix: mastersToFix,
+    skus_skipped_by_guard: skippedGuard,
+    skus_no_fm_match: noFmMatch,
+  };
+  writeFileSync(planFile, JSON.stringify({ counts, plan }, null, 2));
+
+  console.log('=== COUNTS ===');
+  for (const [k, v] of Object.entries(counts)) console.log(`  ${k.padEnd(22)}: ${v}`);
+  console.log(`\nplan written: ${path.relative(ROOT, planFile)}`);
+  if (sampleReport.length) {
+    console.log('\n=== sample SKUs ===');
+    for (const s of sampleReport) {
+      console.log(`  ${s.dw_sku}  keep=${s.keep || '(none)'}  realMfr=${s.realMfr || '(none)'}  delete=[${s.delete.join(', ')}]`);
+    }
+  }
+  console.log('\n[READ-ONLY — no records written. Execute with scripts/execute-sweep.mjs --plan <file> (dry-run) then --apply]');
+}
+
+main().catch((e) => { console.error('FATAL:', e.message); process.exit(1); });

← 377ff9b viewer: per-chip red 'Remove suggested (N)' button — one-cli  ·  back to Mfr Review Viewer Corruption  ·  mfr-master-sweep executor: Cody-hardened — keep-in-delete gu a0b309d →