← back to Mfr Review Viewer Corruption

scripts/mark-superseded.mjs

159 lines

#!/usr/bin/env node
// mark-superseded.mjs — the TK-10922 fallback remediation (DTD dissent, Steve-approved).
//
// FileMaker DELETE is structurally blocked by error [110] (a delete-time relationship
// resolution failure in the WALLPAPER graph). This does NOT delete. It stamps each broken
// placeholder master's existing free-text `Mfr Pattern` field with:
//
//      SUPERSEDED - see master <keepRecordId>
//
// Why this and not "write the real code onto it": writing gz103 onto the placeholder would
// leave TWO records both carrying gz103 and make the duplicate-master ambiguity WORSE. Both
// the first-principles panelist and the contrarian flagged that trap independently.
//
// SAFETY RAILS (mirror execute-sweep.mjs):
//   1. DRY-RUN by default; nothing is written without --apply.
//   2. Snapshot-first: full record snapshotted to data/restore/ BEFORE any write; refuses
//      to write without a snapshot on disk.
//   3. Execute-time re-verify: writes ONLY if the record STILL belongs to this dw_sku
//      (skuMatch), STILL has no sample history, and its mfr is STILL a placeholder.
//   4. Keep-worthiness: the named keep must exist and be keep-worthy, else refuse.
//   5. Idempotent: a record already marked SUPERSEDED is skipped, not double-stamped.
//   6. Every write ledgered with an exact undo (restore the prior field value).
//
// Usage:
//   node scripts/mark-superseded.mjs --plan data/plans/<plan>.json            # DRY-RUN
//   node scripts/mark-superseded.mjs --plan data/plans/<plan>.json --apply    # EXECUTE

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 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_CLIENT = path.join(FM_PROJECT, 'src', 'fm-client.js');
const FM_DB = 'WALLPAPER';
const FM_LAYOUT = '*List Wallpapers - Full View';
const FM_WRITE_LAYOUT = process.env.FM_WRITE_LAYOUT || 'Basic List of Fields';

const arg = (k) => { const i = process.argv.indexOf('--' + k); return i >= 0 ? process.argv[i + 1] : undefined; };
const APPLY = process.argv.includes('--apply');
const PLAN_FILE = arg('plan');
const TICKET = arg('ticket') || 'TK-10922';
const AGENT = arg('agent') || process.env.TK_AGENT || 'claude-run-10922';

for (const line of readFileSync(path.join(FM_PROJECT, '.env'), 'utf8').split('\n')) {
  const m = line.match(/^([A-Z_]+)=(.*)$/); if (m && !process.env[m[1]]) process.env[m[1]] = m[2];
}
let _fm = null;
const fm = async () => (_fm ||= await import('file://' + FM_CLIENT));

