← 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
A scripts/execute-sweep.mjsA scripts/vendor-sweep-analyze.mjs
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 →