[object Object]

← 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

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 →