[object Object]

← back to Designerwallcoverings

Scope showroom-tag rollback/verify per line (fix SHOWROOM_TARGETS cross-contamination)

484a4a8894a0ad7327e939543589d947c994d147 · 2026-09-14 14:09:00 -0700 · Steve Abrams

apply-showroomonly.mjs gained a SHOWROOM_TARGETS override so any showroom line
can be tagged, but DATA/ stayed shared and rollback read it globally: a rollback
for one line would tagsRemove 'ShowroomOnly' from every line whose restore maps
sat in the dir (already mixed on disk: MDC 4728 + Phillip Jeffries 3392),
un-hiding a line the operator never touched.

- resolveBaseline: skip restore maps whose source_id_list != the active MAP_FILE
- rollback: hard-intersect toRemove/preserved with the current target gid set
- doVerify: count per-line via prescan instead of a store-wide productsCount,
  which returned a false MISMATCH (and misled the rollback abort-guard) once
  more than one line was tagged

Verified: MDC rollback now scopes to 4728 (PJ untouched); PJ rollback scopes to
3392 (MDC untouched). Local code only; nothing run against Shopify.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FTQanBRpHfVRuReimUagK5

Files touched

Diff

commit 484a4a8894a0ad7327e939543589d947c994d147
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Sep 14 14:09:00 2026 -0700

    Scope showroom-tag rollback/verify per line (fix SHOWROOM_TARGETS cross-contamination)
    
    apply-showroomonly.mjs gained a SHOWROOM_TARGETS override so any showroom line
    can be tagged, but DATA/ stayed shared and rollback read it globally: a rollback
    for one line would tagsRemove 'ShowroomOnly' from every line whose restore maps
    sat in the dir (already mixed on disk: MDC 4728 + Phillip Jeffries 3392),
    un-hiding a line the operator never touched.
    
    - resolveBaseline: skip restore maps whose source_id_list != the active MAP_FILE
    - rollback: hard-intersect toRemove/preserved with the current target gid set
    - doVerify: count per-line via prescan instead of a store-wide productsCount,
      which returned a false MISMATCH (and misled the rollback abort-guard) once
      more than one line was tagged
    
    Verified: MDC rollback now scopes to 4728 (PJ untouched); PJ rollback scopes to
    3392 (MDC untouched). Local code only; nothing run against Shopify.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01FTQanBRpHfVRuReimUagK5
---
 .../tk11307-showroom-tag/apply-showroomonly.mjs    | 48 +++++++++++++---------
 1 file changed, 28 insertions(+), 20 deletions(-)

diff --git a/scripts/tk11307-showroom-tag/apply-showroomonly.mjs b/scripts/tk11307-showroom-tag/apply-showroomonly.mjs
index c06d099..e6cc90a 100644
--- a/scripts/tk11307-showroom-tag/apply-showroomonly.mjs
+++ b/scripts/tk11307-showroom-tag/apply-showroomonly.mjs
@@ -29,7 +29,9 @@
  *                 subsequent --apply has an authoritative baseline; dry-run is safe/read-only on Shopify.
  *   --apply       Pre-scan → write restore map → tagsAdd 'ShowroomOnly' on every target missing it.
  *   --rollback    Read the newest restore map → tagsRemove 'ShowroomOnly' from products we added it to.
- *   --verify      Count live products carrying the EXACT tag via query:"tag:ShowroomOnly"; compare to 4728.
+ *   --verify      Count how many of THIS run's targets carry the EXACT tag (per-line, via prescan);
+ *                 compare to the target-set size. Scoped per line so a shared data/ dir holding
+ *                 multiple showroom lines does not produce a false MISMATCH.
  *
  * Cost: $0 (Admin API has no per-call charge).
  */
