Item-matching precision levers (per connection)
A wrong item on an order is worse than an unmatched line, so these levers target bind precision. Most of them only abstain (allIdFuzzy on ambiguity,
suppressPlaceholderNameBind), but four can add binds and need measured evidence before they are turned on: prefixCompatibility (removes a block),
codeBearingRelevance (loosens the relevance gate), excludePlaceholderNameSearch (a name bind can land on a stocked item instead of a placeholder) and
despacedNameId (binds a code-less line at identity grade). Each one lives under
erp_connections.extra_config.matchingConfig, is off unless configured, and is AND-composed with the fail-open Flagsmith kill-switch
matching_precision_v1 (only asked when a lever is configured; it can narrow an opted-in connection, never widen a connection that is not).
With nothing configured the matcher is byte-identical, so existing tenants are unchanged.
They apply on both bind paths: the operator-triggered POST /api/erp/parts/auto-validate (batch matcher, then the findFuzzyMatch fallback) and the TS worker’s
persistAutoPricedMatches (batch matcher only). prefixCompatibility only exists on the fallback path.
Levers
Section titled “Levers”| Key | Config | What it does |
|---|---|---|
allIdFuzzy |
{ "enabled": true } |
Searches every printed id, not just itemIds[0]. Implies the all-ids exact slot (erp_product_id or sku, like allIdExact). The batch matcher also runs the fuzzy id search for up to two extra ids and converges or abstains: two ids that fuzzy-match different products leave the line unmatched (multi_id_conflict) and skip the fuzzy fallback. The fallback does exact-on-every-id before any fuzzy, then the same converge-or-abstain. An exact bind always wins. Measured caveat: on the LB Water example set the gain comes from the exact all-ids slot (which the existing allIdExact already provides on the auto-validate route); the fuzzy-on-extra-ids half bound 1 correct and 3 wrong lines, so prefer allIdExact unless you have evidence for the fuzzy half. |
mpnFirst |
{ "enabled": true } |
Reverses the non-blank itemIds so the last printed id is tried first. Assumes the PO prints the customer’s own code before the manufacturer part number; wrong for POs (or extractors) that already put the MPN first. |
placeholderItems |
{ "idPrefixes": ["MSC-"] } |
Marks non-stock placeholder catalog items by case-insensitive id prefix (letters, digits, ., _, -; max 8). Data for the next three levers, inert without them. |
suppressPlaceholderNameBind |
{ "enabled": true } |
The name bucket never binds a placeholder. The line stays unmatched but, unlike suppressNameBucket, may still reach the id-keyed fuzzy fallback (informational abstain placeholder_name_bind_suppressed). |
suppressPlaceholderFuzzyIdBind |
{ "enabled": true } |
The id-keyed fuzzy slots (batch Priority 3, the allIdFuzzy extra-id slot, and the fallback’s findProductMatch with or without allIdFuzzy) never bind a placeholder: a placeholder is a one-off record whose name merely contains the printed code (LB Water: “4" 720A wafer check” fuzzy-bound “WILKINS 3/4IN 720A PRESSURE VACUUM”). Exact id binds still bind. The line abstains (placeholder_fuzzy_id_suppressed), skips the fuzzy fallback and lands in review with a “one-off placeholder” message; the name bucket may still find a stocked item. Webapp route only for now; the worker’s persistAutoPricedMatches does not read it yet. |
abstainNameSiblings |
{ "enabled": true } |
The name bucket (Priority 4) never binds its top hit when another of the name search’s top 5 hits is a sibling SKU: same base id (common prefix of 5+ characters with a digit) and both id remainders at most 6 characters, such as UFR1500-CA-8-U vs -I or 8-DR18NAT vs 8-DR18NA. The bind is kept only when the line text names what separates them: a word (or the id remainder) that only the best hit carries appears in the line, and none that only a sibling carries does. Otherwise the line is left unmatched and, like suppressNameBucket, is withheld from the fuzzy fallback (informational abstain name_sibling_ambiguous, review message “several near-identical catalog items”). Needs no placeholder prefixes. Id-keyed binds are untouched. Also honoured by the identity-match runner (eval, apply) alongside the placeholder and despaced-id levers. |
excludePlaceholderNameSearch |
{ "enabled": true } |
Adds one name search that excludes placeholders server-side (erp_product_id:!=[MSC-*]); the name bucket prefers its hits and falls back to the unfiltered ones only when it finds none. Stops a placeholder-flooded catalog hiding stocked candidates. |
despacedNameId |
{ "enabled": true } |
A line with no printed id whose name is a spaced-out catalog id gets the compact form (spaces and inch, foot and degree marks removed, upper-cased; at most 5 tokens, 5 to 24 characters, alphanumerics and - only, with a digit and a two-letter run such as FLG) as an exact-only candidate through the idstrip slot, bound at 0.97. Built for catalogs whose customers hand-write the distributor’s own item-id grammar: LB Water’s “P6X4 FLG RED” is P6X4FLGRED, “4 FXF 3” is 4FXF3, “4" FLG 90°” is 4FLG90. A non-existent code costs one empty lookup. A trailing length unit written as its own token (4 FXF 18 FT is 4FXF18; an attached AB123FT is left as is) and a trailing substitute qualifier (6FXPE 2-6 or longer is 6FXPE2-6; also or shorter/more/less/equal/equivalent/similar/better) are dropped first; other prose (“w/ handwheel”, an “or (…)” alternative) never qualifies. A printed id written with spaces (the extractor put P6X4 FLGRED in the line’s ids) gets the same compact candidate, and the auto-validate fallback tries it before the id as printed. Unlike codelessLines, it admits only lines that produced a candidate, and such a line gets no fuzzy name bucket and no customer-history bucket, so on its own it adds no name-bucket surface (with codelessLines also on, a miss falls to the name bucket like any code-less line). A foot mark between digits becomes the spool-length dash (4 FXF 1' 8" is 4FXF1-8); a candidate that starts with a placeholderItems prefix is dropped. A leading token equal to the line’s extracted quantity (the quantity before any CSR edit) yields no candidate. The operator route admits the line on the same rule. A despaced bind is recorded with provenance idstrip and lands in review like any auto-match; the bare id it binds is one vendor variant, so keep the connection in review mode until variant ranking lands. |
nameBindGuard |
{ "enabled": true } |
The name bucket binds only a hit that states exactly the line’s numbers (1-1/2" equals 1.5IN, CL52 must stay CL52) and shares more of the line’s words than every other such hit among the top five, covering at least 80% of them. Otherwise the line stays unmatched (informational abstain name_bind_unverified; the id-keyed fuzzy fallback still runs). The name bucket’s own overlap check ignores numbers, so without this lever a 2" line can bind the 2-1/2IN item. Measured on LB Water eval v1 (customer variant, route mode): wrong items written 40.9% to 2.2%, recall 21.8% to 15.8%; the clean variant is unchanged at 100%. Read by the auto-validate route only; the worker’s persistAutoPricedMatches and the identity-match runner do not read it yet. |
prefixCompatibility |
{ "rules": [] } |
Per-connection override of the org’s fuzzyMatchingConfig.prefixRules for the fallback’s prefix check. [] = no rules; absent = the org rules (the shipped defaults are oriented at one tenant’s catalog, e.g. b vs br). |
codeBearingRelevance |
{ "enabled": true, "minIdLength": 6 } |
The relevance gate accepts a fuzzy hit whose id is a transcription slip of a printed code (one OCR look-alike character 0/o/d, 1/i/l, 5/s, 8/b, 2/z; or one stray leading/trailing letter) even with low name overlap, at confidence 0.85, only when no other hit is also a look-alike. Digit-for-digit slips are never tolerated. |
Rollout
Section titled “Rollout”- The flag
matching_precision_v1must exist in Flagsmith (infrastructure/flagsmith/configs/flags.json, default on) before a connection can opt in — an unregistered flag is read as off. - Set the connection’s
extra_config.matchingConfigkeys above. Nothing else is needed; there is no migration. - The existing levers this guide pairs with are gated too:
allIdExactbymatching_pack_aware_v1andsuppressNameBucketbymatching_phase1_abstain_v1. The notes ininfrastructure/flagsmith/configs/segment-overrides.jsonrecord that the live environment-level state of those two flags was false and that CK and VMS needed explicitconn_*segment overrides, so confirm (or add) the override for the connection before assuming those levers take effect. - Expect matched volume to drop and review volume to rise: that is the point. Abstained lines land in
needs_validation(LOW_RELEVANCE), not in a red error.
Coverage note: the identity-match runner (lib/matching/identity-match.ts) reads abstainNameSiblings, the placeholder levers, and despacedNameId; allIdFuzzy, mpnFirst, codeBearingRelevance, and prefixRules remain auto-validate only for now. The worker path does not forward the existing allIdExact (the webapp route does); allIdFuzzy supplies the exact all-ids search there.