Skip to content

History-Aware Item Matching + Match-Identity Clarity

Owner: David Boone · Status: PLAN (autoplan review in progress) · Priority: High (blocks CK UAT confidence) Synthesized: 2026-06-15 from two inputs — (1) “full solution to item matching using customers’ past ordering history”, (2) the pasted Review V2 — Match Identity & Price Clarity UX brief (Appendix A).


⚠️ SUPERSESSION (2026-06-15, after final-gate interrogation)

Section titled “⚠️ SUPERSESSION (2026-06-15, after final-gate interrogation)”

Track 1 (the matching engine) is superseded by the existing, already-autoplan-reviewed docs/designs/xref-bootstrap-from-history.md + its coded packages/db/scripts/backfill-xrefs-from-history.ts. That doc’s mechanism (fill the existing Typesense cross_references P1 bucket from manual_cross_references; recency-decay + quarantine-on-conflict confidence) is better and further along than the Track 1 synthesized here. Do not build Track 1 from this doc. This doc’s lasting contribution is Track 2 (match-clarity UX) + three augmentations the bootstrap doc under-specifies:

  1. Confirmation capture is missingconfirmItem (Confirm/J/Enter) writes nothing today; only the ⌘K modal does. The “hook into review-v2 confirm” is a real new write, not a one-liner.
  2. Persist/autoReprice path auto-ships with no review surface — the confirm/reject UI only fires in auto-validate, not persist-auto-priced-matches. Gate history auto-apply there.
  3. Backfill source is too narrow — it filters customer_part_number IS NOT NULL, but that field isn’t populated by the extractor, so description-only lines are skipped. Broaden the backfill source with findItemPattern’s name tier to cover them. Eng-review decisions #19 (findItemPattern-as-new-bucket) is revised: keep the bootstrap doc’s cross_references mechanism; use findItemPattern only to widen the backfill source.

Re-review of the bootstrap doc vs current tree (2026-06-15) — what’s actually true

