[object Object]

← 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

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 →