Reconciling the Cost Report to the Bond Cost Statement
Before a variance report can be trusted as a baseline, the cost figures behind it have to agree with the figures the completion guarantor is carrying on its own bond cost statement — and they rarely agree on the first pass. The production’s internal cost report and the guarantor’s statement are drawn at different moments, roll costs into different code groupings, and occasionally disagree because one side booked an amount the other did not. The exact task this page solves is a two-sided reconciliation: match the internal cost report against the guarantor’s statement per cost code, and classify every difference as a timing difference that will clear, a classification difference where the same dollars sit under different codes, or a genuine error that needs correction. The failure mode to avoid is a single netted total that hides offsetting differences — a timing lag on one code masking a real overrun on another — so this walkthrough builds the reconciliation as a deterministic function that keeps each difference separate, typed, and traceable to both sources.
Prerequisites and Context
This page extends the reporting engine specified in Guarantor Variance Reporting; it assumes both the internal cost report and the guarantor’s bond cost statement have already been parsed and cost-coded, and focuses entirely on the two-sided tie-out between them. 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 a timezone-aware stamp, hashlib for the reconciliation fingerprint, and Pydantic v2 (model_validate, field_validator) for the boundary schema. Both sides reach this function already normalized by the upstream Cost Ingestion & Data Parsing Workflows subsystem, and each line keys to a shared taxonomy through Cost Code Standardization so the two statements are comparable code for code. Because the materiality threshold that separates a roundable difference from a reportable one is agreed between the production and the guarantor, treat that threshold and the as-of dates of each statement as configurable run parameters rather than constants.
Step-by-Step Implementation
Reconciliation begins at the boundary. A Pydantic v2 model rejects any amount supplied as a float and requires each side to name its as-of date, because a difference is only interpretable once you know the two statements were drawn at different times. The function then joins both sides by cost code, computes each per-code difference in Decimal, and classifies it: a difference on a code the statement has not yet booked is a timing difference; a difference that nets to zero against an equal and opposite difference on a sibling code is a classification difference; a difference that survives both tests and exceeds the materiality threshold is an error requiring correction. Amounts present on only one side are surfaced, never dropped.
from __future__ import annotations
import hashlib
import json
from datetime import date, datetime
from decimal import Decimal, ROUND_HALF_UP
from enum import Enum
from zoneinfo import ZoneInfo
from pydantic import BaseModel, ConfigDict, Field, field_validator
CENTS = Decimal("0.01")
class DiffType(str, Enum):
RECONCILED = "reconciled" # within materiality, no action
TIMING = "timing" # unbooked on the later-dated side
CLASSIFICATION = "classification" # same dollars, different code
ERROR = "error" # material, unexplained; needs fixing
class CostStatementLine(BaseModel):
"""One cost-code amount from either the internal report or the statement."""
model_config = ConfigDict(frozen=True)
cost_code: str
classification_group: str # the roll-up bucket the code sits under
amount: Decimal = Field(ge=Decimal("0"))
booked: bool = True # False = known-pending on this side
@field_validator("amount", mode="before")
@classmethod
def _no_float(cls, v):
if isinstance(v, float):
raise ValueError("amount must be str/Decimal, never float")
return Decimal(str(v))
def _q(amount: Decimal) -> Decimal:
return amount.quantize(CENTS, rounding=ROUND_HALF_UP)
def reconcile(internal: list[CostStatementLine],
statement: list[CostStatementLine],
internal_as_of: date,
statement_as_of: date,
materiality: Decimal) -> dict:
ours = {ln.cost_code: ln for ln in internal}
theirs = {ln.cost_code: ln for ln in statement}
# Net difference per classification group tells classification from error:
# equal-and-opposite code differences inside one group cancel to zero.
group_net: dict[str, Decimal] = {}
for code in ours.keys() | theirs.keys():
our_amt = ours[code].amount if code in ours else Decimal("0")
their_amt = theirs[code].amount if code in theirs else Decimal("0")
grp = (ours.get(code) or theirs[code]).classification_group
group_net[grp] = group_net.get(grp, Decimal("0")) + (our_amt - their_amt)
later = statement_as_of if statement_as_of >= internal_as_of else internal_as_of
lines = []
for code in sorted(ours.keys() | theirs.keys()):
our_ln = ours.get(code)
their_ln = theirs.get(code)
our_amt = our_ln.amount if our_ln else Decimal("0")
their_amt = their_ln.amount if their_ln else Decimal("0")
diff = _q(our_amt - their_amt)
grp = (our_ln or their_ln).classification_group
if abs(diff) <= materiality:
kind = DiffType.RECONCILED
elif (their_ln is None and not (our_ln and our_ln.booked)) or (
our_ln is None and their_ln is not None
and later == statement_as_of):
# Present on one side, pending/unbooked on the later-dated side.
kind = DiffType.TIMING
elif abs(_q(group_net.get(grp, Decimal("0")))) <= materiality:
# The code differs but its classification group nets flat.
kind = DiffType.CLASSIFICATION
else:
kind = DiffType.ERROR
lines.append({
"cost_code": code,
"classification_group": grp,
"internal_amount": str(our_amt),
"statement_amount": str(their_amt),
"difference": str(diff),
"diff_type": kind.value,
})
body = sorted(lines, key=lambda r: r["cost_code"])
canonical = json.dumps(body, sort_keys=True, separators=(",", ":")).encode()
return {
"internal_as_of": internal_as_of.isoformat(),
"statement_as_of": statement_as_of.isoformat(),
"materiality": str(_q(materiality)),
"lines": body,
"net_difference": str(_q(sum(
(Decimal(r["difference"]) for r in body), Decimal("0")))),
"error_count": sum(1 for r in body if r["diff_type"] == "error"),
"computed_at": datetime.now(tz=ZoneInfo("UTC")).isoformat(),
"payload_sha256": hashlib.sha256(canonical).hexdigest(),
}
Because reconcile classifies each code independently and sorts before hashing, identical inputs always produce an identical body and an identical fingerprint. The net difference is reported alongside the per-line detail rather than instead of it, so a guarantor’s analyst sees that a flat net is genuinely flat — reconciled and timing differences that clear — and not two material errors cancelling by accident.
The reconciliation joins both sides by cost code, tests each difference against materiality, then against the timing and classification rules, before the residue is classified as an error.
Audit Trail Requirements
Every reconciliation — including a clean one where every code is within materiality — must be serialized to a write-once store so an examiner sees an unbroken tie-out history between the two sides. At minimum the persisted record must carry: both internal_as_of and statement_as_of dates; the agreed materiality; each line’s cost_code, classification_group, internal_amount, statement_amount, difference, and diff_type; the reported net_difference and error_count; a UTC computed_at; and the payload_sha256 over the canonical body. Persist to append-only, write-once storage — object storage with an immutability lock, or an event-sourced table with no in-place update — before any correcting entry is booked, so the state that prompted a correction stays recoverable. A later reconciliation is a new record referencing the prior one, never an edit, so the difference an error correction resolved remains visible in the history. This is the same deterministic audit discipline the parent Guarantor Variance Reporting engine enforces on the variance package a reconciled cost report ultimately feeds through Generating Guarantor-Facing Variance Reports in Python.
Gotchas and Production Edge Cases
As-of dates make timing legible. A difference is only a timing difference relative to two known draw dates; without them, an unbooked cost is indistinguishable from a missing one. Require each side to declare its as-of date and resolve which is later on the record, rather than inferring recency from the run clock — a reconciliation re-run tomorrow must classify yesterday’s statements exactly as it did today, and that only holds if the dates travel with the data.
Classification differences hide inside a flat net. The most dangerous reconciliation is the one that nets to zero, because a timing lag on one code can offset a real overrun on another and the summary looks clean. Testing the classification-group net separately from the per-code difference is what distinguishes dollars that genuinely moved between codes from two unrelated errors that happen to cancel; never accept a flat top-line net as proof of agreement without the per-line detail behind it.
Float contamination. Never build a Decimal from a float literal — Decimal(0.1) carries the binary rounding error into the comparison, and a reconciliation that should tie to zero drifts by a fractional cent that reads as a spurious error. Read every amount through its string form, as the validators above do, and reject float at the boundary so a mis-typed export fails loudly instead of producing a difference no one can explain.
Materiality is a parameter, not a constant. The threshold that separates a roundable difference from a reportable one is agreed between the production and the guarantor and can differ by picture and by period. Stamp the applied materiality into the record so a later reader knows which threshold classified each difference; a reconciliation that silently changed its own tolerance between periods is one a guarantor cannot rely on.
Idempotency on replay. Because the body hash is deterministic and the store is append-only, re-running a reconciliation after a partial failure is safe only if the consumer deduplicates on (internal_as_of, statement_as_of, payload_sha256); without that guard a retried run double-files the record. When a genuine error is confirmed, the correction and its downstream effect on remaining spend are tracked against the drawdown schedule in Contingency Drawdown Tracking, and the corrected estimate at completion flows back through Cost-to-Complete Projections.
Related Guides
- Guarantor Variance Reporting — the parent engine whose variance report depends on a cost base already reconciled to the guarantor’s statement.
- Generating Guarantor-Facing Variance Reports in Python — the assembly step that consumes the reconciled actuals this tie-out validates.
- Cost-to-Complete Projections — where a corrected estimate at completion is recomputed after a reconciliation resolves an error.
- Contingency Drawdown Tracking — the drawdown-schedule tracking that a confirmed error correction feeds into.
- Schema Validation & Error Handling — the boundary and quarantine discipline this reconciliation reuses for malformed and one-sided lines.
Up one level: Guarantor Variance Reporting, part of Completion Bond Reporting & Guarantor Analytics.