[object Object]

← back to Gmc Titlefix

gmc-titlefix: migrate remaining v2.1 read tools to _mc-read-v1.js shim + flag spent one-offs (TK10993)

f78c7a1e61d85b75528b4b8c373e7a67786eb07f · 2026-09-10 00:15:48 -0700 · Steve

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQ7X5JUdRkJVZ7HbNtzw4e

Files touched

Diff

commit f78c7a1e61d85b75528b4b8c373e7a67786eb07f
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Sep 10 00:15:48 2026 -0700

    gmc-titlefix: migrate remaining v2.1 read tools to _mc-read-v1.js shim + flag spent one-offs (TK10993)
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01NQ7X5JUdRkJVZ7HbNtzw4e
---
 build-fresh-canary.mjs           | 17 ++++++++---------
 diag2.js                         |  2 ++
 enumerate-genuine-rollleaks.mjs  |  2 ++
 gmc-dwla-peryard-primary.js      | 15 ++++++++-------
 gmc-price-override.js            | 20 ++++++++------------
 primary-fix-batch.mjs            | 16 +++++++++++-----
 primary-fix-canary.mjs           | 18 +++++++++++-------
 spotcheck500.js                  |  2 ++
 step-c-verify-one.js             |  2 ++
 step-e-verify-all.js             |  2 ++
 step-e-verify-retry.js           |  2 ++
 tk10993-disapproval-snapshot.mjs |  2 ++
 tk10993-track-d-executor.mjs     |  2 ++
 tk10993-track-g-enumerate.mjs    |  2 ++
 14 files changed, 64 insertions(+), 40 deletions(-)

diff --git a/build-fresh-canary.mjs b/build-fresh-canary.mjs
index 38a7578..fec0afb 100644
--- a/build-fresh-canary.mjs
+++ b/build-fresh-canary.mjs
@@ -6,6 +6,7 @@ import fs from 'fs';
 import { createRequire } from 'module';
 const require = createRequire(import.meta.url);
 const { token, MERCHANT } = require('./_auth.js');
+const mc = require('./_mc-read-v1.js'); // Merchant API v1 read shim (Content API v2.1 sunset 2026-08-18)
 
 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';
@@ -31,15 +32,13 @@ function parseCsv(line){ const out=[];let cur='',q=false;for(let i=0;i<line.leng
   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);
+  const offers = [];
+  // Merchant API v1 paginated list via shim (replaces v2.1 products?maxResults=250 loop).
+  // feedLabel/contentLanguage live in the raw v1 body (_v1), not the v2.1-compat top level.
+  await mc.listProducts(async (p) => {
+    offers.push({ offerId: p.offerId, price: parseFloat(p.price?.value || '0'), feedLabel: p._v1?.feedLabel || 'US', contentLanguage: p._v1?.contentLanguage || 'en', title: (p.title || '').slice(0, 45) });
+    if (offers.length % 10000 === 0) process.stderr.write(`  ...${offers.length} offers read\n`);
+  });
   return offers;
 }
 
