← back to Dw Sku Integrity

classify.mjs

255 lines

// classify.mjs — deterministic, pure classifier for the canonical dw_sku backlog.
//
// DOCTRINE (Steve, 2026-08-26 — memory `dw-dwsku-keep-old-never-mint`):
//   The canonical dw_sku must RECOVER an EXISTING code (self-copy from `sku`,
//   or re-scrape the real mfr/DW code). It must NEVER mint a new sequential.
//   This module therefore only ever proposes a candidate dw_sku that is
//   ALREADY present on the row (in `sku`) — it can never fabricate one.
//
// Pure functions only: no DB, no I/O, no clock, no randomness. Fully testable.
// Parent ticket: TK-10896.

// The unit / variant suffix vocabulary observed on `sku` for blank-dw_sku rows.
// (Verified counts on the Mac2 dw_unified mirror 2026-08-30:
//  Sample 7325, Yard 2104, Roll/roll 656, Per Yard 42, Panel 12, Bolt 9.)
// Extra common UoM tokens are included defensively so the strip stays correct
// if a vendor introduces them; adding a token here can only REMOVE a trailing
// unit word, never alter the code body.
export const UNIT_SUFFIXES = [
  'Sample', 'Per Yard', 'Yard', 'Double Roll', 'Single Roll', 'Roll',
  'Panel', 'Bolt', 'Each', 'Tile', 'SqFt', 'Sq Ft', 'Yд',
];

// Matches ONE trailing unit suffix, preceded by a '-' or whitespace separator.
// Case-insensitive. "Per Yard" contains a space, hence the \s in the alternatives.
const SUFFIX_RE = new RegExp(
  '[\\s-]+(' +
    UNIT_SUFFIXES
      .slice() // longest-first so "Per Yard" wins over "Yard", "Double Roll" over "Roll"
      .sort((a, b) => b.length - a.length)
      .map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+'))
      .join('|') +
  ')$',
  'i'
);

// PROVENANCE GUARD — Phase-4 "greenfield" mint prefixes (TK-10896, verified via
// the ticket event log + sku-integrity-phase2-phase4 decision aid, 2026-08-26).
// These vendor prefixes had ZERO coded products before the reverted Phase-4 mint,
// so ANY code in one of them now sitting in a blank row's `sku` is MINT RESIDUE,
// not a scraper-native code. Self-copying it would silently re-instate a number
// Steve explicitly reverted ("never mint"). Route these to re-scrape instead.
// Bucket A prefixes (registered==already-in-use: DWKN/DWTT/DWRW/DWJS/DWRO/DWCC)
// are intentionally NOT here — a code there may be scraper-native; disambiguating
// them needs the exact reverted-mint number list (follow-up), so they stay
// self-copy by default rather than over-blocking.
export const GREENFIELD_MINT_PREFIXES = new Set([
  'DWAG', // Carnegie
  'DWAX', // Maharam
  'DWCX', // CMO Paris
  'DWST', // Stout Textiles
  'DWSC', // Scalamandre
  'DWDX', // Designtex
  'DWWG', // Wolf Gordon
]);

// Mixed-use Phase-4 prefixes. These prefixes predated the reverted mint, so a
// prefix match alone cannot say whether a row is scraper-native or mint residue.
// Fail closed into a dedicated provenance-review class until the retained
// canonical undo ledger can identify the exact minted numbers. This deliberately
// quarantines legitimate rows too; it never routes them to re-scrape or collision.
export const MIXED_USE_MINT_PREFIXES = new Set([
  'DWKN', // Knoll
  'DWTT', // Thibaut
  'DWRW', // Rebel Walls
  'DWJS', // Jeffrey Stevens / York legacy shared prefix
  'DWRO', // Romo
  'DWCC', // Novasuede
]);

// ---- mfr_sku PROVENANCE ALLOWLIST (DTD verdict A, 2026-09-02, TK-10900) -----
//
// The mfr_sku column is a REAL manufacturer code for only a verified set of
// vendors. For everyone else it is FABRICATED at import time (Maharam "MH-*",
// CMO Paris "CMO_*"), so recovering dw_sku from mfr_sku there would LAUNDER a
// fabricated code into the canonical identity field — the exact mistake this
// gate makes physically impossible.
//
// RULE: mfr_sku -> dw_sku recovery ("mfr self-copy") is permitted ONLY for a
// vendor on this allowlist. Any other vendor's mfr_sku self-copy is hard-routed
// to re-scrape (never written). Evidence: ~/Projects/dw-sku-integrity/evidence/.
// The allowlist is deliberately SMALL and grows only by Steve-ruled evidence.
export const MFR_SKU_REAL_ALLOWLIST = new Set(['carnegie', 'stout']);

// Normalize a vendor label to the allowlist key (lowercased, trimmed, and the
// vendor's leading token so "Stout Textiles" -> "stout"). Must stay in lockstep
// with how the allowlist keys are spelled.
export function vendorKey(vendor) {
  const v = String(vendor || '').trim().toLowerCase();
  if (!v) return '';
  if (MFR_SKU_REAL_ALLOWLIST.has(v)) return v;          // exact match wins
  const first = v.split(/[\s/|,-]+/)[0];                 // "stout textiles" -> "stout"
  return MFR_SKU_REAL_ALLOWLIST.has(first) ? first : v;
}

