Define the v1 rate structure types #29

Closed
opened 2026-08-02 05:17:48 +00:00 by christian · 1 comment
Owner

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

## 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
christian added the
wayfinder:grilling
wayfinder:ticket
labels 2026-08-02 05:17:48 +00:00
christian self-assigned this 2026-08-04 01:16:55 +00:00
Author
Owner

Resolution

Built, not just decided. src/pricing/ is real code — rate.ts, scenario.ts,
evaluate.ts, exception.ts — and npm run check:pricing asserts it against the Friendship PCS
response 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. Potomac
needs 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 fourth
vendor.

So a structure is one base schedule deciding unit price, plus closed modifiers, plus an
optional floor:

Members
Base flat_unit, volume_banded (with explicit application)
Modifiers seasonal_multiplier, per_event_charge, fixed_fee
Floor minimumCharge

Banded-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_basket vs marginal is explicit and never inferred. Potomac's bands are a retroactive
year-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's MAX(1/12 x MAG, % of gross receipts). Genuinely recursive, it
belongs to concessions rather than supply, and SLC extraction is still fog. It falls to
shape_unsupported and abstains, which is the right outcome for a shape we cannot evaluate — better
than 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.

Outcome Cause Recoverable
priced
shape_unsupported no supported shape represents it no — attachment, permanently
input_missing shape supported, a field is absent yes — ask the vendor

Chesapeake's flat_unit + per_event_charge is 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 array
is 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 }. The
breakdown 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. evaluatorVersion exists to explain divergence, not to promise old
versions 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 priceDeltaIfDeclined reads that as "no cost impact" — the inverse of the
truth, on the most consequential of the three.

So the consequence is a closed union: price_delta / withdrawal / unquantified.

  • Two figures, one ranks. asBid is 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.
  • A withdrawal exception makes as-if-declined null, reason withdrawal_conditioned, reusing
    the 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.
  • Unlabelled exceptions never move arithmetic. Chesapeake's is a polite sentence in the Pricing
    section. Detecting it is valuable; pricing it is a model deciding. It surfaces as a finding and
    renders indeterminate.
  • Absence is recorded affirmatively. Potomac has none. "Read the pricing section, found none" and
    "never checked" must not look alike — the answered / not_answered distinction 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 unit
between them — one unit in 21,400, invisible in the total and wrong in the record. to is inclusive
for whole-basket selection; marginal reads the next band's from and treats the ladder as
half-open. bandsContiguous now rejects any ladder where the two readings disagree, because a
structure that prices differently depending on a flag is not something to discover during a protest.

The published scenario was quantizing the seasonal factor. monthlyDistribution started as
twelve 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 calendar
publishes 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

Potomac        computes $589,570.00 at 21,400 cases — and does NOT reproduce the
               stated $589,970.00 (divergence $400.00)
               marginal application yields $599,770.00, pricing every unit exactly once
Chesapeake     abstains; reason input_missing (not shape_unsupported);
               names base.unitPrice as the field to ask for;
               never substitutes the stated $541,280.00
Anacostia      abstains with no calendar, naming scenario.monthlyWeights;
               $649,792.00 with it — MORE than the $624,000.00 a flat assumption gives;
               weighted factor 1.041333333 vs independent 1.041333333
exceptions     as-bid ranks; the withdrawal exception makes as-if-declined undefined,
               not $652,465.00; EX-3's C.3 target surfaces as a completeness finding;
               Chesapeake's unlabelled exception moves nothing
roll-up        3/4 lines price, total withheld, and the $736,792.00 partial is
               asserted to be a number that must never be shown as a total

PRICING OK          tsc --noEmit clean

Schema updated alongside: price_line.required, evaluation_scenario.monthly_weights, a new
scenario_variable table under the basket's disclosure class, exception rebuilt around the
consequence union, and derived_cost carrying nullable amount + reason + breakdown +
evaluator_version + inputs_hash + voided_by_amendment_id.

Handoffs

  • #21derived_cost is now an evidentiary row, not a cached number. The recompute-as-check
    rule 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.
  • #16 — an exception can target a section the extractor never enumerated (EX-3 points at C.3,
    absent from the truth file). That makes exceptions a completeness signal feeding the sweep, not
    merely a response field.
  • #28 — the scenario vocabulary is category-shaped. instructionalWeeks is a school food
    contract'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 here
