From b8e8f1df7af7b584dee4266f8758ea25285f2845 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 03:01:33 +0000 Subject: [PATCH 1/7] wip: cross-mode supply selector --- dag/product/fabric/selection.dag | 874 ++++++++++++++++++ dag/product/fabric/supply.dag | 80 +- .../claim/fabric_selection_witness_test.dag | 484 ++++++++++ 3 files changed, 1419 insertions(+), 19 deletions(-) create mode 100644 dag/product/fabric/selection.dag create mode 100644 dag/test/claim/fabric_selection_witness_test.dag diff --git a/dag/product/fabric/selection.dag b/dag/product/fabric/selection.dag new file mode 100644 index 00000000000..a870a940a6d --- /dev/null +++ b/dag/product/fabric/selection.dag @@ -0,0 +1,874 @@ +module product.fabric.selection + +import std.types { NonEmptyStr, List, Bool } +import std.measure { + MoneyAmountMicro, MoneyOnce, MoneyPerSecond, money_amount_micro, money_amount_micro_count, + money_once_micros, money_rate_micros, + Second, second, second_count, +} +import std.currency { CurrencyCode } +import product.fabric.identity { FabricIdentity, OfferKey, fabric_identity_eq } +import product.fabric.demand { ObservationReceiptRef } +import product.fabric.work { ExecutionRequirements } +import product.fabric.supply { + SupplierOffer, OfferQuote, QuotedFlatPerGrant, QuotedPerSecond, QuotedPerMinute, QuotedPerHour, + OfferFungibility, Fungible, TrustDomainMismatch, CapabilitiesNotOffered, + IsolationNotProvided, ShapeAxesNotCovered, offer_fungibility_for, + DemandOfferAffordability, Affordable, AffordableUnderEstimate, + QuoteExceedsMaximumBuyOrder, QuoteCurrencyMismatch, + offer_affordability_for, offer_quote_currency, offer_quoted_total_for_grant, + GrantDuration, ExactGrantDuration, EstimatedGrantDuration, grant_duration_seconds, +} + +// SELECTION IS THE FOLD product.fabric.supply DECLINED TO WRITE, AND THIS MODULE IS WHY IT +// CAN NOW BE WRITTEN. +// +// supply.dag carries two SCREENS -- offer_fungibility_for, which takes no money, and +// offer_affordability_for, which takes no shape -- and it stated plainly that choosing among +// the survivors would be a lie until six facts existed that no carrier held. Those six are +// modeled here, each as a named observation with an arm for NOT READ that refuses rather than +// defaulting to zero, and the fold below consumes all six. +// +// IT IS A SEPARATE MODULE RATHER THAN A THIRD FOLD IN supply.dag, and the reason is the +// direction of the import. supply.dag owns what an offer IS; it may not import a buyer's +// delay valuation, a commitment ledger, or an alternative-use observation without becoming +// the place where economics live -- which is exactly the boundary its own header draws +// between a supply row and an assignment evaluation. This module imports supply and is +// imported by nobody in supply's direction, so the screens stay usable on their own and the +// selector is one composition of them rather than a competing answer beside them. +// +// WHAT MAKES IT A CROSS-MODE SELECTOR RATHER THAN A RATE COMPARISON. Continuous owned +// capacity, hourly rented bare metal and per-container cloud are not three kinds of offer -- +// supply.dag already refuses that fork -- they are three settings of facts an offer carries: +// a billing quantum of zero against one of 3600, a quote of an explicit zero against a +// published rate, an already-paid-through interval against pay-as-you-go, an operating cost +// we bear against one the vendor bears. Every one of those moves the number this fold ranks +// on, so the modes are compared by ARITHMETIC over one carrier and never by a mode tag. There +// is deliberately no such tag anywhere in this file: an offer that wins because it is ours +// must win because its number is lower, and if a mode enum existed someone would eventually +// branch on it. + +// THE SIX INPUTS, ENUMERATED AS A CLOSED AXIS RATHER THAN AS SIX PROSE SENTENCES. +// +// supply.dag named them in a comment, which meant a seventh could be forgotten and nothing +// would notice. As a coproduct the enumeration is what exhaustiveness protects: every fold +// over an axis below must answer for all six, and adding a seventh fails to compile at each +// site rather than being silently unpriced at one of them. +// +// Each axis is a fact about the OFFER at the moment a decision is taken, and each is +// observed rather than assumed. The unread arms are the whole point: DESIGN section 5 rules +// that a failure arm must refuse and never widen, and the widening available here is +// arithmetic -- an unknown cost silently entering a sum as zero, which does not merely lose +// information, it flatters exactly the supplier nobody measured. +type SelectionInputAxis + = AvailabilityAxis + | CommitmentAxis + | StartCostAxis + | TransitionCostAxis + | OperatingCostAxis + | OpportunityCostAxis + +fn selection_input_axis_wire(axis: SelectionInputAxis) -> String { + match axis { + AvailabilityAxis => "availability interval" + CommitmentAxis => "paid-through commitment state" + StartCostAxis => "start cost" + TransitionCostAxis => "transition cost" + OperatingCostAxis => "marginal operating cost" + OpportunityCostAxis => "opportunity cost" + } +} + +// AXIS 1 -- THE AVAILABILITY INTERVAL, AS TWO DURATIONS RELATIVE TO THE DECISION INSTANT +// RATHER THAN AS TWO TIMESTAMPS. +// +// SupplierOffer already carries ready_at, and it is a std.types Timestamp, which is a String. +// No arithmetic exists over it, so a fold that wanted to know how long a grant must wait +// could not derive it from that field however much the field looks like it should. Deriving +// it anyway would mean inventing timestamp arithmetic in the one place a wrong answer silently +// reorders the ranking. +// +// So the interval arrives as it is actually observed: how long until this offer can start, +// and how long it remains available once it has. Both are stated relative to the instant the +// selection is taken, which is also the instant every other axis below is stated at. A +// consumer that reads these at one moment and selects at another has compared facts from two +// worlds; that is a property of the caller and is named here rather than checked, because +// nothing in this module can observe the caller's clock. +type AvailabilityObservation + = AvailableForInterval { delay: Second, interval: Second, receipt: ObservationReceiptRef } + | AvailabilityUnobserved { obligation: NonEmptyStr } + +// AXIS 2 -- THE PAID-THROUGH COMMITMENT STATE. +// +// An hour already bought has a marginal cash cost of zero until it expires, and that is the +// single fact that decides whether a rented box we are already holding beats an idle owned +// slot on this particular grant. Without it every offer is priced as though its meter starts +// now, which systematically overprices exactly the capacity we have already committed to. +// +// The remaining interval is carried, not the moment it ends, for the reason axis 1 gives. +// PayAsYouGo is a MEASURED absence of commitment and CommitmentUnread is ignorance about it; +// they are separated because their remedies differ -- one needs no work at all, the other +// needs a ledger read -- and because collapsing them makes the ignorance case price as the +// expensive one, which reads as conservative and is simply wrong: it hides supply we own. +type CommitmentState + = PayAsYouGo + | PaidThrough { remaining: Second, receipt: ObservationReceiptRef } + | CommitmentUnread { obligation: NonEmptyStr } + +// AXIS 3 -- THE START COST: what is paid ONCE to bring this offer to a state where it can +// accept the work. Provisioning, boot, image pull, a supplier's fixed or setup charge. +// +// It is a MoneyOnce rather than a bare amount because it carries its own currency, and a +// start charge quoted in a currency the budget does not hold is a refusal rather than a +// number to add. An always-ready host states an explicit zero here; supply.dag's rule that +// absence must never be interpreted as free applies to every one of these axes and not only +// to the quote. +type StartCostObservation + = StartCostMeasured { amount: MoneyOnce, receipt: ObservationReceiptRef } + | StartCostUnread { obligation: NonEmptyStr } + +// AXIS 4 -- THE TRANSITION COST: what is paid ONCE because the work MOVES to this offer +// rather than staying where it is. Egress, a cold cache, a re-materialized input set. +// +// It is a separate axis from the start cost and not a second field of one, because they have +// different owners and different remedies: a start cost is a property of the offer alone, +// while a transition cost exists only relative to where the work is now. The same offer +// carries one start cost and as many transition costs as there are places the work could +// come from, which is why NoTransitionRequired is a real arm -- work already sited on this +// executor moves nowhere -- and is a measured zero rather than an unread one. +type TransitionCostObservation + = NoTransitionRequired + | TransitionCostMeasured { amount: MoneyOnce, receipt: ObservationReceiptRef } + | TransitionCostUnread { obligation: NonEmptyStr } + +// AXIS 5 -- THE MARGINAL OPERATING COST, AND IT IS NOT THE QUOTE. +// +// The quote is a supplier-side ASKING PRICE. For owned capacity that price is an explicit +// zero -- we do not invoice ourselves -- and it would be a serious error to read that zero as +// the cost of running the machine. Power and cooling are incurred by the ACT of running, so +// they are real cash and a real dispatch input; only the historical purchase is sunk, and +// sunk cost is excluded from dispatch while being retained for accounting. An owned offer +// whose operating cost went unpriced would win every comparison it entered, which is the one +// outcome this whole model exists to avoid claiming without evidence. +// +// OperatingCostBorneBySupplier is the arm that keeps the same cash from being counted twice. +// A rented offer's published rate already contains the vendor's power and cooling; adding our +// own figure on top would charge the buyer twice for one kilowatt-hour. That is a statement +// about who bears the cost, which is knowable, and it is deliberately not the same arm as +// having failed to look. +type OperatingCostObservation + = OperatingCostMeasured { rate: MoneyPerSecond, receipt: ObservationReceiptRef } + | OperatingCostBorneBySupplier + | OperatingCostUnread { obligation: NonEmptyStr } + +// AXIS 6 -- THE OPPORTUNITY COST, WITH THE TWO ARMS supply.dag NAMED SPECIFICALLY. +// +// What an hour could otherwise have earned is not a field on the supply row: it depends on +// which OTHER demands could have used that hour, so it belongs to the assignment evaluation, +// which is this fold. It arrives as an observation about the alternative uses that existed. +// +// NoFeasibleAlternativeUse is the arm supply.dag insisted on: an hour about to expire idle +// displaced nobody, so its opportunity cost is a measured zero and must not be priced as +// though a customer had been turned away. Pricing it otherwise makes idle owned capacity look +// expensive precisely when it is free, which inverts the decision. +// +// AlternativeUseUnread is the other arm it insisted on, and it is the one that must never +// become zero. Not having read the alternative uses is not the same as having read them and +// found none, and the difference is the entire distance between a measured zero and an +// absorbing fallback. +type OpportunityCostObservation + = NoFeasibleAlternativeUse { receipt: ObservationReceiptRef } + | AlternativeUseDisplaced { forgone: MoneyOnce, receipt: ObservationReceiptRef } + | AlternativeUseUnread { obligation: NonEmptyStr } + +// THE SIX TRAVEL TOGETHER AND THEY NAME THE OFFER THEY DESCRIBE. +// +// Pricing offer A with offer B's inputs is a silent wrong answer of the worst kind: every +// value is real, the arithmetic is correct, and the ranking is nonsense. The identity is +// therefore carried ON the observation set and checked once, at the only place a pair can be +// formed. +type DispatchInputs

{ + offer: FabricIdentity + availability: AvailabilityObservation + commitment: CommitmentState + start: StartCostObservation + transition: TransitionCostObservation + operating: OperatingCostObservation + opportunity: OpportunityCostObservation +} + +// A CANDIDATE IS A SEALED PAIR, so a mismatched one has no spelling rather than being caught +// by a check the fold has to remember to run. sole_constructor confines construction to +// bind_candidate below, which refuses the mismatch; DESIGN section 4b's ceiling applies +// unchanged -- this confines WHO may construct and establishes nothing about whether the +// observations themselves were honestly taken, which stays a boundary obligation on whoever +// produced them. +type CandidateOffer

sole_constructor { + offer: SupplierOffer

+ dispatch: DispatchInputs

+} + +type CandidateBinding

+ = CandidateBound { candidate: CandidateOffer

} + | DispatchInputsNameAnotherOffer { + offer: FabricIdentity, + inputs_name: FabricIdentity, + } + +fn bind_candidate

(offer: SupplierOffer

, dispatch: DispatchInputs

) -> CandidateBinding

{ + if !fabric_identity_eq(left: offer.id, right: dispatch.offer) { + DispatchInputsNameAnotherOffer { offer: offer.id, inputs_name: dispatch.offer } + } else { + CandidateBound { candidate: CandidateOffer { offer: offer, dispatch: dispatch } } + } +} + +// THE COST IS CARRIED AS ITS COMPONENTS, AND THE TWO TOTALS ARE DERIVED FROM THEM. +// +// A stored total is a second representation of the sum, free to disagree with the parts it +// claims to be the sum of, and the disagreement is invisible exactly when it matters. So +// there is no total field: selection_liability and selection_total below are functions, and +// a component that changes cannot leave a stale total behind it. +// +// The split into LIABILITY and TOTAL is demand.dag's three-fact rule enforced rather than +// restated. Liability is cash that must be reserved before a grant may issue, so it is the +// four axes that actually move money out. Total is the comparison number, so it additionally +// carries opportunity cost -- which is real economics and not cash -- and valued delay, which +// is the buyer's own statement of what waiting is worth. Ranking on cash alone waits minutes +// to save cents; reserving against the comparison number would reserve money nobody pays. +type SelectionCost { + marginal_charge: MoneyAmountMicro + start: MoneyAmountMicro + transition: MoneyAmountMicro + operating: MoneyAmountMicro + opportunity: MoneyAmountMicro + valued_delay: MoneyAmountMicro +} + +fn selection_liability(c: SelectionCost) -> MoneyAmountMicro { + money_amount_micro( + count: money_amount_micro_count(m: c.marginal_charge) + + money_amount_micro_count(m: c.start) + + money_amount_micro_count(m: c.transition) + + money_amount_micro_count(m: c.operating), + ) +} + +fn selection_total(c: SelectionCost) -> MoneyAmountMicro { + money_amount_micro( + count: money_amount_micro_count(m: selection_liability(c: c)) + + money_amount_micro_count(m: c.opportunity) + + money_amount_micro_count(m: c.valued_delay), + ) +} + +// A PROJECTED TOTAL AND A KNOWN ONE ARE DIFFERENT FACTS, and supply.dag already makes the +// same distinction one level down with ExactGrantDuration against EstimatedGrantDuration. +// The basis is derived from the duration arm rather than accepted as an argument, so a caller +// cannot claim an exact basis over an estimated duration. +type CostBasis + = ExactCost + | ProjectedCost + +fn cost_basis_for(duration: GrantDuration) -> CostBasis { + match duration { + ExactGrantDuration { seconds: _ } => ExactCost + EstimatedGrantDuration { seconds: _ } => ProjectedCost + } +} + +// Nat subtraction with a floor at zero, written once. Every use below is a duration remainder +// where the negative case means "already covered", never a defect: a commitment extending +// past the grant leaves zero billable seconds, not minus some. +fn saturating_difference(from: Nat, take: Nat) -> Nat { + if take >= from { + 0 + } else { + from - take + } +} + +// AN UNREAD AXIS IS A TYPED, LOCATED, COUNTABLE REFUSAL rather than a zero entering a sum. +// The axis is carried beside the obligation so a consumer can rank which measurement is +// blocking the most supply, which is the property DESIGN section 5 asks of a degradation: +// its frequency must be observable. +type UnpricedInput { + axis: SelectionInputAxis + obligation: NonEmptyStr +} + +// A currency the budget does not hold is refused per axis rather than converted. Converting +// would require a rate this module has no authority over, and picking one silently is the +// fabricated-plausible-output failure denominated in someone else's money. +type SelectionCurrencyMismatch { + axis: SelectionInputAxis + stated: CurrencyCode + budgeted: CurrencyCode +} + +type PricedAxis + = AxisPriced { amount: MoneyAmountMicro } + | AxisUnpriced { unpriced: UnpricedInput } + | AxisWrongCurrency { mismatch: SelectionCurrencyMismatch } + +fn priced_once( + axis: SelectionInputAxis, + amount: MoneyOnce, + budgeted: CurrencyCode, +) -> PricedAxis { + if amount.currency != budgeted { + AxisWrongCurrency { + mismatch: SelectionCurrencyMismatch { axis: axis, stated: amount.currency, budgeted: budgeted }, + } + } else { + AxisPriced { amount: money_amount_micro(count: money_once_micros(q: amount)) } + } +} + +fn priced_start(o: StartCostObservation, budgeted: CurrencyCode) -> PricedAxis { + match o { + StartCostUnread { obligation: why } => + AxisUnpriced { unpriced: UnpricedInput { axis: StartCostAxis, obligation: why } } + StartCostMeasured { amount: a, receipt: _ } => + priced_once(axis: StartCostAxis, amount: a, budgeted: budgeted) + } +} + +fn priced_transition(o: TransitionCostObservation, budgeted: CurrencyCode) -> PricedAxis { + match o { + TransitionCostUnread { obligation: why } => + AxisUnpriced { unpriced: UnpricedInput { axis: TransitionCostAxis, obligation: why } } + NoTransitionRequired => AxisPriced { amount: money_amount_micro(count: 0) } + TransitionCostMeasured { amount: a, receipt: _ } => + priced_once(axis: TransitionCostAxis, amount: a, budgeted: budgeted) + } +} + +fn priced_opportunity(o: OpportunityCostObservation, budgeted: CurrencyCode) -> PricedAxis { + match o { + AlternativeUseUnread { obligation: why } => + AxisUnpriced { unpriced: UnpricedInput { axis: OpportunityCostAxis, obligation: why } } + NoFeasibleAlternativeUse { receipt: _ } => AxisPriced { amount: money_amount_micro(count: 0) } + AlternativeUseDisplaced { forgone: a, receipt: _ } => + priced_once(axis: OpportunityCostAxis, amount: a, budgeted: budgeted) + } +} + +// Operating cost is a RATE over the seconds the work actually runs, not over the seconds the +// supplier bills. A quantum rounds a bill up; it does not make the fans spin longer. +fn priced_operating( + o: OperatingCostObservation, + run_seconds: Nat, + budgeted: CurrencyCode, +) -> PricedAxis { + match o { + OperatingCostUnread { obligation: why } => + AxisUnpriced { unpriced: UnpricedInput { axis: OperatingCostAxis, obligation: why } } + OperatingCostBorneBySupplier => AxisPriced { amount: money_amount_micro(count: 0) } + OperatingCostMeasured { rate: r, receipt: _ } => + if r.currency != budgeted { + AxisWrongCurrency { + mismatch: SelectionCurrencyMismatch { + axis: OperatingCostAxis, + stated: r.currency, + budgeted: budgeted, + }, + } + } else { + AxisPriced { amount: money_amount_micro(count: money_rate_micros(q: r) * run_seconds) } + } + } +} + +// Waiting is priced at the buyer's own rate. Without this term a fabric optimizing money +// alone prefers a supplier that saves a cent and starts in twenty minutes, which is the +// failure demand.dag names when it defines SelectionCost as distinct from MaximumLiability. +fn priced_delay( + delay_seconds: Nat, + valuation: MoneyPerSecond, + budgeted: CurrencyCode, +) -> PricedAxis { + if valuation.currency != budgeted { + AxisWrongCurrency { + mismatch: SelectionCurrencyMismatch { + axis: AvailabilityAxis, + stated: valuation.currency, + budgeted: budgeted, + }, + } + } else { + AxisPriced { amount: money_amount_micro(count: money_rate_micros(q: valuation) * delay_seconds) } + } +} + +// THE COMMITMENT-REDUCED DURATION, WHICH IS WHERE AXIS 2 ACTUALLY BITES. +// +// A rate quote bills TIME, so an interval already paid through removes that much time from +// the bill. A flat per-grant quote bills no time at all -- it is a charge for the grant, and +// supply.dag already refuses to multiply it by a duration -- so a paid-through interval does +// not reduce it. Treating the two the same would either double-count a prepaid rate or +// discount a fee nobody prepaid, and the arm that decides which is the quote's own shape. +fn quote_bills_time(q: OfferQuote) -> Bool { + match q { + QuotedFlatPerGrant(_) => false + QuotedPerSecond(_) => true + QuotedPerMinute(_) => true + QuotedPerHour(_) => true + } +} + +fn billable_duration(duration: GrantDuration, commitment: CommitmentState, quote: OfferQuote) -> GrantDuration { + let full = second_count(s: grant_duration_seconds(d: duration)) + let covered = match commitment { + PayAsYouGo => 0 + CommitmentUnread { obligation: _ } => 0 + PaidThrough { remaining: r, receipt: _ } => + if quote_bills_time(q: quote) { second_count(s: r) } else { 0 } + } + let remaining = second(count: saturating_difference(from: full, take: covered)) + match duration { + ExactGrantDuration { seconds: _ } => ExactGrantDuration { seconds: remaining } + EstimatedGrantDuration { seconds: _ } => EstimatedGrantDuration { seconds: remaining } + } +} + +// THE CANDIDACY VOCABULARY. Every offer offered to the fold leaves it as exactly one of +// these, so the rejected population is countable at identity grain and a broker can read WHY +// each one lost rather than being told that nothing was admissible. +type OfferCandidacy