// The single provenance predicate the whole pipeline consults. Everything that
// could write an mfr_sku-derived dw_sku MUST gate on this (classify + every plan
// generator), so the fabricated-mfr class can never reach a canonical write.
export function vendorAllowsMfrSelfCopy(vendor) {
  return MFR_SKU_REAL_ALLOWLIST.has(vendorKey(vendor));
}

// Contract-fabric vendors (Carnegie et al.) append a product-type token to the
// real code, e.g. "47081-windows", "100795352-panels-museums". Strip ONLY a
// KNOWN product-type token (repeatedly) so the base colorway code is recovered
// while a genuine colorway word (e.g. "1234-natural") is preserved. Unknown
// tokens are left intact on purpose — a wrong strip could collapse two colorways
// into one identity; the generator's collapse-guard is the second line of defense.
export const MFR_PRODUCT_TYPE_TOKENS = new Set([
  'windows', 'window', 'panels', 'panel', 'museums', 'museum', 'upholstery',
  'wallcovering', 'wallcoverings', 'wall', 'drapery', 'dividers', 'divider',
  'privacy', 'imo', 'vinyl', 'textile', 'textiles', 'ceiling', 'ceilings',
  'wovens', 'woven', 'seating', 'cubicle', 'cubicles',
]);
export function normalizeMfrSku(mfr) {
  if (mfr == null) return mfr;
  let s = String(mfr).trim();
  let prev;
  do {
    prev = s;
    const m = /^(.*?)-([A-Za-z]{2,})$/.exec(s);
    if (m && MFR_PRODUCT_TYPE_TOKENS.has(m[2].toLowerCase())) s = m[1];
  } while (s !== prev && s.length > 0);
  return s;
}

function greenfieldPrefixOf(code, prefixSet) {
  const m = /^(DW[A-Z0-9]{1,6})-/i.exec(code || '');
  if (!m) return null;
  const pfx = m[1].toUpperCase();
  return prefixSet.has(pfx) ? pfx : null;
}

// A canonical DW code prefix, e.g. DWAK-, DWCC-, DWEL-RM-...
const DW_CODE_RE = /^DW[A-Z0-9]{1,6}-/i;
// A Cork- source code (Greenland / Phillipe Romano cork line — self-copy its Cork-<num>).
const CORK_RE = /^cork-/i;
// A literal "null" placeholder sku (import defect, e.g. "null-Sample").
const NULL_RE = /^null([\s-]|$)/i;
// A code-shaped mfr candidate (no whitespace/parens/title junk, ≤40 chars).
// Mirrors apply-plan-gen.mjs CODE_SHAPE so a bad mfr never becomes a dw_sku.
const MFR_CAND_SHAPE = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,39}$/;

/**
 * Strip trailing unit/variant suffix tokens from a sku, repeatedly, so that
 * doubled suffixes ("DWKE-41415-Sample-Sample") collapse to the bare code.
 * Returns the recovered code candidate (the code the row ALREADY holds).
 */
export function stripUnitSuffix(sku) {
  if (sku == null) return sku;
  let s = String(sku).trim();
  let prev;
  do {
    prev = s;
    s = s.replace(SUFFIX_RE, '');
  } while (s !== prev && s.length > 0);
  return s;
}

/**
 * Classify one product row for the canonical-dw_sku backlog.
 *
 * @param {{dw_sku?:string, sku?:string, mfr_sku?:string, status?:string}} row
 * @param {Set<string>} [activeCodeSet] set of dw_sku values already live on an
 *        ACTIVE product — used to flag COLLISION (candidate already taken).
 * @param {{greenfieldPrefixes?:Set<string>,mixedUsePrefixes?:Set<string>}} [opts] provenance-guard config.
 * @returns {{class:string, candidate:(string|null), collides:boolean, note?:string}}
 */
