[object Object]

← 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

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 →