+ = OfferPriced { offer: FabricIdentity, cost: SelectionCost, basis: CostBasis } + | OfferNotFungible { offer: FabricIdentity, fungibility: OfferFungibility } + | OfferUnavailableForGrant { + offer: FabricIdentity, + needed: Second, + available: Second, + } + | OfferNotPriceable { offer: FabricIdentity, unpriced: UnpricedInput } + | OfferWrongCurrency { offer: FabricIdentity, mismatch: SelectionCurrencyMismatch } + | OfferUnaffordable { + offer: FabricIdentity, + affordability: DemandOfferAffordability, + } + | OfferLiabilityOverBuyOrder { + offer: FabricIdentity, + liability: MoneyAmountMicro, + maximum: MoneyAmountMicro, + } + +fn candidacy_offer

(c: OfferCandidacy

) -> FabricIdentity { + match c { + OfferPriced { offer: o, cost: _, basis: _ } => o + OfferNotFungible { offer: o, fungibility: _ } => o + OfferUnavailableForGrant { offer: o, needed: _, available: _ } => o + OfferNotPriceable { offer: o, unpriced: _ } => o + OfferWrongCurrency { offer: o, mismatch: _ } => o + OfferUnaffordable { offer: o, affordability: _ } => o + OfferLiabilityOverBuyOrder { offer: o, liability: _, maximum: _ } => o + } +} + +// THE ORDER OF THE SCREENS IS LOAD-BEARING, and it is supply.dag's order with the two new +// screens placed where they cannot rescue anything. +// +// FUNGIBILITY FIRST, with no money in scope, so price can never rescue an offer that cannot +// run the work. AVAILABILITY SECOND: whether the offer exists for long enough to hold the +// grant is a capability question and not a price one, so it belongs above the money screens +// and below fungibility. PRICEABILITY THIRD, because the affordability screen below is fed a +// commitment-reduced duration and an unread commitment must refuse before it silently prices +// as pay-as-you-go. AFFORDABILITY FOURTH, against the buyer's ceiling. LIABILITY FIFTH, +// because start, transition and operating cash are liability the quote screen never saw. +// CHEAPEST LAST, over whatever survived. +fn price_candidate

( + requirements: ExecutionRequirements, + candidate: CandidateOffer

, + duration: GrantDuration, + maximum_buy_order: MoneyAmountMicro, + budget_currency: CurrencyCode, + delay_valuation: MoneyPerSecond, +) -> OfferCandidacy

{ + let offer = candidate.offer + let inputs = candidate.dispatch + let fungibility = offer_fungibility_for(requirements: requirements, offer: offer) + match fungibility { + TrustDomainMismatch { required: _, offered: _ } => + OfferNotFungible { offer: offer.id, fungibility: fungibility } + CapabilitiesNotOffered { required: _, offered: _ } => + OfferNotFungible { offer: offer.id, fungibility: fungibility } + IsolationNotProvided { missing: _ } => + OfferNotFungible { offer: offer.id, fungibility: fungibility } + ShapeAxesNotCovered { unmet: _ } => + OfferNotFungible { offer: offer.id, fungibility: fungibility } + Fungible => price_available_candidate( + offer: offer, + inputs: inputs, + duration: duration, + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + delay_valuation: delay_valuation, + ) + } +} + +fn price_available_candidate

( + offer: SupplierOffer

, + inputs: DispatchInputs

, + duration: GrantDuration, + maximum_buy_order: MoneyAmountMicro, + budget_currency: CurrencyCode, + delay_valuation: MoneyPerSecond, +) -> OfferCandidacy

{ + let run_seconds = second_count(s: grant_duration_seconds(d: duration)) + match inputs.availability { + AvailabilityUnobserved { obligation: why } => + OfferNotPriceable { + offer: offer.id, + unpriced: UnpricedInput { axis: AvailabilityAxis, obligation: why }, + } + AvailableForInterval { delay: d, interval: window, receipt: _ } => + if second_count(s: window) < run_seconds { + OfferUnavailableForGrant { + offer: offer.id, + needed: second(count: run_seconds), + available: window, + } + } else { + price_committed_candidate( + offer: offer, + inputs: inputs, + duration: duration, + delay_seconds: second_count(s: d), + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + delay_valuation: delay_valuation, + ) + } + } +} + +fn price_committed_candidate

( + offer: SupplierOffer

, + inputs: DispatchInputs

, + duration: GrantDuration, + delay_seconds: Nat, + maximum_buy_order: MoneyAmountMicro, + budget_currency: CurrencyCode, + delay_valuation: MoneyPerSecond, +) -> OfferCandidacy

{ + match inputs.commitment { + CommitmentUnread { obligation: why } => + OfferNotPriceable { + offer: offer.id, + unpriced: UnpricedInput { axis: CommitmentAxis, obligation: why }, + } + PayAsYouGo => price_screened_candidate( + offer: offer, + inputs: inputs, + duration: duration, + delay_seconds: delay_seconds, + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + delay_valuation: delay_valuation, + ) + PaidThrough { remaining: _, receipt: _ } => price_screened_candidate( + offer: offer, + inputs: inputs, + duration: duration, + delay_seconds: delay_seconds, + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + delay_valuation: delay_valuation, + ) + } +} + +// The affordability screen is supply.dag's own fold, called with the commitment-reduced +// duration rather than re-derived here. Pricing is a single authority and this module +// consumes it; a second price computed beside it would be the fork the fabric exists to +// prevent, and it would be the fork in the one place two answers are indistinguishable +// until money moves. +fn price_screened_candidate

( + offer: SupplierOffer

, + inputs: DispatchInputs

, + duration: GrantDuration, + delay_seconds: Nat, + maximum_buy_order: MoneyAmountMicro, + budget_currency: CurrencyCode, + delay_valuation: MoneyPerSecond, +) -> OfferCandidacy

{ + let billable = billable_duration(duration: duration, commitment: inputs.commitment, quote: offer.quote) + let affordability = offer_affordability_for( + offer: offer, + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + duration: billable, + ) + match affordability { + QuoteCurrencyMismatch { quoted: _, budgeted: _ } => + OfferUnaffordable { offer: offer.id, affordability: affordability } + QuoteExceedsMaximumBuyOrder { quoted: _, maximum: _ } => + OfferUnaffordable { offer: offer.id, affordability: affordability } + Affordable => assemble_cost( + offer: offer, + inputs: inputs, + duration: duration, + billable: billable, + delay_seconds: delay_seconds, + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + delay_valuation: delay_valuation, + ) + AffordableUnderEstimate { duration_seconds: _, projected_total: _ } => assemble_cost( + offer: offer, + inputs: inputs, + duration: duration, + billable: billable, + delay_seconds: delay_seconds, + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + delay_valuation: delay_valuation, + ) + } +} + +// The five remaining axes are priced and the first one that cannot be priced decides the +// candidacy. Priced in a fixed order so that an offer missing two measurements always reports +// the same one, which is what makes a refusal reproducible rather than dependent on +// evaluation order. +fn assemble_cost

( + offer: SupplierOffer

, + inputs: DispatchInputs

, + duration: GrantDuration, + billable: GrantDuration, + delay_seconds: Nat, + maximum_buy_order: MoneyAmountMicro, + budget_currency: CurrencyCode, + delay_valuation: MoneyPerSecond, +) -> OfferCandidacy

{ + let run_seconds = second_count(s: grant_duration_seconds(d: duration)) + let charge = offer_quoted_total_for_grant( + quote: offer.quote, + duration: billable, + quantum: offer.billing_quantum, + ) + let start = priced_start(o: inputs.start, budgeted: budget_currency) + let transition = priced_transition(o: inputs.transition, budgeted: budget_currency) + let operating = priced_operating( + o: inputs.operating, + run_seconds: run_seconds, + budgeted: budget_currency, + ) + let opportunity = priced_opportunity(o: inputs.opportunity, budgeted: budget_currency) + let delay = priced_delay( + delay_seconds: delay_seconds, + valuation: delay_valuation, + budgeted: budget_currency, + ) + match first_unpriced(axes: [start, transition, operating, opportunity, delay]) { + Present { value: bad } => bad_axis_candidacy(offer: offer.id, bad: bad) + Absent => screen_liability( + offer: offer.id, + cost: SelectionCost { + marginal_charge: charge, + start: axis_amount(a: start), + transition: axis_amount(a: transition), + operating: axis_amount(a: operating), + opportunity: axis_amount(a: opportunity), + valued_delay: axis_amount(a: delay), + }, + basis: cost_basis_for(duration: duration), + maximum_buy_order: maximum_buy_order, + ) + } +} + +// The last screen, and the one the quote screen above could not perform: start, transition +// and operating cash are liability the quoted total never saw, so an offer whose asking price +// clears the ceiling can still commit more money than the ceiling allows. +fn screen_liability

( + offer: FabricIdentity, + cost: SelectionCost, + basis: CostBasis, + maximum_buy_order: MoneyAmountMicro, +) -> OfferCandidacy

{ + let liability = selection_liability(c: cost) + if money_amount_micro_count(m: liability) > money_amount_micro_count(m: maximum_buy_order) { + OfferLiabilityOverBuyOrder { offer: offer, liability: liability, maximum: maximum_buy_order } + } else { + OfferPriced { offer: offer, cost: cost, basis: basis } + } +} + +// AxisPriced is not a refusal, so it is the arm that returns nothing; the two failure arms are +// carried out whole so the candidacy can name which axis and which currency. +fn first_unpriced(axes: List) -> PricedAxis? { + fold(axes, init: none, f: (found, a) => + match found { + Present { value: _ } => found + Absent => + match a { + AxisPriced { amount: _ } => none + AxisUnpriced { unpriced: _ } => Present { value: a } + AxisWrongCurrency { mismatch: _ } => Present { value: a } + } + }) +} + +fn bad_axis_candidacy

(offer: FabricIdentity, bad: PricedAxis) -> OfferCandidacy

{ + match bad { + AxisUnpriced { unpriced: u } => OfferNotPriceable { offer: offer, unpriced: u } + AxisWrongCurrency { mismatch: m } => OfferWrongCurrency { offer: offer, mismatch: m } + AxisPriced { amount: _ } => + OfferNotPriceable { + offer: offer, + unpriced: UnpricedInput { + axis: AvailabilityAxis, + obligation: "unreachable: a priced axis was routed to the refusal renderer", + }, + } + } +} + +fn axis_amount(a: PricedAxis) -> MoneyAmountMicro { + match a { + AxisPriced { amount: m } => m + AxisUnpriced { unpriced: _ } => money_amount_micro(count: 0) + AxisWrongCurrency { mismatch: _ } => money_amount_micro(count: 0) + } +} + +// THE OUTCOME CARRIES THE WHOLE CONSIDERED POPULATION, INCLUDING ON THE WINNING ARM. +// +// A selector that returned only its winner would make every decision unauditable at the +// moment it is taken: nobody could tell whether the second-cheapest lost by a cent or was +// refused for an unread measurement, and the deficits behind the refusals would have a +// frequency of zero by construction. Carrying the losers is what makes "we could not price +// four of the six offers" a countable fact rather than an invisible one. +// +// NoAdmissibleOffer is a refusal and never a degraded path. supply.dag is explicit that +// budget exhausted, every ask above the ceiling, and nothing fungible all end in a typed +// refusal or a demand that stays pending, and that there is no arm to reach for something +// cheaper and unsuitable. There is none here. +type SupplySelection

+ = OfferSelected { + offer: SupplierOffer

, + cost: SelectionCost, + basis: CostBasis, + considered: List>, + } + | NoAdmissibleOffer { considered: List> } + +type SelectionRanking

{ + best: CandidateOffer

? + best_cost: SelectionCost? + best_basis: CostBasis? + considered: List> +} + +// TIE-BREAK, STATED HONESTLY. The comparison is strictly less-than, so the EARLIEST candidate +// at the minimum wins and a re-run over the same roster in the same order returns the same +// offer. That is stability with respect to the roster, which is all this fold can offer: +// making the tie-break independent of roster order needs a total order over OfferKey, and +// OfferKey is branded over NonEmptyStr, which std carries no ordering for. NEXT-RUNG TRIGGER: +// a string ordering in std, at which point the tie-break becomes the lexicographically +// smallest key and this note is deleted rather than amended. +fn rank_candidate

(acc: SelectionRanking

, candidacy: OfferCandidacy

, candidate: CandidateOffer

) -> SelectionRanking

{ + let considered = concat(acc.considered, [candidacy]) + match candidacy { + OfferPriced { offer: _, cost: c, basis: b } => + match acc.best_cost { + Absent => + SelectionRanking { + best: Present { value: candidate }, + best_cost: Present { value: c }, + best_basis: Present { value: b }, + considered: considered, + } + Present { value: incumbent } => + if money_amount_micro_count(m: selection_total(c: c)) + < money_amount_micro_count(m: selection_total(c: incumbent)) { + SelectionRanking { + best: Present { value: candidate }, + best_cost: Present { value: c }, + best_basis: Present { value: b }, + considered: considered, + } + } else { + SelectionRanking { + best: acc.best, + best_cost: acc.best_cost, + best_basis: acc.best_basis, + considered: considered, + } + } + } + OfferNotFungible { offer: _, fungibility: _ } => carry_ranking(acc: acc, considered: considered) + OfferUnavailableForGrant { offer: _, needed: _, available: _ } => carry_ranking(acc: acc, considered: considered) + OfferNotPriceable { offer: _, unpriced: _ } => carry_ranking(acc: acc, considered: considered) + OfferWrongCurrency { offer: _, mismatch: _ } => carry_ranking(acc: acc, considered: considered) + OfferUnaffordable { offer: _, affordability: _ } => carry_ranking(acc: acc, considered: considered) + OfferLiabilityOverBuyOrder { offer: _, liability: _, maximum: _ } => carry_ranking(acc: acc, considered: considered) + } +} + +fn carry_ranking

(acc: SelectionRanking

, considered: List>) -> SelectionRanking

{ + SelectionRanking { + best: acc.best, + best_cost: acc.best_cost, + best_basis: acc.best_basis, + considered: considered, + } +} + +// THE SELECTOR. Cheapest fungible with delay valuation, which is the one instantiation +// SupplySelectionPolicy names, over a roster of offers each sealed to the six observations +// that describe it. +// +// Rented supply losing to owned supply is a CONSEQUENCE here and never an arm. Nothing in +// this fold knows which offers are ours: owned capacity wins while its total is the lowest +// total, and when it is not -- because it is committed, because its operating cost is real, +// because the alternative use it displaces is worth more than the rent -- the same +// arithmetic picks somebody else's machine, with the receipt saying why. +fn select_supply

