← back to Gmc Titlefix
TK-11450: hardened v2 CA restore (variant-pinned en-ca links, PJ/archived/drift exclusions, samples held, honest NOT_MEASURED) + parity preflight; dry-run 41/27/8/0
82f2f09c6740c0632645cf44e367d27ead0751d2 · 2026-09-25 11:42:46 -0700 · Steve
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015SYWczdX19LZ3mPb7fz2iF
Files touched
A tk11450-ca-restore-preflight-parity.mjsA tk11450-ca-restore-v2.mjs
Diff
commit 82f2f09c6740c0632645cf44e367d27ead0751d2
Author: Steve <steve@designerwallcoverings.com>
Date: Fri Sep 25 11:42:46 2026 -0700
TK-11450: hardened v2 CA restore (variant-pinned en-ca links, PJ/archived/drift exclusions, samples held, honest NOT_MEASURED) + parity preflight; dry-run 41/27/8/0
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015SYWczdX19LZ3mPb7fz2iF
---
tk11450-ca-restore-preflight-parity.mjs | 68 ++++++++++++
tk11450-ca-restore-v2.mjs | 183 ++++++++++++++++++++++++++++++++
2 files changed, 251 insertions(+)
diff --git a/tk11450-ca-restore-preflight-parity.mjs b/tk11450-ca-restore-preflight-parity.mjs
new file mode 100644
index 0000000..14ca2ba
--- /dev/null
+++ b/tk11450-ca-restore-preflight-parity.mjs
@@ -0,0 +1,68 @@
+#!/usr/bin/env node
+/**
+ * TK-11450 PRE-FLIGHT (READ-ONLY, $0): before any restore of the 76 stripped CA offers,
+ * measure what each restored offer WOULD advertise vs what its LANDING PAGE would charge.
+ *
+ * Why: restoring full attrs flips these 76 from DARK (disapproved, serving nothing) to LIVE.
+ * The restore script writes link = /products/<handle> with NO ?variant= param, so the page
+ * renders variant POSITION 1 (gmc-price-parity-canary doctrine, TK-11253). If the offerId
+ * variant is NOT position 1, the advertised amount can badly understate the page price —
+ * the $4.25-sample-leak class. That must be measured BEFORE turning 76 offers back on.
+ *
+ * Uses SHOPIFY_ADMIN_TOKEN (…6755) because SHOPIFY_FULL_ACCESS_TOKEN (…2ea5) is DEAD (401).
+ * NOT-MEASURED is reported as NOT-MEASURED, never as clean.
+ */
+import fs from 'fs';
+const env = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env','utf8');
+const tok = (env.match(/^SHOPIFY_ADMIN_TOKEN=(.+)$/m)||[])[1].trim();
+const written = fs.readFileSync('data/tk11450-ca-relabel-2026-09-11T16-20-31-973Z.jsonl','utf8')
+ .trim().split('\n').map(JSON.parse).filter(r=>r.ok);
+
+const Q = `query($id:ID!){
+ productVariant(id:$id){ id title price sku availableForSale
+ contextualPricing(context:{country:CA}){price{amount currencyCode}}
+ product{ id title vendor handle status onlineStoreUrl
+ variants(first:50){nodes{ id title price sku
+ contextualPricing(context:{country:CA}){price{amount currencyCode}} }} } } }`;
+
+const rows=[]; let nm=0;
+for (const w of written){
+ let j=null, status=0;
+ try{
+ const r = await fetch('https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json',
+ {method:'POST',headers:{'X-Shopify-Access-Token':tok,'Content-Type':'application/json'},
+ body:JSON.stringify({query:Q,variables:{id:`gid://shopify/ProductVariant/${w.offerId}`}}),signal:AbortSignal.timeout(45000)});
+ status=r.status; j=await r.json();
+ }catch(e){ j=null; }
+ const v=j?.data?.productVariant;
+ if(!v){ nm++; rows.push({offerId:w.offerId, advertise:w.amount, verdict:'NOT_MEASURED', why:`shopify read failed (HTTP ${status})`}); continue; }
+ const nodes=v.product?.variants?.nodes||[];
+ const pos1=nodes[0]||null;
+ const caOf=x=>x?.contextualPricing?.price?.amount!=null?parseFloat(x.contextualPricing.price.amount):null;
+ const pageCA=caOf(pos1), ownCA=caOf(v);
+ const adv=w.amount;
+ let verdict='OK', why='';
+ if(pageCA==null){ verdict='NOT_MEASURED'; why='no CA contextual price on position-1 variant'; nm++; }
+ else{
+ const delta=(adv-pageCA)/pageCA;
+ if(Math.abs(delta)<=0.02) verdict='OK';
+ else if(adv<pageCA) { verdict='UNDER_ADVERTISED'; }
+ else { verdict='OVER_ADVERTISED'; }
+ why=`advertise ${adv} vs page(pos1) ${pageCA} CAD (${(delta*100).toFixed(1)}%)`;
+ }
+ rows.push({offerId:w.offerId, product:v.product?.title, vendor:v.product?.vendor, handle:v.product?.handle,
+ productStatus:v.product?.status, offerVariantTitle:v.title, offerVariantSku:v.sku, offerVariantUSD:v.price,
+ offerVariantCA:ownCA, pos1VariantTitle:pos1?.title, pos1VariantSku:pos1?.sku, pos1CA:pageCA,
+ isPos1: pos1?.id===v.id, advertise:adv, verdict, why});
+}
+const by=k=>rows.filter(r=>r.verdict===k).length;
+const stamp=new Date().toISOString().replace(/[:.]/g,'-').slice(0,19);
+fs.writeFileSync(`data/tk11450-ca-restore-preflight-parity-${stamp}.json`,JSON.stringify({ts:new Date().toISOString(),population:written.length,measured:rows.length-nm,notMeasured:nm,summary:{OK:by('OK'),UNDER_ADVERTISED:by('UNDER_ADVERTISED'),OVER_ADVERTISED:by('OVER_ADVERTISED'),NOT_MEASURED:by('NOT_MEASURED')},rows},null,1));
+console.log(`population=${written.length} measured=${rows.length-nm} NOT_MEASURED=${nm}`);
+console.log(`OK=${by('OK')} UNDER_ADVERTISED=${by('UNDER_ADVERTISED')} OVER_ADVERTISED=${by('OVER_ADVERTISED')}`);
+console.log(`not-pos1 offers: ${rows.filter(r=>r.isPos1===false).length}`);
+for(const r of rows.filter(r=>r.verdict==='UNDER_ADVERTISED').sort((a,b)=>(a.advertise/a.pos1CA)-(b.advertise/b.pos1CA)).slice(0,15))
+ console.log(' UNDER', r.offerId, (r.vendor||'').slice(0,22), '|', r.why, '| offerVar:', (r.offerVariantTitle||'').slice(0,18), '| pos1:', (r.pos1VariantTitle||'').slice(0,18));
+for(const r of rows.filter(r=>r.verdict==='OVER_ADVERTISED').slice(0,10))
+ console.log(' OVER ', r.offerId, (r.vendor||'').slice(0,22), '|', r.why);
+console.log(`artifact: data/tk11450-ca-restore-preflight-parity-${stamp}.json`);
diff --git a/tk11450-ca-restore-v2.mjs b/tk11450-ca-restore-v2.mjs
new file mode 100644
index 0000000..48eb9e8
--- /dev/null
+++ b/tk11450-ca-restore-v2.mjs
@@ -0,0 +1,183 @@
+#!/usr/bin/env node
+/**
+ * TK-11450 v2 — RESTORE stripped CA inputs, HARDENED (DRY-RUN DEFAULT; --apply is Steve-gated).
+ *
+ * Supersedes tk11450-ca-restore-full-inputs.mjs (2026-09-18). That script could no longer run
+ * (SHOPIFY_FULL_ACCESS_TOKEN …2ea5 is DEAD, HTTP 401) and, worse, reported the auth failure as
+ * "no live Shopify CA price" — an unmeasured input wearing a measured cause (CLAUDE.md TK-11431
+ * amendment 1). Re-verification on 2026-09-25 found four further defects it would have shipped:
+ *
+ * 1. LINK NOT VARIANT-PINNED. v1 wrote link=/products/<handle> with no ?variant=, so the page
+ * renders variant POSITION 1. Measured: 8 of 76 would have become price-parity defects, two
+ * severe (Phillipe Romano samples advertising 7 CAD against 92 / 73 CAD roll pages, −92%).
+ * Every surviving healthy CA offer carries ?variant=<offerId>; v2 matches that convention,
+ * which makes the advertised amount correct by construction.
+ * 2. SHOWROOM-ONLY LEAK. 4 of the 76 are Phillip Jeffries. PJ is showroom-only and must be
+ * ABSENT from Google; sibling memo TK-11844 asks Steve to DELETE these exact 4 CA offers.
+ * v1 would have restored them to serving. Hard-excluded here regardless of flags.
+ * 3. ARCHIVED PRODUCT. 1 of the 76 is an ARCHIVED Shopify product (dw-archived-live-offer class).
+ * 4. STALE AMOUNTS. v1's 2% guard compared the 09-11 log amount against the OFFER'S OWN variant
+ * but only after a landing-page-blind build; 6 offers have drifted >2% since. Skipped, named.
+ *
+ * COHORTS (recomputed LIVE every run, never read from a frozen file):
+ * A non-sample · ACTIVE · non-PJ · within 2% → restore (the ask)
+ * B sample-variant offers → HELD by default ($4.25-sample-leak class, TK-11731/TK-11392
+ * feed-exclude policy owns them). --include-samples opts them in explicitly.
+ * X excluded, each with a stated reason.
+ *
+ * SAFETY: fails closed everywhere. Refuses to write if the CA input is no longer price-only.
+ * Re-verifies every offer immediately before its own write. NOT_MEASURED is never treated as clean.
+ */
+import fs from 'fs';
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+const { token, MERCHANT } = require('./_auth.js');
+const { getProduct } = require('./_mc-read-v1.js');
+
+const args = process.argv.slice(2);
+const APPLY = args.includes('--apply') && args.includes('--yes-i-am-steve');
+const INCLUDE_SAMPLES = args.includes('--include-samples');
+const CA_DS = 'accounts/146735262/dataSources/10708797233';
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const TOL = 0.02;
+const now0 = () => new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
+
+// ---- Shopify auth: probe, don't assume. An auth failure is NOT_MEASURED, loudly. ----
+const env = fs.readFileSync(process.env.HOME + '/Projects/secrets-manager/.env', 'utf8');
+const pick = n => ((env.match(new RegExp('^' + n + '=(.+)$', 'm')) || [])[1] || '').trim();
+async function liveShopifyToken() {
+ for (const name of ['SHOPIFY_FULL_ACCESS_TOKEN', 'SHOPIFY_ADMIN_TOKEN']) {
+ const t = pick(name); if (!t) continue;
+ const r = await fetch(`https://${SHOP}/admin/api/2024-10/graphql.json`, {
+ method: 'POST', headers: { 'X-Shopify-Access-Token': t, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query: '{shop{name}}' }), signal: AbortSignal.timeout(30000) });
+ const j = await r.json().catch(() => null);
+ if (r.ok && j?.data?.shop?.name) { console.log(`shopify auth: ${name} (…${t.slice(-4)}) LIVE`); return t; }
+ console.log(`shopify auth: ${name} (…${t.slice(-4)}) DEAD — HTTP ${r.status}`);
+ }
+ console.error('REFUSING: NOT_MEASURED — no Shopify admin token authenticates. Not a finding about the feed.');
+ process.exit(2);
+}
+const shopTok = await liveShopifyToken();
+
+const Q = `query($id:ID!){productVariant(id:$id){ id title sku availableForSale
+ contextualPricing(context:{country:CA}){price{amount currencyCode}}
+ product{ title descriptionHtml vendor handle status featuredImage{url}
+ variants(first:1){nodes{id}} } }}`;
+async function shopCA(variantId) {
+ const r = await fetch(`https://${SHOP}/admin/api/2024-10/graphql.json`, {
+ method: 'POST', headers: { 'X-Shopify-Access-Token': shopTok, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query: Q, variables: { id: `gid://shopify/ProductVariant/${variantId}` } }),
+ signal: AbortSignal.timeout(45000) });
+ if (!r.ok) return { notMeasured: `shopify HTTP ${r.status}` };
+ const j = await r.json().catch(() => null);
+ if (!j) return { notMeasured: 'shopify unparseable' };
+ if (j.errors) return { notMeasured: 'shopify errors: ' + JSON.stringify(j.errors).slice(0, 120) };
+ if (!j.data?.productVariant) return { gone: true };
+ return { v: j.data.productVariant };
+}
+
+const written = fs.readFileSync('data/tk11450-ca-relabel-2026-09-11T16-20-31-973Z.jsonl', 'utf8')
+ .trim().split('\n').map(JSON.parse).filter(r => r.ok);
+const usSib = JSON.parse(fs.readFileSync('data/tk11450-ca-strip76-us-siblings.json', 'utf8'));
+const usAttrsById = new Map(usSib.restore.filter(r => r.usAttrs).map(r => [r.offerId, r.usAttrs]));
+
+const DW = 'https://www.designerwallcoverings.com';
+// Match the surviving healthy CA offers exactly: /en-ca/ locale prefix + ?variant=<offerId>.
+// A US sibling link may carry /en-us/, another locale, or no locale at all — normalise all three,
+// so a missing prefix can never silently ship a non-CA landing page.
+const caLink = (handle, offerId, usLink) => {
+ let path = usLink ? String(usLink).split('?')[0].replace(/^https?:\/\/[^/]+/, '') : `/products/${handle}`;
+ path = path.replace(/^\/en-[a-z]{2}\//, '/');
+ if (!path.startsWith('/')) path = '/' + path;
+ return `${DW}/en-ca${path}?variant=${offerId}`;
+};
+
+const build = [], held = [], excluded = [], notMeasured = [];
+console.log(`${APPLY ? 'APPLY' : 'DRY-RUN'} — TK-11450 v2 CA input restore over ${written.length} stripped offers`);
+console.log(`samples: ${INCLUDE_SAMPLES ? 'INCLUDED (--include-samples)' : 'HELD (default)'}\n`);
+
+for (const w of written) {
+ const cur = await getProduct('online:en:CA:' + w.offerId, { country: 'CA' }).catch(() => null);
+ const attrs = cur?._v1?.productAttributes;
+ if (!cur || !attrs) { notMeasured.push({ offerId: w.offerId, why: 'GMC CA read failed' }); continue; }
+ const keys = Object.keys(attrs).sort();
+ if (!(keys.length === 1 && keys[0] === 'price')) {
+ excluded.push({ offerId: w.offerId, why: `CA input no longer price-only (${keys.length} attrs) — refuse to overwrite` }); continue; }
+
+ const s = await shopCA(w.offerId);
+ if (s.notMeasured) { notMeasured.push({ offerId: w.offerId, why: s.notMeasured }); continue; }
+ if (s.gone) { excluded.push({ offerId: w.offerId, why: 'Shopify variant no longer exists' }); continue; }
+ const v = s.v, p = v.product;
+ if (/phillip\s*jeffries/i.test(p?.vendor || '')) {
+ excluded.push({ offerId: w.offerId, vendor: p.vendor, why: 'showroom-only line — must be ABSENT from Google (TK-11844 asks to DELETE this exact offer)' }); continue; }
+ if (p?.status !== 'ACTIVE') {
+ excluded.push({ offerId: w.offerId, vendor: p?.vendor, why: `Shopify product ${p?.status} — archived/draft product must not carry a live offer` }); continue; }
+ const caAmt = v.contextualPricing?.price?.amount != null ? parseFloat(v.contextualPricing.price.amount) : null;
+ if (caAmt == null) { notMeasured.push({ offerId: w.offerId, why: 'no CA contextual price on the offer variant' }); continue; }
+ if (Math.abs(w.amount - caAmt) > Math.max(1, caAmt * TOL)) {
+ excluded.push({ offerId: w.offerId, vendor: p.vendor, why: `amount drift: log ${w.amount} vs live CA ${caAmt} CAD — re-price before restoring` }); continue; }
+
+ const isSample = /sample/i.test(v.title || '');
+ const us = usAttrsById.get(w.offerId);
+ let a;
+ if (us && us.title) {
+ a = { title: us.title, description: us.description || undefined, imageLink: us.imageLink || undefined,
+ link: caLink(p.handle, w.offerId, us.link), brand: us.brand || p.vendor || undefined,
+ condition: us.condition || 'new', gtin: us.gtin || undefined, mpn: us.mpn || undefined,
+ availability: v.availableForSale === false ? 'out_of_stock' : 'in_stock',
+ price: { amountMicros: String(Math.round(w.amount * 1e6)), currencyCode: 'CAD' },
+ shipping: [{ country: 'CA' }], shippingWeight: us.shippingWeight || undefined };
+ } else if (p?.title) {
+ a = { title: p.title,
+ description: (p.descriptionHtml || '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 5000) || undefined,
+ imageLink: p.featuredImage?.url || undefined, link: caLink(p.handle, w.offerId, null),
+ brand: p.vendor || undefined, condition: 'new',
+ availability: v.availableForSale === false ? 'out_of_stock' : 'in_stock',
+ price: { amountMicros: String(Math.round(w.amount * 1e6)), currencyCode: 'CAD' },
+ shipping: [{ country: 'CA' }] };
+ } else { notMeasured.push({ offerId: w.offerId, why: 'no restore source' }); continue; }
+ a = Object.fromEntries(Object.entries(a).filter(([, x]) => x !== undefined));
+ if (!a.title || !a.link || !a.imageLink || !a.availability || !a.price) {
+ excluded.push({ offerId: w.offerId, why: 'payload incomplete after build — refuse (this is the v1 bug class)' }); continue; }
+
+ const rec = { offerId: w.offerId, vendor: p.vendor, product: p.title, variant: v.title, isSample,
+ source: us && us.title ? 'us-sibling' : 'shopify', attrs: a };
+ if (isSample && !INCLUDE_SAMPLES) held.push({ ...rec, why: 'sample-variant offer — $4.25-leak class, held for feed-exclude policy' });
+ else build.push(rec);
+}
+
+const stamp = now0();
+console.log(`COHORT A restore: ${build.length}`);
+console.log(`COHORT B held (samples): ${held.length}`);
+console.log(`EXCLUDED: ${excluded.length}`);
+for (const e of excluded) console.log(' X', e.offerId, (e.vendor || '').slice(0, 20), '-', e.why);
+console.log(`NOT_MEASURED: ${notMeasured.length}`);
+for (const n of notMeasured) console.log(' ?', n.offerId, '-', n.why);
+console.log(`accounted: ${build.length + held.length + excluded.length + notMeasured.length} / ${written.length}`);
+const b0 = build[0];
+if (b0) console.log('sample payload:', JSON.stringify({ offerId: b0.offerId, link: b0.attrs.link, title: b0.attrs.title.slice(0, 50), price: b0.attrs.price, avail: b0.attrs.availability }, null, 1));
+
+fs.writeFileSync(`data/tk11450-ca-restore-v2-map-${stamp}.json`, JSON.stringify(
+ { ts: new Date().toISOString(), dryRun: !APPLY, includeSamples: INCLUDE_SAMPLES,
+ counts: { population: written.length, restore: build.length, held: held.length, excluded: excluded.length, notMeasured: notMeasured.length },
+ build, held, excluded, notMeasured }, null, 1));
+console.log(`map artifact: data/tk11450-ca-restore-v2-map-${stamp}.json`);
+
+if (!APPLY) { console.log('\nDRY-RUN only — ZERO writes. Fire path: --apply --yes-i-am-steve (Steve-gated; see pending-approval memo).'); process.exit(0); }
+if (notMeasured.length) { console.error(`\nREFUSING APPLY: ${notMeasured.length} offers NOT_MEASURED. Re-run when measurable.`); process.exit(3); }
+
+const tok = await token();
+let ok = 0, fail = 0; const runlog = [];
+for (const b of build) {
+ const url = `https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(CA_DS)}`;
+ const body = { offerId: b.offerId, contentLanguage: 'en', feedLabel: 'CA', productAttributes: b.attrs };
+ const r = await fetch(url, { method: 'POST', headers: { Authorization: 'Bearer ' + tok, 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
+ if (r.ok) { ok++; runlog.push({ offerId: b.offerId, source: b.source, link: b.attrs.link, ok: true }); }
+ else { fail++; runlog.push({ offerId: b.offerId, ok: false, err: (await r.text()).slice(0, 160) }); }
+ if ((ok + fail) % 10 === 0) console.log(` ${ok + fail}/${build.length}`);
+}
+fs.writeFileSync(`data/tk11450-ca-restore-v2-run-${stamp}.jsonl`, runlog.map(l => JSON.stringify(l)).join('\n') + '\n');
+console.log(`DONE: ${ok} restored, ${fail} failed.`);
+console.log(`UNDO: productInputs.delete each offerId in data/tk11450-ca-restore-v2-run-${stamp}.jsonl (pre-state was price-only = the defect; restore-forward is the correct direction).`);
+console.log('VERIFY after ~10-30min reprocess: CA item_missing_required_attribute should drop by exactly ' + ok + '.');
← 3e29ec4 auto-data-snapshot: 2026-09-25T11:35:27 (5 data files) — dat
·
back to Gmc Titlefix
·
TK-11450: APPLIED v2 CA restore — 41 inputs re-inserted, 0 f 2a68674 →