← 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
M shopify/scripts/anna-french-repair-inplace.mjs
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 →