CHANGELOG — df-product-db
Append-only dated pass log. Historical "Active work" (was AGENTS.md) and "Recent Work" (was README.md) consolidated here 2026-08-20. Counts below are historical snapshots — current counts are DERIVED (see AGENTS.md / README.md).
2026-09-10 (Rachel/Viktor working session — open-questions 1–17 + DF Mermaid 20–22 rulings)
45-minute working session (Viktor + Rachel; a third male voice, believed Brandon, joins at the end). Rulings recorded verbatim in MASTER-TRANSCRIPT-2026-09-10.md; this entry makes them authoritative. Decisions quoted; caveats flagged inline.
- DFI cross-reference disagreements (item 1) — DFI is authoritative with a sales carve-out. "DFI should be the authoritative source … however, if we have sold the part number … if we've legitimately sold that part number … without the bolt hole, we should keep [it]" (Rachel); Viktor agreed. Do not disturb A–C SKUs: "We don't want to change the SKU … because that would change the customer's order when they came back. And we don't want to change dims" / "We don't wanna change anything on A through C for the most part." New step before deciding: check returns/refunds on the disputed sales — a legit sale with no return supports keeping our data; a return suggests DFI is right (Rachel's action; "a lot of this could be done with AI").
- Media source (item 2) — external resolution order confirmed and it outranks
bdf. Order = OEM/manufacturer → DFI → Clark → competitor listings. "I think all of those come before the bdf media source." Final fallback when every source is silent: stick with 80/20. (~49 blank-Option1 rows exist; they may overlap the unclassified group.) nf-on-sbA–C exceptions (item 3) — keep the plain-NF variant, offer both. Where there is A–C sales history, keep the plain nanofiber variant AND offer the nanofiber-on-spunbond option — do not re-key to exotic-only. "I don't want it to be only nanofiber … I want it to have a nanofiber on Spunbond option." State the spunbond-substrate option in the body copy.- "Likely but not certain" grade combos (item 4) — Viktor's manual regrade. Viktor does a manual re-grade as the last step; Hermes proposes, Viktor looks. (Precision in the middle doesn't matter much: a
dBis "probably a B, maybe a C … it really doesn't matter if we call it an A, B or C.") - Donaldson WSO series (item 5) — likely exotic (Toritex conductive, anti-static). Re-scrape using the BARE SKU (not the long SKU) on Donaldson; the "missing OD/ID" is non-round-by-design. Reusable finding: future scrapes must search the LONG SKU for bare SKUs — "the bare SKUs are being taken literally and it doesn't know about those extra two [digits]." Non-round confirmation still pending.
- 5 parked "unsane dims" primaries (item 6) — OEM/DFI override; no bin. "We don't have a physical bin … [OEM/DFI/Donaldson/Clark] overrides the Damn Filters dimensions." The 6″ rule is remembered as about banding (filters over 6″ have bands), not dimensions — neither fully confident.
- 90/10 glass (item 7) — keep the one downgrade-exception, collapse the rest to one variant. ⚠ Stated from memory, NOT verified — confirm the exception exists before acting.
- House part-number scheme (item 8) — deferred. DF prefix + old E0 number (strip E0, prepend DF); "don't want to work that out right now."
- Duplicate/blank SKUs (item 9) — still unowned, cause unknown. "How is it possible that we have blank SKUs? … I don't know." Joins stay on Variant ID, never SKU. Owner/cause/remedy all open.
- Pricing + sibling cost (item 10) — D/F tail auto, A/B/C banded. "For the D's and F's … it's fine to do the sibling cost at 65 margin." A/B/C not decided; ~600 C's with sales are the exposure. Proposed mechanism (Rachel): auto-apply when the cost+margin price is within a band of the old price; manual review when outside — both overpriced AND underpriced (Viktor: "the underpriced stuff too, which there are a lot"). Exact range still TBD.
- Natural-grade thresholds (item 11) — Viktor owns. "I am going to add the natural grades based on data that Damn Filters provides me. You don't need to do anything with it." (Resolves the units-inconsistency note — Viktor restates.)
- Endcap-config taxonomy (item 12) — flagged and deferred.
- Cross-reference metafields (item 13) — seed them. "Go ahead and populate them" — into the metafields/DB, NOT the website; not blocking.
- Class-ID permanency (item 14) — bounced. "We need a better explanation of 14." Re-state the split→retire-or-inherit question in team terms before asking again.
- Partition / multi-class (item 15) — REJECTED as a question; it is an invariant. "There should never be [a multi-class product] … I don't think a filter can be in two classes." Enforce as an invariant, not a review path.
- People & authority roster (item 16) — Viktor owns, not done.
- P25.70E (item 17) — still open, blocked on a list for Rachel. Hypothesis: a stray dot entered in a past Carl project throws it off. "You're going to have to ask Rachel about this one … give me a list of those so I can check."
- DF Mermaid Q20–22: Q20 (stockless single source) —
sku.in_stockmetafield; "if it's an A that automatically means it's in stock." Q21 (class definitions) — wanted by both, blocked on classes existing. Q22 — expect more review emails: under the all-or-nothing rule, any order with an unpriced or unknown line goes to Rachel for manual review instead of auto-PO.
No data changed. This pass records rulings; the WSO re-scrape, the manual regrade, and the per-row sheet adjudication follow.
2026-09-10 (competitor-price sweep — A/B/C below-target + A/B price sheet)
Second price sweep, focused on A-C natural grades. Two deliverables: an A/B price sheet and a live below-target re-scrape. No catalog data changed.
- A/B price sheet —
generated/ab-price-sheet-2026-09-10.csv(110 rows = 93 primary + 17 non-primary A/B cartridge variants). Columns: house/nat grade, orders, pageviews_ga4, Variant Cost, PO-match / PO-sibling cost provenance, price, and competitor prices (per-source columns + avg/median/ hi/lo). Scriptsscratch/ab_price_sheet.py+scratch/plan_price_sweep.pyscratch/build_sweep_queue.py(read-only; gitignored).
- Sweep —
run_scrape_queue.pyon a below-target queue (A/B -> 3 sources, C -> 2): 428 SKUs, 424 scraped, 195 Firecrawl credits. Result (below-target → at-target): A 11→6, B 24→18, C 389→345 (424→369 total). 55 SKUs reached target. Yield is modest — re-search mostly returns already-cached domains (cr=0), so the 4 B zero-match are structural (no competitor lists the PN); the rest need different queries/backends to surface new sources. - Robustness:
lookup_dims.get_keys()+search_adapters.get_free_keys()now retrybws secret list4× (Bitwarden intermittently returns empty stdout and crashed the run at startup).run_scrape_queue.py --capis CUMULATIVE against the requests ledger, not per-run. - Backup:
data/scrape_cache.db(integrity ok; 3,279 scrapes / 21,262 obs / 2,618 ledger credits) → restic2f034afe(local /mnt/v/backups) +5a645b39(B2). Account remaining 729/6,000 (annual, to 2027-09-02).
2026-09-09 (add ad-competitor price sources: cardinalfilters.com + grainger.com)
Registered two advertising competitors as scrape sources for PRICE capture.
src/scrape_cache.pySOURCES: addedcardinalfilters.com(Cardinal Filters) andgrainger.com(Grainger), both tier 3 / roleboth. Cardinal carries a per-PN "Specifications" block (Height/OD/ID/Media/Configuration) + list/sale price; Grainger's per-PN product pages list the OEM manufacturer PN in the spec table + price. Grainger has heavy bot protection — a challenge-page fetch is expected and is handled as statusmiss(retryable, not fresh).src/tools/lookup_dims.pyis_product_url: also reject/collections/(Shopify plural). The old filter only matched singular/collection/, so Shopify collection listings (cardinalfilters.com and every other Shopify source) leaked through as "product pages" and could scrape a first-product price for the wrong SKU. Tests pinned intests/test_scrape_loader.py(TestIsProductUrl).- Re-seeded
data/scrape_cache.db(66 sources, tier-3 now 13). No data scraped this pass.
2026-09-09 (A-C pricing disparity analysis — read-only)
Competitor-pricing scan of A–C natural-grade primary SKUs (Viktor: flag large ours-vs-competitor gaps; focus high-orders + high margin). No data changed. Results: generated/ac-pricing-disparity-2026-09-09.csv (713 SKUs).
- Competitor price = clean median of
scrape_cache.dblist_priceobservations +competitor_list_price(col 137) fallback. Wrong-match scrapes dropped before aggregation (PN-MISMATCH / BASE-ONLY / multi-PN "page PNs" / wrong-single-PN; 339 dropped). Keyed by SKU via alias_pn → SKU join (catalog_identity+clean_term(sku)+ queueterm→sku) — the catalog_identity-only join under-mapped (290 → 1,124 SKUs) and minted false outliers (e.g. 3333-333 read 13× with a stale $12 instead of the cache's $121). - Result: median our/competitor ratio 1.13. 248 SKUs ≥25% above competitor (93 ≥50%, 15 ≥2×). >20 orders + ≥20% above = 14 high-margin keep-candidates (P7401RM 1.87× same-spec, 216-203 1.43×, 515522 1.43×, k-3-443 1.48× NF+flange). Genuinely underpriced with order volume: 523523 (Empire −47%), 29901704 / 29901706 (Conair −38% / −36%).
- Residual: category-page scrapes whose note is empty still leak a stray high price into the median (7C75BL) — a URL-based category filter would close it.
2026-09-09 (notes for re-homes + scrape-queue recheck column)
- Grade-notes: added "SET A (in-stock via reclass/merge; grade -> a)" to the 88 re-homed SKUs (82 reclass + 6 merge) so the grade change is visible in the spreadsheet. Script: src/tools/note_rehome_20260909.py.
- Scrape queue: build_scrape_queue.py gains a
recheckcolumn (scoped to dcf-default): 'dims' for the 5 parked unsane-dims primaries (wall > 6), and 'endcap' for bemad28 (Closed + blank bolt-hole). Regenerated generated/scrape-queue-2026-09-09.csv: 105 need dims (unchanged) + 6 recheck. Competitive PRICING stays deferred (D/F tail).
Invariants 0 FAIL / 1 warn; tests 134 pass; ruff clean.
2026-09-09 (variant-grade backfill = lowercased natural letter)
Viktor 2026-09-09: the variant-level house grade (col 93) is the LOWERcased natural-grade letter, not the product-level house grade — in-stock is the only path to a, so a not-in-stock natural-A variant gets b (grade-pricing §5/§6; dA/fA -> a in stock / b not). Backfilled 44 non-primary variant rows whose Variant custom_label_0 was blank (b 2 / c 12 / d 26 / f 4): the 42 unclassed parked-product phantom variants (_DAMN2-NF / _DAMN3-SB) + 2 classed 325325-009 twins (cls-1696/cls-1697). Closes the HH-09 #2 grade-product-twin fragility. Script: src/tools/backfill_variant_grade_20260909.py.
Invariants 0 FAIL / 1 warn; tests 134 pass; ruff clean.
2026-09-09 (tolerance-aware reclass + merge)
Two-in-one pass (docs/HANDOFF-2026-09-09-tolerance-reclass.md): reclassify the complete dcf-default primaries within the A2 tolerance window, then merge the within-tolerance duplicate classes. Script: src/tools/tolerance_reclass_merge_20260909.py.
- Merge: 30 duplicate classes collapsed to 24 winners (lowest id keeps), grouped by categorical signature (shape x endcap x media x flange x build, exact; dims within the shared budget) under complete linkage. Retires the
class.no_signature_collisionWARN (15 exact-signature pairs) plus the within-tolerance dupes the DFI check surfaced (e.g. cls-0245/0255/0259 -> cls-0012, cls-0467 -> cls-0348, cls-NR095 -> cls-NR015). 67 rows re-keyed. - Reclass: 166 complete dcf-default primaries (full sane OD/ID/Length + bottom endcap + shape) re-keyed -- 474 variant rows matched to existing classes by full-spread complete linkage; 65 genuinely-new classes minted (new dims / flange / build combos, no within-tolerance duplicate). dcf-default 268 -> 102.
- Parked (stay dcf-default): 102 -- 96 missing a dim (scrape target), 5 with unsane dims (wall > 6; two have body-copy OD contradicting bdf OD -- the dims-disagree workstream), 1 ambiguous endcap (Closed + blank bolt-hole, bemad28).
- Stock re-derivation: 88 SKUs re-homed in-stock (class -> bin), 0 de-stock; grade -> a. In-stock 2,321 -> 2,409 (report 2026-09-09). Classes 1,563 -> 1,598.
- Invariant fix:
check_exotic_media_containednow keys "exotic" on the canonical media code (not 80/20/NF/SB), not on Option1 Value membership in OPTION_EXOTIC. The merge surfaced the gap: cls-1773 ("Nanofiber on Synthetic") and cls-1722 ("Nanofiber on Spunbond Polyester") are both nf-on-sb per DECISION #3, but the old Option1 test flagged the merged class as an exotic/standard mix.
Note vs the handoff: its "172 complete" was 166 (5 unsane + 1 blank-bolt parked), and its "duplicate-class" examples cls-0038 vs cls-1295 and cls-0005 vs cls-0478 are actually a flange-split and a media-split (correctly NOT merged).
Invariants 0 FAIL / 1 warn (duplicate_primary_sku); tests 134 pass; ruff clean.
2026-09-09 (oval label fix + placeholder flags)
- Five cls-NR015 NF-primary ovals carried a wrong Option1 Value ("80/20 Cellulose/Polyester Blend") on an _DAMN2-NF row. Viktor ruled these are Nanofiber (the bin-1 oval class is NF-primary, no 80/20 variety). Fixed the customer-facing Option1 -> "Nanofiber FR" (label-only; class already NF).
- nf-on-sb C-grade revert: p031792 (C) re-keyed back to cls-1770 and flagged — "keep plain NF" is ambiguous for a spunbond-substrate product (bands vs cage build); needs Viktor's clarification. NOT auto-applied.
- Placeholder/junk SKUs flagged in sku_status_notes (resolve real OEM PN from title): 0568j47h, jnuu8-22-l, 033k4l4, k-3-443, 0o-33-3dd, plok-00, polkachic1, po0192, ok-ok-ok, cd-344-344, ll-3ee-33, s-3x8-000, mvc-pplp-pp, ff-rr-4f4f, 6-611-233, ff-33-4443-00, 94-44-333333, ds-3434-3434.
Invariants 0 FAIL / 2 warn; tests 134 pass; ruff clean.
2026-09-09 (scrape queue + DFI cross-reference class check)
- Scrape queue built (
generated/scrape-queue-2026-09-09.csv, 4,565 cartridge primaries A-F). 105 rows still need dims (no ours, no DFI) — the scrape target for next session (1,800-credit cap). 3,360 need competitor price. - DFI cross-reference vs our-classes comparison (Viktor's "spectacular check", read-only): for each EFI line, all its cross-reference SKUs should share one class. Of 827 EFI lines with >=1 classed SKU, 279 carry >=2 SKUs; 97 disagree (their SKUs span >1 of our classes). Break-down:
- 12 media-split — same dims+endcap, different media: EXPECTED (DFI groups media; our class correctly splits 80/20/NF/SB).
- ~85 real disagreements, three shapes: (a) within-tolerance duplicate classes (cls-0038 12.75x8.38x26 vs cls-1295 12.74x8.38x26, same endcap — merge debt; this is what surfaced
class.no_signature_collision), (b) endcap disagreement (cls-0038 closed+bolt vs cls-0267 open, same dims — mis-endcap'd SKU or loose DFI bridge), (c) false PN collision (cls-0038 vs cls-0126 7.9x3.6x16 — the same PN string maps to different filters). - 62 of the 97 carry an A-C SKU. This is the class-error hit-list; the merge + false-bridge + endcap resolutions follow next.
2026-09-09 (DFI dims backfill — local before scrape)
Viktor's local-DB-first flow: backfill blank dims from DFI (authoritative) before burning Firecrawl. 39 bdf.* cells (OD/ID/Length) backfilled onto 20 product primaries whose dims were blank but DFI carries full dims (dfi-review verdict 'unvalidated'). Logged to class-overrides.csv.
- Deliberately dims-only — no reclassify: those rows still carry a blank bottom endcap, and blank->closed is the A4 default under recheck (DECISION #1). Classification waits for the endcap + shape adjudication.
- Side effect: filling the blank IDs surfaced
class.no_signature_collision(15 duplicate-class signature pairs, e.g. cls-0038 vs cls-0233) that the blank dims had masked — a merge follow-up, not this pass.
Invariants 0 FAIL / 2 warn (duplicate_primary_sku + the new signature-collision surface).
2026-09-09 (media-derivation fix — Option1 Value is authoritative, never SKU suffix)
Implements the 2026-09-07 media decision that was decided-but-not-coded: src/media.py canonical_media() now reads standard NF/SB Option1 labels on a BARE suffix, and maps "Nanofiber on Spunbond Polyester" to nf-on-sb exotic (DECISION #3, Viktor 2026-09-09) instead of wide-pleat-nf.
- media.py: added
OPTION_STANDARD(Nanofiber FR/Nanofiber/Nanofilber FR/ Best DAMN Choice 100% Nanofiber[ FR]/Upgrade 100% Nanofiber[ FR] -> NF; Spunbond Polyester[ FR]/Best DAMN Choice 100% Spun Bond/Upgrade 100% Spun Bond -> SB), bare-gated (a _DAMN2/_DAMN3 variant keeps its suffix media). Moved "Nanofiber on Spunbond Polyester" out ofOPTION_WIDE_PLEATinto a bare-gatednf-on-sbmapping (was wide-pleat-nf). - Re-key (
src/tools/rekey_media_fix_20260909.py, APPLIED): 49 rows re-keyed (80/20->NF 30, 80/20->SB 14, wide-pleat-nf->nf-on-sb 7), 1 parked (dims incomplete), 12 classes minted. Stock re-derived from the corrected class: 9 re-homes (open 80/20->NF bin 5->6 / 8->9), 5 ovals newly stocked into cls-NR015 (bin 1 — the bare ovals are NF, DECISION #2), 6 de-stocks (closed-NF + SB have no bin), 15 grade demotions (a -> natural letter / x). - Flagged for review: 2 placeholder SKUs (0568j47h, jnuu8-22-l) re-keyed into cls-NR015 — resolve their real PN from the title in the placeholder-SKU cleanup. A-C nf-on-sb exceptions (keep plain NF) pending Viktor's list.
No bdf.* change, no class-overrides entry. Invariants 0 FAIL / 1 warn (the two duplicate primary SKUs, non-cartridge).
2026-09-09 (wrap de-type completion + P25.70E flag)
Supervised hygiene fix on the tail of the 09-01 wrap-accessory de-type:
- Wrap de-type completion: 8 phantom
_DAMN2-NF/_DAMN3-SBladder variants across 4 products (8PP-39654-00, 8PP-35880-01/-02/-03) still carried Type='Cartridge Filters' after their Accessories primary was de-typed. Set Type='Accessories' (rows already Variant Command=DELETE) — clears theproduct.type_uniformWARN. - P25.70E flag: wrote sku_status_notes on the primary — spunbond-promotion revisit: the 80/20 (bin 8) + NF (bin 9) twins are the in-stock bins, so the true primary may be 80/20. Flagged to Rachel for ruling (open-questions P25.70E).
No bdf.* change, no class-overrides entry. Invariants 0 FAIL; 1 warn remains (the two duplicate primary SKUs, both non-cartridge, unowned Shopify cleanup).
2026-09-09 (dims write-back + reclass + endcap correction)
Second pass of the day: closed the competitor-scrape loop by writing recovered OD/ID/Length to edit_db.csv bdf.* and re-keying the newly-complete dcf-default primaries into classes. All under Viktor's adjudication (approved the candidate list low/med/high confidence).
- Dims write-back (scratch/apply_dims_writeback.py, fill-blanks-only): 25 bdf.* field changes across 12 SKUs, logged to class-overrides.csv (authority Viktor). Sources: competitor scrape (tier1/scraper — flagged when filterjunkie/filterprofessor may mirror DF), Clark cross-ref (data/clark_crossref.db, authoritative — overrides scrape), and AAF datasheet PDFs (info.aafintl.com/hubfs/*.pdf — the one reachable OEM datasheet source).
- Guards skipped 10 rows: 9 false-PN matches (Champion Labs DCF codes, 122006-000, C256L4, 376302, P032221 — recovered dims contradicted existing bdf OD/length beyond tolerance) + 1 in-stock (P191526, deferred).
- Endcap correction (Clark cross-ref disputes A4 blank->closed on 3): P030915_DAMN2-NF + P181134 -> Open (Clark OO); P546761 -> closed+bolt (bolt-hole 0.53" -> 0.5). 4 bdf.* changes.
- Reclass (scratch/reclass_dims_writeback.py, tolerance-window A2 matching, not exact round(,2)): 18 variant rows re-keyed across 10 products, 0 classes minted. dcf-default 278 -> 268. P031593-016-340 parked (WSO non-round).
- Finding — Donaldson WSO series is non-round: 9 P0315xx/P0316xx/P0317xx PNs are labelled shape=Rectangle in the Donaldson catalog series_dims and sit in cls-NR* classes; their "missing OD/ID" is correct-by-design, not a dims gap.
- Credit reconciliation: pre-09-08 study scripts counted 1 call = 1 credit while Firecrawl bills search=2 — the requests ledger undercounted real burn by ~60%. Authoritative balance = GET /v1/team/credit-usage.
2026-09-09 (first live competitor-scrape run — price gaps, dims, D-grade)
First live Firecrawl scrape run on the free-search → scrape-only path. The pilot exposed and fixed three code bugs + one data gap, then the full run filled A/B/C price gaps, force-retried the leftovers, and extended to D-grade by pageviews. 2,255 credits, 1,314 SKUs, 860 competitor prices. No edit_db.csv mutation (scrapes land in data/scrape_cache.db only).
- Pilot bugs fixed (all unit-tested, 134 tests pass):
clean_termdid not strip theREPLphantom marker → the top-A SKUs scraped as BASE-ONLY and lost their price.- a
PN MISMATCH/BASE-ONLYcached scrape (wrong product) was treated as fresh → permanently blocked re-fetching the right page for 90 days. Now retryable. - PDF cut-sheets passed
is_product_url→ wasted credits scraping a PDF instead of a product page. Now rejected.
- Placeholder-SKU resolution (
build_scrape_queue.py): the queue now resolves the OEM PN from the TITLE when the SKU is a placeholder (AGENTS.md trap), with a whole-token guard so a truncated SKU (S-3X4-00) is not mistaken for the title's real PN (S-3X4-0001). 57 of 652 A/B/C price-gap SKUs + 100 of the D-grade run resolve from the title; DF-house products (no OEM PN, e.g. "for DAMN Orbital VII") are excluded. - Day-run driver
src/run_scrape_queue.py: free search (exa→brave) →load_sku(urls=...)scrape-only, credit-capped against the requests ledger, resume-able,--needs any(dims ∪ price) and--force(gap retry). - Run results (2026-09-09): A/B/C price gaps 652 SKUs → 485 prices; gap fill (force retry) 168 → +28; D-grade by PV 494 → +347 (70%). Remaining A/B/C gaps answered in
generated/gap-report-2026-09-09.csv: 99 no-source (no competitor lists the PN), 35 listed-no-price, 4 wrong-page, 4 house — structural, not scrape failures.
2026-09-08 (scrape infrastructure — free search, cache hardening, identity, queue)
Rebuilt the competitor-scrape tooling so a scrape run spends Firecrawl credits only on final page fetches — never on search — and so the stored data is verifiable. Zero Firecrawl credits spent this session; everything offline-tested. No edit_db.csv mutation.
- Free-search adapter
src/search_adapters.py— exa → brave (keys from Bitwardenbws), returns the loader's result shape.scrape_loader.load_sku(urls=[...])now accepts pre-discovered URLs and runs zero paid search (the "Hermes search first, Firecrawl scrape-only" path insrc/README.md). Benchmark 2026-09-08: exa returns 6–8 PN-relevant hits of 8 for representative parts (Donaldson oval, Clark, Nordson, conical, TDC); brave verified as fallback. - Cache hardening (
scrape_cache.py/scrape_loader.py/scrape_parser.py):- refresh integrity —
upsert_scrapereturns the correct row id on refresh (SQLitelastrowidis stale afterON CONFLICT DO UPDATE, reproduced); a successful refresh atomically replaces observations; a failed refresh preserves the last good evidence; error/miss fetches are retried, never treated as fresh for the 90-day TTL. - price history — price observations are append-only (
recorded_atadded + backfilled from the parent fetch); a refresh appends a price row only on change. Spec fields still replace on refresh. - budget ledger —
requeststable; budget is real credits (search=2, scrape=1,CREDIT_COST, reconcile against account); cache hits cost 0 and are served BEFORE any budget reservation.
- refresh integrity —
- Identity resolver
src/resolve_identity.py→catalog_identitytable: resolves each cached part string to our Shopify Variant ID (join key, never SKU) via exact SKU → OEM base (phantom variants) → DFI (manufacturer-scoped, ambiguity-guarded) → Donaldson → Clark.page_statusrecords page validation; aTRUSTED_SCRAPEpredicate keeps PN-MISMATCH/BASE-ONLY pages out of any join. 274/274 cached aliases resolved (258 exact + 16 base-PN). - Source registry (
scrape_cache.py): circular sources (filterjunkie / filterprofessor) KEEP their prices — they are direct competitors — and are flagged only for dims corroboration (they mirror DF's DB). Six negligible-yield sources moved toDEPRIORITIZED(fallback only, ranked last, never a standard top-4 scrape). - Scrape queue
src/build_scrape_queue.py→generated/scrape-queue-*.csv: A/B/C cartridge primaries ranked by missing dims → missing price → anchor → orders → grade. First run (scrape-queue 2026-09-08): A/B essentially covered (9 price gaps total), C is the long tail (645 need price, 29 need dims).
Tests: 124 pass. No Firecrawl search is ever the default again.
2026-09-08 (Claude-review follow-up — Viktor's rulings applied)
Claude's deep review of the 09-07/09-08 commit range flagged four concerns. Verified all four against the live repo, then applied Viktor's rulings.
- §1.6 deleted the 5 in-stock NF twins despite the §1.9 guard — ratified. The audit of the full §1.6 DELETE group showed every deleted row is natural D/F with zero orders (the single A/B/C exception is the kept SB variant below), so the de-stock-then-delete was correct. Recorded here as a decision, not a silent exception. No data change; the DELETE marks stand.
- Kept
224445004_DAMN3-SB(spunbond exemption). It carried the product's only order (natural C, 1 order). Restored to its pre-§1.6 state (Variant Command DELETE -> MERGE, class -> cls-1414, in-stock FALSE) and added a durable "Downgrade exemption" note on the ptfe-sb primary so a future delete sweep skips it. Scriptsrc/tools/fix_review_followup_20260908.py. - Fixed three stale status notes (10005085, P148646, P148646_DAMN2-NF). The 09-08 holdover pass resolved the OD==ID and broken-primary-swap defects, but the notes still described them as unfixed and cited the removed open-question §2.18. All three rewritten to reflect the resolutions.
- P190884 family kept — both natural C with orders; already MERGE, no change.
- Ruling clarified: there is no "D-type ruling". The
product.type_uniformWARN (4 products) traces to the §1.5 wrap rule — a wrap is a standalone accessory with no variants; the 4 DELETE-marked phantom NF/SB variants still carry the "Cartridge Filters" type and clear on the next Shopify sync.
Invariants 0 FAIL / 2 warn (unchanged pre-existing warns).
2026-09-08 (§1.7 exotic/wide-pleat primary demotion — downgrade ladder restored)
Order #17882 (P199413) exposed that the 09-04 Matrixify import relabeled wide-pleat/exotic media onto STANDARD-ladder primaries — the OEM-spec exotic "took over" the 80/20/NF/SB primary slot, and the standard NF got orphaned as a blank-SKU variant. Blast radius: 36 B/C products (0 A) with an exotic/wide-pleat label on a standard-media SKU; 21 restorable + 15 single-variant exotics (pristine export shows they never had a ladder — out of scope).
- 21 products fixed (script
src/tools/fix_exotic_upgrade_demotion_20260908.py):- 18 round/conical → bare relabeled back to 80/20 primary, NF+SB ladder un-deleted (9 Group-B products), the OEM-spec exotic demoted to a NEW non-primary upgrade variant
<base>_DAMN1-<token>(keeps its price, grade x, drop-ship, blank Variant ID for Matrixify-create). - 3 ovals (cls-NR015, in-stock bin 1) → Nanofiber FR stays primary (no 80/20 on ovals per Viktor), wide-pleat demoted to
_DAMN1-WP; "Downgrade exemption … Nanofiber FR primary kept" note. P199419's NF re-priced $101.97 (sibling copy — flagged for confirmation).
- 18 round/conical → bare relabeled back to 80/20 primary, NF+SB ladder un-deleted (9 Group-B products), the OEM-spec exotic demoted to a NEW non-primary upgrade variant
- New SKU convention:
_DAMN1-<token>for exotic/wide-pleat upgrade variants — tokens WP / G90 / PTFE / NXPT / TFL / CARB. Viktor may revise suffixes/tokens later. - 19 new variants appended with blank Variant ID (Shopify assigns on Matrixify-create); no code precedent for variant creation yet.
- Duplicate
-2/blank-SKU 80/20 rows (G61-6250-2, P19-0837 blank, etc.): investigated (research 2026-09-08) — real live variants, standard-80/20 leftovers on exotic products, never ordered, natural D. D1 DELETE marks stand; keep DELETEd, do not restore.
Invariants: 46 checks, 0 FAIL (2 pre-existing warns: duplicate primary SKUs, 8PP mixed-type).
Follow-up (same day):
- Pricing stopgap — the 18 round 80/20 primaries carried the exotic price. 15 got a 65%-margin stopgap (cheapest 80/20 sibling PO cost ÷ 0.35, logged in
sku_status_notes; scriptprice_stopgap_8020_20260908.py); 3 had no sibling cost and were left for the reprice pass (E05286.03, P190848, P191178). P191496's 80/20 stopgap ($107.20) sits above its old 90/10-glass price ($95.90) — the exotic was mispriced low; the reprice pass fixes both sides. - Natural grade follows the variant (Viktor 2026-09-08) — natural grade is keyed to the Variant ID, not the media. The relabeled primary (kept its Variant ID) keeps nat B/C; the 19 new
_DAMN1upgrade variants (blank Variant ID, no sales history) drop to natural F (scriptfix_natgrade_upgrade_20260908.py). Folded intodocs/grade-pricing.md§3. - Variant creation (Matrixify) — the 19 new variants ship with blank Variant ID + blank Variant Inventory Item ID +
Variant CommandMERGE + unique_DAMN1SKU. Matrixify's MERGE adds a new variant when none matches, so the nextbuild_matrixify_import.pyrun creates all 19 — no builder change needed.
2026-09-08 (§1.6 media-ladder cleanup + presumed-exotic promotion)
Finished the §1.6 residual left after the 09-06 passes. Census: 92 exotic-primary + 55 spunbond-primary products; the correct (exotic/SB) variant is primary on all of them. This pass removed the remaining standard 80/20/NF/SB ladder variants and promoted two presumed-exotics.
- 21 standard-ladder variants DELETE-marked (Variant Command=DELETE, class / stock / grade cleared): 12 clean non-in-stock rows (the SB twins on the carbon/teflon exotics 828-0322 / 9420-14 / G75-6250 / G93-3450 / 41262-100, the 3 de-stocked NF twins P031474 / P031652 / P191896, and the CE262-0854 + P3087 spunbond ladders), 5 in-stock phantom NF twins on the same carbon/teflon exotics (bin 4 cls-0459 — de-stocked then deleted; bin 4 remains the standard NF class), and the NF + SB ladders of the two promoted exotics.
- 2 presumed-exotics promoted (OEM-confirmed via DFI, dims match): 224445004 (FARR) -> ptfe-sb (DFI E06817 grade 65 "PTFE Spun Bond", 6.06x2.72x20.25; the "Aluminized" label on its SB variant was wrong) — minted cls-1763; P19-1700-016-429 (TORIT) -> teflon-sb (DFI E05532 grade 64 "Teflon Coated Spun Bond", 13.84 OD x 26; label already "Hydro and Oleophobic Spunbond") -> cls-1415.
- 210823-002 is NOT exotic (web: Dusty Filters + Filter Junkie list it as 80/20 + nanofiber). Its SB variant's "Spunbond Polyester with PTFE Membrane" label was a data-entry error -> "Spunbond Polyester", re-keyed cls-1413 -> cls-0479 (the standard SB class).
- P25.70E held — spunbond primary whose 80/20 + NF twins hold the in-stock bins; the "spunbond promotion possibly wrong" case, needs a ruling.
Script src/tools/fix_media_ladder_cleanup_20260908.py (dry-run default). Invariants 0 FAIL / 3 warn, 100 tests OK.
2026-09-08 (§1.3 10005085 ID + §1.8 P148646 broken primary swap)
The two holdovers, resolved per Viktor's rulings.
- 10005085 (§1.3, TDC round 15.1 x 28): Viktor ruled ID = 10.6 (Farr/Clark). Fixed ID 15.1 -> 10.6, re-keyed out of the phantom cls-0358 (15.1x15.1x28) into minted cls-1764 (15.1x10.6x28 open). The last of the 11 OD==ID cartridges — the
class.od_equals_idWARN is now resolved (11 -> 0). - P148646 (§1.8, Donaldson Torit nanofiber): the broken primary swap. The NF primary carried blank dims (dcf-default) while the bare P148646 (natural B, 80/20 downgrade option) still held 7.93x3.61x16. Moved the bdf dims onto the NF primary (cleared from the bare row) and re-keyed: NF -> cls-0454, SB -> cls-0455, bare 80/20 -> cls-0126 (the 7.93x3.57x16 closed+bolt family).
bdf writes logged to class-overrides.csv. Script src/tools/fix_holdovers_20260908.py (dry-run default). Invariants 0 FAIL / 2 warn (OD==ID resolved), 100 tests OK.
2026-09-07 (open-questions rulings — Viktor)
Viktor answered the 12 human-review open questions (§1.1–§1.12). Decisions recorded inline in docs/open-questions.md; execution is the next set of data passes (each separately, with invariants 0-FAIL + tests green). Docs-only pass — no data change in this commit.
- §1.1 dcf-default — classify the full-dim, never-classified rows (pull dims DFI first, then Clark). Jason's meeting is done; re-check his missing-spec list against the latest Matrixify pull, then classify.
- §1.2 missing dims — backfill every blank dim from DFI / Clark / Donaldson / scraped authoritative competitor DBs (do not retire into dcf-default).
- §1.3 OD==ID — confirm each in DFI / Clark / others, determine which of OD / ID is off, correct it.
- §1.4 blank SKUs — address them, cartridges only (non-cartridge products out of scope).
- §1.5 wraps — no variants at all; standalone accessory. De-type + remove every variant (phantom NF/SB ladder is noise).
- §1.6 media audit — narrowed: not dealing with 80/20-vs-NF discrepancies. Only (1) mark the correct builder primary on exotic-primary / spunbond-primary PNs, and (2) remove the standard ladder variants from those, keeping the A-C downgrade exemptions (documented).
- §1.7 DFI dim disagreements — deep-dive to narrow the 109 (esp. the 30 A-C: 4 A / 2 B / 24 C); cross-check DFI vs Clark / Donaldson / scraped sources, consensus; A's first; never blindly apply DFI.
- §1.8 drift — re-described with per-row detail (all natural C + one natural B):
033k4l4/p031792are a benign wide-pleat-nf media re-derivation (80/20 bands -> wide-pleat-nf; accept, no fix);P148646_DAMN2-NF/P148646_DAMN3-SBleft unclassed by a broken primary swap (NF primary with blank dims; bareP148646natural-B still holds the dims — real defect, fix pending). - §1.9 deletion sign-off — granted; keep the spunbond + exotic exemptions, delete the rest (guards: never delete in-stock or outranking variants).
- §1.10 grade combos — deferred until classes are re-keyed.
- §1.11 downgrade exemptions — approved (stand as kept).
- §1.12 90/10-glass — cleared to delete the standard ladder; keep the 90/10-glass variants for all 11.
2026-09-07 (OD==ID correction pass — §1.3)
Applied Viktor's §1.3 ruling: confirmed the 11 OD==ID cartridges against DFI / Clark / Donaldson / competitor sources, determined which of OD / ID was off, and corrected it. 10 of 11 corrected + re-keyed; 1 flagged (not corrected).
- E040014 (conical, A-grade anchor): ID 12.75 -> 10 (closed end; body copy states "Closed end: 10 ID x 12.75 OD"). Re-keyed cls-NR012 -> cls-NR088 (80/20), variants -> cls-NR089 (NF) / cls-NR090 (SB) — three minted conical classes at 12.75 x 10 x 10.
- p031424_DAMN2-NF (Donaldson DFO/DCS/DWS oval, C-grade): the OD held the minor axis (11.4); the major axis (14.4) was missing. Fixed OD 11.4 -> 14.4. Re-keyed cls-NR021 -> cls-NR015 — the in-stock oval class (bin 1), where 30 sibling NF ovals already sit. This flips p031424 from out-of-stock to in-stock (bin 1): it is the same physical Donaldson Downflo oval, previously hidden in a phantom 11.4 x 11.4 class. new-grade -> 'a' (house/variant already 'a').
- 10001822 (TDC 12.09): ID -> 7.73 (DFI A08008 says 7.72; 50+ catalog siblings 7.73). cls-0211 -> cls-0207.
- 10004383 / 10005853 / 10005116 / 10001322 / 10001241 / 10002565 (TDC 12.75): ID -> 8.375 (every other 12.75-OD TDC filter carries 8.375/8.38). 10004383 -> cls-0029; 10005853 -> minted cls-1757 (stainless, 52"); 10005116 -> cls-1425 (bands); 10001322 -> minted cls-1758 (stainless, 27.65"); 10001241 -> cls-0465; 10002565 -> minted cls-1759 (10").
- 10003289 (TDC 13.865 nominal 13.8): ID -> 9.48 (sibling TDC filters + cls-0005 family). cls-0329 -> cls-0044.
- FLAGGED, not corrected: 10005085 (TDC 15.1 x 28, D-grade). ID is duplicated from OD, but the correct 15.1-OD ID is ambiguous across sources (10.6 Farr/Clark vs 10.71 Camcorp vs 10.72 Clark) and the Filter Professor source it came from left the ID blank. Left for a second look.
Matching = exact categorical signature (shape/endcap/media/flange/build) + dims within the tolerance window (not exact round(,2)) so 8.375 vs 8.38 vs 8.37 land in the same class. None of the 11 were in-stock pre-pass. Script src/tools/fix_od_equals_id_20260907.py (dry-run default); bdf writes logged to class-overrides.csv; 6 classes minted; invariants 0 FAIL / 3 warn (OD==ID warn 11 -> 1), 100 tests OK.
2026-09-07 (wrap variants deleted — §1.5)
Viktor's §1.5 ruling: a wrap is a standalone accessory with no variants at all. The four 8PP-* wraps still carried phantom _DAMN2-NF / _DAMN3-SB ladder variants typed "Cartridge Filters" (two held phantom single-member classes cls-1714 / cls-1715). Marked all 8 phantom variants Variant Command=DELETE + cleared their class — the wrap primary (Type=Accessories) is untouched. The 7 other wraps (3EA-* + p031xxx-016-210) were already single-variant.
src/tools/fix_wrap_variants_20260907.py (class clears logged to class-overrides.csv). Invariants 0 FAIL / 3 warn, 100 tests OK.
2026-09-07 (§2.1–§2.16 rulings + canonical-rule corrections)
Viktor answered the 16 policy questions (§2) and corrected several canonical rules. docs/open-questions.md §2 was restructured (resolved/dismissed items removed, two new questions added), and the canonical docs were corrected:
- DFI is AUTHORITATIVE (was "validation reference only"). Among the highest-authority sources we have, much more authoritative than our own data; we trust it. 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. (AGENTS.md, domain-model §7, skill.)
- Spunbond not-stocked is TEMPORARY, not a rule. There are no rules about what media can or cannot be stocked — that is an ownership/management decision.
stock.spunbond_never_stockedrenamed tostock.spunbond_not_stockedand downgraded FAIL → WARN. - Media =
Option1 Value(or thebdfvalue), never the SKU suffix. External resolution order: OEM/manufacturer → DFI → Clark → competitor listings. Impact: 47 cartridge rows (31 primary) derive 80/20 from the bare suffix while their Option1 labels NF/SB/wide-pleat. Code fix still open (canonical_media()). - DF part number = EFI number with the
EO/E0prefix →DF(EO1425 → DF1425). Complete scheme still unplanned. - Bins 4/6: no re-measurement — the bin dims are the bin dims.
- Conical/oval second face → Body HTML note (not a separate field).
- Pricing + sibling-cost = one question, held until classes trusted, then a 3–10% grade premium over the per-class base price.
- Natural grades out of scope — not this project's; do not touch unless asked. Margin rule comes from n8n later. Endcap taxonomy flagged+deferred. People roster deferred.
bws= Bitwarden (documented). - Data notes added to
sku_status_notes(112): 10005085 (ID ambiguous), p031424_DAMN2-NF (in-stock flip), E040014 (conical), P148646 + P148646_DAMN2-NF (broken primary).
open-questions.md: §2.1 (media fallback), §2.4 (endcap), §2.6 (second-face), §2.7 (pricing), §2.15 (roster), §2.16 (tooling) kept; §2.17 (DFI join), §2.18 (10005085 + P148646) added; §2.2/2.3/2.5/2.9 resolved, §2.10–2.14 dismissed.
2026-09-07 (§2.16 tooling docs + §1.2 partial backfill)
- §2.16 done. New "Optional tooling" section in
src/README.md: how each gitignored reference DB is built (clark_crossref.db / donaldson_catalog.db / scrape_cache.db / products.db),bws= Bitwarden (source ofFIRECRAWL_API_KEY), and the rule to use Hermes's search tools first, then Firecrawl only to scrape final data. - §1.2 partial. Measured 119 primary rows with a blank dim (106 blank ID, 13 blank OD+ID). DFI + Clark resolve only ~3 IDs directly; backfilled those three (
P19-0999-016-429→8.38,7FRO-5016→9.62,PA-2399→11.11) and re-keyed their NF/SB variants (9 rows). The rest need per-SKU OEM web research — Donaldson publishes no ID, Clark covers 812 PNs, scrape cache is sparse.
2026-09-07 (review-fix pass — Codex + Claude re-review of bba3127)
Both reviewers re-measured the three commits (bba3127, 576ce52, 472a11e) and confirmed every other dated claim to the row; they caught four defects, all now fixed:
- open-questions §1.4 duplicate/blank-SKU grade split was wrong (both reviewers). "all D/F-grade, zero A-C" was false —
f4jfj884ris natural C, and 74 of the 93 blank-SKU rows carry no natural grade at all (mostly non-cartridge). Rewrote the impact with the measured split (5 D / 14 F / 74 ungraded blank rows; 2 D + 1 C + 1 ungraded across the dupes) and dropped the "pure tail hygiene" overclaim — the one natural-C dupe needs eyes. - open-questions §1.4 list pointer was wrong (Codex). It directed readers to
build_review_sheet.pyfor blank-SKU rows, but that script'sregister()discards blank SKUs and defines no blank-SKU bucket. Corrected: dupes come fromdata.duplicate_primary_sku+ theduplicate-sku/dupe-suffixbuckets; blank SKUs are enumerated directly fromedit_db.csv(col 62 blank). - README refresh instructions omitted the bdf.* block and Variant Cost (Codex P1). Following them literally would store-copy over sculpted dims / endcaps and PO-cost. Added both to the preserved-cells list + pointed at
merge_matrixify_export.pyas the reference impl. - Count guard: scoped the docstring, did not extend the regex (Claude F2). The keyword pattern can't catch "N rows"/"N TRUE/FALSE" shapes without flagging legitimate prose; the durable fix is deleting the count. Documented that scope in
check_doc_countsso a later pass doesn't write another "reads like protection" guard. - Added the missing
site/build.pytest (Claude F4). A regression test locks the b371535 bold-regex fix (**bdf.* x**spans the literal*; single-asterisk italics stay unsupported).
Also corrected two factual errors in the bba3127 CHANGELOG entry itself: domain-model.md WAS already in the count-guard scan list (the miss was the keyword pattern, not the file), and the bdf.Cross_Ref 62/73 figures were primary-row counts that had not drifted — the 64/74 I "found" was a scope artifact (all-rows vs primary-only).
No data changed. Verified: invariants 0 FAIL, 100 tests OK, ruff clean.
2026-09-07 (project-alignment review — de-hardcode stale counts)
Internal review of the seven unpushed docs-consistency commits against live data. Every dated claim in open-questions.md verified accurate (re-ran natural_grade_triage, media_indicator_audit, dfi_review, override_drift, and direct csv.reader counts). Gates: invariants 0 FAIL (3 standing warn), 99 tests OK, ruff clean, site build clean.
Found three stale hardcoded counts the prior passes missed. Two live in docs OUTSIDE the docs.no_hardcoded_counts scan list (column-map / metafield- definitions); the third (domain-model §7) is IN the scan list but the check's keyword pattern only fires on "N class/SKU/in-stock/dcf-default", never "N rows" — so none were caught:
- column-map.md col 104 — "5,862 primary rows" was the 08-14 count; the live figure is derived (class work is re-keyed constantly). De-hardcoded.
- metafield-definitions.md — "2342 TRUE / 13066 FALSE / 38 blank" for sku.in_stock had drifted (now 2,326 TRUE / 12,935 FALSE / 328 blank). Counts removed — the boolean-vs-string type point doesn't need them.
- domain-model.md §7 item 4 — "bdf.Cross_Ref (62 rows) / Compatible_Sys (73 rows)" were primary-row counts (still correct at that scope); the all-rows count is 64 / 74. Counts removed ("sparse" is the point).
No data changed. Verified: invariants 0 FAIL, 99 tests OK.
2026-09-07 (doc-consistency review fixes — Codex + Claude re-review)
Reconciled the live docs to the locked rules and code across three commits (daf7f95, 605965d, b371535). No data changed.
- DFI role narrowed (open-questions §2.3) — validation-only, never an authority; dropped seed/override as options (disagreements adjudicate, never auto-apply). Only the join-key question remains.
- Endcap taxonomy corrected — "bands" is construction, not endcap hardware; build hardware (twist-lock, threaded-stud, d-ring, …) is already class-defining via
unique_build.py. The unenumerated remainder is residualbdf.Top_Config/bdf.Bottom_Configfree-text. - DF numbering scope narrowed — only the
DFprefix + no-media-suffix are settled; sequential-vs-encoded is still open. - n8n class_description format superseded — the 09-04 5-part string (
OD×ID×L · shape · media · endcap · build) omitted two of the eight class attributes; extended to name top+bottom endcap + build. §3.3 is the live format; the §7 09-04 record is kept verbatim with a dated superseding note. - Field levels reconciled — anchor is variant-only (col 92, no product twin); non-primary in-stock variants carry
a(notx). - Tolerance vocabulary — "slack"/"band" → "ratio"/"axis allowance" in prose; code constants
MAX_AXIS_SLACK/SLACK_BUDGETkept as legacy names. - Handbook renderer — bold regex now spans a literal
*; single-asterisk italics normalized to**bold**(renderer omits italics — wildcard ambiguity). This touched one historical 08-31 line; content-neutral. - KITCHEN-SINK-DEMO archived (stale mutator instructions + retired fixed-1/16″ tolerance).
- README refresh reworded as a supervised Variant-ID merge, not a copy.
Verified: invariants 0 FAIL (3 standing warn), 99 tests OK, site build clean.
2026-09-07 (docs-consistency pass — Claude review)
Applied a full doc-set review. Fixed docs that contradicted the code or data:
- Media derivation was documented as implemented, but
canonical_media()still falls to the SKU suffix. Marked step 3 NOT IMPLEMENTED in domain-model §5.4, AGENTS.md, and media-taxonomy.md; all point at open-questions §2.1. - ANCHOR corrected to col 92 (43 TRUE; col 66 is an empty
TEMP: ANCHOR) — column-map.md + final-export-fields.md updated. - bdf.* protection range fixed to cols 72–87 (
bdf.2nd_Descrat 72) in AGENTS.md + domain-model.md. - domain-model §8 bin table refreshed to
src/bin_map.py— bins 3/5/8 class IDs (cls-0458/0470/0477 → cls-0038/0267/0005) were stale since the 2026-08-27 merge. - README refresh range → 61–67 and 104–149 (was 104–136, would drop the competitor-spec block + vendor.cost) + a note that 63/65/68/92/93 are native metafield columns a fresh export overwrites.
- architecture.md — SKU suffix reframed diagnostic-only; "7 duplicates" de-hardcoded; nonexistent config/config.yaml dropped.
- Defined "DFI"/"EFI" (same dataset, legacy
efi_prefix) + "Canon" in AGENTS.md; added a full docs/ index to "Read first". - open-questions.md — wide-pleat moved to Resolved (settled 2026-09-01); "conical anchor" reserved-word fix; §2 renumbered.
- Misc: stock_id 1–10, "D9 margin rule" → TBD, natural-grade threshold units flagged, media-code range #12–#68, dangling pointers, stale column ranges.
No data changed. Verified: invariants 0 FAIL, 99 tests OK.
2026-09-07 (docs: reserved words, media taxonomy, + static handbook site)
- AGENTS.md — reserved words + trap fixes. New "Reserved words" section: "anchor" (Anchor SKU only), "bands" (construction straps/bindings), "grade" (house/natural only, always distinguished). Clarified placeholder-SKU and variant-SKU traps (SKUs ARE unique per Shopify; the duplicates/blanks in edit_db.csv are defects, not the norm). Physical measurement = top authority but rare and fallible (human error). SB/exotic handling spelled out: exempt a few, DELETE the ladder variants on the rest.
- domain-model.md §4/§5.4 — SKU suffix is an internal marker, not a classification input. The
_DAMN2/_DAMN3suffix is a DF-internal phantom-variant marker (a vestige; Shopify's unique-SKU requirement is why it exists). Media derivation no longer falls to the SKU suffix. *(Superseded same-day by the docs-consistency pass above —canonical_media()still falls to the SKU suffix for standard media; see open-questions §2.1.)* - grade-pricing.md — anchor count + column. Anchor count ~43 (was "~10, expanding to ~20–40"); ANCHOR column corrected to col 92 (was retired col 66).
- media-taxonomy.md — #10 Cellulose retired + new media definitions. #10 removed (only #12 80/20 used). Added
wide-pleat-nf,nf-on-sb,polyester-feltunder "New media definitions (no DFI code)". - New handbook site (
site/). Stdlib-only build (site/build.py) renders the repo docs into a static HTML site (10 pages) with Mermaid visuals. Local preview only — not deployed.
2026-09-07 (adopt Ruff — dev-only linter)
ruff.toml(select F401/F811/F821) + a CI step. Catches unused imports, shadowed names, and undefined names — the class of bug the 09-06 review found by hand. Dev-only, never imported at runtime; applied pass scripts are linted but never auto-formatted.- Fixed 20 pre-existing unused imports (one-shot + report scripts); the fixture_builder
Reportre-export is marked noqa.
2026-09-07 (Fable review follow-up: drift-report re-bucket + Type-uniform invariant)
- override-drift report re-bucketed. The headline active-drift bucket was ~10x noise (DELETE-marked variants + free-text class-note). deleted-row and class-note now get their own buckets; active-drift = 4 genuine rows. Live rows are keyed preferring the primary row.
- product.type_uniform WARN invariant (+ fixture tests): flags products whose rows span more than one Type — currently 4 (the 8PP wrap accessories whose NF/SB ladder variants still say Cartridge Filters; open ruling).
- Attribution + dead-link fixes: the 09-06 review entry credits Claude Fable 5.1 (was "Claude 3.5"); requirements-optional.txt points at docs/duckdb-analysis.md (was references/).
2026-09-07 (override-drift report + remaining wrap-accessory de-types)
- override-drift report (
src/override_drift.py, read-only -> generated/): compares the class-overrides.csv "last wins" log against live edit_db.csv and buckets every difference (active-drift / dead-class / gone-sku / blank-sku), turning the 09-06 "silently undone corrections" finding into a recurring check. - Seven more wrap accessories de-typed. The report's first run surfaced the 3EA-* and p031xxx-016-210 wraps ("Dryflo Wrap" / "Wrap" in body copy) whose 08-18 de-type was also silently undone. Restored Type -> Accessories + cleared class and the 09-01 A4/A5 bdf stamps (
fix_wrap_accessories_20260907.py), matching the 09-06 8PP-* fix. bdf edits logged to class-overrides.csv.
2026-09-06 (Fable-review: reversions + WARN invariants + hygiene)
Claude Fable 5.1 review surfaced six findings; all verified and acted on. Two supervised-correction reversions fixed per Viktor's rulings, two WARN invariants added, apply_overrides.py decommissioned, local-only vendor sources gitignored.
- Finding 2a — wrap accessories de-typed back to Accessories. Four wrap SKUs (8PP-39654-00, 8PP-35880-01/02/03) were de-typed on 08-18, then the 09-01 A4/A5 backfills re-stamped them as cartridges (Shape=Round / Bottom=Closed / Bolt_Hole=No) and 8PP-39654-00 re-entered cls-1679. Restored: Type -> Accessories, class cleared, the three bdf cartridge fields cleared (
fix_review_reversions_20260906.py); the wrap's own OD/ID/Length kept. - Finding 2b — P032931_DAMN2-NF stainless price. The 08-20 raise (115->138) was clobbered by the 09-04 bulk-price pass, which applied +3% to the pre-correction 115 (=>118.45). Restored 138 + 3% = 142.14, prev_price 138.00.
- Finding 2c — 43 gone override SKUs confirmed expected. 43 (sku, field) override pairs reference 23 SKUs no longer in the file: 20 are Donaldson OEM PNs removed by the 09-06 revert/ladder passes, 3 are bin-10 cls-0453 rows merged away 09-01. Nothing to resurrect.
- Finding 1 — apply_overrides.py decommissioned. Marked DO NOT RUN (docstring
- src/README.md): its "last wins per SKU+field" re-applier would change 483 cells and resurrect retired classes. Re-gate behind a dry-run that refuses dead classes before any reuse.
- Findings 3+4 — two WARN invariants added (
class.od_equals_id,data.duplicate_primary_sku) with positive/negative fixture tests: the 11 cartridges with OD==ID and the 2 duplicate primary SKUs are now visible and cannot grow silently. - Finding 5 — vendor sources gitignored. imports/orrstien-files/ and the Clark cross-ref PDF are local-only (like dfi/); provenance_join.py needs them restored from the source drive on a fresh clone.
2026-09-06 (Gemini-review fix: body-copy alignment + blank-shape backfills)
Gemini 3.1 Pro review surfaced two invariants WARN findings; both verified against live data and OEM sources, then fixed (fix_body_copy_shape_20260906.py).
- body_copy.field_alignment (3 Clark NF bodies). NF40197/NF40198/NF20322 carried a dimension where the top-endcap state belongs — the OD/ID/Top labels shifted one slot left ("Outside Diameter: Oval", "Top: 11.40""). Realigned the body copy only; bdf.* was already correct (14.4 x 11.4 x 26 oval).
- class.shape_inference_flag — P280475 (Donaldson). Blank bdf.Shape with "Square Footage of Media: Oval" (shape leaked into the sq-ft slot). bdf.Shape -> Oval; body restored a "Shape: Oval" field. Stays dcf-default (media is PTFE-membrane exotic — out of scope).
- class.shape_inference_flag — P527081-016-340 (Torit conical). Blank bdf.Shape; Donaldson catalog "Coned Ultra-Web FR" + Clark xref "OO Conical" (P527081 -> NF40029) confirm conical. bdf dims were a wrong-end read (wide end 16.0 x 11.5) corrected to the small-end values 12.75 x 8.375, matching cross-ref siblings NF40029 and P520444 already in cls-NR007. Re-keyed out of round cls-0379 into the conical family: bare 80/20 -> cls-NR007, _DAMN2-NF -> cls-NR049, _DAMN3-SB -> cls-NR050. Body block realigned (Shape: Open -> Conical; ID/Top/Bottom filled). bdf.* edits logged to class-overrides.csv (baseline gate green).
2026-09-06 (corrective — Codex review: tolerance fragmentation + re-run safety)
Codex review of the 09-06 passes flagged four findings; all four are fixed.
- Tolerance-window fragmentation (P1). The revert + spunbond passes built class signatures with EXACT rounded dims (
round(,2)) instead of the locked tolerance window, minting 9 classes that should collapse into within-tolerance classes. Merged viamerge_tolerance_window_20260906.py:cls-1724→1387,cls-1731→1728,cls-1733→1730,cls-1739→1423,cls-1742→0054,cls-1743→0033,cls-1749→1559,cls-1751→0831,cls-1754→0825(11 rows re-keyed). - Re-run safety (P2). The spunbond pass now refuses to re-apply on an already-applied database (idempotency guard on the exemption note); both passes resolve scope from PRIMARY rows only and warn on duplicate primary SKUs (surfaces the two known dupes
F4JFJ884R/P281426-016-002instead of silently taking the first row). - In-stock variant deletion (P1). The revert's ladder-delete step DELETE-marked three in-stock NF twins without the "never delete an in-stock variant" guard:
P031474_DAMN2-NF(bin 6),P031652_DAMN2-NF(bin 6),P191896_DAMN2-NF(bin 4). Their SB twins were not stocked (correct to delete). De-stocked the three NF twins instead of deleting them (fix_instock_ladder_deletion_20260906.py): Variant Command → MERGE, class restored (cls-0471/cls-0459),sku_in_stock→ FALSE,prev_grade→ 'a'. - Full cleanup. After the scoped merge,
merge_tolerance_window_full_20260906.pycollapsed the remaining 19 within-tolerance duplicates — 16 from earlier passes (cls-1639…1721) and 3 pre-existing pairs (cls-0088→0012, cls-0270→0028, cls-0261→0247) — 38 rows re-keyed. 9 + 19 = 28 classes collapsed total. - Claude re-review (script fidelity, no data change). Revert pass now resolves scope by primary SKU BASE (
split('_DAMN'), so an SB/NF primary likeP031790-016-436_DAMN3-SBresolves its bare EXEMPT key) and de-stocks (never deletes) an in-stock ladder variant in step 3; the scoped merge script regainedEXCLUDE+ class-overrides logging; dropped an unusedimport rein the full merge script; fixed the "19 plus 3" wording (the 3 pre-existing pairs are inside the 19).
2026-09-06 (corrective — revert/exemption collision + lost exempt media)
Claude review of the two 2026-09-06 data passes flagged two committed defects, both now corrected.
- P031122 collision. The Donaldson revert (natural D →
polyester-felt, ladder deleted) ran before the spunbond downgrade-exemption pass, whose scope wrongly listed P031122 as a downgrade-exempt "exotic primary". The downgrade study found exactly ONE exotic-primary (P190884, 90/10-glass); P031122 is natural D and gets NO exemption (locked rule). Re-DELETE-marked the NF/SB ladder (P031122_DAMN2-NF/P031122_DAMN3-SB) and dropped the contradictory exemption note from the primary. - Lost exempt media. P031790-016-436 (wide-pleat-nf) carried "OEM media None" — the revert script reverse-matched the primary's SKU (
…_DAMN3-SB) against the EXEMPT key (bare). Fixed to "OEM media wide-pleat-nf". - Root-cause fixes in the two passes: the revert script drops a dead
by_skuloop, removes two unreachableMEDIA_TO_OPTION1entries, resolves exempt media at scope time (not by reverse SKU lookup), and reads the actual house grade intosku_prev_gradeon de-stock; the spunbond script now resolves scope from primary rows only and drops P031122 fromSCOPE_PRIMARY_SKUS. src/scrape_parser.pynow maps "Nanofiber on Synthetic" →nf-on-sband "Needled/Polyester Felt" →polyester-felt(was 80/20 / ''), matching the substrate rule.src/media.pycomment corrected (the SB-family set is no longer "automatic" for labels that don't literally say "spunbond"). Tests added for both construction mappings, the new Option1 labels, andnorm_media.src/tools/fix_revert_exemption_collision_20260906.py(dry-run default).- Verified: invariants 43 checks 0 FAIL (2 warn), 89 tests OK.
2026-09-06 (spunbond downgrade exemption)
Applied the downgrade exemption to the 24 downgrade-study products (22 spunbond-primary + 2 exotic-primary). These sell standard-media variants against a premium primary — 213 downgraded-variant orders across the set.
- Keep all variants. Un-deleted the 38 DELETE-marked standard ladder rows (bare 80/20 + NF, and the 2 exotic-primaries' NF/SB): Variant Command → MERGE, class re-keyed to the correct media class, grade reset to non-primary
x(house blank, variantx, prevx). 18 classes minted where the dims' only members had been the deleted ladder. - Exemption note on all 24 primaries'
sku_status_notes: "Downgrade exemption … premium primary, standard ladder kept (legit downgrade, revisit)". This marks them to skip a future variant-delete sweep. - Follow-up (not done — needs OEM lookup): some spunbond primaries may actually have 80/20 as their true primary (spunbond promotion wrong). Once identified, those switch to single-variant 80/20 (drop spunbond + NF).
src/tools/spunbond_downgrade_exemption_20260906.py(dry-run default).
2026-09-06 (Donaldson media mapping — confirmed by Brandon Brigham + data pass)
Closed the Donaldson media-vocabulary choke point and applied the resulting media corrections.
- Brandon Brigham (DF owner) ruling — substrate rule. Nanofiber is a *surface layer, not a media* — the substrate it's coated onto determines classification. Cellulose substrate → NF (#24); any other substrate → exotic. Donaldson branding (Ultra-Web / Fibra-Web / Ultra-Tek / Thermo-Tek) does not indicate the substrate. Full mapping in
docs/donaldson-media-mapping.md(new). - Two new exotic media codes in
src/media.py(no DFI media match exists — closest are #24 nanofiber-on-cellulose and #60 plain spunbond, neither matches):nf-on-sb— nanofiber on a spunbond/polyester substrate ("Nanofiber on Synthetic"); spunbond bands construction (SB_FAMILY_EXOTIC); drop-ship until quote volume justifies stocking.polyester-felt— needled polyester felt (Donaldson Ultra-Tek / Thermo-Tek / High Temp); cage construction; no home in the standard model. Both are exotic: own class, no standard ladder, drop ship.wide-pleat-nfunchanged (geometry, already exotic).docs/domain-model.md§5.4 updated.
- Data pass (45 Donaldson-exotic SKUs) via
src/tools/donaldson_media_revert_20260906.py:- 28 natural D/F → REVERTED to OEM spec, no exemption. PN reverted to the OEM config PN (SKU + MPN + Title; Handle untouched per Viktor); media set to the OEM exotic (13 polyester-felt / 10 ptfe-sb / 5 nf-on-sb); 54 phantom NF/SB ladder variants deleted; 3 in-stock SKUs de-stocked (P031474, P031652, P191896 — grade demoted a→natural letter, prev_grade preserved); 27 re-keyed into 17 minted exotic classes.
- 17 natural A/B/C → EXEMPTION (Viktor). Kept as the standard 80/20 + NF + SB ladder; no media/SKU change; bare SKUs left bare. Rationale: these are high-order (231 orders, 2,623 PV) and we assume legitimate media downgrades — customers downgrading a premium (PTFE/wide-pleat/felt) to a cheaper filter. Recorded in
sku_status_notesas "Downgrade exemption … revisit". This is an exception to the exotic rule; revisit later. - Every affected primary carries a
sku_status_notesentry.
- Downgrade study (delegated). 23 products sell downgraded media against a premium primary (212 orders) — overwhelmingly spunbond-primary (22/23, including 18 already carrying DELETE-marked standard ladders with 162 orders on them), only 1 exotic-primary (P190884, 90/10-glass). Conclusion: downgrade behavior is a spunbond phenomenon; if the exemption is extended it should target spunbond-primaries, not exotics.
- Verified: invariants 43 checks 0 FAIL (2 warn), 82 tests OK.
2026-09-06 (OEM PDF ingests — Donaldson catalog + Clark cross-ref)
Ingested two OEM source PDFs into local reference SQLite DBs (gitignored, under data/) to cross-check our specs against authoritative sources. Neither mutated edit_db.csv — both are read-only reference layers.
- Donaldson Torit cartridge catalog →
data/donaldson_catalog.db. A 136-page SCAN (no text layer; OCR'd with rapidocr-onnxruntime). Cartridge part-number tables on even pages 12–54, facing dims tables on odd pages. 605 part rows / 278 unique Donaldson PNs with media type, filter area, pleat height, FR/liner/stainless flags, OD (competitive section), and competitor OEM PN. Key gap: Donaldson publishes OD + Length only — no Inside Diameter. Parserscratch/parse_donaldson_catalog.py. Cross-check vs catalog: 156 bareP#####SKUs prefix-match, 81 full PNs, 6 competitor OEM PNs. - Clark Filter DC cross-reference →
data/clark_crossref.db. A 51-page Excel-export (text) cross-ref (author "Pete Rynier", 2014-05-27). 812 rows mapping OEM PN → Clark PN with FULL dimsOD × H × ID+ endcap code (OO/OC/OCB bolt-hole/Flange/Conical). The only ingested source with the Inside Diameter. Parserscratch/parse_clark_xref.py. - Provenance finding. The Clark cross-ref matches our
bdf.*at OD 92% / ID 92% / Length 98% (disagreements are rounding-level: 7.8 vs 7.625) across 685 OEM-PN and 728 Clark-PN SKU matches — strong evidence the Clark cross-ref (or a derivative) is the source of our dims for these SKUs. A handful of real gaps flagged for review (P18-1062 OD 6.09 vs 5.25, P181106 10.375 vs 10.22).
Source PDFs, DBs, and parsers are not committed (data/ and scratch/ are gitignored; the PDFs are untracked). Full workflow + parser gotchas live in the df-product-db skill (references/donaldson-catalog-ingest.md, references/clark-crossref-provenance.md).
2026-09-05 (Codex handoff — dims-drift research)
Created docs/HANDOFF-2026-09-05-dims-drift-codex.md — a self-contained handoff for an external agent (Codex/ChatGPT) to scrape manufacturer + Tier-2/3 distributor sites and resolve the 134 dims-drift rows (source-vs-current disagreements >0.05" on OD/ID/Length). Embeds the full 134-row list (pn, vendor, our dims, source dims) + the source-priority list + the circular-source blacklist (filterjunkie.com / filterprofessor.com) + the return CSV schema. Data also in generated/dims-drift-handoff-2026-09-05.csv (gitignored).
2026-09-05 (Orrstein source files — provenance join)
Viktor provided the original Jeffrey Orrstein source files (the initial DamnFilters product database). Two readable sources:
imports/orrstien-files/DAMNNEWADDSv2.xlsx— the original Shopify catalog.imports/orrstien-files/damnaddsforvictor.xls.xlsx— the DIRECTOR spec directory (added 2026-09-05, same pass).- What it is.
shop.final(7,343 variant rows) is the original Shopify export; its Option1 Value is the ORIGINAL media declaration (bare = "Standard: 80/20",_DAMN2-NF= "Upgrade: ...Nanofiber FR",_DAMN3-SB= "Upgrade: ...Spun Bond Polyester").damn.wash3(2,448 rows, one per product) holds the original spec columns (OD / Length / ID / Top / Bottom / Shape / flange / sqft / Sell Price / media adders).products(275 rows) = the pre-ladder starter set.Panels(783) = a part→dims lookup. Toolchain clues (AutoCrat, NV Scripts, wash/presort/merge/final sheet names) record the original workflow. The DIRECTOR (DIRECTOR2,469 rows +Dups removed 11,591) is a master spec/price directory: Vendor, SKU, OD/ID/Length, Bolt hole, Top, Bottom, Shape ("Picture": Round/CONE), SqFt, Sell Price + NF/SB/Gasket adders — NO media declaration.Dups removed 1also carries "Real sell/Real nano/real spun" (a pricing-correction signal, surfaced not consumed). - New diagnostic
src/provenance_join.py(read-only →generated/provenance-join-<date>.csv): joins both sources → edit_db.csv on part number (case-insensitive, P-prefix normalized), emitting source_origin / source_media / source dims/endcap/bolt/shape/flange / price/adders per row. First run (2026-09-05): 12,889 cartridge variants → 6,945 original (53%) / 4,190 director (32%) / 1,754 later-add (13%) — 86% now have source provenance. Source media vs our derived agrees 6,915/6,944; the 29 disagreements are all exotic-corrections (source=80/20 was wrong, exotic pass fixed it). 142 rows show dims drift >0.05" vs the source — the correction-vs-drift candidates. Source files are UNTRACKED (not committed).
2026-09-05 (SKU media indicators — diagnostic, never classification)
Viktor ruled that SKU media indicators (bare / _DAMN2/_DAMN3 / NF-prefix / -NF/-SB/-N101 suffix / -orig) must NEVER assign media — they only FLAG problems or CONFIRM accuracy, resolved via OEM/competitor/DFI lookup. Captured as a locked rule in AGENTS.md + the skill.
- Context. A bare SKU is, in almost all cases, the ORIGINAL OEM marketing SKU (the ad-buying SKU) — being bare does NOT mean 80/20. The three-media expansion ladder (
_DAMN2/_DAMN3) is a PHANTOM SKU (generated upgrade row), with exceptions (P191920: entered as NF, the 80/20 was removed after complaints, NF/SB kept).-orig= an open-box return resold at OEM price (vanity, not a real item). Media written into the SKU name (NF-prefix like Clark's NF20xxx,-NF/-SB/-N101suffix) is industry part-numbering — OEM naming or a reseller renaming an old PN — and UNRELIABLE (some Clark NF-prefix SKUs show competitor evidence of 80/20).FR/BHare NOT media indicators. - New diagnostic
src/media_indicator_audit.py(read-only →generated/ media-indicator-audit-<date>.csv): cross-checks, per cartridge variant, the derived media (canonical_media()), the declared media (Option1 incl. STANDARD NF/SB labels), the body-copy media signal, the SKU media indicator, and scrape-cache OEM/competitor evidence. Flagssku-indicator/label-says-NF|SB/body-says-NF|SB/oem-conflict/oem+label-agree. First run (2026-09-05): 298 flagged rows, 67 in-stock — the resolution set for the OEM/competitor lookup pass (no data mutated).
2026-09-05 (OEM spec-source research + source-registry model)
Delegated research across all 227 catalog vendors to find who publishes per-PN cartridge specs; rebuilt the source registry around a two-axis model. The conclusion: only a small, fixed set of OEMs publishes per-PN specs — everything else is reseller-consensus, non-cartridge, or defunct.
- norm_media fix (09-04): was matching "nanofiber"/"spun bond" in site chrome (banners, PNG filenames) — now reads the labeled "Media Type" field (captures the full 80/20-base + NF-upgrade ladder) plus a ±200-char context window. Media flags in the comparison dropped 204 → 78.
- Vendor-scope guard (09-04, subdomain-aware 09-05): a Tier-0 domain is authoritative ONLY for its own vendor's PNs — robovent.com no longer surfaces as "Tier-0" spec for a Donaldson PN; filters.camfilapc.com still counts as Camfil's own domain.
- Two-axis source model (09-05, Viktor):
tier= authority class (0 OEM / 2 aftermarket / 3-4 retail);is_active= whether it publishes per-PN specs at all. Non-publishing OEMs (TDC, Wheelabrator, Micro Air, …) stay tier-0 butis_active=0— NOT demoted into the retail tiers, which we still need good info on. Encoded asNON_PUBLISHING_OEM+TIER_ADJUSTMENTSinsrc/scrape_cache.py; loader skipsis_active=0. - Full vendor research (3 rounds): authoritative per-PN set = Donaldson, Camfil (filters subdomain), AAF, Nordson, Parker/BHA, RoboVent, Imperial, ACT, Filpro, Wynn, Royal, Mikropor, C&W DustTech + partial Filtration Group/Nederman. Full map incl. non-cartridge (hold for non-cartridge work) and defunct (re-research later) lists:
references/oem-spec-sources.md. Registry now 64 sources. - Media conformance rules (Viktor 09-04):
references/oem-media-conformance.md— collapse to SB-only / bare-exotic when the OEM says so; keep the 3-variant ladder when OEM = 80/20/NF; anchor overrides (P191889/P191920 ovals).
Next up (not started): canonical_media derivation fix — bare-SKU media falls through to the 80/20 default despite body-copy/Option1 evidence.
2026-09-04 (Firecrawl scrape-cache pipeline + A/B/C competitive re-scrape)
New competitor-data infrastructure, replacing the old flat-CSV tooling:
- Scrape cache:
src/scrape_cache.py(4-table SQLite store + 59-source 5-tier registry),src/scrape_parser.py(raw markdown → observations),src/scrape_loader.py(search→scrape→parse→store→dedupe, 90-day TTL).data/scrape_cache.db(gitignored) holds 843 scrapes / 6,536 observations / 274 OEM part numbers. - Re-scrape: natural-A (34) + B (76) + C
orders_sku>5(217) cartridges → 270 unique OEM PNs, 1,015 credits (the earlier A/B/C studies were never backfilled into the cache — D4 deferral). - Parser fixes: (1) scrape-error note clobber — errors were overwritten as "PN MISMATCH"; now preserved. (2) length/mm — the ">40 ⇒ mm" heuristic fired on explicit-inch units (52" → 2.047"); now explicit inch units are trusted and bare >40 converts mm only for diameters, never length.
- Comparison + adjudication:
generated/compare-catalog-2026-09-04.csv(274 PNs; flags media 204 / price 153 / dims 59 / bottom 8) andgenerated/mismatch-adjudication-2026-09-04.csv(16 false-match / 20 likely-same / 26 base-only / 28 no-dims). The media flag is dominated by "our 80/20 vs comp NF" — mostly comp-parser noise, not a data gap (seegenerated/media-derivation-finding-2026-09-04.md).
2026-09-04 (Matrixify import — metafields, vendor cost, pricing)
First Matrixify import built. Three data changes (Viktor 2026-09-04 decisions: mint fresh import; keep sku.in_stock boolean with lowercase values; 3% round to nearest cent; backup old price in sku_prev_price):
- vendor.cost re-added (col 149): appended
vendor_cost, populated from the native Variant Cost (49) on the 964vendor_status == passed-reviewPO-match rows only (PO-asserted unit rate; under-review rows left blank). - Class-label re-sync: live Google Shopping labels 65 (product) / 68 (variant) re-mirrored from working col 104 (new-class) — stale after the 09-01..09-04 re-key passes.
- Pricing: +3% (Decimal ROUND_HALF_UP) on 10,545 non-in-stock cartridges; in-stock schedule from
imports/Priced_SKUs - in-stock-skus-2026-08-20-priced.csv, priced by SKU (the CSV's class column is stale — ignored): use recommended_price if present, else sell_price, for all 2,353 CSV SKUs (54 numeric price changes; the rest re-synced to sell_price; 1 phantom SKU61E45— stored as Excel-corrupted6.10E+46— has no CSV price and is skipped). Both snapshot current price tosku_prev_price(114) before a numeric change to Variant Price (39).
Built generated/matrixify-import-<date>.csv: 129 cols re-mapped onto the live store's Matrixify_Full.csv header (the authority on live metafields/types), 15,589 rows. sku.in_stock TRUE/FALSE lower-cased to true/false for the boolean definition.
Post-review refinements (Claude Code review, Viktor rulings 2026-09-04):
- 61E45 SKU fix: the base 80/20 row (VID 8473434751037) carried the Excel-corrupted SKU
6.10E+46(live store had6.1E+46); corrected to61E45in edit_db.csv + the priced CSV (title/handle + NF/SB siblings confirm the real part number). Variant MPN (col 96) was also6.10E+46— corrected to61E45(feeds Google Shopping product matching). - Deletions deferred (force-MERGE): the 114 variant + 1 product DELETE markers (D1 exotic + spunbond ladder; all natural C/D, 0 in-stock, 0 A/B, guards hold) are neutralized to MERGE in the import OUTPUT only.
edit_db.csvkeeps the DELETE markers; a dedicated signed-off deletion pass will apply them later. - Row sorting (Matrixify dry-run fix):
edit_db.csvis ordered primaries-then-variants, which Matrixify flags as "same Handle repeats after other rows" (4,405 handles). The builder now re-sorts product-grouped (Product ID → primary first → Variant Position) so every product's rows are contiguous; dry-run warning cleared. - Supplemental follow-up import (main import already running): 61E45 price $147.89 → $107.70 (class-consistent; prev_price already $147.89 so backup is implicit), plus 40 Variant Position collisions renumbered to 3 (38 spunbond bare-80/20 rows colliding with their SB primary + 2 keep-ladder NF rows colliding with bare at pos 2). Built via
src/tools/build_matrixify_supplemental.py→generated/matrixify-supplemental-<date>.csv(41 rows, minimal 8 cols, all MERGE).
Applied one-shot: src/tools/matrixify_import_data_pass.py. Re-runnable: src/tools/build_matrixify_import.py. Verified: invariants 43 checks 0 FAIL (2 warn), 82 tests OK.
2026-09-04 (revert 22111/22211 dims correction — Daniel measured 9.48)
Reverted the 22111/22211 half of the 2026-09-04 competitor-study correction: Daniel physically measured the bin-8 stock item associated with 22111 at 13.84 OD × 9.48 ID × 26 L, O/O — the "correction" to 13.8 × 10.37 was wrong.
The 10.37 came from a filterprofessor.com scrape the study report itself flagged "may mirror DF" (circular), plus a false "ACT 2-series family uniformly 13.8×10.375" claim (the family is heterogeneous: 22111/22211/22611 = 9.48 O/O, 22411 = 10.50 flanged). 22111's body HTML carried the smoking gun — a "Dimensions: 10.37 x 13.80 x 26" summary line contradicting its labeled "Inside Diameter: 9.48 / Outside Diameter: 13.84"; 22211's body already read 9.48, so the correction made 22211's bdf contradict its own body copy.
Restored: bdf OD 13.84 / ID 9.48; classes 80/20→cls-0005 (bin 8), NF→cls-0478 (bin 9), SB→cls-0479 (no bin); in-stock TRUE for 80/20 + NF; grades a (primary), a (in-stock NF), x (SB); sku_prev_grade c (primary) / x (variants); cleared the competitor-study class-note suffix + grade-note. Fixed 22111's body-copy summary line 10.37 x 13.80 → 9.48 x 13.84. Removed the 4 stale 22111/22211 class-overrides.csv lines (apply_overrides would otherwise re-apply them) and appended a revert note. 33-0230's O/O correction untouched (order-history-based).
Applied one-shot: src/tools/revert_22111_22211_20260904.py. Verified: invariants 43 checks 0 FAIL (2 warn), 82 tests OK.
2026-09-04 (competitor-study corrections — Rachel adjudicated)
Adjudicated corrections from the natural-B + class-sibling competitive studies (surveyed competitor sites for dims/media/endcap on sampled SKUs; Rachel ruled the site data wrong on three):
- 33-0230 (UAS/Dust Hog) — bottom endcap is O/O (open/open), NOT closed+bolt. Order 13888 shipped O/O; the one closed+bolt order (14552) was returned/refunded. Fixed
bdf.BottomClosed→Open,bdf.Bolt_HoleYes→No, clearedbdf.Bolt_Hole_Dim. Re-keyed cls-0038→cls-0267 (open 12.74×8.38×26 80/20), bin 3→5 (still in stock, grade a). NF variant cls-0459→cls-0471 (bin 4→6); SB variant cls-0460→cls-0472. - 22111 + 22211 (ACT) — true dims 13.8 × 10.37 × 26 (the "9.48" ID was misapplied; the ACT 2-series family is uniformly 13.8 × 10.375). Fixed
bdf.Outside_Diameter13.84→13.8,bdf.Inside_Diameter9.48→10.37 on both. They are the SAME filter sharing dims — 22211 is 80/20 FR, which collapses to 80/20 (media taxonomy), so both land in the same class: 80/20→cls-1722, NF→cls-1723, SB→cls-1724. De-stocked (no bin maps 13.8×10.37): bin 8/9 cleared, grades demoted a→c (primary) / a→x (non-primary), sku_prev_grade recorded. - Intermediate revert corrected: an earlier version of this pass wrongly split 22211 back to 13.84 × 9.48 on the FR line; reverted (FR is not a class split). Final state has 22111 and 22211 sharing cls-1722.
- Log: 7 lines in
imports/class-overrides.csv(authority Rachel, 2026-09-03). - Applied one-shots:
src/tools/rekey_competitor_study_20260903.py+src/tools/restore_22211_competitor_study_20260903.py(both# APPLIED). - Verified: invariants 43 checks 0 FAIL (2 warn), 82 tests OK.
2026-09-03 (anchor SKU designation — Viktor's April sales/ad-spend review)
Viktor identified ~40 "anchor" SKUs from a review of sales + ad-spend figures (two-year period, April-start). Anchors = competitive loss-leader entry-point products (searched by OEM part number), watched closely in advertising as a predictable profit center.
- Applied: anchor designation (
Variant Metafield custom_label_3, col 92 =TRUE) to the PRIMARY variant of 43 product handles — 37 newly set, 6 alreadyTRUE. Value matches the existing convention (col 92, variant-level only, no product twin — Viktor 2026-08-27). Legacy col 66ANCHORleft empty (retired). - Pre-existing anchor outside the list:
original-donaldson-p191920-016-436(SKUp191920-016-436-orig, cls-NR042) already carriedTRUE; removed 2026-09-03 per Viktor (the original Donaldson part is NOT an anchor). Anchor total 43. - Data-quality flags: two anchors are unclassified with missing dims —
P148646_DAMN2-NFandP030915_DAMN2-NF(bothdcf-default, blank bdf dims). Designated anchor but not yet classified — needs dims backfill + reclass. - Handle typo corrected:
e040013-ny-blower-oem-replacement-filter→e040013-ny-blower-oem-replacement-dust-collector-filter(SKUE040013, cls-NR011). - Log
imports/anchor-designation-2026-09-03.csv(44 rows). - Verified: invariants 43 checks 0 FAIL (2 warn), 82 tests OK.
2026-09-02 (A4 blank-endcap recheck)
Viktor: revisit the A4 decision (blank bottom endcap → Closed/No) — many likely should be bolt-hole. New tool src/tools/a4_endcap_recheck.py scrapes the 129 "no bolt hole" A4 primaries against competitors (Firecrawl, 244 credits).
- 46/129 flip candidates (16 closed+bolt + 30 open), 6 confirm-closed, 77 miss. Hypothesis confirmed — and the correction runs BOTH ways (open, not just bolt).
- ~25 clean product-page flips; ~21 noisy (champion-la category pages, fliphtml5 catalogs, test SKUs). High-confidence closed+bolt flips: p033418, 1525856-002, E04918, P546761, 2116150M, 3EA-35880-02, P526509.
- Report
generated/a4-endcap-recheck-report-2026-09-02.md; resultsgenerated/a4-endcap-recheck-2026-09-02.csv. Flag list only — no edit_db writes.
2026-09-02 (media-primary swaps — P191544 NF, two spunbond primaries)
Viktor rulings on the sibling-probe media findings. New tool src/tools/swap_primary_to_media.py (promote a variant to primary, keep the ladder, move product-level fields, swap Top Row/Variant Position/variant-grade).
- P191544 → Nanofiber primary (confirmed NF on multiple sites). P191544_DAMN2-NF promoted to primary (cls-0483); bare 80/20 demoted (cls-0482), SB kept (cls-0496).
- P031790-016-436 → Spunbond primary (body states "media is Spun Bond"), 80/20 + NF variants KEPT (in-stock bins 8/9). P031790-016-436_DAMN3-SB promoted (cls-0479).
- 219617001 → Spunbond primary, 80/20 + NF kept (in-stock bins 3/4). 219617001_DAMN3-SB promoted (cls-0460).
- 22 bdf.* cells logged to class-overrides.csv; log
imports/media-primary-swap-2026-09-02.csv. - Note: the two SB primaries carry variant-grade
a(mirrors product house grade) while themselves drop-ship (not in-stock) — the in-stock 80/20/NF twins keep their bins. Invariants 0 FAIL.
2026-09-02 (class-sibling misclassification probe)
Random sample (seed 42) of 100 A/B/C-graded primary SKUs from the 9 large classes containing a natural-A sibling (cls-0038/0005/0478/1300/0029/0467/NR015/0067/0482), scraped vs competitors (Firecrawl, 197 credits). New tool src/tools/class_sibling_sample.py; results generated/class-sibling-results-*.csv, report generated/class-sibling-report-2026-09-02.md.
- Dims: sound — no dimensional misclassification; the few dims flags were false-matches (NF40198→NF20029, 190818→E06071, 325370, E05262 parse).
- Media-primary: P191544 (cls-0482) NEW — body says "OEM spec is Nanofiber" + competitor NF, but our 80/20 is primary. P031790-016-436 + 219617001 re-confirmed (already REVIEW in spunbond-reclass, in-stock conflict, kept as-is 09-01).
- Systemic bolt-hole flag (~20/100): competitor "Closed" vs our "closed+bolt" on cls-0038/1300/0029 — filterjunkie's bolt field is inconsistent (records "with Bolt Hole" only on some pages), so flag-not-conclusion; DFI spot-check recommended.
- Parser bug (documented, not fixed): exotic-first media parse reads a "90/10 glass" dropdown OPTION as primary (1212727 false positive). Best-effort parser; evidence string carries the context for human review.
2026-09-02 (natural-A competitive study)
Study of the 66 natural-grade-A SKUs (34 cartridges + 32 non-cartridge). Scraped the 34 cartridges against competitor sites (Firecrawl search + scrape, 68 credits); wrote durable TEMP competitor-spec columns + a full comparison report.
- Tooling:
src/tools/natural_a_manifest.py(extract the 66 A-SKUs),natural_a_competitive_study.py(scrape; parses competitor dims/media/shape/ bottom-endcap/bolt-hole/flange/price with evidence-carrying best-effort parsers),natural_a_analysis.py(comparison summary),append_competitor_specs.py(writeback: appends cols 138–148 + fills col 137, verified field-by-field). - TEMP columns 138–148 (
competitor_sale_price/od/id/length/shape/media/ bottom/bolt_hole/flange/spec_source/spec_confidence) — reusable store for future Firecrawl scrapes.edit_db.csv138 → 149 cols; column-map + tests updated. - Findings (report
generated/natural-a-study-report-2026-09-02.md): dims correct on ~32/34 (P190598 L 26-vs-25.5 real; a few false-match/parse artifacts). No exotics in the 66; 1 candidate (FCS183600HF — competitor says spunbond+PTFE membrane). 12 NF-primary rows show the competitor generic "80/20 base + NF upgrade" ladder (non-authoritative on primary). 6 bolt-hole disputes (ours closed+bolt vs competitor "Closed"). P191115 flange disputed. Pricing: median our/competitor 0.95; high outliers P7401RM 1.77× / 325325-001_NF 1.50× / P191115 1.40×; low outliers 156996 0.68× / P190817-818 0.75×. - Action list: FCS183600HF (media re-class), P191115 (flange + price), P7401RM (price), 156996 (bolt + price), 325325-016/001 (media + unclassed NF variant), P190598 (dims + bolt).
2026-09-02 (search backend — Firecrawl for search AND scrape)
Viktor: drop Tavily and SearXNG entirely — use Firecrawl for both search and scrape.
- Scripts (
lookup_dims.py,natural_a_competitive_study.py,refix_bulk_prices.py):tavily_search→search()which calls Firecrawl/v1/searchdirectly (normalized to{results:[{url,title,content}]}).get_keys()pulls onlyFIRECRAWL_API_KEYfrom bws. SearXNG (degraded — all engines suspended) and Tavily are both dropped. - Firecrawl
/v1/searchverified working (data[].url/title/description, rich spec snippets). Cost: each lookup now burns 2 Firecrawl credits (1 search + 1 scrape) against the 5,000/mo budget. - Hermes
config.yamlsearch_backendstill on tavily (separate from these scripts); switch it when a working search backend is chosen.
2026-09-02 (web-gap re-run + competitor pricing + class backfill)
Viktor wired DF's NEW Firecrawl account (5000/mo; key viktor_hermes in bws) and re-ran the competitor dim-lookup with source-trust tiers + price capture.
- Lookup (
src/tools/lookup_dims.py): tier-1/tier-2 site lists + filterjunkie/filterprofessor flagged scraper-derived (counted normally, flagged not excluded) + manufacturer tie-break (donaldsontoritfilter.com, clarkfiltersdirect.com). Confidence is cross-source (high = 2+ sources). Scrapes the PRODUCT page (never a category listing) on every SKU; captures competitor LIST price (sale-aware, $10–$5000 cap). Budget 900 → 2000. - Full run: 414 researchable SKUs, 826 credits (414 Tavily + 412 Firecrawl) → 206 full + 141 partial dims, 335 prices.
generated/dim-lookup-results-2026-09-02.csv. - Dims writeback (
src/tools/writeback_lookup_dims.py): high/med full dims → 72 primary SKUs, 160 changes (45 OD / 60 ID / 55 L). Nat-grade exposure D 68 / C 3 / F 1 — zero A/B, zero in-stock. - 6 duplicate classes merged (
src/tools/merge_dup_classes.py— now dominant-by-size): cls-0445→0088, cls-0057→0267, cls-0901→0471, cls-0902→0472, cls-1259→0533, cls-1260→0534. - dcf-default reclass (
src/tools/reclass_dcf_default.py): 63 products → 58 re-classified + 5 parked (unsane dims); 134 rows re-keyed, 49 classes minted. dcf-default 348→290; distinct classes 1515→1558. - Competitor price column: col 137
competitor_list_price(variant-level,src/tools/append_competitor_price.py); column-map 137→138. 331 rows. Median competitor/our = 0.94 (we're ~6% high; 37% within ±10%). - $2500 placeholder re-fix (
src/tools/refix_bulk_prices.py): 36 SKUs had donaldsontoritfilter.com's "$2,500" bulk placeholder → re-fixed to real reseller unit prices ($108–$1,101); 0 remain.
2026-09-02 (Claude review of ca48790 — grade fixes)
- 146416B (APPLIED): the NF→80/20 primary swap promoted it to primary but left variant grade (93)=
xand set house grade (63)=awhile the SKU has no stock id andsku_in_stock=FALSE.ameans "in-stock, high margin" — an out-of-stock primary isb. Set 63=b, 93=b+ grade-note. - 4 spun-bond bare-OEM variants (APPLIED): 1212249 / 1566126 / E05814 / NF20080-L were demoted to non-primary + DELETE by the spun-bond reclass, but the reclass moved product-level fields to the new SB primary without clearing the old primary's grade cells — leaving house63=
f/ var93=fon a non-primary row. Reset to house63='' / var93=x(non-primary inherit), matching the_DAMN2-NFsibling template. - 10 de-stock demotions (APPLIED): the cls-0038 / wide-pleat splits set house grade to the lowercased natural-grade letter (C→c, D→d) with no grade-note and left
sku_prev_grade(116) holding the pre-promotion grade instead of the overwrittena. Added a grade-note to each row and set prev_grade=a. Demotion rule written intodocs/grade-pricing.md§3.src/tools/fix_review_20260902.py; logimports/grade-fix-20260902.csv. - Still open (not in this pass): col63 ≠ col93 on 44 primary rows (the remaining
d/x,b/x,c/x,f/x,''/a,''/x,a/xbuckets — 146416B was one of the 3a/x). Separately, the 5-exotic reclass+de-stock (review item 4) is Viktor's call, not applied here.
2026-09-02 (media taxonomy + cls-0038 split — wide-pleat, carbon/teflon, primary swap)
Viktor: media conforms to 3 standard types (80/20 / NF / SB); exotics and wide-pleat nanofiber are isolated to their own classes. 80/20 FR and 100% Cellulose collapse to 80/20 (FR language stays in body HTML).
src/media.py: addedwide-pleat-nf(a distinct media, not a pure exotic) for "Nanofiber on Spunbond Polyester" / "Wide Pleat Nanofiber*"; it wins over the BARE suffix only (a_DAMN2/_DAMN3variant keeps its own media) and is added toSB_FAMILY_EXOTICso its construction defaults to bands (spunbond-substrate, body band/cage language is boilerplate — domain-model 5.3).- cls-0038 split (APPLIED): 8 primaries re-keyed out of the 80/20 bin class — 3 wide-pleat →
cls-1665, 4 carbon →cls-1667(non-flanged;cls-1395is the flanged carbon class), 1 teflon →cls-1666— Option1 changed to the exotic label on the 5 DFI-exotic, in-stock cleared, house grade set to the natural-grade letter. 22 "Default Title" Option1 cells backfilled to "80/20 Cellulose/Polyester Blend".src/tools/reclass_cls0038_media.py; logimports/cls0038-media-split.csv. - Wide-pleat catalog sweep (APPLIED): the media fix surfaced 8 more bare-suffix wide-pleat members scattered across 80/20 classes — re-keyed to
cls-1665(p031792),cls-1668(7.93×3.57×16),cls-1669(12.74×8.38×26 open, 033k4l4 + P191119),cls-1670(12.74×8.75×26),cls-1671(13.84×9.48×26),cls-1672(17.5×14×26),cls-NR087(17.5×13×26 conical). P191119 (bin 5) and P191133-016-431 (bin 8) pulled from stock (no wide-pleat bin). 4 carbon fixed cls-1395 → cls-1667 (flange split).src/tools/reclass_widepleat_catalog.py; logimports/widepleat-rekey.csv. - Primary swap NF→80/20 (APPLIED): 9 bare-80/20 variants had their
_DAMN2-NF(one_DAMN3-SB) sibling flagged primary — the 2026-08-28 swap missed them. Promoted the 80/20 (product fields moved, fill-blanks-only so the 3 cls-0458 merge-ins' existing dims survive), demoted the NF/SB to a non-primary upgrade (NOT deleted — in-stock bin 4). 50 bdf.* cells logged toimports/class-overrides.csv(CI baseline stays green).src/tools/swap_primary_nf_to_8020.py; logimports/primary-swap-nf-8020.csv. - Result: 10 in-stock SKUs pulled (8 primaries from bin 3 + 2 wide-pleat from bins 5/8) → in-stock 2,332 (report 2026-09-02); 1,515 classes (report 2026-09-02). Verified: invariants 43 checks 0 FAIL (2 warn), 82 tests OK,
--baseline HEADbdf-immutable OK.
2026-09-01 (regrade/reprice kickoff — grade model doc + house-grade misgrade correction)
Viktor introduced the regrade/reprice pass: three parameters — house grade (a–f), natural grade (A–F), and anchor (*) — whose combination drives bid priority and price. Captured canonically in docs/grade-pricing.md (combo table
- premiums + two-track base-price framework); cross-linked from AGENTS.md (read-first + a new locked rule), house-grades.md, natural-grades.md, anchor-skus.md, and open-questions.md (deferred pricing item UNBLOCKED).
- Base-price research delegated (2 tracks, parallel sub-agents, read-only): Track 1 derives a clean base price from the old/current sell price; Track 2 derives implied cost (PO rate + sibling/class matches) → base price ≈ cost/0.35 (~65% margin). TEMP-column layout + evaluation/impact review follow.
- House-grade misgrade correction (APPLIED). The "should not exist" combos are unambiguous natural-vs-house contradictions, corrected on the primary row (variant grade 93 authoritative; house grade 63 mirrors it):
dA→b(3 rows, all HEPA; 0 in-stock),dB→b(2 HEPA),fB→b(5 cartridge),fC→c(166 = 161 cartridge + 1 high-efficiency + 3 baghouse + 1 HEPA). 176 rows total, 10 non-cartridge.sku_prev_grade(116) untouched (already holds the pre-correction grade); each row gets a grade-note + a durable logimports/house-grade-regrade.csv. Scriptsrc/tools/regrade_house_grades.py(dry-run default,--apply).- Side-fix: 4 spun-bond primary rows (e.g.
1212249_DAMN3-SB) carried variant gradexon a primary row (spun-bond reclass artifact); nowc. - "Likely" combos (
dC,dF,fD,fF,cA/cB/cF) left for human review, not auto-corrected. - Verified: invariants 43 checks 0 FAIL (2 warn), 82 tests OK.
2026-09-01 (09-01 Matrixify export review + merge applied on branch merge-0901)
Field-by-field diff of imports/Matrixify_Full_09-01-2026.csv vs pristine Matrixify_Full.csv (csv.reader). Read-only; the merge runs on a separate feature branch after this.
- Metafields — all present and typed (
sku.in_stockexcepted). 27/27 variant metafields (sku.×13,vendor.×9,dfi.×5) present, types matchdocs/metafield-definitions.md; all value cells empty (definitions live, data not imported — our project holds the values).sku.in_stockstill[boolean](the PENDING flip).google.gmc_idretyped[string]→[single_line_text_field].V_Rankstill bare in the store (global.rename is working-file only). - 53 new products (+100 new variant rows on existing products = 153 new rows), 10 variants removed. 50 are "Custom Build — …" pages + equipment/filters (non-cartridge); 3 are "Custom Build — Cartridge Filter #N" placeholders (no dims) — no new real cartridge needs classing. 10 removed = equipment "Default Title" variants replaced by option variants.
- Jason's edits — prices, SKUs, copy; ZERO dims. 100 price changes (16 cartridge), 6 SKU renames (MERV 13→14 + size corrections, resolving 3 duplicate-SKU pairs), 1 shape fix (10020003 Round→Oval, already in our data), 24 body rewrites, 5 Type changes (4 Falcon IX replacement filters re-typed off "Cartridge Filters"), 8 archives. No
bdf.*field changed (0 diffs on OD/ID/ Length/Height/Bottom/Bolt_Hole) and 0 of 153 new variant rows carry bdf dims. - Project dims divergence (ours vs store) = corrections, not drift. 19 transpositions + ~120 backfill/corrections (store ID frequently = copy of OD)
- ~317 blank-store backfills, all logged in
class-overrides.csv. Store dims are stale incomplete Carl data; project dims win.
- ~317 blank-store backfills, all logged in
Merge decisions locked (Viktor 2026-09-01)
- Adopt Jason's 100 price changes + 6 SKU renames.
- Project dims win everywhere; only new products + previously-blank store cells take store dims.
- Adopt Falcon IX Type changes (Cartridge→High Efficiency/HEPA); clear the 3
dcf-defaulton those rows. - Remove duplicate SKU
P281426-016-002: delete the lower-traffic listing (prod 7147161976893, pv 18) over the kept (4856959270973, pv 55). - Re-apply the 38 D1
Variant Command=DELETE markers (store not yet deleted). - Merge = rebuild
edit_db.csvfrom the 09-01 export, overlay project-owned columns (bdf.*, grades, 104–136, Variant Command) by Variant ID.
Applied 2026-09-01 (branch merge-0901, via src/tools/merge_matrixify_export.py)
edit_db.csv merged: 15,589 rows (53 new products + 100 new variant rows on existing products = 153 new rows added, 10 store-deleted variants dropped), project columns overlaid by Variant ID (not SKU — the 6 renames kept their Variant ID), 3 Falcon IX dcf-default cleared, 38 D1 Variant Command=DELETE markers re-applied, duplicate P281426-016-002 (prod 7147161976893) marked product Command=DELETE. Matrixify_Full.csv refreshed to the 09-01 export (129 cols). Verified: invariants 0 FAIL, 82 tests OK, classification unchanged (1,573 classes / 2,342 in-stock / 11,908 classified), project columns byte-identical for all 15,436 shared variants except the 3 intentional dcf-default clears.
Claude review fixes (2026-09-01, same branch): (1) new rows now carry Variant Command=MERGE (was blank — the 38 D1 DELETE markers are re-applied after the store-copy); (2) the Google Shopping pass-through columns (ad-group 64/69, class 65/68, anchor 92, cleared 70) are preserved from edit_db, not store-copied — the earlier pass had wiped ~12k cls-* cells from 65/68 and the 7 anchors from 92; (3) SKU copy is normalized (float-equal / boolean-equal are treated as identical), which reverts Jason's 16 number-formatting corruptions (102903→102903.00) and preserves the project's P199612 rename; only the 6 MERV/size renames are adopted; (4) gmc_id and V_Rank values are adopted via explicit header-name aliases (they were silently frozen by name drift).
Claude re-review fix (2026-09-01): Variant Cost (col 49) added to the project-owned set — it holds the PO-cost backfill with blank = unmatched (build_po_backfill.py), and the first pass was refilling 1,172 unmatched blanks from the store. Preserved (4,980 nonblank, −2 from dropped variants). Boolean columns (Top Row, Gift Card, Published, Variant Taxable, Variant Requires Shipping) normalized to uppercase TRUE/FALSE on write (694 cells) — the store emits lowercase true.
2026-09-01 (class-structure review — Viktor answers Claude's A1–A8)
Claude's classification review raised 8 structural questions. Viktor's answers are locked policy below; the data mutations they authorize are supervised corrections (logged to imports/class-overrides.csv + a curated human-review list), not applied blind.
- A1 — merge near-identical class groups. 44 classes / 117 SKUs / 32 A–C in 20 groups identical on 7 categorical axes + within 1/16″ → merge. EXCEPTION to review first:
cls-0276+cls-1427(18+5 SKUs, 80/20 media, "bands" build) — bands likely signals spunbond primary; known/regularly-sold spunbond SKUs stay "SB-only" (no 80/20/NF upgrade variants), same discipline as exotics. - A2 — dimension tolerance becomes a flexibility function, not fixed 1/16″. Relative tolerance (longer dims → more leeway) + absolute floor, multi-parameter, per-axis weighting (OD/ID fit-critical, length not). Delegated to a sub-agent (2026-09-01); its result gates the A1 merge sweep.
- A3 — build-config from body text is class-defining, with caution. wrap / d-ring = real accessory → distinct class; stainless = stainless endcap → distinct. But "bands" is NOT a clear signal (likely spunbond-primary, but body copy is historical cut-and-paste → erroneous band language likely). Evaluate the class impact of "bands", cross-ref DFI to strengthen spunbond-primary evidence. Curate the full human-review list (esp. A–C-impacting classes).
- A4 — blank bottom endcap → treat as
closed, no bolt hole. 21 blank-endcap primaries (8 A–C) + 50 classes / 87 SKUs (16 A–C): change to closed, confirm no bolt hole, class accordingly, flag for human review with reason. - A5 — blank shape → Round. 45 primaries blank
bdf.Shape: set Round unless body/DFI/other provenance shows oval/conical/non-cartridge square. Flag for review. - A6 — one-member classes (304) mostly fine; scour for merge candidates. 304 classes / 120 A–C SKUs. List printed on screen (no CSV) for scouring.
- A7 — NF/SB variants with no class. 44 non-DELETE NF/SB variant rows (13 A–C) hang off 80/20-primary products with blank class. Policy: 80/20 vs NF stays lenient (deferred to Donaldson media-spec comparison); SB is strict. → SB variants get real SB classes (exotic discipline); NF variants park with the 80/20-vs-NF deferral.
- A8 — DFI wins on dimension conflicts. 109 dims disagreements (30 A–C): adopt DFI dims, log to
class-overrides.csv, flag for human review. Review note follows the existingclass-note(col 105) pattern; DFI join detail lands in thedfi_*cols when the bridge is written in. - Terminology (Viktor 2026-09-01): "band(s)" is reserved for the material dust-collector band construction; "slack" is reserved for the Slack app. The dimension-merge allowance is called the tolerance window (per-axis allowance
- shared tolerance budget) — never "band" or "slack".
- Stated-media signal (Claude, verified 2026-09-01): ~79 cartridge primaries state "Spun Bond" in structured body copy ("media … is Spun Bond" / "Media: SpunBond") while derived 80/20 from the SKU suffix; 44 are A–C (e.g. FCS183600HF grade A; 146418B / 262-6432 / 7FRO2029 grade B). 66 already carry an SB variant — same filter under two media labels. Per the SB-only rule these lose the variant ladder and the primary reclasses SB: new review bucket + DFI cross-ref, not folded into A4/A5.
- A4/A5 scope correction (verified 2026-09-01): real populations are 143 blank-bottom-endcap and 405 blank-shape cartridge primaries (not 21 / 45) — the sheet counted only already-classed rows. ~360 primaries parked in
dcf-defaultreclassify. 4 blank-bottom primaries carry bolt-hole evidence (P148646_DAMN2-NF, P191528-016-436_DAMN2-NF, 8PP-40764-00_DAMN2-NF, 8PP-21347-00_DAMN2-NF) → closed+bolt, not closed.
Applied 2026-09-01 (passes A5 + A4)
- A5 (blank shape → Round): 403 primaries backfilled to
bdf.Shape=Round (2 contrary kept blank: P280475 oval, P527081-016-340 conical — review list).src/tools/backfill_blank_shape_round.py. Invariants 0-FAIL, 82 tests OK. - A4 (blank endcap → closed): 143 primaries backfilled (133 closed-no-bolt, 1 closed+bolt bolt-evidence, 9 bin-defined = in-stock rows keep their bin's endcap). Re-keyed non-stock rows to their corrected signature (exact nominal, dims numeric-normalized); 48 new classes minted; incomplete-dims rows parked (
dcf-defaultprimary / blank variant).src/tools/backfill_blank_endcap_closed.py. Three bugs caught and fixed during the pass: (1) in-stock rows must keep their bin class (not re-key/park), (2) the signature map must exclude affected rows or an affected row self-matches its own pre-change class, (3) parked non-primary rows go blank, notdcf-default(class.dcf_primary_only). Invariants 0-FAIL, 82 tests OK.
Applied 2026-09-01 (A2 tolerance window + merge sweep)
- A2 rule change: the fixed 1/16″ tolerance is replaced by the tolerance window —
tolerance(d) = clamp(1/16″, 0.005·d, 1/4″)with a shared budget (per-axis slack ≤ 1.35, sum ≤ 2.0).src/invariants.py(dim_tolerance/MAX_AXIS_SLACK/SLACK_BUDGET) anddocs/domain-model.md§6.1 updated;class.spread_within_tolerancenow enforces the window. - A1/A2/A6 merge sweep:
src/tools/merge_tolerance_window.pygroups classes by exact categorical signature (shape/media/endcap/flange/build) and clusters by modal dims under complete linkage + the tolerance window. 73 classes collapsed → 20 groups (258 rows re-keyed), excluding cls-0276/cls-1427 (mixed-media, routed to the spun-bond pass). A4-minted near-dups (cls-16xx) re-merged into existing classes. Invariants 0-FAIL, 82 tests OK.
Applied 2026-09-01 (spun-bond reclass + review list)
- Spun-bond reclass (A1 SB-only rule): 79 primaries state "media … is Spun Bond" in body copy but derive 80/20 (44 are A–C). 66 carried a
_DAMN3-SBvariant → SB row promoted to primary (product-level fields moved), 80/20 + NF ladder DELETEd (132 rows). 13 with no SB variant left for review.src/tools/reclass_spunbond.py; logimports/spunbond-reclass.csv. DFI cross-ref: 30/38 bridged carry a spunbond grade (60/65/68). - Review list:
src/tools/build_review_list.py→generated/review-list-2026-09-01.csv(gitignored): spun-bond 79, blank-endcap 140, blank-shape 2, dfi-dims 109 (+DFI candidate dims), singleton-cand, cls-1427-split. A8's 109 land here for adjudication (per "do 1" — no auto-apply). Invariants 0-FAIL, 82 tests OK.
Applied 2026-09-01 (spun-bond reclass CORRECTION — Claude review)
The spun-bond pass initially promoted all 66 and DELETEd 132 rows, but that removed 26 in-stock SKUs (13 products lost in-stock entirely) and 19 higher-selling listings — the body-copy signal is weaker than Option1 + order history. Reverted the DELETE half and re-ran with two guards:
- never delete an in-stock variant (its bin defines the listing);
- never delete a variant whose natural grade is A/B, or that outranks the SB row it would replace.
Result: 38 promoted + 76 DELETEd; 28 conflict (13 in-stock + 15 grade) and 13 no-SB-variant → review list. In-stock preserved at 2,342; DELETE rows 38 → 114. src/tools/reclass_spunbond.py.
2026-08-31 (background review fixes — dfi_review / class_self_test / import / metafield renderer)
dfi_review.pyparse_title_pntruncated hyphenated part numbers. Pattern B^([^-]+?)\s*-\s*stopped at a hyphen inside the PN (22-5075 - …→22). Fixed to^(.+?)\s+-\s+(spaced-hyphen anchor). Post-fix run (2026-08-31): 4,048 cartridge primaries parse a title PN; 1,783 bridge to DFI, 1,420 dims-validated, 109 dims-disagree.dfi_review.pynondeterministic bolt-conflict count.efisis aset; the dims-agreeingbreakpicked set-iteration order. Nowsorted(efis)— stable at 2 bolt-hole conflicts across runs.dfi_review.pyblank-our-dims bridges counted asok. 18 rows whose bdf OD/ID/Length are blank were bridged by PN only and reportedclean. Now a distinctunvalidatedverdict, excluded fromclean.dfi_review.pyconflated "DFI cost row lacks dims" with "no cost row at all". Split intodfi-no-dims(1 row) vsdfi-no-cost-row(235 rows). Removed deadour_closed/closed_capassignments.class_self_test.pyforced ZERO in-stock classes.BIN_MAPis keyed by bin number; the class isvalue[0].sorted(set(BIN_MAP.keys()))produced{'1'..'9'}which never intersectsby_class. Fixed to{v[0] for v in BIN_MAP.values()}— 9 in-stock classes now forced and their CSVin_stockflag is True.class_self_test.pyper-axis denominator was full sample. Now reportsdisagree / adjudicated(excludes unadjudicable rows), e.g.0/25not0/30.class_self_test.pyflange/build axes were circular.db.flanged_products()anddb.build_config()derive from the same Title+Body HTML the test reads, so a "DISAGREE" was detector drift, not a data error. Markedderived(informational) and dropped from the disagreement rate.build_metafield_mutations.pyemittedownerType: "PRODUCTVARIANT"quoted.MetafieldOwnerTypeis a GraphQL enum (unquoted); only namespace/key/name/type areString!. Fixed — the aliased mutation document is now valid.import_matrixify.pysilently zeroedv_rank. The 2026-08-31 rename toglobal.V_Rankwas applied toPRODUCT_COLS+schema.sql+edit_db.csvheader, but the pristineMatrixify_Full.csv(the actual import input) still carries the bareMetafield: V_Rank [number_decimal]header. Import now accepts both spellings (bare +global.).
2026-08-31 (background review round 2 — transpose A/B, #14, #16, #12)
fix_body_copy_transpose.pyfind_after(A): now masks tag spans (equal-length spaces, offsets stay aligned) instead of anchoring on>, so the bare<li>Outside Diameter: 12.75</li>family (value shares a text node with the label) resolves too, and numeric tag attributes can't be read as the value.NUMvalidation (B):NUMstays loose ([\d.]+) so malformed source (8..38) is DETECTED, thenre.fullmatch(r'\d+(?:\.\d+)?')refuses it instead of half-matching a partial value into the copy.- #14
SHAPE_FIXre-key guard: before overwriting, the row's current class must equal the recordedold_cls; a since-re-keyed class is skipped + reported (SKIPPED … class is X, expected Y), not clobbered + mis-logged. - #16 deterministic DFI cost lookup:
dfi_review.pyresolves a base PN's.NNrevisions deterministically (exact-PN match wins, else lowest-sorted revision) instead of whichever row the reader happened to hit last. - #12
sku.in_stock: Viktor will change the definition typeboolean→single_line_text_fieldmanually in admin (delete + recreate; empty def). Until then the manifest + live store stayboolean. No export-time lowercase once text.
2026-08-31 (fix_body_copy_transpose hardening — Claude review)
- 5 review findings fixed in
src/tools/fix_body_copy_transpose.py: (1) idempotency guard — swap only when labeled OD < ID, so re-running--applycan't transpose correct data back; (2)shutil.copyto*.bakbefore writing (the import was dead); (3) class-overrides log written fromb_changes(what actually changed), not the SHAPE_FIX target list; (4) match on SKU AND product id — the pid was decorative; (5)find_afteranchored on the closing>so a numeric tag attribute (class="col-2",width="50") can't be read as the diameter. - Plus a 6th (beyond review): CHANGE B idempotency guard, so re-running
--applydoesn't re-log duplicate class-overrides entries (apply_overrides.pyre-applies that log). Verified: dry-run reports 0/16 + 0/2 on already-correct data; 82 tests OK; invariants 0 FAIL.
2026-08-31 (metafield definitions created on Shopify)
- 28 metafield definitions created live on
damnfilters-com(production) via browser automation (collaborator login, no API token — n8n Shopify OAuth2 credential is order-scoped and returns ACCESS_DENIED onmetafieldDefinitionCreate). 27 variant-level (sku.×13,vendor.×9,dfi.×5) + 1 product-level (google.gmc_id). Types verified on the store:sku.in_stockboolean,sku.prev_price/sku.prev_cost/vendor.costnumber_decimal,vendor.origin_price_datedate,vendor.descriptionmulti-line, rest single-line. vendor.costREINSTATED (reverses 2026-08-13 "vendor_cost redundant, remove" decision 14). PO-asserted unit cost (rate), PO-match rows only (vendor_status==passed-review), distinct from the hand-editable native Shopifycostfield. Manifest +docs/metafield-definitions.mdupdated (vendor namespace 8 → 9). Data-side follow-up still open: thevendor_costVALUE column was removed fromedit_db.csv2026-08-14 — re-add + populate from POratebefore the Matrixify import.global.title_tag/global.description_tagDEFERRED (Viktor). Manifest previously assumed namespaceglobal., but the export header is bare (Metafield: title_tag [string]), the field map says "n/a", andfinal-export-fields.md's kept list omits them. Left alone; removed from the manifest (now 28 defs).google.gmc_idretro-typed 648 existing unstructured values on create (single_line_text_field). FLAGGED out-of-scope after the fact: the field map (imports/field_definitions_and_context.csv) marks it "Unstructured / Used by Google/GMC? / Out of Scope" — it was NOT meant to be part of this project. Created by mistake (trustedfinal-export-fields.md"kept" over the field map). Values intact; definition left in place — Viktor: leave for now, deal with later (possible revert = delete the definition, returning the 648 values to unstructured).- Incident (recovered): an automation bug (stale namespace field across a failed save) created "SKU In Stock" keyed as
sku.prev_pricewith the wrong type. Deleted (0 values) and recreated both correctly; verified all 27 variant types against the live list. No existing data touched — all creates additive, one empty mis-created definition deleted with Viktor's OK.
2026-08-31 (final-phase kickoff)
- Phase 3 follow-through — body-copy + shape corrections (Viktor). Two supervised edits to
edit_db.csvviasrc/tools/fix_body_copy_transpose.py: (1) 16 primaries had transposed OD/ID in the customer-facing Body HTML ("Outside Diameter: 11.40 / Inside Diameter: 14.40" while bdf correctly says 14.4 > 11.4) — swapped the labeled spans; the "Dimensions:" summary line was left alone (catalog convention there is consistently "smaller × larger × L"). Scope note: the self-test only sampled 4 of these; a full body-copy scan found 16 (8 oval + 5 conical + 3 round). (2) 2 primaries mis-shaped as Oval — P032931_DAMN2-NF + P033615_DAMN3-SB are flanged round filters (Top_Config "14.25 × 16.00 flange"): bdf.Shape Oval→Round, body "Shape:"→Round, re-keyed cls-NR079→cls-1620 (NF) and cls-NR078→cls-1621 (SB) — flange kept, both singletons, no existing round class at 14.4×11.4×26. Logged to class-overrides.csv (bdf.Shape + new-class). Self-test re-run: 0/30 on all 8 axes; invariants 0 FAIL / 2 warn; 82 tests OK; distinct classes unchanged 1,573. - Class-accuracy self-test (Phase 3). New read-only
src/class_self_test.py: seeded stratified sample (n=30 incl. all 9 in-stock classes) adjudicated against primary-row Body HTML (not bdf.*). Result (2026-08-31): 0 errors on the axes that matter most for routing — bottom endcap, bolt hole, shape, length, and build all 0/30 (Wilson 95% upper bound 11.4%). Four OD/ID "disagreements" are NOT class errors: the BODY COPY still shows transposed oval/conical dims (e.g. cls-NR042 copy "Outside Diameter: 11.40 / Inside Diameter: 14.40", bdf corrected to 14.4/11.4) — a customer-facing copy fix, not a class fix. Two genuine flags: cls-NR078 (P033615_DAMN3-SB) + cls-NR079 (P032931_DAMN2-NF), the two stainless oval products whosebdf.Top_Configcarries a dimensioned14.25" x 16.00" flangedespite being ovals — contradicts domain-model §5.2 "ovals never have flanges"; needs human adjudication (shape wrong vs flange noise). Note: this is a cleaner bolt-axis result than the 08-20 sample's ~13-17%, but 0/30 only bounds it below ~11%, it doesn't prove the axis clean. - DFI full re-review (Phase 2). New read-only
src/dfi_review.pybuilds the full dims-validated bridge (title OEM PN →efi_cross_reference→efi_cost_dataset) across all cartridge primaries and flags dims-disagree + bolt-hole conflicts. Result (2026-08-31): 4,568 cartridge primaries → 4,330 title PN parsed → 1,053 bridged; 861 dims-validated, 89 dims-disagree (suspect false bridge or our-dims-wrong), 103 DFI-no-dims (not evidence), 3 bolt-hole conflicts (all grade-D: 8PP-45368-00, 1292267, 8PP-48507-00 — DFI says bolt hole 0.53, we say Open). Conflict distribution is tail-heavy: dims A2/B1/C20/D65/F1; bolt D3. Consistent with Phase 1 (problems concentrate in D-F). Two correctness fixes en route: (1) bolt-hole check is ONE-directional (DFIbolt_hole_diablank = "not recorded", not "no hole"); (2) split dims-disagree from DFI-no-dims so the false-bridge signal isn't diluted. Findings are flagged-not-applied (DFI is validation-only, domain-model §7); media carried through as a reference column only (parked per Viktor). - Metafield definitions — scripted creation path. Viktor switched the metafield-definition creation from a Rachel/Zach manual handoff to a scripted GraphQL create. Delivered
docs/metafield-definitions.md(29 definitions: 26 variant-levelsku./vendor./dfi.+ 3 legacy upgradesglobal.title_tag/global.description_tag/google.gmc_id), the manifestsrc/tools/metafield_definitions.json, the renderersrc/tools/build_metafield_mutations.py(one aliasedmetafieldDefinitionCreaterequest), and an n8n workflowumHUfO9EJd1tyOhW. Blocked on a custom-app Admin API token withread_products+write_products— the existing n8n "Shopify" OAuth2 credential returnsACCESS_DENIED(order-scoped). Tracked as open-questions Blocking #3. - V_Rank namespace fixed (Viktor). Header
Metafield: V_Rank [number_decimal]→Metafield: global.V_Rank [number_decimal]acrossedit_db.csvrow-2,import_matrixify.py, andschema.sql(the field map's rename target wasglobal.V_Rank). Cross_Ref / Compatible_Sys KEPT as single_line_text_field holding JSON-array strings (Viktor: "keep as JSON-array for now"). Invariants 0 FAIL / 2 warn. - Natural-grade triage (Phase 1). New read-only report
src/natural_grade_triage.pycross-tabs every "unsatisfactory state" bucket by natural grade (col 136). Headline: data-quality problems concentrate in the D-F tail, not the money. Only 82 A-C rows across all 8 buckets (and most of those are C); zero in-stock SKUs are flagged in any bucket. Bucket A-C%: duplicate-sku 57% (8 of 14 rows), no-info 33% (11 of 33), unresolved-endcap 27%, missing-dims 22%, od-equals-id 13%, dcf-default 9%, non-primary-blank 3%. Scoping correction en route: non-primary-blank is cartridge-only (blank class is correct-by-design for non-cartridge), excludes D1 DELETE + blank-SKU → 590 live rows (not 1,896).
2026-08-31
- Natural grade added (Viktor). New permanent variant-level metafield
sku_natural_grade(col 136, finalsku.natural_grade): a per-SKU grade (uppercase A/B/C/D/F) reflecting the SKU's own pageviews + orders, NOT shared with class siblings (house grade inherits; natural grade does not). Populated fromimports/natural_grade.csv(15,446 rows, joined by Variant ID — the only clean key: Variant SKU has 8 dupes + 19 blanks, and the two files are not row-aligned). Distribution: A 66 / B 122 / C 1,983 / D 13,013 / F 262. Scriptsrc/tools/populate_natural_grade.py. Invariants 0 FAIL / 2 warn. - PO near-miss seeds fixed (Viktor). 12 SKUs have a direct PO in
po-cost.csvbut never got their vendor + cost, because the original backfill matched SKUs exactly and their QuickBooks string differs trivially: 10 with a literalSKUprefix, 1 with a case diff (P031664-016-190vsp031664-016-190), 1 with vendor notes baked into the catalog SKU (P15-6334+ "GAS TURBINE..." tail). Attached their natural-match vendor data (most-recent PO, same as the backfill): 9 cartridge + 3 non-cartridge (RCA3-5V5002 / RCA3-8V8002 Valves, P031664-016-190 High Efficiency Filter). Scriptsrc/tools/fix_po_near_misses.py. Note: P031664-016-190 has 5 PO lines across 3 vendors (Glasfloss $222.72 most recent 1/6/2026; also DHA Filter $315, Air Filters $230) — assigned most-recent per the backfill rule, older POs invendor_history_po. Invariants 0 FAIL / 2 warn. - PO sibling backfill reverted (Viktor). The PO-cost backfill had written vendor name + cost onto every class sibling of a natural PO match, not just the natural seed. Reverted: only the original seed (the SKU with a direct PO in
po-cost.csv) keeps its vendor_* metafields + Variant Cost; siblings are restored to their pre-backfill state (Variant Cost ←sku_prev_cost,vendor_status→under-review, 7 vendor_* columns blanked). 10,232 sibling rows reverted — 8,116 cartridge + 2,116 non-cartridge that had wrongly inherited via a blank-class key (incl. 17 blank-SKU and 31 already-D1-deleted rows). 954 natural seeds across 254 classes untouched. Scriptsrc/tools/revert_po_sibling_backfill.py. Invariants 0 FAIL / 2 warn.
2026-08-30
- D1 exotic-variant removal applied. Removed all standard-media (80/20/NF/SB) variant rows from exotic-media products — an exotic product sells only the exotic media (domain-model §4, safety rationale). 38 variant rows across 20 exotic-primary products (17 NF / 16 SB / 3 80/20 + 2 blank-SKU), by exotic media: 90/10-glass 22 (incl. both blank-SKU), ptfe-sb 8, carbon-80/20 3, nomex-ptfe 3, teflon-sb 2. 11 of the 38 were in-stock (all NF) and are pulled from the in-stock list (2,353 → 2,342). Mechanism: native Matrixify control column
Variant Command(col 27) set toDELETE(replacing MERGE) on the removed rows, plus clearednew-class/sku_stock_id/sku_in_stock/new-gradeon them. Durable logimports/exotic-variant-removals.csv; scriptsrc/tools/remove_exotic_variants.py. Two singleton classes emptied (cls-1557/cls-1558); distinct classes 1,575 → 1,573. - Invariant fix (Claude review).
class.no_bottom_endcap_spanreadbdf.Bottom/bdf.Bolt_Holefrom the VARIANT row — blank on every non-primary member by design — so theif bottom_endcap(r)guard dropped every non-primary member and the set could never exceed size 1: structurally blind. Now resolves via the product's primary row (db.product_row(r)), matchingclassification_report.pyand the domain model. (The span it would have caught — cls-0945/cls-0946 via p191065 — is also resolved by the D1 removal, which deleted p191065's NF/SB variants.) - Invariants 0 FAIL / 2 warn (warns unchanged).
2026-08-28
- PO-match survey (cartridge-only). Measured PO cost coverage against the cartridge catalog: 8,630 of 12,882 cartridge SKUs (67%) have a vendor+price match (
vendor_preferred+Variant Costpopulated — perfectly correlated, zero vendor-only/price-only rows); 4,252 (33%) do not. At the class level, 1,176 of 1,595 classes (74%) have ZERO members with a PO match, holding 4,080 classed SKUs (zero-PO classes by media: SB 401 / NF 389 / 80/20 374 / exotic 12; 570 are single-member). Full per-class table:generated/class-po-report-2026-08-28.csv. Implication for the deferred sibling-cost→PO work (open-questions #8): a cost can only extend from an existing match, so current reach is ~26% of classes. - DFI bolt-hole review applied (first DFI-driven correction). Bridged the cartridge catalog to DFI (ex-EFI) — title OEM part number → cross-ref → cost sheet — and validated every bridge by dims agreement (1/16″). 10 of 12 candidate bolt-hole discrepancies held up; 2 discarded as false bridges (1319234 = part-number collision across manufacturers; 10000940 = conical-vs-round shape dispute, parked). Applied and logged 50 rows to class-overrides.csv:
- bdf.Bottom / bdf.Bolt_Hole / bdf.Bolt_Hole_Dim backfill on 10 primary rows (diameters normalized to the 0.5/0.9 convention).
- 5 bdf.Inside_Diameter backfills from DFI (8.38 ×4, 5.84 ×1) so the re-key lands on real classes rather than blank-ID phantoms.
- 21 class re-keys (7 products × 3 variants): 6 products 80/20→cls-0038, NF→cls-0459, SB→cls-0460; P199469-016-002 (flanged 9.2×5.84×22.3) → new cls-1617/1618/1619. 3 dcf-default rows got the bolt-hole backfill but stay unclassed. Distinct classes 1,595→1,581 (report 2026-08-28).
- Fixed 2 stale class-overrides.csv entries (drift from the 08-27 split/merge passes that edited edit_db.csv directly): 10004600→cls-1427, P134158→cls-0001.
- Invariants 0 FAIL / 2 warn.
- DFI media + primary-flag corrections (round 2). From DFI media-grade cross-checking (title → cross-ref → cost sheet, dims-validated):
- Cellulose retired → 80/20 (Viktor). DFI grade #10 (pure cellulose) is retired and treated as 80/20. Removed
100% Cellulosefrom the exotic taxonomy (src/media.pyOPTION_EXOTIC + BODY_EXOTIC; domain-model §4), and re-keyed the 6 "100% Cellulose" primaries to their 80/20 classes (P150464→cls-0453, 29901706→cls-0020, P136835→cls-0025, P191937→cls-1270, P191037→cls-0038, P535114→cls-0050). 90/10-glass left as-is pending DFI reconciliation. - Primary-flag swap (NF→80/20). 13 products had the
_DAMN2-NFvariant as primary (Top Row) where DFI says the OEM spec is 80/20 (#12/#18). Swapped the primary flag to the bare 80/20 SKU — moved product-level fields (Body HTML, bdf.*, product metafields, product custom labels) and swapped Top Row (61), Variant Position (34), and variant grade (93); NF/SB variants left in place. P25.70E_DAMN3-SB (SB-primary, house gradef) HELD pending Rachel's SB decision (gradefvs in-stockaconflict). - Invariants 0 FAIL / 2 warn.
- Cellulose retired → 80/20 (Viktor). DFI grade #10 (pure cellulose) is retired and treated as 80/20. Removed
- Popularity data integrated (Zach's GA4 + Shopify pull). Added 4
TEMPcolumns toedit_db.csv(132–135):pageviews_ga4(handle-level, 2023-05-24→today),orders_sku(variant-level, 2012→today),pageviews_clean(Shopify organic+paid),pageviews_shopify(Shopify entrances). Joined positionally (row i → row i) — the source mirrors our 8 duplicate variant SKUs, so a SKU-map join would have collapsed them. All 61,784 values verified clean non-negative integers; pageviews confirmed handle-level. Source kept outside the repo (Zach's pull,Master_Products_DB_v3.0_with_popularity.csv).
2026-08-27
- Final-export kickoff. Viktor answered the full 21-item outstanding list; reconciled into
docs/open-questions.md(live backlog) +docs/domain-model.md§7. Key dispositions:- DFI becomes a validation reference — Viktor imports DFI data; Hermes reviews questionable bolt-hole/class assignments against it. Join-key / cross-reference policy stays deferred.
- Pricing held to a final pass until classes are trusted; old price kept in the
prev_pricemetafield. - Old metafields KEPT —
global.Org_Views/Org_Value/Page_Views/Page_Value/prev_price+google.gmc_idretained as historical reference; the "deleted Oct/2025" note infield_definitions_and_context.csvis superseded. - Product-page content is DB-driven — Body HTML + metafields in
edit_db.csvare the live site once imported; oval-transposition, P032931 price, and P199612 rename are all DB fixes (no separate Shopify-side action). - Deferred: conical second-face, DF numbering, DFI join policy, cross-ref role, endcap taxonomy, sibling-cost→PO (gated on class trust).
- Verified 0 partition violations (2026-08-27) — no multi-class product exists.
- Authorized: re-file flange/gasket-in-bottom → top endcap + drop the round tag on the last oval (log to
class-overrides.csv). - New deliverables: Jason handoff spreadsheet (missing specs + dupes), review spreadsheet (OD==ID + missing-data classes), metafield list + definitions + Matrixify type conformance (→ Zach + Rachel).
- Web tooling fixed + spec-gap research. Switched search/extract from broken keyless Exa + dead SearXNG to Tavily (search) + Firecrawl (extract) via Bitwarden keys. Ran a 520-SKU dim + bottom-endcap lookup, then handed the remainder to Claude (bulk Fab-Tex catalog harvest + Filter Professor cross-ref). ~450 SKUs got OD/ID/Length/bottom.
- Dim write-back applied. 1,032 bdf corrections (OD 75 / ID 267 / Length 94 / Bottom 273 / Bolt_Hole 323) to
edit_db.csv, gated by sanity (ID >= 2", wall <= 6", OD 3–30", L 3–60") + class-consistency. First pass reverted after catching systematic harvest errors (implausible IDs 0.276"/0.33"/1.181", OD/ID swap). Logged toclass-overrides.csv(authority Viktor). - Split 3 overspread classes. cls-0267/0328/1450 were keyed on OD==ID and lumped different filters; the write-back exposed the spread. Re-grouped by build (Shape_Config, typo-normalized) + bottom + dims; modal cluster kept the id; 7 rows moved to cls-1611..1617. Invariants back to 0 FAIL. New warn
class.no_signature_collision(25 dup signatures, incl. cls-1617 == cls-1427) → merge pass pending. - Merged duplicate classes. 26 class merges (25 exact-signature collision groups) — keep lowest id, re-key 1,247 rows. Includes collapsing cls-1616 (split's "Helix" class) back into cls-0328 (helix == expanded metal, same cage build). Updated
bin_map.pybins 3/5/8 (cls-0458→0038, cls-0470→0267, cls-0477→0005) so stock routing stays consistent. Invariants 0 FAIL / 2 warn (the 2 are pre-existing shape-inference + body-copy flags).
- Final-export field plan (Viktor directives). Dictation
vt-final-note-2.txt→docs/final-export-fields.md. Key dispositions:- Label conventions: labels ALL CAPS (guidance only, stripped from upload); new permanent metafields get the shortest identifying label; temp fields get
TEMP:prefix.edit_db.csv= local DB, morphs into the Matrixify import. - Google Shopping four pass-through fields (grade/class/ad-group/anchor). Grade =
custom_label_0(63/93), ad-group =custom_label_1(64/69), class =custom_label_2(65/68), anchor → variantcustom_label_3(92) only, no product twin. - Custom-label audit: labels 3/4 hold no grades/classes. Product
_3(70) = product sub-type (Dust Collector/Downdraft-Table/Mist Collector/Ambient Air Cleaner) + strayh2412a;custom_product(71) = TRUE/FALSE; variant_3/_4empty. Dan's early classes live in_2(65/68); cls-* in col 104 (working, not yet any label). - Grade duplication rule: col 63 must equal col 93 exactly (primary row a–f; non-primary
xvariant-only). Audit found 113 primary mismatches — reconciled: variant 93 authoritative, copy 93→63 on primary rows. - Temp vs permanent: temp = col 67; temp-with-new-home = 66 (anchor), 104 (new-class → 65/68), 107 (new-grade), 108 (new-ad-group). Keep + flag for later: 59/60/97/98/99/94/95. Keep bdf (72–87), OTHER (88/89/90, 101).
sku_*/vendor_*/dfi_*(105–131) all KEEP, rename_→.(e.g.sku_prev_grade→sku.prev_grade, a permanent record of the old grade). - Legacy metafields: restore the five deleted Oct/2025 (
global.Org_Views,Org_Value,Page_Views,Page_Value,prev_price) — source pending (absent from bothedit_db.csvandMatrixify_Full.csv). - Misc answers: ~10 DF house SKUs route to Jason (no-info items already passed to him); web tooling already switched to Tavily+Firecrawl.
- Field plan APPLIED (
src/tools/apply_final_export_fields.py): relabeled row-1 guidance labels (33: ALL CAPS / TEMP: / dot-namespace); moved ANCHOR 66 → variantcustom_label_3(92) and cleared 66 (7 rows); copied new-class 104 → productcustom_label_2(65, primary-only) + variant (68, all rows) for 12,292 rows (incl. 348dcf-default); reconciled grade 93→63 on 113 primary rows; cleared productcustom_label_3(70, 26 cells) +h2412astrays (64/65/71/117). Cols 104/107 NOT cleared — invariant/report scripts still read them; removal deferred to Phase E. Invariants 0 FAIL / 2 warn. - In-stock grade promoted (
src/tools/promote_in_stock_grade.py): 93 =afor all 2,353 in-stock SKUs (2,145 changed, incl. 1,147 non-primaryx→a), then 63←93 reconciled (998 primary). Old grade preserved insku_prev_grade(116) — verified 116 == 93 on every row before the pass. Invariants 0 FAIL / 2 warn.
- Label conventions: labels ALL CAPS (guidance only, stripped from upload); new permanent metafields get the shortest identifying label; temp fields get
2026-08-20
- Conical re-orientation: the 5 reversed siblings in cls-NR008 (OD=12.75/ID=17.5) swapped to OD=17.5/ID=12.75 to match P191281 (F4: longer dim = OD).
ID_GT_OD_ALLOWEDemptied — geometry.id_not_gt_od now has teeth. - Gasket-in-bottom: 26 "gasket"-only rows re-filed Bottom_Config → Top_Config (F5). Added
endcap.gasket_is_top_endcapinvariant. (5 flange+gasket compound rows remain — part of the unresolved flange-in-bottom set.) - P199612_DAMN2-NF renamed → P199612 (D9: Nomex is the product, drop the in-house
_DAMN2-NFsuffix). - P032931_DAMN2-NF stainless price $115 → $138 (Donaldson site says SS).
- Body copy "Shape: Round" → "Oval" on 10020003_DAMN2-NF (bdf.Shape was already Oval; the customer-facing copy now matches).
- Invariants added:
no_shape_span+shape_prefix_agree(F8, both directions),shape_inference_flag(D10),gasket_is_top_endcap,no_hardcoded_counts. - F1 media-aware construction: 19 phantom SB "bands" classes merged back into parents (50 rows; 1,632→1,613). Human stock notes removed (127 cells); conflict/status_note/SKU_NOT_STOCKED machinery removed. NF20322 restored to in-stock (bin 1); in-stock 2,351→2,353.
- Conical merges: "Cone" vs "Conical" spelling split minted 4 phantom conical classes, merged back (1,636→1,632).
- Report-output hygiene:
pricing_report.py+in_stock_matrix.pynowos.makedirsgenerated/before writing (fresh-clone fix); repointedpartition_violations.py/classification_backlog.py/refigure_bins.pyoffimports/ontogenerated/.no_hardcoded_countsnow also scansdocs/open-questions.md.in_stock_matrix.pydims now derived from data (was a hand-maintained table that had drifted).
2026-08-19
- Build-primary fix: the unique-build re-key read build from the VARIANT row (empty Body HTML on non-primaries) — re-detected from the PRIMARY row (
Db.product_row), 49 more in-stock unique-build SKUs re-keyed (2,400→2,351). Duplicate classes merged (cls-1482←cls-1542, cls-1483←cls-1544). - Rachel/Brandon session absorbed (D1–D10, F1–F11).
docs/domain-model.mdrewritten as the canonical 8-attribute rules doc.
2026-08-18
- Unique-build re-key: 178 variants → 76 new classes (construction/prewrap/ stainless/hardware); 53 in-stock cleared (2,453→2,400); 11 wrap accessories de-typed Cartridge → Accessories. Distinct classes 1,458→1,533.
- Exotic media pass: 60 exotic variants → 39 new classes; 14 pulled from in-stock (2,467→2,453).
src/media.pycanonical_media()consolidated (Option1 Value exotic wins, else SKU suffix). Distinct classes 1,425→1,458. - Nominal split + gray-zone merge: 36 NF/SB classes over-merged → split; the 15×10×28 gray-zone family merged. Distinct classes 1,533→1,638.
2026-08-17
- Flange re-split: 677 flanged products → 90 new flange classes; 327 flanged in-stock cleared. Distinct classes 1,310→1,397→1,420→1,427.
- Oval re-key: lm990ddd2 → cls-NR042; 10020003_DAMN2-NF → cls-NR015; 10020003_DAMN3-SB → cls-NR043.
- In-stock CSV + oval-duplicate merge: cls-NR020==cls-NR015 and cls-NR057==cls-NR043 merged.
src/bin_map.pyconsolidated as the single BIN_MAP source. - Pricing report added (
class-pricing-report-2026-08-17.csv).
2026-08-16
- Media split: 18 media-spanning classes re-keyed; 802 sibling classes → 7,724 non-primary variants. Distinct classes = 1,310.
- Classes cartridge-only: 1,283 non-cartridge dcf-default retired → blank.
- Shape inference: blank bdf.Shape = Round unless "conical"/"oval" in copy.
- Bin 7 resolved = cls-0951 (closed+bolt NF).
2026-08-14
- Class redefinition (Rachel fact-check): class = OD×ID×L×shape×media×top ×bottom×build (8 attributes).
2026-08-13
- Field-expansion plan (sku_*/vendor_*/dfi_* metafields).
- Stock-list correction root-caused (order #17662, E04450 80/20).