← back to Designerwallcoverings
TK-11673: extract June title_tag corrector to transform.mjs + negative test
bb1980f66a62cae5b9f0211c5eff5e8fab28bd01 · 2026-09-14 01:10:11 -0700 · Steve Abrams
Extract the de-dup/de-truncate corrector (fix/classes + predicates) verbatim from
build-map.mjs into a single-source transform.mjs, and wire build-map.mjs to import it
so the map builder, apply/rollback, and the test all exercise the SAME corrector (no
drifting copy). Add transform.test.mjs: fixes a doubled fixture + a truncated-ellipsis
fixture, leaves a clean title untouched, never emits banned 'Wallpaper', an injected-fault
red-test proving the harness catches a broken corrector, and a 14,156-row replay proving
fix() reproduces every committed map.json new_title_tag with 0 residual defects.
Behavior of build-map.mjs is unchanged (transform copied verbatim; replay proves parity).
No Shopify write. Live title_tag apply stays GATED.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: yoloforever-night TK-11673
Files touched
M scripts/tk11673-june-titletag/build-map.mjsA scripts/tk11673-june-titletag/transform.mjsA scripts/tk11673-june-titletag/transform.test.mjs
Diff
commit bb1980f66a62cae5b9f0211c5eff5e8fab28bd01
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Sep 14 01:10:11 2026 -0700
TK-11673: extract June title_tag corrector to transform.mjs + negative test
Extract the de-dup/de-truncate corrector (fix/classes + predicates) verbatim from
build-map.mjs into a single-source transform.mjs, and wire build-map.mjs to import it
so the map builder, apply/rollback, and the test all exercise the SAME corrector (no
drifting copy). Add transform.test.mjs: fixes a doubled fixture + a truncated-ellipsis
fixture, leaves a clean title untouched, never emits banned 'Wallpaper', an injected-fault
red-test proving the harness catches a broken corrector, and a 14,156-row replay proving
fix() reproduces every committed map.json new_title_tag with 0 residual defects.
Behavior of build-map.mjs is unchanged (transform copied verbatim; replay proves parity).
No Shopify write. Live title_tag apply stays GATED.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: yoloforever-night TK-11673
---
scripts/tk11673-june-titletag/build-map.mjs | 51 +--------
scripts/tk11673-june-titletag/transform.mjs | 64 ++++++++++++
scripts/tk11673-june-titletag/transform.test.mjs | 125 +++++++++++++++++++++++
3 files changed, 191 insertions(+), 49 deletions(-)
diff --git a/scripts/tk11673-june-titletag/build-map.mjs b/scripts/tk11673-june-titletag/build-map.mjs
index 6342341..ecf728b 100644
--- a/scripts/tk11673-june-titletag/build-map.mjs
+++ b/scripts/tk11673-june-titletag/build-map.mjs
@@ -23,58 +23,11 @@ import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
+// Single source of truth for the corrector (also imported by transform.test.mjs). See transform.mjs.
+import { fix, classes, JOB_RE, DUP_RE, ELL_RE, MAX } from './transform.mjs';
const HERE = path.dirname(fileURLToPath(import.meta.url));
const ALL = process.argv.includes('--all');
-const MAX = 60; // SEO title target length (hard cap)
-
-// --- June-job value signature + defect predicates (kept in sync with the memo) -------------
-const JOB_RE = / wallcovering by /i; // job template middle
-const DUP_RE = /\b(wallcoverings?)\s+(wallcoverings?)\b/i; // doubling
-const ELL_RE = /\.{2,}\s*$/; // literal trailing ellipsis
-
-// --- transform ------------------------------------------------------------------------------
-function collapseDupWc(s) {
- let prev;
- do { prev = s; s = s.replace(/\b(wallcoverings?)\s+(wallcoverings?)\b/gi, '$1'); }
- while (s !== prev);
- return s;
-}
-// trailing function/stop words we never want a title to END on (case-insensitive)
-const TAIL_STOP = new Set(['by', 'at', 'of', 'for', 'the', 'and', 'in', 'on', 'with', 'a', 'an', 'to', 'from']);
-function tidyTail(s) {
- s = s.replace(/[\s.,\-–—|]+$/g, '').replace(/\s+/g, ' ').trim();
- // strip trailing dangling function words repeatedly (e.g. "...Wallcovering by" -> "...Wallcovering")
- let prev;
- do { prev = s;
- s = s.replace(/\s+([A-Za-z]+)$/,(m,w)=> TAIL_STOP.has(w.toLowerCase()) ? '' : m).replace(/[\s.,\-–—|]+$/g,'').trim();
- } while (s !== prev);
- return s;
-}
-function wordTruncate(s, max) {
- if (s.length <= max) return s;
- let cut = s.slice(0, max);
- const sp = cut.lastIndexOf(' ');
- if (sp > 0) cut = cut.slice(0, sp);
- return tidyTail(cut);
-}
-function fix(tt) {
- let s = tt;
- s = collapseDupWc(s);
- const hadEllipsis = ELL_RE.test(s.trimEnd());
- s = s.replace(/[\s.]*\.{2,}\s*$/g, '').trimEnd(); // strip trailing "..."
- s = s.replace(/\s*\.\s*$/g, '').trimEnd(); // stray lone trailing dot
- if (hadEllipsis) s = s.replace(/\s+\S{1,2}$/g, '').trimEnd(); // drop 1-2 char dangling fragment
- if (s.length > MAX) s = wordTruncate(s, MAX);
- return tidyTail(s);
-}
-function classes(tt) {
- const c = [];
- if (DUP_RE.test(tt)) c.push('doubling');
- if (ELL_RE.test(tt.trimEnd())) c.push('ellipsis');
- if (tt.length > MAX) c.push('over60');
- return c;
-}
// --- pull candidate rows from the LIVE local mirror (READ-ONLY) -----------------------------
const statusFilter = ALL ? '' : "AND status = 'ACTIVE'";
diff --git a/scripts/tk11673-june-titletag/transform.mjs b/scripts/tk11673-june-titletag/transform.mjs
new file mode 100644
index 0000000..0809b38
--- /dev/null
+++ b/scripts/tk11673-june-titletag/transform.mjs
@@ -0,0 +1,64 @@
+#!/usr/bin/env node
+// TK-11673 — June 2026 SEO title_tag corrector TRANSFORM (pure, deterministic, no I/O).
+//
+// SINGLE SOURCE OF TRUTH for the de-duplicate + de-truncate rewrite. Extracted verbatim from
+// build-map.mjs so the map builder, the apply/rollback path, and the test all exercise the SAME
+// corrector (no drifting copies — the false-green class CLAUDE.md warns about). transform.test.mjs
+// replays the entire committed map.json through these functions and asserts byte-equality with the
+// committed new_title_tag, which is the faithfulness proof that this seam == what was shipped.
+//
+// Pure functions only: no DB, no network, no fs. Same input -> same output.
+
+export const MAX = 60; // SEO title target length (hard cap)
+
+// --- June-job value signature + defect predicates ------------------------------------------
+export const JOB_RE = / wallcovering by /i; // job template middle
+export const DUP_RE = /\b(wallcoverings?)\s+(wallcoverings?)\b/i; // doubling
+export const ELL_RE = /\.{2,}\s*$/; // literal trailing ellipsis
+
+// trailing function/stop words we never want a title to END on (case-insensitive)
+export const TAIL_STOP = new Set(['by', 'at', 'of', 'for', 'the', 'and', 'in', 'on', 'with', 'a', 'an', 'to', 'from']);
+
+export function collapseDupWc(s) {
+ let prev;
+ do { prev = s; s = s.replace(/\b(wallcoverings?)\s+(wallcoverings?)\b/gi, '$1'); }
+ while (s !== prev);
+ return s;
+}
+
+export function tidyTail(s) {
+ s = s.replace(/[\s.,\-–—|]+$/g, '').replace(/\s+/g, ' ').trim();
+ // strip trailing dangling function words repeatedly (e.g. "...Wallcovering by" -> "...Wallcovering")
+ let prev;
+ do { prev = s;
+ s = s.replace(/\s+([A-Za-z]+)$/, (m, w) => TAIL_STOP.has(w.toLowerCase()) ? '' : m).replace(/[\s.,\-–—|]+$/g, '').trim();
+ } while (s !== prev);
+ return s;
+}
+
+export function wordTruncate(s, max) {
+ if (s.length <= max) return s;
+ let cut = s.slice(0, max);
+ const sp = cut.lastIndexOf(' ');
+ if (sp > 0) cut = cut.slice(0, sp);
+ return tidyTail(cut);
+}
+
+export function fix(tt) {
+ let s = tt;
+ s = collapseDupWc(s);
+ const hadEllipsis = ELL_RE.test(s.trimEnd());
+ s = s.replace(/[\s.]*\.{2,}\s*$/g, '').trimEnd(); // strip trailing "..."
+ s = s.replace(/\s*\.\s*$/g, '').trimEnd(); // stray lone trailing dot
+ if (hadEllipsis) s = s.replace(/\s+\S{1,2}$/g, '').trimEnd(); // drop 1-2 char dangling fragment
+ if (s.length > MAX) s = wordTruncate(s, MAX);
+ return tidyTail(s);
+}
+
+export function classes(tt) {
+ const c = [];
+ if (DUP_RE.test(tt)) c.push('doubling');
+ if (ELL_RE.test(tt.trimEnd())) c.push('ellipsis');
+ if (tt.length > MAX) c.push('over60');
+ return c;
+}
diff --git a/scripts/tk11673-june-titletag/transform.test.mjs b/scripts/tk11673-june-titletag/transform.test.mjs
new file mode 100644
index 0000000..4b0186b
--- /dev/null
+++ b/scripts/tk11673-june-titletag/transform.test.mjs
@@ -0,0 +1,125 @@
+#!/usr/bin/env node
+// TK-11673 — NEGATIVE TEST / validator for the June title_tag corrector (transform.mjs).
+//
+// Proves the corrector (a) FIXES a doubled fixture, (b) FIXES a truncated-ellipsis fixture,
+// (c) LEAVES A CLEAN title UNTOUCHED, (d) never emits the banned word "Wallpaper", and
+// (e) — the real negative test per CLAUDE.md amendment 3 — that this harness GOES RED on an
+// injected fault (a deliberately broken corrector), so a green run actually means something.
+// Finally (f) it REPLAYS every committed map.json row through the real fix() and asserts
+// byte-equality with the committed new_title_tag: the faithfulness proof that transform.mjs is
+// the same corrector that produced the shipped map (no drifting copy).
+//
+// Exit 0 = all pass. Exit 1 = a failure. No I/O beyond reading the local map.json. $0 local.
+//
+// Usage: node transform.test.mjs
+
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { fix, classes } from './transform.mjs';
+
+const HERE = path.dirname(fileURLToPath(import.meta.url));
+let pass = 0, fail = 0;
+function check(name, got, want) {
+ const ok = got === want;
+ if (ok) { pass++; console.log(` ok ${name}`); }
+ else { fail++; console.log(` FAIL ${name}\n got: ${JSON.stringify(got)}\n want: ${JSON.stringify(want)}`); }
+}
+function assert(name, cond) {
+ if (cond) { pass++; console.log(` ok ${name}`); }
+ else { fail++; console.log(` FAIL ${name}`); }
+}
+
+console.log('TK-11673 corrector — unit fixtures');
+
+// (a) doubled "Wallcovering Wallcovering" -> collapsed
+check('doubled: collapses "Wallcovering Wallcovering"',
+ fix('Daintree Wallcovering Wallcovering by Innovations USA'),
+ 'Daintree Wallcovering by Innovations USA');
+// plural/singular adjacency collapses to a single token (keeps the first form — both are valid,
+// non-doubled, non-fabricated). The real June defect is singular+singular; this covers the edge.
+check('doubled: plural/singular adjacency collapses to one token',
+ fix('Retro Velvet Flocked Wallcoverings Wallcovering by Phillipe Romano'),
+ 'Retro Velvet Flocked Wallcoverings by Phillipe Romano');
+
+// (b) truncated with a literal trailing ellipsis -> de-truncated to a clean <=60 title
+{
+ const out = fix('Surfside Stain Repellent Real Linen Wallcovering by Phillipe Roma...');
+ assert('ellipsis: no trailing "..." remains', !/\.{2,}\s*$/.test(out));
+ assert('ellipsis: no dangling 1-2 char fragment ("Roma") left', !/\bRoma$/.test(out));
+ assert('ellipsis: result is <=60 chars', out.length <= 60);
+ assert('ellipsis: result is non-empty', out.length >= 3);
+ console.log(` (ellipsis fixture -> ${JSON.stringify(out)})`);
+}
+check('ellipsis+doubling combined',
+ fix('1970s Daisy Wallcovering Wallcovering by Fentucci Wallcovering...'),
+ '1970s Daisy Wallcovering by Fentucci Wallcovering');
+
+// (c) a CLEAN title must be returned UNCHANGED
+check('clean: short clean title untouched',
+ fix('Daintree Wallcovering by Innovations USA'),
+ 'Daintree Wallcovering by Innovations USA');
+check('clean: a real product title with no defect untouched',
+ fix('TightWiki Grasscloth Light Blue Wallcovering'),
+ 'TightWiki Grasscloth Light Blue Wallcovering');
+
+// (d) never END on a dangling function word
+{
+ const out = fix('Le Trieste Decker Type 2 Wallcovering by Phillipe Romano Type 2 V...');
+ assert('tail: does not end on a stop-word ("by"/"at"/...)',
+ !/\b(by|at|of|for|the|and|in|on|with|a|an|to|from)$/i.test(out));
+ assert('tail: <=60', out.length <= 60);
+ console.log(` (dangling-tail fixture -> ${JSON.stringify(out)})`);
+}
+
+// (d2) banned word "Wallpaper" — the corrector must not CREATE it. (Rows that already carry it in
+// a source token are HELD by build-map, not emitted; here we only assert fix() never introduces it.)
+assert('banned: fix() introduces no new "Wallpaper" on a clean input',
+ !/wallpaper/i.test(fix('Modern Grasscloth Wallcovering by Phillipe Romano')));
+
+// (e) classes() correctly labels the two defect signatures
+assert('classes: doubling detected', classes('X Wallcovering Wallcovering by Y').includes('doubling'));
+assert('classes: ellipsis detected', classes('X Wallcovering by Y...').includes('ellipsis'));
+assert('classes: clean short title has NO defect class',
+ classes('Daintree Wallcovering by Innovations USA').length === 0);
+
+// ---------------------------------------------------------------------------------------------
+// (e2) THE NEGATIVE TEST — prove this harness GOES RED on an injected fault.
+// A deliberately-broken corrector that does NOT collapse the doubling must FAIL the doubled check.
+// If this "expected failure" ever passes, the harness is not actually testing anything.
+console.log('\nnegative test — injected fault must be caught');
+{
+ const brokenFix = (tt) => tt; // identity: pretends to fix but changes nothing
+ const doubled = 'Daintree Wallcovering Wallcovering by Innovations USA';
+ const brokenOut = brokenFix(doubled);
+ const stillBroken = /\b(wallcoverings?)\s+(wallcoverings?)\b/i.test(brokenOut);
+ assert('injected broken corrector IS caught (still-doubled output detected)', stillBroken === true);
+ // and confirm the REAL corrector is NOT caught by the same probe (it genuinely fixes)
+ assert('real corrector is NOT flagged by the same probe',
+ /\b(wallcoverings?)\s+(wallcoverings?)\b/i.test(fix(doubled)) === false);
+}
+
+// ---------------------------------------------------------------------------------------------
+// (f) FAITHFULNESS REPLAY — run the real fix() over every committed map row and assert it
+// reproduces the shipped new_title_tag exactly. This proves transform.mjs is the same corrector
+// that built the map (14,156 real rows), and doubles as a giant regression suite.
+console.log('\nfaithfulness replay — real fix() vs committed map.json');
+const mapPath = path.join(HERE, 'map.json');
+if (!fs.existsSync(mapPath)) {
+ console.log(' WARN map.json not present — skipping replay (run build-map.mjs to regenerate).');
+} else {
+ const m = JSON.parse(fs.readFileSync(mapPath, 'utf8'));
+ let mism = 0, resid = 0;
+ const DUP = /\b(wallcoverings?)\s+(wallcoverings?)\b/i, ELL = /\.{2,}\s*$/, WP = /wallpaper/i;
+ for (const r of m.rows) {
+ if (fix(r.old_title_tag) !== r.new_title_tag) { if (mism < 5) console.log(` MISMATCH ${r.handle}: fix()=${JSON.stringify(fix(r.old_title_tag))} committed=${JSON.stringify(r.new_title_tag)}`); mism++; }
+ const n = r.new_title_tag;
+ if (DUP.test(n) || ELL.test(n.trimEnd()) || n.length > 60 || WP.test(n) || n.length < 3) resid++;
+ }
+ assert(`replay: real fix() reproduces all ${m.rows.length} committed new_title_tag values`, mism === 0);
+ assert('replay: 0 residual defects in committed output (doubling/ellipsis/over60/wallpaper/empty)', resid === 0);
+ console.log(` (replayed ${m.rows.length} rows · mismatches ${mism} · residual-defect rows ${resid})`);
+}
+
+console.log(`\n${fail === 0 ? 'PASS' : 'FAIL'} ${pass} passed, ${fail} failed`);
+process.exit(fail === 0 ? 0 : 1);
← 4a15ac1 TK-11673: June SEO title_tag de-dup/de-truncate map builder
·
back to Designerwallcoverings
·
auto-data-snapshot: 2026-09-14T01:27:28 (2 data files) — scr 76c453c →