← back to Dw Unbuyable Recovery Pilot
TK-11041: portal pull must capture WHICH price it read, not just the number
00cbb254f424235a3f072158a9170c62be87f7f9 · 2026-09-11 11:23:00 -0700 · Steve Abrams
A logged-in trade portal may show our NET price or the LIST price. Capturing the
number without the basis is the same error class as the 10%-vs-20% discount conflict
already blocking this ticket: the number is real, it is just a number about a
different thing. If the portal shows LIST the discount applies on top; if NET it
does not, and applying the wrong one is a silent cost-basis error that reaches
customers through cost/0.65/0.85.
extractPrice now reads the label around the number and records basis NET/LIST/UNKNOWN
plus 120 chars of verbatim context so a human adjudicates in one glance. UNKNOWN is a
legitimate answer and is never guessed into NET. Four new negative tests, 11/11 PASS.
Output carries basis_unknown and the innovations_catalog target columns.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGo6pAQho6w9xgqghMgNKh
Files touched
M tk11041-innovations-reconcile/rescrape/innovations-trade-pull.js
Diff
commit 00cbb254f424235a3f072158a9170c62be87f7f9
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 11 11:23:00 2026 -0700
TK-11041: portal pull must capture WHICH price it read, not just the number
A logged-in trade portal may show our NET price or the LIST price. Capturing the
number without the basis is the same error class as the 10%-vs-20% discount conflict
already blocking this ticket: the number is real, it is just a number about a
different thing. If the portal shows LIST the discount applies on top; if NET it
does not, and applying the wrong one is a silent cost-basis error that reaches
customers through cost/0.65/0.85.
extractPrice now reads the label around the number and records basis NET/LIST/UNKNOWN
plus 120 chars of verbatim context so a human adjudicates in one glance. UNKNOWN is a
legitimate answer and is never guessed into NET. Four new negative tests, 11/11 PASS.
Output carries basis_unknown and the innovations_catalog target columns.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CGo6pAQho6w9xgqghMgNKh
---
.../rescrape/innovations-trade-pull.js | 54 +++++++++++++++++++---
1 file changed, 48 insertions(+), 6 deletions(-)
diff --git a/tk11041-innovations-reconcile/rescrape/innovations-trade-pull.js b/tk11041-innovations-reconcile/rescrape/innovations-trade-pull.js
index a5745a3..13b7c1a 100644
--- a/tk11041-innovations-reconcile/rescrape/innovations-trade-pull.js
+++ b/tk11041-innovations-reconcile/rescrape/innovations-trade-pull.js
@@ -49,13 +49,37 @@ function authState(text) {
return 'LOGGED_OUT';
}
-// Returns {value, unit, raw} or null. NEVER returns 0 and never invents a unit.
+// Is this OUR NET price or the LIST price? Getting this wrong is the same class of error as the
+// 10%-vs-20% discount conflict: the number is real, it is just a number about a DIFFERENT thing.
+// A logged-in trade portal may show either. We never guess — UNKNOWN is a legitimate, reportable
+// answer and forces a human to look once, rather than a silent assumption repricing 39 products.
+const NET_HINT = /\b(your\s*(net|price)|net\s*price|trade\s*price|dealer|wholesale|net\s*less)\b/i;
+const LIST_HINT = /\b(list\s*price|msrp|retail\s*price|suggested)\b/i;
+
+function priceBasis(context) {
+ const net = NET_HINT.test(context), list = LIST_HINT.test(context);
+ if (net && !list) return 'NET';
+ if (list && !net) return 'LIST';
+ return 'UNKNOWN'; // both or neither — do not pick one
+}
+
+// Returns {value, unit, raw, basis, context} or null. NEVER returns 0, never invents a unit,
+// never assumes a basis.
function extractPrice(text) {
- const m = text.match(/\$\s?([0-9][0-9,]*(?:\.[0-9]{1,2})?)\s*(?:\/|per\s+)?\s*(yard|yd|lineal\s*yard|roll|sq\s*ft|each)?/i);
+ const re = /\$\s?([0-9][0-9,]*(?:\.[0-9]{1,2})?)\s*(?:\/|per\s+)?\s*(yard|yd|lineal\s*yard|roll|sq\s*ft|each)?/i;
+ const m = text.match(re);
if (!m) return null;
const value = parseFloat(m[1].replace(/,/g, ''));
if (!Number.isFinite(value) || value <= 0) return null; // a $0 token is a defect, not a price
- return { value, unit: m[2] ? m[2].toLowerCase().replace(/\s+/g, ' ') : null, raw: m[0].trim() };
+ const at = m.index || 0;
+ const context = text.slice(Math.max(0, at - 120), at + 60).replace(/\s+/g, ' ').trim();
+ return {
+ value,
+ unit: m[2] ? m[2].toLowerCase().replace(/\s+/g, ' ') : null,
+ raw: m[0].trim(),
+ basis: priceBasis(context),
+ context, // kept verbatim so a human can adjudicate basis in one glance
+ };
}
// One page -> one verdict. This is the whole safety contract in one function, so the
@@ -94,6 +118,14 @@ function selftest() {
classifyPage('Sign Out | My Account — Costine CSO-001').status === 'NOT_MEASURED');
check('a $0.00 token is rejected, not recorded as a price',
classifyPage('Sign Out Account 58315 — Price $0.00').status === 'NOT_MEASURED');
+ check('basis NET is read from the label, not assumed',
+ classifyPage('Sign Out 58315 Your Net Price $84.50 per yard').price.basis === 'NET');
+ check('basis LIST is read from the label, not assumed',
+ classifyPage('Sign Out 58315 List Price $84.50 per yard').price.basis === 'LIST');
+ check('an unlabelled price is UNKNOWN basis, never guessed NET',
+ classifyPage('Sign Out 58315 Costine CSO-001 $84.50 per yard').price.basis === 'UNKNOWN');
+ check('both NET and LIST labels present => UNKNOWN, not a coin flip',
+ classifyPage('Sign Out 58315 List Price / Your Net Price $84.50').price.basis === 'UNKNOWN');
check('authed page WITH price => MEASURED + numeric',
(() => { const v = classifyPage('Sign Out Account 58315 — $84.50 per yard');
return v.status === 'MEASURED' && v.price.value === 84.5 && /yard/.test(v.price.unit); })());
@@ -182,7 +214,7 @@ function creds() {
await page.waitForTimeout(THROTTLE_MS + 1200);
const text = await page.evaluate(() => document.body.innerText);
const v = classifyPage(text);
- console.log(` ${v.status.padEnd(15)} ${pattern.padEnd(12)} ${v.price ? '$' + v.price.value + (v.price.unit ? '/' + v.price.unit : '') : ''}`);
+ console.log(` ${v.status.padEnd(15)} ${pattern.padEnd(12)} ${v.price ? '$' + v.price.value + (v.price.unit ? '/' + v.price.unit : '') + ' basis=' + v.price.basis : ''}`);
results.push({ pattern, url: url_, ...v, chars: text.length });
if (v.status === 'SESSION_FAILURE') {
aborted = `session dropped at ${pattern} — aborting rather than recording ${targets.length - results.length} false absences`;
@@ -203,11 +235,21 @@ function creds() {
pages_attempted: targets.length, pages_returned: results.length,
measured: results.filter(r => r.status === 'MEASURED').length,
not_measured: results.filter(r => r.status === 'NOT_MEASURED').length,
+ basis_unknown: results.filter(r => r.price && r.price.basis === 'UNKNOWN').length,
aborted,
retail_computed: false,
retail_blocked_reason:
- 'Discount basis unsettled: vendor_registry.innovations=10.00 vs "Net Less 20%" on every DW ' +
- 'sample request for acct 58315. Retail must not be computed until TK-11041 settles it.',
+ 'TWO separate unknowns, both of which must be closed before retail is computed. ' +
+ '(1) DISCOUNT: vendor_registry.innovations=10.00 vs "Net Less 20%" on every DW sample request ' +
+ 'for acct 58315. (2) BASIS: whether the portal number is our NET or the LIST price — see ' +
+ 'basis_unknown and each result.price.context. If the portal shows LIST, the discount applies ' +
+ 'on top; if NET, it does not. Applying the wrong one is a silent cost-basis error that ' +
+ 'propagates straight through cost/0.65/0.85 to customers.',
+ target_columns: {
+ note: 'innovations_catalog already has the columns; the write itself stays gated.',
+ net_price: 'price_trade', unit: 'price_unit', retail: 'our_price',
+ provenance: "price_source = 'innovations portal <date>'", stamped: 'price_updated_at',
+ },
results,
};
const outPath = path.join(OUT_DIR, `portal-pull-${ts}.json`);
← 0e2141a TK-11041: portal-pull script written + self-tested (7/7), wa
·
back to Dw Unbuyable Recovery Pilot
·
TK-11041: settle Innovations (acct 58315) discount conflict b756810 →