comes from one solicitation in one category: the vocabulary has met food service and nothing else.

## Resolution Built, not just decided. `src/pricing/` is real code — `rate.ts`, `scenario.ts`, `evaluate.ts`, `exception.ts` — and `npm run check:pricing` asserts it against the Friendship PCS response 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. Potomac needs 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 fourth vendor. So a structure is **one base schedule** deciding unit price, plus **closed modifiers**, plus an optional floor: | | Members | |---|---| | Base | `flat_unit`, `volume_banded` (with explicit `application`) | | Modifiers | `seasonal_multiplier`, `per_event_charge`, `fixed_fee` | | Floor | `minimumCharge` | Banded-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_basket` vs `marginal` is explicit and never inferred.** Potomac's bands are a retroactive year-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's `MAX(1/12 x MAG, % of gross receipts)`. Genuinely recursive, it belongs to concessions rather than supply, and SLC extraction is still fog. It falls to `shape_unsupported` and abstains, which is the right outcome for a shape we cannot evaluate — better than 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. | Outcome | Cause | Recoverable | |---|---|---| | priced | | | | `shape_unsupported` | no supported shape represents it | no — attachment, permanently | | `input_missing` | shape supported, **a field is absent** | **yes — ask the vendor** | Chesapeake's `flat_unit + per_event_charge` is 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 array *is* 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 }`. The **breakdown** 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. `evaluatorVersion` exists to explain divergence, not to promise old versions 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 `priceDeltaIfDeclined` reads that as "no cost impact" — the inverse of the truth, on the most consequential of the three. So the consequence is a closed union: `price_delta` / `withdrawal` / `unquantified`. - **Two figures, one ranks.** `asBid` is 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. - **A `withdrawal` exception makes as-if-declined null**, reason `withdrawal_conditioned`, reusing the 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. - **Unlabelled exceptions never move arithmetic.** Chesapeake's is a polite sentence in the Pricing section. Detecting it is valuable; pricing it is a model deciding. It surfaces as a finding and renders indeterminate. - **Absence is recorded affirmatively.** Potomac has none. "Read the pricing section, found none" and "never checked" must not look alike — the `answered` / `not_answered` distinction 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 unit between them — one unit in 21,400, invisible in the total and wrong in the record. `to` is inclusive for whole-basket *selection*; marginal reads the next band's `from` and treats the ladder as half-open. `bandsContiguous` now rejects any ladder where the two readings disagree, because a structure that prices differently depending on a flag is not something to discover during a protest. **The published scenario was quantizing the seasonal factor.** `monthlyDistribution` started as twelve 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 calendar publishes 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 ``` Potomac computes $589,570.00 at 21,400 cases — and does NOT reproduce the stated $589,970.00 (divergence $400.00) marginal application yields $599,770.00, pricing every unit exactly once Chesapeake abstains; reason input_missing (not shape_unsupported); names base.unitPrice as the field to ask for; never substitutes the stated $541,280.00 Anacostia abstains with no calendar, naming scenario.monthlyWeights; $649,792.00 with it — MORE than the $624,000.00 a flat assumption gives; weighted factor 1.041333333 vs independent 1.041333333 exceptions as-bid ranks; the withdrawal exception makes as-if-declined undefined, not $652,465.00; EX-3's C.3 target surfaces as a completeness finding; Chesapeake's unlabelled exception moves nothing roll-up 3/4 lines price, total withheld, and the $736,792.00 partial is asserted to be a number that must never be shown as a total PRICING OK tsc --noEmit clean ``` Schema updated alongside: `price_line.required`, `evaluation_scenario.monthly_weights`, a new `scenario_variable` table under the basket's disclosure class, `exception` rebuilt around the consequence union, and `derived_cost` carrying nullable amount + reason + breakdown + evaluator_version + inputs_hash + voided_by_amendment_id. ### Handoffs - **#21** — `derived_cost` is now an evidentiary row, not a cached number. The recompute-as-check rule 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. - **#16** — an exception can target a section the extractor never enumerated (EX-3 points at C.3, absent from the truth file). That makes exceptions a **completeness signal** feeding the sweep, not merely a response field. - **#28** — the scenario vocabulary is category-shaped. `instructionalWeeks` is a school food contract'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 here comes from one solicitation in one category: the vocabulary has met food service and nothing else.
Sign in to join this conversation.
No description provided.