import { MONTHLY_WEIGHTS, type RateStructure, type ScalarVariable, type ScenarioVariable, requiredVariables, } from './rate.js' /** * The published scenario. #10 put it in the RFP so price is bindable up front, * and #29 made its variable set closed (see rate.ts). * * Note what a scenario variable IS: an assumption declared 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 and * nobody should be surprised by that. Naming it an assumption here is cheaper * than explaining it during a protest. */ export interface Scenario { /** Per price line. Fractional quantities are fine; the evaluator scales to integers. */ readonly quantities: Readonly> readonly scalars: Readonly>> /** * Relative volume per month, index 0 = January. Non-negative integers in any * consistent unit — instructional days is the natural one for a school food * contract. Only the ratios are read, so nothing here needs normalising and * therefore nothing here gets rounded. */ readonly monthlyWeights?: readonly number[] } /** * Submission-time validation: a structure that reads a variable this RFP did * not publish is rejected NOW, while the vendor can still fix it — not * resolved to a null in the evaluator months later when nobody can act on it. */ export function missingVariables(rate: RateStructure, scenario: Scenario): ScenarioVariable[] { return requiredVariables(rate).filter((v) => v === MONTHLY_WEIGHTS ? scenario.monthlyWeights === undefined : scenario.scalars[v] === undefined, ) } /** * A malformed published scenario is a bug, not a bid outcome — so this throws * rather than abstaining. Abstention is reserved for facts about the response. */ export function assertWellFormed(scenario: Scenario): void { const w = scenario.monthlyWeights if (w === undefined) return if (w.length !== 12) throw new Error(`monthlyWeights has ${w.length} entries, expected 12`) if (w.some((n) => !Number.isInteger(n) || n < 0)) { throw new Error('monthlyWeights must be non-negative integers') } if (w.reduce((a, b) => a + b, 0) <= 0) throw new Error('monthlyWeights sum to zero') }