const numericTail = (s) => { const m = String(s || '').match(/(\d[\d]*)\s*$/); return m ? m[1] : ''; };
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);
  return m ? m[1].replace(/[.,]$/, '').trim() : t;
}
function classify(fd, dwSku) {
  const clean = String(dwSku || '').trim().replace(/[-_ ]?sample$/i, '');
  const tail = numericTail(clean);
  const mfrPattern = String(fd['Mfr Pattern'] || '').trim();
  const chase = String(fd['Vendor Sample - Where is Memo Send 2nd day'] || '').trim();
  const noteMfr = parseNoteMfr(mfrPattern || (/#/.test(chase) ? chase : '')) || parseNoteMfr(chase);
  const sampleOrdered = !!(String(fd['today for client'] || '').trim() || String(fd['Date WP Sample Sent'] || '').trim());
  const codeDigits = String(noteMfr || mfrPattern || '').replace(/[^0-9]/g, '');
  const mfrPlaceholder = !/[A-Za-z]/.test(noteMfr || '') && !!tail && codeDigits === tail;
  const norm = (s) => String(s || '').toUpperCase().replace(/[^A-Z0-9]/g, '');
  const target = norm(clean);
  const skuMatch = !!target && (norm(String(fd.Series || '') + String(fd['JS Pattern'] || '')) === target || norm(fd['combo sku']) === target);
  return { sampleOrdered, mfrPlaceholder, mfrPattern, noteMfr, skuMatch };
}
async function snapshot(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 f = path.join(RESTORE_DIR, `${rid}-${iso}.json`);
  writeFileSync(f, JSON.stringify({ recordId: rec.recordId || rid, db: FM_DB, layout: FM_LAYOUT,
    modId: rec.modId || null, captured_at: new Date().toISOString(), fieldData: rec.fieldData || {} }, null, 2));
  if (!existsSync(f)) throw new Error(`snapshot write failed for ${rid}`);
  return f;
}
function ledger(action, extra) {
  try {
    execFileSync('node', [LOG_EXEC, '--agent', AGENT, '--ticket', TICKET, '--action', action,
      '--blast', String(extra.blast || 1), '--undo', extra.undo, '--verify', extra.verify], { stdio: 'pipe' });
  } catch (e) { console.error('  ! ledger failed:', e.message); }
}

const MARK = /^SUPERSEDED\b/i;

async function main() {
  if (!PLAN_FILE) { console.error('ERROR: --plan <file.json> required'); process.exit(2); }
  const plan = JSON.parse(readFileSync(PLAN_FILE, 'utf8'));
  console.log(`\n=== mark-superseded ${APPLY ? 'APPLY (LIVE canonical writes)' : 'DRY-RUN (no writes)'} ===`);
  console.log(`plan   : ${PLAN_FILE}`);
  console.log(`writes : ${plan.reduce((n, e) => n + e.deleteRecordIds.length, 0)} field stamps   ticket: ${TICKET}\n`);

  const iso = new Date().toISOString().replace(/[:.]/g, '-');
  let done = 0, skipped = 0, failed = 0;
  const { getRecord, updateRecord } = await fm();

  for (const e of plan) {
    console.log(`--- ${e.dw_sku} ---`);
    const keep = await getRecord(FM_DB, FM_LAYOUT, e.keepRecordId).catch(() => null);
    if (!keep) { console.log(`  GUARD: keep ${e.keepRecordId} not found live — refusing`); skipped += e.deleteRecordIds.length; continue; }
    const kc = classify(keep.fieldData || {}, e.dw_sku);
    if (!(kc.sampleOrdered || (!kc.mfrPlaceholder && /[A-Za-z]/.test(kc.noteMfr || kc.mfrPattern || '')))) {
      console.log(`  GUARD: keep ${e.keepRecordId} is NOT keep-worthy — refusing (inverted plan)`); skipped += e.deleteRecordIds.length; continue;
    }
    console.log(`  keep ${e.keepRecordId} verified keep-worthy (mfr "${kc.mfrPattern}")`);

    for (const rid of e.deleteRecordIds) {
      const rec = await getRecord(FM_DB, FM_LAYOUT, rid).catch(() => null);
      if (!rec) { console.log(`  SKIP ${rid}: no longer exists`); skipped++; continue; }
      const c = classify(rec.fieldData || {}, e.dw_sku);
      const oldVal = String(rec.fieldData['Mfr Pattern'] || '');
      if (MARK.test(oldVal)) { console.log(`  SKIP ${rid}: already marked ("${oldVal}")`); skipped++; continue; }
      if (!c.skuMatch)        { console.log(`  SKIP ${rid}: foreign record — does NOT belong to ${e.dw_sku}`); skipped++; continue; }
      if (c.sampleOrdered)    { console.log(`  SKIP ${rid}: has SAMPLE history — this is a real master`); skipped++; continue; }
      if (!c.mfrPlaceholder)  { console.log(`  SKIP ${rid}: mfr "${oldVal}" is not a placeholder`); skipped++; continue; }

      const newVal = `SUPERSEDED - see master ${e.keepRecordId}`;
      let snapFile;
      try { snapFile = await snapshot(rid, iso); }
      catch (err) { console.log(`  ! SNAPSHOT FAILED ${rid} (${err.message}) — REFUSING to write`); failed++; continue; }

      if (!APPLY) { console.log(`  ${rid}: WOULD set Mfr Pattern "${oldVal}" -> "${newVal}"  (snapshot ${path.relative(ROOT, snapFile)}) (dry-run)`); continue; }
      try {
        const r = await updateRecord(FM_DB, FM_WRITE_LAYOUT, rid, { 'Mfr Pattern': newVal }, { dryRun: false });
        if (!r.committed) { console.log(`  ${rid}: no change (${r.note || 'values matched'})`); skipped++; continue; }
        const back = await getRecord(FM_DB, FM_LAYOUT, rid);
        const now = String(back?.fieldData?.['Mfr Pattern'] || '');
        if (now !== newVal) { console.log(`  ! ${rid}: WRITE UNVERIFIED — reads back as "${now}"`); failed++; continue; }
        console.log(`  ${rid}: "${oldVal}" -> "${now}" [committed + verified]`);
        done++;
        ledger(`TK-10922 mark placeholder master ${rid} (${e.dw_sku}) SUPERSEDED -> keep ${e.keepRecordId}; Mfr Pattern "${oldVal}" -> "${newVal}"`, {
          blast: 1,
          undo: `cd ${ROOT} && node -e "import('file://${FM_CLIENT}').then(m=>m.updateRecord('WALLPAPER','${FM_WRITE_LAYOUT}','${rid}',{'Mfr Pattern':'${oldVal}'},{dryRun:false}))"`,
          verify: `getRecord ${rid}."Mfr Pattern" === "${newVal}"`,
        });
      } catch (err) { console.log(`  ! WRITE FAILED ${rid}: ${err.message}`); failed++; }
    }
    console.log('');
  }
  console.log('=== SUMMARY ===');
  console.log(`  stamped : ${done}${APPLY ? '' : ' (dry-run: 0 written)'}`);
  console.log(`  skipped : ${skipped}`);
  console.log(`  failed  : ${failed}`);
  console.log(APPLY ? '\n[APPLIED — live writes committed, verified + ledgered]' : '\n[DRY-RUN — nothing written. Add --apply.]');
}
main().catch((e) => { console.error('FATAL:', e.message); process.exit(1); });