Domain Model — Damn Filters Product Catalog
Status: canonical business rules — the single source of truth for class assignment, product-page conventions, and eventual house part numbering. Rewritten 2026-08-19 to absorb the Rachel/Brandon working session (HERMES-BRIEF-2026-08-19.md — not in the repo) and to fix the class-definition drift. This file supersedes the older fact-check / meeting-prep docs (now archived).
Counts are never written here. Class counts, in-stock counts, and the dcf-default backlog are DERIVED — run python3 src/classification_report.py and python3 src/invariants.py for current numbers. If a number must be cited, cite date + source (e.g. "N,NNN classes, report YYYY-MM-DD").
1. Business Model
DF sells replacement filters for OEM designs whose patents are open or expired. The catalog is built around the OEM identity of each filter, not a DF-native identity.
2. Product Page Convention (marketing-critical)
Every product carries the OEM brand name and OEM part number in:
- the product title
- the product page body
- the URL
Observed title pattern: {OEM_PART_NUMBER} - {OEM_BRAND} - OEM Replacement {Descriptor} (e.g. P191920-016-436 - Torit - OEM Replacement Oval Filter).
Constraint: the OEM part number is the primary search entity for this business. It must not be normalized, stripped, reformatted, or replaced in titles, page copy, or URLs. Tooling treats the OEM part number string as immutable.
3. OEM-SPEC Filter (edit_db.csv)
Column OEM-SPEC (PRIMARY) (0-indexed col 61). TRUE = the "top row" for that OEM part number — the variant most closely matching the OEM's own spec. FALSE = sibling media variants. Standard first step for grade/class work: OEM-SPEC (PRIMARY) == TRUE → the primary rows — the real catalog; the remaining rows are media alternatives. (The primary-row total is DERIVED — run classification_report.py.)
Primary-row detection (B6, 2026-08-19): Viktor's rule — variant order is the best primary signal ("variant order is the... best one to refer to"). Write this as one definition in src/invariants.py (Db.product_row()) and use it everywhere; don't re-derive it per-script.
4. Variant Structure
Each OEM part number has up to three variants differentiated by filter media:
xyz123 base / OEM spec media (primary row)
xyz123_DAMN2-NF nanofiber (phantom variant)
xyz123_DAMN3-SB spunbond (phantom variant)
The _DAMN2-NF / _DAMN3-SB suffix is an internal marker, not a classification input. DF appended it to the OEM SKU to flag where a phantom media variant — usually Nanofiber or Spunbond — was added to the database. It is immaterial to classification: no rule or policy requires it, and we are moving away from this part-numbering scheme, so it survives only as a vestige. It remains useful because it generally identifies the filter uniquely.
Shopify requires a unique SKU per variant. A phantom NF/SB variant cannot share the OEM part number, so the suffix (or another unique SKU) is what keeps each variant addressable in Shopify. That constraint — not classification — is why variants carry distinct SKUs.
Diagnostic key (never authoritative):
| Suffix | Media |
|---|---|
| (no suffix) | 80/20 Cellulose/Polyester Blend |
_DAMN2-NF | Nanofiber FR |
_DAMN3-SB | Spunbond Polyester |
Bare
(no suffix)= the OEM marketing SKU, not a media claim — the 80/20 reading above is the code's current fallback (canonical_media()), never authoritative (see §5.4).
Non-standard spellings (_DAMN2-NanoFR, _DAMN2-NF-DFz2, _DAMN3-SBFR, -DFz, -DFx1, …) decode by substring (_DAMN3→SB, _DAMN2→NF) when the suffix is read as a diagnostic hint — never to assign media.
Media derivation. Option1 Value (the variant's "Filter Media Type" label), or the bdf value when available, is the authoritative classification input for media; the SKU suffix is never a classification input. External resolution order: OEM/manufacturer → DFI → Clark → competitor listings. NOT IMPLEMENTED — canonical_media() still falls to the SKU suffix for standard media; see §5.4.
Exotic media (D1, 2026-08-19). Exotic-media products — Nomex/aramid, carbon-impregnated, aluminized, PTFE, 90/10 glass, hydro/oleophobic — lose their standard 80/20/NF/SB variants: sell only the exotic media. Rationale: safety (an order "all checks out and then they send them something that is not explosion [proof]"). Confirmed by Rachel, Viktor, and Brandon. The full exotic taxonomy (the DF #12–#68 media catalog) lives in docs/media-taxonomy.md; the exact Option1 Value → media-code map is in the exotic pass record. The Nomex price $1,123.61 is real (F6 — media label was wrong, price was right). P199612 drops its -NF (D9 — Nomex is the product).
Spun-bond primary (A1, Viktor 2026-09-01). A product whose body-copy spec states "media … is Spun Bond" (or "Media: SpunBond") is a spunbond product, even when its Option1 Value says 80/20 — the structured body statement overrides the cut-and-paste Option1 data. Such products are SB-only: they sell only the spunbond media and lose their 80/20/NF variant ladder (same discipline as exotics). Applied 2026-09-01: 79 primaries → 38 _DAMN3-SB-promoted + ladder DELETEd; 28 (in-stock or grade conflict) + 13 (no SB variant) to review. Two guards (Claude review): never delete an in-stock variant, and never delete a variant whose natural grade is A/B or outranks the SB row it replaces.
5. Classes
A class asserts that a set of OEM part numbers are, for practical purposes, the same physical filter — identical on eight attributes:
- Outside diameter (OD)
- Inside diameter (ID)
- Length
- Shape (round / oval / conical)
- Media
- Top endcap
- Bottom endcap
- Build configuration
Seven are product-level (OD, ID, length, shape, top endcap, bottom endcap, build); media is the only variant-level attribute, so non-primary variants need a media-specific class ID — never "inherit". Matched at nominal dimension (one class spans 12.74/12.75/12.8 OD and 8.37/8.375/8.38 ID).
Shape is class-keying because it changes what OD/ID mean — round = outer / inner concentric diameters; oval = the two cross-section axes; conical = the mating faces. A round and an oval can never share a class even at "matching" numbers.
This is a classic industrial cross-reference problem: many OEM part numbers name the same physical filter. Cardinality: many OEM part numbers → one class. A class is the seed of a DF house part number.
5.1 Shape (round / oval / conical)
- Round — default. OD and ID are the outer and inner diameters of a cylinder.
- Oval (
cls-NR*) — OD is the LARGER cross-section number. - Conical (
cls-NR*) — F4 (2026-08-19, resolved 2026-08-20): the two numbers are the mating faces (where two filters connect), not tip-and-base; conicals are identical where they connect. Resolved (Viktor 2026-08-20): conicals re-oriented to OD > ID — the five siblings published ID > OD (12.75/17.5) were swapped to 17.5 × 12.75 to match Donaldson's P191281, per F4 ("longer dim = OD"). Length is almost always the longest dimension. (Nobody in the room has physically handled a conical.)
Flanged is not a shape (F3). Shapes are round / oval / conical (conical is very rare). Flange is a top-endcap configuration — typically a square flange on a round filter.
Shape inference (D10, 2026-08-19): a cartridge with blank bdf.Shape is Round UNLESS "conical"/"oval" appears in title/body — but any contrary signal flags the row for review rather than auto-rounding. (F8: two Round-tagged ovals were found — both corrected to Oval.) APPLIED 2026-09-01 (A5): 403 blank bdf.Shape cells backfilled to Round; the 2 contrary rows (P280475 oval, P527081-016-340 conical) left blank for review.
Enforcement: shape is class-keying, asserted in both directions by class.no_shape_span + class.shape_prefix_agree (in src/invariants.py) — no class spans >1 shape, and the class-ID prefix agrees with the shape (oval/conical ⇒ cls-NR*, cls-NR* ⇒ not round).
5.2 Endcaps
- Top endcap — always open (A01; the durable form Rachel stands behind is "one end has to be open"). Carries the flange (always a top-endcap feature — Brandon 2026-08-19) and the gasket (F5: gasket is always top).
- Bottom endcap — the only varying opening: open / closed / closed+bolt. Derived from
bdf.Bottom×bdf.Bolt_Hole(Open/No → open, Closed/No → closed, Closed/Yes → closed+bolt). ALWAYS a distinct class, no exceptions — open, closed, closed+bolt are three different part numbers, always three classes. Blank bottom endcap → closed, no bolt hole (A4, Viktor 2026-09-01): a cartridge with a blankbdf.Bottomis treated asClosed/No(bolt-hole evidence ⇒Closed/Yes; in-stock rows inherit their bin's endcap). Applied 2026-09-01 — 143 primaries backfilled, classes re-keyed.
Flange (top-endcap attribute; always splits a class): flanged (flange / flanged top endcap / mounting plate) is NEVER the same class as non-flange, even at matching dims. Ovals never have flanges — "flange" text on an oval is noise, not a split.
Gasket (top-endcap attribute; D6 2026-08-19): disregarded except on flanged filters — the operative split is flanged vs not-flanged. Gasket position is always top (F5; Rachel believes there is always a gasket, which is why gasket alone rarely splits anything). Square-vs-round gasket is only a sellability question for the Camfil Farr 325325 family (F7): a round / circular gasket on the square flange is sellable, a square gasket is not; 325325 parts generally archive / not customer-facing. NOT generalizable.
Bolt hole (D5): a filter either has a bolt hole or it doesn't — size is never classed. No cartridge has more than one bolt hole (A05; the rows that showed 2–3 bolts were a parser bug, not data). Written many ways in source (ob, o/b, O-B, O-B-H, w/bh, bh, "with bolt hole") — anything not closed or open is with-bolt-hole. Shelf labels (O/O, O/C, O/CBH) are quoted verbatim, never parsed.
5.3 Build configuration & construction (F1/F2)
Construction is the eighth class-defining attribute, and media determines it (F1, 2026-08-19):
- 80/20 → metal cage
- Nanofiber → metal cage
- Spunbond → bands
Any deviation from this default ⇒ custom (flag it). The class attribute takes observed construction where body text names one, else the media-expected default; observed ≠ expected ⇒ custom.
The ONLY body-text construction signal is band/strap language ("bands" / "straps"); it overrides 80/20/NF → bands. "cage"/"helix"/"expanded metal" text is never a signal — Body HTML is product-level (shared across a product's 80/20, NF and SB variants), so its "expanded metal" wording describes the 80/20 default core and leaks into SB/NF variants that carry no cage. Sales data confirms no sold SB filter has a metal cage (2026-08-20). Helix = expanded metal = cage (A03: helix is the process, not the object) — silence and all three wordings are the same cage build. For SB, construction defaults to bands and is effectively unobservable from body text (the cage wording is 80/20-default contamination), so no SB construction deviation is expressible — by design.
- Band count is irrelevant (A04; 1 band == 4 bands; some clients request extra bands, never a class attribute).
- A "strap" is a band (Q02).
Small filters may have neither cage nor bands (F2) — physically observed. General standard: filters 8″ or less (Brandon 2026-08-20; supersedes the earlier "~20\"" figure) often have neither cage nor bands. This is a general standard, NOT a hard rule — some filters above 8″ also have neither, and some below 8″ do carry a cage/bands. It is an enumerated exception: each such row is an explicit entry in imports/class-overrides.csv with a reason containing "neither", and the invariant build.neither_enumerated asserts every cartridge with explicit "no cage / no bands" language appears in that list. "Neither" does NOT determine a class independently (Viktor 2026-08-20) — it is a human-review flag, not a class attribute. No size threshold is coded; each new one is forced through a human.
Other build attributes (each independent; any one ⇒ a distinct class): prewrap (any "wrap" language incl. Vibra Shake / Dryflo), stainless ("stainless" or Notes "SS" / "(SS)", word-boundary excludes OEM part numbers), and hardware (twist-lock, tri-bolt, threaded-stud, d-ring, ground-wire, coupling-washer, lip-for-clips, hook, spring-retainer-clip). NOT distinguished: bolt-hole size (D5), bolt-hole count (A05), media square footage (D5). Detector: src/unique_build.py.
5.4 Media as a class attribute
Media is a class-defining attribute — one class never spans two media (80/20, NF, SB are three classes for one physical filter). Media matching is exact — no cross-media collapsing. Exotic media are also class-defining (2026-08-18), and exotic products carry only the exotic media (D1, §4).
Media taxonomy (Viktor 2026-09-01): three standard media — 80/20, NF, SB — plus isolated exotics and wide-pleat nanofiber. 80/20 FR collapses to 80/20 (leave the FR language in the body HTML; no FR-vs-80/20 rule exists) and 100% Cellulose is retired to 80/20. Wide-pleat nanofiber (Nanofiber on Spunbond Polyester / Wide Pleat Nanofiber* → media wide-pleat-nf) is a distinct media: its own class, spunbond-substrate (bands) construction, isolated like an exotic but not a pure exotic — the wide-pleat aspect is marketed, not treated as a safety-specialty.
Substrate rule (Brandon Brigham, DF owner, 2026-09-06): nanofiber is a surface layer, not a media — what it's coated onto determines classification. Cellulose substrate → NF (DFI #24); ANY other substrate → exotic. Two new exotic codes added with no DFI media match (closest DFI = #24 nanofiber-on-cellulose and #60 plain spunbond, neither matches): nf-on-sb (Nanofiber on Synthetic — nanofiber on a spunbond/polyester substrate; spunbond bands construction; drop-ship until quote volume justifies stocking) and polyester-felt (Needled Polyester Felt — Donaldson Ultra-Tek/Thermo-Tek/High Temp; no home in the standard model). Mapping: docs/donaldson-media-mapping.md.
Media derivation precedence (src/media.py canonical_media):
Option1 Valueexotic → that exotic (unchanged).Option1 Valuewide-pleat + bare suffix →wide-pleat-nf(a_DAMN2/_DAMN3variant keeps its own media — a wide-pleat label on those rows is the product-level label leaking down).- Else the SKU suffix → 80/20 / NF / SB. NOT IMPLEMENTED as intended: the
Option1 Valuestandard media label (NF/SB) is NOT yet read — a bare SKU whose Option1 names NF/SB still falls to the suffix. Open:docs/open-questions.md§2.1.
Substitution is ops reality, not a matching rule. Exact matching is the target and the hard rule. In the real world a rare 80/20 ↔ Nano substitution can occur; Spunbond substitution (either direction) never does. This does not relax the matching rule — it explains why an agent might see a substituted fulfillment. Spunbond "never in stock" is current practice, not immutable law.
5.5 Classification scope (product-level vs variant-level)
OD, ID, length, shape, both endcaps, and build config are product-level facts — every variant row shares its product's values and carries zero dimensional/build data of its own (non-primary variant rows carry empty Body HTML). Media is the only variant-level attribute. So "class each variant on its own dimensions" is a misnomer for non-primary rows: a variant's dimensions ARE its product's dimensions; the only thing it can differ on is media. Read dims/endcaps/build from the primary row (Db.product_row()), never the variant row.
5.6 Partition rule
Classes are a strict partition with rare exceptions. Default: one primary belongs to exactly one class. The schema should permit a documented exception rather than silently allowing multi-membership — an exception is a flagged, reviewed state, not a normal one. (The mechanism for flagging/reviewing a genuine multi-class product is still not designed — §7.)
5.7 Identifiers
Final house IDs take the form DF1234 — one unique DF number per class, no media suffix (media is encoded in the class itself). Provisional IDs are cls-NNNN (round) and cls-NRnnn (non-round), visually distinct from final DF IDs so a temp value can never be mistaken for an issued part number.
Class IDs stay opaque until DFI data lands (§7); every re-key writes old,new,reason,date to a committed imports/class-id-map.csv so the eventual renumber is a pure function of that map, run once.
5.8 Unclassed state (dcf-default)
Classes are CARTRIDGE-ONLY. Non-cartridge products get no class — no new-class, no dcf-default, just blank.
dcf-default is a transitional marker, not a class — a cartridge awaiting ID/bottom-endcap backfill. A26 reframed (B1, 2026-08-19): an unknown bottom endcap is no longer a blocking state — it triggers a backfill-then-delete workflow: (a) backfill the bottom endcap from Body HTML where possible, (b) report how many of the backlog have no information at all, (c) those may be archived (not deleted). The root cause is F10 — Carl's bdf fields are knowingly incomplete. Trust in class assignment is ~70–80% self-assessed (F11), which gates how far to extend sibling-cost (D2).
5.9 Class assignment is a business decision
Two dimensions within the tolerance window are likely the same nominal size (§6), but that is a probable-interchange signal, not automatic admission to a class. Class assignment is the business decision layered on top; classifiers must not treat the tolerance window as proof of physical identity.
5.10 Terminology: anchor
"Anchor" has exactly one meaning: an Anchor SKU is a competitive loss-leader product (a popular entry-point filter competitors bid hardest on) — see docs/grade-pricing.md (Anchor SKUs section). The classification pass does NOT use the word "anchor" (no "class anchor", no "anchor-based clustering") — any such usage is vestige and must be removed.
6. Dimensional tolerance
No industry standard defines when two dust-collector cartridge dimensions are the same nominal size, so DF sets its own policy. Two filters within tolerance are not declared "the same filter" — they are members of the same dimensional family / probable interchange group, and class assignment is the business decision layered on top.
6.1 The rule
Two filters are dimensionally equivalent when they fit inside a tolerance window (A2, Viktor 2026-09-01) — a per-axis allowance that scales with the dimension, plus a shared budget across the three axes:
tolerance(d) = clamp(1/16″ (0.0625″), 0.005 × d, 1/4″ (0.25″))
ratio_a = spread_a / tolerance(max_a)
A class is valid when every member is within the window of every other member: no single axis may exceed 1.35× its tolerance, and the three ratios must sum to ≤ 2.0. (This stops a class widening one near-miss at a time — 12.75 / 12.80 / 12.85 — while still admitting the "everything's close but one number is just outside" case: two matching axes buy the third 35% past its axis allowance.) The fixed 1/16″ rule survives as the floor — below ~12.5″ on any axis nothing changes (0.005 × 12.5 = 1/16″); the relative term only bites above ~12.5″ — in practice, lengths (diameters sit near the floor, and the 1/4″ cap would need a 50″ dimension and never binds). Enforced by class.spread_within_tolerance in src/invariants.py (dim_tolerance, MAX_AXIS_SLACK, SLACK_BUDGET — code names are legacy: MAX_AXIS_SLACK is the per-axis ratio cap, SLACK_BUDGET the sum cap).
6.2 Which field is length
For cartridge filters the third dimension is bdf.Length — authoritative (D4, 2026-08-19). bdf.Height is never used on cartridge filters — it stores non-cartridge height (HEPA panels: length/width/height). The tolerance window (§6.1) applies to bdf.Length.
6.3 Gating attributes (exact match, no tolerance)
Dimensional tolerance is evaluated only after the categorical attributes agree exactly (no tolerance): shape (§5.1), media (§5.4), top endcap (§5.2), bottom endcap (§5.2), build configuration (§5.3). The bottom endcap is 3-state exact; the two data-unknown states (Closed/blank, blank/blank) are no longer blocking — they feed the backfill-then-delete workflow (§5.8).
7. Unresolved / open items
Reconciled 2026-08-27 (Viktor). The live tracker is
docs/open-questions.md. Disposition of the items below as of 2026-08-27: #1 + #6 deferred (no numbering this phase, class IDs opaque until DFI); #2 deferred (endcap taxonomy — Viktor fills later, DFI endcap data helps); #3 safeguard-only (0 violations verified 2026-08-27); #4 deferred (decide when DFI lands); #5 resolved, plus a new OD == ID flag for review; #7 Jason completes specs then archive the no-info remainder; #8 on hold (Viktor questioning the need); #9 Jason (handoff spreadsheet); #10 gated on class trust.DFI status (2026-09-07): DFI is authoritative — among the highest-authority sources we have, more authoritative than our own data. The DFI cross-references are already in the DFI data: each EFI part line carries manufacturer + part number (Donaldson P…, Camfil …, Clark …, Nordson B…), and every cross-reference on one EFI line is one class. Parse manufacturer+PN → EFI_PN → class and join our SKUs to it. The ~109 dims-disagree rows are checked, not auto-applied.
Explicitly unanswered. Do not infer answers. (Operational blockers B1–B7 are tracked in the plan and now live in docs/open-questions.md, the single live tracker.)
What "DFI" is: DFI is the vendor-confidential reference dataset in dfi/ (legacy name EFI — the on-disk files still use the efi_ prefix: efi_cost_dataset.csv, efi_cross_reference*.csv). "DFI" and "EFI" are the same data under two names (CHANGELOG 2026-08-28: "DFI (ex-EFI)").
- House part-number scheme (partly resolved). The
DFprefix and no-media-suffix rule are settled. Numbering (2026-09-07): take the old EFI number, strip theEO/E0prefix, replace withDF— EO1425 → DF1425. The complete numbering scheme is still not fully planned; leading-zero padding TBD. - Endcap-config taxonomy. Build hardware (twist-lock, tri-bolt, threaded-stud, d-ring, ground-wire, coupling-washer, lip-for-clips, hook, spring-retainer-clip) is already class-defining (§5.3,
src/unique_build.py), and "bands" is construction, not endcap hardware. What is not formally enumerated is the residual free-text endcap config inbdf.Top_Config/bdf.Bottom_Config(lips, clips, studs, stainless endcaps, …) beyond flange / bolt hole / gasket — it gates nothing until enumerated + backfilled. - Partition-violation mechanism — flagging/reviewing a product genuinely in multiple classes is not designed.
- Cross-reference metafields —
bdf.Cross_Refandbdf.Compatible_Sysare sparse; whether they seed or validate class assignment is undecided. - ID > OD transpositions — 16 products had
bdf.Inside_Diametergreater thanbdf.Outside_Diameter. All switched (supervised, logged) — including the conical family (cls-NR008/cls-NR053/cls-NR054, re-oriented 2026-08-20 to OD > ID: the five reversed siblings swapped to 17.5 × 12.75 to match P191281).ID_GT_OD_ALLOWEDinsrc/invariants.pyis now empty — no exemptions remain. - Class-ID permanency — deferred (Viktor 2026-08-17): proper rule-conformant classes first; DF part numbers only after DFI lands. Revisit "does a split retire the parent ID or does one child inherit it?" then.
- dcf-default backlog (B1) — backfill → report "no info at all" → archive.
- Bins 4/6 dims (A11/B3) — resolved 2026-09-07: the bin dimensions are the bin dimensions; DF has given them, no re-measurement is needed.
- Shopify duplicate-SKU cleanup unowned (F9/B5) — candidates Zach / Jason Thompson.
- Sibling-cost → PO (D2) — approved with an email notification (not a PO note); the cost/PO model lives in
docs/grade-pricing.md§8 (Cost model).
8. Physical stock (sku_stock_id)
DF physically holds a small "Stock family" of filters, each identified by dimensions, not a part number (STOCK 12.74x8.38x26 O/O 80/20 FR). Unit counts live in QuickBooks. Bins are numbered via sku_stock_id (edit_db.csv col 106). Single source of truth: src/bin_map.py. Bin 10 is out of stock (its old class cls-0453 merged into cls-0126 by the 2026-09-01 merge sweep).
| bin | class | dims (OD×ID×L) | bottom endcap | media |
|---|---|---|---|---|
| 1 | cls-NR015 | 14.38×11.38×26 (oval) | open | NF |
| 2 | cls-0012 | 12.74×8.38×26 | closed | 80/20 |
| 3 | cls-0038 (was cls-0458) | 12.74×8.38×26 | closed+bolt | 80/20 |
| 4 | cls-0459 | 12.74×8.38×26 | closed+bolt | NF |
| 5 | cls-0267 (was cls-0470) | 12.74×8.38×26 | open | 80/20 |
| 6 | cls-0471 | 12.74×8.38×26 | open | NF |
| 7 | cls-0951 | 13.84×9.48×26 | closed+bolt | NF |
| 8 | cls-0005 (was cls-0477) | 13.84×9.48×26 | open | 80/20 |
| 9 | cls-0478 | 13.84×9.48×26 | open | NF |
| 10 | cls-0126 (was cls-0453) | 7.93×3.57×16 | closed+bolt | 80/20 — out of stock |
- Bin 7 (cls-0951) = closed+bolt, stock PRE-DRILLED going forward (D3). The vendor supplies with the bolt hole; every checked order was with a bolt hole. (This supersedes the "must drill holes before shipping" note; the routing rework-flag for bin 7 is updated accordingly.)
- Bin 2 = cls-0012 (closed no-bolt) — correct, not a classing error: only 3 closed-no-bolt 12.74×8.38×26 cartridges exist. Verified against the live product list (K03).
- Bins 4 and 6 (NF twins of 3 and 5) exist and are populated, but their dims are still inferred, not physically verified (A11/B3).
- Bin 1 oval has no STOCK descriptor; its class is cls-NR015 (14.38×11.38×26 NF). No stainless in stock — the bin-1 ovals are standard (Q01).
- Spunbond (
_DAMN3-SB) is not currently stocked (drop-ship) — not a permanent rule. Flanged cartridges map to no bin (not stocked now).
A physical bin is not a catalog SKU. A STOCK <dims> label never appears as a row in the in-stock list; it is represented indirectly as the catalog part numbers carrying its stock-id. In-stock status is asserted per catalog SKU; unit counts live at the bin level (QB). In-stock requires config match — both endcaps (opening, bolt hole, flange, gasket) AND the build configuration must match the bin, not merely class + media. The derived in-stock list (src/build_in_stock_csv.py → imports/in-stock-skus-*.csv) is the n8n routing authority; on CSV = in stock.
9. Regression fixtures (human-labeled — do not drop)
Canonical edge cases with Rachel's reasoning attached. Any re-key or detector change must not regress these.
| SKU / ID | Verdict | Reasoning |
|---|---|---|
P032931_DAMN2-NF | stainless → pull from in-stock + raise price | Donaldson's site says SS |
P25.98E | strap = band | same construction |
S52282 (SS) vs S52282-C1 | stainless vs sibling | SS in notes ⇒ stainless |
P191281 (conical) | OD > ID (17.5×12.75×26) — correct as published; 5 reversed siblings re-oriented to match | Donaldson's ordering was right; the other 5 vendors published ID>OD |
325325-016 REPL | sellable only with round gasket on square flange | Camfil Farr 325325 only |
325325-016 | archive, not customer-facing | 325325 parts not sellable |
156903-dupe | delete the dupe | "the dupe is the dupe" |
DFO18201M8SCE / DF20204M13B / DF16204M13B | true duplicates, unowned | Shopify cleanup unassigned |
P199612 (was P199612_DAMN2-NF) | Nomex, $1,123.61 real, renamed — dropped -NF | media label wrong, price right |
E04450 | both 80/20 and NF stocked | resolves order 17662 |
lm990ddd2 (A10125) | oval; rename (last pass) | randomly generated, disliked |
10020003_DAMN2-NF | oval, corrected on site | one of two Round-tagged ovals |
| second Round-tagged oval | still needs changing to Oval | confirmed via Filter Professor / DFI |
cls-NR020 = cls-NR015 | same filter | 11.40 vs 11.4 + transposition |
cls-NR057 = cls-NR043 | same filter | same cause |
NF40197/NF40198/NF20322_DAMN2-NF | transposed OD/ID | same root cause as the 16 |
Related
docs/column-map.md— authoritative 0-indexed column offsets.docs/grade-pricing.md— house × natural × anchor grading + pricing model (house/natural/anchor definitions, cost model, D2 sibling-cost email).docs/media-taxonomy.md— the DF media catalog (exotic taxonomy).docs/architecture.md— database schema and data model.src/invariants.py— the verification surface (a rule not enforced there isn't enforced).bdf.*metafields (cols 72–87, incl.bdf.2nd_Descrat 72) are Carl's public data — DO NOT EDIT OR DELETE; supervised corrections (transposed OD/ID, bottom-endcap backfill) are permitted and MUST be logged inimports/class-overrides.csv.