Define the v1 rate structure types #29
Reference in New Issue
Block a user
No description provided.
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Question
Surfaced by the common schema resolution, which established that price is a typed rate function
evaluated against an RFP-declared scenario, and flagged the rate structure as the most intricate
object in the schema and the one most likely to need extending.
Resolve: which rate shapes v1 supports (flat unit rate, volume bands, tiered breaks with marginal
vs. whole-basket application, minimums, surcharges, fixed fees); how each is represented; the
evaluation function that turns a rate structure plus a scenario quantity into a comparable cost;
what happens when a vendor's pricing does not fit any supported shape; and whether unsupported
shapes degrade to an attachment with the ranking engine abstaining on price for that bid.
Note the fairness constraint from the parent decision: the scenario is published with the RFP, so
the evaluation function must be deterministic and reproducible — a recomputation months later
during a protest has to produce the same number.
Parent: #1
Resolution
Built, not just decided.
src/pricing/is real code —rate.ts,scenario.ts,evaluate.ts,exception.ts— andnpm run check:pricingasserts it against the Friendship PCSresponse set. This ticket was takeable ahead of the other seven precisely because the corpus can
falsify it: the other frontier tickets resolve by argument, this one resolves by arithmetic.
Decision 1 — a scenario is a closed vocabulary, not a quantity map
The seam here was
evaluate(rate, {[priceLineId]: number}): number. Both halves were wrong. Potomacneeds one number. Chesapeake needs deliveries-per-week, site count and weeks. Anacostia needs volume
distributed across months. So a rate structure consumes a typed set of variables, and v1 names
them centrally:
siteCount,deliveriesPerWeek,instructionalWeeks, plus the one vector,monthlyWeights.Free-form retailer-authored keys were rejected. The scenario is published with the RFP so every
bid in a field is priced against identical assumptions; let vendors reference arbitrary keys and two
bids in one field read different variables, which is the single thing derived cost exists to
prevent. A closed set also turns "your structure needs a variable this RFP does not publish" into a
validation error at bid time, while the vendor can still fix it — not a null in the evaluator
months later when nobody can act on it.
Extending the vocabulary is a schema change on purpose, the same posture #26 took on the category
spine.
One thing named explicitly in the code, because it will otherwise be asked during a protest: a
scenario variable is an assumption for comparison, not a contract term. Chesapeake's delivery
frequency is changeable mid-term on two weeks' notice, so the awarded price will not match the
derived cost.
Decision 2 — composition, not an enumeration of shapes
Three vendors on one small RFP produced three structures, and two of them are the same object
wearing different decoration. Enumerating whole shapes costs a cross-product —
flat,flat+surcharge,banded,banded+seasonal,seasonal+surcharge— before anyone has met a fourthvendor.
So a structure is one base schedule deciding unit price, plus closed modifiers, plus an
optional floor:
flat_unit,volume_banded(with explicitapplication)seasonal_multiplier,per_event_charge,fixed_feeminimumChargeBanded-and-seasonal composes for free without anyone designing it, which matters because it is
obviously real and no corpus vendor happens to bid it. Extending v1 adds one union member rather
than a row and a column.
whole_basketvsmarginalis explicit and never inferred. Potomac's bands are a retroactiveyear-end rebate repricing every case; marginal ladders are equally common elsewhere. One word of
data, and on this bid a $10,200 difference. No default is right more than about half the time,
so the extractor must state what it saw.
Structures attach to price lines, not to bids. Anacostia's seasonal multiplier applies to produce
only; grocery, dairy and protein are flat. The seam's per-line keying was already right about this.
Deferred:
greater_of— SLC'sMAX(1/12 x MAG, % of gross receipts). Genuinely recursive, itbelongs to concessions rather than supply, and SLC extraction is still fog. It falls to
shape_unsupportedand abstains, which is the right outcome for a shape we cannot evaluate — betterthan a half-built one we can evaluate wrong.
Decision 3 — three outcomes, because the corpus has three
The ticket offered two: it fits, or it degrades to an attachment. Chesapeake is the third and the
one that matters.
shape_unsupportedinput_missingChesapeake's
flat_unit + per_event_chargeis fully supported. Its unit case price is simply absent,living in an Excel sheet the extractor never received (KM-1). Collapsing that into "doesn't fit"
throws away the only actionable fact. So money leaves in the type are nullable on purpose,
extraction produces a structure with holes, and the evaluator's
missing[]names them — that arrayis the clarification request. A parse that rejected the whole structure would lose which field to
ask about.
Three rules on top:
Abstain is not zero and not exclusion. Price is 40 points at Friendship. A null scored as zero
disqualifies on arithmetic; a null dropped from the field does it more quietly. "This bid has no
price" is a decision, and #15 says the system does not make those. It routes to a human, achromatic
and dashed, the same encoding #36 gave
indeterminate— because it is the same statement.A partial sum is never shown as a total. If any required line abstains the bid total is null and
the surface shows
3/4 lines priced. The check asserts the withheld partial is $736,792.00 —arithmetically correct, and not this bid's price. Exactly #37's roll-up bug, in money.
Stated never substitutes for computed. Chesapeake's stated total is $541,280 — the lowest number
in the field, unverifiable, on the bid that fails two gates. Separate columns; only computed ranks.
Where both exist and disagree — Potomac's planted $400 — that is a finding on the bid, not a
tiebreak.
Decision 4 — the derivation is the record, not the number
Data persists; the evaluator is code, and code moves. Recomputing during a challenge and getting a
different answer than the award is this ticket's worst failure.
Stored per computation:
{ amount, reason, breakdown, evaluatorVersion, inputsHash }. Thebreakdown does the real work — band chosen, seasonal factor, each modifier's contribution, floor
if it bound. A protest asks why did this bid rank here, and a bare scalar can only assert.
Recompute on read is a check: matching is silent, divergence is a flagged finding, the stored
number stays authoritative.
evaluatorVersionexists to explain divergence, not to promise oldversions stay runnable forever.
Rounding is pinned, not discovered. A line accumulates as an exact rational in bigint —
numerator and denominator, never divided — and rounds once at the end, half-up. Nothing quantizes on
the way through.
A scenario amendment voids derived costs rather than recomputing them, consistent with #16 and
#37. Every figure was computed against assumptions that no longer hold, and a silently recomputed
price is more dangerous than a silently recomputed score, because nobody watches arithmetic the way
they watch a rubric.
Decision 5 — the exception that has no price
An exception modifies the terms and sometimes carries a price consequence. It is not a price
field. Anacostia labels three: two carry deltas, and EX-3 says that if declined the vendor cannot bid
produce at all. A nullable
priceDeltaIfDeclinedreads that as "no cost impact" — the inverse of thetruth, on the most consequential of the three.
So the consequence is a closed union:
price_delta/withdrawal/unquantified.asBidis what the vendor is offering and the only comparable number.As-if-declined is computed and shown beside it and never ranks — accepting an exception is the
retailer's call, and folding it into the ordering is the system pre-deciding it.
withdrawalexception makes as-if-declined null, reasonwithdrawal_conditioned, reusingthe Decision 3 machinery. Anacostia's own arithmetic (624,315 + 18,400 + 9,750 = 652,465) is
internally consistent and still incomplete, because it silently omits the exception that cannot be
added.
section. Detecting it is valuable; pricing it is a model deciding. It surfaces as a finding and
renders indeterminate.
"never checked" must not look alike — the
answered/not_answereddistinction from #36.Two defects the corpus check caught that review would not have
Both were found by assertions failing, not by reading the code.
The band ladder priced two different ways depending on a flag. Bands read naturally as
[0, 11999]then[12000, 23999], and subtracting those bounds in the marginal path loses the unitbetween them — one unit in 21,400, invisible in the total and wrong in the record.
tois inclusivefor whole-basket selection; marginal reads the next band's
fromand treats the ladder ashalf-open.
bandsContiguousnow rejects any ladder where the two readings disagree, because astructure that prices differently depending on a flag is not something to discover during a protest.
The published scenario was quantizing the seasonal factor.
monthlyDistributionstarted astwelve basis-point shares. Rounding twelve shares moved the weighted factor by 3e-5 — about $19
on a $624,000 line — and #22 is emphatic that rounding erases results. The fix was to stop
normalising: the field is now
monthlyWeights, raw non-negative integers, and a school calendarpublishes instructional days per month as an exact integer nobody has to round. The evaluator takes
the ratio last. The independent float check now agrees to nine decimal places instead of five.
The second one is the more interesting finding. The evaluator was exact the whole time; the input
format was throwing away precision before the evaluator ever saw it, which no amount of careful
arithmetic downstream could recover.
Verified
Schema updated alongside:
price_line.required,evaluation_scenario.monthly_weights, a newscenario_variabletable under the basket's disclosure class,exceptionrebuilt around theconsequence union, and
derived_costcarrying nullable amount + reason + breakdown +evaluator_version + inputs_hash + voided_by_amendment_id.
Handoffs
derived_costis now an evidentiary row, not a cached number. The recompute-as-checkrule and the void-on-amendment rule both belong to the audit design, and the "database-level tests
are not optional" note already sitting on that ticket now has a concrete first subject.
absent from the truth file). That makes exceptions a completeness signal feeding the sweep, not
merely a response field.
instructionalWeeksis a school foodcontract's variable; logistics and facilities will want their own. Worth checking the spine's
groups against it rather than discovering the gap per-RFP.
What this does not settle
Translating a PDF into these structures is extraction (#16) and is unbuilt — the corpus check
transcribes structures by hand from the truth files, so it verifies the evaluator, not the
extractor. The greater-of shape stays deferred behind
shape_unsupported. And every number herecomes from one solicitation in one category: the vocabulary has met food service and nothing else.