[object Object]

← back to Gmc Titlefix

track canary tooling (apply-canary/build-fresh-canary/verify-canary) used by TK-10635 remediation

463ca3dfdb4579fd7f849d49c25509aad63277e8 · 2026-08-27 10:34:10 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 463ca3dfdb4579fd7f849d49c25509aad63277e8
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Aug 27 10:34:10 2026 -0700

    track canary tooling (apply-canary/build-fresh-canary/verify-canary) used by TK-10635 remediation
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
 apply-canary.mjs       | 24 +++++++++++++++
 build-fresh-canary.mjs | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++
 verify-canary.mjs      | 32 ++++++++++++++++++++
 3 files changed, 137 insertions(+)

diff --git a/apply-canary.mjs b/apply-canary.mjs
new file mode 100644
index 0000000..ffb6508
--- /dev/null
+++ b/apply-canary.mjs
@@ -0,0 +1,24 @@
+// Push the ~400-row FRESH canary price overrides onto the supplemental datasource.
+// Reversible: same offerId override; removing the supplement reverts to $4.25.
+import fs from 'fs';
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+const { token, MERCHANT } = require('./_auth.js');
+const DS = 'accounts/146735262/dataSources/10693978453';
+const CANARY = '/Users/macstudio3/.claude/yolo-queue/gmc-fresh-override-canary.json';
+const sleep = ms => new Promise(r=>setTimeout(r,ms));
+const list = JSON.parse(fs.readFileSync(CANARY,'utf8')).overrides;
+let tok = await token(), tokAt = Date.now(), ok=0, fail=0; const fails=[];
+console.log(`Pushing ${list.length} canary overrides -> ${DS}`);
+for (let i=0;i<list.length;i++){
+  if (Date.now()-tokAt > 50*60*1000){ tok=await token(); tokAt=Date.now(); }
+  const row=list[i];
+  const url=`https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(DS)}`;
+  const body={ offerId:row.offerId, contentLanguage:row.contentLanguage, feedLabel:row.feedLabel, productAttributes:{ price:{ amountMicros:String(Math.round(row.realPrice*1e6)), currencyCode:'USD' } } };
+  const r=await fetch(url,{method:'POST',headers:{Authorization:'Bearer '+tok,'Content-Type':'application/json'},body:JSON.stringify(body)});
+  if (r.ok) ok++; else { fail++; const t=await r.text(); if(fails.length<12) fails.push(`${row.offerId} ${r.status} ${t.slice(0,110)}`); if(r.status===429) await sleep(3000); }
+  if (i%50===0) process.stdout.write(`  ${i}/${list.length} ok ${ok} fail ${fail}\n`);
+}
+console.log(`\nCANARY DONE: ok ${ok} / fail ${fail} of ${list.length}`);
+if (fails.length){ console.log('--- first failures ---'); fails.forEach(f=>console.log('  '+f)); }
+fs.writeFileSync('/Users/macstudio3/.claude/yolo-queue/gmc-canary-apply-result.json', JSON.stringify({ when:'today', ds:DS, pushed:list.length, ok, fail },null,2));
diff --git a/build-fresh-canary.mjs b/build-fresh-canary.mjs
new file mode 100644
index 0000000..38a7578
--- /dev/null
+++ b/build-fresh-canary.mjs
@@ -0,0 +1,81 @@
+// READ-ONLY: build a FRESH $4.25→roll price override list by joining live MC offers
+// to TODAY's live roll prices (out/active-roll-and-sample.csv, scanned today), via pid
+// extracted from offerId shopify_US_<pid>_<vid>. Guarantees pushed price == today's landing.
+// Writes full list + a ~400 systematic-sample canary. NO writes to Google.
+import fs from 'fs';
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+const { token, MERCHANT } = require('./_auth.js');
+
+const CSV = '/Users/macstudio3/.claude/skills/google-merchant-agent/out/active-roll-and-sample.csv';
+const OUT_FULL = '/Users/macstudio3/.claude/yolo-queue/gmc-fresh-override-full.json';
+const OUT_CANARY = '/Users/macstudio3/.claude/yolo-queue/gmc-fresh-override-canary.json';
+const CANARY_N = 400;
+
+// pid -> today's roll price (maxVariantPrice), only where roll>4.26
+function loadFreshRoll() {
+  const lines = fs.readFileSync(CSV, 'utf8').split('\n');
+  const m = new Map();
+  for (let i = 1; i < lines.length; i++) {
+    if (!lines[i]) continue;
+    // naive split is unsafe (quoted commas in title/vendor); but pid(0), rollPrice(8), maxPrice(10)
+    // are numeric and BEFORE... no — title/vendor are quoted & contain commas. Parse robustly:
+    const f = parseCsv(lines[i]);
+    const pid = f[0], roll = parseFloat(f[8]);
+    if (pid && !isNaN(roll) && roll > 4.26) m.set(pid, roll);
+  }
+  return m;
+}
+function parseCsv(line){ const out=[];let cur='',q=false;for(let i=0;i<line.length;i++){const c=line[i];
+  if(q){ if(c==='"'){ if(line[i+1]==='"'){cur+='"';i++;} else q=false; } else cur+=c; }
+  else { if(c===','){out.push(cur);cur='';} else if(c==='"')q=true; else cur+=c; } } out.push(cur); return out; }
+
+async function listOffers() {
+  const tok = await token(); const H = { Authorization: 'Bearer ' + tok };
+  const offers = []; let page = null, scanned = 0;
+  do {
+    const r = await (await fetch(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/products?maxResults=250` + (page ? `&pageToken=${page}` : ''), { headers: H })).json();
+    if (r.error) { console.error('MC err', JSON.stringify(r.error).slice(0,150)); break; }
+    for (const p of (r.resources || [])) offers.push({ offerId: p.offerId, price: parseFloat(p.price?.value || '0'), feedLabel: p.feedLabel || 'US', contentLanguage: p.contentLanguage || 'en', title: (p.title||'').slice(0,45) });
+    page = r.nextPageToken; scanned += (r.resources||[]).length;
+    if (scanned % 10000 < 250) process.stderr.write(`  ...${scanned} offers read\n`);
+  } while (page);
+  return offers;
+}
+
+const roll = loadFreshRoll();
+console.log(`Fresh roll prices (pid→roll>4.26): ${roll.size}`);
+const offers = await listOffers();
+console.log(`Live MC offers read: ${offers.length}`);
+
+const RE = /^shopify_[A-Z]+_(\d+)_\d+$/;
+let leak=0, joined=0, sampleOnlyOrNoRoll=0, legacyBareVid=0, alreadyReal=0;
+const overrides=[];
+for (const o of offers) {
+  if (o.price > 4.26) { alreadyReal++; continue; }
+  leak++;
+  const mm = o.offerId.match(RE);
+  if (!mm) { legacyBareVid++; continue; }          // bare-vid CA/GB legacy — defer to full rollout
+  const pid = mm[1];
+  const rp = roll.get(pid);
+  if (rp === undefined) { sampleOnlyOrNoRoll++; continue; } // sample-only or no fresh roll → leave $4.25
+  joined++;
+  overrides.push({ offerId:o.offerId, contentLanguage:o.contentLanguage, feedLabel:o.feedLabel, pid, currentPrice:o.price, realPrice:rp, title:o.title });
+}
+fs.writeFileSync(OUT_FULL, JSON.stringify({ generated_at:'today-live', total_offers:offers.length, leak_offers:leak, override_rows_us_pidjoin:overrides.length, deferred_legacy_bare_vid:legacyBareVid, left_at_425_sampleonly_or_noroll:sampleOnlyOrNoRoll, already_real:alreadyReal, overrides }, null, 2));
+
+// systematic sample for representativeness across the (vendor-clustered) list
+const step = Math.max(1, Math.floor(overrides.length / CANARY_N));
+const canary = []; for (let i=0; i<overrides.length && canary.length<CANARY_N; i+=step) canary.push(overrides[i]);
+fs.writeFileSync(OUT_CANARY, JSON.stringify({ generated_at:'today-live', datasource:'accounts/146735262/dataSources/10693978453', count:canary.length, sampled_every_nth:step, overrides:canary }, null, 2));
+
+console.log(`\n=== FRESH OVERRIDE BUILD (read-only) ===`);
+console.log(`  leak offers (<=$4.26):        ${leak}`);
+console.log(`  US pid-join override rows:    ${overrides.length}`);
+console.log(`  deferred legacy bare-vid:     ${legacyBareVid}  (CA/GB — need vid->pid, full rollout)`);
+console.log(`  left at $4.25 (sampleonly):   ${sampleOnlyOrNoRoll}`);
+console.log(`  canary rows (~${CANARY_N}):           ${canary.length}  every ${step}th`);
+console.log(`  full  -> ${OUT_FULL}`);
+console.log(`  canary-> ${OUT_CANARY}`);
+console.log('--- canary sample ---');
+canary.slice(0,10).forEach(o=>console.log(`  ${o.offerId}  $${o.currentPrice} -> $${o.realPrice}  ${o.title}`));
diff --git a/verify-canary.mjs b/verify-canary.mjs
new file mode 100644
index 0000000..0d165b0
--- /dev/null
+++ b/verify-canary.mjs
@@ -0,0 +1,32 @@
+// READ-ONLY canary verification: run 24-48h after apply. Reads the 400 canary offers'
+// processed price + approval status; reports % flipped to real price and % approved.
+// GATE: release the remaining US overrides only if approved >=95% and no NEW disapprovals.
+import fs from 'fs';
+import { createRequire } from 'module';
+const require = createRequire(import.meta.url);
+const { token, MERCHANT } = require('./_auth.js');
+const canary = JSON.parse(fs.readFileSync('/Users/macstudio3/.claude/yolo-queue/gmc-fresh-override-canary.json','utf8')).overrides;
+const tok = await token(); const H={Authorization:'Bearer '+tok};
+let flipped=0, still425=0, approved=0, disapproved=0, other=0, err=0;
+const bad=[];
+for (let i=0;i<canary.length;i++){
+  const row=canary[i];
+  const rid=`online:${row.contentLanguage}:${row.feedLabel}:${row.offerId}`;
+  try{
+    const st=await (await fetch(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/productstatuses/${encodeURIComponent(rid)}`,{headers:H})).json();
+    const pr=await (await fetch(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/products/${encodeURIComponent(rid)}`,{headers:H})).json();
+    const price=parseFloat(pr.price?.value||'0');
+    if (price>4.26) flipped++; else still425++;
+    const ds=(st.destinationStatuses||[]);
+    const s=(ds.find(d=>/Shopping/i.test(d.destination))||ds[0]||{}).status||'';
+    if(s==='disapproved'){disapproved++; if(bad.length<15) bad.push(`${row.offerId} price=${price} DISAPPROVED`);}
+    else if(s==='approved'){approved++;} else other++;
+  }catch(e){err++;}
+  if(i%50===0) process.stderr.write(`  ${i}/${canary.length}\n`);
+}
+const pctFlip=(flipped/canary.length*100).toFixed(1), pctAppr=(approved/canary.length*100).toFixed(1);
+console.log(`\n=== CANARY VERIFY (n=${canary.length}) ===`);
+console.log(`  price flipped to real: ${flipped} (${pctFlip}%)   still $4.25: ${still425}`);
+console.log(`  approved: ${approved} (${pctAppr}%)   disapproved: ${disapproved}   other/pending: ${other}   err: ${err}`);
+console.log(`  GATE (release remaining US if approved>=95% AND disapproved not rising): ${pctAppr>=95?'PASS':'HOLD'}`);
+if(bad.length){console.log('--- disapproved sample ---'); bad.forEach(b=>console.log('  '+b));}

← 436960a TK-10635: add gated feed-linkage fix (root cause of $4.25 le  ·  back to Gmc Titlefix  ·  TK-10451: primary-source GMC $4.25 leak fix — canary proves 0ab1c50 →