← back to Gmc Titlefix
TK-11568: FLS-2768 image fix executor — 94px crop -> 200px dy24-62 parity image + Google republish (dry-run verified, negative-tested, NOT applied)
655c417c6d85c6eec03a90cb370e4307b1e0fde2 · 2026-09-13 02:40:41 -0700 · Steve
Identity evidence: pixel slide-match of the 94px crop over all 60 sibling FLS
images -> best mean-abs-diff 14.67 on the three dy24-62 products vs 57.51 for the
next-best different colorway (median 223.23). Independently corroborated by the
AI colour hex (#6D4C38 vs #674734, delta 6/5/4 per channel; a different colorway
is #D3CBAF) and by FileMaker master rec 72119 (Mfr Pattern dy24-62).
Guards fail closed and are negative-tested: aborts if the target is already
>=200px, if the donor is below the 200px floor, or if the Google publication
state cannot be read. Rollback map written before any mutation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HGZ2dpPSE6bcNgojVpho1
Files touched
A tk11568-fls2768-image-fix.mjs
Diff
commit 655c417c6d85c6eec03a90cb370e4307b1e0fde2
Author: Steve <steve@designerwallcoverings.com>
Date: Sun Sep 13 02:40:41 2026 -0700
TK-11568: FLS-2768 image fix executor — 94px crop -> 200px dy24-62 parity image + Google republish (dry-run verified, negative-tested, NOT applied)
Identity evidence: pixel slide-match of the 94px crop over all 60 sibling FLS
images -> best mean-abs-diff 14.67 on the three dy24-62 products vs 57.51 for the
next-best different colorway (median 223.23). Independently corroborated by the
AI colour hex (#6D4C38 vs #674734, delta 6/5/4 per channel; a different colorway
is #D3CBAF) and by FileMaker master rec 72119 (Mfr Pattern dy24-62).
Guards fail closed and are negative-tested: aborts if the target is already
>=200px, if the donor is below the 200px floor, or if the Google publication
state cannot be read. Rollback map written before any mutation.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015HGZ2dpPSE6bcNgojVpho1
---
tk11568-fls2768-image-fix.mjs | 175 ++++++++++++++++++++++++++++++++++++++++++
1 file changed, 175 insertions(+)
diff --git a/tk11568-fls2768-image-fix.mjs b/tk11568-fls2768-image-fix.mjs
new file mode 100644
index 0000000..52464d6
--- /dev/null
+++ b/tk11568-fls2768-image-fix.mjs
@@ -0,0 +1,175 @@
+#!/usr/bin/env node
+/**
+ * TK-11568 — FLS-2768 "Faux Leaf Squares" image fix + Google republish.
+ *
+ * WHAT: replace FLS-2768's 94x94 crop with the 200x200 dy24-62 colorway image
+ * already LIVE on FLS-2514 / FLS-2531 / FLS-2751 and Google-APPROVED on
+ * 39 offers in this same line, then republish to Google & YouTube.
+ *
+ * WHY THIS IMAGE (identity PROVEN, not inferred):
+ * The 94x94 asset is a crop OF the dy24-62 image. Slide-match of the 94px tile
+ * over all 60 sibling FLS images: best mean-abs-diff 14.67 for the three
+ * dy24-62 products vs 57.51 for the next-best DIFFERENT colorway (3.9x
+ * separation; population median 223.23). FileMaker master rec 72119 also
+ * records Mfr Pattern = dy24-62.
+ *
+ * GATED: customer-facing Shopify media write + external publish. DRY-RUN BY
+ * DEFAULT. --apply requires an explicit Steve go.
+ *
+ * REVERSIBLE: writes data/tk11568-rollback-map.json BEFORE any mutation,
+ * capturing the existing media ids + CDN URLs. rollback.mjs path documented in
+ * the memo. Shopify CDN files persist after media delete, so the old asset URL
+ * stays fetchable for restore.
+ */
+import fs from 'node:fs';
+import path from 'node:path';
+import { execSync } from 'node:child_process';
+
+const APPLY = process.argv.includes('--apply');
+const DELETE_OLD = process.argv.includes('--delete-old'); // default: keep old as gallery #2
+const SHOP = 'designer-laboratory-sandbox.myshopify.com';
+const API = '2024-10';
+const TOKEN = execSync(`grep -E '^SHOPIFY_ADMIN_TOKEN=' ~/Projects/secrets-manager/.env | cut -d= -f2-`, { shell: '/bin/bash' }).toString().trim();
+
+const PRODUCT_ID = 'gid://shopify/Product/1495077552240'; // faux-leaf-squares-fls-2768
+const SOURCE_HANDLE = 'faux-leaf-squares-fls-2514'; // dy24-62 donor (also 2531 / 2751)
+const GOOGLE_PUB = 'gid://shopify/Publication/29646651457'; // Google & YouTube
+const OUT = path.join(import.meta.dirname, 'data', 'tk11568-rollback-map.json');
+
+async function gql(query, variables = {}) {
+ const r = await fetch(`https://${SHOP}/admin/api/${API}/graphql.json`, {
+ method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOKEN, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query, variables }),
+ });
+ const j = await r.json();
+ if (j.errors) throw new Error('GraphQL: ' + JSON.stringify(j.errors).slice(0, 300));
+ return j.data;
+}
+const die = (m) => { console.error('ABORT: ' + m); process.exit(1); };
+
+// ---------- 1. READ current state (always, both modes) ----------
+const cur = await gql(`query($id:ID!){ product(id:$id){
+ id handle title status
+ media(first:20){ nodes{ ... on MediaImage { id image{ url width height } } } }
+ publishedOnGoogle: publishedOnPublication(publicationId:"gid://shopify/Publication/29646651457")
+ resourcePublications(first:30){ nodes{ isPublished publication{ id name } } }
+} }`, { id: PRODUCT_ID });
+const p = cur.product;
+if (!p) die('FLS-2768 not found');
+if (p.handle !== 'faux-leaf-squares-fls-2768') die(`handle drift: got ${p.handle}`);
+
+const src = await gql(`query($h:String!){ productByHandle(handle:$h){
+ handle media(first:5){ nodes{ ... on MediaImage { image{ url width height } } } } } }`, { h: SOURCE_HANDLE });
+const donor = src.productByHandle?.media?.nodes?.[0]?.image;
+if (!donor?.url) die(`donor image not found on ${SOURCE_HANDLE}`);
+
+// ---------- 2. PRE-FLIGHT GUARDS (fail closed) ----------
+const featured = p.media.nodes[0]?.image;
+if (!featured) die('FLS-2768 has no media to replace — state drifted');
+if (Math.min(featured.width, featured.height) >= 200)
+ die(`FLS-2768 featured image is already ${featured.width}x${featured.height} — already fixed, refusing to re-run`);
+if (Math.min(donor.width, donor.height) < 200)
+ die(`donor image is only ${donor.width}x${donor.height} — below the 200px parity floor, refusing`);
+if (typeof p.publishedOnGoogle !== 'boolean') die('could not read Google & YouTube publication state — refusing (fail closed)');
+const googleNow = { isPublished: p.publishedOnGoogle };
+
+// ---------- 3. ROLLBACK MAP written BEFORE any mutation ----------
+const rollback = {
+ ts: new Date().toISOString(),
+ ticket: 'TK-11568',
+ mode: APPLY ? 'apply' : 'dry-run',
+ product: { id: p.id, handle: p.handle, status: p.status },
+ undo: [
+ 'productDeleteMedia the newly-created MediaImage (id recorded in applied.newMediaId)',
+ 'if --delete-old was used: productCreateMedia with before.media[0].url to restore the 94px asset',
+ 'productReorderMedia to put before.media[0].id back at position 0',
+ googleNow.isPublished
+ ? 'Google & YouTube was ALREADY published — no publish undo needed'
+ : 'publishableUnpublish from gid://shopify/Publication/29646651457',
+ ],
+ before: {
+ media: p.media.nodes.map(n => ({ id: n.id, url: n.image.url, w: n.image.width, h: n.image.height })),
+ googlePublished: googleNow.isPublished,
+ publications: p.resourcePublications.nodes.map(n => ({ name: n.publication.name, isPublished: n.isPublished })),
+ },
+ donor: { handle: SOURCE_HANDLE, url: donor.url, w: donor.width, h: donor.height },
+ applied: null,
+};
+fs.mkdirSync(path.dirname(OUT), { recursive: true });
+fs.writeFileSync(OUT, JSON.stringify(rollback, null, 2));
+
+console.log(`TK-11568 FLS-2768 image fix — ${APPLY ? 'APPLY' : 'DRY RUN'}`);
+console.log(` product : ${p.handle} (${p.status})`);
+console.log(` current img : ${featured.width}x${featured.height} ${featured.url}`);
+console.log(` new img : ${donor.width}x${donor.height} ${donor.url} (from ${SOURCE_HANDLE}, mfr dy24-62)`);
+console.log(` google now : ${googleNow.isPublished ? 'PUBLISHED' : 'not published'}`);
+console.log(` delete old : ${DELETE_OLD}`);
+console.log(` rollback : ${OUT}`);
+
+if (!APPLY) {
+ console.log('\nDRY RUN — no writes made. Re-run with --apply only on Steve\'s explicit go.');
+ process.exit(0);
+}
+
+// ---------- 4. APPLY ----------
+const add = await gql(`mutation($id:ID!,$media:[CreateMediaInput!]!){
+ productCreateMedia(productId:$id, media:$media){
+ media{ ... on MediaImage { id status } }
+ mediaUserErrors{ field message }
+ } }`, { id: PRODUCT_ID, media: [{ originalSource: donor.url, mediaContentType: 'IMAGE', alt: 'Faux Leaf Squares | Hollywood Wallcoverings' }] });
+const errs = add.productCreateMedia.mediaUserErrors;
+if (errs?.length) die('productCreateMedia: ' + JSON.stringify(errs));
+const newId = add.productCreateMedia.media[0].id;
+console.log(' created media:', newId);
+
+// wait for Shopify to finish processing before reordering
+for (let i = 0; i < 30; i++) {
+ await new Promise(r => setTimeout(r, 2000));
+ const s = await gql(`query($id:ID!){ node(id:$id){ ... on MediaImage { status image{ url width height } } } }`, { id: newId });
+ if (s.node?.status === 'READY') { console.log(' media READY:', s.node.image.width + 'x' + s.node.image.height); break; }
+ if (s.node?.status === 'FAILED') die('media processing FAILED (check the 500GB file-storage cap)');
+ if (i === 29) die('media never reached READY — aborting before reorder');
+}
+
+const reorder = await gql(`mutation($id:ID!,$moves:[MoveInput!]!){
+ productReorderMedia(id:$id, moves:$moves){ mediaUserErrors{ field message } } }`,
+ { id: PRODUCT_ID, moves: [{ id: newId, newPosition: '0' }] });
+if (reorder.productReorderMedia.mediaUserErrors?.length)
+ die('productReorderMedia: ' + JSON.stringify(reorder.productReorderMedia.mediaUserErrors));
+console.log(' reordered to featured');
+
+if (DELETE_OLD) {
+ const del = await gql(`mutation($id:ID!,$mediaIds:[ID!]!){
+ productDeleteMedia(productId:$id, mediaIds:$mediaIds){ deletedMediaIds mediaUserErrors{ field message } } }`,
+ { id: PRODUCT_ID, mediaIds: [rollback.before.media[0].id] });
+ if (del.productDeleteMedia.mediaUserErrors?.length)
+ console.error(' WARN productDeleteMedia: ' + JSON.stringify(del.productDeleteMedia.mediaUserErrors));
+ else console.log(' deleted old 94px media:', del.productDeleteMedia.deletedMediaIds);
+}
+
+const pub = await gql(`mutation($id:ID!,$input:[PublicationInput!]!){
+ publishablePublish(id:$id, input:$input){ userErrors{ field message } } }`,
+ { id: PRODUCT_ID, input: [{ publicationId: GOOGLE_PUB }] });
+if (pub.publishablePublish.userErrors?.length) die('publishablePublish: ' + JSON.stringify(pub.publishablePublish.userErrors));
+console.log(' republished to Google & YouTube');
+
+// ---------- 5. READ-BACK VERIFY ----------
+const after = await gql(`query($id:ID!){ product(id:$id){
+ media(first:20){ nodes{ ... on MediaImage { id image{ url width height } } } }
+ publishedOnGoogle: publishedOnPublication(publicationId:"gid://shopify/Publication/29646651457") } }`, { id: PRODUCT_ID });
+const feat = after.product.media.nodes[0]?.image;
+const g = { isPublished: after.product.publishedOnGoogle };
+const pass = feat && Math.min(feat.width, feat.height) >= 200 && g.isPublished === true;
+rollback.applied = { newMediaId: newId, deletedOld: DELETE_OLD, after: { featured: feat, googlePublished: g.isPublished }, verify: pass ? 'PASS' : 'FAIL' };
+fs.writeFileSync(OUT, JSON.stringify(rollback, null, 2));
+console.log(`\nVERIFY: featured ${feat?.width}x${feat?.height}, Google published=${g?.isPublished} -> ${pass ? 'PASS' : 'FAIL'}`);
+if (!pass) process.exit(1);
+
+try {
+ execSync(`node ~/.claude/yolo-queue/log-exec.mjs`, { input: JSON.stringify({
+ ts: new Date().toISOString(), agent: 'claude-run-11568', ticket: 'TK-11568',
+ action: 'FLS-2768 featured image 94px->200px (dy24-62 parity) + republish to Google & YouTube',
+ blast_radius: 1, undo_cmd: `see ${OUT}`, verify: 'featured>=200px && Google published',
+ }), stdio: ['pipe', 'inherit', 'inherit'] });
+} catch { console.error(' (ledger log failed — record manually)'); }
← 3d1db1c auto-data-snapshot: 2026-09-13T02:36:34 (1 data files) — dat
·
back to Gmc Titlefix
·
TK-11449: pre-delete restore-map for the 1 residual orphan o a67d0c3 →