[object Object]

← 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

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 →