( + requirements: ExecutionRequirements, + candidates: List>, + duration: GrantDuration, + maximum_buy_order: MoneyAmountMicro, + budget_currency: CurrencyCode, + delay_valuation: MoneyPerSecond, +) -> SupplySelection

{ + let ranked = fold( + candidates, + init: SelectionRanking { best: none, best_cost: none, best_basis: none, considered: [] }, + f: (acc, candidate) => + rank_candidate( + acc: acc, + candidacy: price_candidate( + requirements: requirements, + candidate: candidate, + duration: duration, + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + delay_valuation: delay_valuation, + ), + candidate: candidate, + ), + ) + match ranked.best { + Absent => NoAdmissibleOffer { considered: ranked.considered } + Present { value: winner } => + match ranked.best_cost { + Absent => NoAdmissibleOffer { considered: ranked.considered } + Present { value: c } => + match ranked.best_basis { + Absent => NoAdmissibleOffer { considered: ranked.considered } + Present { value: b } => + OfferSelected { + offer: winner.offer, + cost: c, + basis: b, + considered: ranked.considered, + } + } + } + } +} diff --git a/dag/product/fabric/supply.dag b/dag/product/fabric/supply.dag index fd95ac5fb34..0bfbf683c37 100644 --- a/dag/product/fabric/supply.dag +++ b/dag/product/fabric/supply.dag @@ -135,17 +135,37 @@ type SupplierOffer

