← back to Sanderson Onboard
tk10873/build_harvest_proposals.mjs
70 lines
#!/usr/bin/env node
// build_harvest_proposals.mjs — TK-10873 cycle 6+. Merge the harvest_batch*.json US-retail
// findings into a single proposals artifact for the HELD_UNVERIFIED rows. PROPOSALS ONLY —
// nothing is promoted to REPRICE here (per the cycle-5 Cody lesson: web-confirm per SKU;
// promotion to reprice-eligible is a later gated step once a batch is confirmed + reviewed).
//
// READ-ONLY. NO Shopify/dw_unified writes. Reads harvest_batch*.json + v3, writes local files.
import fs from 'node:fs';
const DIR = new URL('.', import.meta.url).pathname;
const V3_IN = `${DIR}zoffany_optionA_draft_v3.json`;
const OUT = `${DIR}held_price_proposals.json`;
const MD_OUT = `${DIR}held_price_proposals.md`;
const SAMPLE = 4.25;
// Pure: classify one harvest row into a disposition. Testable.
export function dispositionOf(row) {
if (row.status === 'FABRIC_MISCLASS') return 'RECLASSIFY_FABRIC'; // wrong product type — not a wallpaper roll
if (row.status === 'NOT_FOUND' || row.us_retail == null) return 'HOLD_NO_US_PRICE';
const p = Number(row.us_retail);
if (!(p > SAMPLE)) return 'HOLD_BAD_PRICE';
if (row.status === 'PROPOSED_UNIT_FLAG') return 'PROPOSE_UNIT_REVIEW'; // per-metre/double-roll — unit needs review
if (row.status === 'PROPOSED_SIBLING_EST') return 'PROPOSE_LOW_CONF'; // sibling-estimated, med conf
return 'PROPOSE'; // firm US price
}
function main() {
const v3 = JSON.parse(fs.readFileSync(V3_IN, 'utf8'));
const heldSet = new Set(v3.filter(r => r.action === 'HELD_UNVERIFIED').map(r => r.mfr_sku));
const batches = fs.readdirSync(DIR).filter(f => /^harvest_batch\d+\.json$/.test(f)).sort();
const proposals = [];
for (const b of batches) {
const data = JSON.parse(fs.readFileSync(DIR + b, 'utf8'));
for (const row of data.rows) {
const inHeld = heldSet.has(row.mfr_sku);
proposals.push({ ...row, batch: data.batch, in_held_set: inHeld, disposition: dispositionOf(row) });
}
}
const byDisp = {};
for (const p of proposals) byDisp[p.disposition] = (byDisp[p.disposition] || 0) + 1;
const harvested = proposals.map(p => p.mfr_sku);
const remaining = [...heldSet].filter(m => !harvested.includes(m));
const out = {
ticket: 'TK-10873', generated_at: new Date().toISOString(), read_only: true,
held_total: heldSet.size, harvested: proposals.length, remaining_to_harvest: remaining.length,
dispositions: byDisp,
note: 'PROPOSALS ONLY — no REPRICE promotion here. PROPOSE rows have a firm US retail; UNIT_REVIEW/LOW_CONF need a human look; RECLASSIFY_FABRIC is a data-type bug; HOLD_* stay held. Live reprice + any promotion stays Steve-gated.',
proposals,
remaining_mfr_skus: remaining,
};
fs.writeFileSync(OUT, JSON.stringify(out, null, 2));
const md = [`# TK-10873 — HELD_UNVERIFIED US-retail harvest proposals`, '',
`- Held total: ${heldSet.size} · harvested: ${proposals.length} · remaining: ${remaining.length}`,
`- Dispositions: ${JSON.stringify(byDisp)}`, '',
`| mfr_sku | pattern | zc | proposed US | disposition | conf | source |`,
`|---|---|---|---|---|---|---|`,
...proposals.map(p => `| ${p.mfr_sku} | ${p.pattern} | $${p.zc_price} | ${p.us_retail == null ? '—' : '$' + p.us_retail} | ${p.disposition} | ${p.confidence} | ${p.source} |`),
'', `_PROPOSALS ONLY — Steve/pricing-owner reviews before any promotion; live reprice gated._`, ''];
fs.writeFileSync(MD_OUT, md.join('\n') + '\n');
console.log('=== held-price harvest proposals (READ-ONLY) ===');
console.log(`held=${heldSet.size} harvested=${proposals.length} remaining=${remaining.length}`);
console.log(`dispositions=${JSON.stringify(byDisp)}`);
console.log(`artifacts:\n ${OUT}\n ${MD_OUT}`);
}
if (import.meta.url === `file://${process.argv[1]}`) main();