[object Object]

← back to Gmc Titlefix

canary diag: verified full override chain (supplemental→owning primary 180695450→feed rule); flip is pure reprocess-cadence wait, not misconfig; self-terminating poller

18e376c4564ddd0d187da0d01a65ea00e884c57c · 2026-06-17 15:48:00 -0700 · SteveStudio2

Files touched

Diff

commit 18e376c4564ddd0d187da0d01a65ea00e884c57c
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed Jun 17 15:48:00 2026 -0700

    canary diag: verified full override chain (supplemental→owning primary 180695450→feed rule); flip is pure reprocess-cadence wait, not misconfig; self-terminating poller
---
 diag-titlefix.js                |  20 +++++
 diag2.js                        |  12 +++
 diag3.js                        |  14 ++++
 poll-canary.sh                  |  17 ++++
 review-viewer.mjs               | 175 ++++++++++++++++++++++++++++++++++++++++
 step-a-create-datasource.js     |  30 +++++--
 step-a-create-datasource.js.bak |  12 +++
 step-b-test-one.js              |   4 +-
 step-b-test-one.js.bak          |  19 +++++
 step-b-test-one.js.bak2         |  19 +++++
 step-d-scale-all.js             |   4 +-
 step-d-scale-all.js.bak         |  28 +++++++
 step-d-scale-all.js.bak2        |  28 +++++++
 13 files changed, 370 insertions(+), 12 deletions(-)

