Cost-to-Complete Projections: Deterministic Estimate-at-Completion Engines in Python
The number a completion guarantor watches most closely is not what a picture has spent — it is what the picture will ultimately cost. Actual costs are history; the estimate-at-completion (EAC) is the forecast that decides whether the approved budget still holds, whether a reserve must be placed, and whether the bond is on track to close clean. Production accountants who compute that figure by hand, dragging a spreadsheet’s forecast column forward each week, produce a number nobody can reproduce and every underwriter distrusts: change one cell, and the whole projection shifts with no record of why. This reference specifies the alternative — a deterministic estimate-at-completion engine that aggregates actuals, open commitments, and forecast-to-complete into a Decimal-exact projection, versions every result by report date, and stamps each with a SHA-256 fingerprint a guarantor can recompute months later. It is one subsystem of the broader Completion Bond Reporting & Guarantor Analytics reference architecture, and it feeds the variance and cash-flow work that sits alongside it.
Prerequisites and Expected Inputs
The implementation targets Python 3.11+, both for zoneinfo in the standard library and for modern union-type syntax. It leans on a small, deliberate stack: Pydantic v2 for boundary schema validation through model_validate and field_validator, as documented in the Pydantic v2 reference; the standard-library decimal, hashlib, and zoneinfo modules for currency-safe arithmetic, deterministic snapshot hashing, and timezone-aware report timestamps; and, at production scale, polars or SQLAlchemy to stream cost-ledger rows and persist an append-only projection history. Money is never a float — as the Python decimal module documentation makes explicit, only fixed-point arithmetic gives the deterministic rounding a bond cost statement requires, and a fractional-cent drift compounded across thousands of purchase-order lines is a reconciliation the guarantor will make you redo.
The engine assumes its inputs arrive already normalized. Cost lines do not originate here: heterogeneous general-ledger extracts, purchase-order registers, and forecast worksheets are cleaned upstream by the Cost Ingestion & Data Parsing Workflows subsystem, where Schema Validation & Error Handling rejects malformed payloads and Currency & FX Normalization converts every foreign figure to the reporting currency before it reaches a projection. Each line is keyed against the taxonomy defined in Cost Code Standardization, so a committed dollar maps to exactly one cost code, one department, and one budget line. The single input this engine consumes is a validated CostLine carrying an actual, an open commitment, and a forecast-to-complete for one cost code; its output is an immutable ProjectionSnapshot — the estimate-at-completion per code, the variance to the approved budget, and a fingerprint of the inputs that produced it.
Architecture: Three Ledgers Roll Into One Projection
The estimate-at-completion is not a single subtraction. It is the disciplined sum of three distinct kinds of money, each with its own provenance and its own failure mode. Actuals are costs already incurred and posted — invoices paid, payroll run, the settled past. Open commitments are money the production is contractually obligated to spend but has not yet paid: outstanding purchase orders, signed vendor contracts, and equipment rentals whose invoices have not landed. Forecast-to-complete (ETC) is the accountant’s estimate of everything still uncommitted — the remaining shoot days, post-production, and contingency draws not yet ordered. The identity the whole engine enforces is EAC = actuals + open_commitments + ETC, computed per cost code and rolled up to the categories a guarantor reviews.
The design that makes this defensible treats each cost code as an independent projection unit. A record is validated at the boundary, its three components are summed in Decimal, the result is compared against the approved budget for that code, and the whole run is frozen into a snapshot stamped with the report date and a payload hash. Because the projection is versioned by report date rather than overwritten in place, last week’s estimate-at-completion survives as a first-class artifact — the guarantor can diff this week’s snapshot against last week’s and see exactly which cost codes moved and by how much.
The property that turns this from a convenience into an audit instrument is that every cost code either resolves to a projection or is quarantined. A commitment that arrives without a resolvable cost code is not folded into a catch-all bucket where it silently distorts the total; it is diverted to a triage queue, so the estimate-at-completion always sums a set of lines an accountant can enumerate. That completeness is what lets the guarantor’s analyst trust the rollup — the reported number is the sum of named, traceable parts, not an aggregate with an unexplained remainder.
Core Implementation
The reference engine models a cost line as a frozen Pydantic v2 object validated at the boundary, aggregates the three components per cost code in Decimal, and freezes the run into a hashed, report-dated snapshot. Monetary fields reject float outright; the report timestamp is timezone-aware; a commitment with no cost code is quarantined rather than absorbed.
import hashlib
import json
from datetime import date, datetime
from decimal import Decimal, ROUND_HALF_UP
from typing import Any
from zoneinfo import ZoneInfo
from pydantic import BaseModel, ConfigDict, Field, field_validator
CENTS = Decimal("0.01")
REPORT_TZ = ZoneInfo("America/Los_Angeles")
class CostLine(BaseModel):
"""One cost code's actual, open commitment, and forecast-to-complete."""
model_config = ConfigDict(frozen=True)
cost_code: str
department: str
approved_budget: Decimal = Field(ge=Decimal("0"))
actual: Decimal = Field(ge=Decimal("0"))
committed: Decimal = Field(ge=Decimal("0"))
forecast_to_complete: Decimal = Field(ge=Decimal("0"))
@field_validator(
"approved_budget", "actual", "committed", "forecast_to_complete",
mode="before",
)
@classmethod
def reject_float(cls, v: Any) -> Decimal:
# Reject binary floats outright; parse strings/ints exactly.
if isinstance(v, float):
raise ValueError("monetary fields must be str/Decimal, never float")
return Decimal(str(v))
@field_validator("cost_code")
@classmethod
def code_present(cls, v: str) -> str:
if not v or not v.strip():
raise ValueError("cost line must carry a resolvable cost code")
return v
class CodeProjection(BaseModel):
model_config = ConfigDict(frozen=True)
cost_code: str
department: str
approved_budget: Decimal
estimate_at_completion: Decimal
variance_at_completion: Decimal # approved_budget - EAC (negative == over)
over_budget: bool
class ProjectionSnapshot(BaseModel):
model_config = ConfigDict(frozen=True)
report_date: date
projections: tuple[CodeProjection, ...]
total_eac: Decimal
total_approved: Decimal
total_variance: Decimal
payload_hash: str
computed_at: str
operator_id: str = "SYS_AUTO"
class EACEngine:
"""Deterministic estimate-at-completion aggregator, versioned by report date."""
rule_version = "eac-illustrative-1"
def __init__(self) -> None:
self.quarantine: list[dict] = []
def _q(self, amount: Decimal) -> Decimal:
return amount.quantize(CENTS, rounding=ROUND_HALF_UP)
def _project_code(self, line: CostLine) -> CodeProjection:
eac = self._q(line.actual + line.committed + line.forecast_to_complete)
variance = self._q(line.approved_budget - eac)
return CodeProjection(
cost_code=line.cost_code,
department=line.department,
approved_budget=self._q(line.approved_budget),
estimate_at_completion=eac,
variance_at_completion=variance,
over_budget=variance < 0,
)
def _hash(self, report_date: date, projections: list[CodeProjection]) -> str:
# Canonical serialization so identical inputs hash identically.
material = json.dumps(
{
"report_date": report_date.isoformat(),
"rule_version": self.rule_version,
"projections": [p.model_dump(mode="json") for p in projections],
},
sort_keys=True,
separators=(",", ":"),
).encode("utf-8")
return hashlib.sha256(material).hexdigest()
def build_snapshot(
self, report_date: date, lines: list[CostLine]
) -> ProjectionSnapshot:
projections: list[CodeProjection] = []
for line in lines:
if not line.cost_code.strip():
self._quarantine(line, "un-coded commitment")
continue
projections.append(self._project_code(line))
# Deterministic order so the hash does not depend on input sequence.
projections.sort(key=lambda p: p.cost_code)
total_eac = self._q(sum((p.estimate_at_completion for p in projections), Decimal("0")))
total_approved = self._q(sum((p.approved_budget for p in projections), Decimal("0")))
return ProjectionSnapshot(
report_date=report_date,
projections=tuple(projections),
total_eac=total_eac,
total_approved=total_approved,
total_variance=self._q(total_approved - total_eac),
payload_hash=self._hash(report_date, projections),
computed_at=datetime.now(tz=ZoneInfo("UTC")).isoformat(),
)
def _quarantine(self, line: CostLine, reason: str) -> None:
self.quarantine.append({
"payload": line.model_dump(mode="json"),
"reason_code": reason,
"quarantined_at": datetime.now(tz=ZoneInfo("UTC")).isoformat(),
"requires_manual_override": True,
})
Three design choices carry the audit weight. The frozen=True config makes both the incoming line and the emitted snapshot immutable once validated, so no downstream caller can nudge a forecast after it has been hashed. Sorting projections by cost code before hashing means the fingerprint depends on the content of the run, not the accident of input order — two runs over the same lines in different sequences produce the same hash. And the running totals are quantized at the aggregate, so the reported estimate-at-completion is the exact sum of the per-code figures a guarantor can re-add by hand. The variance sign convention is deliberate: a negative variance_at_completion means the projected cost exceeds the approved budget, which is precisely the condition the bond machinery reacts to.
Bond Specifics: How the Guarantor Reads an Estimate-at-Completion
A completion guarantor does not underwrite a budget; it underwrites the delivery of a finished picture for a fixed sum, and the estimate-at-completion is the running proof that the sum still holds. The relationship between the EAC and the approved budget is not informational — it is contractual. When the projected estimate-at-completion for a category rises above its approved line, the overage is a signal the guarantor is entitled to act on, and the standard response is a reserve hold: the guarantor withholds a release of funds until the production either absorbs the overage from contingency or presents a credible plan to bring the estimate back under. An engine that surfaces an over-EAC condition the moment it appears, per cost code, is what lets a producer answer that hold with a targeted correction rather than a defensive summary.
Three bond-specific disciplines shape the projection. First, the estimate-at-completion is measured against the approved budget — the version the bond was written on — not against a working budget a line producer has quietly revised. The approved figure is a versioned input, stamped on the snapshot, so a mid-shoot reforecast never silently reprices the baseline the guarantor is holding to. Second, the contingency is not part of the estimate-at-completion for a cost code; it is a separate reserve the production draws against as categories run over, and tracking that draw is the job of Contingency Drawdown Tracking — the estimate-at-completion tells you how much pressure a category is under, and the contingency ledger tells you whether there is room left to absorb it. Third, an over-EAC condition on a single category is exactly the flag that should reach an underwriter early, which is the trigger developed in Flagging Categories Before a Bond Cutback. The projection engine and the reserve machinery are two halves of one covenant: the estimate-at-completion is the forecast, and the reserve hold is the consequence.
The precision of the estimate-at-completion also determines how much of it a guarantor will trust. The forecast-to-complete is the softest of the three components — it is a human estimate, not a settled figure — so a defensible engine keeps it isolated as its own field rather than blending it into committed cost. That separation lets the guarantor’s analyst see the ratio of hard money (actuals plus commitments) to soft money (forecast) at a glance, and a category whose estimate-at-completion is mostly forecast reads very differently from one that is mostly committed. When the forecast-to-complete itself needs a defensible upper bound rather than a point estimate — sizing the contingency a covenant requires — the projection hands off to the simulation in Monte Carlo Cost-to-Complete Simulation in Python.
Error Handling and Quarantine
Not every cost line should resolve to a projection. The engine mirrors the boundary discipline of the ingestion layer: aggregation handles complete, coded lines; quarantine handles lines that cannot be projected honestly. A commitment with no resolvable cost code, a line whose approved budget is missing, or a payload carrying a float monetary field is not a candidate for the aggregate — it is quarantined, so the estimate-at-completion never silently absorbs a figure that belongs to no category.
Every quarantine event serializes the original payload verbatim, attaches a machine-readable reason_code, and pushes the record to a reconciliation queue for human triage. The failing line never enters the snapshot as a resolved projection; it enters a separate exception store, so the projection history stays clean while the discrepancy is preserved for review. This is the same quarantine contract enforced in Schema Validation & Error Handling, and it matters more here than almost anywhere else in the platform: an un-coded commitment folded into a catch-all is money the guarantor’s analyst cannot trace, and an untraceable dollar in an estimate-at-completion is the fastest way to lose an underwriter’s confidence in the whole projection. The snapshot’s own SHA-256 payload hash is the second half of that discipline — it is computed over the sorted, canonical set of per-code projections, so any later tampering with a stored snapshot, or any silent drift in the aggregation logic, is detectable by recomputation against the archived inputs.
The diagram below shows the split: a valid, fully-coded line resolves through the aggregator into the report-dated snapshot, while a line that cannot be projected honestly is barred from the snapshot and diverted to the exception store with its reason code and manual-override flag.
Verification
A cost-to-complete engine is only trustworthy if you can prove, after the fact, that a given estimate-at-completion is the exact sum of a named set of cost lines. Verification checks three artifacts, not one.
First, the arithmetic identity. For every projection, estimate_at_completion must equal actual + committed + forecast_to_complete quantized to cents, and variance_at_completion must equal approved_budget - estimate_at_completion. The snapshot totals must equal the sum of the per-code figures — the rollup a guarantor reviews has to reconcile to its own parts exactly, or the whole projection is suspect.
Second, the snapshot fingerprint. Re-building a snapshot from the same report date and the same lines must yield an identical payload_hash and an identical total_eac — the idempotency check that lets you replay a disputed reporting week deterministically. Because the hash is computed over cost-code-sorted, canonically serialized projections, it is stable across input order and stable across re-runs, and it changes the instant any input figure changes.
Third, the projection history shape. A report-dated series of snapshots should let an analyst diff any two dates and see, per cost code, how the estimate-at-completion moved and whether the variance crossed from positive to negative. A healthy series shows the estimate-at-completion converging on the approved budget as forecast-to-complete shrinks and actuals grow; a series where a category’s estimate-at-completion climbs week over week is the early signal a producer wants long before it becomes a reserve hold. A minimal harness confirms the invariants:
def verify(snapshot: ProjectionSnapshot, engine: EACEngine,
report_date, lines) -> None:
replay = engine.build_snapshot(report_date, lines)
assert replay.payload_hash == snapshot.payload_hash, "non-deterministic hash"
assert replay.total_eac == snapshot.total_eac, "non-deterministic EAC"
for p in snapshot.projections:
assert isinstance(p.estimate_at_completion, Decimal), "money must be Decimal"
assert p.over_budget == (p.variance_at_completion < 0), "variance sign drift"
recomputed = sum((p.estimate_at_completion for p in snapshot.projections),
Decimal("0"))
assert recomputed == snapshot.total_eac, "rollup does not tie to its parts"
Engineered with per-code aggregation, Decimal-exact arithmetic, approved-budget versioning, and report-dated immutable snapshots, a cost-to-complete engine turns the single most scrutinized figure in bond reporting into a controlled, reproducible workflow: the estimate-at-completion does not drift between runs, an over-budget category surfaces the week it appears rather than at wrap, and every projected dollar remains traceable to a coded, hashed source the completion guarantor can recompute on demand.
Related
- Completion Bond Reporting & Guarantor Analytics — the parent architecture that turns projections, variance, and cash-flow forecasts into guarantor-facing reporting.
- Building an Estimate-at-Completion from Committed and Forecast Costs — the exact per-cost-code arithmetic behind the aggregator, worked line by line.
- Monte Carlo Cost-to-Complete Simulation in Python — a seeded simulation that bounds the forecast-to-complete and sizes the contingency a covenant requires.
- Contingency Drawdown Tracking — the reserve ledger an over-EAC category draws against, and the covenant machinery behind a bond cutback.
- Guarantor Variance Reporting — where a report-dated estimate-at-completion becomes the variance a guarantor reconciles against the bond cost statement.