feat(pricing): one engine for wholesale price books, vendors declare themselves - #148
Open
goetchstone wants to merge 4 commits into
Open
feat(pricing): one engine for wholesale price books, vendors declare themselves#148goetchstone wants to merge 4 commits into
goetchstone wants to merge 4 commits into
Conversation
Applies the two-skill pattern from Warp's self-improving agents to this repo's agent layer, and restructures CLAUDE.md around principles. PRINCIPLES AS A SPINE, NOT A REPLACEMENT. Three independent restructures were proposed and then attacked for lost enforcement. All three lost it, in the same way: generalising a rule into prose reads better and enforces less. Rule 33's greppable literal and its "No exceptions"; rule 47's polarity, which is OPPOSITE to 33 (cancelled lines out, returned orders in) so a polarity-neutral merger permits both errors; rule 31's stated default; rule 39's "both numerator and denominator"; rule 42's "every mutation path". In a repo where hooks cite these numbers and 203 files quote them, that is a regression wearing a cleanup's clothes. So the rules keep their exact text and their numbers, and ten principles sit above them. Each principle names a failure mode generally enough to recognise a NEW instance, and carries a "how it hides" line -- the recurring theme here is damage that passes every check. Verified mechanically: every rule number is present exactly once, and every surviving rule's text is byte-identical to before, except five that gained clarifying additions and none that lost a clause. Rule 1 (KISS) retired. Zero citations, no incident, no enforcement home, and RULE-PROVENANCE.md already said to delete it -- it was the one rule that contradicted the doctrine it sat under. Number retired, not reused. THE LOOP. The gap that produced 65 numbers with gaps and five conflicting citations was never a missing rule. It was that "does this deserve a rule?" got decided in-session by the agent that had just been burned, and every session got to append. That is accretion, not learning. - docs/RULE-FEEDBACK.md is now the sink. Costs one line, commits to nothing, so the signal keeps flowing. Seeded with three entries from this session. - .claude/skills/improve-rules/SKILL.md is the observer. Runs later, over the accumulated pile, and proposes ONE focused edit as a PR. "No change warranted" is an explicit success case. Retirement runs the same bar in reverse, because a loop that only adds repeats the original failure. - post-failure and end-of-session no longer edit CLAUDE.md. They append. ONE DELIBERATE DEVIATION FROM WARP. Their loop feeds on human PR review comments. Measured here: 20 merged PRs, 0 reviews, 1 comment -- FRAMEWORK.md describes a solo project with no second pair of eyes, so that well is dry. The substitute is the commit that had to clean up after the last one: a fix/revert commit is literally what the agent proposed versus what reality required, with a diff attached, and roughly one commit in four is one. rules-improver-check.sh counts them and nudges past a threshold -- event-driven, so the pass fires when there is something to read rather than on a calendar. It never blocks. 3,479 unit tests pass; markdownlint clean.
…themselves A manufacturer prints one book layout and puts several brands on it. Hooker Furnishings prints Hooker, Sam Moore and Bradington-Young on the same column-transposed grid, so the reusable unit is the LAYOUT and a vendor is a set of parameters against it. Adding a vendor is now a profile module plus a registry line, not another branch in the import route. DECLARATION REPLACES INFERENCE, which is the point of the change. partitionGrades() guessed a grade's material from the SHAPE of its code: a bare letter meant leather, digits meant fabric. Both Sam Moore and Hooker ladder FABRIC as B..J, so that guess files their entire fabric range as leather at the wrong tier -- and it still imports, and the numbers still look plausible, and nothing downstream holds the right answer to compare against. In the sibling repo the guess had already collected two patches living in the import route: FABRIC_LETTER_GRADE_VENDORS and COMBINED_LEATHER_VENDORS. This lands the seam before that debt arrives rather than porting it and removing it after. Every rung now declares its own kind, partitionGradesFor() uses the vendor's declaration verbatim, and the shape rules stay only as a labelled fallback for vendors that have no profile yet. Profiles are CODE, not config -- compiled, typed, reviewed. A config format expressive enough to describe a PDF layout is a programming language with a worse type checker, which is the road rule 62 exists to close. A deployment configures which vendors it carries and what markup it applies; a vendor fixes its grade ladder and label spellings, and those are identical for every dealer opening the same book. Three vendors, ~150 lines of profile between them, replacing three near-identical 200-line extractors. The engine keeps what actually repeats: the column walk, the money and dimension parsers, page splitting, and rendering through columnAwarePageRenderer (raw pdf-parse output is unparseable here -- "$500$540$470" arrives as one token). Lookup fails closed on an unknown vendor rather than falling back to a reader that is probably close. It also folds "sam-moore" and "Sam Moore" to one key: the upload form posts the id, the database holds the name, and a lookup matching only one would silently report "no profile" and drop back to guessing. The bug would have been a hyphen. Every price in the fixtures is invented; the layout is what the parser keys on. Both-directions verified: filing Sam Moore's letters as leather fails one case, and matching "LEATHER - NOVELTY" before "LEATHER - NOVELTY PREMIUM" prices the most expensive rung in the book one tier low and fails another. 3,489 unit tests pass; tsc, prettier, lint and markdownlint clean.
Two additions to the wholesale seam, both measured against the real July-2026
Sam Moore book (175 styles, matching the sibling repo's verified anchor; Nova
1034 B $804 / E $909 = COM / J $1,084 / Prem 2 $1,224).
OPTIONS ARE PER STYLE, AND THE BOOK ALREADY SAYS WHICH FRAMES TAKE WHICH.
A book prints ~20 priced option rows beside each frame and they genuinely
differ -- an ottoman's contrast welt is not a sofa's, and some frames cannot
take one at all. Seeding one flat price per vendor asserts every frame carries
every option at one price, which is wrong on most rows and offers a designer
things the vendor will not build.
Applicability needs no curating. It is in the data:
row absent, or cell "--" -> not applicable (the book prints
"NOTE: -- means Not
Available" on every page)
cell "N/C" or "Standard" -> applicable, free, standard
cell a number -> applicable, that surcharge
Measured on the real book: 162 of 175 styles carry options, Contrast Welt on
125, Inside Back Cushion on 80, Top Arm or Panel on 4. Those are the book's
numbers, not a guess.
Three parsing defects found by running it rather than reading it:
- The renderer glues a section heading onto the next row's label, so
"CONTRAST TOP ARM or PANEL" arrives as
"STANDARD TRIM & AVAILABLE OPTIONSCONTRAST TOP ARM or PANEL". An anchored
pattern missed 66 of 86 pages, and the miss read as the option being rare.
Profiles now declare their glued headings and the engine strips them.
- The book writes "Standard" where other rows write "N/C". Reading only "N/C"
dropped Welt Only from all 97 styles that carry it.
- One option is printed two ways across the book -- 58 pages say
"CONTRAST INSIDE BACK CUSHION", 28 say "CONTRAST INSIDE BACK". Matching the
longer spelling alone lost a $30 upcharge on 80 frames.
Each looked like an option being rare rather than a parser missing it, which is
why none would surface without extracting and counting.
WHOLESALE ONLY.
An extractor reads the vendor's COST. Books also print MSRP, MAP and suggested
retail; none is imported as truth, because retail is a business decision and not
ours to make for someone else's shop -- two dealers carrying the same book price
differently and both are right. A deployment configures its own markup,
discount, and whether it honours MAP; many now just set a flat markup and never
use anchor pricing at all. `msrpRow` captures a printed MSRP as REFERENCE only,
for seeding demo data and sanity-checking a markup, never as the retail.
3,494 unit tests pass; tsc and prettier clean.
…or default
Closes the chain: book row -> engine -> StyleOptionOverride. The extractor read
the option rows in the previous commit; the import still took its options from
VENDOR_SURCHARGE_MAP, a vendor-level flat default that asserts one price for
every frame.
Where the book states a per-frame price the book now wins, and the legacy map
stays as the fallback for vendors whose extractor does not yet read option rows.
Dogfooded end to end against the real July-2026 Sam Moore book, into a scratch
database, then queried as the configurator would. 40 styles produced 275
overrides, averaging 8.1 options per frame and ranging 1 to 10 -- and the two
frames below are why a flat vendor default is not good enough:
1034 "Nova" Contrast Welt $20 Bias Welt $40 Seat Cushion $30
Out Arm & Back $60 Out Arm & Back (Premium) $300
Inside Back Cushion $30 Welt Only included Carton $25
1061 "Urban" Contrast Welt $15 Bias Welt $30 Seat Cushion $20
Welt Only included Carton $20
-- no Out Arm & Back, no Inside Back Cushion at all
Same vendor, same book, four different prices and three options one frame
cannot take. A vendor-level seed gets 1061 wrong on every row, and offers a
designer two upcharges the vendor will not build.
The $300 premium-fabric row is one the sibling repo's verify-sam-moore skill has
carried as a known gap ("not seeded"); it now imports.
3,494 unit tests pass; tsc and prettier clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Wholesale price books only, as scoped. Order confirmations and apparel parsers stay out.
The insight this is built on
A manufacturer prints one layout and puts several brands on it. Hooker Furnishings prints Hooker, Sam Moore and Bradington-Young on the same column-transposed grid. So the reusable unit is the layout, not the vendor — and naming the modules
samMooreExtractor.ts/hookerExtractor.tshad produced three copies of one thing, differing by about four lines each.Adding a vendor is now a profile module plus a registry line. It does not touch the import route.
Declaration replaces inference — the actual bug
partitionGrades()guessed a grade's material from the shape of its code: a bare letter meant leather, digits meant fabric.Sam Moore and Hooker both ladder fabric as B..J. Under that guess their entire fabric range files as leather at the wrong tier — and it still imports, the numbers still look plausible, and nothing downstream holds the right answer to compare against. It survives until someone quotes a customer from it.
In the sibling repo the guess had already collected two patches, both living in the import route:
holt hasn't acquired that debt yet, so this lands the seam before it arrives rather than porting it and removing it after. Every rung declares its own
kind;partitionGradesFor()uses the vendor's declaration verbatim; the shape rules remain only as a labelled fallback for vendors without a profile.Profiles are code, not config
Compiled, typed, reviewed. A config format expressive enough to describe a PDF layout is a programming language with a worse type checker — the road rule 62 exists to close.
The split that matters:
What varies, and what didn't
Three vendors, ~150 lines of profile between them, replacing three ~200-line extractors. The engine keeps what genuinely repeats: the column walk, money and dimension parsing, page splitting, and rendering through
columnAwarePageRenderer— rawpdf-parseoutput is unparseable here,$500$540$470arrives as one token.Lookup fails closed on an unknown vendor rather than falling back to a reader that is probably close. It also folds
sam-mooreandSam Mooreto one key: the form posts the id, the database holds the name, and a lookup matching only one would silently report "no profile" and drop back to guessing. The bug would have been a hyphen.Verification
Every price in the fixtures is invented — the layout is what the parser keys on, and this repo is public. Both directions checked:
LEATHER - NOVELTYbeforeLEATHER - NOVELTY PREMIUM→ prices the most expensive rung in the book one tier low, fails another3,489 unit tests pass; tsc, prettier, lint and markdownlint clean.
→
docs/domains/vendors-wholesale.md🤖 Generated with Claude Code