← back to Gmc Titlefix
Harden GMC Track C preflight and rollback
279280894c8190e06cd810a10b215c8b7a503130 · 2026-08-31 02:08:55 -0700 · Steve
Files touched
A data/track-c-canary-baseline-attempt.jsonA data/track-c-target-reconciliation.jsonM link-price-override-ds-ROLLBACK.mjsM link-price-override-ds.mjsA reconcile-track-c-targets.mjsA test/track-c-safety.test.mjsA track-c-safety.mjsM verify-canary.mjs
Diff
commit 279280894c8190e06cd810a10b215c8b7a503130
Author: Steve <steve@designerwallcoverings.com>
Date: Mon Aug 31 02:08:55 2026 -0700
Harden GMC Track C preflight and rollback
---
data/track-c-canary-baseline-attempt.json | 60 +++++++++++++++++++
data/track-c-target-reconciliation.json | 40 +++++++++++++
link-price-override-ds-ROLLBACK.mjs | 19 +++---
link-price-override-ds.mjs | 14 +++--
reconcile-track-c-targets.mjs | 51 ++++++++++++++++
test/track-c-safety.test.mjs | 46 +++++++++++++++
track-c-safety.mjs | 77 +++++++++++++++++++++++++
verify-canary.mjs | 96 ++++++++++++++++++++++---------
8 files changed, 363 insertions(+), 40 deletions(-)
diff --git a/data/track-c-canary-baseline-attempt.json b/data/track-c-canary-baseline-attempt.json
new file mode 100644
index 0000000..d37a8bd
--- /dev/null
+++ b/data/track-c-canary-baseline-attempt.json
@@ -0,0 +1,60 @@
+{
+ "generated_at": "2026-08-31T09:04:52.333Z",
+ "total": 400,
+ "approved": 385,
+ "disapproved": 3,
+ "other": 0,
+ "flipped": 350,
+ "still425": 38,
+ "readErrors": 12,
+ "errors": [
+ {
+ "offerId": "shopify_US_7421471817779_42654206787635",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/products/online%3Aen%3AUS%3Ashopify_US_7421471817779_42654206787635"
+ },
+ {
+ "offerId": "shopify_US_7810571173939_44170603298867",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/products/online%3Aen%3AUS%3Ashopify_US_7810571173939_44170603298867"
+ },
+ {
+ "offerId": "shopify_US_6811234533427_40875215519795",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/products/online%3Aen%3AUS%3Ashopify_US_6811234533427_40875215519795"
+ },
+ {
+ "offerId": "shopify_US_7810436300851_44170214211635",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/products/online%3Aen%3AUS%3Ashopify_US_7810436300851_44170214211635"
+ },
+ {
+ "offerId": "shopify_US_7810578677811_44170611327027",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/products/online%3Aen%3AUS%3Ashopify_US_7810578677811_44170611327027"
+ },
+ {
+ "offerId": "shopify_US_7506089508915_42949341741107",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/productstatuses/online%3Aen%3AUS%3Ashopify_US_7506089508915_42949341741107"
+ },
+ {
+ "offerId": "shopify_US_7552701497395_43243527340083",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/products/online%3Aen%3AUS%3Ashopify_US_7552701497395_43243527340083"
+ },
+ {
+ "offerId": "shopify_US_7822321745971_44223552225331",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/productstatuses/online%3Aen%3AUS%3Ashopify_US_7822321745971_44223552225331"
+ },
+ {
+ "offerId": "shopify_US_7552741834803_43243618959411",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/products/online%3Aen%3AUS%3Ashopify_US_7552741834803_43243618959411"
+ },
+ {
+ "offerId": "shopify_US_7855821946931_44417989279795",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/productstatuses/online%3Aen%3AUS%3Ashopify_US_7855821946931_44417989279795"
+ },
+ {
+ "offerId": "shopify_US_7810545025075_44170472357939",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/productstatuses/online%3Aen%3AUS%3Ashopify_US_7810545025075_44170472357939"
+ },
+ {
+ "offerId": "shopify_US_7430004637747_42683857797171",
+ "error": "HTTP 404 https://shoppingcontent.googleapis.com/content/v2.1/146735262/products/online%3Aen%3AUS%3Ashopify_US_7430004637747_42683857797171"
+ }
+ ]
+}
diff --git a/data/track-c-target-reconciliation.json b/data/track-c-target-reconciliation.json
new file mode 100644
index 0000000..d6bce4a
--- /dev/null
+++ b/data/track-c-target-reconciliation.json
@@ -0,0 +1,40 @@
+{
+ "schema": 1,
+ "generated_at": "2026-08-31T09:08:11.113Z",
+ "datasource": "accounts/146735262/dataSources/10693978453",
+ "exact_current_target_count": 10352,
+ "datasource_seed_evidence": {
+ "source": "/Users/macstudio3/.claude/yolo-queue/gmc-canary-apply-result.json",
+ "successful_inserts": 388,
+ "failed_inserts": 12,
+ "exact_successful_offer_ids_recorded": false
+ },
+ "known_datasource_canary_count": 400,
+ "artifact_intersection_count": 400,
+ "proven_datasource_intersection_count": null,
+ "proven_current_served_intersection_count": 388,
+ "current_unreadable_offer_ids": [
+ "shopify_US_6811234533427_40875215519795",
+ "shopify_US_7421471817779_42654206787635",
+ "shopify_US_7430004637747_42683857797171",
+ "shopify_US_7506089508915_42949341741107",
+ "shopify_US_7552701497395_43243527340083",
+ "shopify_US_7552741834803_43243618959411",
+ "shopify_US_7810436300851_44170214211635",
+ "shopify_US_7810545025075_44170472357939",
+ "shopify_US_7810571173939_44170603298867",
+ "shopify_US_7810578677811_44170611327027",
+ "shopify_US_7822321745971_44223552225331",
+ "shopify_US_7855821946931_44417989279795"
+ ],
+ "target_outside_current_full": 0,
+ "full_target_duplicates": 0,
+ "canary_duplicates": 0,
+ "invalid_target_rows": 0,
+ "artifact_clean": true,
+ "datasource_membership_exact": false,
+ "immutable_baseline_captured": false,
+ "ready_to_link": false,
+ "historical_466_claim": "obsolete served-leak count from a different observation; not the current target or datasource row count",
+ "interpretation": "10352 is the current exact candidate set and all 400 canary IDs are a clean subset. The retained apply result records 388 successes and 12 failures without IDs. A fresh read-only baseline attempt independently found 12 unreadable IDs, proving 388 currently served canary rows and identifying the missing IDs. Because a complete immutable baseline cannot be captured, Track C remains HOLD. The historical 466 must not be used as blast radius."
+}
diff --git a/link-price-override-ds-ROLLBACK.mjs b/link-price-override-ds-ROLLBACK.mjs
index 9af0ed6..c025ffa 100644
--- a/link-price-override-ds-ROLLBACK.mjs
+++ b/link-price-override-ds-ROLLBACK.mjs
@@ -1,22 +1,27 @@
// ROLLBACK for link-price-override-ds.mjs — restores primary feed 180695450's
-// defaultRule to the EXACT pre-change snapshot saved at /tmp/gmc-primary-180695450-CURRENT.json.
-// Dry-run by default; --apply --yes-i-am-steve to fire.
-import fs from 'fs';
+// defaultRule to an immutable, SHA-256-verified pre-change snapshot.
+// Dry-run by default; requires --manifest plus --apply --yes-i-am-steve to fire.
import { createRequire } from 'module';
+import { loadValidatedSnapshot } from './track-c-safety.mjs';
const require = createRequire(import.meta.url);
const { token, MERCHANT } = require('./_auth.js');
const PRIMARY = '180695450';
-const SNAP = '/tmp/gmc-primary-180695450-CURRENT.json';
const args = new Set(process.argv.slice(2));
const ARMED = args.has('--apply') && args.has('--yes-i-am-steve');
+const manifestFlag = process.argv.indexOf('--manifest');
+const manifestPath = manifestFlag >= 0 ? process.argv[manifestFlag + 1] : '';
-if (!fs.existsSync(SNAP)) { console.error('No snapshot at', SNAP, '— cannot roll back safely. Abort.'); process.exit(1); }
-const snap = JSON.parse(fs.readFileSync(SNAP, 'utf8'));
+if (!manifestPath) { console.error('Missing --manifest <immutable-manifest.json> — abort.'); process.exit(1); }
+let loaded;
+try { loaded = loadValidatedSnapshot(manifestPath); }
+catch (e) { console.error('Snapshot validation failed:', e.message); process.exit(1); }
+const { snapshot: snap, manifest } = loaded;
const rule = snap.primaryProductDataSource?.defaultRule?.takeFromDataSources || [];
+console.log('Validated snapshot SHA-256:', manifest.snapshot_sha256);
console.log('Will RESTORE takeFromDataSources to:', rule.map(x => x.self ? 'SELF' : x.supplementalDataSourceName.split('/').pop()).join(' > '));
-if (!ARMED) { console.log('\nDRY-RUN. To apply: node ~/Projects/gmc-titlefix/link-price-override-ds-ROLLBACK.mjs --apply --yes-i-am-steve'); process.exit(0); }
+if (!ARMED) { console.log(`\nDRY-RUN. To apply: node ~/Projects/gmc-titlefix/link-price-override-ds-ROLLBACK.mjs --manifest ${manifestPath} --apply --yes-i-am-steve`); process.exit(0); }
const tok = await token(); const H = { Authorization: 'Bearer ' + tok, 'Content-Type': 'application/json' };
const base = `https://merchantapi.googleapis.com/datasources/v1/accounts/${MERCHANT}/dataSources/${PRIMARY}`;
diff --git a/link-price-override-ds.mjs b/link-price-override-ds.mjs
index 252489a..9590a09 100644
--- a/link-price-override-ds.mjs
+++ b/link-price-override-ds.mjs
@@ -11,9 +11,9 @@
// This is a CUSTOMER-FACING GMC FEED STRUCTURE CHANGE (changes what price wins for the
// whole catalog on the live Google feed) => GATED. Dry-run by default; requires
// --apply --yes-i-am-steve to fire. Reversible via link-price-override-ds-ROLLBACK.mjs
-// (restores the exact pre-change rule saved to /tmp/gmc-primary-180695450-CURRENT.json).
-import fs from 'fs';
+// (restores the exact immutable pre-change snapshot written only by an armed run).
import { createRequire } from 'module';
+import { writeImmutableSnapshot } from './track-c-safety.mjs';
const require = createRequire(import.meta.url);
const { token, MERCHANT } = require('./_auth.js');
@@ -30,9 +30,6 @@ const rule = cur.primaryProductDataSource?.defaultRule?.takeFromDataSources || [
const ids = rule.map(x => x.self ? 'SELF' : x.supplementalDataSourceName.split('/').pop());
console.log('CURRENT takeFromDataSources:', ids.join(' > '));
-// Save an exact rollback snapshot every run.
-fs.writeFileSync('/tmp/gmc-primary-180695450-CURRENT.json', JSON.stringify(cur, null, 2));
-
if (rule.some(x => x.supplementalDataSourceName === PRICE_DS)) {
console.log('ALREADY LINKED — price-override DS 10693978453 is present. No change needed.');
process.exit(0);
@@ -55,10 +52,15 @@ const body = {
if (!ARMED) {
console.log('\nDRY-RUN (not armed). To apply the live feed-link change:');
console.log(' node ~/Projects/gmc-titlefix/link-price-override-ds.mjs --apply --yes-i-am-steve');
- console.log('Rollback after apply: node ~/Projects/gmc-titlefix/link-price-override-ds-ROLLBACK.mjs --apply --yes-i-am-steve');
+ console.log('An armed run writes an immutable manifest; rollback requires that exact --manifest path.');
process.exit(0);
}
+const saved = writeImmutableSnapshot(cur, new URL('./data/track-c-snapshots', import.meta.url));
+console.log('IMMUTABLE PRE-CHANGE SNAPSHOT:', saved.snapshotPath);
+console.log('ROLLBACK MANIFEST:', saved.manifestPath);
+console.log('SNAPSHOT SHA-256:', saved.manifest.snapshot_sha256);
+
const url = `${base}?updateMask=primaryProductDataSource.defaultRule`;
const r = await fetch(url, { method: 'PATCH', headers: H, body: JSON.stringify(body) });
const j = await r.json();
diff --git a/reconcile-track-c-targets.mjs b/reconcile-track-c-targets.mjs
new file mode 100644
index 0000000..27cba8e
--- /dev/null
+++ b/reconcile-track-c-targets.mjs
@@ -0,0 +1,51 @@
+// Local-only evidence builder. Reconciles the current full target artifact, the
+// applied supplemental canary artifact, and the historical "466" memo claim.
+import fs from 'fs';
+const FULL = '/Users/macstudio3/.claude/yolo-queue/gmc-fresh-override-full.json';
+const CANARY = '/Users/macstudio3/.claude/yolo-queue/gmc-fresh-override-canary.json';
+const APPLY = '/Users/macstudio3/.claude/yolo-queue/gmc-canary-apply-result.json';
+const ATTEMPT = new URL('./data/track-c-canary-baseline-attempt.json', import.meta.url);
+const OUT = new URL('./data/track-c-target-reconciliation.json', import.meta.url);
+
+const fullDoc = JSON.parse(fs.readFileSync(FULL));
+const canaryDoc = JSON.parse(fs.readFileSync(CANARY));
+const apply = JSON.parse(fs.readFileSync(APPLY));
+const attempt = fs.existsSync(ATTEMPT) ? JSON.parse(fs.readFileSync(ATTEMPT)) : null;
+const full = fullDoc.overrides || [], canary = canaryDoc.overrides || [];
+const fullIds = new Set(full.map(x => x.offerId));
+const canaryIds = new Set(canary.map(x => x.offerId));
+const duplicateFull = full.length - fullIds.size;
+const duplicateCanary = canary.length - canaryIds.size;
+const intersection = [...canaryIds].filter(id => fullIds.has(id));
+const outside = [...canaryIds].filter(id => !fullIds.has(id));
+const invalid = full.filter(x => !x.offerId || x.feedLabel !== 'US' || !(x.realPrice > 4.26));
+const artifactClean = !duplicateFull && !duplicateCanary && !outside.length && !invalid.length;
+const datasourceMembershipExact = apply.fail === 0 && apply.ok === canary.length;
+const failedReadIds = new Set((attempt?.errors || []).map(x => x.offerId));
+const readFailureIdsExact = !!attempt && attempt.readErrors === failedReadIds.size && [...failedReadIds].every(id => canaryIds.has(id));
+const servedIntersectionCount = readFailureIdsExact ? canary.length - failedReadIds.size : null;
+const report = {
+ schema: 1,
+ generated_at: new Date().toISOString(),
+ datasource: canaryDoc.datasource,
+ exact_current_target_count: full.length,
+ datasource_seed_evidence: { source: APPLY, successful_inserts: apply.ok, failed_inserts: apply.fail, exact_successful_offer_ids_recorded: false },
+ known_datasource_canary_count: canary.length,
+ artifact_intersection_count: intersection.length,
+ proven_datasource_intersection_count: datasourceMembershipExact ? intersection.length : null,
+ proven_current_served_intersection_count: servedIntersectionCount,
+ current_unreadable_offer_ids: readFailureIdsExact ? [...failedReadIds].sort() : null,
+ target_outside_current_full: outside.length,
+ full_target_duplicates: duplicateFull,
+ canary_duplicates: duplicateCanary,
+ invalid_target_rows: invalid.length,
+ artifact_clean: artifactClean,
+ datasource_membership_exact: datasourceMembershipExact,
+ immutable_baseline_captured: false,
+ ready_to_link: false,
+ historical_466_claim: 'obsolete served-leak count from a different observation; not the current target or datasource row count',
+ interpretation: `${full.length} is the current exact candidate set and all ${canary.length} canary IDs are a clean subset. The retained apply result records ${apply.ok} successes and ${apply.fail} failures without IDs. A fresh read-only baseline attempt independently found ${attempt?.readErrors ?? 'unknown'} unreadable IDs, proving ${servedIntersectionCount ?? 'unknown'} currently served canary rows and identifying the missing IDs. Because a complete immutable baseline cannot be captured, Track C remains HOLD. The historical 466 must not be used as blast radius.`,
+};
+fs.writeFileSync(OUT, JSON.stringify(report, null, 2) + '\n');
+console.log(JSON.stringify(report, null, 2));
+if (!report.ready_to_link) process.exitCode = 2;
diff --git a/test/track-c-safety.test.mjs b/test/track-c-safety.test.mjs
new file mode 100644
index 0000000..69f0c51
--- /dev/null
+++ b/test/track-c-safety.test.mjs
@@ -0,0 +1,46 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import fs from 'fs';
+import os from 'os';
+import path from 'path';
+import { evaluateCanary, loadValidatedSnapshot, writeImmutableSnapshot } from '../track-c-safety.mjs';
+
+const snapshot = { name: 'accounts/146735262/dataSources/180695450', primaryProductDataSource: { defaultRule: { takeFromDataSources: [{ self: true }] } } };
+
+test('immutable snapshot is hash-bound and cannot be overwritten', () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'track-c-'));
+ const now = new Date('2026-08-31T08:00:00.000Z');
+ const saved = writeImmutableSnapshot(snapshot, dir, now);
+ assert.equal(loadValidatedSnapshot(saved.manifestPath).snapshot.name, snapshot.name);
+ assert.throws(() => writeImmutableSnapshot(snapshot, dir, now), /EEXIST/);
+ fs.appendFileSync(saved.snapshotPath, 'tamper');
+ assert.throws(() => loadValidatedSnapshot(saved.manifestPath), /SHA-256 mismatch/);
+});
+
+test('immutable snapshot accepts a file URL directory', () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'track-c-url-'));
+ const saved = writeImmutableSnapshot(snapshot, new URL(`file://${dir}/`), new Date('2026-08-31T08:01:00.000Z'));
+ assert.equal(fs.existsSync(saved.snapshotPath), true);
+});
+
+test('rollback validation rejects wrong account/feed', () => {
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'track-c-'));
+ assert.throws(() => writeImmutableSnapshot({ ...snapshot, name: 'accounts/9/dataSources/8' }, dir), /identity mismatch/);
+});
+
+test('canary passes only with approval floor and no worsening', () => {
+ const baseline = { total: 100, approved: 94, disapproved: 6, readErrors: 0 };
+ const result = evaluateCanary({ total: 100, approved: 96, disapproved: 4, readErrors: 0 }, baseline);
+ assert.equal(result.pass, true);
+ assert.equal(result.approvalDelta, 2);
+ assert.equal(result.disapprovalDelta, -2);
+});
+
+test('canary fails closed on read error, approval loss, or new disapproval', () => {
+ const baseline = { total: 100, approved: 96, disapproved: 4, readErrors: 0 };
+ const result = evaluateCanary({ total: 100, approved: 94, disapproved: 5, readErrors: 1 }, baseline);
+ assert.equal(result.pass, false);
+ assert.match(result.reasons.join(';'), /read errors/);
+ assert.match(result.reasons.join(';'), /approved fell/);
+ assert.match(result.reasons.join(';'), /new disapprovals/);
+});
diff --git a/track-c-safety.mjs b/track-c-safety.mjs
new file mode 100644
index 0000000..392240c
--- /dev/null
+++ b/track-c-safety.mjs
@@ -0,0 +1,77 @@
+import crypto from 'crypto';
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+export const EXPECTED_MERCHANT = '146735262';
+export const EXPECTED_PRIMARY = '180695450';
+
+export function sha256(data) {
+ return crypto.createHash('sha256').update(data).digest('hex');
+}
+
+export function assertPrimaryIdentity(snapshot, merchant = EXPECTED_MERCHANT, primary = EXPECTED_PRIMARY) {
+ const expected = `accounts/${merchant}/dataSources/${primary}`;
+ if (!snapshot || snapshot.name !== expected) {
+ throw new Error(`snapshot identity mismatch: expected ${expected}, got ${snapshot?.name || '<missing>'}`);
+ }
+ if (!snapshot.primaryProductDataSource?.defaultRule?.takeFromDataSources) {
+ throw new Error('snapshot missing primaryProductDataSource.defaultRule.takeFromDataSources');
+ }
+ return expected;
+}
+
+export function writeImmutableSnapshot(snapshot, dir, now = new Date()) {
+ assertPrimaryIdentity(snapshot);
+ if (dir instanceof URL) dir = fileURLToPath(dir);
+ fs.mkdirSync(dir, { recursive: true });
+ const stamp = now.toISOString().replace(/[:.]/g, '-');
+ const snapshotPath = path.join(dir, `primary-${EXPECTED_PRIMARY}-pre-link-${stamp}.json`);
+ const body = JSON.stringify(snapshot, null, 2) + '\n';
+ fs.writeFileSync(snapshotPath, body, { flag: 'wx', mode: 0o600 });
+ const manifest = {
+ schema: 1,
+ created_at: now.toISOString(),
+ merchant: EXPECTED_MERCHANT,
+ primary_data_source: EXPECTED_PRIMARY,
+ snapshot_path: snapshotPath,
+ snapshot_sha256: sha256(body),
+ };
+ const manifestPath = snapshotPath.replace(/\.json$/, '.manifest.json');
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n', { flag: 'wx', mode: 0o600 });
+ return { snapshotPath, manifestPath, manifest };
+}
+
+export function loadValidatedSnapshot(manifestPath) {
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
+ if (String(manifest.merchant) !== EXPECTED_MERCHANT || String(manifest.primary_data_source) !== EXPECTED_PRIMARY) {
+ throw new Error('rollback manifest merchant/feed mismatch');
+ }
+ const body = fs.readFileSync(manifest.snapshot_path, 'utf8');
+ if (sha256(body) !== manifest.snapshot_sha256) throw new Error('rollback snapshot SHA-256 mismatch');
+ const snapshot = JSON.parse(body);
+ assertPrimaryIdentity(snapshot, manifest.merchant, manifest.primary_data_source);
+ return { manifest, snapshot };
+}
+
+export function evaluateCanary(current, baseline, thresholds = {}) {
+ const minApprovalPct = thresholds.minApprovalPct ?? 95;
+ const maxReadErrors = thresholds.maxReadErrors ?? 0;
+ const n = current.total;
+ if (!Number.isInteger(n) || n <= 0) throw new Error('current total must be positive');
+ if (!baseline || baseline.total !== n) throw new Error('baseline missing or population mismatch');
+ for (const [label, row] of [['current', current], ['baseline', baseline]]) {
+ for (const key of ['approved', 'disapproved', 'readErrors']) {
+ if (!Number.isInteger(row[key]) || row[key] < 0) throw new Error(`${label}.${key} invalid`);
+ }
+ }
+ const approvalPct = current.approved / n * 100;
+ const approvalDelta = current.approved - baseline.approved;
+ const disapprovalDelta = current.disapproved - baseline.disapproved;
+ const reasons = [];
+ if (current.readErrors > maxReadErrors) reasons.push(`read errors ${current.readErrors} > ${maxReadErrors}`);
+ if (approvalPct < minApprovalPct) reasons.push(`approval ${approvalPct.toFixed(1)}% < ${minApprovalPct}%`);
+ if (approvalDelta < 0) reasons.push(`approved fell by ${-approvalDelta}`);
+ if (disapprovalDelta > 0) reasons.push(`new disapprovals +${disapprovalDelta}`);
+ return { pass: reasons.length === 0, approvalPct, approvalDelta, disapprovalDelta, reasons };
+}
diff --git a/verify-canary.mjs b/verify-canary.mjs
index 0d165b0..6ea7e66 100644
--- a/verify-canary.mjs
+++ b/verify-canary.mjs
@@ -1,32 +1,74 @@
-// 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.
+// READ-ONLY canary verifier. First capture a pre-link baseline, then compare the
+// same fixed population after reprocessing. Fails closed on every read error.
import fs from 'fs';
import { createRequire } from 'module';
+import { evaluateCanary } from './track-c-safety.mjs';
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 CANARY = '/Users/macstudio3/.claude/yolo-queue/gmc-fresh-override-canary.json';
+const BASELINE = new URL('./data/track-c-canary-baseline.json', import.meta.url);
+const BASELINE_ATTEMPT = new URL('./data/track-c-canary-baseline-attempt.json', import.meta.url);
+const RESULT = new URL('./data/track-c-canary-verification.json', import.meta.url);
+const capture = process.argv.includes('--capture-baseline');
+const canary = JSON.parse(fs.readFileSync(CANARY, 'utf8')).overrides;
+
+async function getJson(url, headers) {
+ const response = await fetch(url, { headers });
+ if (!response.ok) throw new Error(`HTTP ${response.status} ${url}`);
+ const value = await response.json();
+ if (value.error) throw new Error(`API error ${JSON.stringify(value.error).slice(0, 180)}`);
+ return value;
}
-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));}
+
+async function scan() {
+ const tok = await token(); const headers = { Authorization: 'Bearer ' + tok };
+ const out = { generated_at: new Date().toISOString(), total: canary.length, approved: 0, disapproved: 0, other: 0, flipped: 0, still425: 0, readErrors: 0, errors: [] };
+ for (let i = 0; i < canary.length; i++) {
+ const row = canary[i], rid = `online:${row.contentLanguage}:${row.feedLabel}:${row.offerId}`;
+ try {
+ const [status, product] = await Promise.all([
+ getJson(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/productstatuses/${encodeURIComponent(rid)}`, headers),
+ getJson(`https://shoppingcontent.googleapis.com/content/v2.1/${MERCHANT}/products/${encodeURIComponent(rid)}`, headers),
+ ]);
+ const price = Number(product.price?.value);
+ if (!Number.isFinite(price)) throw new Error('missing/non-numeric served price');
+ if (price > 4.26) out.flipped++; else out.still425++;
+ const destinations = status.destinationStatuses || [];
+ const state = (destinations.find(d => /Shopping/i.test(d.destination)) || destinations[0] || {}).status;
+ if (state === 'approved') out.approved++;
+ else if (state === 'disapproved') out.disapproved++;
+ else out.other++;
+ } catch (error) {
+ out.readErrors++;
+ if (out.errors.length < 20) out.errors.push({ offerId: row.offerId, error: error.message });
+ }
+ if (i % 50 === 0) process.stderr.write(` ${i}/${canary.length}\n`);
+ }
+ return out;
+}
+
+const current = await scan();
+if (capture) {
+ fs.writeFileSync(BASELINE_ATTEMPT, JSON.stringify(current, null, 2) + '\n');
+ if (current.readErrors) {
+ console.error(`BASELINE REFUSED: ${current.readErrors} read errors; diagnostics: ${BASELINE_ATTEMPT.pathname}`);
+ process.exit(1);
+ }
+ fs.writeFileSync(BASELINE, JSON.stringify(current, null, 2) + '\n', { flag: 'wx' });
+ console.log(`Immutable baseline captured: ${BASELINE.pathname}`);
+ console.log(JSON.stringify(current, null, 2));
+ process.exit(0);
+}
+
+if (!fs.existsSync(BASELINE)) {
+ console.error(`HOLD: no immutable baseline at ${BASELINE.pathname}; run --capture-baseline before linking.`);
+ process.exit(1);
+}
+const baseline = JSON.parse(fs.readFileSync(BASELINE));
+let gate;
+try { gate = evaluateCanary(current, baseline); }
+catch (error) { console.error('HOLD:', error.message); process.exit(1); }
+const result = { schema: 1, baseline, current, gate };
+fs.writeFileSync(RESULT, JSON.stringify(result, null, 2) + '\n');
+console.log(JSON.stringify(result, null, 2));
+if (!gate.pass) process.exit(1);
← 50a1f6e auto-data-snapshot: 2026-08-28T00:19:23 (5 data files) — dat
·
back to Gmc Titlefix
·
auto-data-snapshot: 2026-08-31T02:00:54 (3 data files) — dat b2e9868 →