[object Object]

← 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

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 →