diff --git a/diag2.js b/diag2.js
index 60b9e55..654866e 100644
--- a/diag2.js
+++ b/diag2.js
@@ -1,3 +1,5 @@
+// NOTE 2026-09-10: uses Content API v2.1 (sunset 2026-08-18). Spent one-off; if re-running,
+// migrate reads to _mc-read-v1.js (see verify-canary.mjs for the pattern).
 const {token,MERCHANT}=require('./_auth');
 const offerId=require('fs').readFileSync('/tmp/gmc-titlefix-canary-offer.txt','utf8').trim();
 (async()=>{
diff --git a/enumerate-genuine-rollleaks.mjs b/enumerate-genuine-rollleaks.mjs
index ccebfbe..3021d7b 100644
--- a/enumerate-genuine-rollleaks.mjs
+++ b/enumerate-genuine-rollleaks.mjs
@@ -1,3 +1,5 @@
+// NOTE 2026-09-10: uses Content API v2.1 (sunset 2026-08-18). Spent one-off; if re-running,
+// migrate reads to _mc-read-v1.js (see verify-canary.mjs for the pattern).
 // READ-ONLY: enumerate GENUINE roll-variant leaks in the live GMC feed.
 // A leak candidate = offer served price <= 4.26. EXCLUDE sample offers
 // (title starts "Sample:" OR offerId/title signals sample). Classify variant, not pid.
diff --git a/gmc-dwla-peryard-primary.js b/gmc-dwla-peryard-primary.js
index 375e58e..657d9d2 100644
--- a/gmc-dwla-peryard-primary.js
+++ b/gmc-dwla-peryard-primary.js
@@ -27,6 +27,7 @@
 //
 // Cost: $0 (Google Merchant API + local feed read; no per-call charge).
 const { token, MERCHANT } = require('./_auth.js');
+const mc = require('./_mc-read-v1.js'); // Merchant API v1 read shim (Content API v2.1 sunset 2026-08-18)
 const fs = require('fs');
 
 const FEED_TSV = '/Users/macstudio3/Projects/designerwallcoverings/today-viewer/data/google-merchant-feed.tsv';
@@ -144,14 +145,14 @@ async function insertOffers(ds, offers, label) {
 
 // ---- 4) Verify offers are ingested (read back from MC) -----------------------
 async function verify(ds, sample) {
-  const tok = await token(); const H = { Authorization: 'Bearer ' + tok };
-  console.log(`Verifying ${sample.length} per-yard offers on live MC (Content API products.get)...`);
+  console.log(`Verifying ${sample.length} per-yard offers on live MC (Merchant API v1 products.get via shim)...`);
   for (const o of sample) {
-    const pid = `online:en:US:${o.offerId}`;
-    const r = await fetch(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/products/${encodeURIComponent(pid)}`, { headers: H });
-    const j = await r.json();
-    if (j.error) { console.log(`  ${o.offerId}  NOT-FOUND-YET (${j.error.code}) — expected within Google's ~24h merge`); continue; }
-    console.log(`  ${o.offerId}  price=${j.price?.value} ${j.price?.currency} | title="${(j.title || '').slice(0, 45)}" | shippingWeight=${JSON.stringify(j.shippingWeight || null)}`);
+    const rid = `online:en:US:${o.offerId}`;
+    let p;
+    try { p = await mc.getProduct(rid); } // v2.1-COMPAT shape; throws on non-200 (e.g. not-merged-yet)
+    catch (e) { console.log(`  ${o.offerId}  NOT-FOUND-YET (${e.status}) — expected within Google's ~24h merge`); continue; }
+    const sw = p._v1?.productAttributes?.shippingWeight; // shippingWeight only via _v1 raw body
+    console.log(`  ${o.offerId}  price=${p.price?.value} ${p.price?.currency} | title="${(p.title || '').slice(0, 45)}" | shippingWeight=${JSON.stringify(sw || null)}`);
   }
 }
 
diff --git a/gmc-price-override.js b/gmc-price-override.js
index fa07c74..8d7f286 100644
--- a/gmc-price-override.js
+++ b/gmc-price-override.js
@@ -13,6 +13,7 @@
 //
 // Cost: $0 (Google Content/Merchant API + local feed read; no per-call charge).
 const { token, MERCHANT } = require('./_auth.js');
+const mc = require('./_mc-read-v1.js'); // Merchant API v1 read shim (Content API v2.1 sunset 2026-08-18)
 const fs = require('fs');
 const { execFileSync } = require('child_process');
 
@@ -122,18 +123,13 @@ async function dryRunDwlaPidJoin() {
 // --- end DWLA pid-join path -------------------------------------------------------------------
 
 async function listMcOffers() {
-  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 list err:', JSON.stringify(r.error).slice(0, 200)); break; }
-    for (const p of (r.resources || [])) {
-      scanned++;
-      offers.push({ offerId: p.offerId, mpn: p.mpn || '', price: parseFloat(p.price?.value || '0'), feedLabel: p.feedLabel || 'US', contentLanguage: p.contentLanguage || 'en', title: (p.title || '').slice(0, 50) });
-    }
-    page = r.nextPageToken;
-    if (scanned % 5000 === 0) process.stderr.write(`  ...${scanned} MC offers read\n`);
-  } while (page);
+  const offers = [];
+  // Merchant API v1 paginated list via shim (replaces v2.1 products?maxResults=250 loop).
+  // mpn/feedLabel/contentLanguage live in the raw v1 body (_v1), not the v2.1-compat top level.
+  await mc.listProducts(async (p) => {
+    offers.push({ offerId: p.offerId, mpn: p._v1?.productAttributes?.mpn || '', price: parseFloat(p.price?.value || '0'), feedLabel: p._v1?.feedLabel || 'US', contentLanguage: p._v1?.contentLanguage || 'en', title: (p.title || '').slice(0, 50) });
+    if (offers.length % 5000 === 0) process.stderr.write(`  ...${offers.length} MC offers read\n`);
+  });
   return offers;
 }
 
diff --git a/primary-fix-batch.mjs b/primary-fix-batch.mjs
index 02fb0cc..8df5a5d 100644
--- a/primary-fix-batch.mjs
+++ b/primary-fix-batch.mjs
@@ -38,6 +38,7 @@ import crypto from 'crypto';
 import { createRequire } from 'module';
 const require = createRequire(import.meta.url);
 const { token, MERCHANT } = require('./_auth.js');
+const mc = require('./_mc-read-v1.js'); // Merchant API v1 read shim (Content API v2.1 sunset 2026-08-18)
 
 const HOME = process.env.HOME;
 const PRIMARY_DS = `accounts/${MERCHANT}/dataSources/180695450`;
@@ -87,10 +88,16 @@ async function getProcessed(tok, offerId) {
   return { status: 429, body: {} };
 }
 
-async function getServedV21(tok, offerId) {
+// Read back the served product via the Merchant API v1 shim (v2.1-COMPAT shape). Throws on
+// non-200 -> mapped to {error:{code}} for the caller. `link` lives in the raw v1 body (_v1).
+async function getServed(offerId) {
   const rid = `online:en:US:${offerId}`;
-  const r = await fetch(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/products/${encodeURIComponent(rid)}`, { headers: { Authorization: 'Bearer ' + tok } });
-  return r.json();
+  try {
+    const p = await mc.getProduct(rid);
+    return { ...p, link: p._v1?.productAttributes?.link }; // link only via _v1 raw body
+  } catch (e) {
+    return { error: { code: e.status, message: e.message } };
+  }
 }
 
 async function insertPrimary(tok, body) {
@@ -165,12 +172,11 @@ async function doVerify(n) {
   const step = Math.max(1, Math.floor(landed.length / n));
   const sample = [];
   for (let i = 0; i < landed.length && sample.length < n; i += step) sample.push(landed[i]);
-  const tok = await token();
   console.log(`=== VERIFY served price for ${sample.length} landed offers (spread across ${new Set(landed.map(x=>x.vendor)).size} vendors) ===`);
   const rows = [];
   let stillSample = 0;
   for (const o of sample) {
-    const pr = await getServedV21(tok, o.offerId);
+    const pr = await getServed(o.offerId);
     const served = pr.error ? `ERR ${pr.error.code}` : (pr.price ? parseFloat(pr.price.value) : null);
     const link = pr.link || '';
     const linkOk = /[?&]variant=/.test(link) ? '✓' : '—';
diff --git a/primary-fix-canary.mjs b/primary-fix-canary.mjs
index e60daef..7068560 100644
--- a/primary-fix-canary.mjs
+++ b/primary-fix-canary.mjs
@@ -21,6 +21,7 @@ import fs from 'fs';
 import { createRequire } from 'module';
 const require = createRequire(import.meta.url);
 const { token, MERCHANT } = require('./_auth.js');
+const mc = require('./_mc-read-v1.js'); // Merchant API v1 read shim (Content API v2.1 sunset 2026-08-18)
 
 const PRIMARY_DS = `accounts/${MERCHANT}/dataSources/180695450`;
 const ROLLBACK_DIR = new URL('./data/primary-fix-rollback/', import.meta.url).pathname;
@@ -55,11 +56,15 @@ async function getProcessed(tok, offerId) {
   return { status: r.status, body: await r.json() };
 }
 
-async function getServedV21(tok, offerId) {
+// Read back the served product via the Merchant API v1 shim (returns v2.1-COMPAT shape;
+// throws on non-200, so a removed offer surfaces as {error:{code}} for the caller below).
+async function getServed(offerId) {
   const rid = `online:en:US:${offerId}`;
-  const r = await fetch(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/products/${encodeURIComponent(rid)}`,
-    { headers: { Authorization: 'Bearer ' + tok } });
-  return r.json();
+  try {
+    return await mc.getProduct(rid); // { price:{value,currency}, title, destinationStatuses, itemLevelIssues, _v1 }
+  } catch (e) {
+    return { error: { code: e.status, message: e.message } };
+  }
 }
 
 // Build the FULL roll-variant productAttributes from the current (sample) processed offer + Shopify roll variant.
@@ -153,11 +158,10 @@ async function main() {
 }
 
 if (has('--verify')) {
-  const tok = await token();
-  console.log('=== VERIFY served price (Content API v2.1 products.get) ===');
+  console.log('=== VERIFY served price (Merchant API v1 products.get via shim) ===');
   for (const c of CANARY) {
     const offerId = `shopify_US_${c.pid}_${c.sample}`;
-    const pr = await getServedV21(tok, offerId);
+    const pr = await getServed(offerId);
     if (pr.error) { console.log(`  ${c.name} ${offerId}: ${pr.error.code} ${(pr.error.message || '').slice(0, 40)}`); continue; }
     console.log(`  ${c.name} ${offerId}: SERVED $${pr.price?.value} title="${(pr.title || '').slice(0, 45)}"`);
   }
diff --git a/spotcheck500.js b/spotcheck500.js
index 1516dc1..90b3409 100644
--- a/spotcheck500.js
+++ b/spotcheck500.js
@@ -1,3 +1,5 @@
+// NOTE 2026-09-10: uses Content API v2.1 (sunset 2026-08-18). Spent one-off; if re-running,
+// migrate reads to _mc-read-v1.js (see verify-canary.mjs for the pattern).
 const {token,MERCHANT}=require('./_auth');
 const list=require('./gmc_titlefix_list.json');
 // sample 8 offers from within the 500 batch (rows 1..499), spread out
diff --git a/step-c-verify-one.js b/step-c-verify-one.js
index f8a7172..964a48e 100644
--- a/step-c-verify-one.js
+++ b/step-c-verify-one.js
@@ -1,3 +1,5 @@
+// NOTE 2026-09-10: uses Content API v2.1 (sunset 2026-08-18). Spent one-off; if re-running,
+// migrate reads to _mc-read-v1.js (see verify-canary.mjs for the pattern).
 // READ-ONLY verify — re-pulls the canary offer; confirms effective title == "Sample: ...".
 const {token,MERCHANT}=require('./_auth');
 const fs=require('fs');
diff --git a/step-e-verify-all.js b/step-e-verify-all.js
index 761d8c9..83e3f9d 100644
--- a/step-e-verify-all.js
+++ b/step-e-verify-all.js
@@ -1,3 +1,5 @@
+// NOTE 2026-09-10: uses Content API v2.1 (sunset 2026-08-18). Spent one-off; if re-running,
+// migrate reads to _mc-read-v1.js (see verify-canary.mjs for the pattern).
 // STEP 2 — READ-ONLY full verify. Pages the entire live Content feed once, then
 // confirms each of the 29,672 risky offers now has an effective title starting "Sample:".
 // Writes /tmp/gmc-verify-result.json + a human summary. Safe to re-run anytime.
diff --git a/step-e-verify-retry.js b/step-e-verify-retry.js
index f9e0cfe..6209689 100644
--- a/step-e-verify-retry.js
+++ b/step-e-verify-retry.js
@@ -1,3 +1,5 @@
+// NOTE 2026-09-10: uses Content API v2.1 (sunset 2026-08-18). Spent one-off; if re-running,
+// migrate reads to _mc-read-v1.js (see verify-canary.mjs for the pattern).
 // DTD-B (2026-06-18, 3/3): page the live processed feed, find which of our 29,672
 // offers did NOT flip to a "Sample:" title (transient-500 fails + reprocess-lag),
 // and re-push ONLY those to the canonical supplemental DS. Doubles as the verify pass.
diff --git a/tk10993-disapproval-snapshot.mjs b/tk10993-disapproval-snapshot.mjs
index 01d7e0a..b8fdddc 100644
--- a/tk10993-disapproval-snapshot.mjs
+++ b/tk10993-disapproval-snapshot.mjs
@@ -1,3 +1,5 @@
+// NOTE 2026-09-10: uses Content API v2.1 (sunset 2026-08-18). Spent one-off; if re-running,
+// migrate reads to _mc-read-v1.js (see verify-canary.mjs for the pattern).
 // TK-10993 — READ-ONLY US disapproval snapshot by issue code, straight from productstatuses.
 // Used to measure Track F's effect. Writes nothing anywhere; only GETs Merchant Center.
 import { createRequire } from 'module'; import fs from 'fs';
diff --git a/tk10993-track-d-executor.mjs b/tk10993-track-d-executor.mjs
index 61e5c93..cf4dd3e 100644
--- a/tk10993-track-d-executor.mjs
+++ b/tk10993-track-d-executor.mjs
@@ -1,4 +1,6 @@
 #!/usr/bin/env node
+// NOTE 2026-09-10: uses Content API v2.1 (sunset 2026-08-18). Spent one-off; if re-running,
+// migrate reads to _mc-read-v1.js (see verify-canary.mjs for the pattern).
 import fs from 'fs';
 import crypto from 'crypto';
 
diff --git a/tk10993-track-g-enumerate.mjs b/tk10993-track-g-enumerate.mjs
index 3862df7..3ed3839 100644
--- a/tk10993-track-g-enumerate.mjs
+++ b/tk10993-track-g-enumerate.mjs
@@ -1,3 +1,5 @@
+// NOTE 2026-09-10: uses Content API v2.1 (sunset 2026-08-18). Spent one-off; if re-running,
+// migrate reads to _mc-read-v1.js (see verify-canary.mjs for the pattern).
 // TK-10993 Track G — enumerate the US policy-violation disapprovals so the
 // false-positive claim can be checked per-offer instead of assumed. READ-ONLY.
 import { createRequire } from 'module'; import fs from 'fs';

← 3628b9f gmc-titlefix: add Merchant API v1 read shim + migrate verify  ·  back to Gmc Titlefix  ·  auto-data-snapshot: 2026-09-10T00:28:57 (1 data files) — dat be34cf5 →