← 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
M scripts/tk11307-showroom-tag/apply-showroomonly.mjs
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 →