← back to Designerwallcoverings
TK-10484: word-boundary settlement matcher + negative test + gated flag correction (dry-run default, --expect guard, exact --rollback)
590af0cc0b709ae42ae9d9df192c7633b29088fc · 2026-09-25 11:46:57 -0700 · Steve Abrams
The 2026-09-24 sweep's keyword matcher was never committed; this reconstructs it as
mode:'legacy' (reproduces 96/97 applied labels, 1/1162 population disagreement) and ships
the fixed mode:'word' (\b-anchored with explicit plurals/compounds). Palmero/Fernanda/Bayleaf
false positives removed; birds/songbird/hummingbird/boughs now caught. DWAA-510300 held
(Steve's final DISAPPROVE). No DB writes; apply is gated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RgbJjouBQ3xFfkmBq17Jik
Files touched
A data/tk10484-settlement-matcher-audit-2026-09-25.jsonA scripts/lib/tk10484-settlement-matcher.mjsA scripts/tests/tk10484-settlement-matcher.test.mjsA scripts/tk10484-settlement-flag-correct.mjsA scripts/tk10484-settlement-matcher-audit.mjs
Diff
commit 590af0cc0b709ae42ae9d9df192c7633b29088fc
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 25 11:46:57 2026 -0700
TK-10484: word-boundary settlement matcher + negative test + gated flag correction (dry-run default, --expect guard, exact --rollback)
The 2026-09-24 sweep's keyword matcher was never committed; this reconstructs it as
mode:'legacy' (reproduces 96/97 applied labels, 1/1162 population disagreement) and ships
the fixed mode:'word' (\b-anchored with explicit plurals/compounds). Palmero/Fernanda/Bayleaf
false positives removed; birds/songbird/hummingbird/boughs now caught. DWAA-510300 held
(Steve's final DISAPPROVE). No DB writes; apply is gated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RgbJjouBQ3xFfkmBq17Jik
---
...k10484-settlement-matcher-audit-2026-09-25.json | 248 +++++++++++++++++++++
scripts/lib/tk10484-settlement-matcher.mjs | 104 +++++++++
scripts/tests/tk10484-settlement-matcher.test.mjs | 74 ++++++
scripts/tk10484-settlement-flag-correct.mjs | 108 +++++++++
scripts/tk10484-settlement-matcher-audit.mjs | 76 +++++++
5 files changed, 610 insertions(+)
diff --git a/data/tk10484-settlement-matcher-audit-2026-09-25.json b/data/tk10484-settlement-matcher-audit-2026-09-25.json
new file mode 100644
index 0000000..f9ead6f
--- /dev/null
+++ b/data/tk10484-settlement-matcher-audit-2026-09-25.json
@@ -0,0 +1,248 @@
+{
+ "ts": "2026-09-25T18:46:57.482Z",
+ "population": 1162,
+ "flagged_now": 97,
+ "flagged_live_now": 82,
+ "replica": {
+ "reproduced": 96,
+ "of": 97,
+ "mismatches": [
+ {
+ "sku": "DWAA-501220-Roll",
+ "title": "Slate Feuilles Peel and Stick",
+ "applied": "APPROVE/floral_low_risk",
+ "replica": "NONE"
+ }
+ ]
+ },
+ "changes": [
+ {
+ "sku": "DWAA-501220-Roll",
+ "title": "Slate Feuilles Peel and Stick",
+ "live": false,
+ "old": "APPROVE/floral_low_risk",
+ "new": "NONE",
+ "matched": [],
+ "direction": "dropped",
+ "steve_decision": null,
+ "cause": "replica_mismatch",
+ "held": true
+ },
+ {
+ "sku": "DWAA-504960-Roll",
+ "title": "Golden lily Boughs Green Peel and Stick",
+ "live": true,
+ "old": "NONE",
+ "new": "REVIEW/directional_foliage_needs_vision",
+ "matched": [
+ "boughs"
+ ],
+ "direction": "newly_flagged",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-505030-Roll",
+ "title": "Meadow Sweet Bayleaf and Manilla Peel and Stick",
+ "live": true,
+ "old": "REVIEW/directional_foliage_needs_vision",
+ "new": "NONE",
+ "matched": [],
+ "direction": "dropped",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-505050-Roll",
+ "title": "MCS6610 - Rosehip Stripe Boughs Green Peel and Stick Wallcovering - by Morris & Co x Jeffrey Stevens",
+ "live": true,
+ "old": "NONE",
+ "new": "REVIEW/directional_foliage_needs_vision",
+ "matched": [
+ "boughs"
+ ],
+ "direction": "newly_flagged",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-505130-Roll",
+ "title": "Willow Boughs Carmine Peel and Stick",
+ "live": true,
+ "old": "NONE",
+ "new": "REVIEW/directional_foliage_needs_vision",
+ "matched": [
+ "boughs"
+ ],
+ "direction": "newly_flagged",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-505150-Roll",
+ "title": "Willow Boughs Ink",
+ "live": true,
+ "old": "NONE",
+ "new": "REVIEW/directional_foliage_needs_vision",
+ "matched": [
+ "boughs"
+ ],
+ "direction": "newly_flagged",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-505160-Roll",
+ "title": "Willow Boughs",
+ "live": true,
+ "old": "NONE",
+ "new": "REVIEW/directional_foliage_needs_vision",
+ "matched": [
+ "boughs"
+ ],
+ "direction": "newly_flagged",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-507090-Roll",
+ "title": "NUS4158 - Navy Fernanda Peel and Stick Wallcovering - by Jeffrey Stevens",
+ "live": true,
+ "old": "REVIEW/directional_foliage_needs_vision",
+ "new": "NONE",
+ "matched": [],
+ "direction": "dropped",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-507550-Roll",
+ "title": "NUS4469 - Blue Songbird Peel and Stick Wallcovering - by Jeffrey Stevens",
+ "live": true,
+ "old": "NONE",
+ "new": "EXCLUDE/part_b_element",
+ "matched": [
+ "songbird"
+ ],
+ "direction": "newly_flagged",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-507560-Roll",
+ "title": "This is Wallcovering's Back English text — no translation needed. Sage Songbird Peel and Stick",
+ "live": true,
+ "old": "NONE",
+ "new": "EXCLUDE/part_b_element",
+ "matched": [
+ "songbird"
+ ],
+ "direction": "newly_flagged",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-509520-Roll",
+ "title": "Grey Aqua Jaybird Peel and Stick",
+ "live": true,
+ "old": "NONE",
+ "new": "EXCLUDE/part_b_element",
+ "matched": [
+ "jaybird"
+ ],
+ "direction": "newly_flagged",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-509540-Roll",
+ "title": "Sage Jaybird Peel and Stick",
+ "live": true,
+ "old": "NONE",
+ "new": "EXCLUDE/part_b_element",
+ "matched": [
+ "jaybird"
+ ],
+ "direction": "newly_flagged",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-510300-Roll",
+ "title": "Green Palmero Leaf Peel and Stick",
+ "live": true,
+ "old": "EXCLUDE/palm_tropical",
+ "new": "REVIEW/directional_foliage_needs_vision",
+ "matched": [
+ "leaf"
+ ],
+ "direction": "reclassified",
+ "steve_decision": "disapprove",
+ "cause": "word_boundary_fix",
+ "held": true
+ },
+ {
+ "sku": "DWAA-510980-Roll",
+ "title": "Green Fernanda Peel and Stick",
+ "live": true,
+ "old": "REVIEW/directional_foliage_needs_vision",
+ "new": "NONE",
+ "matched": [],
+ "direction": "dropped",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-511300-Roll",
+ "title": "Hummingbird Garden Wall Mural",
+ "live": true,
+ "old": "NONE",
+ "new": "EXCLUDE/part_b_element",
+ "matched": [
+ "hummingbird"
+ ],
+ "direction": "newly_flagged",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ },
+ {
+ "sku": "DWAA-511310-Roll",
+ "title": "Floral Birds Wall Mural",
+ "live": true,
+ "old": "APPROVE/floral_low_risk",
+ "new": "EXCLUDE/part_b_element",
+ "matched": [
+ "birds"
+ ],
+ "direction": "reclassified",
+ "steve_decision": null,
+ "cause": "word_boundary_fix",
+ "held": false
+ }
+ ],
+ "summary": {
+ "changed_total": "16 of 1162",
+ "changed_among_flagged": "6 of 97",
+ "changed_among_viewer_82": "5 of 82",
+ "newly_flagged": 10,
+ "caused_by_fix": "15 of 1162",
+ "actionable_in_viewer_82": "4 of 82",
+ "held_skus": [
+ "DWAA-501220-Roll",
+ "DWAA-510300-Roll"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/scripts/lib/tk10484-settlement-matcher.mjs b/scripts/lib/tk10484-settlement-matcher.mjs
new file mode 100644
index 0000000..2849030
--- /dev/null
+++ b/scripts/lib/tk10484-settlement-matcher.mjs
@@ -0,0 +1,104 @@
+// TK-10484 — Jeffrey Stevens settlement title pre-screen matcher (keyword classifier).
+//
+// WHY THIS FILE EXISTS
+// The 2026-09-24 sweep (data/tk10484-jstevens-settlement-sweep-2026-09-24.csv, applied to
+// jeffrey_stevens_catalog.settlement_status/_flag_reason by tk10484-settlement-flag-apply.mjs)
+// was produced by a keyword matcher that was NEVER committed (commit c39fa7b added only the CSV
+// and the applier). This module (a) reconstructs that matcher's behaviour as mode:'legacy' —
+// validated against the applied labels, see tk10484-settlement-matcher-audit.mjs — and
+// (b) ships the FIXED matcher as mode:'word' (the default).
+//
+// THE BUG (legacy): bare substring matching — `title.toLowerCase().includes(kw)`. Proven false
+// positives on the live data: 'palm' inside "Palmero" (DWAA-510300 → EXCLUDE palm_tropical),
+// 'fern' inside "Fernanda" (x2 → REVIEW), 'leaf' inside the Morris colour "Bayleaf" (→ REVIEW).
+// (The legacy matcher was also INCONSISTENT: 'bird' behaved word-bounded — "Floral Birds" and
+// "Hummingbird" did NOT fire — so plural/compound birds were silently missed.)
+//
+// THE FIX (word): every keyword is anchored on BOTH sides with \b, and genuine inflections /
+// compounds are listed EXPLICITLY instead of being caught by accident:
+// plurals (leaves, ferns, fronds, vines, palms, birds, butterflies, bananas, grapes),
+// compounds that ARE the motif (songbird/hummingbird/jaybird, cornflower/wildflower,
+// superbloom/everblooming, grapevine, palmetto), and 'bough(s)' so Willow Boughs keeps its
+// foliage flag now that "Bayleaf" no longer fakes one. Name-only decoys (Palmero, Fernanda,
+// Grapefruit, Bayleaf, Flutter, Palmer) no longer fire.
+//
+// Precedence (unchanged from legacy): named5_dispute > part_b_element > palm_tropical >
+// directional_foliage_needs_vision > floral_low_risk. 'tropical' is a CLASSIFIER only (it
+// upgrades a candidate to palm_tropical) and is NOT on its own a candidate — legacy behaviour,
+// preserved so this change isolates the boundary fix. (Known gap, reported separately:
+// "Charcoal/Sand Tropical Oasis" are therefore unflagged in both modes.)
+
+const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+
+// ---- legacy (reconstructed) vocabulary: plain substrings ----
+const LEGACY = {
+ named5: ['larkspur', 'brownstone blooms', 'belles fleurs', 'field of flowers'],
+ partB: ['banana', 'grape', 'butterfly'], // + 'bird' (word-bounded, see below)
+ palmCand: ['palm'],
+ palmClass: ['palm', 'tropical'],
+ foliage: ['leaf', 'leaves', 'fern', 'foliage', 'vine', 'frond'],
+ floral: ['floral', 'flower', 'bloom', 'botanical', 'petal', 'fleur', 'poppy'],
+};
+// The ONE substring test the legacy matcher used (quoted in the TK-10484 report):
+const legacyHit = (t, kw) => t.includes(kw);
+const legacyBird = (t) => /\bbird\b/.test(t);
+
+// ---- fixed vocabulary: word-bounded regexes with explicit inflections/compounds ----
+const W = (src) => new RegExp(`\\b(?:${src})\\b`);
+const WORD = {
+ named5: [W('larkspurs?'), W('brownstone blooms'), W('belles fleurs'), W('fields? of flowers')],
+ partB: [
+ W('(?:song|humming|jay|blue|love|black|red|fire|thunder|lyre|paradise)?birds?'),
+ W('butterfl(?:y|ies)'),
+ W('bananas?'),
+ W('grapes?|grapevines?'),
+ ],
+ palmCand: [W('palms?|palmettos?')],
+ palmClass: [W('palms?|palmettos?'), W('tropical')],
+ foliage: [W('leaf|leaves'), W('ferns?'), W('foliage'), W('vines?'), W('fronds?'), W('boughs?')],
+ floral: [
+ W('florals?'),
+ W('(?:corn|wild|straw|sun|may|ice)?flowers?'),
+ W('(?:super|ever)?bloom(?:s|ing)?'),
+ W('botanicals?'),
+ W('petals?'),
+ W('fleurs?'),
+ W('popp(?:y|ies)'),
+ ],
+};
+
+function hitsLegacy(t) {
+ const f = (list) => list.filter((k) => legacyHit(t, k));
+ return {
+ named5: f(LEGACY.named5),
+ partB: [...f(LEGACY.partB), ...(legacyBird(t) ? ['bird'] : [])],
+ palmCand: f(LEGACY.palmCand),
+ palmClass: f(LEGACY.palmClass),
+ foliage: f(LEGACY.foliage),
+ floral: f(LEGACY.floral),
+ };
+}
+function hitsWord(t) {
+ const f = (list) => list.map((re) => (t.match(re) || [])[0]).filter(Boolean);
+ const out = {};
+ for (const k of Object.keys(WORD)) out[k] = f(WORD[k]);
+ return out;
+}
+
+/**
+ * classify(title, { mode: 'word' | 'legacy' })
+ * → null (not a botanical candidate) | { status, reason, matched: [words] }
+ */
+export function classify(title, { mode = 'word' } = {}) {
+ const t = String(title || '').toLowerCase();
+ const h = mode === 'legacy' ? hitsLegacy(t) : hitsWord(t);
+ const candidate = h.named5.length || h.partB.length || h.palmCand.length || h.foliage.length || h.floral.length;
+ if (!candidate) return null;
+ if (h.named5.length) return { status: 'EXCLUDE', reason: 'named5_dispute', matched: h.named5 };
+ if (h.partB.length) return { status: 'EXCLUDE', reason: 'part_b_element', matched: h.partB };
+ if (h.palmClass.length) return { status: 'EXCLUDE', reason: 'palm_tropical', matched: h.palmClass };
+ if (h.foliage.length) return { status: 'REVIEW', reason: 'directional_foliage_needs_vision', matched: h.foliage };
+ return { status: 'APPROVE', reason: 'floral_low_risk', matched: h.floral };
+}
+
+export const key = (c) => (c ? `${c.status}/${c.reason}` : 'NONE');
diff --git a/scripts/tests/tk10484-settlement-matcher.test.mjs b/scripts/tests/tk10484-settlement-matcher.test.mjs
new file mode 100644
index 0000000..defb187
--- /dev/null
+++ b/scripts/tests/tk10484-settlement-matcher.test.mjs
@@ -0,0 +1,74 @@
+// TK-10484 — negative test for the settlement title matcher (both directions).
+// node --test scripts/tests/tk10484-settlement-matcher.test.mjs
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { classify, key } from '../lib/tk10484-settlement-matcher.mjs';
+
+// 1) Genuine hits (multi-word / compound / plural) MUST still fire in the fixed matcher.
+const MUST_FLAG = [
+ ['Banana Leaf', 'EXCLUDE/part_b_element'],
+ ['Arcadia Light Green Banana Leaf', 'EXCLUDE/part_b_element'],
+ ['Tropical Palm', 'EXCLUDE/palm_tropical'],
+ ['MS6135 - Bangalow Tropical Palm Peel and Stick Wallcovering - by InHome', 'EXCLUDE/palm_tropical'],
+ ['Palm Leaf', 'EXCLUDE/palm_tropical'],
+ ['Palm Leaf Stay Palm Leaf Peel and Stick', 'EXCLUDE/palm_tropical'],
+ ['Tropical Leaves Wall Mural', 'EXCLUDE/palm_tropical'],
+ ['Palms and Palmettos', 'EXCLUDE/palm_tropical'],
+ ['Bird and Pomegranate', 'EXCLUDE/part_b_element'],
+ ['Bird and Pomegranate Indigo Peel and Stick', 'EXCLUDE/part_b_element'],
+ ['Floral Birds Wall Mural', 'EXCLUDE/part_b_element'], // plural: legacy MISSED this
+ ['Hummingbird Garden Wall Mural', 'EXCLUDE/part_b_element'], // compound: legacy MISSED this
+ ['Sage Butterfly Flutter Peel and Stick', 'EXCLUDE/part_b_element'],
+ ['Butterflies in Flight', 'EXCLUDE/part_b_element'],
+ ['Grapes on the Vine', 'EXCLUDE/part_b_element'],
+ ['Grapevine Trellis', 'EXCLUDE/part_b_element'],
+ ['Blue Larkspur Peel and Stick', 'EXCLUDE/named5_dispute'],
+ ['Orange Field of Flowers Peel and Stick', 'EXCLUDE/named5_dispute'],
+ ['Heather Belles Fleurs Peel and Stick', 'EXCLUDE/named5_dispute'],
+ ['Atlis Beige Fern', 'REVIEW/directional_foliage_needs_vision'],
+ ['Sea Ferns', 'REVIEW/directional_foliage_needs_vision'],
+ ['Turquoise Leaves Peel and Stick', 'REVIEW/directional_foliage_needs_vision'],
+ ['Herb Green Terrace Vines Peel and Stick', 'REVIEW/directional_foliage_needs_vision'],
+ ['Willow Boughs Bayleaf Peel and Stick', 'REVIEW/directional_foliage_needs_vision'], // must not weaken
+ ['Cornflower Aya Flower Peel and Stick', 'APPROVE/floral_low_risk'],
+ ['Mustard Grey StrawFlower Peel and Stick', 'APPROVE/floral_low_risk'],
+ ['Lime Superbloom Peel and Stick', 'APPROVE/floral_low_risk'],
+ ['Dove Everblooming Rosettes Peel and Stick', 'APPROVE/floral_low_risk'],
+ ['Grey Petals Wall Mural', 'APPROVE/floral_low_risk'],
+];
+
+// 2) Decoys: a keyword buried inside an unrelated word. MUST NOT flag (or must not escalate).
+const MUST_NOT_FLAG = [
+ 'Palmero Stripe Peel and Stick', // the confirmed bug
+ 'Grapefruit Geometric',
+ 'Fernanda Damask',
+ 'Meadow Sweet Bayleaf and Manilla Peel and Stick',
+ 'Palmer Plaid',
+ 'Napalm Grey Texture',
+ 'Bananarama Stripe',
+ 'Birdie Check',
+ 'Leaflike Texture', // 'leaf' inside a longer word
+ 'Flutter Stripe',
+ 'Charcoal Tropical Oasis Peel and Stick', // 'tropical' alone is a classifier, not a candidate (legacy)
+];
+
+test('fixed matcher: genuine multi-word/compound/plural hits still fire', () => {
+ for (const [title, want] of MUST_FLAG) assert.equal(key(classify(title)), want, title);
+});
+
+test('fixed matcher: substring decoys do NOT flag', () => {
+ for (const title of MUST_NOT_FLAG) assert.equal(key(classify(title)), 'NONE', title);
+});
+
+test('Palmero no longer escalates to palm_tropical (drops to leaf REVIEW)', () => {
+ assert.equal(key(classify('Green Palmero Leaf Peel and Stick')), 'REVIEW/directional_foliage_needs_vision');
+});
+
+test('legacy replica DOES reproduce the bug (proves the test can go red)', () => {
+ assert.equal(key(classify('Green Palmero Leaf Peel and Stick', { mode: 'legacy' })), 'EXCLUDE/palm_tropical');
+ assert.equal(key(classify('Green Fernanda Peel and Stick', { mode: 'legacy' })), 'REVIEW/directional_foliage_needs_vision');
+ assert.equal(key(classify('Grapefruit Botanical', { mode: 'legacy' })), 'EXCLUDE/part_b_element');
+ // and running the decoy list against legacy must produce at least one false flag
+ const falseFlags = MUST_NOT_FLAG.filter((t) => key(classify(t, { mode: 'legacy' })) !== 'NONE');
+ assert.ok(falseFlags.length >= 5, `legacy false flags: ${falseFlags.join(' | ')}`);
+});
diff --git a/scripts/tk10484-settlement-flag-correct.mjs b/scripts/tk10484-settlement-flag-correct.mjs
new file mode 100644
index 0000000..174e7f3
--- /dev/null
+++ b/scripts/tk10484-settlement-flag-correct.mjs
@@ -0,0 +1,108 @@
+#!/usr/bin/env node
+// TK-10484 — re-flag jeffrey_stevens_catalog settlement columns with the WORD-BOUNDARY matcher.
+// GATED: jeffrey_stevens_catalog writes are Steve-gated (2026-09-24). Default is a DRY-RUN.
+//
+// node scripts/tk10484-settlement-flag-correct.mjs # dry-run plan (no writes)
+// node scripts/tk10484-settlement-flag-correct.mjs --apply --expect N # snapshot, then UPDATE N rows
+// node scripts/tk10484-settlement-flag-correct.mjs --rollback <restore.json> # exact restore of old values
+//
+// Safety:
+// - The plan is recomputed from the LIVE table at run time; --apply refuses unless the plan size
+// equals --expect (the count Steve approved), so a drifted table cannot be silently rewritten.
+// - HELD rows are never touched: DWAA-510300-Roll (Steve DISAPPROVED 2026-09-24 20:20 + 20:45 —
+// final), any SKU with a Steve decision in tmp/settlement-viewer/decisions.jsonl, and any row
+// whose change comes from the legacy-replica residual rather than the boundary fix.
+// - Before any UPDATE, the OLD values of all 4 settlement_* columns for every touched SKU are
+// written to data/tk10484-settlement-correct-restore-<ts>.json; --rollback restores them exactly.
+// - Single transaction, ON_ERROR_STOP; post-write read-back must match the plan or it exits 3.
+import { execFileSync } from 'node:child_process';
+import { readFileSync, writeFileSync, existsSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+import { classify, key } from './lib/tk10484-settlement-matcher.mjs';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+// --test seam: target a throwaway scratch DB (never dw_unified). Plists/ops must never pass --test.
+const TEST = process.argv.includes('--test');
+if (TEST && !/^tk10484_scratch/.test(process.env.TK10484_TEST_DB || '')) { console.error('--test requires TK10484_TEST_DB=tk10484_scratch*'); process.exit(1); }
+const DB = TEST ? process.env.TK10484_TEST_DB : 'dw_unified';
+const PG = ['-h', '/tmp', '-d', DB, '-v', 'ON_ERROR_STOP=1'];
+const HELD_SKUS = new Set(['DWAA-510300-Roll']);
+const DECISIONS = join(ROOT, 'tmp', 'settlement-viewer', 'decisions.jsonl');
+const arg = (f) => (process.argv.includes(f) ? process.argv[process.argv.indexOf(f) + 1] : undefined);
+const q = (s) => (s === null || s === undefined ? 'NULL' : `'${String(s).replace(/'/g, "''")}'`);
+const psqlRows = (sql) => execFileSync('psql', [...PG, '-At', '-F', '\t', '-c', sql], { encoding: 'utf8' })
+ .split('\n').filter(Boolean).map((l) => l.split('\t'));
+const psqlStdin = (sql) => execFileSync('psql', PG, { input: sql, encoding: 'utf8' });
+
+// ---- rollback ----
+if (process.argv.includes('--rollback')) {
+ const f = arg('--rollback');
+ if (!f || !existsSync(f)) { console.error('usage: --rollback <restore.json>'); process.exit(1); }
+ const snap = JSON.parse(readFileSync(f, 'utf8'));
+ let sql = 'BEGIN;\n';
+ for (const r of snap.rows) sql += `UPDATE jeffrey_stevens_catalog SET settlement_status=${q(r.settlement_status)}, settlement_flag_reason=${q(r.settlement_flag_reason)}, settlement_cody_flag=${q(r.settlement_cody_flag)}, settlement_flagged_at=${q(r.settlement_flagged_at)} WHERE sku=${q(r.sku)};\n`;
+ psqlStdin(sql + 'COMMIT;\n');
+ console.log(`ROLLBACK done — restored ${snap.rows.length} rows from ${f}`);
+ process.exit(0);
+}
+
+// ---- plan (always recomputed from live data) ----
+const steve = {};
+if (existsSync(DECISIONS)) for (const l of readFileSync(DECISIONS, 'utf8').split('\n').filter(Boolean)) {
+ try { const e = JSON.parse(l); if (e.decision === 'clear') delete steve[e.sku]; else steve[e.sku] = e; } catch {}
+}
+const rows = psqlRows(`select sku, coalesce(name,''), coalesce(settlement_status,''), coalesce(settlement_flag_reason,''),
+ coalesce(settlement_cody_flag,''), coalesce(settlement_flagged_at::text,''), (archived_at is null)::text
+ from jeffrey_stevens_catalog where sku like 'DWAA-%' order by sku`);
+if (!rows.length) { console.error('NOT-MEASURED: 0 rows read — refusing'); process.exit(2); }
+
+const plan = [], held = [];
+for (const [sku, name, st, rs, cody, at, live] of rows) {
+ const cur = st ? `${st}/${rs}` : 'NONE';
+ const legacy = key(classify(name, { mode: 'legacy' }));
+ const w = classify(name, { mode: 'word' });
+ if (key(w) === cur) continue;
+ const item = { sku, name, live: live === 'true', old: cur, new: key(w), matched: w?.matched || [],
+ before: { settlement_status: st || null, settlement_flag_reason: rs || null, settlement_cody_flag: cody || null, settlement_flagged_at: at || null } };
+ const why = HELD_SKUS.has(sku) ? 'steve_final_decision' : steve[sku] ? `steve_decision:${steve[sku].decision}`
+ : legacy !== cur ? 'replica_residual (not caused by the fix)' : null;
+ if (why) held.push({ ...item, why }); else plan.push(item);
+}
+
+console.log(`TK-10484 settlement flag correction — population ${rows.length} DWAA rows`);
+console.log(`PLAN: ${plan.length} rows would change; HELD (never touched): ${held.length}`);
+for (const p of plan) console.log(` ${p.sku}${p.live ? '' : ' [archived]'} | ${p.name} | ${p.old} -> ${p.new}${p.matched.length ? ` (${p.matched.join(',')})` : ''}`);
+for (const h of held) console.log(` HELD ${h.sku} | ${h.name} | ${h.old} (would be ${h.new}) — ${h.why}`);
+
+if (!process.argv.includes('--apply')) {
+ console.log(`\nDRY-RUN — no writes. Gated apply: node scripts/tk10484-settlement-flag-correct.mjs --apply --expect ${plan.length}`);
+ process.exit(0);
+}
+
+// ---- apply (GATED — only run on Steve's approval) ----
+const expect = Number(arg('--expect'));
+if (!Number.isInteger(expect) || expect !== plan.length) {
+ console.error(`REFUSING: plan has ${plan.length} rows but --expect is ${arg('--expect')} (table drifted or wrong count)`);
+ process.exit(4);
+}
+const ts = new Date().toISOString().replace(/[:.]/g, '-');
+const restorePath = TEST ? join(process.env.TMPDIR || '/tmp', `tk10484-test-restore-${ts}.json`) : join(ROOT, 'data', `tk10484-settlement-correct-restore-${ts}.json`);
+writeFileSync(restorePath, JSON.stringify({ ts, ticket: 'TK-10484', undo: `node scripts/tk10484-settlement-flag-correct.mjs --rollback ${restorePath}`,
+ rows: plan.map((p) => ({ sku: p.sku, ...p.before })) }, null, 2));
+
+let sql = 'BEGIN;\n';
+for (const p of plan) {
+ const [status, reason] = p.new === 'NONE' ? [null, null] : p.new.split('/');
+ // newly-flagged rows carry an explicit Cody flag so nothing is "cleared by omission"
+ const cody = p.new === 'NONE' ? null : p.old === 'NONE' ? 'tk10484_matcher_fix_new_flag' : p.before.settlement_cody_flag;
+ sql += `UPDATE jeffrey_stevens_catalog SET settlement_status=${q(status)}, settlement_flag_reason=${q(reason)}, settlement_cody_flag=${q(cody)}, settlement_flagged_at=now() WHERE sku=${q(p.sku)};\n`;
+}
+psqlStdin(sql + 'COMMIT;\n');
+
+const back = new Map(psqlRows(`select sku, coalesce(settlement_status,''), coalesce(settlement_flag_reason,'') from jeffrey_stevens_catalog where sku in (${plan.map((p) => q(p.sku)).join(',')})`)
+ .map(([s, st, rs]) => [s, st ? `${st}/${rs}` : 'NONE']));
+const bad = plan.filter((p) => back.get(p.sku) !== p.new);
+if (bad.length) { console.error(`READ-BACK MISMATCH on ${bad.length} rows — run --rollback ${restorePath}`); process.exit(3); }
+console.log(`APPLIED + read-back verified: ${plan.length} rows. Restore: ${restorePath}`);
+console.log(`Undo: node scripts/tk10484-settlement-flag-correct.mjs --rollback ${restorePath}`);
diff --git a/scripts/tk10484-settlement-matcher-audit.mjs b/scripts/tk10484-settlement-matcher-audit.mjs
new file mode 100644
index 0000000..42ab733
--- /dev/null
+++ b/scripts/tk10484-settlement-matcher-audit.mjs
@@ -0,0 +1,76 @@
+#!/usr/bin/env node
+// TK-10484 — READ-ONLY audit: (1) validate the reconstructed legacy matcher against the labels
+// actually applied to jeffrey_stevens_catalog, (2) measure what changes under the word-boundary fix.
+// Never writes to the DB. Writes one audit JSON to data/.
+//
+// node scripts/tk10484-settlement-matcher-audit.mjs [--out <path>]
+import { execFileSync } from 'node:child_process';
+import { writeFileSync, readFileSync, existsSync } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname, join } from 'node:path';
+import { classify, key } from './lib/tk10484-settlement-matcher.mjs';
+
+const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
+const HELD_SKUS = new Set(['DWAA-510300-Roll']); // Steve DISAPPROVED 2026-09-24 20:20 + 20:45 — final
+const DECISIONS = join(ROOT, 'tmp', 'settlement-viewer', 'decisions.jsonl');
+
+const tsv = execFileSync('psql', ['-h', '/tmp', '-d', 'dw_unified', '-v', 'ON_ERROR_STOP=1', '-At', '-F', '\t', '-c',
+ `select sku, coalesce(name,''), coalesce(settlement_status,''), coalesce(settlement_flag_reason,''),
+ case when archived_at is null then 'live' else 'arch' end
+ from jeffrey_stevens_catalog where sku like 'DWAA-%' order by sku`], { encoding: 'utf8' });
+const rows = tsv.trim().split('\n').filter(Boolean).map((l) => {
+ const [sku, name, status, reason, arch] = l.split('\t');
+ return { sku, name, cur: status ? `${status}/${reason}` : 'NONE', live: arch === 'live' };
+});
+if (!rows.length) { console.error('NOT-MEASURED: population query returned 0 rows'); process.exit(2); }
+
+// Steve's latest decision per sku (viewer log) — reported, and used by the correction to hold rows.
+const steve = {};
+if (existsSync(DECISIONS)) for (const l of readFileSync(DECISIONS, 'utf8').split('\n').filter(Boolean)) {
+ try { const e = JSON.parse(l); if (e.decision === 'clear') delete steve[e.sku]; else steve[e.sku] = e; } catch {}
+}
+
+const out = { ts: new Date().toISOString(), population: rows.length, flagged_now: 0, flagged_live_now: 0,
+ replica: { reproduced: 0, of: 0, mismatches: [] }, changes: [] };
+for (const r of rows) {
+ const legacy = key(classify(r.name, { mode: 'legacy' }));
+ const wordC = classify(r.name, { mode: 'word' });
+ const word = key(wordC);
+ if (r.cur !== 'NONE') { out.flagged_now++; if (r.live) out.flagged_live_now++; }
+ // replica fidelity is judged on the whole population: every applied label AND every non-flag
+ if (legacy === r.cur) { if (r.cur !== 'NONE') out.replica.reproduced++; }
+ else out.replica.mismatches.push({ sku: r.sku, title: r.name, applied: r.cur, replica: legacy });
+ if (word !== r.cur) {
+ const legacyOld = r.cur === 'NONE' ? 'NONE' : r.cur;
+ out.changes.push({
+ sku: r.sku, title: r.name, live: r.live, old: legacyOld, new: word, matched: wordC?.matched || [],
+ direction: legacyOld === 'NONE' ? 'newly_flagged' : word === 'NONE' ? 'dropped' : 'reclassified',
+ steve_decision: steve[r.sku]?.decision || null,
+ // replica_mismatch = the change comes from the lost legacy matcher's unexplained behaviour,
+ // NOT from the boundary fix — never corrected automatically.
+ cause: legacy !== r.cur ? 'replica_mismatch' : 'word_boundary_fix',
+ held: HELD_SKUS.has(r.sku) || !!steve[r.sku] || legacy !== r.cur,
+ });
+ }
+}
+out.replica.of = out.flagged_now;
+const inViewer = out.changes.filter((c) => c.old !== 'NONE' && c.live);
+out.summary = {
+ changed_total: `${out.changes.length} of ${out.population}`,
+ changed_among_flagged: `${out.changes.filter((c) => c.old !== 'NONE').length} of ${out.flagged_now}`,
+ changed_among_viewer_82: `${inViewer.length} of ${out.flagged_live_now}`,
+ newly_flagged: out.changes.filter((c) => c.direction === 'newly_flagged').length,
+ caused_by_fix: `${out.changes.filter((c) => c.cause === 'word_boundary_fix').length} of ${out.population}`,
+ actionable_in_viewer_82: `${inViewer.filter((c) => !c.held).length} of ${out.flagged_live_now}`,
+ held_skus: out.changes.filter((c) => c.held).map((c) => c.sku),
+};
+const outPath = process.argv.includes('--out') ? process.argv[process.argv.indexOf('--out') + 1]
+ : join(ROOT, 'data', `tk10484-settlement-matcher-audit-${out.ts.slice(0, 10)}.json`);
+writeFileSync(outPath, JSON.stringify(out, null, 2));
+
+console.log(`population (DWAA-*): ${out.population} flagged now: ${out.flagged_now} flagged+non-archived (viewer): ${out.flagged_live_now}`);
+console.log(`legacy replica reproduces ${out.replica.reproduced} of ${out.flagged_now} applied labels; ${out.replica.mismatches.length} population rows disagree:`);
+for (const m of out.replica.mismatches) console.log(` ${m.sku} | ${m.title} | applied=${m.applied} replica=${m.replica}`);
+console.log(`\nWORD-BOUNDARY CHANGE-SET: ${out.summary.changed_total} rows (among flagged: ${out.summary.changed_among_flagged}; among viewer set: ${out.summary.changed_among_viewer_82})`);
+for (const c of out.changes) console.log(` [${c.direction}${c.cause === 'replica_mismatch' ? ' REPLICA-RESIDUAL' : ''}${c.held ? ' HELD' : ''}${c.live ? '' : ' archived'}] ${c.sku} | ${c.title} | ${c.old} -> ${c.new}${c.matched.length ? ` (${c.matched.join(',')})` : ''}${c.steve_decision ? ` steve=${c.steve_decision}` : ''}`);
+console.log(`\naudit JSON: ${outPath}`);
← 40908be TK-10484: dry-run-default staging script for 8 recovered Jef
·
back to Designerwallcoverings
·
TK-11076: Option B — undo DELETEs a metafield the forward ru f47fd6c →