[object Object]

← 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

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 →