Section titled “Re-review of the bootstrap doc vs current tree (2026-06-15) — what’s actually true”
  • Mechanism SOUND: Typesense cross_references IS the customer+connection-scoped P1 bucket, and it self-gates stale IDs (fetches the xref’s target product; drops the hit if SKU absent/deleted; + Jaccard guard) — typesense-search-service.ts:1821-1885. This is the real stale-ID defense.
  • The doc’s specified backfill is INERT for CK: backfill-xrefs-from-history.ts filters customer_part_number IS NOT NULL, but the extractor never populates that field (codes go to item_ids[]; only VMS sets it). Author already pivoted to backfill-xrefs-from-golden.ts keyed on item_ids + item_name. Adopt -from-golden, not -from-history.
  • Stale-ID defense is RUNTIME-ONLY: the dagster cross_references asset has no reconcile step (the products asset does), no decay built, gold model doesn’t join live products. Dead xrefs linger, masked only at query time. New delta #4: add a cross_references reconcile to the dagster asset (mirror products’ reconcile_documents) so SKU-collapse dead rows get swept, not just runtime-hidden.
  • STALE refs: doc + scripts reference retired connection 093e4161 — update to current sandbox 4f234677.
  • Net deltas this autoplan contributes to the bootstrap doc: (#1) build the live capture hook — confirmItem writes nothing today; (#2) gate the persist/autoReprice path (no review surface there); (#3) key the go-forward source on item_ids+item_name (= -from-golden/findItemPattern tiers), not customer_part_number; (#4) add cross_references reconcile on reindex.

Typesense gives us deterministic, catalog-driven matching. It cannot capture the single strongest real-world signal: a given customer reorders the same items, and the operator already told us the right ERP product for those items on prior POs. Today that signal is visible to operators (the Pattern card reads order history) but is thrown away by the matcher — it never closes the loop. This plan does two complementary things: (Track 1) turn confirmed history into a high-precedence matching signal (a learning loop), and (Track 2) make the match visible and trustworthy in Review V2 so operators stop distrusting correct matches. The two tracks reinforce each other: every confirmation an operator makes in the clearer UI becomes training data for the matcher.


1.1 The matcher ignores the customer’s own history (Track 1)

Section titled “1.1 The matcher ignores the customer’s own history (Track 1)”
  • findProductMatchesBatch (apps/webapp/src/services/search/typesense-search-service.ts) runs a 5-way multi_search: customer xref, universal xref, fuzzy product, UPC-filter, name+description. Phase A of docs/designs/typesense-matching-architecture.md is built.
  • BUT the customer-keyed cross_references collection is never populated from confirmed matches. manual_cross_references (Postgres) exists but the webapp never writes to it; apps/dagster/dbt/models/bronze/raw_cross_references.sql is still SELECT * FROM iceberg_source('cross_references') with no UNION to the Postgres table. There is no xref-learner.
  • findItemPattern (apps/webapp/src/services/order-item-pattern.ts) already resolves, per customer, the prior accepted (erpItemId, price, qty, PO, date) for a line. It is called only by the review-v2 Pattern card (/api/extracted-orders/[id]/items/[itemId]/pattern). It never informs matching.
  • Net: a CK customer who has ordered “CK PICKLED RED ONION 16 OZ” 12 times, each confirmed by an operator, gets matched from scratch by fuzzy text every time. We re-derive what we already know.

1.2 Match identity is invisible in Review V2 (Track 2)

Section titled “1.2 Match identity is invisible in Review V2 (Track 2)”
  • Rail (ReviewIssuesRail.tsx:531) and center heading (ReviewCenterPane.tsx:~485) render item.itemName (raw OCR) everywhere. The ERP match card (~652) shows only erpItemId (e.g. “774488”) — never the product name.
  • The matched product name is available at match time (BatchMatchResult.product.name, typesense-search-service.ts:38) and even flows into persist-auto-priced-matches.ts:343, but is not persisted as a column and not shown. Confirmed gap.

1.3 Document price vs. ERP price is ambiguous (Track 2)

Section titled “1.3 Document price vs. ERP price is ambiguous (Track 2)”
  • resolveAndReviewPrices (apps/webapp/src/services/pricing/auto-price-resolve.ts) computes an ERP gold-layer / tier price and a source. The price is written to extracted_order_items.unit_price on reprice, but the distinct ERP-resolved price and its source are discarded. LineOverrideEditor (25% deviation threshold) compares only document price vs. operator-typed value — no ERP baseline.

1.4 Pattern card has no empty/error affordance (Track 2)

Section titled “1.4 Pattern card has no empty/error affordance (Track 2)”
  • The card has loading/loaded/null/error states, but null AND error both render nothing (ReviewCenterPane.tsx:~1033-1052). On a 10s timeout the card silently vanishes; operators can’t tell “no history” from “lookup failed.” (The brief’s “stuck loading” is slightly off — the real defect is silent-null, no retry/empty affordance.)

2. What already exists (reuse map — corrected after CEO review)

Section titled “2. What already exists (reuse map — corrected after CEO review)”

CEO-review correction (2026-06-15): a confirmed-match learning loop already exists and is live — the original plan wrongly called it a “stub.” There are THREE mapping systems; the matcher consults two of them in sequence.

CapabilityStatusLocation
5-way multi_search matcher (primary)Builttypesense-search-service.ts:1517-2187
Postgres-native fuzzy fallback after Typesense missBuiltauto-validate.ts:417findFuzzyMatch
partNumberMappings learning loop — LIVE & CLOSEDBuiltwritten by savePartNumberMapping (erpApi.ts:185) from assignErpMatch (orderActions.ts:301); read + reinforced (matchCount++) by findFuzzyMatch (fuzzy-matching.ts:345-384); schema erp-mappings.ts
itemName/itemDescription passed into matcherBuiltauto-validate.ts:303
Customer-keyed cross_references Typesense collectionBuilt, unpopulated from confirmationsfindProductMatchesBatch:1641; typesense.py
manual_cross_references Postgres tableBuilt, NO writers, NO confirmation_count/last_seen_at (only source,confidence,sourceDetail,theirItemId)manual-cross-references.ts:48-72
Order-history lookup per customer+item, tiered identityBuilt, display-onlyorder-item-pattern.ts (findItemPattern)
ERP gold-layer / tier price resolution with sourceBuilt, source discardedauto-price-resolve.ts
Matched product name returned from hitBuilt, not persistedBatchMatchResult.product.name
Vector/hybrid name_embedding searchPartial — vector fallback only, not in batch pathtypesense-search-service.ts:592-618
pricingConfig.autoRepriceOnExtraction per-connection flagBuilterpConnections.extraConfig

2.1 The three mapping systems (precedence today)

Section titled “2.1 The three mapping systems (precedence today)”
  1. Typesense cross_references (primary, customer-scoped) — populated only from P21 Iceberg + stale universal rows; never from confirmations.
  2. partNumberMappings (Postgres) — the live loop, but it fires only as a fallback after Typesense misses, and only on part-code lines (inputPartNumbers), never description-only lines. Low precedence, narrow trigger.
  3. manual_cross_references (Postgres) — dormant; the design doc’s intended customer-xref source, never wired.

The real gap (reframed): confirmed history IS captured and IS consulted — but at the wrong precedence (last-resort fallback) and for the wrong subset (part-codes only). Track 1 is no longer “build a loop”; it’s “promote the loop that exists to a high-precedence signal and broaden its trigger to description lines, with confidence + invalidation.”


3. Track 1 — History-aware matching engine (reframed after CEO review)

Section titled “3. Track 1 — History-aware matching engine (reframed after CEO review)”

3.0 Prerequisite — identity hygiene FIRST (CEO finding F8/F4)

Section titled “3.0 Prerequisite — identity hygiene FIRST (CEO finding F8/F4)”

A history loop built on bad identifiers learns bad mappings faster. For the lead customer (CK) the matching pain traces to stale catalog IDs (the two-series SKU collapse left ~62/76 numeric inventory IDs stale; PO 1204843 already bound stale 261915) and corrupted WhereFour UPCs (UPC enrichment overlay half-built in PR #1527). Do not auto-apply any learned mapping whose target erpItemId is not a member of the current catalog index. Catalog reindex / SKU-collapse MUST invalidate (deactivate) affected learned mappings. This is the foundation the loop sits on.

3.1 Mechanism — add a history bucket to the batch matcher, sourced from findItemPattern (corrected after ENG review)

Section titled “3.1 Mechanism — add a history bucket to the batch matcher, sourced from findItemPattern (corrected after ENG review)”

Eng review (F1/F2) found the original “promote the existing loop” framing mechanically false: the primary matcher findProductMatchesBatch (used by BOTH auto-validate.ts:304 and the autoReprice path persist-auto-priced-matches.ts:322) never queries Postgres. The partNumberMappings loop is only reachable as a post-miss fallback in auto-validate.ts:418 and is invisible to the persist path. And partNumberMappings is the wrong source — connection-scoped, part-codes-only. The right source already exists.

Recommended mechanism (3-way taste call → gate):

  • (b, RECOMMENDED) History bucket from findItemPattern. Add a new high-precedence bucket inside findProductMatchesBatch’s app-code ranking (slot it relative to xref/UPC — see §3.1a), sourced from findItemPattern (order-item-pattern.ts) which is already customer-scoped, name-tiered (erp_item_id → item_ids → item_name), org-double-scoped, and accepted-gated. Resolve its result to a Typesense product via the existing getProductsByErpIds hop — which IS the catalog-membership/stale-ID gate for free (F8: it returns null when the ERP id is absent from the index). Wire the bucket into BOTH callers. This reuses customer-scope + name-tier + accepted-gating that exist and are tested, so it dissolves the cross-customer leak (F10) and the constraint-migration churn (F6).
  • (a) Promote partNumberMappings — rejected: connection-only (cross-customer leak F10), needs a unique-constraint change + legacy customer_id backfill (F6), part-codes-only.
  • (c) Typesense cross_references UNION path — Iceberg/dbt cost + no existing writers (further from “reuse” than the plan implied, F12).

3.1a Precedence + guards (the bucket is not a blank check)

Section titled “3.1a Precedence + guards (the bucket is not a blank check)”
  • Precedence: history bucket sits below corrected cross_references/UPC-exact (so an operator’s explicit xref fix always wins) and above fuzzy-product/name. This prevents the “2am Friday” regression where a stale name-keyed history row out-ranks a just-corrected xref.
  • Jaccard guard: apply the same name-overlap guard the xref/UPC buckets use (:1700,:1768) to the history bucket — the plan must NOT omit it.
  • Graded confidence: the bucket emits a distinct matchType: 'history' and confidence derived from confirmation_count, NOT the hardcoded exact/1.0 the current findPartNumberMappingMatch returns (F3).
  • Name-specificity floor (F7): reject name-keyed auto-apply for low-specificity normalized names (token-count floor; or reject names that map to >1 distinct erpItemId in history) so “ALLOWANCE”/“FREIGHT” don’t bind to one wrong product.

3.2 Confidence gating + capture + demotion (CEO F3 + ENG F5/F9/F11)

Section titled “3.2 Confidence gating + capture + demotion (CEO F3 + ENG F5/F9/F11)”
  • Confirmation CAPTURE is missing today (ENG F5 — critical). savePartNumberMapping fires only from assignErpMatch (manual modal pick). confirmItem (Confirm button + J/Enter, orderActions.ts:313) writes nothing. So “every confirmation is training data” is false now. Add a capture step to confirmItem and the keyboard-confirm path that records an operator confirmation. If using the findItemPattern source, the “table” is the accepted-order history itself — capture = ensuring confirmed rows are queryable with a source/confirmed marker; spec the exact write.
  • Only human confirmations count. Auto-validated rows may surface as suggested but never auto-promote on their own count (no laundering).
  • Demotion that can’t inherit trust (ENG F9): override flows through the fire-and-forget assignErpMatch upsert which does matchCount + 1 on (connectionId, inputPartNumber) — so changing the target silently inherits the old count onto the new product. Reset confirmation count on target change, and await the demotion (not fire-and-forget).
  • Auto-apply only when: operator_confirmations >= 2 AND target ∈ current catalog (§3.0/F8). First confirmation → suggested (Review V2 confirm/reject, §4.5).
  • Persist-path gating (ENG F11 — the riskiest cell): when historyAwareMatching AND autoRepriceOnExtraction are both ON, a history match flows through persist-auto-priced-matches.ts and is written isAutoValidated:true with no operator review (Track-2’s confirm UI only fires in the auto-validate path). Gate history auto-apply in the persist path to suggested/needs-review until reviewed, or block it when autoReprice is on.
  • Schema: confirmation_count/last_seen_at do not exist on manual_cross_references; the findItemPattern path needs no new table but does need a confirmed-marker. Spec per chosen mechanism.

3.3 (DEMOTED) Hybrid embeddings — separate research spike, not this plan (CEO finding F7)

Section titled “3.3 (DEMOTED) Hybrid embeddings — separate research spike, not this plan (CEO finding F7)”

A prior naïve-MiniLM attempt regressed CK product search; only vector-fallback+rerank helped. Repeating it in the batch path is the most speculative, least UAT-relevant track. Cut from this plan; track as a standalone spike with its own eval gate. (Surfaced at the final gate.)

3.4 Per-connection flag — inventory the sprawl (CEO finding F6)

Section titled “3.4 Per-connection flag — inventory the sprawl (CEO finding F6)”

Gate behind matchingConfig.historyAwareMatching on erpConnections.extraConfig, default off, CK sandbox 4f234677 first. But this joins pricingConfig.autoRepriceOnExtraction, CK_UPC_OVERRIDE_CONNECTION_IDS, discount-config, two-series-collapse — a real combinatorial matrix. Plan must inventory existing per-connection flags and state the interaction test burden (history-match × UPC-override × collapse).


4. Track 2 — Match identity & price clarity UX (revised after design review)

Section titled “4. Track 2 — Match identity & price clarity UX (revised after design review)”

4.1 Persist + surface the matched product name

Section titled “4.1 Persist + surface the matched product name”
  • Migration 0078: matched_product_name TEXT. Write in persist activity (persist-auto-priced-matches.ts:343) and assignErpMatch (carry result.name, stop discarding).
  • Center h2 switches to ERP product name when matched. Extraction text is NOT muted — render a persistent Matched from «raw extraction» line at --om-surface-ink-2 (the diff the operator’s eye runs to catch a mismatch). For low-trust matchType (fuzzy/trigram/fulltext) render that line with more weight, not less. (Design F-D2)
  • Long names: clamp h2 to 2 lines + title attr. (F-D8)
  • NULL-name fallback: historical rows have erpItemId but NULL matched_product_name (0078 backfills nothing). Specify: h2 keeps extraction text + ERP card shows “ERP name unavailable” — never silently pass extraction off as the ERP name. Backfill historical matched names where resolvable. (F-D3)
  • Custom/discount lines (isCustomLineItemDiscountMatchPanel) have no ERP name by design: carve them out of the matched-name and red-unmatched treatments entirely. (F-D4)

4.2 Persist + surface the ERP-resolved price (highest trust/safety risk — F-D1, F-D5)

Section titled “4.2 Persist + surface the ERP-resolved price (highest trust/safety risk — F-D1, F-D5)”
  • Migration 0078: erp_price_source TEXT; anchor document price on existing extractedUnitPrice. Store resolved ERP price + source + UOM.
  • ERP card shows “ERP price: $4.97 / case (gold tier)” — UOM mandatory (case-vs-each is the CK GTIN-14 pain). The 25% deviation check must compare like-for-like UOM or suppress when UOMs differ, else false-positives on every case/each line. (F-D5)
  • Suggested ≠ confirmed (CRITICAL): when document price is missing and we show ERP price as a suggestion, it must be visually unmistakable from a real value (dashed underline / ink-3 italic / “suggested” chip) AND block silent Enter — confirming a line whose price is an unconfirmed suggestion requires an explicit accept, never advance-through. (F-D1)
  • Surface the deviation warning + doc-vs-ERP side-by-side inline in the ERP card during the default flow, not gated behind the O override editor. (F-D1)
  • No-ERP-price case → “ERP price: not in catalog”, not blank.

4.3 Pattern card empty/error states (good in brief; spec the divergence)

Section titled “4.3 Pattern card empty/error states (good in brief; spec the divergence)”
  • loaded + null → “No prior orders for this item”. error/timeout → “History unavailable — retry”, not silent null.
  • Divergence callout: compare price and qty independently, skip non-positive prices (credits/discounts trip naive ratios), render an amber line inside the pattern card AND flip the rail dot amber so it’s visible without focusing the line. Threshold ≥2× stated explicitly. (F-D9)

4.4 Sidebar — CONSOLIDATE signals, don’t add a 4th (F-D7, TASTE→gate)

Section titled “4.4 Sidebar — CONSOLIDATE signals, don’t add a 4th (F-D7, TASTE→gate)”
  • The rail row already carries ~6 signals (dot, name, idx, ×qty, status word, MatchTypeBadge, sometimes DiscountAppliedBadge). Adding green/amber/red on top = noise.
  • Recommended: the left dot carries the single weight (green=no attention, amber=needs review incl. price-missing/divergence, red=blocking); keep status word; remove MatchTypeBadge from the rail (redundant with dot+word; match basis belongs in the center ERP card per §4.1). Ensure focused-row orange left-border doesn’t collide with a red dot. (Rail-density consolidation surfaced at gate — it removes an existing element.)

4.5 Suggested → confirm/reject surface (NEW — reuse existing pattern, F-D6, F-D11)

Section titled “4.5 Suggested → confirm/reject surface (NEW — reuse existing pattern, F-D6, F-D11)”
  • §3.2’s suggested state needs a surface. Reuse the existing inline Reject/Confirm banner auto-matched lines already render (ReviewCenterPane.tsx:522-527) — same shell, new amber-info variant, copy that names the source (“Suggested from this customer’s history — confirm to learn it”). Enter=confirm (reuse isAuto path), X=reject; no modal, no new keys. Reject must visibly demote (§3.2).
  • Support (read-only) mode: suggested banner renders informationally (source + confidence), no confirm/reject affordance, no key binding. Confirm new states keep the J→Enter→J focus-advance rhythm (explicit non-regression). (F-D11)

4.6 Token vocabulary (TASTE→gate, F-D10)

Section titled “4.6 Token vocabulary (TASTE→gate, F-D10)”
  • review-v2 uses a parallel hardcoded palette (--om-paper, #1F5F33, etc.), not the DESIGN.md workspace tokens. Recommended: follow the existing review-v2 token set for local consistency (avoid a third color vocabulary in one pane); migrate the whole pane in a separate cleanup. New green/amber/red weights get named tokens, not new hexes.

  • Phase A (days): Track-2 UX + data wiring (migration 0078, persist name+price+source, rail/heading/card surfacing, pattern empty/error states). Ships value immediately, no pipeline risk. Also lights up the operator-confirmation capture (4.1/assignErpMatch) that feeds Track 1.
  • Phase B (days, not the original “week”): Track-1 = promote + broaden the existing partNumberMappings loop (§3.1 recommended path): raise precedence, key on item_name/item_description, add operator_confirmations/last_seen_at + demotion + catalog-membership gate (§3.0/§3.2). Per-connection flag, CK sandbox first. Backfill from operator-confirmed rows only (dry-run → reviewed → live). The Iceberg/manual_cross_references path is the alternative if the gate picks it.
  • Phase C — REMOVED from this plan. Hybrid embeddings → separate research spike with its own eval gate (prior MiniLM regression).
  • Pre-Phase-B gate: run the P2 stability query on CK history; if IDs churn faster than §3.0 invalidation can catch, stop and fix hygiene first.

6. Test / eval plan (outline — full plan to be written by eng phase)

Section titled “6. Test / eval plan (outline — full plan to be written by eng phase)”
  • Extend packages/eval/scripts/run-auto-validation-eval.ts with CK history fixtures: a customer with N prior confirmed orders → assert history xref wins over fuzzy.
  • xref-learner unit tests: idempotent upsert, confirmation_count increment, source precedence.
  • dbt UNION test: Postgres row visible in stg_cross_references without overwriting Iceberg rows.
  • Regression: universal-UPC fixture, vendor-conflict fixture (customer-scoped wins).
  • UX: pattern card null/error/divergence states; matched-name rendering for matched/unmatched/discount-line items; price side-by-side + deviation.

  • Redesigning the three-pane layout. Mobile/responsive. Replacing Typesense. New ERP connectors. (Phase C embeddings are in scope but last and gated.)

  • P1. The user wants the full two-track solution (engine + UX), not just the pasted UX brief — the brief explicitly defers “new matching strategies,” but the user’s framing (“full solution… match with higher fidelity based on past ordering history”) overrides that deferral.
  • P2. “Customers rarely change what they order” is true enough that a confirmed (customer, item) → erpItemId is the single highest-precedence signal, above fuzzy text. (Validated by the existing order-item-pattern design rationale.)
  • P3. Precision beats recall here: a wrong auto-match that looks confident is worse than no match. Hence confidence gating (>=2) and surfacing suggestions for confirm/reject.
  • P4 (corrected). Ship Track 2 first because it is cheap, reversible, and measurement-generating — pure display/persistence, zero precision/pipeline risk, and it surfaces match-name correctness rates that let us validate P2 empirically before betting on the engine. (The original “turns on confirmation-capture” reason was wrong — capture is already live via savePartNumberMapping.) Gate Track 1 on what Track 2’s data reveals about CK match-name correctness.
  • P2 (needs measurement, not assertion). “Customers rarely change what they order” is checkable today: query CK’s confirmed history for (customer, item-desc) → erpItemId stability over time. Given the SKU collapse, IDs demonstrably churn — so P2 is conditionally false unless §3.0 invalidation holds. Run the stability query before Phase B.

(Preserved verbatim from the user’s input — “Review V2 — Match Identity & Price Clarity UX Design Pass”. Sections 1–9: match identity invisible in primary reading positions; doc-price vs ERP-price disambiguation; pattern card stuck loading; design tokens from DESIGN.md; keyboard-first/support-mode interaction notes; out-of-scope; deliverables; engineering dependencies — erpProductName persistence, ERP resolved price, pattern timeout handling; reference screenshots for HEB Houston PO #287756 line 03.)


9. CEO-phase registries (mandatory outputs)

Section titled “9. CEO-phase registries (mandatory outputs)”
ErrorTriggerCaught whereOperator seesTested?
StaleMappingErrorLearned mapping targets erpItemId not in current catalog§3.0 membership gate at apply timeMapping skipped, falls to fuzzy; no silent wrong-bindnew test
MappingPoisonGuardAuto-validated row would increment trust counter§3.2 source checkn/a (suppressed)new test
Pattern fetch timeout / 4xx/5xx/items/[id]/pattern slow or errorsPattern card state machine”History unavailable — retry” (not silent null)new test
Price resolve missresolveAndReviewPrices returns no gold pricepersist activity”ERP price: not in catalog” not blankextend test
Migration 0078 partial applycolumn add fails mid-deploydrizzle migraten/a; additive IF NOT EXISTS, rollback-safemigration test

9.2 Failure Modes Registry (with critical-gap flags)

Section titled “9.2 Failure Modes Registry (with critical-gap flags)”
Failure modeSeverityDefended byGap?
Self-poisoning loop (auto-matches counted as confirmations)CRITICAL§3.2 operator-only counter + demotionCLOSED by plan revision
Stale-ID bind post-SKU-collapseCRITICAL§3.0 catalog-membership gate + invalidationCLOSED by plan revision
Duplicate learning systems disagreeHIGH§3.1 promote-in-place (single loop)CLOSED if reuse path chosen at gate
Per-connection flag interaction bugsMEDIUM§3.4 flag inventory + interaction testsOPEN — needs test matrix
dbt UNION / Iceberg fragilityMEDIUMreuse path avoids Iceberg entirelyCLOSED if reuse path chosen
Embedding maintenance / regressionMEDIUM§3.3 removed to spikeCLOSED (descoped)
  • Hybrid embeddings (→ separate spike). Cross-customer pattern transfer. Active-learning prioritization. Three-pane layout redesign. Mobile. Replacing Typesense. New ERP connectors. → write to TODOS.md.
  • Right problem: Track 2 yes; Track 1 reframed from “build a loop” to “promote the live loop + add safety.” Net scope is smaller and safer than the original plan.
  • Biggest risks (both now defended): self-poisoning, stale-ID bind.
  • Open taste calls for gate: (a) Track-1 mechanism — promote-in-place vs Typesense-cross_references; (b) embeddings descope confirmation.
  • Dream-state delta: builds the self-improving loop + trust surface; defers cross-customer transfer and embeddings.

10.1 Architecture (corrected) — where the history bucket slots

Section titled “10.1 Architecture (corrected) — where the history bucket slots”
auto-validate.ts:304 persist-auto-priced-matches.ts:322
│ │
▼ ▼
┌──────────────────────── findProductMatchesBatch ───────────────────────┐
│ multi_search ranking (app-code, typesense-search-service.ts:1671) │
│ P1 cross_references (customer xref) ← operator's explicit fix wins │
│ P1.5 ★ NEW history bucket ★ (from findItemPattern → getProductsByErpIds│
│ resolve = catalog-membership gate F8; Jaccard guard; graded conf) │
│ P2 UPC-filter exact │
│ P2.5 idstrip exact-id │
│ P3 fuzzy product (name/desc) │
└──────────────────────────────────────────────────────────────────────────┘
│ │
(auto-validate ONLY) unmatched → findFuzzyMatch → partNumberMappings (legacy fallback, unchanged)
CAPTURE LOOP: confirmItem (Confirm/J/Enter) ──[NEW write, F5]──┐
assignErpMatch (manual modal) ──savePartNumberMapping (await, reset-on-retarget F9)─┘
accepted-order history (findItemPattern source) + confirmed marker
read back as P1.5 bucket next PO ◀─────┘ (operator_confirmations>=2 → auto; ==1 → suggested)

Key correction: the bucket lives inside the batch matcher and is wired into both callers — not a precedence tweak on the post-miss findFuzzyMatch fallback (which the persist path never calls).

Full test diagram and eval suite list written to: ~/.gstack/projects/ERP-Unlocked-ordermatic/claude-ecstatic-curie-5b1f9f-test-plan-20260615-074952.md Critical gaps flagged: confirmation-capture test (path writes nothing today), precedence-regression test, persist-path history-gating test, history-bucket Jaccard guard test.

Failure modeSeverityDefended byGap?
History bucket unreachable from primary matcherCRITICAL§3.1 add bucket to findProductMatchesBatch + both callersCLOSED by revision
Confirmation capture absent (confirmItem writes nothing)CRITICAL§3.2 add capture to confirm pathsOPEN — must build
Cross-customer mis-bind (connection-scoped name key)HIGH§3.1 use findItemPattern (customer-scoped)CLOSED if path (b)
Persist/autoReprice auto-ships history w/o reviewHIGH§3.2 persist-path gating to needs-reviewOPEN — must gate
Demotion inherits trust via upsert matchCount+1HIGH§3.2 reset-on-retarget + awaitOPEN — must build
Low-specificity name binds wrong productHIGH§3.1a name-specificity floorOPEN — must build
Graded confidence has no home (returns 1.0/exact)MEDIUM§3.1a matchType:'history' + conf from countOPEN — must build
  • Track 2: sound, ship-first correct, low risk (additive migration + display). Ready to detail.
  • Track 1: mechanism corrected (history bucket from findItemPattern, not partNumberMappings). Net scope is larger than the original “days” framing — it’s a new matcher bucket + two-caller wiring + capture path + persist gating. Honest re-estimate: ~1.5-2 wks human / CC phased over days.
  • Reuse wins: catalog-membership gate (F8), rowVersion CAS (F4), name normalization (F7), customer-scoping (F2) all already exist.

#PhaseDecisionClassificationPrincipleRationaleRejected
1CEOScope = full two-track, UX(Track2) first then engine(Track1)Premise gate (user-confirmed)User confirmed all 4 premises at the gate; not auto-decidedEngine-first; UX-brief-only
2CEOReframe Track 1: promote existing partNumberMappings loop, not build parallel Iceberg oneTASTE (→gate)P4 DRY, P5 explicitA live closed loop already exists (orderActions.ts:301/fuzzy-matching.ts:345); building a parallel one duplicates itParallel manual_cross_references+Iceberg loop
3CEOAdd confirmation_count/last_seen_at/their_upc_variants columns (don’t exist)MechanicalP1Verified absent in manual-cross-references.ts; §3.2 depends on themAssume built
4CEOSelf-poison guard: only operator_selection counts; demotion-on-override; never auto-promote auto-matchesMechanical (critical)P3Backfill from isValidated includes auto-validated → launderingCount any validated row
5CEOStale-ID guard: invalidate learned mappings on catalog reindex; gate apply on catalog membershipMechanical (critical)P1CK SKU collapse → 62/76 stale IDs; loop would re-encode WhereFour bind bugNo invalidation
6CEOCorrect P4 justification to de-risk/measurementMechanicalP6Confirmation capture already live; original reason falseKeep false reason
7CEODemote Phase C embeddings to separate spikeTASTE (→gate)P3, P6Prior MiniLM regressed CK search; not UAT-blockingKeep in plan
8CEOIdentity hygiene = prerequisite foundation (§3.0), not alternativeMechanicalP2Loop on bad IDs learns bad mappings fasterTreat as either/or
9DesignSuggested price visually distinct + block silent Enter + inline deviation (§4.2)Mechanical (critical)P1 trust”Suggestion” rendered as confirmed value → wrong-price shipSame KV row as real value
10DesignKeep extraction as “Matched from” line at ink-2, weight up for fuzzy (§4.1)MechanicalP1Muting it buries the mismatch evidenceMute to ink-3
11DesignNULL matched_product_name fallback + historical backfill (§4.1)MechanicalP1Historical rows have erpItemId but no nameSilent fallback to extraction
12DesignCarve out custom/discount lines from name + red treatments (§4.1)MechanicalP1No ERP name by designTreat as unmatched
13DesignERP price row carries UOM; deviation compares like-UOM (§4.2)MechanicalP1case-vs-each false positives (CK GTIN-14)Single price, naive ratio
14DesignSuggested→confirm reuses existing inline banner, Enter/X, no modal (§4.5)MechanicalP5Pattern already exists at :522-527New modal
15DesignRail: dot carries weight, remove MatchTypeBadge from railTASTE (→gate)P5 subtractionRow already has ~6 signals; 4th = noiseAdd weight on top
16Designh2 long-name clamp + title; divergence skips non-positive, dual-surfaceMechanicalP1wrap/overflow + credit-line ratio tripsunbounded
17DesignSupport read-only: suggested banner info-only, focus non-regression (§4.5)MechanicalP1read-only audit mode must not learnunspecified
18DesignToken vocabulary: follow existing review-v2 palette, defer pane migrationTASTE (→gate)P5avoid a 3rd color vocabulary in one panemix DESIGN.md tokens now
19EngTrack-1 mechanism = history bucket in findProductMatchesBatch from findItemPatternTASTE (→gate)P4 DRY, P5Loop unreachable from primary matcher; findItemPattern already customer-scoped/name-tieredpromote partNumberMappings; cross_references UNION
20EngAdd confirmation capture to confirmItem + keyboard confirmMechanical (critical)P1confirmItem writes nothing today → thesis falseleave capture to manual modal only
21EngGate history auto-apply to needs-review in persist/autoReprice pathMechanical (critical)P1, P3riskiest cell auto-ships w/o review surfacerely on auto-validate UI only
22EngReset confirmation count on retarget; await demotion (not fire-and-forget)MechanicalP3upsert matchCount+1 inherits trust onto new targetkeep fire-and-forget
23EngName-specificity floor + Jaccard guard on history bucketMechanicalP1”ALLOWANCE”/“FREIGHT” bind wrong productname trigger ungated
24EngmatchType:‘history’ + confidence from confirmation_count (not 1.0/exact)MechanicalP3hardcoded 1.0 makes gating invisible downstreamreuse exact/1.0
25EngCite existing catalog-membership gate (getProductsByErpIds null) + rowVersion CASMechanicalP4already built; not net-new worktreat as new
26EngHonest re-estimate: Track 1 ~1.5-2 wks, not “days”MechanicalP6new bucket + 2-caller wiring + capture + gatingkeep “days”
27GateTrack 1 SUPERSEDED by existing xref-bootstrap-from-history.md (+coded backfill)Reframe (user-surfaced via Devin)P4 DRYA better, autoplan-reviewed design + Phase A.5 code already existre-synthesize Track 1 here
28GateREVISE #19: keep bootstrap doc’s cross_references mechanism; use findItemPattern only to widen backfill sourceMechanicalP4cross_references is the existing P1 bucket (no new bucket); findItemPattern fixes the customer_part_number coverage gapfindItemPattern as the bucket
29GateCarry forward 3 deltas as augmentations to bootstrap doc: capture (F5), persist auto-ship (F11), name-tier backfill sourceMechanicalP1bootstrap doc under-specifies thesedrop them
30Re-reviewAdopt -from-golden backfill, NOT -from-history (inert: customer_part_number never extractor-populated)Mechanical (critical)P4author’s own pivot note in -from-golden.ts:5-7; field dead for CKrun -from-history
31Re-reviewDelta #4: add cross_references reconcile to dagster asset (sweep stale xrefs on reindex)MechanicalP1only runtime self-gate today; dead rows lingerrely on self-gate alone
32Re-reviewRefresh stale refs 093e41614f234677; cross_references P1 self-gate confirmed soundMechanicalP6doc predates two-series collapse + Demo retirementleave stale