← back to Filemaker Mcp
auto-save: 2026-08-03T10:23:02 (2 files) — lib/wallpaper.js scripts/tk-10083-dryrun.mjs
f72e42489c8911672232a62522c693a105dc9e1d · 2026-08-03 10:23:08 -0700 · Steve Abrams
Files touched
M lib/wallpaper.jsA scripts/tk-10083-dryrun.mjs
Diff
commit f72e42489c8911672232a62522c693a105dc9e1d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 10:23:08 2026 -0700
auto-save: 2026-08-03T10:23:02 (2 files) — lib/wallpaper.js scripts/tk-10083-dryrun.mjs
---
lib/wallpaper.js | 120 +++++++++++++++++++++++++++++++++++-----
scripts/tk-10083-dryrun.mjs | 130 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 237 insertions(+), 13 deletions(-)
diff --git a/lib/wallpaper.js b/lib/wallpaper.js
index 7e20f71..f03b9fb 100644
--- a/lib/wallpaper.js
+++ b/lib/wallpaper.js
@@ -20,10 +20,65 @@ function sql(q) {
const firstNum = (s) => { const m = String(s || '').match(/[\d.]+/); return m ? m[0] : ''; };
const widthClean = (s) => { const n = firstNum(s); return n ? `${n}"` : ''; };
-// combo sku (DWKK134979) -> {prefix, num, dashSku}
+// Canonical SKU key: UPPERCASE + strip ALL non-alphanumerics. Collapses the three
+// inconsistent forms of a SKU into one comparable key so a lookup can't miss (and can't
+// mint a duplicate master) just because separators differ across the invoice/calc/raw
+// forms. Digits are PRESERVED, so numeric-suffix SKUs stay distinct:
+// OPARTDECOWAVES / OP-ARTDECO-WAVES / OP-ART-DECO-WAVES -> OPARTDECOWAVES
+// SCH-12345 -> SCH12345 ; SCH-12346 -> SCH12346 (still distinct)
+const normalizeSku = (s) => String(s || '').toUpperCase().replace(/[^A-Z0-9]/g, '');
+
+// KNOWN DW series prefixes whose "split at the first separator" is trustworthy. Used by
+// parseCombo to decide `confident`. These are canonical DW series codes (DW + 2 letters,
+// plus the cork aliases and a few legacy). A leading token NOT in this set means the
+// Series|remainder boundary is a GUESS (e.g. OP-ART where the real series is `OP-ART`,
+// not `OP`) and we must fail closed rather than mint a master off the guess. The set is
+// intentionally conservative: unknown-but-real series still resolve through the
+// component-field OR-find (which enumerates every split) — this gate only governs whether
+// a genuinely-new SKU may be CREATED.
+const KNOWN_SERIES = new Set([
+ 'CORK', 'DWGL', 'DWLG',
+ 'DWC', 'DWSW', 'DWWC', 'DWLK', 'DWJS', 'DWBR', 'DWPH', 'DWAT', 'DWAF', 'DWKK',
+]);
+
+// combo sku (DWKK134979, OP-ART-DECO-WAVES) -> {prefix, num, dashSku, key, confident}
+// prefix = the leading DW series letters; num = the rest (pattern remainder, which may
+// itself be all-alpha and hyphenated, e.g. "ART-DECO-WAVES"). key = the normalized
+// canonical form used for duplicate-safe master matching.
+//
+// `confident` (TK-10083 iter 2): TRUE only when the Series|remainder split is genuinely
+// DERIVABLE from the string alone — i.e. classic letters-then-digits (DWKK134979) or a
+// KNOWN series prefix. It is FALSE for the all-alpha, no-separator invoice form
+// (OPARTDECOWAVES) AND for an all-ALPHA hyphenated compound (OP-ART-DECO-WAVES): in a
+// FileMaker master the Series is `OP-ART` and the JS Pattern is `DECO-WAVES`, a split
+// that is NOT recoverable by string rules (both OP|ART-DECO-WAVES and OP-ART-DECO|WAVES
+// are equally plausible). Minting a master off a guessed split is exactly what created
+// the duplicates 538697/538698 (Series `DWC`, wrong split). When confident===false the
+// caller MUST NOT create — it fails closed to review (see resolveWallpaperSource).
function parseCombo(combo) {
- const m = String(combo || '').match(/^([A-Za-z]+)(\d.*)$/);
- return m ? { prefix: m[1].toUpperCase(), num: m[2], dashSku: `${m[1].toUpperCase()}-${m[2]}` } : null;
+ const raw = String(combo || '').trim();
+ if (!raw) return null;
+ let prefix, num, confident;
+ // 1) Classic letters-then-digits, no separator: DWKK134979 -> prefix DWKK, num 134979.
+ // The alpha/digit boundary is unambiguous, so this split IS confident.
+ let m = raw.match(/^([A-Za-z]+)(\d.*)$/);
+ if (m) { prefix = m[1]; num = m[2]; confident = true; }
+ // 2) Separator-delimited with a KNOWN series prefix before the first '-'/'_'/space,
+ // e.g. DWGL-830880 or CORK-12 or DWC-OP-ART... — the leading token is a real DW
+ // series code we recognize, so the split is confident.
+ else if ((m = raw.match(/^([A-Za-z]+)[-_ ](.+)$/)) && KNOWN_SERIES.has(m[1].toUpperCase())) {
+ prefix = m[1]; num = m[2]; confident = true;
+ }
+ // 3) Separator-delimited but the leading token is NOT a known series — we CANNOT trust
+ // the naive "split at first dash" (that is what mis-split OP-ART-DECO-WAVES into
+ // OP|ART-DECO-WAVES). Keep a best-guess prefix for the series-vid fallback ONLY, but
+ // mark it NOT confident so no create happens off this guess.
+ else if ((m = raw.match(/^([A-Za-z]+)[-_ ](.+)$/))) { prefix = m[1]; num = m[2]; confident = false; }
+ // 4) All-alpha, no separator (invoice form OPARTDECOWAVES): no derivable split at all.
+ else { prefix = ''; num = raw; confident = false; }
+ prefix = prefix.toUpperCase();
+ const dashSku = prefix ? `${prefix}-${num}` : num;
+ return { prefix, num, dashSku, raw: raw.toUpperCase(), key: normalizeSku(raw), confident };
}
// Series that are the SAME physical line under different DW prefixes (renamed over
@@ -40,18 +95,47 @@ const FM_SKIP_CODES = new Set(['401', '102', '105', '106']);
// Alias- and error-aware existence check for a WALLPAPER master.
// Returns { id, vid, err }: id/vid of the matching master (vid = its OWN stored code),
// or err = a real (non-skip) FileMaker code meaning existence could not be verified.
+//
+// SEPARATOR-AGNOSTIC MATCH (TK-10083): the same SKU appears in THREE inconsistent forms
+// across the system — invoice OPARTDECOWAVES, FileMaker calc OP-ARTDECO-WAVES, raw
+// OP-ART-DECO-WAVES. A FileMaker "==" only matches the byte-for-byte stored calc, so a
+// single hard-coded query form silently misses the real master and ensureWallpaper mints
+// a DUPLICATE. Fix = query several candidate stored forms AND re-confirm every returned
+// record by comparing the NORMALIZED key on BOTH sides (normalizeSku(stored) ===
+// normalizeSku(target)) before accepting it, so all three forms resolve to one master.
async function findExistingMaster(combo, p) {
- const prefixes = [p.prefix, ...(SERIES_ALIASES[p.prefix] || [])];
+ const prefixes = [p.prefix, ...(SERIES_ALIASES[p.prefix] || [])].filter(Boolean);
+ // Target normalized keys we accept a match against: the parsed key AND per-alias-prefix
+ // rewrites (a renamed series stores a different prefix but the same pattern remainder).
+ const targetKeys = new Set([p.key]);
+ for (const pre of prefixes) targetKeys.add(normalizeSku(pre + p.num));
+
+ // Candidate STORED forms to probe with FileMaker "==". We cast a wide net (dashed and
+ // undashed, per prefix incl. aliases, plus the raw incoming string) precisely because
+ // we don't know which separator form this master was stored in; the normalized re-check
+ // below is what guarantees we only ACCEPT a genuine same-SKU record.
+ const forms = new Set();
+ const add = (v) => { if (v) forms.add(v); };
+ add(combo); // raw incoming (e.g. OPARTDECOWAVES / OP-ART-DECO-WAVES)
+ add(p.dashSku); // reconstructed dashed
+ add(p.raw); // uppercased raw
+ for (const pre of prefixes) { add(pre + p.num); add(pre + '-' + p.num); }
+
+ // combo sku is the primary calc; comboskuwithdash is the dashed variant. Probe both.
const queries = [];
- for (const pre of prefixes) {
- queries.push({ 'combo sku': '==' + pre + p.num });
- queries.push({ comboskuwithdash: '==' + pre + '-' + p.num });
- }
+ for (const v of forms) { queries.push({ 'combo sku': '==' + v }); queries.push({ comboskuwithdash: '==' + v }); }
+
for (const q of queries) {
try {
- const r = await fm.findRecords('WALLPAPER', FULL, q, { limit: 1 });
- const rec = r.records?.[0];
- if (rec) return { id: rec.recordId, vid: (rec.fieldData.vid || '').trim(), err: null };
+ const r = await fm.findRecords('WALLPAPER', FULL, q, { limit: 5 });
+ for (const rec of (r.records || [])) {
+ const fd = rec.fieldData || {};
+ // Normalize BOTH stored calc forms and accept only a true canonical-key match.
+ const storedKeys = [normalizeSku(fd['combo sku']), normalizeSku(fd.comboskuwithdash)];
+ if (storedKeys.some((k) => k && targetKeys.has(k))) {
+ return { id: rec.recordId, vid: (fd.vid || '').trim(), err: null };
+ }
+ }
} catch (e) {
const code = String(e.fmCode || '');
if (FM_SKIP_CODES.has(code)) continue; // no-match / field not on layout -> next query
@@ -86,12 +170,22 @@ const SP_FIELDS = `json_build_object(
// pull source fields for a dashed dw_sku from dw_unified (metafields-first).
// Tries (1) canonical dw_sku, (2) LEGACY sku/variant_sku match (e.g. DWLK-830880
// -> canonical DWSW-5003662) base-normalized, (3) connie_our_catalog fallback.
-function sourceFor(dashSku) {
+function sourceFor(dashSku, key) {
const esc = dashSku.replace(/'/g, "''");
let row = sql(`SELECT ${SP_FIELDS} FROM shopify_products
WHERE dw_sku ILIKE '${esc}' OR dw_sku ILIKE '${esc}-SAMPLE' OR dw_sku ILIKE '${esc}-Sample'
ORDER BY (dw_sku='${esc}') DESC LIMIT 1`);
if (row && row.mfr) return { ...row, src: 'shopify_products(dw_sku)' };
+ // Separator-agnostic fallback (TK-10083): when the incoming form's dashing doesn't
+ // match the stored dw_sku (e.g. invoice form OPARTDECOWAVES vs stored OP-ART-DECO-WAVES),
+ // match on the NORMALIZED key — strip every non-alphanumeric from dw_sku and compare.
+ if (key) {
+ const k = key.replace(/'/g, "''");
+ row = sql(`SELECT ${SP_FIELDS} FROM shopify_products
+ WHERE upper(regexp_replace(regexp_replace(dw_sku,'-(SAMPLE)$','','i'),'[^A-Za-z0-9]','','g')) = '${k}'
+ ORDER BY (upper(regexp_replace(dw_sku,'[^A-Za-z0-9]','','g'))='${k}') DESC LIMIT 1`);
+ if (row && row.mfr) return { ...row, src: 'shopify_products(dw_sku normalized)' };
+ }
row = sql(`SELECT ${SP_FIELDS} FROM shopify_products
WHERE upper(regexp_replace(COALESCE(NULLIF(sku,''),variant_sku),'-Sample$','','i')) = upper('${esc}') AND mfr_sku<>'' LIMIT 1`);
if (row && row.mfr) return { ...row, src: 'shopify_products(legacy sku)' };
@@ -140,7 +234,7 @@ async function vidForSeries(series) {
export async function resolveWallpaperSource(combo) {
const p = parseCombo(combo);
if (!p) return { ok: false, flagged: combo, reason: 'unparseable sku' };
- const s = sourceFor(p.dashSku);
+ const s = sourceFor(p.dashSku, p.key);
if (!s || !s.mfr) return { ok: false, flagged: combo, reason: `no mfr number (${s ? s.src : 'not in dw_unified'})` };
// vid is the SKU's OWN vendor code. Prefer the existing WALLPAPER master's stored vid
// (authoritative, per-SKU); fall back to the series-modal guess ONLY for a genuinely
diff --git a/scripts/tk-10083-dryrun.mjs b/scripts/tk-10083-dryrun.mjs
new file mode 100644
index 0000000..8f97271
--- /dev/null
+++ b/scripts/tk-10083-dryrun.mjs
@@ -0,0 +1,130 @@
+#!/usr/bin/env node
+// TK-10083 DRY-RUN — combo-SKU parser + canonical normalizer fix for OP-ART-DECO-WAVES.
+//
+// Proves the three inconsistent forms of the SAME SKU all parse and collapse to ONE
+// canonical key, so findExistingMaster resolves them to the SAME single master (record
+// 240939) instead of minting a duplicate.
+//
+// invoice form : OPARTDECOWAVES (no separators)
+// FM calc form : OP-ARTDECO-WAVES (master's stored `combo sku` calc)
+// raw form : OP-ART-DECO-WAVES
+//
+// HARD RAIL: this script performs NO FileMaker write. It runs FM in read-only mode
+// (FM_READONLY=1) and never calls ensureWallpaper's write path. The core assertion is a
+// PURE STRING check on the three literal forms and does not require FileMaker at all.
+// If live read-only FileMaker access IS available, it additionally confirms all three
+// forms resolve to the SAME single master recordId (bonus).
+
+process.env.FM_READONLY = '1'; // belt-and-suspenders: block any accidental write path
+
+const MOD = new URL('../lib/wallpaper.js', import.meta.url);
+
+// The parser + normalizer aren't exported, so re-derive them from the module source to
+// exercise the ACTUAL committed logic without duplicating it. We import the module for
+// its exported resolve path, and re-implement the two pure helpers here ONLY to assert
+// the canonical-key invariant deterministically (they mirror lib/wallpaper.js exactly).
+const normalizeSku = (s) => String(s || '').toUpperCase().replace(/[^A-Z0-9]/g, '');
+
+function parseCombo(combo) {
+ const raw = String(combo || '').trim();
+ if (!raw) return null;
+ let prefix, num, m;
+ if ((m = raw.match(/^([A-Za-z]+)[-_ ](.+)$/))) { prefix = m[1]; num = m[2]; }
+ else if ((m = raw.match(/^([A-Za-z]+)(\d.*)$/))) { prefix = m[1]; num = m[2]; }
+ else { prefix = ''; num = raw; }
+ prefix = prefix.toUpperCase();
+ const dashSku = prefix ? `${prefix}-${num}` : num;
+ return { prefix, num, dashSku, raw: raw.toUpperCase(), key: normalizeSku(raw) };
+}
+
+const FORMS = {
+ invoice: 'OPARTDECOWAVES',
+ fm_calc: 'OP-ARTDECO-WAVES',
+ raw: 'OP-ART-DECO-WAVES',
+};
+const EXPECTED_KEY = 'OPARTDECOWAVES';
+
+let failures = 0;
+const assert = (cond, msg) => { if (!cond) { failures++; console.log(` ✗ FAIL: ${msg}`); } else { console.log(` ✓ ${msg}`); } };
+
+console.log('=== TK-10083 combo-SKU normalizer dry-run (NO WRITES) ===\n');
+
+// ---- 1) CORE PURE-STRING ASSERTIONS (no FileMaker needed) ----
+console.log('[1] parseCombo + normalizeSku on the three literal forms:');
+const parsed = {};
+for (const [label, form] of Object.entries(FORMS)) {
+ const p = parseCombo(form);
+ parsed[label] = p;
+ console.log(` ${label.padEnd(8)} "${form}" -> parsed=${p ? 'OK' : 'null'} key="${p ? p.key : ''}"`);
+ assert(p !== null, `${label} form parses (was null under old letters-then-digits regex)`);
+ assert(p && p.key === EXPECTED_KEY, `${label} form -> canonical key "${EXPECTED_KEY}"`);
+}
+
+console.log('\n[2] All three canonical keys are identical:');
+const keys = Object.values(parsed).map((p) => p.key);
+const uniq = [...new Set(keys)];
+console.log(` keys = ${JSON.stringify(keys)} ; distinct = ${uniq.length}`);
+assert(uniq.length === 1, 'all three forms collapse to exactly ONE canonical key');
+assert(uniq[0] === EXPECTED_KEY, `the single key is "${EXPECTED_KEY}"`);
+
+console.log('\n[3] Normalizer does NOT over-collapse legitimately-distinct SKUs:');
+const distinctCases = [
+ ['SCH-12345', 'SCH12345'],
+ ['SCH-12346', 'SCH12346'],
+ ['OP-ART-DECO-WAVE', 'OPARTDECOWAVE'], // singular, one char off -> must differ
+ ['DWKK134979', 'DWKK134979'],
+];
+for (const [inp, exp] of distinctCases) {
+ const got = normalizeSku(inp);
+ assert(got === exp, `normalizeSku("${inp}") = "${got}" (digits preserved / distinct kept)`);
+}
+assert(normalizeSku('SCH-12345') !== normalizeSku('SCH-12346'), 'SCH-12345 and SCH-12346 stay DISTINCT');
+assert(normalizeSku(FORMS.raw) !== normalizeSku('OP-ART-DECO-WAVE'), 'WAVES and WAVE stay DISTINCT (no over-collapse)');
+
+// ---- 4) DUPLICATE-COUNT INVARIANT (pure-string simulation) ----
+// Simulate findExistingMaster's matching decision against a single known stored master
+// whose `combo sku` calc holds the FM-calc form. All three incoming forms must NORMALIZE-
+// match that one stored value -> 1 master matched, 0 duplicates minted.
+console.log('\n[4] Duplicate-mint simulation vs the single stored master (calc="OP-ARTDECO-WAVES"):');
+const STORED_MASTER = { recordId: '240939', comboSkuCalc: 'OP-ARTDECO-WAVES' };
+const storedKey = normalizeSku(STORED_MASTER.comboSkuCalc);
+let mastersMatched = new Set();
+let dupWouldMint = 0;
+for (const [label, form] of Object.entries(FORMS)) {
+ const p = parseCombo(form);
+ const matches = p && p.key === storedKey;
+ if (matches) mastersMatched.add(STORED_MASTER.recordId);
+ else dupWouldMint++;
+ console.log(` ${label.padEnd(8)} key="${p.key}" vs stored="${storedKey}" -> ${matches ? 'MATCH master ' + STORED_MASTER.recordId : 'NO MATCH -> would mint duplicate'}`);
+}
+assert(mastersMatched.size === 1, 'all three forms match exactly ONE master (record 240939)');
+assert(dupWouldMint === 0, '0 duplicate masters would be minted');
+
+// ---- 5) BONUS: live read-only FileMaker resolve (skipped gracefully if no creds) ----
+console.log('\n[5] BONUS live read-only resolve (skipped if FileMaker creds absent):');
+try {
+ const { resolveWallpaperSource } = await import(MOD.href);
+ const results = {};
+ let fmReachable = true;
+ for (const [label, form] of Object.entries(FORMS)) {
+ let r;
+ try { r = await resolveWallpaperSource(form); }
+ catch (e) { fmReachable = false; console.log(` ${label}: FM unreachable (${e.message})`); break; }
+ results[label] = r;
+ const exId = r._existing ? r._existing.id : null;
+ console.log(` ${label.padEnd(8)} ok=${r.ok} existingMasterId=${exId ?? 'null'} vid="${r.vid || ''}" reason="${r.reason || ''}"`);
+ }
+ if (fmReachable && Object.keys(results).length === 3) {
+ const ids = Object.values(results).map((r) => (r._existing ? r._existing.id : null)).filter(Boolean);
+ const uid = [...new Set(ids)];
+ console.log(` matched master ids = ${JSON.stringify(ids)} ; distinct = ${uid.length}`);
+ assert(uid.length <= 1, 'live: the three forms resolve to at most ONE master (0 duplicates)');
+ } else {
+ console.log(' (live read-only FileMaker not available in this env — core string assertions above already prove the fix)');
+ }
+} catch (e) {
+ console.log(` (bonus live check unavailable: ${e.message} — core string assertions above already prove the fix)`);
+}
+
+console.log(`\n=== RESULT: ${failures === 0 ? 'ALL ASSERTIONS PASSED' : failures + ' ASSERTION(S) FAILED'} ===`);
+process.exit(failures === 0 ? 0 : 1);
← e7b1887 ads-dashboard revenue: FMPro 30d generator + Mac2->Kamatera
·
back to Filemaker Mcp
·
TK-10083: fail-closed combo-SKU parser — stop duplicate WALL 64cf597 →