← back to Designerwallcoverings
TK-11076: undo verify compares tag SETS, not containment+length
8de18318dc05e721b3a668f5d762f03db2e1b33d · 2026-09-13 16:15:39 -0700 · Steve Abrams
Self-audit of my own read-back found a false-PASS path: containment plus equal
length is not set equality when old_tags holds duplicates-after-normalisation.
want=[A,A] vs live=[A,B] passed while B was still on the product — the undo would
have reported 'reverted + verified' on a product that had not returned.
Latent, not live: 0 of 7,885 recorded undo entries across 50 rollback files carry
such a duplicate. Fixed anyway — this is the check the memo's reversibility
guarantee rests on. Covered by two new self-test cases; both proven to redden on
an injected fault.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6fLv15vwEHvH3kFfhMXPt
Files touched
M scripts/colorway-title-anchor/tk11076-rollback.mjs
Diff
commit 8de18318dc05e721b3a668f5d762f03db2e1b33d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Sep 13 16:15:39 2026 -0700
TK-11076: undo verify compares tag SETS, not containment+length
Self-audit of my own read-back found a false-PASS path: containment plus equal
length is not set equality when old_tags holds duplicates-after-normalisation.
want=[A,A] vs live=[A,B] passed while B was still on the product — the undo would
have reported 'reverted + verified' on a product that had not returned.
Latent, not live: 0 of 7,885 recorded undo entries across 50 rollback files carry
such a duplicate. Fixed anyway — this is the check the memo's reversibility
guarantee rests on. Covered by two new self-test cases; both proven to redden on
an injected fault.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6fLv15vwEHvH3kFfhMXPt
---
scripts/colorway-title-anchor/tk11076-rollback.mjs | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/scripts/colorway-title-anchor/tk11076-rollback.mjs b/scripts/colorway-title-anchor/tk11076-rollback.mjs
index 602ff99..5c57503 100644
--- a/scripts/colorway-title-anchor/tk11076-rollback.mjs
+++ b/scripts/colorway-title-anchor/tk11076-rollback.mjs
@@ -26,6 +26,14 @@ const APPLY = process.argv.includes('--apply');
const SELFTEST = process.argv.includes('--self-test');
const arg = k => (process.argv.find(a => a.startsWith(`--${k}=`)) || '').split('=').slice(1).join('=');
const norm = s => String(s == null ? '' : s).trim().toLowerCase();
+// Set equality, both directions. Containment + equal length is NOT set equality when the
+// wanted list holds duplicates-after-normalisation: want=[A,A] vs live=[A,B] passes that test
+// while B is still on the product. Latent today (0 of 7,885 recorded undo entries have such a
+// duplicate) but this is the check the reversibility guarantee rests on, so it compares sets.
+const sameTagSet = (want, live) => {
+ const a = new Set(want.map(norm)), b = new Set(live.map(norm));
+ return a.size === b.size && [...a].every(t => b.has(t));
+};
// A genuine undo record is an object carrying the Shopify product gid we must write back to.
// Everything else in the file is bookkeeping and must never reach productUpdate.
@@ -63,6 +71,8 @@ if (SELFTEST) {
['an entry with no gid is rejected', a.skipped.includes('no-id-handle')],
['--only restricts to the named record', b.entries.length === 1],
['--only flags a handle with no record', b.missing.length === 1 && b.missing[0] === 'never-written-handle'],
+ ['tag verify rejects a stray tag', sameTagSet(['A', 'B'], ['A', 'B']) && !sameTagSet(['A', 'A'], ['A', 'B'])],
+ ['tag verify is case/whitespace-insensitive', sameTagSet([' Beige'], ['beige'])],
];
let bad = 0;
for (const [name, ok] of checks) { console.log(` ${ok ? 'PASS' : 'FAIL'} ${name}`); if (!ok) bad++; }
@@ -100,7 +110,7 @@ for (const [h, r] of entries) {
// Verify by read-back — a mutation that returned clean is not proof the state came back.
const v = await gql(`query($id:ID!){ product(id:$id){ tags m:metafield(namespace:"custom",key:"real_color_name"){ value } } }`, { id: r.id });
const lt = (v && v.product && v.product.tags) || [];
- const tagsOk = want.tags.length === lt.length && want.tags.every(t => lt.some(x => norm(x) === norm(t)));
+ const tagsOk = sameTagSet(want.tags, lt);
const metaOk = norm((v && v.product && v.product.m && v.product.m.value) || '') === norm(want.meta);
if (tagsOk && metaOk) { ok++; console.log(` ✓ reverted + verified`); }
else { fail++; console.error(` VERIFY FAIL ${h}: tagsOk=${tagsOk} metaOk=${metaOk}`); }
← cce7427 TK-11635: Fentucci $0 Per-Yard variant sweep + dry-default g
·
back to Designerwallcoverings
·
TK-11076: three more undo defects, two surfaced by a second- bf894f2 →