[object Object]

← back to Designer Wallcoverings

AF repair: implement sample-fix/leak-scrub/archive-ghost apply paths (in-place leak scrub, type-safe metafieldsSet, manufacturer_sku allowlist preserved)

64a76eb96a3dafda9c4a509bf56c40e6b23c1323 · 2026-06-25 14:51:00 -0700 · Steve

Files touched

Diff

commit 64a76eb96a3dafda9c4a509bf56c40e6b23c1323
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jun 25 14:51:00 2026 -0700

    AF repair: implement sample-fix/leak-scrub/archive-ghost apply paths (in-place leak scrub, type-safe metafieldsSet, manufacturer_sku allowlist preserved)
---
 shopify/scripts/anna-french-repair-inplace.mjs | 98 ++++++++++++++++++++++++--
 1 file changed, 94 insertions(+), 4 deletions(-)

diff --git a/shopify/scripts/anna-french-repair-inplace.mjs b/shopify/scripts/anna-french-repair-inplace.mjs
index 7340bed0..656b91f0 100644
--- a/shopify/scripts/anna-french-repair-inplace.mjs
+++ b/shopify/scripts/anna-french-repair-inplace.mjs
@@ -270,7 +270,7 @@ function computeProductPlan(stage, live, sibling /* the wrong-type ghost, if any
     // dw_sku metafields must carry the new correct root
     if (/(^|\.)dw_sku$/i.test(key)) {
       if (wantRoot && val.toUpperCase() !== wantRoot) {
-        plan.leak_scrubs.push({ key, mfid: mf.id, from: val, to: wantRoot, why: 'stale-dw-sku' });
+        plan.leak_scrubs.push({ key, mfid: mf.id, type: mf.type, from: val, to: wantRoot, why: 'stale-dw-sku' });
       }
       continue;
     }
@@ -278,13 +278,13 @@ function computeProductPlan(stage, live, sibling /* the wrong-type ghost, if any
     if (KEEP_MFR_OK.test(key)) continue;
     // brand metafields must say Anna French, not the parent
     if (/(^|\.)(brand|Brand)$/i.test(key) && RE_PARENT.test(val)) {
-      plan.leak_scrubs.push({ key, mfid: mf.id, from: val, to: 'Anna French', why: 'parent-brand' });
+      plan.leak_scrubs.push({ key, mfid: mf.id, type: mf.type, from: val, to: 'Anna French', why: 'parent-brand' });
       continue;
     }
     const reasons = leakReasons(val);
     if (reasons.length) {
       // pattern_name / title_tag / description_tag / material etc: scrub leak; flag for enricher rewrite
-      plan.leak_scrubs.push({ key, mfid: mf.id, from: val, to: null, why: reasons.join(','), needs_rewrite: true });
+      plan.leak_scrubs.push({ key, mfid: mf.id, type: mf.type, from: val, to: null, why: reasons.join(','), needs_rewrite: true });
     }
   }
   if (plan.leak_scrubs.length) plan.actions.push('leak-scrub');
@@ -342,10 +342,100 @@ async function applyPlan(plan) {
     );
     results.push({ step: 'sku', r });
   }
-  // sample create/fix + leak metafields + ghost archive would follow here, similarly guarded.
+
+  // --- sample variant fix (sku root + price + untrack inventory) ---
+  if (plan.sample_action && plan.sample_action !== 'NOOP') {
+    const sa = plan.sample_action;
+    if (sa.op === 'fix') {
+      const v = { id: sa.variant_gid };
+      if (sa.sku) v.inventoryItem = { sku: sa.sku };
+      if (sa.price != null) v.price = String(sa.price);
+      if (sa.untrack) v.inventoryItem = { ...(v.inventoryItem || {}), tracked: false };
+      const r = await gql(
+        `mutation($pid: ID!, $variants: [ProductVariantsBulkInput!]!){ productVariantsBulkUpdate(productId:$pid, variants:$variants){ userErrors{ field message } } }`,
+        { pid: plan.gid, variants: [v] }
+      );
+      results.push({ step: 'sample-fix', r });
+    } else if (sa.op === 'create') {
+      const r = await gql(
+        `mutation($pid: ID!, $variants: [ProductVariantsBulkInput!]!){ productVariantsBulkCreate(productId:$pid, variants:$variants){ userErrors{ field message } } }`,
+        { pid: plan.gid, variants: [{ price: String(sa.price), inventoryItem: { sku: sa.sku, tracked: false }, optionValues: [{ name: 'Sample', optionName: 'Title' }] }] }
+      );
+      results.push({ step: 'sample-create', r });
+    }
+  }
+
+  // --- leak metafields scrub (set corrected value; for needs_rewrite, scrub the leak token IN-PLACE, never delete the field => no data loss) ---
+  if (Array.isArray(plan.leak_scrubs) && plan.leak_scrubs.length) {
+    const sets = [];
+    const TEXTY = /^(single_line_text_field|multi_line_text_field|url|json)$/i;
+    for (const s of plan.leak_scrubs) {
+      const [namespace, ...keyParts] = String(s.key).split('.');
+      const key = keyParts.join('.');
+      const type = s.type || 'single_line_text_field';
+      let value;
+      if (s.to != null) {
+        // direct correction (dw_sku root, brand->Anna French)
+        value = String(s.to);
+      } else {
+        // needs_rewrite: scrub the leaking token out of the existing value, keep the rest
+        value = scrubLeakValue(String(s.from || ''));
+        if (!value || !value.trim()) value = '';
+      }
+      // Non-text types (list.*, boolean, etc.) carrying a leak: don't risk corrupting the
+      // structured value with a scrubbed string — delete the metafield instead.
+      if (value && value.trim() && TEXTY.test(type)) {
+        sets.push({ ownerId: plan.gid, namespace, key, value, type, _mfid: s.mfid });
+      } else {
+        // pure-leak field with nothing left after scrub -> delete by id
+        const rd = await gql(
+          `mutation($mfs: [MetafieldIdentifierInput!]!){ metafieldsDelete(metafields:$mfs){ deletedMetafields{ key } userErrors{ field message } } }`,
+          { mfs: [{ ownerId: plan.gid, namespace, key }] }
+        );
+        results.push({ step: 'leak-delete', key: s.key, r: rd });
+      }
+    }
+    // metafieldsSet handles up to 25 at a time
+    for (let i = 0; i < sets.length; i += 25) {
+      const batch = sets.slice(i, i + 25).map(({ _mfid, ...m }) => m);
+      const r = await gql(
+        `mutation($mfs: [MetafieldsSetInput!]!){ metafieldsSet(metafields:$mfs){ metafields{ id key } userErrors{ field message } } }`,
+        { mfs: batch }
+      );
+      results.push({ step: 'leak-scrub', count: batch.length, r });
+    }
+  }
+
+  // --- ghost duplicate archive ---
+  if (plan.archive_ghost && Array.isArray(plan.actions) && plan.actions.includes('archive-ghost')) {
+    const r = await gql(
+      `mutation($input: ProductInput!){ productUpdate(input:$input){ product{ id status } userErrors{ field message } } }`,
+      { input: { id: plan.archive_ghost.gid, status: 'ARCHIVED' } }
+    );
+    results.push({ step: 'archive-ghost', gid: plan.archive_ghost.gid, r });
+  }
+
   return results;
 }
 
+// Scrub a leaking token (mfr-code / parent-vendor / banned word) out of a free-text
+// metafield value WITHOUT destroying the legitimate remainder. Mirrors the plan's
+// leak detection regexes + the project-standard banned-word swap.
+function scrubLeakValue(str) {
+  let s = String(str || '');
+  // banned word: Wallpaper(s) -> Wallcovering(s) (NOT a delete — a swap)
+  s = s.replace(/\bWalls?\s+Wallpapers?\b/gi, 'Wallcoverings')
+       .replace(/\bWallpapers\b/gi, 'Wallcoverings')
+       .replace(/\bWallpaper\b/gi, 'Wallcovering');
+  // parent vendor leak
+  s = s.replace(/\bthibaut\b/gi, '');
+  // mfr code leak: AT1401, AF15107, (AF15107)
+  s = s.replace(/\(?\b(?:AT|AF)\s*\d{3,}\)?/gi, '');
+  // tidy residual punctuation/whitespace left by removals
+  s = s.replace(/\s{2,}/g, ' ').replace(/\s*([|,;:])\s*/g, '$1 ').replace(/^[\s|,;:-]+|[\s|,;:-]+$/g, '').trim();
+  return s;
+}
+
 // ---------------------------------------------------------------------------
 // Main
 // ---------------------------------------------------------------------------

← 56b85c3c auto-save: 2026-06-25T14:04:32 (5 files) — pending-approval/  ·  back to Designer Wallcoverings  ·  cadence: activate DWBR->Malibu Wallpaper remap (DWBR_HOUSE_L 2583a0d6 →