{ // an auction: suppliers publish asks, each demand carries a buyer reservation // price, and the fabric takes the cheapest admissible ask. No clearing-price // machinery exists because none is needed to make that decision. -// Rented supply being selected only after owned supply is exhausted is a -// CONSEQUENCE of cheapest-admissible-ask, never a fallback arm, and the -// distinction decides whether cost can run away. A fallback is a failure arm -// that widens when the preferred path is unavailable, and it hides the deficit -// that triggered it. Here nothing widens: owned capacity at an explicit zero -// quote is simply the cheapest admissible ask while any remains, and when none -// remains the same fold evaluates the same predicate over the remaining asks. +// WHICHEVER MODE IS SELECTED IS A CONSEQUENCE of cheapest-admissible-ask, +// never a fallback arm, and the distinction decides whether cost can run away. +// A fallback is a failure arm that widens when the preferred path is +// unavailable, and it hides the deficit that triggered it. Here nothing widens: +// one fold evaluates one predicate over every ask, and when the cheapest one +// stops being cheapest the same fold picks the next without changing shape. // If no ask is admissible - budget exhausted, every ask above the reservation // price or the maximum buy order, no offer fungible - the outcome is a typed // refusal or a demand that stays pending. It is never a degraded execution // path, and there is no arm to reach for one. +// +// AN EARLIER REVISION OF THIS PARAGRAPH STATED THE CONSEQUENCE AS A FIXED +// ORDER -- rented supply selected only after owned supply is exhausted, owned +// capacity at an explicit zero quote being simply the cheapest admissible ask +// while any remains -- and that is FALSE, ruled so by the operator on +// 2026-08-27. A rented hour already bought has had its cash spent +// unrecoverably, so its marginal cost over the remainder of that hour is zero, +// while owned metal is never zero over the same interval because power and +// cooling are incurred by the act of running. The already-paid rented hour is +// therefore strictly cheaper at the margin and must WIN, leaving our own +// machine idle. The old sentence would have had a reader conclude the opposite +// from this very file. +// +// The error is worth keeping visible because of its shape rather than its +// content: it took a PREDICTION about how the numbers usually come out -- that +// owned compute will turn out to be the most profitable arbitrage most of the +// time -- and wrote it down as a PROPERTY of the fold. A selector shaped to +// reach a conclusion is not ranking, and the zero quote it leaned on is an +// asking price rather than a running cost, which is precisely the confusion the +// explicit-zero rule above exists to prevent. What is actually a consequence is +// only this: nothing in the ranking knows which offers are ours. type SupplySelectionPolicy = | CheapestFungibleWithDelayValuation @@ -460,19 +480,41 @@ fn offer_quoted_total_for_grant( // HISTORICAL PURCHASE is sunk, and sunk cost is excluded from dispatch while // being retained for accounting. // -// WHAT SELECTION WOULD NEED, none of which exists here: the offer's availability -// interval, the SUPPLIER'S OWN tariff (offer_quoted_total_for_grant's -// billed_unit_count is a synthetic one-hour quantum for the affordability -// screen, not a per-supplier tariff row), the paid-through commitment state -// that makes an already-bought hour's marginal cash zero until it expires, -// transition and start costs, and the alternative uses that give an -// opportunity cost its value -- including the arm where there is NO feasible -// alternative use, so an hour about to expire idle is not priced as though a -// customer had been displaced, and the arm where the alternative is simply -// UNREAD, so unknown does not silently become zero. +// WHAT SELECTION NEEDS BEYOND THIS FOLD, AND WHERE EACH OF IT NOW LIVES. +// +// This paragraph used to be a list of six things that existed nowhere, ending +// in the sentence "until those land, this fold is honest as a screen and would +// be a lie as a selector". Both halves have been overtaken, in two separate +// ways that are worth keeping apart. +// +// ONE OF THE SIX HAD ALREADY LANDED WHEN THE LIST STILL CLAIMED IT HAD NOT. +// The entry read that offer_quoted_total_for_grant's billed_unit_count is a +// synthetic one-hour quantum rather than a per-supplier tariff row. That +// stopped being true when billing_quantum landed as a field on this very +// carrier: the fold reads offer.billing_quantum, the declaration a few lines +// up says in terms that the quantum is now the supplier's own, and this +// sentence sat directly beneath it saying the opposite. A stale claim inside +// the authority that owns the fact is worse than an absent one, because a +// reader who checks it here stops looking. +// +// THE OTHER FIVE ARE MODELED IN product.fabric.selection, one named +// observation each -- the availability interval, the paid-through commitment +// state, the start cost, the transition cost and the alternative uses that +// give an opportunity cost its value, the last carrying both arms this list +// asked for: no feasible alternative use, so an hour about to expire idle is +// not priced as though a customer had been displaced, and the alternative +// simply UNREAD, so unknown does not silently become zero. A sixth axis joins +// them there that this list did not name and should have: the marginal +// operating cost, which is what an owned host's explicit zero quote does NOT +// state and what a rented offer's published rate already contains. // -// Until those land, this fold is honest as a screen and would be a lie as a -// selector. +// SO THIS FOLD IS STILL A SCREEN, and that is now a division of labour rather +// than a limitation. It answers whether a demand can pay one offer's asking +// price; product.fabric.selection composes it with the fungibility screen above +// and the six observations to rank offers against each other. Selection lives +// there and not here because a supply row may not import a buyer's delay +// valuation or a commitment ledger without becoming the place economics live, +// which is the boundary this header already draws. // The budget ceiling arrives as SCALARS rather than as demand.BuyTerms, because // supply must not import demand -- a backward edge this module already carries diff --git a/dag/test/claim/fabric_selection_witness_test.dag b/dag/test/claim/fabric_selection_witness_test.dag new file mode 100644 index 00000000000..b0db36ed16b --- /dev/null +++ b/dag/test/claim/fabric_selection_witness_test.dag @@ -0,0 +1,484 @@ +module test.claim.fabric_selection_witness + +import std.logic { Bool } +import std.nat { Nat } +import std.types { NonEmptyStr, List } +import std.measure { + MoneyAmountMicro, MoneyRate, MoneyPerSecond, money_amount_micro, money_amount_micro_count, + Second, second, + HardwareThreadCount, hardware_thread_count, +} +import std.currency { Usd } +import product.fabric.identity { FabricIdentity, OfferKey } +import product.fabric.work { + Shape, HardRequirements, ExecutionRequirements, CapabilityManifestRef, TrustDomainRef, +} +import product.fabric.envelope { unstated_envelope } +import product.fabric.isolation { IsolationProfile } +import product.fabric.supply { + SupplierOffer, OfferQuote, QuotedPerSecond, QuotedPerHour, ObservedSupply, + GrantDuration, ExactGrantDuration, +} +import product.fabric.selection { + CandidateOffer, DispatchInputs, bind_candidate, CandidateBound, DispatchInputsNameAnotherOffer, + AvailabilityObservation, AvailableForInterval, AvailabilityUnobserved, + CommitmentState, PayAsYouGo, PaidThrough, + StartCostObservation, StartCostMeasured, + TransitionCostObservation, NoTransitionRequired, + OperatingCostObservation, OperatingCostMeasured, OperatingCostBorneBySupplier, + OpportunityCostObservation, NoFeasibleAlternativeUse, AlternativeUseDisplaced, AlternativeUseUnread, + OfferCandidacy, OfferPriced, OfferNotPriceable, OfferUnavailableForGrant, + SupplySelection, OfferSelected, NoAdmissibleOffer, + SelectionCost, selection_total, selection_liability, + select_supply, OpportunityCostAxis, AvailabilityAxis, SelectionInputAxis, +} + +data live_tree_disposition: LiveTreeDisposition = SubstrateInputsOnly + +// THE SUBJECT IS A CROSS-MODE RANKING, AND EVERY WITNESS BELOW IS A PAIR OF OFFERS THAT +// DIFFER IN ONE OBSERVED FACT. +// +// A suite asserting that some offer can be selected would establish nothing that was in +// doubt. What is in doubt is that the SAME two offers change places when one input moves -- +// that the ranking is arithmetic over the six observations and not a preference for a mode. +// So the fixtures below are one owned continuous offer and one hourly rented offer, held +// fixed, with a single field varying per witness. +// +// The operator's falsifier is the first witness: a Hetzner hour we have already paid for +// beats our own metal, because the cash for that hour is spent and unrecoverable while power +// and cooling for our own machine are incurred by the act of running. If this file could not +// express that, the model would be wrong. + +fn t_caps() -> CapabilityManifestRef { + "gunbc.capabilities.linux-rust-toolchain" +} + +fn t_trust() -> TrustDomainRef { + "gunbc.trust.internal-fleet" +} + +fn t_open_isolation() -> IsolationProfile { + IsolationProfile { guarantees: [] } +} + +fn t_shape(threads: HardwareThreadCount) -> Shape { + Shape { hard: HardRequirements { threads: threads }, envelope: unstated_envelope() } +} + +fn t_requirements() -> ExecutionRequirements { + ExecutionRequirements { + shape: t_shape(threads: hardware_thread_count(count: 8)), + capabilities: t_caps(), + trust_domain: t_trust(), + isolation: t_open_isolation(), + } +} + +fn t_offer(key: OfferKey, quote: OfferQuote, quantum: Second) -> SupplierOffer { + SupplierOffer { + id: FabricIdentity { principal: "gunbc", key: key }, + executor: "gunbc", + shape: t_shape(threads: hardware_thread_count(count: 64)), + capabilities: t_caps(), + trust_domain: t_trust(), + isolation: t_open_isolation(), + quantity_bound: 1, + ready_at: "2026-08-27T00:00:00Z", + quote: quote, + billing_quantum: quantum, + evidence: ObservedSupply { + observed_at: "2026-08-27T00:00:00Z", + receipt: "gunbc.observation.fleet-1", + }, + } +} + +// OUR OWN METAL. Continuous, so no quantum rounds its bill up, and an EXPLICIT ZERO asking +// price because we do not invoice ourselves. Its operating cost is NOT zero: a hundred micros +// a second of power and cooling, incurred by running. +fn t_owned_offer() -> SupplierOffer { + t_offer( + key: "offer-owned-srv1", + quote: QuotedPerSecond(MoneyRate { amount: money_amount_micro(count: 0), currency: Usd }), + quantum: second(count: 0), + ) +} + +// A RENTED HOURLY MACHINE. One-hour quantum, ten thousand micros an hour, and its power is +// the vendor's problem and already inside that rate. +fn t_rented_offer() -> SupplierOffer { + t_offer( + key: "offer-rented-hourly", + quote: QuotedPerHour(MoneyRate { amount: money_amount_micro(count: 10000), currency: Usd }), + quantum: second(count: 3600), + ) +} + +fn t_inputs( + key: OfferKey, + availability: AvailabilityObservation, + commitment: CommitmentState, + operating: OperatingCostObservation, + opportunity: OpportunityCostObservation, +) -> DispatchInputs { + DispatchInputs { + offer: FabricIdentity { principal: "gunbc", key: key }, + availability: availability, + commitment: commitment, + start: StartCostMeasured { + amount: MoneyRate { amount: money_amount_micro(count: 0), currency: Usd }, + receipt: "gunbc.observation.start-cost-1", + }, + transition: NoTransitionRequired, + operating: operating, + opportunity: opportunity, + } +} + +fn t_ready_now() -> AvailabilityObservation { + AvailableForInterval { + delay: second(count: 0), + interval: second(count: 86400), + receipt: "gunbc.observation.availability-1", + } +} + +fn t_idle() -> OpportunityCostObservation { + NoFeasibleAlternativeUse { receipt: "gunbc.observation.alternative-uses-1" } +} + +fn t_owned_operating() -> OperatingCostObservation { + OperatingCostMeasured { + rate: MoneyRate { amount: money_amount_micro(count: 100), currency: Usd }, + receipt: "gunbc.observation.power-1", + } +} + +fn t_owned_candidate(commitment: CommitmentState, opportunity: OpportunityCostObservation) -> List> { + t_bound( + offer: t_owned_offer(), + inputs: t_inputs( + key: "offer-owned-srv1", + availability: t_ready_now(), + commitment: commitment, + operating: t_owned_operating(), + opportunity: opportunity, + ), + ) +} + +fn t_rented_candidate( + availability: AvailabilityObservation, + commitment: CommitmentState, + opportunity: OpportunityCostObservation, +) -> List> { + t_bound( + offer: t_rented_offer(), + inputs: t_inputs( + key: "offer-rented-hourly", + availability: availability, + commitment: commitment, + operating: OperatingCostBorneBySupplier, + opportunity: opportunity, + ), + ) +} + +// A FIXTURE HELPER MAY NOT INVENT A CANDIDATE ON THE SEAL'S REFUSAL ARM, and it may not +// silently swallow it either. There is no CandidateOffer to return when binding refuses -- +// that is the whole point of the seal -- so the helper returns a LIST, empty on refusal. A +// fixture that accidentally mismatched its keys would then present fewer offers than the +// witness names, and every witness below would go red rather than quietly ranking a shorter +// roster. Returning an unconstructable value or re-binding in a loop were the alternatives, +// and both hide the same mistake. +fn t_bound(offer: SupplierOffer, inputs: DispatchInputs) -> List> { + match bind_candidate(offer: offer, inputs: inputs) { + CandidateBound { candidate: c } => [c] + DispatchInputsNameAnotherOffer { offer: _, inputs_name: _ } => [] + } +} + +fn t_ninety_seconds() -> GrantDuration { + ExactGrantDuration { seconds: second(count: 90) } +} + +fn t_no_delay_valuation() -> MoneyPerSecond { + MoneyRate { amount: money_amount_micro(count: 0), currency: Usd } +} + +fn t_select(candidates: List>) -> SupplySelection { + select_supply( + requirements: t_requirements(), + candidates: candidates, + duration: t_ninety_seconds(), + maximum_buy_order: money_amount_micro(count: 1000000), + budget_currency: Usd, + delay_valuation: t_no_delay_valuation(), + ) +} + +fn selected_key(s: SupplySelection) -> NonEmptyStr { + match s { + OfferSelected { offer: o, cost: _, basis: _, considered: _ } => (o.id.key as NonEmptyStr) + NoAdmissibleOffer { considered: _ } => "none" + } +} + +fn selected_total(s: SupplySelection) -> Nat { + match s { + OfferSelected { offer: _, cost: c, basis: _, considered: _ } => + money_amount_micro_count(m: selection_total(c: c)) + NoAdmissibleOffer { considered: _ } => 0 + } +} + +fn considered_of(s: SupplySelection) -> List> { + match s { + OfferSelected { offer: _, cost: _, basis: _, considered: cs } => cs + NoAdmissibleOffer { considered: cs } => cs + } +} + +fn unpriced_axes(cs: List>) -> List { + fold(cs, init: [], f: (acc, c) => + match c { + OfferNotPriceable { offer: _, unpriced: u } => concat(acc, [u.axis]) + OfferPriced { offer: _, cost: _, basis: _ } => acc + OfferNotFungible { offer: _, fungibility: _ } => acc + OfferUnavailableForGrant { offer: _, needed: _, available: _ } => acc + OfferWrongCurrency { offer: _, mismatch: _ } => acc + OfferUnaffordable { offer: _, affordability: _ } => acc + OfferLiabilityOverBuyOrder { offer: _, liability: _, maximum: _ } => acc + }) +} + +fn unavailable_count(cs: List>) -> Nat { + fold(cs, init: 0, f: (n, c) => + match c { + OfferUnavailableForGrant { offer: _, needed: _, available: _ } => n + 1 + OfferPriced { offer: _, cost: _, basis: _ } => n + OfferNotFungible { offer: _, fungibility: _ } => n + OfferNotPriceable { offer: _, unpriced: _ } => n + OfferWrongCurrency { offer: _, mismatch: _ } => n + OfferUnaffordable { offer: _, affordability: _ } => n + OfferLiabilityOverBuyOrder { offer: _, liability: _, maximum: _ } => n + }) +} + +// THE OPERATOR'S FALSIFIER, AND IT IS THE FIRST WITNESS FOR A REASON. +// +// A Hetzner hour bought forty minutes ago has forty minutes left on it. The cash is spent and +// nothing we do recovers it, so the marginal cost of running a ninety-second job inside that +// remainder is ZERO. Our own metal is not zero over the same ninety seconds: power and +// cooling are incurred by running it. The rented machine therefore WINS, and the fold must +// reach that by arithmetic rather than by any rule about who owns what. +// +// Nothing in the selector knows which of these two offers is ours. There is no mode tag, no +// owned arm and no sunk-cost flag: the only things that differ are the commitment interval, +// the quantum and who bears the operating cost, all of them facts either offer could carry. +test fn a_paid_through_rented_hour_beats_our_own_metal() -> Bool { + let selection = t_select(candidates: concat( + t_owned_candidate(commitment: PayAsYouGo, opportunity: t_idle()), + t_rented_candidate( + availability: t_ready_now(), + commitment: PaidThrough { + remaining: second(count: 2400), + receipt: "gunbc.observation.hetzner-hour-1", + }, + opportunity: t_idle(), + ), + )) + selected_key(s: selection) == "offer-rented-hourly" && selected_total(s: selection) == 0 +} + +// THE SYMMETRIC ERROR, AND THE SAME TWO OFFERS SAY IT. +// +// Sunk is not free-forever. The hour expires in forty seconds, and a ninety-second job runs +// fifty seconds past the boundary -- which on a one-hour quantum buys a WHOLE SECOND HOUR, +// the exact wasted-remainder case the billing quantum exists to price. So the winner flips +// back to our own metal, and it flips on ONE field: the remaining commitment. A selector that +// treated a rented machine as free because it had once been paid for would still pick it +// here, and would quietly buy an hour to run a ninety-second job. +test fn an_expiring_commitment_buys_the_next_unit_and_stops_winning() -> Bool { + let selection = t_select(candidates: concat( + t_owned_candidate(commitment: PayAsYouGo, opportunity: t_idle()), + t_rented_candidate( + availability: t_ready_now(), + commitment: PaidThrough { + remaining: second(count: 40), + receipt: "gunbc.observation.hetzner-hour-2", + }, + opportunity: t_idle(), + ), + )) + selected_key(s: selection) == "offer-owned-srv1" && selected_total(s: selection) == 9000 +} + +// OPERATING COST IS NOT THE QUOTE, AND THIS IS THE INPUT THAT PROVES IT. +// +// Our own metal quotes an explicit zero. If the fold read that zero as the cost of running +// the machine, its total would be zero and it would win the witness above rather than losing +// it, and it would win every comparison it ever entered. The total it actually carries is +// nine thousand micros -- ninety seconds of power at a hundred micros a second -- which is +// cash the quote never mentioned. +test fn an_explicit_zero_quote_is_not_a_zero_running_cost() -> Bool { + let selection = t_select( + candidates: t_owned_candidate(commitment: PayAsYouGo, opportunity: t_idle()), + ) + selected_key(s: selection) == "offer-owned-srv1" + && selected_total(s: selection) == 9000 + && money_amount_micro_count(m: money_amount_micro(count: 0)) == 0 +} + +// AN IDLE HOUR DISPLACED NOBODY, AND AN HOUR SOMEONE WOULD HAVE PAID FOR DID. +// +// Same paid-through rented hour that won the first witness, with one field moved: the +// alternative use it displaces is now worth twenty thousand micros. Its total stops being +// zero and our own metal wins. That is the sixth axis doing real work rather than being +// carried and ignored -- and it is why a paid-through hour is only free when nothing else +// wanted it. +test fn an_alternative_use_displaced_prices_a_paid_through_hour_above_zero() -> Bool { + let selection = t_select(candidates: concat( + t_owned_candidate(commitment: PayAsYouGo, opportunity: t_idle()), + t_rented_candidate( + availability: t_ready_now(), + commitment: PaidThrough { + remaining: second(count: 2400), + receipt: "gunbc.observation.hetzner-hour-3", + }, + opportunity: AlternativeUseDisplaced { + forgone: MoneyRate { amount: money_amount_micro(count: 20000), currency: Usd }, + receipt: "gunbc.observation.alternative-uses-2", + }, + ), + )) + selected_key(s: selection) == "offer-owned-srv1" +} + +// UNREAD IS NOT ZERO, AND THE DISTINCTION IS THE WHOLE POINT OF THE ARM. +// +// The winning offer from the first witness, with its alternative uses UNREAD rather than +// observed to be none. It does not become free and it does not become expensive: it becomes +// UNPRICEABLE, refused by name on the opportunity-cost axis, and the selection falls to our +// own metal. If the unread arm defaulted to zero this witness would return the rented offer +// and be indistinguishable from the first one. +test fn an_unread_alternative_use_refuses_rather_than_pricing_at_zero() -> Bool { + let selection = t_select(candidates: concat( + t_owned_candidate(commitment: PayAsYouGo, opportunity: t_idle()), + t_rented_candidate( + availability: t_ready_now(), + commitment: PaidThrough { + remaining: second(count: 2400), + receipt: "gunbc.observation.hetzner-hour-4", + }, + opportunity: AlternativeUseUnread { + obligation: "Alternative uses for this rented hour have not been read.", + }, + ), + )) + selected_key(s: selection) == "offer-owned-srv1" + && unpriced_axes(cs: considered_of(s: selection)).length() == 1 +} + +// AN UNOBSERVED AVAILABILITY INTERVAL REFUSES ON ITS OWN AXIS, not on the opportunity one. +// Two unread axes on two offers must not report as one cause, which is what makes the refusal +// population countable per measurement rather than as a single blocked total. +test fn an_unobserved_availability_refuses_on_the_availability_axis() -> Bool { + let selection = t_select(candidates: concat( + t_owned_candidate(commitment: PayAsYouGo, opportunity: t_idle()), + t_rented_candidate( + availability: AvailabilityUnobserved { + obligation: "Provisioning delay for this supplier has not been measured.", + }, + commitment: PayAsYouGo, + opportunity: t_idle(), + ), + )) + selected_key(s: selection) == "offer-owned-srv1" + && unpriced_axes(cs: considered_of(s: selection)).length() == 1 +} + +// AN OFFER THAT EXPIRES BEFORE THE GRANT ENDS IS A CAPABILITY REFUSAL, NOT A PRICE ONE. +// +// The rented machine is available for thirty seconds and the job needs ninety. That is not +// expensive, it is impossible, and it is reported as its own arm above the money screens -- +// so a broker reading the receipt is told to find capacity rather than to raise its ceiling. +test fn an_interval_shorter_than_the_grant_refuses_before_any_price_is_compared() -> Bool { + let selection = t_select(candidates: t_rented_candidate( + availability: AvailableForInterval { + delay: second(count: 0), + interval: second(count: 30), + receipt: "gunbc.observation.availability-2", + }, + commitment: PayAsYouGo, + opportunity: t_idle(), + )) + unavailable_count(cs: considered_of(s: selection)) == 1 + && selected_key(s: selection) == "none" +} + +// A ROSTER WHOSE EVERY MEMBER REFUSES RETURNS A REFUSAL CARRYING ALL OF THEM, never a +// degraded pick. The considered list is the receipt: nothing was selected AND here is one +// typed cause per offer. +test fn nothing_admissible_returns_every_typed_cause_and_selects_nothing() -> Bool { + let selection = t_select(candidates: t_rented_candidate( + availability: AvailabilityUnobserved { obligation: "delay unmeasured" }, + commitment: PayAsYouGo, + opportunity: t_idle(), + )) + match selection { + NoAdmissibleOffer { considered: cs } => cs.length() == 1 + OfferSelected { offer: _, cost: _, basis: _, considered: _ } => false + } +} + +// THE SEAL. Observations naming a different offer refuse at the only place a pair can be +// formed, so pricing offer A with offer B's measurements has no spelling downstream. +test fn dispatch_inputs_naming_another_offer_refuse_to_bind() -> Bool { + match bind_candidate( + offer: t_owned_offer(), + inputs: t_inputs( + key: "offer-rented-hourly", + availability: t_ready_now(), + commitment: PayAsYouGo, + operating: OperatingCostBorneBySupplier, + opportunity: t_idle(), + ), + ) { + DispatchInputsNameAnotherOffer { offer: _, inputs_name: n } => (n.key as NonEmptyStr) == "offer-rented-hourly" + CandidateBound { candidate: _ } => false + } +} + +// LIABILITY AND COMPARISON COST ARE DIFFERENT NUMBERS, and demand.dag requires that they stay +// different. The valued delay of a supplier that starts in sixty seconds enters the ranking +// and must NOT enter the amount reserved: reserving it would reserve money nobody is paid. +test fn valued_delay_enters_the_ranking_and_not_the_reservation() -> Bool { + let selection = select_supply( + requirements: t_requirements(), + candidates: t_bound( + offer: t_owned_offer(), + inputs: t_inputs( + key: "offer-owned-srv1", + availability: AvailableForInterval { + delay: second(count: 60), + interval: second(count: 86400), + receipt: "gunbc.observation.availability-3", + }, + commitment: PayAsYouGo, + operating: t_owned_operating(), + opportunity: t_idle(), + ), + ), + duration: t_ninety_seconds(), + maximum_buy_order: money_amount_micro(count: 1000000), + budget_currency: Usd, + delay_valuation: MoneyRate { amount: money_amount_micro(count: 5), currency: Usd }, + ) + match selection { + OfferSelected { offer: _, cost: c, basis: _, considered: _ } => + money_amount_micro_count(m: selection_liability(c: c)) == 9000 + && money_amount_micro_count(m: selection_total(c: c)) == 9300 + NoAdmissibleOffer { considered: _ } => false + } +} From e5f7641880a963d34574861f784fa63fc6e5dec7 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 03:10:17 +0000 Subject: [PATCH 2/7] wip2 --- dag/product/fabric/selection.dag | 31 ++++++++++++------- .../claim/fabric_selection_witness_test.dag | 4 +-- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/dag/product/fabric/selection.dag b/dag/product/fabric/selection.dag index a870a940a6d..5d0f0557d04 100644 --- a/dag/product/fabric/selection.dag +++ b/dag/product/fabric/selection.dag @@ -7,6 +7,7 @@ import std.measure { Second, second, second_count, } import std.currency { CurrencyCode } +import std.nat { Nat, nat_min } import product.fabric.identity { FabricIdentity, OfferKey, fabric_identity_eq } import product.fabric.demand { ObservationReceiptRef } import product.fabric.work { ExecutionRequirements } @@ -280,12 +281,13 @@ fn cost_basis_for(duration: GrantDuration) -> CostBasis { // Nat subtraction with a floor at zero, written once. Every use below is a duration remainder // where the negative case means "already covered", never a defect: a commitment extending // past the grant leaves zero billable seconds, not minus some. +// +// It takes at most what is there rather than branching on whether it can. The guarded form was +// written first and could not be typed without a Nat zero literal, which std does not carry; +// clamping the SUBTRAHEND removes both the branch and the need for one, and an unreachable +// negative case stops being something a reader has to check the guard for. fn saturating_difference(from: Nat, take: Nat) -> Nat { - if take >= from { - 0 - } else { - from - take - } + from - nat_min(a: from, b: take) } // AN UNREAD AXIS IS A TYPED, LOCATED, COUNTABLE REFUSAL rather than a zero entering a sum. @@ -417,14 +419,21 @@ fn quote_bills_time(q: OfferQuote) -> Bool { } } -fn billable_duration(duration: GrantDuration, commitment: CommitmentState, quote: OfferQuote) -> GrantDuration { - let full = second_count(s: grant_duration_seconds(d: duration)) - let covered = match commitment { - PayAsYouGo => 0 - CommitmentUnread { obligation: _ } => 0 +// The interval a commitment already covers, which is zero unless the quote bills TIME and an +// interval is actually paid through. Every arm answers in Second rather than in a count, so the +// two zeros are durations rather than bare numbers standing in for one. +fn covered_interval(commitment: CommitmentState, quote: OfferQuote) -> Second { + match commitment { + PayAsYouGo => second(count: 0) + CommitmentUnread { obligation: _ } => second(count: 0) PaidThrough { remaining: r, receipt: _ } => - if quote_bills_time(q: quote) { second_count(s: r) } else { 0 } + if quote_bills_time(q: quote) { r } else { second(count: 0) } } +} + +fn billable_duration(duration: GrantDuration, commitment: CommitmentState, quote: OfferQuote) -> GrantDuration { + let full = second_count(s: grant_duration_seconds(d: duration)) + let covered = second_count(s: covered_interval(commitment: commitment, quote: quote)) let remaining = second(count: saturating_difference(from: full, take: covered)) match duration { ExactGrantDuration { seconds: _ } => ExactGrantDuration { seconds: remaining } diff --git a/dag/test/claim/fabric_selection_witness_test.dag b/dag/test/claim/fabric_selection_witness_test.dag index b0db36ed16b..ad2174dc67e 100644 --- a/dag/test/claim/fabric_selection_witness_test.dag +++ b/dag/test/claim/fabric_selection_witness_test.dag @@ -192,7 +192,7 @@ fn t_rented_candidate( // roster. Returning an unconstructable value or re-binding in a loop were the alternatives, // and both hide the same mistake. fn t_bound(offer: SupplierOffer, inputs: DispatchInputs) -> List> { - match bind_candidate(offer: offer, inputs: inputs) { + match bind_candidate(offer: offer, dispatch: inputs) { CandidateBound { candidate: c } => [c] DispatchInputsNameAnotherOffer { offer: _, inputs_name: _ } => [] } @@ -437,7 +437,7 @@ test fn nothing_admissible_returns_every_typed_cause_and_selects_nothing() -> Bo test fn dispatch_inputs_naming_another_offer_refuse_to_bind() -> Bool { match bind_candidate( offer: t_owned_offer(), - inputs: t_inputs( + dispatch: t_inputs( key: "offer-rented-hourly", availability: t_ready_now(), commitment: PayAsYouGo, From 2b990d2889600eb84a776e389a5b66c834cd80b8 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 03:18:46 +0000 Subject: [PATCH 3/7] wip3 --- dag/test/claim/fabric_selection_witness_test.dag | 1 + 1 file changed, 1 insertion(+) diff --git a/dag/test/claim/fabric_selection_witness_test.dag b/dag/test/claim/fabric_selection_witness_test.dag index ad2174dc67e..fe9ad14203b 100644 --- a/dag/test/claim/fabric_selection_witness_test.dag +++ b/dag/test/claim/fabric_selection_witness_test.dag @@ -1,6 +1,7 @@ module test.claim.fabric_selection_witness import std.logic { Bool } +import v2.std.live_tree { LiveTreeDisposition, SubstrateInputsOnly } import std.nat { Nat } import std.types { NonEmptyStr, List } import std.measure { From a7eba0ff1d87cf765e6231bf00e812c2fbfef0ae Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 03:50:21 +0000 Subject: [PATCH 4/7] wip4: duration state, commitment horizon --- dag/product/fabric/selection.dag | 430 ++++++++++++------ dag/product/fabric/supply.dag | 158 +++++-- .../claim/fabric_selection_witness_test.dag | 207 ++++++++- 3 files changed, 622 insertions(+), 173 deletions(-) diff --git a/dag/product/fabric/selection.dag b/dag/product/fabric/selection.dag index 5d0f0557d04..baf8dd789a0 100644 --- a/dag/product/fabric/selection.dag +++ b/dag/product/fabric/selection.dag @@ -17,8 +17,11 @@ import product.fabric.supply { IsolationNotProvided, ShapeAxesNotCovered, offer_fungibility_for, DemandOfferAffordability, Affordable, AffordableUnderEstimate, QuoteExceedsMaximumBuyOrder, QuoteCurrencyMismatch, + QuoteNotPriceableWithoutDuration, offer_affordability_for, offer_quote_currency, offer_quoted_total_for_grant, - GrantDuration, ExactGrantDuration, EstimatedGrantDuration, grant_duration_seconds, + QuotedTotal, QuotedTotalDerived, QuotedTotalNeedsDuration, + GrantDuration, ExactGrantDuration, EstimatedGrantDuration, UnobservedGrantDuration, + grant_duration_bound_seconds, } // SELECTION IS THE FOLD product.fabric.supply DECLINED TO WRITE, AND THIS MODULE IS WHY IT @@ -267,14 +270,36 @@ fn selection_total(c: SelectionCost) -> MoneyAmountMicro { // same distinction one level down with ExactGrantDuration against EstimatedGrantDuration. // The basis is derived from the duration arm rather than accepted as an argument, so a caller // cannot claim an exact basis over an estimated duration. +// CommitmentFloorCost is the third basis and it is NOT a worse projection -- it is a +// different quantity. Under an unobserved duration no total exists to project, so what the +// fold prices instead is the money the DECISION irreversibly commits: the billing units it +// cannot unbuy once the work starts. That number is a LOWER BOUND on what will be paid, never +// an estimate of it, and a consumer that read it as a total would understate every offer. +// +// The basis is uniform across one selection because it is derived from ONE duration state, so +// the ranking never compares a floor against a projection. It is carried out of the fold so +// that a caller can see which question it just had answered. type CostBasis = ExactCost | ProjectedCost + | CommitmentFloorCost -fn cost_basis_for(duration: GrantDuration) -> CostBasis { +fn cost_basis_for(duration: GrantDuration, horizon: CommitmentHorizon) -> CostBasis { + match horizon { + SpillPricedForUnobservedDuration { wall: _, billed: _, remainder: _ } => CommitmentFloorCost + CoveredByCommitment { wall: _, billed: _, remainder: _ } => bounded_cost_basis(duration: duration) + BilledBeyondCommitment { wall: _, billed: _, remainder: _ } => bounded_cost_basis(duration: duration) + } +} + +// The unobserved arm is unreachable from here -- the horizon fold above routes it to +// CommitmentFloorCost before this is called -- so it is answered rather than defaulted, and +// answered with the basis it would deserve if a route ever appeared. +fn bounded_cost_basis(duration: GrantDuration) -> CostBasis { match duration { ExactGrantDuration { seconds: _ } => ExactCost EstimatedGrantDuration { seconds: _ } => ProjectedCost + UnobservedGrantDuration { obligation: _ } => CommitmentFloorCost } } @@ -431,13 +456,108 @@ fn covered_interval(commitment: CommitmentState, quote: OfferQuote) -> Second { } } -fn billable_duration(duration: GrantDuration, commitment: CommitmentState, quote: OfferQuote) -> GrantDuration { - let full = second_count(s: grant_duration_seconds(d: duration)) - let covered = second_count(s: covered_interval(commitment: commitment, quote: quote)) - let remaining = second(count: saturating_difference(from: full, take: covered)) +// THE COMMITMENT HORIZON -- WHAT INTERVAL THIS DECISION ACTUALLY BUYS, and the reason a +// paid-through hour is not simply free. +// +// A paid interval is free only for work that FITS IN IT. The operator's case is a machine with +// seven minutes left on a bought hour and a job nobody has timed: starting there cannot be +// priced at zero, because crossing the boundary buys a WHOLE further hour that cannot be +// unbought afterwards. Pricing that spill is a decision made deliberately under uncertainty, +// not a value written into the model where a duration belongs -- the duration stays +// unobserved and says so, and what the fold computes is the price of the decision. +// +// THE THREE ARMS ARE THE THREE ANSWERS, AND THEY HAVE DIFFERENT REMEDIES. Covered means the +// bound fits inside the remainder and nothing further is billed. BilledBeyondCommitment means +// a KNOWN bound runs past the remainder -- the remedy is to choose a different offer, and with +// a remainder of zero it is simply ordinary pay-as-you-go. SpillPricedForUnobservedDuration +// means the crossing could not be RULED OUT -- the remedy is to measure. Collapsing the last +// two would tell a broker to go shopping when it should have gone measuring. +// +// AND SPILL IS NOT SPECIAL-CASED AWAY FOR CONTINUOUS CAPACITY, IT DOES NOT ARISE THERE. The +// spilled interval is ONE BILLING QUANTUM, and a continuous offer's quantum is zero, so its +// spill term is zero by the same arithmetic that gives an hourly supplier 3600 seconds. There +// is no arm testing for continuity and none is needed; if there were, it would be bolted on. +// +// WHAT THIS DELIBERATELY DOES NOT DO: substitute a worst case wherever a duration is missing. +// The spill price is computed for THIS question -- what does crossing a commitment boundary +// commit -- and the unobserved state travels onward intact, with the basis it produced saying +// which arm was taken. A model that answered every unknown with a maximum would always answer, +// never refuse, and would zero the frequency of its own estimation deficit by construction. +type CommitmentHorizon + = CoveredByCommitment { wall: Second, billed: GrantDuration, remainder: Second } + | BilledBeyondCommitment { wall: Second, billed: GrantDuration, remainder: Second } + | SpillPricedForUnobservedDuration { wall: Second, billed: GrantDuration, remainder: Second } + +fn commitment_horizon( + duration: GrantDuration, + commitment: CommitmentState, + quote: OfferQuote, + quantum: Second, +) -> CommitmentHorizon { + let remainder = covered_interval(commitment: commitment, quote: quote) + match grant_duration_bound_seconds(d: duration) { + Absent => + SpillPricedForUnobservedDuration { + wall: second(count: second_count(s: remainder) + second_count(s: quantum)), + billed: ExactGrantDuration { seconds: quantum }, + remainder: remainder, + } + Present { value: bound } => + bounded_horizon(duration: duration, bound: bound, remainder: remainder) + } +} + +// The billed interval is what is left of the bound after the commitment covers what it +// covers; the quantum rounds THAT up downstream, in the supplier's own pricing fold, which is +// where rounding already lives. +fn bounded_horizon(duration: GrantDuration, bound: Second, remainder: Second) -> CommitmentHorizon { + let beyond = saturating_difference( + from: second_count(s: bound), + take: second_count(s: remainder), + ) + if beyond == 0 { + CoveredByCommitment { + wall: bound, + billed: same_evidence_duration(duration: duration, seconds: second(count: 0)), + remainder: remainder, + } + } else { + BilledBeyondCommitment { + wall: bound, + billed: same_evidence_duration(duration: duration, seconds: second(count: beyond)), + remainder: remainder, + } + } +} + +// THE BILLED INTERVAL INHERITS THE EVIDENCE OF THE DURATION IT WAS DERIVED FROM. Subtracting a +// commitment from an ESTIMATE leaves an estimate: the remainder is exact, the bound was not, +// and the difference carries the bound's uncertainty. Rebuilding it as Exact would have made +// the affordability screen report Affordable where it should report AffordableUnderEstimate, +// which is the estimate's error vanishing the moment it is multiplied -- precisely the failure +// the two arms exist to prevent. The unobserved arm cannot be reached here, because the caller +// above routes an unobserved duration to its own horizon before this is called. +fn same_evidence_duration(duration: GrantDuration, seconds: Second) -> GrantDuration { match duration { - ExactGrantDuration { seconds: _ } => ExactGrantDuration { seconds: remaining } - EstimatedGrantDuration { seconds: _ } => EstimatedGrantDuration { seconds: remaining } + ExactGrantDuration { seconds: _ } => ExactGrantDuration { seconds: seconds } + EstimatedGrantDuration { seconds: _ } => EstimatedGrantDuration { seconds: seconds } + UnobservedGrantDuration { obligation: why } => UnobservedGrantDuration { obligation: why } + } +} + +fn horizon_billed(h: CommitmentHorizon) -> GrantDuration { + match h { + CoveredByCommitment { wall: _, billed: b, remainder: _ } => b + BilledBeyondCommitment { wall: _, billed: b, remainder: _ } => b + SpillPricedForUnobservedDuration { wall: _, billed: b, remainder: _ } => b + } +} + +fn horizon_wall(h: CommitmentHorizon) -> Second { + match h { + CoveredByCommitment { wall: w, billed: _, remainder: _ } => w + BilledBeyondCommitment { wall: w, billed: _, remainder: _ } => w + SpillPricedForUnobservedDuration { wall: w, billed: _, remainder: _ } => w } } @@ -445,7 +565,12 @@ fn billable_duration(duration: GrantDuration, commitment: CommitmentState, quote // these, so the rejected population is countable at identity grain and a broker can read WHY // each one lost rather than being told that nothing was admissible. type OfferCandidacy

- = OfferPriced { offer: FabricIdentity, cost: SelectionCost, basis: CostBasis } + = OfferPriced { + offer: FabricIdentity, + cost: SelectionCost, + basis: CostBasis, + horizon: CommitmentHorizon, + } | OfferNotFungible { offer: FabricIdentity, fungibility: OfferFungibility } | OfferUnavailableForGrant { offer: FabricIdentity, @@ -466,7 +591,7 @@ type OfferCandidacy

fn candidacy_offer

(c: OfferCandidacy

) -> FabricIdentity { match c { - OfferPriced { offer: o, cost: _, basis: _ } => o + OfferPriced { offer: o, cost: _, basis: _, horizon: _ } => o OfferNotFungible { offer: o, fungibility: _ } => o OfferUnavailableForGrant { offer: o, needed: _, available: _ } => o OfferNotPriceable { offer: o, unpriced: _ } => o @@ -518,6 +643,10 @@ fn price_candidate

( } } +// The commitment horizon is derived FIRST inside this step, because every later question -- +// how long the offer must stay available, what the quote bills, how long the machine runs -- +// is asked about the interval this decision actually buys rather than about the duration a +// caller happened to name. fn price_available_candidate

( offer: SupplierOffer

, inputs: DispatchInputs

, @@ -526,7 +655,50 @@ fn price_available_candidate

( budget_currency: CurrencyCode, delay_valuation: MoneyPerSecond, ) -> OfferCandidacy

{ - let run_seconds = second_count(s: grant_duration_seconds(d: duration)) + match inputs.commitment { + CommitmentUnread { obligation: why } => + OfferNotPriceable { + offer: offer.id, + unpriced: UnpricedInput { axis: CommitmentAxis, obligation: why }, + } + PayAsYouGo => price_over_horizon( + offer: offer, + inputs: inputs, + duration: duration, + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + delay_valuation: delay_valuation, + ) + PaidThrough { remaining: _, receipt: _ } => price_over_horizon( + offer: offer, + inputs: inputs, + duration: duration, + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + delay_valuation: delay_valuation, + ) + } +} + +// AVAILABILITY IS CHECKED AGAINST THE HORIZON'S WALL INTERVAL, not against a caller-named +// duration. Under an unobserved duration the wall is what the decision commits -- the +// remainder plus one billing unit -- so an offer that cannot hold even that much is refused +// on capability grounds before any money is compared. +fn price_over_horizon

( + offer: SupplierOffer

, + inputs: DispatchInputs

, + duration: GrantDuration, + maximum_buy_order: MoneyAmountMicro, + budget_currency: CurrencyCode, + delay_valuation: MoneyPerSecond, +) -> OfferCandidacy

{ + let horizon = commitment_horizon( + duration: duration, + commitment: inputs.commitment, + quote: offer.quote, + quantum: offer.billing_quantum, + ) + let wall = second_count(s: horizon_wall(h: horizon)) match inputs.availability { AvailabilityUnobserved { obligation: why } => OfferNotPriceable { @@ -534,17 +706,18 @@ fn price_available_candidate

( unpriced: UnpricedInput { axis: AvailabilityAxis, obligation: why }, } AvailableForInterval { delay: d, interval: window, receipt: _ } => - if second_count(s: window) < run_seconds { + if second_count(s: window) < wall { OfferUnavailableForGrant { offer: offer.id, - needed: second(count: run_seconds), + needed: second(count: wall), available: window, } } else { - price_committed_candidate( + price_screened_candidate( offer: offer, inputs: inputs, duration: duration, + horizon: horizon, delay_seconds: second_count(s: d), maximum_buy_order: maximum_buy_order, budget_currency: budget_currency, @@ -554,73 +727,46 @@ fn price_available_candidate

( } } -fn price_committed_candidate

( - offer: SupplierOffer

, - inputs: DispatchInputs

, - duration: GrantDuration, - delay_seconds: Nat, - maximum_buy_order: MoneyAmountMicro, - budget_currency: CurrencyCode, - delay_valuation: MoneyPerSecond, -) -> OfferCandidacy

{ - match inputs.commitment { - CommitmentUnread { obligation: why } => - OfferNotPriceable { - offer: offer.id, - unpriced: UnpricedInput { axis: CommitmentAxis, obligation: why }, - } - PayAsYouGo => price_screened_candidate( - offer: offer, - inputs: inputs, - duration: duration, - delay_seconds: delay_seconds, - maximum_buy_order: maximum_buy_order, - budget_currency: budget_currency, - delay_valuation: delay_valuation, - ) - PaidThrough { remaining: _, receipt: _ } => price_screened_candidate( - offer: offer, - inputs: inputs, - duration: duration, - delay_seconds: delay_seconds, - maximum_buy_order: maximum_buy_order, - budget_currency: budget_currency, - delay_valuation: delay_valuation, - ) - } -} - -// The affordability screen is supply.dag's own fold, called with the commitment-reduced -// duration rather than re-derived here. Pricing is a single authority and this module -// consumes it; a second price computed beside it would be the fork the fabric exists to -// prevent, and it would be the fork in the one place two answers are indistinguishable -// until money moves. +// The affordability screen is supply.dag's own fold, called with the horizon's BILLED interval +// rather than re-derived here. Pricing is a single authority and this module consumes it; a +// second price computed beside it would be the fork the fabric exists to prevent, and it would +// be the fork in the one place two answers are indistinguishable until money moves. +// +// QuoteNotPriceableWithoutDuration cannot be reached from here, because the horizon has +// already resolved an unobserved duration into the billing unit the decision commits. It is +// answered rather than ignored, since the screen is a shared authority and another caller can +// reach it with a raw unobserved duration. fn price_screened_candidate

( offer: SupplierOffer

, inputs: DispatchInputs

, duration: GrantDuration, + horizon: CommitmentHorizon, delay_seconds: Nat, maximum_buy_order: MoneyAmountMicro, budget_currency: CurrencyCode, delay_valuation: MoneyPerSecond, ) -> OfferCandidacy

{ - let billable = billable_duration(duration: duration, commitment: inputs.commitment, quote: offer.quote) let affordability = offer_affordability_for( offer: offer, maximum_buy_order: maximum_buy_order, budget_currency: budget_currency, - duration: billable, + duration: horizon_billed(h: horizon), ) match affordability { QuoteCurrencyMismatch { quoted: _, budgeted: _ } => OfferUnaffordable { offer: offer.id, affordability: affordability } QuoteExceedsMaximumBuyOrder { quoted: _, maximum: _ } => OfferUnaffordable { offer: offer.id, affordability: affordability } + QuoteNotPriceableWithoutDuration { obligation: why } => + OfferNotPriceable { + offer: offer.id, + unpriced: UnpricedInput { axis: CommitmentAxis, obligation: why }, + } Affordable => assemble_cost( offer: offer, inputs: inputs, duration: duration, - billable: billable, + horizon: horizon, delay_seconds: delay_seconds, maximum_buy_order: maximum_buy_order, budget_currency: budget_currency, @@ -630,7 +776,7 @@ fn price_screened_candidate

( offer: offer, inputs: inputs, duration: duration, - billable: billable, + horizon: horizon, delay_seconds: delay_seconds, maximum_buy_order: maximum_buy_order, budget_currency: budget_currency, @@ -647,18 +793,13 @@ fn assemble_cost

( offer: SupplierOffer

, inputs: DispatchInputs

, duration: GrantDuration, - billable: GrantDuration, + horizon: CommitmentHorizon, delay_seconds: Nat, maximum_buy_order: MoneyAmountMicro, budget_currency: CurrencyCode, delay_valuation: MoneyPerSecond, ) -> OfferCandidacy

{ - let run_seconds = second_count(s: grant_duration_seconds(d: duration)) - let charge = offer_quoted_total_for_grant( - quote: offer.quote, - duration: billable, - quantum: offer.billing_quantum, - ) + let run_seconds = second_count(s: horizon_wall(h: horizon)) let start = priced_start(o: inputs.start, budgeted: budget_currency) let transition = priced_transition(o: inputs.transition, budgeted: budget_currency) let operating = priced_operating( @@ -672,21 +813,34 @@ fn assemble_cost

( valuation: delay_valuation, budgeted: budget_currency, ) - match first_unpriced(axes: [start, transition, operating, opportunity, delay]) { - Present { value: bad } => bad_axis_candidacy(offer: offer.id, bad: bad) - Absent => screen_liability( - offer: offer.id, - cost: SelectionCost { - marginal_charge: charge, - start: axis_amount(a: start), - transition: axis_amount(a: transition), - operating: axis_amount(a: operating), - opportunity: axis_amount(a: opportunity), - valued_delay: axis_amount(a: delay), - }, - basis: cost_basis_for(duration: duration), - maximum_buy_order: maximum_buy_order, - ) + match offer_quoted_total_for_grant( + quote: offer.quote, + duration: horizon_billed(h: horizon), + quantum: offer.billing_quantum, + ) { + QuotedTotalNeedsDuration { obligation: why } => + OfferNotPriceable { + offer: offer.id, + unpriced: UnpricedInput { axis: CommitmentAxis, obligation: why }, + } + QuotedTotalDerived { amount: charge } => + match first_unpriced(axes: [start, transition, operating, opportunity, delay]) { + Present { value: bad } => bad_axis_candidacy(offer: offer.id, bad: bad) + Absent => screen_liability( + offer: offer.id, + cost: SelectionCost { + marginal_charge: charge, + start: axis_amount(a: start), + transition: axis_amount(a: transition), + operating: axis_amount(a: operating), + opportunity: axis_amount(a: opportunity), + valued_delay: axis_amount(a: delay), + }, + basis: cost_basis_for(duration: duration, horizon: horizon), + horizon: horizon, + maximum_buy_order: maximum_buy_order, + ) + } } } @@ -697,13 +851,14 @@ fn screen_liability

( offer: FabricIdentity, cost: SelectionCost, basis: CostBasis, + horizon: CommitmentHorizon, maximum_buy_order: MoneyAmountMicro, ) -> OfferCandidacy

{ let liability = selection_liability(c: cost) if money_amount_micro_count(m: liability) > money_amount_micro_count(m: maximum_buy_order) { OfferLiabilityOverBuyOrder { offer: offer, liability: liability, maximum: maximum_buy_order } } else { - OfferPriced { offer: offer, cost: cost, basis: basis } + OfferPriced { offer: offer, cost: cost, basis: basis, horizon: horizon } } } @@ -753,6 +908,12 @@ fn axis_amount(a: PricedAxis) -> MoneyAmountMicro { // frequency of zero by construction. Carrying the losers is what makes "we could not price // four of the six offers" a countable fact rather than an invisible one. // +// IT ALSO CARRIES THE HORIZON, so the receipt says WHICH ARM priced the winner: covered by a +// commitment already paid for, billed beyond one, or spill-priced because the duration could +// not rule the crossing out. Those have opposite remedies -- choose another offer, or go and +// measure -- and a decision that reported only its total would leave a reader unable to tell +// which one it needs. +// // NoAdmissibleOffer is a refusal and never a degraded path. supply.dag is explicit that // budget exhausted, every ask above the ceiling, and nothing fungible all end in a typed // refusal or a demand that stays pending, and that there is no arm to reach for something @@ -762,14 +923,23 @@ type SupplySelection

offer: SupplierOffer

, cost: SelectionCost, basis: CostBasis, + horizon: CommitmentHorizon, considered: List>, } | NoAdmissibleOffer { considered: List> } +// The incumbent travels as ONE value rather than as four parallel optionals that could +// disagree about which offer they describe. Four fields each separately absent has states in +// which a cost belongs to one offer and a basis to another; this has none. +type PricedCandidate

{ + candidate: CandidateOffer

+ cost: SelectionCost + basis: CostBasis + horizon: CommitmentHorizon +} + type SelectionRanking

{ - best: CandidateOffer

? - best_cost: SelectionCost? - best_basis: CostBasis? + best: PricedCandidate

? considered: List> } @@ -780,35 +950,20 @@ type SelectionRanking

{ // OfferKey is branded over NonEmptyStr, which std carries no ordering for. NEXT-RUNG TRIGGER: // a string ordering in std, at which point the tie-break becomes the lexicographically // smallest key and this note is deleted rather than amended. -fn rank_candidate

(acc: SelectionRanking

, candidacy: OfferCandidacy

, candidate: CandidateOffer

) -> SelectionRanking

{ +fn rank_candidate

( + acc: SelectionRanking

, + candidacy: OfferCandidacy

, + candidate: CandidateOffer

, +) -> SelectionRanking

{ let considered = concat(acc.considered, [candidacy]) match candidacy { - OfferPriced { offer: _, cost: c, basis: b } => - match acc.best_cost { - Absent => - SelectionRanking { - best: Present { value: candidate }, - best_cost: Present { value: c }, - best_basis: Present { value: b }, - considered: considered, - } - Present { value: incumbent } => - if money_amount_micro_count(m: selection_total(c: c)) - < money_amount_micro_count(m: selection_total(c: incumbent)) { - SelectionRanking { - best: Present { value: candidate }, - best_cost: Present { value: c }, - best_basis: Present { value: b }, - considered: considered, - } - } else { - SelectionRanking { - best: acc.best, - best_cost: acc.best_cost, - best_basis: acc.best_basis, - considered: considered, - } - } + OfferPriced { offer: _, cost: c, basis: b, horizon: h } => + SelectionRanking { + best: better_of( + incumbent: acc.best, + challenger: PricedCandidate { candidate: candidate, cost: c, basis: b, horizon: h }, + ), + considered: considered, } OfferNotFungible { offer: _, fungibility: _ } => carry_ranking(acc: acc, considered: considered) OfferUnavailableForGrant { offer: _, needed: _, available: _ } => carry_ranking(acc: acc, considered: considered) @@ -819,24 +974,32 @@ fn rank_candidate

(acc: SelectionRanking

, candidacy: OfferCandidacy

, can } } -fn carry_ranking

(acc: SelectionRanking

, considered: List>) -> SelectionRanking

{ - SelectionRanking { - best: acc.best, - best_cost: acc.best_cost, - best_basis: acc.best_basis, - considered: considered, +fn better_of

(incumbent: PricedCandidate

?, challenger: PricedCandidate

) -> PricedCandidate

? { + match incumbent { + Absent => Present { value: challenger } + Present { value: held } => + if money_amount_micro_count(m: selection_total(c: challenger.cost)) + < money_amount_micro_count(m: selection_total(c: held.cost)) { + Present { value: challenger } + } else { + Present { value: held } + } } } +fn carry_ranking

(acc: SelectionRanking

, considered: List>) -> SelectionRanking

{ + SelectionRanking { best: acc.best, considered: considered } +} + // THE SELECTOR. Cheapest fungible with delay valuation, which is the one instantiation // SupplySelectionPolicy names, over a roster of offers each sealed to the six observations // that describe it. // -// Rented supply losing to owned supply is a CONSEQUENCE here and never an arm. Nothing in -// this fold knows which offers are ours: owned capacity wins while its total is the lowest -// total, and when it is not -- because it is committed, because its operating cost is real, -// because the alternative use it displaces is worth more than the rent -- the same -// arithmetic picks somebody else's machine, with the receipt saying why. +// WHICH MODE WINS IS A CONSEQUENCE HERE AND NEVER AN ARM. Nothing in this fold knows which +// offers are ours. Owned capacity wins while its total is the lowest total, and when it is not +// -- because a rented hour is already paid for and this work fits inside it, because its +// operating cost is real, because the alternative use it displaces is worth more than the rent +// -- the same arithmetic picks somebody else's machine, with the receipt saying why. fn select_supply

( requirements: ExecutionRequirements, candidates: List>, @@ -847,7 +1010,7 @@ fn select_supply

( ) -> SupplySelection

{ let ranked = fold( candidates, - init: SelectionRanking { best: none, best_cost: none, best_basis: none, considered: [] }, + init: SelectionRanking { best: none, considered: [] }, f: (acc, candidate) => rank_candidate( acc: acc, @@ -865,19 +1028,12 @@ fn select_supply

( match ranked.best { Absent => NoAdmissibleOffer { considered: ranked.considered } Present { value: winner } => - match ranked.best_cost { - Absent => NoAdmissibleOffer { considered: ranked.considered } - Present { value: c } => - match ranked.best_basis { - Absent => NoAdmissibleOffer { considered: ranked.considered } - Present { value: b } => - OfferSelected { - offer: winner.offer, - cost: c, - basis: b, - considered: ranked.considered, - } - } + OfferSelected { + offer: winner.candidate.offer, + cost: winner.cost, + basis: winner.basis, + horizon: winner.horizon, + considered: ranked.considered, } } } diff --git a/dag/product/fabric/supply.dag b/dag/product/fabric/supply.dag index 0bfbf683c37..0634b555115 100644 --- a/dag/product/fabric/supply.dag +++ b/dag/product/fabric/supply.dag @@ -1,6 +1,6 @@ module product.fabric.supply -import std.types { Timestamp, Int, List } +import std.types { Timestamp, Int, List, NonEmptyStr } import std.measure { ByteSize, byte_size_count, @@ -231,22 +231,53 @@ type OfferFungibility // estimate's uncertainty vanish the moment it is multiplied. A caller that // only knows an estimate cannot construct ExactGrantDuration and claim // otherwise -- the two arms are the wall, not a comment beside a single field. +// A THIRD ARM, AND IT IS THE ONE THE OTHER TWO CANNOT STAND IN FOR (operator ruling, +// 2026-08-27): "the unknowns have to be represented properly in the modeling, so we can make +// actual decisions". +// +// How long a job will run is NOT KNOWN at selection time, and the two arms above both carry a +// Second -- so before this arm existed, a caller with no measurement at all had to pick one of +// them and write a number down. Whatever it wrote then read downstream as a measurement. +// "This job takes six minutes" and "nobody has ever timed this job" are different states with +// different correct decisions, and a carrier that can only spell the first has erased the +// second at the moment it is most load-bearing: a paid-through interval is free only for work +// that FITS IN IT, and whether it fits is precisely the question an invented estimate answers +// falsely. +// +// The arm carries the obligation rather than a placeholder duration, so the remedy -- go and +// measure this -- is attached to the state instead of being lost behind a plausible integer. type GrantDuration = ExactGrantDuration { seconds: Second } | EstimatedGrantDuration { seconds: Second } - -fn grant_duration_seconds(d: GrantDuration) -> Second { + | UnobservedGrantDuration { obligation: NonEmptyStr } + +// THE ACCESSOR IS PARTIAL AND IS NAMED FOR WHAT IT RETURNS, which is an upper BOUND on the +// run and not "the duration". It was called grant_duration_seconds and returned a bare +// Second; both had to change, and the rename is deliberate rather than incidental. A total +// accessor over a partial fact would have needed something to return for the unobserved arm, +// and every candidate -- zero, a default, the estimate -- is the fabrication the arm exists to +// prevent. Keeping the old NAME over the new signature would have let a caller update one +// line and carry on believing it holds a duration. +fn grant_duration_bound_seconds(d: GrantDuration) -> Second? { match d { - ExactGrantDuration { seconds: s } => s - EstimatedGrantDuration { seconds: s } => s + ExactGrantDuration { seconds: s } => Present { value: s } + EstimatedGrantDuration { seconds: s } => Present { value: s } + UnobservedGrantDuration { obligation: _ } => none } } +// QuoteNotPriceableWithoutDuration is NOT a fifth way of being unaffordable. It says the +// screen could not be RUN, which is a different fact from a demand that cannot pay -- the +// remedy is a measurement, not a bigger budget -- and it exists because a rate quote and an +// unobserved duration cannot produce a total at all. A flat per-grant quote does not reach it: +// that quote prices the grant whole, so it is answerable with no duration whatsoever, and the +// partition between the two falls out of the quote's own shape rather than a policy choice. type DemandOfferAffordability = Affordable | AffordableUnderEstimate { duration_seconds: Second, projected_total: MoneyAmountMicro } | QuoteExceedsMaximumBuyOrder { quoted: MoneyAmountMicro, maximum: MoneyAmountMicro } | QuoteCurrencyMismatch { quoted: CurrencyCode, budgeted: CurrencyCode } + | QuoteNotPriceableWithoutDuration { obligation: NonEmptyStr } fn offer_covers_shape(offered: Shape, needed: Shape) -> Bool { (unmet_shape_axes(offered: offered, needed: needed) |> count) == 0 @@ -441,20 +472,67 @@ fn billed_seconds(seconds: Nat, quantum_seconds: Nat) -> Nat { // unwrapped to counts on the next two lines because what follows is integer // ceiling arithmetic over unit counts, and a count scaled by measure_scale_fraction_ceil // is a dimensionless multiplier, not a duration. +// THE RESULT IS A COPRODUCT because one of its inputs is now partial. A rate quote times an +// unobserved duration is not a number, and the two candidates for returning one anyway -- +// zero, or a silently substituted estimate -- are the fabricated-plausible-output failure in +// the fold that decides how much money moves. A flat per-grant quote is priced regardless, +// since duration never entered its arithmetic. +type QuotedTotal + = QuotedTotalDerived { amount: MoneyAmountMicro } + | QuotedTotalNeedsDuration { obligation: NonEmptyStr } + +fn quoted_total_amount(t: QuotedTotal) -> MoneyAmountMicro? { + match t { + QuotedTotalDerived { amount: a } => Present { value: a } + QuotedTotalNeedsDuration { obligation: _ } => none + } +} + fn offer_quoted_total_for_grant( quote: OfferQuote, duration: GrantDuration, quantum: Second, -) -> MoneyAmountMicro { - let seconds = second_count(s: grant_duration_seconds(d: duration)) - let billed = billed_seconds(seconds: seconds, quantum_seconds: second_count(s: quantum)) +) -> QuotedTotal { match quote { - QuotedFlatPerGrant(r) => r.amount - QuotedPerSecond(r) => measure_scale_fraction_ceil(m: r.amount, num: billed, den: 1) + QuotedFlatPerGrant(r) => QuotedTotalDerived { amount: r.amount } + QuotedPerSecond(r) => + rate_total_for_grant(rate: r.amount, den: 1, duration: duration, quantum: quantum) QuotedPerMinute(r) => - measure_scale_fraction_ceil(m: r.amount, num: billed, den: seconds_per_minute()) + rate_total_for_grant( + rate: r.amount, + den: seconds_per_minute(), + duration: duration, + quantum: quantum, + ) QuotedPerHour(r) => - measure_scale_fraction_ceil(m: r.amount, num: billed, den: seconds_per_hour()) + rate_total_for_grant( + rate: r.amount, + den: seconds_per_hour(), + duration: duration, + quantum: quantum, + ) + } +} + +fn rate_total_for_grant( + rate: MoneyAmountMicro, + den: Nat, + duration: GrantDuration, + quantum: Second, +) -> QuotedTotal { + match grant_duration_bound_seconds(d: duration) { + Absent => + QuotedTotalNeedsDuration { + obligation: "A rate quote cannot be totalled over an unobserved grant duration. Measure the work's running time, or bound it, before asking this quote for a per-grant total.", + } + Present { value: s } => + QuotedTotalDerived { + amount: measure_scale_fraction_ceil( + m: rate, + num: billed_seconds(seconds: second_count(s: s), quantum_seconds: second_count(s: quantum)), + den: den, + ), + } } } @@ -532,21 +610,47 @@ fn offer_affordability_for

( if offer_quote_currency(q: offer.quote) != budget_currency { QuoteCurrencyMismatch { quoted: offer_quote_currency(q: offer.quote), budgeted: budget_currency } } else { - let projected = offer_quoted_total_for_grant(quote: offer.quote, duration: duration, quantum: offer.billing_quantum) - if money_amount_micro_count(m: projected) > money_amount_micro_count(m: maximum_buy_order) { - QuoteExceedsMaximumBuyOrder { quoted: projected, maximum: maximum_buy_order } - } else { - match duration { - ExactGrantDuration { seconds: _ } => Affordable - EstimatedGrantDuration { seconds: s } => match offer.quote { - QuotedFlatPerGrant(_) => Affordable - QuotedPerSecond(_) => - AffordableUnderEstimate { duration_seconds: s, projected_total: projected } - QuotedPerMinute(_) => - AffordableUnderEstimate { duration_seconds: s, projected_total: projected } - QuotedPerHour(_) => - AffordableUnderEstimate { duration_seconds: s, projected_total: projected } - } + match offer_quoted_total_for_grant( + quote: offer.quote, + duration: duration, + quantum: offer.billing_quantum, + ) { + QuotedTotalNeedsDuration { obligation: why } => + QuoteNotPriceableWithoutDuration { obligation: why } + QuotedTotalDerived { amount: projected } => + screen_projected_total( + quote: offer.quote, + duration: duration, + projected: projected, + maximum_buy_order: maximum_buy_order, + ) + } + } +} + +// An estimated duration yields a PROJECTED total whose error is inherited from the estimate, +// so the verdict says so -- except under a flat per-grant quote, where duration never entered +// the arithmetic and there is nothing for the estimate to have distorted. +fn screen_projected_total( + quote: OfferQuote, + duration: GrantDuration, + projected: MoneyAmountMicro, + maximum_buy_order: MoneyAmountMicro, +) -> DemandOfferAffordability { + if money_amount_micro_count(m: projected) > money_amount_micro_count(m: maximum_buy_order) { + QuoteExceedsMaximumBuyOrder { quoted: projected, maximum: maximum_buy_order } + } else { + match duration { + ExactGrantDuration { seconds: _ } => Affordable + UnobservedGrantDuration { obligation: _ } => Affordable + EstimatedGrantDuration { seconds: s } => match quote { + QuotedFlatPerGrant(_) => Affordable + QuotedPerSecond(_) => + AffordableUnderEstimate { duration_seconds: s, projected_total: projected } + QuotedPerMinute(_) => + AffordableUnderEstimate { duration_seconds: s, projected_total: projected } + QuotedPerHour(_) => + AffordableUnderEstimate { duration_seconds: s, projected_total: projected } } } } diff --git a/dag/test/claim/fabric_selection_witness_test.dag b/dag/test/claim/fabric_selection_witness_test.dag index fe9ad14203b..ab9c57681f9 100644 --- a/dag/test/claim/fabric_selection_witness_test.dag +++ b/dag/test/claim/fabric_selection_witness_test.dag @@ -18,7 +18,7 @@ import product.fabric.envelope { unstated_envelope } import product.fabric.isolation { IsolationProfile } import product.fabric.supply { SupplierOffer, OfferQuote, QuotedPerSecond, QuotedPerHour, ObservedSupply, - GrantDuration, ExactGrantDuration, + GrantDuration, ExactGrantDuration, UnobservedGrantDuration, } import product.fabric.selection { CandidateOffer, DispatchInputs, bind_candidate, CandidateBound, DispatchInputsNameAnotherOffer, @@ -31,6 +31,9 @@ import product.fabric.selection { OfferCandidacy, OfferPriced, OfferNotPriceable, OfferUnavailableForGrant, SupplySelection, OfferSelected, NoAdmissibleOffer, SelectionCost, selection_total, selection_liability, + CostBasis, ExactCost, ProjectedCost, CommitmentFloorCost, + CommitmentHorizon, CoveredByCommitment, BilledBeyondCommitment, + SpillPricedForUnobservedDuration, select_supply, OpportunityCostAxis, AvailabilityAxis, SelectionInputAxis, } @@ -208,10 +211,17 @@ fn t_no_delay_valuation() -> MoneyPerSecond { } fn t_select(candidates: List>) -> SupplySelection { + t_select_for(candidates: candidates, duration: t_ninety_seconds()) +} + +fn t_select_for( + candidates: List>, + duration: GrantDuration, +) -> SupplySelection { select_supply( requirements: t_requirements(), candidates: candidates, - duration: t_ninety_seconds(), + duration: duration, maximum_buy_order: money_amount_micro(count: 1000000), budget_currency: Usd, delay_valuation: t_no_delay_valuation(), @@ -220,14 +230,14 @@ fn t_select(candidates: List>) -> SupplySelection) -> NonEmptyStr { match s { - OfferSelected { offer: o, cost: _, basis: _, considered: _ } => (o.id.key as NonEmptyStr) + OfferSelected { offer: o, cost: _, basis: _, horizon: _, considered: _ } => (o.id.key as NonEmptyStr) NoAdmissibleOffer { considered: _ } => "none" } } fn selected_total(s: SupplySelection) -> Nat { match s { - OfferSelected { offer: _, cost: c, basis: _, considered: _ } => + OfferSelected { offer: _, cost: c, basis: _, horizon: _, considered: _ } => money_amount_micro_count(m: selection_total(c: c)) NoAdmissibleOffer { considered: _ } => 0 } @@ -235,7 +245,7 @@ fn selected_total(s: SupplySelection) -> Nat { fn considered_of(s: SupplySelection) -> List> { match s { - OfferSelected { offer: _, cost: _, basis: _, considered: cs } => cs + OfferSelected { offer: _, cost: _, basis: _, horizon: _, considered: cs } => cs NoAdmissibleOffer { considered: cs } => cs } } @@ -244,7 +254,7 @@ fn unpriced_axes(cs: List>) -> List match c { OfferNotPriceable { offer: _, unpriced: u } => concat(acc, [u.axis]) - OfferPriced { offer: _, cost: _, basis: _ } => acc + OfferPriced { offer: _, cost: _, basis: _, horizon: _ } => acc OfferNotFungible { offer: _, fungibility: _ } => acc OfferUnavailableForGrant { offer: _, needed: _, available: _ } => acc OfferWrongCurrency { offer: _, mismatch: _ } => acc @@ -257,7 +267,7 @@ fn unavailable_count(cs: List>) -> Nat { fold(cs, init: 0, f: (n, c) => match c { OfferUnavailableForGrant { offer: _, needed: _, available: _ } => n + 1 - OfferPriced { offer: _, cost: _, basis: _ } => n + OfferPriced { offer: _, cost: _, basis: _, horizon: _ } => n OfferNotFungible { offer: _, fungibility: _ } => n OfferNotPriceable { offer: _, unpriced: _ } => n OfferWrongCurrency { offer: _, mismatch: _ } => n @@ -429,7 +439,7 @@ test fn nothing_admissible_returns_every_typed_cause_and_selects_nothing() -> Bo )) match selection { NoAdmissibleOffer { considered: cs } => cs.length() == 1 - OfferSelected { offer: _, cost: _, basis: _, considered: _ } => false + OfferSelected { offer: _, cost: _, basis: _, horizon: _, considered: _ } => false } } @@ -477,9 +487,188 @@ test fn valued_delay_enters_the_ranking_and_not_the_reservation() -> Bool { delay_valuation: MoneyRate { amount: money_amount_micro(count: 5), currency: Usd }, ) match selection { - OfferSelected { offer: _, cost: c, basis: _, considered: _ } => + OfferSelected { offer: _, cost: c, basis: _, horizon: _, considered: _ } => money_amount_micro_count(m: selection_liability(c: c)) == 9000 && money_amount_micro_count(m: selection_total(c: c)) == 9300 NoAdmissibleOffer { considered: _ } => false } } + +// THE UNCERTAINTY FAMILY, AND THE FIRST FOUR MEMBERS ARE ONE OFFER WITH ONE FIELD MOVING. +// +// The operator's second ruling is that job duration is NOT KNOWN at selection time and that +// the unknown must be a modeled state rather than a number. These witnesses hold the rented +// hourly offer fixed and vary two things independently -- what is known about the duration, +// and how much of the paid hour is left -- so that the arm the fold takes is visible on each +// combination rather than inferred from a total. + +fn t_unknown_duration() -> GrantDuration { + UnobservedGrantDuration { + obligation: "This work has never been timed. Record its running time before pricing a rate quote against it.", + } +} + +fn t_fifty_minute_job() -> GrantDuration { + ExactGrantDuration { seconds: second(count: 3000) } +} + +fn t_rented_with_remainder(remaining: Second) -> List> { + t_rented_candidate( + availability: AvailableForInterval { + delay: second(count: 0), + interval: second(count: 86400), + receipt: "gunbc.observation.availability-family", + }, + commitment: PaidThrough { remaining: remaining, receipt: "gunbc.observation.remainder" }, + opportunity: t_idle(), + ) +} + +fn horizon_of(s: SupplySelection) -> List { + match s { + OfferSelected { offer: _, cost: _, basis: _, horizon: h, considered: _ } => [h] + NoAdmissibleOffer { considered: _ } => [] + } +} + +fn spill_priced_for_unobserved(hs: List) -> Bool { + fold(hs, init: false, f: (hit, h) => + match h { + SpillPricedForUnobservedDuration { wall: _, billed: _, remainder: _ } => true + CoveredByCommitment { wall: _, billed: _, remainder: _ } => hit + BilledBeyondCommitment { wall: _, billed: _, remainder: _ } => hit + }) +} + +fn covered_by_commitment(hs: List) -> Bool { + fold(hs, init: false, f: (hit, h) => + match h { + CoveredByCommitment { wall: _, billed: _, remainder: _ } => true + SpillPricedForUnobservedDuration { wall: _, billed: _, remainder: _ } => hit + BilledBeyondCommitment { wall: _, billed: _, remainder: _ } => hit + }) +} + +fn billed_beyond_commitment(hs: List) -> Bool { + fold(hs, init: false, f: (hit, h) => + match h { + BilledBeyondCommitment { wall: _, billed: _, remainder: _ } => true + CoveredByCommitment { wall: _, billed: _, remainder: _ } => hit + SpillPricedForUnobservedDuration { wall: _, billed: _, remainder: _ } => hit + }) +} + +fn basis_of(s: SupplySelection) -> List { + match s { + OfferSelected { offer: _, cost: _, basis: b, horizon: _, considered: _ } => [b] + NoAdmissibleOffer { considered: _ } => [] + } +} + +fn is_commitment_floor(bs: List) -> Bool { + fold(bs, init: false, f: (hit, b) => + match b { + CommitmentFloorCost => true + ExactCost => hit + ProjectedCost => hit + }) +} + +// FAMILY MEMBER ONE -- THE OPERATOR'S CASE. Seven minutes left on the hour and a job nobody +// has timed. The remainder does NOT make it free: the fold cannot rule out crossing the +// boundary, and crossing buys a whole further hour that cannot be unbought, so it prices that +// hour. The arm says WHY -- spill priced because the duration was unobserved, whose remedy is +// to measure -- rather than reporting a bare total a reader would have to guess at. +test fn seven_minutes_left_and_an_untimed_job_prices_the_spill_hour() -> Bool { + let selection = t_select_for( + candidates: t_rented_with_remainder(remaining: second(count: 420)), + duration: t_unknown_duration(), + ) + spill_priced_for_unobserved(hs: horizon_of(s: selection)) + && selected_total(s: selection) == 10000 + && is_commitment_floor(bs: basis_of(s: selection)) +} + +// FAMILY MEMBER TWO -- THE SAME UNTIMED JOB AGAINST FIFTY-FIVE MINUTES, AND IT PRICES +// IDENTICALLY. THAT IS THE MODEL BEING HONEST, NOT THE MODEL BEING BLUNT. +// +// A longer remainder is only worth more if something is known about how long the work runs. +// With nothing known, both remainders commit exactly one further hour, because neither can +// rule out the crossing -- so the two really are the same decision and pricing them apart +// would be inventing information. What separates them is a BOUND, and the two witnesses below +// show it separating them the moment one exists. The remedy this pair points at is to measure +// the job, which is precisely what the unobserved arm carries as its obligation. +test fn an_untimed_job_prices_the_same_against_seven_minutes_and_fifty_five() -> Bool { + let short = t_select_for( + candidates: t_rented_with_remainder(remaining: second(count: 420)), + duration: t_unknown_duration(), + ) + let long = t_select_for( + candidates: t_rented_with_remainder(remaining: second(count: 3300)), + duration: t_unknown_duration(), + ) + selected_total(s: short) == selected_total(s: long) + && spill_priced_for_unobserved(hs: horizon_of(s: long)) +} + +// FAMILY MEMBER THREE -- A HARD BOUND THAT FITS. Ninety seconds inside a seven-minute +// remainder is genuinely free, and the arm says so by name: covered by the commitment. This is +// the sunk-cost case with its boundary attached. +test fn a_bounded_job_that_fits_the_remainder_is_covered_and_free() -> Bool { + let selection = t_select_for( + candidates: t_rented_with_remainder(remaining: second(count: 420)), + duration: t_ninety_seconds(), + ) + covered_by_commitment(hs: horizon_of(s: selection)) + && selected_total(s: selection) == 0 +} + +// FAMILY MEMBER FOUR -- A HARD BOUND THAT DOES NOT FIT. Fifty minutes against a seven-minute +// remainder crosses the boundary and buys the next hour, and the arm is BilledBeyondCommitment +// rather than the unobserved one. Same price as member one, different cause, opposite remedy: +// here the answer is to choose a different offer, not to go and measure. A model that +// collapsed these two would send a broker shopping when it should have sent it measuring. +test fn a_bounded_job_that_exceeds_the_remainder_is_billed_beyond_it() -> Bool { + let selection = t_select_for( + candidates: t_rented_with_remainder(remaining: second(count: 420)), + duration: t_fifty_minute_job(), + ) + billed_beyond_commitment(hs: horizon_of(s: selection)) + && selected_total(s: selection) == 10000 +} + +// FAMILY MEMBER FIVE -- THE CONTROL. The same untimed job against continuous owned capacity, +// where spill has no meaning at all. Nothing in the fold tests for continuity: the spilled +// interval is ONE BILLING QUANTUM and this offer's quantum is zero, so the spill term is zero +// by the same arithmetic that gives an hourly supplier three thousand six hundred seconds. +// +// The arm is still the unobserved one, because the duration is still unobserved -- what the +// quantum decides is the PRICE of that arm, never whether the arm applies. A construction that +// needed a special case to keep spill from firing on a continuous offer would be bolted on, +// and this witness is what would catch it: the arm survives and the number is zero. +test fn spill_costs_nothing_on_continuous_capacity_without_a_special_case() -> Bool { + let selection = t_select_for( + candidates: t_owned_candidate(commitment: PayAsYouGo, opportunity: t_idle()), + duration: t_unknown_duration(), + ) + spill_priced_for_unobserved(hs: horizon_of(s: selection)) + && selected_total(s: selection) == 0 + && selected_key(s: selection) == "offer-owned-srv1" +} + +// AND THE CROSS-MODE CONSEQUENCE THE FAMILY ADDS UP TO. Untimed work does not belong on seven +// minutes of somebody else's hour: the rented offer commits an unrecoverable hour it cannot +// unbuy, our own continuous capacity commits nothing beyond the seconds it actually uses, and +// the fold picks ours. Compare this with the first witness in the file, where the SAME two +// offers go the other way because the work was known to fit. Both are true, and a selector +// needs both. +test fn untimed_work_does_not_belong_on_a_seven_minute_remainder() -> Bool { + let selection = t_select_for( + candidates: concat( + t_owned_candidate(commitment: PayAsYouGo, opportunity: t_idle()), + t_rented_with_remainder(remaining: second(count: 420)), + ), + duration: t_unknown_duration(), + ) + selected_key(s: selection) == "offer-owned-srv1" +} From 74954e6be85360b330de6dbfc4a056dbd310ee1c Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 04:48:29 +0000 Subject: [PATCH 5/7] review: fail-closed unbounded-duration availability, quote billing basis projection, Second-typed axis params --- dag/product/fabric/selection.dag | 152 +++++++++++++----- dag/product/fabric/supply.dag | 22 +++ .../claim/fabric_selection_witness_test.dag | 87 +++++++++- 3 files changed, 211 insertions(+), 50 deletions(-) diff --git a/dag/product/fabric/selection.dag b/dag/product/fabric/selection.dag index baf8dd789a0..a7d52e71e98 100644 --- a/dag/product/fabric/selection.dag +++ b/dag/product/fabric/selection.dag @@ -18,6 +18,7 @@ import product.fabric.supply { DemandOfferAffordability, Affordable, AffordableUnderEstimate, QuoteExceedsMaximumBuyOrder, QuoteCurrencyMismatch, QuoteNotPriceableWithoutDuration, + QuoteBillingBasis, BillsElapsedTime, BillsPerGrant, quote_billing_basis, offer_affordability_for, offer_quote_currency, offer_quoted_total_for_grant, QuotedTotal, QuotedTotalDerived, QuotedTotalNeedsDuration, GrantDuration, ExactGrantDuration, EstimatedGrantDuration, UnobservedGrantDuration, @@ -98,8 +99,16 @@ fn selection_input_axis_wire(axis: SelectionInputAxis) -> String { // consumer that reads these at one moment and selects at another has compared facts from two // worlds; that is a property of the caller and is named here rather than checked, because // nothing in this module can observe the caller's clock. +// AvailableIndefinitely IS NOT A LARGE INTERVAL, AND THAT DISTINCTION IS LOAD-BEARING RATHER +// THAN TIDY. An offer with no stated end -- capacity we own, a vendor we can keep buying hours +// from -- can hold work of ANY length, including work whose length nobody knows. An offer with +// a finite window cannot make that promise, and the difference decides whether an unbounded +// job may be admitted at all. Spelling "no stated end" as a big number would make the two +// indistinguishable and would let a comparison against an invented ceiling stand in for a +// guarantee nobody gave. type AvailabilityObservation = AvailableForInterval { delay: Second, interval: Second, receipt: ObservationReceiptRef } + | AvailableIndefinitely { delay: Second, receipt: ObservationReceiptRef } | AvailabilityUnobserved { obligation: NonEmptyStr } // AXIS 2 -- THE PAID-THROUGH COMMITMENT STATE. @@ -385,7 +394,7 @@ fn priced_opportunity(o: OpportunityCostObservation, budgeted: CurrencyCode) -> // supplier bills. A quantum rounds a bill up; it does not make the fans spin longer. fn priced_operating( o: OperatingCostObservation, - run_seconds: Nat, + run: Second, budgeted: CurrencyCode, ) -> PricedAxis { match o { @@ -402,7 +411,9 @@ fn priced_operating( }, } } else { - AxisPriced { amount: money_amount_micro(count: money_rate_micros(q: r) * run_seconds) } + AxisPriced { + amount: money_amount_micro(count: money_rate_micros(q: r) * second_count(s: run)), + } } } } @@ -411,7 +422,7 @@ fn priced_operating( // alone prefers a supplier that saves a cent and starts in twenty minutes, which is the // failure demand.dag names when it defines SelectionCost as distinct from MaximumLiability. fn priced_delay( - delay_seconds: Nat, + delay: Second, valuation: MoneyPerSecond, budgeted: CurrencyCode, ) -> PricedAxis { @@ -424,7 +435,9 @@ fn priced_delay( }, } } else { - AxisPriced { amount: money_amount_micro(count: money_rate_micros(q: valuation) * delay_seconds) } + AxisPriced { + amount: money_amount_micro(count: money_rate_micros(q: valuation) * second_count(s: delay)), + } } } @@ -435,15 +448,6 @@ fn priced_delay( // supply.dag already refuses to multiply it by a duration -- so a paid-through interval does // not reduce it. Treating the two the same would either double-count a prepaid rate or // discount a fee nobody prepaid, and the arm that decides which is the quote's own shape. -fn quote_bills_time(q: OfferQuote) -> Bool { - match q { - QuotedFlatPerGrant(_) => false - QuotedPerSecond(_) => true - QuotedPerMinute(_) => true - QuotedPerHour(_) => true - } -} - // The interval a commitment already covers, which is zero unless the quote bills TIME and an // interval is actually paid through. Every arm answers in Second rather than in a count, so the // two zeros are durations rather than bare numbers standing in for one. @@ -452,7 +456,10 @@ fn covered_interval(commitment: CommitmentState, quote: OfferQuote) -> Second { PayAsYouGo => second(count: 0) CommitmentUnread { obligation: _ } => second(count: 0) PaidThrough { remaining: r, receipt: _ } => - if quote_bills_time(q: quote) { r } else { second(count: 0) } + match quote_billing_basis(q: quote) { + BillsElapsedTime => r + BillsPerGrant => second(count: 0) + } } } @@ -577,6 +584,10 @@ type OfferCandidacy

needed: Second, available: Second, } + | OfferCannotHoldAnUnboundedGrant { + offer: FabricIdentity, + available: Second, + } | OfferNotPriceable { offer: FabricIdentity, unpriced: UnpricedInput } | OfferWrongCurrency { offer: FabricIdentity, mismatch: SelectionCurrencyMismatch } | OfferUnaffordable { @@ -594,6 +605,7 @@ fn candidacy_offer

(c: OfferCandidacy

) -> FabricIdentity { OfferPriced { offer: o, cost: _, basis: _, horizon: _ } => o OfferNotFungible { offer: o, fungibility: _ } => o OfferUnavailableForGrant { offer: o, needed: _, available: _ } => o + OfferCannotHoldAnUnboundedGrant { offer: o, available: _ } => o OfferNotPriceable { offer: o, unpriced: _ } => o OfferWrongCurrency { offer: o, mismatch: _ } => o OfferUnaffordable { offer: o, affordability: _ } => o @@ -680,10 +692,26 @@ fn price_available_candidate

( } } -// AVAILABILITY IS CHECKED AGAINST THE HORIZON'S WALL INTERVAL, not against a caller-named -// duration. Under an unobserved duration the wall is what the decision commits -- the -// remainder plus one billing unit -- so an offer that cannot hold even that much is refused -// on capability grounds before any money is compared. +// AVAILABILITY IS CHECKED AGAINST THE DURATION'S OWN BOUND, AND AN UNBOUNDED JOB CANNOT BE +// SHOWN TO FIT ANY FINITE WINDOW. +// +// An earlier revision compared the window against the HORIZON's wall interval -- the +// remainder plus one billing unit -- and that was fail-open in the direction that is hardest +// to see. The horizon's wall is the interval the decision COMMITS, which is a real quantity; +// it is not, and cannot be, an upper bound on how long the work runs. Comparing a window +// against it admitted an offer available for eight minutes to a job of unknown length, on the +// strength of a number that was never a duration. That is a fabricated bound standing where a +// guarantee was required, which DESIGN section 5 forbids outright. +// +// So the finite-window arm refuses an unbounded duration BY NAME rather than comparing. +// OfferCannotHoldAnUnboundedGrant is a separate arm from OfferUnavailableForGrant for the +// usual reason: the second says this window is too small for a job we measured, whose remedy +// is a bigger window, and the first says we cannot know, whose remedy is to measure the job. +// +// AvailableIndefinitely admits it, and that is not a loophole -- it is the whole content of +// the arm. An offer with no stated end really can hold work of any length, which is why +// continuous owned capacity and a vendor we can keep buying hours from remain selectable for +// untimed work while a time-boxed offer does not. fn price_over_horizon

( offer: SupplierOffer

, inputs: DispatchInputs

, @@ -698,31 +726,46 @@ fn price_over_horizon

( quote: offer.quote, quantum: offer.billing_quantum, ) - let wall = second_count(s: horizon_wall(h: horizon)) match inputs.availability { AvailabilityUnobserved { obligation: why } => OfferNotPriceable { offer: offer.id, unpriced: UnpricedInput { axis: AvailabilityAxis, obligation: why }, } + AvailableIndefinitely { delay: d, receipt: _ } => + price_screened_candidate( + offer: offer, + inputs: inputs, + duration: duration, + horizon: horizon, + delay: d, + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + delay_valuation: delay_valuation, + ) AvailableForInterval { delay: d, interval: window, receipt: _ } => - if second_count(s: window) < wall { - OfferUnavailableForGrant { - offer: offer.id, - needed: second(count: wall), - available: window, - } - } else { - price_screened_candidate( - offer: offer, - inputs: inputs, - duration: duration, - horizon: horizon, - delay_seconds: second_count(s: d), - maximum_buy_order: maximum_buy_order, - budget_currency: budget_currency, - delay_valuation: delay_valuation, - ) + match grant_duration_bound_seconds(d: duration) { + Absent => + OfferCannotHoldAnUnboundedGrant { offer: offer.id, available: window } + Present { value: bound } => + if second_count(s: window) < second_count(s: bound) { + OfferUnavailableForGrant { + offer: offer.id, + needed: bound, + available: window, + } + } else { + price_screened_candidate( + offer: offer, + inputs: inputs, + duration: duration, + horizon: horizon, + delay: d, + maximum_buy_order: maximum_buy_order, + budget_currency: budget_currency, + delay_valuation: delay_valuation, + ) + } } } } @@ -741,7 +784,7 @@ fn price_screened_candidate

( inputs: DispatchInputs

, duration: GrantDuration, horizon: CommitmentHorizon, - delay_seconds: Nat, + delay: Second, maximum_buy_order: MoneyAmountMicro, budget_currency: CurrencyCode, delay_valuation: MoneyPerSecond, @@ -767,7 +810,7 @@ fn price_screened_candidate

( inputs: inputs, duration: duration, horizon: horizon, - delay_seconds: delay_seconds, + delay: delay, maximum_buy_order: maximum_buy_order, budget_currency: budget_currency, delay_valuation: delay_valuation, @@ -777,7 +820,7 @@ fn price_screened_candidate

( inputs: inputs, duration: duration, horizon: horizon, - delay_seconds: delay_seconds, + delay: delay, maximum_buy_order: maximum_buy_order, budget_currency: budget_currency, delay_valuation: delay_valuation, @@ -794,22 +837,21 @@ fn assemble_cost

( inputs: DispatchInputs

, duration: GrantDuration, horizon: CommitmentHorizon, - delay_seconds: Nat, + delay: Second, maximum_buy_order: MoneyAmountMicro, budget_currency: CurrencyCode, delay_valuation: MoneyPerSecond, ) -> OfferCandidacy

{ - let run_seconds = second_count(s: horizon_wall(h: horizon)) let start = priced_start(o: inputs.start, budgeted: budget_currency) let transition = priced_transition(o: inputs.transition, budgeted: budget_currency) let operating = priced_operating( o: inputs.operating, - run_seconds: run_seconds, + run: horizon_wall(h: horizon), budgeted: budget_currency, ) let opportunity = priced_opportunity(o: inputs.opportunity, budgeted: budget_currency) let delay = priced_delay( - delay_seconds: delay_seconds, + delay: delay, valuation: delay_valuation, budgeted: budget_currency, ) @@ -943,6 +985,29 @@ type SelectionRanking

{ considered: List> } +// THE COMPARATOR RANKS ON selection_total, AND UNDER CommitmentFloorCost THAT AMOUNT IS A +// LOWER BOUND. THE RESULTING BIAS IS DECLARED HERE RATHER THAN HIDDEN. +// +// One billing quantum is the MINIMUM additional commitment that starting incurs. It is a real, +// derivable, honest quantity and it is not a prediction of the bill: a job of wholly unbounded +// duration can cross two boundaries, or ten, and nothing here claims otherwise. Pricing a +// second quantum "to be safer" would be strictly worse -- it would invent the prior this fold +// refuses to invent when it declines to separate a seven-minute remainder from a fifty-five +// minute one, and it would always answer, which is the absorbing fallback. +// +// WHAT THE FLOOR COSTS US IS DIRECTIONAL, AND THE DIRECTION IS UNFLATTERING. A floor +// understates by up to one quantum less a second per additional crossing, so an hourly +// supplier's floor can understate by nearly 3600 seconds' worth while a continuous offer's +// floor is EXACT. Ranking on floors therefore favours the coarse-quantum offer -- which is the +// mode we have the least measured experience of. The basis travels out of the fold on the +// selected arm so a consumer can see which quantity it is holding, but the comparator itself +// has one number to sort by and this is it. +// +// NEXT-RUNG TRIGGER: a bounded-duration carrier on the demand side. The bias exists only under +// CommitmentFloorCost, which is reachable only from UnobservedGrantDuration, so any duration +// bound at all -- however loose -- retires it for that demand. When every demand carries one, +// this note and the CommitmentFloorCost arm are deleted together rather than amended. + // TIE-BREAK, STATED HONESTLY. The comparison is strictly less-than, so the EARLIEST candidate // at the minimum wins and a re-run over the same roster in the same order returns the same // offer. That is stability with respect to the roster, which is all this fold can offer: @@ -967,6 +1032,7 @@ fn rank_candidate

( } OfferNotFungible { offer: _, fungibility: _ } => carry_ranking(acc: acc, considered: considered) OfferUnavailableForGrant { offer: _, needed: _, available: _ } => carry_ranking(acc: acc, considered: considered) + OfferCannotHoldAnUnboundedGrant { offer: _, available: _ } => carry_ranking(acc: acc, considered: considered) OfferNotPriceable { offer: _, unpriced: _ } => carry_ranking(acc: acc, considered: considered) OfferWrongCurrency { offer: _, mismatch: _ } => carry_ranking(acc: acc, considered: considered) OfferUnaffordable { offer: _, affordability: _ } => carry_ranking(acc: acc, considered: considered) diff --git a/dag/product/fabric/supply.dag b/dag/product/fabric/supply.dag index 0634b555115..1b860254bac 100644 --- a/dag/product/fabric/supply.dag +++ b/dag/product/fabric/supply.dag @@ -80,6 +80,28 @@ fn offer_quote_amount(q: OfferQuote) -> MoneyAmountMicro { } } +// WHETHER A QUOTE BILLS ELAPSED TIME IS A FACT ABOUT THE QUOTE, so it is projected here, +// beside the coproduct it reads, rather than re-derived by each consumer that needs it. It +// decides two different things downstream -- whether a paid-through interval can reduce the +// bill, and whether a duration multiplies the rate at all -- and a consumer open-coding the +// match would answer them from its own copy of the partition. +// +// It is a NAMED PROJECTION rather than a Bool for the reason this file gives everywhere else: +// a Bool puts the meaning in the caller's variable name, and the two answers here have +// different consequences rather than being true and not-true of one property. +type QuoteBillingBasis + = BillsElapsedTime + | BillsPerGrant + +fn quote_billing_basis(q: OfferQuote) -> QuoteBillingBasis { + match q { + QuotedFlatPerGrant(_) => BillsPerGrant + QuotedPerSecond(_) => BillsElapsedTime + QuotedPerMinute(_) => BillsElapsedTime + QuotedPerHour(_) => BillsElapsedTime + } +} + fn offer_quote_currency(q: OfferQuote) -> CurrencyCode { match q { QuotedPerSecond(r) => r.currency diff --git a/dag/test/claim/fabric_selection_witness_test.dag b/dag/test/claim/fabric_selection_witness_test.dag index ab9c57681f9..d5e9766b221 100644 --- a/dag/test/claim/fabric_selection_witness_test.dag +++ b/dag/test/claim/fabric_selection_witness_test.dag @@ -22,13 +22,14 @@ import product.fabric.supply { } import product.fabric.selection { CandidateOffer, DispatchInputs, bind_candidate, CandidateBound, DispatchInputsNameAnotherOffer, - AvailabilityObservation, AvailableForInterval, AvailabilityUnobserved, + AvailabilityObservation, AvailableForInterval, AvailableIndefinitely, AvailabilityUnobserved, CommitmentState, PayAsYouGo, PaidThrough, StartCostObservation, StartCostMeasured, TransitionCostObservation, NoTransitionRequired, OperatingCostObservation, OperatingCostMeasured, OperatingCostBorneBySupplier, OpportunityCostObservation, NoFeasibleAlternativeUse, AlternativeUseDisplaced, AlternativeUseUnread, OfferCandidacy, OfferPriced, OfferNotPriceable, OfferUnavailableForGrant, + OfferCannotHoldAnUnboundedGrant, SupplySelection, OfferSelected, NoAdmissibleOffer, SelectionCost, selection_total, selection_liability, CostBasis, ExactCost, ProjectedCost, CommitmentFloorCost, @@ -139,10 +140,12 @@ fn t_inputs( } } +// The two standing fixtures have NO STATED END -- capacity we own, and a vendor we can keep +// buying hours from -- so they carry the indefinite arm rather than a large number standing in +// for it. A finite interval appears only in the two witnesses whose subject IS the window. fn t_ready_now() -> AvailabilityObservation { - AvailableForInterval { + AvailableIndefinitely { delay: second(count: 0), - interval: second(count: 86400), receipt: "gunbc.observation.availability-1", } } @@ -257,6 +260,7 @@ fn unpriced_axes(cs: List>) -> List acc OfferNotFungible { offer: _, fungibility: _ } => acc OfferUnavailableForGrant { offer: _, needed: _, available: _ } => acc + OfferCannotHoldAnUnboundedGrant { offer: _, available: _ } => acc OfferWrongCurrency { offer: _, mismatch: _ } => acc OfferUnaffordable { offer: _, affordability: _ } => acc OfferLiabilityOverBuyOrder { offer: _, liability: _, maximum: _ } => acc @@ -267,6 +271,7 @@ fn unavailable_count(cs: List>) -> Nat { fold(cs, init: 0, f: (n, c) => match c { OfferUnavailableForGrant { offer: _, needed: _, available: _ } => n + 1 + OfferCannotHoldAnUnboundedGrant { offer: _, available: _ } => n OfferPriced { offer: _, cost: _, basis: _, horizon: _ } => n OfferNotFungible { offer: _, fungibility: _ } => n OfferNotPriceable { offer: _, unpriced: _ } => n @@ -471,9 +476,8 @@ test fn valued_delay_enters_the_ranking_and_not_the_reservation() -> Bool { offer: t_owned_offer(), inputs: t_inputs( key: "offer-owned-srv1", - availability: AvailableForInterval { + availability: AvailableIndefinitely { delay: second(count: 60), - interval: second(count: 86400), receipt: "gunbc.observation.availability-3", }, commitment: PayAsYouGo, @@ -514,9 +518,8 @@ fn t_fifty_minute_job() -> GrantDuration { fn t_rented_with_remainder(remaining: Second) -> List> { t_rented_candidate( - availability: AvailableForInterval { + availability: AvailableIndefinitely { delay: second(count: 0), - interval: second(count: 86400), receipt: "gunbc.observation.availability-family", }, commitment: PaidThrough { remaining: remaining, receipt: "gunbc.observation.remainder" }, @@ -672,3 +675,73 @@ test fn untimed_work_does_not_belong_on_a_seven_minute_remainder() -> Bool { ) selected_key(s: selection) == "offer-owned-srv1" } + +// A FINITE WINDOW CANNOT HOLD AN UNBOUNDED JOB, AND SAYING SO IS A DIFFERENT REFUSAL FROM +// SAYING THE WINDOW IS TOO SMALL. +// +// An earlier revision of the fold compared the window against the COMMITMENT HORIZON's wall -- +// the remainder plus one billing unit -- and admitted this offer, because that number is +// finite and small. It was never a duration: the horizon says what the decision commits, not +// how long the work runs, so the comparison was a fabricated bound standing where a guarantee +// was required. This witness is that hole nailed shut. +// +// The arm is its own, not folded into OfferUnavailableForGrant, because the remedies differ: +// too small says find a bigger window, and cannot-know says measure the job. +test fn a_finite_window_refuses_an_unbounded_job_rather_than_comparing() -> Bool { + let selection = t_select_for( + candidates: t_rented_candidate( + availability: AvailableForInterval { + delay: second(count: 0), + interval: second(count: 480), + receipt: "gunbc.observation.availability-boxed", + }, + commitment: PaidThrough { + remaining: second(count: 420), + receipt: "gunbc.observation.remainder-boxed", + }, + opportunity: t_idle(), + ), + duration: t_unknown_duration(), + ) + match selection { + NoAdmissibleOffer { considered: cs } => unbounded_refusals(cs: cs) == 1 + OfferSelected { offer: _, cost: _, basis: _, horizon: _, considered: _ } => false + } +} + +fn unbounded_refusals(cs: List>) -> Nat { + fold(cs, init: 0, f: (n, c) => + match c { + OfferCannotHoldAnUnboundedGrant { offer: _, available: _ } => n + 1 + OfferUnavailableForGrant { offer: _, needed: _, available: _ } => n + OfferPriced { offer: _, cost: _, basis: _, horizon: _ } => n + OfferNotFungible { offer: _, fungibility: _ } => n + OfferNotPriceable { offer: _, unpriced: _ } => n + OfferWrongCurrency { offer: _, mismatch: _ } => n + OfferUnaffordable { offer: _, affordability: _ } => n + OfferLiabilityOverBuyOrder { offer: _, liability: _, maximum: _ } => n + }) +} + +// AND THE OTHER SIDE OF THAT WALL: the same time-boxed offer with a MEASURED job that fits is +// admitted normally. Without this control the witness above would be satisfied by a fold that +// refused every finite window, which would be a wall in the wrong place. +test fn a_finite_window_still_admits_a_bounded_job_that_fits_it() -> Bool { + let selection = t_select_for( + candidates: t_rented_candidate( + availability: AvailableForInterval { + delay: second(count: 0), + interval: second(count: 480), + receipt: "gunbc.observation.availability-boxed-2", + }, + commitment: PaidThrough { + remaining: second(count: 420), + receipt: "gunbc.observation.remainder-boxed-2", + }, + opportunity: t_idle(), + ), + duration: t_ninety_seconds(), + ) + selected_key(s: selection) == "offer-rented-hourly" + && selected_total(s: selection) == 0 +} From dd1e3401f7e1ba4afcc5b93984f69b8efc2b7d82 Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 04:57:13 +0000 Subject: [PATCH 6/7] witness: choose the window that the previous fold actually admitted --- dag/test/claim/fabric_selection_witness_test.dag | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dag/test/claim/fabric_selection_witness_test.dag b/dag/test/claim/fabric_selection_witness_test.dag index d5e9766b221..f7a6853852e 100644 --- a/dag/test/claim/fabric_selection_witness_test.dag +++ b/dag/test/claim/fabric_selection_witness_test.dag @@ -683,7 +683,11 @@ test fn untimed_work_does_not_belong_on_a_seven_minute_remainder() -> Bool { // the remainder plus one billing unit -- and admitted this offer, because that number is // finite and small. It was never a duration: the horizon says what the decision commits, not // how long the work runs, so the comparison was a fabricated bound standing where a guarantee -// was required. This witness is that hole nailed shut. +// was required. This witness is that hole nailed shut, and its window is chosen to be the +// exact fail-open case rather than merely a refusal: five thousand seconds EXCEEDS the old +// comparison's wall of four thousand and twenty (a seven-minute remainder plus one hour), so +// the previous fold admitted this offer and priced it. A shorter window would have been +// refused by both folds, on different arms, and would have proved much less. // // The arm is its own, not folded into OfferUnavailableForGrant, because the remedies differ: // too small says find a bigger window, and cannot-know says measure the job. @@ -692,7 +696,7 @@ test fn a_finite_window_refuses_an_unbounded_job_rather_than_comparing() -> Bool candidates: t_rented_candidate( availability: AvailableForInterval { delay: second(count: 0), - interval: second(count: 480), + interval: second(count: 5000), receipt: "gunbc.observation.availability-boxed", }, commitment: PaidThrough { From 40d4336a0bc3177ea83387f9aa0e9221e020340b Mon Sep 17 00:00:00 2001 From: Brian Searls Date: Thu, 27 Aug 2026 05:37:30 +0000 Subject: [PATCH 7/7] Answer the new affordability arm in the sibling fabric witness suite --- dag/test/claim/fabric_witness_run_test.dag | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dag/test/claim/fabric_witness_run_test.dag b/dag/test/claim/fabric_witness_run_test.dag index 8c4c13010f2..5a7544a9b8a 100644 --- a/dag/test/claim/fabric_witness_run_test.dag +++ b/dag/test/claim/fabric_witness_run_test.dag @@ -139,12 +139,19 @@ fn fungible(e: OfferFungibility) -> Bool { // helper that only accepted the exact arm would make the floor's own duration // (EstimatedGrantDuration, see fabric_witness_run.dag) permanently ineligible // by construction, which is exactly the fusion this suite exists to keep out. +// QuoteNotPriceableWithoutDuration answers false here, and it is worth saying why rather than +// letting the arm read as a fourth refusal. It is not "this demand cannot pay"; it is "the +// screen could not be run", which is a different fact with a different remedy -- a measurement +// rather than a bigger budget. A Bool cannot carry that distinction, so what this helper +// promises is narrower than its name: NOT known-affordable. Callers that need the distinction +// match the coproduct, which is why the two witnesses below do. fn affordable(a: DemandOfferAffordability) -> Bool { match a { Affordable => true AffordableUnderEstimate { duration_seconds: _, projected_total: _ } => true QuoteExceedsMaximumBuyOrder { quoted: _, maximum: _ } => false QuoteCurrencyMismatch { quoted: _, budgeted: _ } => false + QuoteNotPriceableWithoutDuration { obligation: _ } => false } } @@ -232,6 +239,7 @@ test fn quote_above_the_maximum_buy_order_refuses() -> Bool { Affordable => false AffordableUnderEstimate { duration_seconds: _, projected_total: _ } => false QuoteCurrencyMismatch { quoted: _, budgeted: _ } => false + QuoteNotPriceableWithoutDuration { obligation: _ } => false } } @@ -251,6 +259,7 @@ test fn foreign_currency_quote_refuses_distinctly() -> Bool { Affordable => false AffordableUnderEstimate { duration_seconds: _, projected_total: _ } => false QuoteExceedsMaximumBuyOrder { quoted: _, maximum: _ } => false + QuoteNotPriceableWithoutDuration { obligation: _ } => false } }