Flagging Over-Budget Categories Before a Bond Cutback
A completion guarantor’s cutback rarely arrives as a surprise to anyone who was watching the right number. It arrives when the projected cost to finish a picture no longer fits inside the budget plus the remaining contingency, and the guarantor exercises its contractual remedy on the categories driving the overage. The exact problem this page solves is to see that trajectory early: given each cost category’s budget, its committed and forecast costs, and a modest threshold policy, compute the projected estimate-at-completion (EAC) per category, compare it against the budget, and classify each category into a severity tier — so a line producer can act on a caution while it is still a department conversation rather than a guarantor’s letter. A raw over-budget flag fires only after the money is spent; a projection fires while there is still time to reallocate. This walkthrough builds that flagging logic as a pure function with Decimal money throughout, as one focused procedure inside Contingency Drawdown Tracking.
Prerequisites and Context
This page assumes each category’s committed and forecast figures have already been assembled by the estimate-at-completion machinery documented in Cost-to-Complete Projections; it focuses entirely on the per-category comparison and the severity tiering around it. It targets Python 3.11+ for standard-library zoneinfo, and uses the same deliberate dependency set as the rest of Completion Bond Reporting & Guarantor Analytics: decimal for every monetary value, zoneinfo for timezone-aware timestamps (never a bare UTC offset), hashlib for the payload fingerprint, and Pydantic v2 (model_validate, field_validator) for the boundary schema. Category figures reach this function already cost-coded through Cost Code Standardization, so each projection maps to exactly one budget line. The parameters that matter are the severity thresholds and any category-level contingency allocation: because a bond agreement defines its own tolerances, treat the caution and critical ratios and the remaining contingency as configurable inputs rather than hardcoded law.
Step-by-Step Implementation
Flagging begins at the boundary. A Pydantic v2 model rejects any figure that is a float or negative, because a contaminated cost is the single largest source of a mis-fired or missed flag. The projection is then a pure function: for each category, the estimate-at-completion is the sum of committed cost and the forecast cost-to-complete, the overage is EAC minus budget, and the severity tier follows from the overage measured against the category’s own budget and the contingency available to absorb it.
from __future__ import annotations
import hashlib
import json
from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_UP
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator
# Severity thresholds as an overage fraction of the category budget.
CAUTION_RATIO = Decimal("0.05") # projected 5% over budget
CRITICAL_RATIO = Decimal("0.10") # projected 10% over budget
def _quantize(amount: Decimal) -> Decimal:
return amount.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
class CategoryProjection(BaseModel):
"""Committed and forecast figures for one cost category."""
model_config = ConfigDict(frozen=True)
category: str
budget: Decimal = Field(gt=Decimal("0"))
committed: Decimal = Field(ge=Decimal("0")) # spent + committed to date
forecast_to_complete: Decimal = Field(ge=Decimal("0")) # ETC
contingency_available: Decimal = Field(ge=Decimal("0"))
@field_validator(
"budget", "committed", "forecast_to_complete", "contingency_available",
mode="before",
)
@classmethod
def _reject_float(cls, v: Any) -> Decimal:
if isinstance(v, float):
raise ValueError("monetary fields must be str/Decimal, never float")
return Decimal(str(v))
def flag_category(proj: CategoryProjection) -> dict[str, Any]:
eac = proj.committed + proj.forecast_to_complete
overage = eac - proj.budget
# Overage as a fraction of the category's own budget drives the tier.
overage_ratio = (overage / proj.budget).quantize(
Decimal("0.0001"), rounding=ROUND_HALF_UP
)
# Contingency headroom decides whether the overage is already absorbed.
covered_by_contingency = overage <= proj.contingency_available
if overage <= 0:
severity = "within_budget"
elif overage_ratio < CAUTION_RATIO:
severity = "watch"
elif overage_ratio < CRITICAL_RATIO:
severity = "caution"
else:
severity = "critical"
# A projected overage that outstrips the remaining contingency is the
# trajectory a guarantor reads as cutback-eligible, regardless of tier.
cutback_risk = overage > 0 and not covered_by_contingency
payload = {
"category": proj.category,
"budget": str(_quantize(proj.budget)),
"estimate_at_completion": str(_quantize(eac)),
"projected_overage": str(_quantize(overage)),
"overage_ratio": str(overage_ratio),
"contingency_available": str(_quantize(proj.contingency_available)),
"covered_by_contingency": covered_by_contingency,
"severity": severity,
"cutback_risk": cutback_risk,
"evaluated_at": datetime.now(tz=timezone.utc).isoformat(),
}
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
payload["payload_sha256"] = hashlib.sha256(canonical).hexdigest()
return payload
Because flag_category derives every figure from the validated projection and never touches the network, identical inputs always produce an identical payload and an identical hash. That determinism is what lets a guarantor’s auditor recompute a disputed flag from the archived projection alone. The two outputs work together: severity ranks how far over budget a category is trending, while cutback_risk answers the sharper question of whether the remaining contingency can still absorb the overage — a category can be critical yet covered, or only caution yet already past its headroom, and the guarantor cares most about the second.
The thresholds deliberately fire earlier than the point of no return. A five-percent projected overage is a watch-level signal precisely because it is still small enough to unwind by trimming a department’s discretionary spend, whereas a ten-percent trajectory left unaddressed is the kind of number that turns a routine reporting call into a reserve discussion. Running the flag on every category on every cost-report cycle, rather than only at the formal covenant test, is what converts the guarantor’s periodic scrutiny into a continuous internal signal a line producer can act on first.
The flagging flow validates each category projection, computes the estimate-at-completion and the projected overage, and tiers the result against both the budget thresholds and the available contingency.
Audit Trail Requirements
Every evaluation — including a category comfortably within budget — must be serialized to a write-once store so a guarantor’s examiner sees an unbroken record rather than only the exceptions. At minimum the persisted payload must carry: the category; the budget, estimate_at_completion, projected_overage, and overage_ratio; the contingency_available and the covered_by_contingency flag; the resolved severity and cutback_risk; the UTC evaluated_at; and the payload_sha256 fingerprint of the canonical payload. Persist the record to append-only, write-once storage — object storage with an immutability lock, or an event-sourced table with no in-place update — before any dashboard or covenant certificate is published, so a crash mid-run leaves a replayable record of intent rather than a silent gap. A later revision is a new evaluation record, never an edit of the posted one, so the trajectory a producer acted on stays recoverable. This is the same write-once discipline specified in Contingency Drawdown Tracking and the ledger design in Audit Logging & Immutability, which is why the hashing belongs in one shared module rather than reimplemented per report.
Gotchas and Production Edge Cases
Committed versus forecast confusion. The estimate-at-completion is only meaningful if committed cost and forecast cost-to-complete are strictly disjoint — the money already spent-or-committed, plus the money still to spend. Double-counting a purchase order that appears in both inflates the EAC and fires a false critical flag; omitting an open commitment understates it and misses a real one. Reconcile the two upstream in Cost-to-Complete Projections so this function receives a clean split.
Contingency double-allocation. A category flag that reads contingency_available must draw from a figure that reflects contingency already committed elsewhere; otherwise two departments each believe the same cushion is theirs. Derive contingency_available from the same authorized draw stream that feeds Tracking Contingency Utilization Against a Drawdown Schedule, so remaining contingency is counted once across the whole picture rather than optimistically per category.
Severity tier versus cutback risk. These are different axes and must not be collapsed into one number. Severity ranks the size of the overage against the budget; cutback risk asks whether the contingency can still absorb it. Reporting only the tier hides a small overage that has already exhausted its headroom — the exact case a guarantor escalates — so both fields belong in every record.
Idempotency and determinism. Because the flag is a pure function of a validated projection and the store is append-only, re-evaluating a category after a partial run is safe only if the consumer deduplicates on (category, payload_sha256); without that guard a retried batch posts the same flag twice and distorts a trend line. Keeping every threshold a configurable parameter, never a literal buried in a branch, is what lets the same projection reproduce the same tier across a re-run and a re-audit.
Float contamination. Never build a Decimal from a float literal — Decimal(0.1) imports binary rounding error, and a fractional cent compounded across every category becomes a total the guarantor cannot reconcile. Read every figure through its string form, as Decimal(str(...)) above, and quantize to cents only where a figure becomes reportable.
Related Guides
- Contingency Drawdown Tracking — the parent engine that tracks the authorized draws feeding the contingency figure this flag consumes.
- Tracking Contingency Utilization Against a Drawdown Schedule — the reconciliation that establishes how much contingency each category has genuinely left.
- Cost-to-Complete Projections — the estimate-at-completion machinery that supplies the committed and forecast figures behind every flag.
- Guarantor Variance Reporting — where these category flags surface in the variance report a guarantor reviews before deciding on a cutback.
- Audit Logging & Immutability — the write-once, hash-stamped storage discipline this flagging step persists into.
Up one level: Contingency Drawdown Tracking, part of Completion Bond Reporting & Guarantor Analytics.