← 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
A diag-titlefix.jsA diag2.jsA diag3.jsA poll-canary.shA review-viewer.mjsM step-a-create-datasource.jsA step-a-create-datasource.js.bakM step-b-test-one.jsA step-b-test-one.js.bakA step-b-test-one.js.bak2M step-d-scale-all.jsA step-d-scale-all.js.bakA step-d-scale-all.js.bak2
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=>({'&':'&','<':'<','>':'>'}[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 →