diff --git a/diag-titlefix.js b/diag-titlefix.js
new file mode 100644
index 0000000..23c88a2
--- /dev/null
+++ b/diag-titlefix.js
@@ -0,0 +1,20 @@
+const {token,MERCHANT}=require('./_auth');
+const offerId=require('fs').readFileSync('/tmp/gmc-titlefix-canary-offer.txt','utf8').trim();
+(async()=>{
+  const tok=await token();
+  const H={Authorization:'Bearer '+tok};
+  let r=await fetch(`https://merchantapi.googleapis.com/datasources/v1/accounts/${MERCHANT}/dataSources`,{headers:H});
+  let ds=await r.json();
+  console.log('=== DATA SOURCES (HTTP '+r.status+') ===');
+  let suppName=null;
+  (ds.dataSources||[]).forEach(d=>{
+    const kind=d.primaryProductDataSource?'PRIMARY':d.supplementalProductDataSource?'SUPPLEMENTAL':'?';
+    if(d.supplementalProductDataSource) suppName=d.name;
+    console.log(`  ${kind}  name=${d.name}  displayName=${d.displayName||''}`);
+    if(d.primaryProductDataSource){
+      const dr=d.primaryProductDataSource.defaultRule;
+      console.log('     defaultRule.takeFromDataSources =',JSON.stringify(dr&&dr.takeFromDataSources));
+    }
+  });
+  console.log('\nsupplemental name =',suppName);
+})().catch(e=>console.error('ERR',e.message));
diff --git a/diag2.js b/diag2.js
new file mode 100644
index 0000000..60b9e55
--- /dev/null
+++ b/diag2.js
@@ -0,0 +1,12 @@
+const {token,MERCHANT}=require('./_auth');
+const offerId=require('fs').readFileSync('/tmp/gmc-titlefix-canary-offer.txt','utf8').trim();
+(async()=>{
+  const tok=await token(); const H={Authorization:'Bearer '+tok};
+  const id=`online:en:US:${offerId}`;
+  const r=await fetch(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/products/${encodeURIComponent(id)}`,{headers:H});
+  const j=await r.json();
+  console.log('offerId:',offerId);
+  console.log('title  :',j.title);
+  console.log('source :',j.source);            // 'api' vs 'feed:...' etc
+  console.log('channel:',j.channel,'| feedLabel:',j.feedLabel,'| contentLanguage:',j.contentLanguage);
+})().catch(e=>console.error('ERR',e.message));
diff --git a/diag3.js b/diag3.js
new file mode 100644
index 0000000..53c42b1
--- /dev/null
+++ b/diag3.js
@@ -0,0 +1,14 @@
+const {token,MERCHANT}=require('./_auth');
+const offerId=require('fs').readFileSync('/tmp/gmc-titlefix-canary-offer.txt','utf8').trim();
+(async()=>{
+  const tok=await token(); const H={Authorization:'Bearer '+tok};
+  // Merchant API processed product: name=accounts/{a}/products/{channel~contentLang~feedLabel~offerId}
+  const name=`accounts/${MERCHANT}/products/online~en~US~${offerId}`;
+  const r=await fetch(`https://merchantapi.googleapis.com/products/v1/${name}`,{headers:H});
+  const j=await r.json();
+  console.log('HTTP',r.status);
+  console.log('name        :',j.name);
+  console.log('dataSource  :',j.dataSource,'  <-- owning PRIMARY');
+  console.log('title (eff) :',j.attributes&&j.attributes.title);
+  console.log('versionNumber:',j.versionNumber);
+})().catch(e=>console.error('ERR',e.message));
diff --git a/poll-canary.sh b/poll-canary.sh
new file mode 100755
index 0000000..afdb8dc
--- /dev/null
+++ b/poll-canary.sh
@@ -0,0 +1,17 @@
+#!/bin/zsh
+cd "$HOME/Projects/gmc-titlefix"
+for i in $(seq 1 36); do            # 36 × 30min = 18h cap
+  OUT="$(node step-c-verify-one.js 2>/dev/null)"
+  T="$(echo "$OUT" | grep 'EFFECTIVE title now:' | sed 's/.*EFFECTIVE title now: //')"
+  TS="$(date '+%H:%M:%S')"
+  if echo "$T" | grep -q '^Sample:'; then
+    echo "FLIPPED ✅ $TS — title now: $T"
+    echo "PINGME: GMC canary flipped to Sample-prefixed. step-d (29,672) is now UNLOCKED for Steve's go."
+    exit 0
+  fi
+  echo "[check $i] not flipped $TS — $T"
+  sleep 1800
+done
+echo "TIMEOUT after 18h — still not flipped; reprocess cadence longer than expected. Mechanism remains verified-correct."
+echo "PINGME: GMC canary still not flipped after 18h — needs a look at Google reprocess cadence."
+exit 2
diff --git a/review-viewer.mjs b/review-viewer.mjs
new file mode 100644
index 0000000..8c1f909
--- /dev/null
+++ b/review-viewer.mjs
@@ -0,0 +1,175 @@
+#!/usr/bin/env node
+// GMC $4.25 title-fix — review viewer (zero-dep). http://localhost:9934
+// Reads the 29,672 before/after list + the gated memo + live vanguard status.
+import http from 'http';
+import fs from 'fs';
+const PORT = 9934;
+const DIR = '/Users/stevestudio2/Projects/gmc-titlefix';
+const MEMO = '/Users/stevestudio2/.claude/yolo-queue/pending-approval/30-gmc-title-fix.md';
+const LIST = `${DIR}/gmc_titlefix_list.json`;
+const DS_FILE = '/tmp/gmc_titlefix_ds.txt';
+const STEPB = '/tmp/gmc_stepB.out';
+const VERIFY = '/tmp/gmc-titlefix-verify-pass.txt';
+
+let ROWS = [];
+try { ROWS = JSON.parse(fs.readFileSync(LIST, 'utf8')); } catch {}
+const SHOP = 'designer-laboratory-sandbox';
+const SHOP_DOMAIN = `${SHOP}.myshopify.com`;
+let TOKEN = '';
+try { TOKEN = (fs.readFileSync('/Users/stevestudio2/Projects/secrets-manager/.env','utf8').match(/^SHOPIFY_ADMIN_TOKEN=(.*)$/m)||[])[1]?.replace(/['"]/g,'').trim() || ''; } catch {}
+const handleCache = {};
+async function resolveHandles(ids){
+  const need = ids.filter(id => handleCache[id] === undefined);
+  for (let i=0;i<need.length;i+=120){
+    const batch = need.slice(i,i+120).map(x=>`gid://shopify/Product/${x}`);
+    try{
+      const r = await fetch(`https://${SHOP_DOMAIN}/admin/api/2024-10/graphql.json`,{method:'POST',
+        headers:{'X-Shopify-Access-Token':TOKEN,'Content-Type':'application/json'},
+        body:JSON.stringify({query:`query($ids:[ID!]!){nodes(ids:$ids){... on Product{id handle status onlineStoreUrl}}}`,variables:{ids:batch}})});
+      const j = await r.json();
+      for (const n of (j.data?.nodes||[])){ if(n){ const pid=n.id.split('/').pop(); handleCache[pid]={handle:n.handle,status:n.status,live:!!n.onlineStoreUrl}; } }
+      for (const id of need.slice(i,i+120)) if(handleCache[id]===undefined) handleCache[id]=null; // mark missing
+    }catch(e){ for (const id of need.slice(i,i+120)) handleCache[id]=null; }
+  }
+  const out={}; for(const id of ids) out[id]=handleCache[id]||null; return out;
+}
+const read = (p) => { try { return fs.readFileSync(p, 'utf8'); } catch { return ''; } };
+const exists = (p) => { try { fs.accessSync(p); return true; } catch { return false; } };
+
+// vendor tally (from "by <Vendor>" / "| <Vendor>" suffix) — best-effort
+function vendorOf(t){ const m=(t||'').match(/(?:by|\|)\s+([^|]+?)\s*$/i); return m?m[1].trim().slice(0,28):'(other)'; }
+
+const PAGE = `<!doctype html><html><head><meta charset="utf-8"><title>GMC $4.25 Title-Fix — Review</title>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<style>
+ :root{--bg:#0b0e14;--panel:#11151f;--ink:#e7ecf3;--dim:#8a94a6;--line:#1f2735;--ok:#34d399;--warn:#fbbf24;--bad:#f87171;--accent:#34f4ce;--add:#7dd3fc}
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--ink);font:13.5px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,Arial}
+ header{padding:16px 24px;border-bottom:1px solid var(--line);position:sticky;top:0;background:#0b0e14;z-index:5}
+ h1{margin:0;font-size:18px}h1 small{color:var(--dim);font-weight:400;font-size:12px;margin-left:8px}
+ main{max-width:1280px;margin:0 auto;padding:18px 24px 80px}
+ .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin:4px 0 16px}
+ .card{background:var(--panel);border:1px solid var(--line);border-radius:10px;padding:11px 13px}
+ .card .k{font-size:10.5px;letter-spacing:.05em;text-transform:uppercase;color:var(--dim)}
+ .card .v{font-size:23px;font-weight:700;margin-top:2px}
+ .steps{display:flex;gap:8px;flex-wrap:wrap;margin:6px 0 18px}
+ .step{flex:1;min-width:150px;background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:9px 12px;font-size:12px}
+ .step b{display:block;font-size:11px;color:var(--dim);text-transform:uppercase;letter-spacing:.04em}
+ .pill{font-size:11px;padding:1px 8px;border-radius:10px;border:1px solid var(--line)}
+ .pend{color:var(--dim)} .go{color:var(--ok);border-color:#1c5} .hold{color:var(--warn);border-color:#a83}
+ h2{font-size:12px;letter-spacing:.05em;text-transform:uppercase;color:var(--dim);margin:22px 0 8px}
+ .memo{background:#0a0e16;border:1px solid var(--line);border-radius:9px;padding:12px 14px;white-space:pre-wrap;font:12px/1.5 ui-monospace,monospace;color:#c8d2e0;max-height:340px;overflow:auto}
+ .toolbar{display:flex;gap:10px;align-items:center;margin:6px 0 10px;flex-wrap:wrap}
+ #q{flex:1;max-width:420px;background:#0a0e16;border:1px solid #2b3650;color:#e7ecf3;border-radius:8px;padding:8px 11px}
+ #q:focus{outline:none;border-color:var(--accent)}
+ select{background:#0a0e16;border:1px solid #2b3650;color:#e7ecf3;border-radius:8px;padding:7px 9px}
+ table{width:100%;border-collapse:collapse;font-size:12.5px}
+ th,td{text-align:left;padding:7px 9px;border-bottom:1px solid var(--line);vertical-align:top}
+ th{color:var(--dim);font-size:10.5px;text-transform:uppercase;letter-spacing:.04em}
+ .cur{color:var(--bad)} .prop{color:var(--ok)} .prop b{color:var(--accent)}
+ td.n{font:11px ui-monospace,monospace;color:var(--dim);white-space:nowrap}
+ .pager{display:flex;gap:8px;align-items:center;margin:12px 0;color:var(--dim)}
+ .pager button{background:#121826;border:1px solid var(--line);color:#e7ecf3;border-radius:7px;padding:5px 12px;cursor:pointer}
+ .pager button:disabled{opacity:.4;cursor:default}
+ .trunc{color:var(--warn);font-size:10px;border:1px solid #a83;border-radius:8px;padding:0 5px;margin-left:5px}
+ .note{color:var(--dim);font-size:11.5px}
+</style></head><body>
+<header><h1>GMC $4.25 Title-Fix <small>Review — 29,672 sample-only offers · "Sample: " prefix · Google-feed only</small></h1></header>
+<main>
+ <div class="cards" id="cards"></div>
+ <h2>Vanguard (gated) — test before scale</h2>
+ <div class="steps" id="steps"></div>
+ <h2>Before → After (the exact change to each Google feed title)</h2>
+ <div class="toolbar">
+   <input id="q" placeholder="🔎 filter by title / SKU / productId…">
+   <select id="vendor"><option value="">All vendors</option></select>
+   <label class="note"><input type="checkbox" id="tonly"> truncated only</label>
+   <span class="note" id="count"></span>
+ </div>
+ <table><thead><tr><th style="width:42px">#</th><th>Current feed title ($4.25)</th><th>→ Proposed</th><th style="width:120px">links</th></tr></thead><tbody id="rows"></tbody></table>
+ <div class="pager"><button id="prev">◀ Prev</button><span id="pg"></span><button id="next">Next ▶</button></div>
+ <h2>The plan + DTD verdicts + officer sign-offs (gated memo)</h2>
+ <div class="memo" id="memo">loading…</div>
+</main>
+<script>
+let page=0, size=50, q='', vendor='', tonly=false, total=0;
+async function summary(){
+ const s=await (await fetch('/api/summary')).json();
+ const c=[['Feed offers (total)',s.feedTotal],['$4.25 offers',s.p425],['Already "Sample"',s.alreadySample],['FIX SET (this review)',s.fixCount],['Truncated (body trimmed)',s.truncated],['Top vendor',s.topVendor]];
+ document.getElementById('cards').innerHTML=c.map(([k,v])=>'<div class="card"><div class="k">'+k+'</div><div class="v">'+(typeof v==='number'?v.toLocaleString():v)+'</div></div>').join('');
+ const stp=[['A · create datasource',s.stepA],['B · 1-offer canary',s.stepB],['C · verify title flip',s.stepC],['D · scale 29,672',s.stepD]];
+ document.getElementById('steps').innerHTML=stp.map(([k,st])=>'<div class="step"><b>'+k+'</b><span class="pill '+(st.cls)+'">'+st.txt+'</span></div>').join('');
+ const vs=document.getElementById('vendor');
+ if(vs.options.length<2 && s.vendors){ for(const [v,n] of s.vendors) vs.innerHTML+='<option value="'+v+'">'+v+' ('+n+')</option>'; }
+}
+async function load(){
+ const r=await (await fetch('/api/rows?page='+page+'&size='+size+'&q='+encodeURIComponent(q)+'&vendor='+encodeURIComponent(vendor)+'&tonly='+(tonly?1:0))).json();
+ total=r.total;
+ const ADMIN='https://admin.shopify.com/store/designer-laboratory-sandbox/products/';
+ document.getElementById('rows').innerHTML=r.rows.map((x,i)=>'<tr data-pid="'+x.productId+'"><td class="n">'+(page*size+i+1)+'</td><td class="cur">'+esc(x.currentTitle)+'</td><td class="prop">'+hl(x.proposedTitle)+(x.truncated?'<span class=trunc>trim</span>':'')+'</td><td class="n"><a target="_blank" href="'+ADMIN+x.productId+'">⚙️ admin</a><span class="front" data-pid="'+x.productId+'"> · <span class=note>front…</span></span></td></tr>').join('') || '<tr><td colspan=4 class=note>no matches</td></tr>';
+ // lazy-resolve storefront handles for this page
+ const ids=r.rows.map(x=>x.productId);
+ if(ids.length){ fetch('/api/handles?ids='+ids.join(',')).then(r=>r.json()).then(map=>{
+   document.querySelectorAll('span.front').forEach(el=>{ const h=map[el.dataset.pid];
+     el.innerHTML = h&&h.handle ? ' · <a target="_blank" href="https://designerwallcoverings.com/products/'+h.handle+'">🛍 front</a>'+(h.live?'':' <span class=trunc>off</span>') : ' · <span class=note>(no handle)</span>'; });
+ }).catch(()=>{}); }
+ document.getElementById('count').textContent=total.toLocaleString()+' rows';
+ document.getElementById('pg').textContent='page '+(page+1)+' / '+Math.max(1,Math.ceil(total/size));
+ document.getElementById('prev').disabled=page<=0;
+ document.getElementById('next').disabled=(page+1)*size>=total;
+}
+function esc(s){return (s==null?'':String(s)).replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));}
+function hl(s){s=esc(s); return s.replace(/^Sample:/,'<b>Sample:</b>');}
+async function memo(){ document.getElementById('memo').textContent=await (await fetch('/api/memo')).text(); }
+document.getElementById('q').oninput=e=>{q=e.target.value;page=0;load();};
+document.getElementById('vendor').onchange=e=>{vendor=e.target.value;page=0;load();};
+document.getElementById('tonly').onchange=e=>{tonly=e.target.checked;page=0;load();};
+document.getElementById('prev').onclick=()=>{if(page>0){page--;load();}};
+document.getElementById('next').onclick=()=>{if((page+1)*size<total){page++;load();}};
+summary(); load(); memo(); setInterval(summary,5000);
+</script></body></html>`;
+
+function stepStatus(){
+  const dsLive = exists(DS_FILE) && read(DS_FILE).includes('dataSources/');
+  const b = read(STEPB);
+  const bOk = /productInputs|"name"|inserted|200|201/i.test(b) && !/error/i.test(b);
+  const cOk = exists(VERIFY);
+  return {
+    stepA: dsLive ? {cls:'go', txt:'created ✓'} : {cls:'pend', txt:'pending (API enabling)'},
+    stepB: dsLive ? (bOk ? {cls:'go', txt:'canary pushed ✓'} : (b?{cls:'hold',txt:'pushed — check log'}:{cls:'pend',txt:'pending'})) : {cls:'pend', txt:'waiting on A'},
+    stepC: cOk ? {cls:'go', txt:'VERIFIED — title flipped ✓'} : {cls:'pend', txt:'pending'},
+    stepD: {cls:'hold', txt:'HELD for Steve confirm'},
+  };
+}
+
+http.createServer((req,res)=>{
+  const u = new URL(req.url, 'http://x');
+  if (u.pathname==='/') { res.writeHead(200,{'content-type':'text/html'}); return res.end(PAGE); }
+  if (u.pathname==='/api/summary') {
+    const vt={}; for(const r of ROWS){ const v=vendorOf(r.currentTitle); vt[v]=(vt[v]||0)+1; }
+    const vendors=Object.entries(vt).sort((a,b)=>b[1]-a[1]).slice(0,40);
+    res.writeHead(200,{'content-type':'application/json'});
+    return res.end(JSON.stringify({
+      feedTotal:105730, p425:62173, alreadySample:26618, fixCount:ROWS.length,
+      truncated:ROWS.filter(r=>r.truncated).length, topVendor:(vendors[0]||['—'])[0],
+      vendors, ...stepStatus()
+    }));
+  }
+  if (u.pathname==='/api/rows') {
+    const page=+(u.searchParams.get('page')||0), size=Math.min(200,+(u.searchParams.get('size')||50));
+    const q=(u.searchParams.get('q')||'').toLowerCase(), vendor=u.searchParams.get('vendor')||'', tonly=u.searchParams.get('tonly')==='1';
+    let f=ROWS;
+    if(vendor) f=f.filter(r=>vendorOf(r.currentTitle)===vendor);
+    if(tonly) f=f.filter(r=>r.truncated);
+    if(q) f=f.filter(r=>((r.currentTitle||'')+' '+(r.productId||'')+' '+(r.offerId||'')).toLowerCase().includes(q));
+    res.writeHead(200,{'content-type':'application/json'});
+    return res.end(JSON.stringify({total:f.length, rows:f.slice(page*size,page*size+size)}));
+  }
+  if (u.pathname==='/api/memo') { res.writeHead(200,{'content-type':'text/plain'}); return res.end(read(MEMO)||'(memo not found)'); }
+  if (u.pathname==='/api/handles') {
+    const ids=(u.searchParams.get('ids')||'').split(',').filter(Boolean).slice(0,200);
+    resolveHandles(ids).then(map=>{ res.writeHead(200,{'content-type':'application/json'}); res.end(JSON.stringify(map)); })
+      .catch(e=>{ res.writeHead(200,{'content-type':'application/json'}); res.end('{}'); });
+    return;
+  }
+  res.writeHead(404); res.end('nope');
+}).listen(PORT, ()=>console.log(`GMC title-fix review viewer → http://localhost:${PORT}  (${ROWS.length} rows)`));
diff --git a/step-a-create-datasource.js b/step-a-create-datasource.js
index bd95e1f..eb9b33d 100644
--- a/step-a-create-datasource.js
+++ b/step-a-create-datasource.js
@@ -1,12 +1,26 @@
-// GATED WRITE — creates a supplemental data source in Merchant Center.
+// GATED WRITE — creates a supplemental data source in Merchant Center (Merchant API v1).
+// v1 rejects feedLabel/contentLanguage at create-time on a supplemental source, so we create
+// an UNBOUND supplemental data source ({}), which merges overrides onto products by offerId.
 const {token,MERCHANT}=require('./_auth');
 (async()=>{
   const tok=await token();
-  const url=`https://merchantapi.googleapis.com/datasources/v1beta/accounts/${MERCHANT}/dataSources`;
-  const body={ displayName:'DW Sample Title Overrides',
-    supplementalProductDataSource:{ contentLanguage:'en', feedLabel:'US' } };
-  const r=await fetch(url,{method:'POST',headers:{Authorization:'Bearer '+tok,'Content-Type':'application/json'},body:JSON.stringify(body)});
-  const j=await r.json();
-  console.log('HTTP',r.status); console.log(JSON.stringify(j,null,2));
-  if(j.name) console.log('\n>>> SAVE THIS dataSource name for steps B/D:\n'+j.name);
+  const url=`https://merchantapi.googleapis.com/datasources/v1/accounts/${MERCHANT}/dataSources`;
+  const shapes=[
+    { displayName:'DW Sample Title Overrides', supplementalProductDataSource:{} },
+    { displayName:'DW Sample Title Overrides', supplementalProductDataSource:{ contentLanguage:'en' } },
+    { displayName:'DW Sample Title Overrides', supplementalProductDataSource:{ contentLanguage:'en', feedLabel:'US' } },
+  ];
+  for(const body of shapes){
+    const r=await fetch(url,{method:'POST',headers:{Authorization:'Bearer '+tok,'Content-Type':'application/json'},body:JSON.stringify(body)});
+    const j=await r.json();
+    console.log('shape='+JSON.stringify(body.supplementalProductDataSource)+' -> HTTP '+r.status);
+    if(r.status>=200 && r.status<300 && j.name){
+      console.log(JSON.stringify(j,null,2));
+      console.log('\n>>> SAVE THIS dataSource name for steps B/D:\n'+j.name);
+      return;
+    } else {
+      console.log('  '+(j.error?.message||JSON.stringify(j)).slice(0,160));
+    }
+  }
+  console.log('\n>>> all shapes failed — see errors above');
 })();
diff --git a/step-a-create-datasource.js.bak b/step-a-create-datasource.js.bak
new file mode 100644
index 0000000..bd95e1f
--- /dev/null
+++ b/step-a-create-datasource.js.bak
@@ -0,0 +1,12 @@
+// GATED WRITE — creates a supplemental data source in Merchant Center.
+const {token,MERCHANT}=require('./_auth');
+(async()=>{
+  const tok=await token();
+  const url=`https://merchantapi.googleapis.com/datasources/v1beta/accounts/${MERCHANT}/dataSources`;
+  const body={ displayName:'DW Sample Title Overrides',
+    supplementalProductDataSource:{ contentLanguage:'en', feedLabel:'US' } };
+  const r=await fetch(url,{method:'POST',headers:{Authorization:'Bearer '+tok,'Content-Type':'application/json'},body:JSON.stringify(body)});
+  const j=await r.json();
+  console.log('HTTP',r.status); console.log(JSON.stringify(j,null,2));
+  if(j.name) console.log('\n>>> SAVE THIS dataSource name for steps B/D:\n'+j.name);
+})();
diff --git a/step-b-test-one.js b/step-b-test-one.js
index d6c57a4..3fb0894 100644
--- a/step-b-test-one.js
+++ b/step-b-test-one.js
@@ -7,9 +7,9 @@ const list=require('./gmc_titlefix_list.json');
 const row=list[0]; // first risky offer as the canary
 (async()=>{
   const tok=await token();
-  const url=`https://merchantapi.googleapis.com/products/v1beta/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(DS)}`;
+  const url=`https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(DS)}`;
   const body={ offerId:row.offerId, contentLanguage:'en', feedLabel:'US',
-    attributes:{ title: row.proposedTitle } };
+    productAttributes:{ title: row.proposedTitle } };
   console.log('TEST offer:',row.offerId);
   console.log('  current :',row.currentTitle);
   console.log('  proposed:',row.proposedTitle);
diff --git a/step-b-test-one.js.bak b/step-b-test-one.js.bak
new file mode 100644
index 0000000..d6c57a4
--- /dev/null
+++ b/step-b-test-one.js.bak
@@ -0,0 +1,19 @@
+// GATED WRITE — pushes ONE title override. Usage: node step-b-test-one.js <dataSourceName>
+const {token,MERCHANT}=require('./_auth');
+const fs=require('fs');
+const DS=process.argv[2]; // e.g. accounts/146735262/dataSources/1234567890
+if(!DS){ console.error('Usage: node step-b-test-one.js <dataSourceName from step A>'); process.exit(1); }
+const list=require('./gmc_titlefix_list.json');
+const row=list[0]; // first risky offer as the canary
+(async()=>{
+  const tok=await token();
+  const url=`https://merchantapi.googleapis.com/products/v1beta/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(DS)}`;
+  const body={ offerId:row.offerId, contentLanguage:'en', feedLabel:'US',
+    attributes:{ title: row.proposedTitle } };
+  console.log('TEST offer:',row.offerId);
+  console.log('  current :',row.currentTitle);
+  console.log('  proposed:',row.proposedTitle);
+  const r=await fetch(url,{method:'POST',headers:{Authorization:'Bearer '+tok,'Content-Type':'application/json'},body:JSON.stringify(body)});
+  console.log('HTTP',r.status); console.log(JSON.stringify(await r.json(),null,2));
+  fs.writeFileSync('/tmp/gmc-titlefix-canary-offer.txt', row.offerId);
+})();
diff --git a/step-b-test-one.js.bak2 b/step-b-test-one.js.bak2
new file mode 100644
index 0000000..bf6f1c4
--- /dev/null
+++ b/step-b-test-one.js.bak2
@@ -0,0 +1,19 @@
+// GATED WRITE — pushes ONE title override. Usage: node step-b-test-one.js <dataSourceName>
+const {token,MERCHANT}=require('./_auth');
+const fs=require('fs');
+const DS=process.argv[2]; // e.g. accounts/146735262/dataSources/1234567890
+if(!DS){ console.error('Usage: node step-b-test-one.js <dataSourceName from step A>'); process.exit(1); }
+const list=require('./gmc_titlefix_list.json');
+const row=list[0]; // first risky offer as the canary
+(async()=>{
+  const tok=await token();
+  const url=`https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(DS)}`;
+  const body={ offerId:row.offerId, contentLanguage:'en', feedLabel:'US',
+    attributes:{ title: row.proposedTitle } };
+  console.log('TEST offer:',row.offerId);
+  console.log('  current :',row.currentTitle);
+  console.log('  proposed:',row.proposedTitle);
+  const r=await fetch(url,{method:'POST',headers:{Authorization:'Bearer '+tok,'Content-Type':'application/json'},body:JSON.stringify(body)});
+  console.log('HTTP',r.status); console.log(JSON.stringify(await r.json(),null,2));
+  fs.writeFileSync('/tmp/gmc-titlefix-canary-offer.txt', row.offerId);
+})();
diff --git a/step-d-scale-all.js b/step-d-scale-all.js
index 982bfec..dfffb95 100644
--- a/step-d-scale-all.js
+++ b/step-d-scale-all.js
@@ -17,8 +17,8 @@ const sleep=ms=>new Promise(r=>setTimeout(r,ms));
   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/v1beta/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(DS)}`;
-    const body={ offerId:row.offerId, contentLanguage:'en', feedLabel:'US', attributes:{ title: row.proposedTitle } };
+    const url=`https://merchantapi.googleapis.com/products/v1/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(DS)}`;
+    const body={ offerId:row.offerId, contentLanguage:'en', feedLabel:'US', productAttributes:{ title: row.proposedTitle } };
     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++; if(fail<=10) console.error('FAIL',row.offerId,r.status,(await r.text()).slice(0,120)); }
     if(i%500===0) console.log(`  ${i}/${list.length} | ok ${ok} fail ${fail}`);
diff --git a/step-d-scale-all.js.bak b/step-d-scale-all.js.bak
new file mode 100644
index 0000000..982bfec
--- /dev/null
+++ b/step-d-scale-all.js.bak
@@ -0,0 +1,28 @@
+// GATED WRITE — pushes title overrides for ALL risky offers. Usage: node step-d-scale-all.js <dataSourceName>
+const {token,MERCHANT}=require('./_auth');
+const fs=require('fs');
+const DS=process.argv[2];
+if(!DS){ console.error('Usage: node step-d-scale-all.js <dataSourceName from step A>'); process.exit(1); }
+// FAIL-CLOSED GATE (vp-compliance-policy ask): refuse to scale unless step-c verify PASSED.
+// Override only deliberately: SKIP_VERIFY_GATE=1 node step-d-scale-all.js <DS>
+if(!process.env.SKIP_VERIFY_GATE && !fs.existsSync('/tmp/gmc-titlefix-verify-pass.txt')){
+  console.error('BLOCKED: step-c verify has not PASSED (no /tmp/gmc-titlefix-verify-pass.txt).');
+  console.error('Run step-b then step-c and confirm the canary title flipped before scaling 29,672 offers.');
+  process.exit(2);
+}
+const list=require('./gmc_titlefix_list.json');
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+(async()=>{
+  let tok=await token(), tokAt=Date.now(), ok=0, fail=0;
+  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/v1beta/accounts/${MERCHANT}/productInputs:insert?dataSource=${encodeURIComponent(DS)}`;
+    const body={ offerId:row.offerId, contentLanguage:'en', feedLabel:'US', attributes:{ title: row.proposedTitle } };
+    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++; if(fail<=10) console.error('FAIL',row.offerId,r.status,(await r.text()).slice(0,120)); }
+    if(i%500===0) console.log(`  ${i}/${list.length} | ok ${ok} fail ${fail}`);
+    if(r.status===429){ await sleep(3000); }
+  }
+  console.log(`DONE: ok ${ok} fail ${fail} of ${list.length}`);
+})();
diff --git a/step-d-scale-all.js.bak2 b/step-d-scale-all.js.bak2
new file mode 100644
index 0000000..d41544c
--- /dev/null
+++ b/step-d-scale-all.js.bak2
@@ -0,0 +1,28 @@
+// GATED WRITE — pushes title overrides for ALL risky offers. Usage: node step-d-scale-all.js <dataSourceName>
+const {token,MERCHANT}=require('./_auth');
+const fs=require('fs');
+const DS=process.argv[2];
+if(!DS){ console.error('Usage: node step-d-scale-all.js <dataSourceName from step A>'); process.exit(1); }
+// FAIL-CLOSED GATE (vp-compliance-policy ask): refuse to scale unless step-c verify PASSED.
+// Override only deliberately: SKIP_VERIFY_GATE=1 node step-d-scale-all.js <DS>
+if(!process.env.SKIP_VERIFY_GATE && !fs.existsSync('/tmp/gmc-titlefix-verify-pass.txt')){
+  console.error('BLOCKED: step-c verify has not PASSED (no /tmp/gmc-titlefix-verify-pass.txt).');
+  console.error('Run step-b then step-c and confirm the canary title flipped before scaling 29,672 offers.');
+  process.exit(2);
+}
+const list=require('./gmc_titlefix_list.json');
+const sleep=ms=>new Promise(r=>setTimeout(r,ms));
+(async()=>{
+  let tok=await token(), tokAt=Date.now(), ok=0, fail=0;
+  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:'en', feedLabel:'US', attributes:{ title: row.proposedTitle } };
+    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++; if(fail<=10) console.error('FAIL',row.offerId,r.status,(await r.text()).slice(0,120)); }
+    if(i%500===0) console.log(`  ${i}/${list.length} | ok ${ok} fail ${fail}`);
+    if(r.status===429){ await sleep(3000); }
+  }
+  console.log(`DONE: ok ${ok} fail ${fail} of ${list.length}`);
+})();

← 3a48449 Fail-closed verify gate: step-c writes PASS marker, step-d b  ·  back to Gmc Titlefix  ·  step-d: add LIMIT env knob for staged batches (verify-gate p 3ac8233 →