Documentation
¶
Overview ¶
Package ladder defines the types, interfaces, and plan rendering for the commitment-laddering engine. This package contains the public contract only; the allocation algorithm and provider implementations live in internal/ and providers/ respectively.
Index ¶
- Constants
- type ActionType
- type AllocateResult
- type Allocation
- type AllocationInput
- type BufferReshapeConfig
- type LadderCadence
- type LadderCapability
- type LadderConfig
- type LadderMode
- type LadderPlan
- type LadderStore
- type LayerRole
- type LayerSpec
- type LayerState
- type LayerType
- type PaymentOption
- type PlannedAction
- type RampSchedule
- type RampStep
- type ReshapeSummary
- type RunRecord
- type RunStatus
- type Scope
- type Term
- type Tranche
- type TrancheInput
- type TrancheResult
- type TrancheStatus
- type UsageBaseline
Constants ¶
const ( // DefaultTargetCoveragePct is the percentage of on-demand spend to cover // with commitments when no explicit target is configured. DefaultTargetCoveragePct = 100.0 // DefaultBufferFraction is the share of the base allocation reserved in // short-term or convertible buffer commitments that can be reshaped as // usage patterns shift. DefaultBufferFraction = 0.10 // DefaultBaselinePercentile is the usage percentile used to anchor the // base commitment layer. A low percentile guards against over-committing // on volatile workloads. DefaultBaselinePercentile = 5.0 // DefaultLookbackDays is the historical window (in days) used to compute // the usage baseline when none is specified. DefaultLookbackDays = 30 // DefaultBufferUtilizationThresholdPct is the buffer-layer utilization // percentage below which the engine emits a reshape recommendation. // A value of 90 means reshape when commitments are less than 90% utilized. DefaultBufferUtilizationThresholdPct = 90.0 )
Default configuration constants. Every default is named and documented so callers can reference the symbolic name in code rather than bare literals.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ActionType ¶
type ActionType string
ActionType describes what a PlannedAction will do.
const ( ActionPurchase ActionType = "purchase" ActionReshape ActionType = "reshape" ActionHold ActionType = "hold" )
func ParseActionType ¶
func ParseActionType(s string) (ActionType, error)
ParseActionType converts s into an ActionType, returning a descriptive error when s is not a recognized value.
func (ActionType) Validate ¶
func (a ActionType) Validate() error
Validate returns an error when a is not a recognized ActionType.
type AllocateResult ¶
type AllocateResult struct {
Allocations []Allocation
Reshapes []PlannedAction
Holds []PlannedAction
}
AllocateResult is the output of Allocate.
Allocations may be accompanied by informational Holds (e.g. "buffer utilization unknown; reshape not evaluated", or a sub-minimum split amount that was skipped) and by Reshapes for under-utilized buffer layers. Reshapes are detected independently of the purchase gap, so they can appear alongside an early-exit Hold (baseline unavailable, target met). A run is a no-op if and only if Allocations and Reshapes are both empty (see IsNoOp); in that case Holds carries the explanation (baseline unavailable, gap below the minimum threshold, or every split amount below the minimum).
func Allocate ¶
func Allocate(in *AllocationInput) (*AllocateResult, error)
Allocate determines how much new commitment to place on each layer for one ladder scope run. All monetary values are computed in exact *big.Rat arithmetic; float64 inputs are converted at the boundary via ratFromFloat.
Steps (see inline comments):
- Validate config, layers (enums + topology), and layer states.
- Detect under-utilized buffer layers (reshape, or informational hold when utilization is unknown on a non-empty buffer layer). This runs INDEPENDENTLY of the purchase gap: an over-committed account is exactly when reshaping matters, so the early no-purchase exits below must not mask it. Reshape detection needs only valid layer states.
- Nil baseline -> Hold (explainable no-op, not an error), plus any buffer maintenance from step 2.
- Compute net existing per layer; derive gap.
- Gap <= minimum threshold -> Hold with numbers in rationale, plus any buffer maintenance from step 2.
- Split gap across base, flex, and buffer roles.
- Apply per-run spend cap (proportional scaling), then drop sub-minimum allocations with an informational hold each (never silently lost).
- Truncate to MaxActionsPerRun: sort purchases by GapUSDPerHour descending, keep the largest, emit one ActionHold per dropped action for auditability (reshapes are always retained first; no truncated money action is silently lost).
func (*AllocateResult) IsNoOp ¶
func (r *AllocateResult) IsNoOp() bool
IsNoOp reports whether the result proposes no state-changing actions: no allocations to purchase and no buffer reshapes. Informational Holds do not affect no-op status.
type Allocation ¶
type Allocation struct {
Layer LayerType
GapUSDPerHour *big.Rat
Rationale string
DataSources []string
}
Allocation is a per-layer sizing decision produced by Allocate. The GapUSDPerHour field carries the positive hourly commitment delta this layer should grow by. Ramp and tranche logic (PR 3) converts Allocations into time-indexed purchase tranches.
type AllocationInput ¶
type AllocationInput struct {
Now time.Time
LayerStates map[LayerType]LayerState
Layers []LayerSpec
DataSources []string
Baseline UsageBaseline
Config LadderConfig
InFlightUSDPerHour *float64
}
AllocationInput carries everything Allocate needs. All provider data must be pre-fetched by the caller; Allocate performs no I/O.
Contract on LayerStates: for every LayerSpec in Layers, LayerStates must contain an entry whose ExistingUSDPerHour and ExpiringUSDPerHour are both non-nil. Providers must supply an explicit zero rather than a nil pointer when a field is genuinely zero; a nil pointer is treated as missing data and causes Allocate to return an error (fail loud: incomplete provider data aborts the run).
InFlightUSDPerHour is the hourly commitment already in flight for this config's scope: the sum of its scheduled (not-yet-fired) ladder tranches ONLY. Fired/completed tranches are deliberately excluded because they are executed purchases already reflected in each layer's ExistingUSDPerHour; counting them here as well would double-subtract and under-purchase. It is required non-nil (fail loud) so callers must always explicitly account for in-flight commitment. A genuinely zero in-flight must be passed as a pointer to 0.0, not nil. Subtracting in-flight from the gap prevents new runs from re-planning commitment that is already scheduled, eliminating the pile-up that occurs when multiple daily runs each see the full gap before prior scheduled tranches fire.
DataSources lists the data feeds (e.g. "cost-explorer", "cloudwatch") used to derive the inputs. The slice is propagated verbatim to every Allocation and PlannedAction produced by Allocate so that approval emails and audit logs identify the source of each decision.
Now must be set by the caller; Allocate never calls time.Now internally.
type BufferReshapeConfig ¶
type BufferReshapeConfig struct {
// MaxPaymentPerExchangeUSD caps the payment for a single exchange. nil
// means no per-exchange cap is applied.
MaxPaymentPerExchangeUSD *float64
// MaxPaymentDailyUSD caps the total exchange payments for the current day.
// nil means no daily cap is applied.
MaxPaymentDailyUSD *float64
// UtilizationThresholdPct triggers a reshape when commitment utilization
// drops below this percentage.
UtilizationThresholdPct float64
// LookbackDays is the window used to measure utilization when deciding
// whether to trigger a reshape.
LookbackDays int
// DryRun, when true, simulates the reshape without executing any exchanges.
DryRun bool
// LadderRunID links the exchange records this reshape creates to the ladder
// run that triggered it, so the exchange layer scopes its pending
// cancellation to this origin (ladder) instead of the standalone task's
// pendings (gap G10 / issue #1348). Nil means "no ladder run" (the caller is
// not a ladder run); the reshape runner then behaves as the standalone
// origin. The concrete runner (wired in L16) MUST forward this to
// exchange.RunAutoExchangeParams.LadderRunID — the exchangeRunner seam
// requires it so a future runner cannot silently drop it and reintroduce the
// cross-origin cancellation bug.
LadderRunID *string
}
BufferReshapeConfig parameterizes a buffer reshape operation.
The money caps are config-boundary floats, consistent with LadderConfig.MaxHourlyCommitPerRun (implementations convert to pkg/exchange float64 anyway). Convert via ratFromFloat where exact math is needed; NaN/Inf/non-positive values are rejected wherever these caps are validated.
type LadderCadence ¶
type LadderCadence string
LadderCadence controls how often the ladder engine runs.
const ( CadenceDaily LadderCadence = "daily" CadenceWeekly LadderCadence = "weekly" )
func ParseLadderCadence ¶
func ParseLadderCadence(s string) (LadderCadence, error)
ParseLadderCadence converts s into a LadderCadence, returning a descriptive error when s is not a recognized value.
func (LadderCadence) Validate ¶
func (c LadderCadence) Validate() error
Validate returns an error when c is not a recognized LadderCadence.
type LadderCapability ¶
type LadderCapability interface {
// Provider returns the cloud provider identifier (common.ProviderAWS,
// common.ProviderAzure, or common.ProviderGCP).
Provider() common.ProviderType
// SupportedLayers returns the commitment layers this provider can fulfill.
// Each LayerSpec declares both the layer type and the roles it covers.
//
// Role-cardinality contract (enforced by the engine): the returned set
// must contain exactly one layer carrying RoleFlex, at most one carrying
// RoleBase, and at most one carrying RoleBuffer. The only permitted
// multi-role merge is base+buffer on a single layer (e.g. an Azure
// reservation serving both roles).
SupportedLayers() []LayerSpec
// ListCommitments returns all active commitments for the given scope.
ListCommitments(ctx context.Context, scope Scope) ([]common.Commitment, error)
// GetLayerStates returns a point-in-time snapshot for each supported layer.
// Layers with no active commitments carry explicit zeros in
// ExistingUSDPerHour/ExpiringUSDPerHour; nil is reserved for genuinely
// unmeasured metrics (e.g. UtilizationPct on an empty layer) and is
// treated as missing data by the engine.
GetLayerStates(ctx context.Context, scope Scope) (map[LayerType]LayerState, error)
// GetUsageBaseline computes a statistical baseline from historical
// on-demand usage over the given lookback window and percentile.
GetUsageBaseline(ctx context.Context, scope Scope, lookbackDays int, percentile float64) (UsageBaseline, error)
// PurchaseLayer buys commitments for the given layer using the provided
// recommendation. Implementations return an error wrapping
// common.ErrCommitmentPurchaseNotSupported when the layer cannot be
// purchased programmatically; callers detect it with
// errors.Is(err, common.ErrCommitmentPurchaseNotSupported).
//
// Precision note: the engine plans amounts as exact *big.Rat values
// (PlannedAction.AmountUSDPerHour), but this boundary converts them to
// the float64 cost fields of common.Recommendation. This is a documented
// precision seam; exact-decimal plumbing through the purchase path is
// addressed in a later PR.
PurchaseLayer(ctx context.Context, layer LayerType, rec common.Recommendation, opts common.PurchaseOptions) (common.PurchaseResult, error)
// ReshapeBuffer exchanges or modifies buffer-layer commitments to improve
// utilization when it falls below the configured threshold.
ReshapeBuffer(ctx context.Context, scope Scope, cfg BufferReshapeConfig) (ReshapeSummary, error)
}
LadderCapability is implemented by each cloud provider to give the ladder engine a uniform interface for querying commitment state and executing purchases. Implementations live in providers/ and are injected into the engine at startup.
type LadderConfig ¶
type LadderConfig struct {
Scope Scope
Mode LadderMode
Cadence LadderCadence
// MaxHourlyCommitPerRun caps the total hourly commitment delta a single
// run may purchase. nil means no cap is applied; when present the cap
// must be positive (validated by Validate).
MaxHourlyCommitPerRun *float64
Ramp RampSchedule
TargetCoveragePct float64
BufferFraction float64
BaselinePercentile float64
LookbackDays int
// MaxActionsPerRun limits how many PlannedActions the engine may execute
// per run. Must be > 0.
MaxActionsPerRun int
// BufferUtilizationThresholdPct is the utilization percentage below which a
// buffer layer is flagged for reshape. Must be in (0, 100]. Use
// DefaultBufferUtilizationThresholdPct when no threshold is configured.
BufferUtilizationThresholdPct float64
}
LadderConfig holds the full configuration for a single ladder scope run.
func (*LadderConfig) Validate ¶
func (c *LadderConfig) Validate() error
Validate checks that all configuration fields are within valid ranges and all sub-types are well-formed. It returns a specific, descriptive error on any violation -- callers must not silently default away a validation failure.
type LadderMode ¶
type LadderMode string
LadderMode controls whether ladder runs require human approval before executing purchases.
const ( ModeEmailApproval LadderMode = "email_approval" ModeAutoApprove LadderMode = "auto_approve" )
func ParseLadderMode ¶
func ParseLadderMode(s string) (LadderMode, error)
ParseLadderMode converts s into a LadderMode, returning a descriptive error when s is not a recognized value.
func (LadderMode) Validate ¶
func (m LadderMode) Validate() error
Validate returns an error when m is not a recognized LadderMode.
type LadderPlan ¶
type LadderPlan struct {
Scope Scope
GeneratedAt time.Time
// TargetUSDPerHour is the hourly commitment target derived from
// Baseline * TargetCoveragePct. nil means it could not be computed.
TargetUSDPerHour *big.Rat
// ExistingUSDPerHour is the sum of hourly amortized costs across all
// active commitment layers. nil means it could not be measured.
ExistingUSDPerHour *big.Rat
// GapUSDPerHour is TargetUSDPerHour - ExistingUSDPerHour. nil when either
// input is nil.
GapUSDPerHour *big.Rat
Actions []PlannedAction
Baseline UsageBaseline
}
LadderPlan is a complete, validated commitment plan for one scope and run. It captures the baseline, the monetary gap, and the ordered list of actions the engine proposes to close that gap.
func (*LadderPlan) Explain ¶
func (p *LadderPlan) Explain() string
Explain returns a deterministic, human-readable multi-line summary of the plan. The output is suitable for use as an approval email body and is designed to be read by a non-technical budget owner.
Layout:
- Scope header and generation timestamp
- Baseline parameters (lookback window, percentile)
- Target / existing / gap hourly rates
- Numbered action list (or "none")
- "Data sources:" line aggregating the actions' DataSources (omitted when no action carries any)
Every interpolated free-form field is passed through sanitizeLine so a crafted value cannot spoof additional lines in the email body.
func (*LadderPlan) Validate ¶
func (p *LadderPlan) Validate() error
Validate checks that the plan is self-consistent: a valid scope, a set generation timestamp, and all PlannedActions valid.
type LadderStore ¶
type LadderStore interface {
// SaveRun persists a new run record or updates an existing one (upsert by
// ID).
SaveRun(ctx context.Context, run *RunRecord) error
// LatestRunStartedAt returns the CreatedAt timestamp of the most recent
// run for the given scope, or nil when no run has been recorded yet.
LatestRunStartedAt(ctx context.Context, scope Scope) (*time.Time, error)
// SaveTranches persists a batch of tranches. Every tranche must carry a
// non-empty RunID (Tranche.RunID is the single source of truth for run
// linkage); implementations persist tranches exactly as given and must
// not infer linkage from anything else. Tranches are fully
// self-describing (Layer, AmountUSDPerHour, Term, PaymentOption):
// executors must be able to fire a tranche as a purchase without any
// RunRecord or PlanJSON lookup. Callers may call this multiple times
// (e.g., once per ramp step) and implementations should upsert by
// tranche ID.
SaveTranches(ctx context.Context, tranches []Tranche) error
}
LadderStore is the storage contract for the ladder engine. The concrete implementation lives in internal/ (separate Go module) and is injected into the engine at startup; pkg/ defines only the interface.
type LayerRole ¶
type LayerRole string
LayerRole describes the role a layer plays in the ladder allocation.
func ParseLayerRole ¶
ParseLayerRole converts s into a LayerRole, returning a descriptive error when s is not a recognized value.
type LayerSpec ¶
LayerSpec describes a layer and the roles it fulfills within the ladder. Azure reservations carry both base and buffer roles simultaneously.
type LayerState ¶
type LayerState struct {
// ExistingUSDPerHour is the total hourly amortized cost of active
// commitments in this layer.
ExistingUSDPerHour *float64
// ExpiringUSDPerHour is the share of ExistingUSDPerHour whose commitments
// expire within the current run cadence window.
ExpiringUSDPerHour *float64
// CoveragePct is the percentage of eligible on-demand spend currently
// covered by commitments in this layer.
CoveragePct *float64
// UtilizationPct is the current utilization percentage of commitments in
// this layer.
UtilizationPct *float64
// Layer identifies which commitment layer this snapshot describes.
Layer LayerType
}
LayerState is a point-in-time snapshot of an existing commitment layer. All monetary and percentage fields are pointers to distinguish "not yet measured" from "measured zero" (project rule: absent numbers are nil).
type LayerType ¶
type LayerType string
LayerType identifies a commitment layer in the ladder hierarchy.
func ParseLayerType ¶
ParseLayerType converts s into a LayerType, returning a descriptive error when s is not a recognized value. Use it to validate external input at the boundary instead of casting raw strings.
type PaymentOption ¶
type PaymentOption string
PaymentOption is the payment structure of a purchase action.
const ( PaymentAllUpfront PaymentOption = "all-upfront" PaymentPartialUpfront PaymentOption = "partial-upfront" PaymentNoUpfront PaymentOption = "no-upfront" )
func ParsePaymentOption ¶
func ParsePaymentOption(s string) (PaymentOption, error)
ParsePaymentOption converts s into a PaymentOption, returning a descriptive error when s is not a recognized value.
func (PaymentOption) Validate ¶
func (p PaymentOption) Validate() error
Validate returns an error when p is not a recognized PaymentOption.
type PlannedAction ¶
type PlannedAction struct {
Action ActionType
Layer LayerType
// AmountUSDPerHour is the hourly commitment delta. It must be non-nil and
// positive for ActionPurchase, and must be nil for ActionHold and
// ActionReshape (whose financial impact is implicit in the underlying
// exchange operation).
AmountUSDPerHour *big.Rat
// Term is the commitment term. Must be a valid Term for purchase
// actions; empty for hold and reshape.
Term Term
// PaymentOption is the payment structure. Must be a valid PaymentOption
// for purchase actions; empty for hold and reshape.
PaymentOption PaymentOption
// Rationale is a human-readable explanation of why this action was chosen.
// Must be non-empty for all action types.
Rationale string
// DataSources lists the data feeds (Cost Explorer, CloudWatch, etc.) used
// to derive this action. Aids auditability.
DataSources []string
}
PlannedAction is a single commitment action proposed by the ladder engine.
Every action must carry a non-empty Rationale for audit and approval-email readability -- automated money-path decisions must be explained so the human approver (or the audit log) can understand why each action was chosen.
func (*PlannedAction) Validate ¶
func (a *PlannedAction) Validate() error
Validate checks that the action is self-consistent:
- action type and layer type must be recognized
- rationale must be non-empty
- purchase actions require a positive AmountUSDPerHour and valid Term and PaymentOption enum values (money-shaping fields must be present)
- hold and reshape actions require a nil AmountUSDPerHour and empty Term and PaymentOption
type RampSchedule ¶
type RampSchedule struct {
Steps []RampStep `json:"steps"`
}
RampSchedule spreads commitment purchases across time-indexed tranches.
This type mirrors the semantic intent of internal/config/types.go RampSchedule (which uses a percent-per-step + interval model) but is defined independently because pkg/ and internal/ are separate Go modules and pkg/ cannot import internal/. See internal/config/types.go for the internal variant.
func (RampSchedule) Validate ¶
func (r RampSchedule) Validate() error
Validate checks that the ramp schedule is well-formed:
- at least one step
- AfterDays values are strictly ascending
- each fraction is in (0, 1]
- fractions sum to 1.0 within rampSumEpsilon
type RampStep ¶
type RampStep struct {
// AfterDays is the number of days after the run starts at which this
// tranche fires. Must be strictly greater than the previous step's
// AfterDays (ascending order required by RampSchedule.Validate).
AfterDays int `json:"after_days"`
// Fraction is the share of the total target allocation committed by this
// tranche. Must be in (0, 1]; fractions across all steps must sum to 1.0.
Fraction float64 `json:"fraction"`
}
RampStep is a single tranche within a ramp schedule.
The json tags are load-bearing: the frontend sends and the DB stores this shape as snake_case (`{"after_days":N,"fraction":F}`). Without the tags Go's case-insensitive matching bridges `fraction`->Fraction but NOT `after_days`->AfterDays (the underscore breaks the match), so AfterDays would silently decode to 0 and multi-step ramps would fail RampSchedule.Validate (ascending-AfterDays) or collapse to fire at day 0.
type ReshapeSummary ¶
type ReshapeSummary struct {
// Details holds per-commitment outcome descriptions for logging and audit.
Details []string
// Analyzed is the total number of commitments inspected.
Analyzed int
// Reshaped is the number of commitments exchanged or modified.
Reshaped int
// Skipped is the number of commitments that did not meet the utilization
// threshold or were blocked by a spend cap.
Skipped int
}
ReshapeSummary reports the outcome of a buffer reshape operation.
type RunRecord ¶
type RunRecord struct {
ID string
Scope Scope
Status RunStatus
CreatedAt time.Time
CompletedAt *time.Time
// PlanJSON holds a serialized LadderPlan for audit. Stored as a string
// rather than an embedded struct so the store interface stays
// serialization-format-agnostic.
PlanJSON string
}
RunRecord is the persistent record of a single ladder engine run. IDs are strings to remain agnostic to the backing store's key scheme (UUID, ULID, etc.).
type RunStatus ¶
type RunStatus string
RunStatus is the lifecycle state of a ladder engine run.
const ( RunStatusPlanned RunStatus = "planned" RunStatusAwaitingApproval RunStatus = "awaiting_approval" RunStatusApproved RunStatus = "approved" RunStatusExecuting RunStatus = "executing" RunStatusCompleted RunStatus = "completed" RunStatusFailed RunStatus = "failed" RunStatusCancelled RunStatus = "cancelled" RunStatusExpired RunStatus = "expired" )
func ParseRunStatus ¶
ParseRunStatus converts s into a RunStatus, returning a descriptive error when s is not a recognized value.
func (RunStatus) IsTerminal ¶
IsTerminal reports whether s is a terminal run status: RunStatusCompleted, RunStatusFailed, RunStatusCancelled, or RunStatusExpired. Terminal runs are the only ones allowed to carry a CompletedAt timestamp (see RunRecord.Validate).
type Scope ¶
type Scope struct {
Provider common.ProviderType
AccountID string
}
Scope identifies the ladder scope: a specific provider account or subscription that the ladder engine operates on.
Note: GCP scopes validate here, but no GCP LayerType exists yet, so every GCP run fails loud at layer validation until GCP layers land.
type Term ¶
type Term string
Term is the commitment duration of a purchase action.
type Tranche ¶
type Tranche struct {
// FireAfter is the wall-clock time at or after which this tranche fires.
FireAfter time.Time
// FiredAt is nil until the tranche actually fires.
FiredAt *time.Time
// AmountUSDPerHour is the hourly commitment delta for this tranche step.
// Validation requires any string that big.Rat.SetString parses as a
// positive rational (e.g. "3/2", "1.5"); producers should emit the
// canonical big.Rat.RatString() form, which is lossless and round-trips
// through big.Rat.SetString without floating-point precision loss,
// making it safe for DB persistence and rehydration.
AmountUSDPerHour string
ID string
RunID string
Status TrancheStatus
// Layer identifies the commitment layer this tranche purchases into.
// Required so a fired tranche is executable without consulting the parent
// run's plan.
Layer LayerType
// Term is the commitment term for the purchase (Term1Year or Term3Year).
// Must pass Term.Validate: an unset or unknown term would silently
// default at the provider boundary (money-shaping field, same rule as
// PlannedAction.Term).
Term Term
// PaymentOption is the payment structure for the purchase (e.g.
// PaymentNoUpfront). Must pass PaymentOption.Validate, for the same
// reason as Term.
PaymentOption PaymentOption
StepIndex int
}
Tranche is one ramp step that has been persisted for scheduled firing.
A tranche is fully self-describing: Layer, AmountUSDPerHour, Term, and PaymentOption together specify the exact purchase to execute when the tranche fires. Executors must never need to reconstruct purchase parameters from the parent RunRecord's PlanJSON -- two allocations with equal gaps on different layers must remain distinguishable from the tranche row alone.
func (*Tranche) Validate ¶
Validate checks that the tranche is self-consistent: non-empty ID and RunID (RunID is the single source of run linkage, see LadderStore.SaveTranches), non-negative step index, a set FireAfter timestamp, complete purchase-execution fields (recognized Layer, an AmountUSDPerHour parsing as a positive rational, valid Term and PaymentOption enum values), recognized status, and a FiredAt timestamp only when the status implies the tranche fired.
type TrancheInput ¶
type TrancheInput struct {
// Config provides the ramp schedule and is re-validated by BuildTranches.
Config *LadderConfig
// RunID stamps every produced Tranche row for run linkage. Required.
RunID string
// Term is the commitment term (Term1Year or Term3Year) passed to each
// buy-now purchase action and future tranche. The caller selects the
// term; v1 uses a single term for all allocations. Must pass
// Term.Validate.
Term Term
// PaymentOption is the payment structure (e.g. PaymentNoUpfront) passed
// to each buy-now purchase action and future tranche. Must pass
// PaymentOption.Validate.
PaymentOption PaymentOption
// NewID is called once per produced Tranche to assign a unique identifier.
// Callers may inject a UUID function, a sequential counter, or any other
// scheme. Must return a non-empty string on every call.
NewID func() string
// Now is the wall-clock time of the run. BuildTranches never calls
// time.Now internally; callers inject the clock for testability and
// audit-trail accuracy.
Now time.Time
// Allocations is the full set of per-layer sizing decisions from Allocate.
// Each allocation is split across all ramp steps in the schedule.
Allocations []Allocation
}
TrancheInput carries everything BuildTranches needs to turn a set of Allocations into immediate purchase actions and future ramp tranches. All time and ID generation is injected so BuildTranches is deterministic and free of hidden side effects.
type TrancheResult ¶
type TrancheResult struct {
// BuyNow holds ActionPurchase actions for the immediate (AfterDays == 0)
// ramp step, if one exists in the schedule.
BuyNow []PlannedAction
// Tranches holds future scheduled ramp rows for every step with
// AfterDays > 0. Each row has status TrancheStatusScheduled and a
// FireAfter timestamp set to Now + AfterDays.
Tranches []Tranche
}
TrancheResult is the output of BuildTranches.
When the ramp schedule contains no step with AfterDays == 0 (a fully- delayed ramp), BuyNow will be empty and all commitment activity appears as future Tranches. This is a valid configuration; callers should document a fully-delayed ramp explicitly so operators are not surprised by the absence of an immediate purchase.
func BuildTranches ¶
func BuildTranches(in *TrancheInput) (*TrancheResult, error)
BuildTranches turns each Allocation into (a) buy-now PlannedActions for the ramp step with AfterDays == 0, if present, and (b) future Tranche rows for every ramp step with AfterDays > 0, staggered over the ramp schedule so commitment terms expire at different times instead of bunching.
Step amounts are computed in exact big.Rat arithmetic. Every step amount is clamped to the remaining unallocated gap and the last step receives the exact leftover, so the total across all produced items reconstructs the gap exactly -- no cent lost, duplicated, or over-allocated -- for every schedule that passes RampSchedule.Validate, despite the binary-float representation of step fractions. Steps whose computed amount is zero (possible when the clamp floors a step after earlier fractions consumed the whole gap) are skipped entirely without affecting total exactness.
BuildTranches performs no I/O and never calls time.Now.
type TrancheStatus ¶
type TrancheStatus string
TrancheStatus is the lifecycle state of a single ramp tranche.
const ( TrancheStatusScheduled TrancheStatus = "scheduled" TrancheStatusFired TrancheStatus = "fired" TrancheStatusCompleted TrancheStatus = "completed" TrancheStatusCancelled TrancheStatus = "cancelled" TrancheStatusFailed TrancheStatus = "failed" )
func ParseTrancheStatus ¶
func ParseTrancheStatus(s string) (TrancheStatus, error)
ParseTrancheStatus converts s into a TrancheStatus, returning a descriptive error when s is not a recognized value.
func (TrancheStatus) Validate ¶
func (s TrancheStatus) Validate() error
Validate returns an error when s is not a recognized TrancheStatus.
type UsageBaseline ¶
type UsageBaseline struct {
// LowWaterUSDPerHour is the usage floor derived from the configured
// percentile (e.g., the 5th percentile of hourly spend).
LowWaterUSDPerHour *float64
// StableUSDPerHour is the estimated stable portion after applying the
// buffer fraction to LowWaterUSDPerHour.
StableUSDPerHour *float64
// Series holds the raw hourly values used for computation. It is optional
// and may be nil when only the summary statistics are needed (e.g., when
// populating the approval email body).
Series []float64
// LookbackDays is the window (in days) over which the baseline was
// computed.
LookbackDays int
// Percentile is the statistical percentile used for LowWaterUSDPerHour.
Percentile float64
}
UsageBaseline is a statistical summary of recent on-demand usage used to size the base commitment layer. Every monetary field is a pointer to distinguish "absent" from "genuinely zero" (project rule: absent numbers are pointers/nil, never 0).