[object Object]

← back to Mfr Review Viewer Corruption

mfr-master-sweep executor: Cody-hardened — keep-in-delete guard, keep-worthiness guard (reject inverted plans), real runnable restore-record.mjs undo

a0b309dc4bb520bd106fed90533f82acdf6923d2 · 2026-08-27 09:26:49 -0700 · vp-dw-commerce

Files touched

Diff

commit a0b309dc4bb520bd106fed90533f82acdf6923d2
Author: vp-dw-commerce <steve@designerwallcoverings.com>
Date:   Thu Aug 27 09:26:49 2026 -0700

    mfr-master-sweep executor: Cody-hardened — keep-in-delete guard, keep-worthiness guard (reject inverted plans), real runnable restore-record.mjs undo
---
 scripts/execute-sweep.mjs  | 38 ++++++++++++++++++++++++++--
 scripts/restore-record.mjs | 63 ++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 99 insertions(+), 2 deletions(-)

diff --git a/scripts/execute-sweep.mjs b/scripts/execute-sweep.mjs
index 64025a1..459b0e3 100644
--- a/scripts/execute-sweep.mjs
+++ b/scripts/execute-sweep.mjs
@@ -200,10 +200,25 @@ async function main() {
   const iso = new Date().toISOString().replace(/[:.]/g, '-');
   let deleted = 0, mfrFixed = 0, skippedGuard = 0, skippedMismatch = 0, snapFail = 0;
 
-  for (const e of plan) {
+  for (let e of plan) {
     console.log(`--- ${e.dw_sku} ---`);
     const { getRecord } = await fm();
 
+    // ---- SANITY GUARD (Cody hole #1): the keepRecordId must NEVER also be in the
+    // delete set. A malformed/stale plan that lists the same rid as keep AND delete
+    // would otherwise write mfr onto it then delete it. Drop the keep-rid from the
+    // delete list AND refuse the whole SKU (a plan this broken is not trustworthy). ----
+    const keepRid = String(e.keepRecordId || '');
+    const dedupedDeletes = [...new Set(e.deleteRecordIds.map(String))].filter((r) => r && r !== keepRid);
+    if (dedupedDeletes.length !== e.deleteRecordIds.length) {
+      console.log(`  GUARD: plan listed keep=${keepRid} inside its own delete set (or dup rids) — DROPPING keep-rid + dups from deletes.`);
+    }
+    e = { ...e, deleteRecordIds: dedupedDeletes };
+    if (!e.deleteRecordIds.length) {
+      console.log(`  (no deletes remain after keep/dup sanitation)`);
+      // still allow the KEEP mfr-write below to run; fall through
+    }
+
     // 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 = {};
@@ -212,6 +227,25 @@ async function main() {
       catch { live[rid] = null; }
     }
 
+    // ---- KEEP-worthiness guard (Cody hole #3): don't trust the plan blindly.
+    // The named keep record must ACTUALLY be keep-worthy (sample-ordered OR a real
+    // alpha mfr). If it's a placeholder-no-sample record, the plan is inverted — the
+    // "keep" is itself a broken master — so refuse ALL deletes for this SKU. ----
+    if (e.deleteRecordIds.length && live[keepRid]) {
+      const kc = classifyRecord(live[keepRid].fieldData || {}, e.dw_sku);
+      const keepWorthy = kc.sampleOrdered || (!kc.mfrPlaceholder && /[A-Za-z]/.test(kc.noteMfr || kc.mfrPattern || ''));
+      if (!keepWorthy) {
+        console.log(`  GUARD: named keep ${keepRid} is NOT keep-worthy (no sample + placeholder mfr) — REFUSING all deletes AND the mfr-write for ${e.dw_sku} (inverted plan, untrusted).`);
+        skippedGuard += e.deleteRecordIds.length;
+        e = { ...e, deleteRecordIds: [], realMfr: '' }; // also don't trust the plan's realMfr
+      }
+    } else if (e.deleteRecordIds.length && !live[keepRid]) {
+      // keep record missing at execute time — cannot prove a survivor; refuse deletes.
+      console.log(`  GUARD: named keep ${keepRid} not found live — cannot guarantee a survivor, REFUSING all deletes for ${e.dw_sku}.`);
+      skippedGuard += e.deleteRecordIds.length;
+      e = { ...e, deleteRecordIds: [] };
+    }
+
     // 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 = [];
@@ -289,7 +323,7 @@ async function main() {
         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"`,
+          undo: `cd ${ROOT} && node scripts/restore-record.mjs ${path.relative(ROOT, snap.file)} --apply`,
           verify: `candidatesForSku('${e.dw_sku}') no longer lists rid ${rid}`,
         });
       } catch (err) { console.log(`  ! DELETE failed for ${rid}: ${err.message}`); }
diff --git a/scripts/restore-record.mjs b/scripts/restore-record.mjs
new file mode 100644
index 0000000..b2d9bb6
--- /dev/null
+++ b/scripts/restore-record.mjs
@@ -0,0 +1,63 @@
+#!/usr/bin/env node
+// restore-record.mjs — REVERSE a delete performed by execute-sweep.mjs.
+//
+// Re-creates a deleted WALLPAPER master from its saved data/restore/<rid>-<ISO>.json
+// snapshot via the filemaker-mcp createRecord (allowDuplicate:true, since the sweep
+// intentionally removed a duplicate master — restoring re-adds that exact copy).
+//
+// This is the CONCRETE, runnable undo the ledger's undo_cmd points at — so a deleted
+// record is genuinely reversible, not just "we have a JSON somewhere".
+//
+// NOTE: FileMaker assigns a NEW recordId on re-create (the old internal id is gone).
+// Field DATA is fully restored; only the opaque recordId differs. Container/calc/
+// summary fields that FileMaker rejects on write are dropped with a warning.
+//
+// Usage:
+//   node scripts/restore-record.mjs data/restore/391954-2026-08-27T16-17-13-048Z.json          # DRY-RUN
+//   node scripts/restore-record.mjs data/restore/391954-2026-08-27T16-17-13-048Z.json --apply   # RE-CREATE
+
+import { readFileSync, existsSync } from 'node:fs';
+import path from 'node:path';
+import os from 'node:os';
+import { fileURLToPath } from 'node:url';
+
+const __dir = path.dirname(fileURLToPath(import.meta.url));
+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';
+
+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();
+
+const APPLY = process.argv.includes('--apply');
+const snapArg = process.argv.slice(2).find((a) => !a.startsWith('--'));
+if (!snapArg) { console.error('Usage: restore-record.mjs <snapshot.json> [--apply]'); process.exit(2); }
+
+// Fields FileMaker will reject on a write (calc / summary / container / global metadata).
+// We keep it conservative: strip nothing by name here, but let createRecord surface a
+// field error — if it does, re-run with an explicit --skip-fields list. Most Full-View
+// text/number fields write back fine.
+async function main() {
+  const snapPath = path.isAbsolute(snapArg) ? snapArg : path.join(__dir, '..', snapArg);
+  if (!existsSync(snapPath)) { console.error(`snapshot not found: ${snapPath}`); process.exit(2); }
+  const snap = JSON.parse(readFileSync(snapPath, 'utf8'));
+  const fieldData = snap.fieldData || {};
+  const rid = snap.recordId;
+  console.log(`restore ${rid} (${Object.keys(fieldData).length} fields) from ${path.basename(snapPath)} — ${APPLY ? 'APPLY' : 'DRY-RUN'}`);
+
+  const { createRecord } = await import('file://' + FM_CLIENT);
+  const r = await createRecord(FM_DB, FM_LAYOUT, fieldData, { dryRun: !APPLY, allowDuplicate: true });
+  if (r.committed) console.log(`  RECREATED as new recordId ${r.recordId} (old id ${rid} is retired by FileMaker).`);
+  else console.log(`  ${r.note || JSON.stringify(r)}`);
+  console.log(APPLY ? '\n[RESTORED]' : '\n[DRY-RUN — add --apply to re-create the record]');
+}
+
+main().catch((e) => { console.error('FATAL:', e.message); process.exit(1); });

← 23d36ff mfr-master-sweep: gated executor (dry-run default, snapshot-  ·  back to Mfr Review Viewer Corruption  ·  auto-data-snapshot: 2026-08-27T09:33:34 (3 data files) — dat df9f241 →