export function classifyRow(row, activeCodeSet, opts = {}) {
  const greenfield = opts.greenfieldPrefixes || GREENFIELD_MINT_PREFIXES;
  const mixedUse = opts.mixedUsePrefixes || MIXED_USE_MINT_PREFIXES;
  const status = String(row.status || '').trim().toLowerCase();
  const dw = String(row.dw_sku || '').trim();
  if (dw) return { class: 'NOT_BLANK', candidate: null, collides: false };
  if (status !== 'active') return { class: 'OUT_OF_SCOPE_STATUS', candidate: null, collides: false };

  const sku = String(row.sku || '').trim();
  const mfr = String(row.mfr_sku || '').trim();
  const has = (c) => !!(activeCodeSet && c && activeCodeSet.has(c));

  // HIGHEST-TRUST PATH — verified-real-mfr vendor (DTD verdict A, TK-10900):
  // recover dw_sku directly from the NORMALIZED mfr_sku. This BEATS the `sku`
  // path because these vendors' `sku` column carries reverted greenfield-mint
  // residue (DWAG/DWAX...) that must never be self-copied. Gated by the machine
  // allowlist — an off-allowlist vendor can NEVER reach this branch.
  if (mfr && vendorAllowsMfrSelfCopy(row.vendor)) {
    const cand = normalizeMfrSku(mfr);
    if (cand && MFR_CAND_SHAPE.test(cand)) {
      const collides = has(cand);
      return { class: collides ? 'SELF_COPY_MFR_COLLISION' : 'SELF_COPY_MFR', candidate: cand, collides, note: `mfr_sku recovery (verified-real vendor '${vendorKey(row.vendor)}')` };
    }
    // Allowlisted vendor but the mfr_sku is not code-shaped -> re-scrape.
    return { class: 'RESCRAPE', candidate: null, collides: false, note: 'verified-real vendor but mfr_sku is not code-shaped' };
  }

  // Literal "null" placeholder sku = scraper import defect (JOB 4).
  if (sku && NULL_RE.test(sku)) {
    // A non-allowlisted vendor's mfr_sku is potentially fabricated -> re-scrape,
    // NEVER staging-link/self-copy (the fabricated-mfr laundering guard).
    if (mfr) return { class: 'MFR_FABRICATED_RESCRAPE', candidate: null, collides: false, note: `null-literal sku; mfr_sku not verified-real for vendor '${String(row.vendor || '').trim()}'` };
    return { class: 'IMPORT_DEFECT', candidate: null, collides: false };
  }

  if (sku) {
    const cand = stripUnitSuffix(sku);
    if (!cand) return { class: 'RESCRAPE', candidate: null, collides: false, note: 'sku was all-suffix' };
    if (DW_CODE_RE.test(cand)) {
      // Provenance guard FIRST: a greenfield-prefix code is reverted-mint residue,
      // never scraper-native → must re-scrape, must NOT self-copy.
      const gf = greenfieldPrefixOf(cand, greenfield);
      if (gf) return { class: 'MINT_RESIDUE_RESCRAPE', candidate: null, collides: false, note: `greenfield mint prefix ${gf} (reverted Phase-4) — recover real mfr code, do not self-copy ${cand}` };
      const collides = has(cand);
      const mixed = greenfieldPrefixOf(cand, mixedUse);
      // A code already owned by another active product is a definite collision;
      // that stronger fact wins over the unresolved mixed-prefix provenance hold.
      if (mixed && collides) return { class: 'SELF_COPY_DW_COLLISION', candidate: cand, collides: true };
      if (mixed) return { class: 'PROVENANCE_REVIEW', candidate: null, collides: false, note: `mixed-use Phase-4 prefix ${mixed} — compare ${cand} with retained undo ledger before self-copy` };
      return { class: collides ? 'SELF_COPY_DW_COLLISION' : 'SELF_COPY_DW', candidate: cand, collides };
    }
    if (CORK_RE.test(cand)) {
      const collides = has(cand);
      return { class: collides ? 'SELF_COPY_COLLISION' : 'SELF_COPY_CORK', candidate: cand, collides };
    }
    // Non-DW, non-cork existing source code — still a recover-existing-code fix.
    const collides = has(cand);
    return { class: collides ? 'SELF_COPY_COLLISION' : 'SELF_COPY_SOURCE', candidate: cand, collides };
  }

  // No usable sku. Reaching here with an mfr means the vendor is NOT on the
  // verified-real allowlist (allowlisted vendors were handled at the top), so
  // the mfr_sku is potentially fabricated -> re-scrape, never self-copy.
  if (mfr) return { class: 'MFR_FABRICATED_RESCRAPE', candidate: null, collides: false, note: `mfr_sku not verified-real for vendor '${String(row.vendor || '').trim()}' — re-scrape, never self-copy` };
  return { class: 'RESCRAPE', candidate: null, collides: false };
}

// Recovery-path grouping used for the backlog rollup / job routing.
export const RECOVERY_GROUP = {
  SELF_COPY_DW: 'recoverable_now_self_copy',
  SELF_COPY_CORK: 'recoverable_now_self_copy',
  SELF_COPY_SOURCE: 'recoverable_now_self_copy',
  // mfr_sku recovery — verified-real-mfr vendors ONLY (Carnegie, Stout).
  SELF_COPY_MFR: 'recoverable_now_mfr',
  SELF_COPY_MFR_COLLISION: 'dedup_gated_TK10649',
  STAGING_LINK: 'recoverable_now_staging_link', // legacy — no longer produced by classify
  SELF_COPY_DW_COLLISION: 'dedup_gated_TK10649',
  SELF_COPY_COLLISION: 'dedup_gated_TK10649',
  RESCRAPE: 'rescrape_program_TK10900',
  IMPORT_DEFECT: 'rescrape_program_TK10900',
  MINT_RESIDUE_RESCRAPE: 'rescrape_program_TK10900',
  // Fabricated mfr_sku on a non-allowlisted vendor — must re-scrape, never self-copy.
  MFR_FABRICATED_RESCRAPE: 'rescrape_program_TK10900',
  PROVENANCE_REVIEW: 'provenance_review_TK10896',
  NOT_BLANK: 'out_of_scope',
  OUT_OF_SCOPE_STATUS: 'out_of_scope',
};