← back to Designerwallcoverings
TK-10895 fixpos: read variant order on the same plane we write it
a505db3c458f01c33ed42afd6735f6b6e82b0180 · 2026-09-10 11:39:27 -0700 · Steve Abrams
Shopify REST returns a stale variants array AND a stale position field after a
GraphQL productVariantsBulkReorder. That cost two wrong readings today:
1. read-back on REST reported 127 x 'MUTATION ACCEPTED BUT READ-BACK STILL
WRONG' on products GraphQL and the rendered storefront both showed correctly
ordered - false negatives that read like a failed run.
2. DETECTION on REST then made the re-run report 'fixed' on 221 products that
were already correct, issuing 221 redundant live writes. Harmless, but an
idempotent tool that cannot tell 'already right' from 'I fixed it' stops
being evidence.
Both paths now read via GraphQL with a REST fallback. Variant ids are
normalised to their numeric tail before re-wrapping, since REST yields bare
numerics and GraphQL yields full GIDs - without that a GraphQL-sourced id
double-prefixes and the mutation fails.
Verified: dry-run over all 226 now reports alreadyCorrect=226 fixed=0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B18ECN33SGCpS3ntckQMc2
Files touched
M scripts/tk10895-fixpos.mjs
Diff
commit a505db3c458f01c33ed42afd6735f6b6e82b0180
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 10 11:39:27 2026 -0700
TK-10895 fixpos: read variant order on the same plane we write it
Shopify REST returns a stale variants array AND a stale position field after a
GraphQL productVariantsBulkReorder. That cost two wrong readings today:
1. read-back on REST reported 127 x 'MUTATION ACCEPTED BUT READ-BACK STILL
WRONG' on products GraphQL and the rendered storefront both showed correctly
ordered - false negatives that read like a failed run.
2. DETECTION on REST then made the re-run report 'fixed' on 221 products that
were already correct, issuing 221 redundant live writes. Harmless, but an
idempotent tool that cannot tell 'already right' from 'I fixed it' stops
being evidence.
Both paths now read via GraphQL with a REST fallback. Variant ids are
normalised to their numeric tail before re-wrapping, since REST yields bare
numerics and GraphQL yields full GIDs - without that a GraphQL-sourced id
double-prefixes and the mutation fails.
Verified: dry-run over all 226 now reports alreadyCorrect=226 fixed=0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B18ECN33SGCpS3ntckQMc2
---
scripts/tk10895-fixpos.mjs | 59 +++++++++++++++++++++++++++++++++++++++-------
1 file changed, 51 insertions(+), 8 deletions(-)
diff --git a/scripts/tk10895-fixpos.mjs b/scripts/tk10895-fixpos.mjs
index 91729ec..3138ae4 100644
--- a/scripts/tk10895-fixpos.mjs
+++ b/scripts/tk10895-fixpos.mjs
@@ -47,7 +47,13 @@ async function reorder(pid, orderedIds, tries = 5) {
}`;
const variables = {
productId: `gid://shopify/Product/${pid}`,
- positions: orderedIds.map((id, i) => ({ id: `gid://shopify/ProductVariant/${id}`, position: i + 1 })),
+ // ids arrive as BARE numerics from REST and as full GIDs from GraphQL. Normalise to the
+ // numeric tail before re-wrapping, or a GraphQL-sourced id double-prefixes into
+ // gid://shopify/ProductVariant/gid://shopify/ProductVariant/NNN and the mutation fails.
+ positions: orderedIds.map((id, i) => ({
+ id: `gid://shopify/ProductVariant/${String(id).replace(/^.*\//, '')}`,
+ position: i + 1,
+ })),
};
for (let a = 1; a <= tries; a++) {
let r, j;
@@ -73,15 +79,45 @@ async function reorder(pid, orderedIds, tries = 5) {
return { ok: false, why: 'transport failed after retries', raw: null };
}
+// Authoritative variant order. GraphQL reflects a reorder immediately; REST does not.
+async function gqlRead(pid, tries = 3) {
+ const q = `{ product(id:"gid://shopify/Product/${pid}"){ variants(first:20){ nodes{ id title price position } } } }`;
+ for (let a = 1; a <= tries; a++) {
+ try {
+ const r = await fetch(`https://${SHOP}/admin/api/${VER}/graphql.json`, {
+ method: 'POST',
+ headers: { 'X-Shopify-Access-Token': TOK, 'Content-Type': 'application/json' },
+ body: JSON.stringify({ query: q }),
+ });
+ if (r.status === 429 || r.status >= 500) { await sleep(1200 * a); continue; }
+ const j = await r.json();
+ const n = j?.data?.product?.variants?.nodes;
+ if (Array.isArray(n) && n.length) {
+ return [...n].sort((x, y) => (x.position || 99) - (y.position || 99));
+ }
+ return [];
+ } catch { await sleep(1000 * a); }
+ }
+ return [];
+}
+
console.log(`${APPLY ? '*** APPLY ***' : 'DRY-RUN'} — checking ${work.length} products\n`);
let already = 0, fixed = 0, stillBad = 0, failed = 0, skipped = 0;
const bad = [];
for (const [i, w] of work.entries()) {
const tag = `[${i + 1}/${work.length}] ${w.dw_sku}`;
- const g = await rest(`/products/${w.pid}.json`);
- if (g.status !== 200 || !g.body?.product) { console.log(`${tag} FAIL GET ${g.status}`); failed++; continue; }
- const vs = g.body.product.variants || [];
+ // DETECTION on GraphQL too, not just the read-back. Reading REST here made the 2026-09-10 re-run
+ // report "fixed" on 221 products that were ALREADY correct — REST served a stale order, the script
+ // believed a reorder was needed, and issued 221 redundant live writes. Harmless but dishonest:
+ // an idempotent tool must be able to tell "already right" from "I fixed it", or its own output
+ // stops being evidence. Same plane for read AND write.
+ let vs = await gqlRead(w.pid);
+ if (!vs.length) { // fallback only if GraphQL is unavailable
+ const g = await rest(`/products/${w.pid}.json`);
+ if (g.status !== 200 || !g.body?.product) { console.log(`${tag} FAIL GET ${g.status}`); failed++; continue; }
+ vs = [...(g.body.product.variants || [])].sort((a, b) => (a.position || 99) - (b.position || 99));
+ }
if (vs.length < 2) { console.log(`${tag} skip — only ${vs.length} variant`); skipped++; continue; }
const byPos = [...vs].sort((a, b) => (a.position || 99) - (b.position || 99));
@@ -96,10 +132,17 @@ for (const [i, w] of work.entries()) {
const res = await reorder(w.pid, ordered);
if (!res.ok) { console.log(`${tag} REORDER FAILED — ${res.why}`); failed++; bad.push(w.dw_sku); await sleep(400); continue; }
- await sleep(700); // let it settle before proving it
- const v2 = await rest(`/products/${w.pid}.json`); // READ-BACK: the only proof that counts
- const after = [...(v2.body?.product?.variants || [])].sort((a, b) => (a.position || 99) - (b.position || 99));
- if (after[0] && String(after[0].id) === String(target.id)) {
+ await sleep(700);
+ // READ-BACK via GraphQL, NOT REST. REST returns a STALE variants array AND a stale position
+ // field after a GraphQL reorder — codified on this ticket 2026-09-04 and walked into anyway on
+ // 2026-09-10, when a REST read-back reported 127 false "still wrong" on products GraphQL and the
+ // rendered storefront both showed correctly reordered. Verify on the same plane you wrote on.
+ const vq = await gqlRead(w.pid);
+ const after = vq.length
+ ? vq
+ : [...(await rest(`/products/${w.pid}.json`)).body?.product?.variants || []]
+ .sort((a, b) => (a.position || 99) - (b.position || 99));
+ if (after[0] && String(after[0].id).replace(/\D/g, '') === String(target.id).replace(/\D/g, '')) {
fixed++; console.log(`${tag} ✓ fixed — $${after[0].price} now position 1`);
} else {
stillBad++; bad.push(w.dw_sku);
← 5405216 TK-10895: storefront verify needs a CDN grace period
·
back to Designerwallcoverings
·
auto-data-snapshot: 2026-09-10T11:43:04 (3 data files) — dat 24d79fd →