← back to Dw Sku Integrity

dwsku-backlog-scan.mjs

138 lines

#!/usr/bin/env node
// dwsku-backlog-scan.mjs — READ-ONLY scanner for the canonical dw_sku backlog.
//
// Runs the deterministic classifier (classify.mjs) over every ACTIVE product
// whose dw_sku is blank, and reports the A-E / job segmentation plus a plan
// JSONL of (row -> class -> recovered candidate -> collision flag).
//
// SAFETY: this tool issues ONLY SELECT statements. It never UPDATE/INSERT/
// DELETEs, never mints a code, never writes to the database. The single
// artifact it writes is a local plan/summary file under --out.
//
// PORTABILITY: the psql prefix is parameterized so the SAME tool produces the
// canonical number on Kamatera and the mirror number on Mac2:
//   Mac2 mirror (default):  node dwsku-backlog-scan.mjs
//   Kamatera (canonical):   DWSKU_PSQL='ssh <kam> psql' node dwsku-backlog-scan.mjs
//                           (or set DWSKU_PSQL to any psql-compatible prefix)
//
// Parent ticket: TK-10896.

import { execFileSync } from 'node:child_process';
import { writeFileSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { classifyRow, RECOVERY_GROUP } from './classify.mjs';

const US = '\x1f'; // ASCII unit separator — safe field delimiter for code columns
const RS = '\x1e'; // ASCII record separator

// psql prefix (everything up to the -c). Default = local Mac2 mirror socket.
const PSQL = (process.env.DWSKU_PSQL || 'psql -h /tmp -d dw_unified').split(/\s+/);

const argv = process.argv.slice(2);
const outArg = argv.indexOf('--out');
const OUT = outArg >= 0 ? argv[outArg + 1] : null;
const emitPlan = argv.includes('--plan'); // also emit per-row plan JSONL next to --out

function psql(sql) {
  // Feed SQL via STDIN, not -c: over `ssh <host> psql ...` the -c argument would be
  // re-parsed by the REMOTE shell and choke on SQL parens/quotes. psql reads stdin
  // when given no -c/-f, which works identically local and over ssh.
  const args = [...PSQL.slice(1), '-tA', '-F', US, '-R', RS];
  const raw = execFileSync(PSQL[0], args, { input: sql, maxBuffer: 1 << 30, encoding: 'utf8' });
  return raw.split(RS).map((r) => r.replace(/\n$/, '')).filter((r) => r.length).map((r) => r.split(US));
}

const BLANK_ACTIVE = `lower(coalesce(status,''))='active' and (dw_sku is null or btrim(dw_sku)='')`;

console.error(`[dwsku-scan] psql prefix: ${PSQL.join(' ')}`);
console.error('[dwsku-scan] reading active dw_sku code set...');
const activeCodeSet = new Set(
  psql(`select distinct dw_sku from shopify_products where lower(coalesce(status,''))='active' and dw_sku is not null and btrim(dw_sku)<>''`)
    .map((r) => r[0])
);
console.error(`[dwsku-scan] active codes: ${activeCodeSet.size}`);

console.error('[dwsku-scan] reading blank-dw_sku active rows...');
const rows = psql(
  `select coalesce(vendor,''), coalesce(sku,''), coalesce(mfr_sku,''), coalesce(dw_sku,''), coalesce(status,'') ` +
  `from shopify_products where ${BLANK_ACTIVE}`
);
console.error(`[dwsku-scan] blank rows: ${rows.length}`);

const byClass = {};
const byGroup = {};
const byVendorRescrape = {};
const planLines = [];
// Provenance cross-tab: among self-copy-recoverable rows, how many ALSO carry a
// real mfr_sku (so the recovered code can be CROSS-VERIFIED against the vendor's
// staging catalog) vs sku-only (no independent check). Higher mfr coverage =
// stronger evidence the code is scraper-assigned, not mint residue.
let selfCopyWithMfr = 0;
let selfCopyNoMfr = 0;

// First pass: count how many blank rows resolve to each self-copy candidate, so
// we can distinguish a candidate shared by a pattern's own sellable+sample rows
// (BENIGN — dw_sku is intentionally non-unique across a pattern's variants) from
// a true cross-product collision (caught separately by the distinct-title test /
// activeCodeSet check). A downstream apply MUST NOT treat dw_sku as a per-row
// unique key; these shared groups are surfaced so it doesn't choke on them.
const candCount = new Map();
for (const [vendor, sku, mfr_sku, dw_sku, status] of rows) {
  const v = classifyRow({ vendor, sku, mfr_sku, dw_sku, status }, activeCodeSet);
  if (v.candidate) candCount.set(v.candidate, (candCount.get(v.candidate) || 0) + 1);
}
let sharedGroupRows = 0;
let sharedGroups = 0;
for (const n of candCount.values()) if (n > 1) { sharedGroups++; sharedGroupRows += n; }

for (const [vendor, sku, mfr_sku, dw_sku, status] of rows) {
  const v = classifyRow({ vendor, sku, mfr_sku, dw_sku, status }, activeCodeSet);
  const sharedCandidate = !!(v.candidate && candCount.get(v.candidate) > 1);
  byClass[v.class] = (byClass[v.class] || 0) + 1;
  const grp = RECOVERY_GROUP[v.class] || 'unknown';
  byGroup[grp] = (byGroup[grp] || 0) + 1;
  if (grp === 'recoverable_now_self_copy') {
    if (mfr_sku && mfr_sku.trim()) selfCopyWithMfr++;
    else selfCopyNoMfr++;
  }
  if (grp === 'rescrape_program_TK10900') {
    byVendorRescrape[vendor] = (byVendorRescrape[vendor] || 0) + 1;
  }
  if (emitPlan) planLines.push(JSON.stringify({ vendor, sku, mfr_sku, class: v.class, candidate: v.candidate, collides: v.collides, shared_pattern_candidate: sharedCandidate, group: grp }));
}

const summary = {
  generated_by: 'dwsku-backlog-scan.mjs',
  ticket: 'TK-10896',
  psql_prefix: PSQL.join(' '),
  source_note: process.env.DWSKU_PSQL ? 'CUSTOM (verify which DB)' : 'Mac2 dw_unified mirror (NOT canonical — Kamatera owns shopify_products)',
  active_code_count: activeCodeSet.size,
  active_blank_dwsku: rows.length,
  by_class: Object.fromEntries(Object.entries(byClass).sort((a, b) => b[1] - a[1])),
  by_recovery_group: Object.fromEntries(Object.entries(byGroup).sort((a, b) => b[1] - a[1])),
  self_copy_provenance: {
    with_real_mfr_sku_cross_verifiable: selfCopyWithMfr,
    sku_only_no_independent_check: selfCopyNoMfr,
  },
  within_batch_shared_candidates: {
    note: 'BENIGN: same-pattern sellable+sample rows resolving to one shared dw_sku (dw_sku is intentionally non-unique per pattern). These are NOT collisions; an apply step must upsert, not assume per-row uniqueness. True cross-product collisions are counted under *_COLLISION classes.',
    shared_candidate_groups: sharedGroups,
    rows_in_shared_groups: sharedGroupRows,
  },
  rescrape_cohorts_top: Object.fromEntries(Object.entries(byVendorRescrape).sort((a, b) => b[1] - a[1]).slice(0, 15)),
};

const out = JSON.stringify(summary, null, 2);
console.log(out);

if (OUT) {
  mkdirSync(dirname(OUT), { recursive: true });
  writeFileSync(OUT, out + '\n');
  console.error(`[dwsku-scan] wrote summary -> ${OUT}`);
  if (emitPlan) {
    const planPath = OUT.replace(/\.json$/, '') + '.plan.jsonl';
    writeFileSync(planPath, planLines.join('\n') + '\n');
    console.error(`[dwsku-scan] wrote plan (${planLines.length} rows) -> ${planPath}`);
  }
}