@@ -150,6 +152,15 @@ function resolveBaseline(explicitMap) {
     let map;
     try { map = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { continue; }
     if (!Array.isArray(map.rows)) continue;
+    // Only merge maps written for THIS run's id-list source. The data/ dir is shared across
+    // showroom lines (SHOWROOM_TARGETS), so without this filter a rollback would fold in another
+    // line's baseline and strip its tag too. Skipped only when an explicit --map was given (the
+    // operator chose that file); a legacy map lacking source_id_list is kept (the rollback call
+    // site also hard-intersects with the current target set as a second guard).
+    if (!explicitMap && map.source_id_list && map.source_id_list !== MAP_FILE) {
+      used.push({ file: path.basename(file), rows: map.rows.length, contributed: 0, skipped: 'other-source' });
+      continue;
+    }
     let contributed = 0;
     for (const r of map.rows) {
       if (!r || !r.gid) continue;
@@ -182,23 +193,16 @@ async function batchTagOp(op, gids) {
 }
 
 async function doVerify(targets) {
-  // EXACT-tag live count. 'tag:ShowroomOnly' measures 0 collisions today (never 'tag:Showroom').
+  // Per-LINE verification: count how many of THIS run's targets carry the exact tag.
+  // Do NOT use a store-wide productsCount(tag:ShowroomOnly): the data/ dir is shared across
+  // showroom lines, so once more than one line is tagged the store-wide count is the UNION
+  // across lines and would report a false MISMATCH against a single line's target set — and
+  // this function also feeds the rollback abort-guard, so a store-wide count would mislead it.
+  const byGid = await prescan(targets);
   let count = 0;
-  const d = await gql(`query{ productsCount(query:"tag:'${TAG}'"){ count } }`);
-  if (d && !d.__err && d.productsCount) count = d.productsCount.count;
-  else {
-    // fallback: paginate
-    let after = null, more = true;
-    while (more) {
-      const q = `query($after:String){ products(first:250, query:"tag:'${TAG}'", after:$after){ pageInfo{hasNextPage endCursor} nodes{id} } }`;
-      const r = await gql(q, { after });
-      if (r?.__err) throw new Error('verify gql err: ' + JSON.stringify(r.__err).slice(0, 200));
-      count += r.products.nodes.length;
-      more = r.products.pageInfo.hasNextPage; after = r.products.pageInfo.endCursor;
-    }
-  }
-  console.log(`\n  live products with EXACT tag '${TAG}': ${count}`);
-  console.log(`  expected (target set): ${targets.length}`);
+  for (const v of byGid.values()) if (v.had_tag) count++;
+  const notFound = targets.length - byGid.size;
+  console.log(`\n  targets carrying EXACT tag '${TAG}': ${count} / ${targets.length}` + (notFound ? `  (${notFound} not found in store)` : ''));
   console.log(`  ${count === targets.length ? 'MATCH ✓' : 'MISMATCH — investigate'}`);
   return count;
 }
@@ -216,11 +220,15 @@ async function doVerify(targets) {
     const { byGid: baseline, used } = resolveBaseline(MAP_ARG);
     console.log(MAP_ARG ? `  restore map (explicit --map): ${MAP_ARG}`
                         : `  reconstructing baseline from ${used.length} restore map(s), earliest-wins:`);
-    for (const u of used) console.log(`    ${u.file}  rows=${u.rows}  baseline rows contributed=${u.contributed}`);
+    for (const u of used) console.log(`    ${u.file}  rows=${u.rows}  baseline rows contributed=${u.contributed}${u.skipped ? '  (skipped: ' + u.skipped + ')' : ''}`);
     const rows = [...baseline.values()];
+    // Hard-scope rollback to the CURRENT run's target set (from SHOWROOM_TARGETS / default MAP_FILE).
+    // The data/ dir is shared across showroom lines, so even if a baseline row from another line
+    // slips through (e.g. a legacy map with no source_id_list), we never tagsRemove outside this line.
+    const targetGids = new Set(targets.map(t => t.gid));
     // remove ONLY from products we added it to (had_tag === false at their EARLIEST observation)
-    const toRemove = rows.filter(r => r.present_in_store && r.had_tag === false).map(r => r.gid);
-    const preserved = rows.filter(r => r.had_tag === true).length;
+    const toRemove = rows.filter(r => r.present_in_store && r.had_tag === false && targetGids.has(r.gid)).map(r => r.gid);
+    const preserved = rows.filter(r => r.had_tag === true && targetGids.has(r.gid)).length;
     console.log(`  will tagsRemove '${TAG}' from ${toRemove.length} products (preserving ${preserved} that had it before).`);
 
     // FAIL LOUD: a rollback that would remove nothing while the tag is still live is the

← c964046 auto-data-snapshot: 2026-09-14T14:02:07 (2 data files) — scr  ·  back to Designerwallcoverings  ·  tk11323-twil-8yd: remove dead/malformed unused mutation stri c16eadd →