Each agent has a pydantic Inputs model, a pydantic Outputs model, and a
check_postconditions() static method. Contracts are pure-Python and do not
invoke Claude — they describe what must be true after the agent claims done.
Reference: src/bfev/contracts/.
| Inputs | client_yaml, project_pdf (user-provided EIES) |
| Outputs | schema_xlsx, request_pdf |
| Hard rule | both output files must exist |
| Refuses if | client.yaml missing country or sector |
| Inputs | schema_xlsx, source_pdf, client_yaml |
| Outputs | filled_xlsx, provenance_yaml |
| Hard rule | every row in provenance.yaml has source: pdf-page:N or source: default-assumption:RULE |
| Refuses if | source PDF cannot be opened, or client.yaml.actors is empty |
| Inputs | filled_xlsx, client_yaml, categories_json |
| Outputs | aggregates_json, results_json, crosscheck_xlsx, meta_yaml, audit_log |
| Hard rules | (1) aggregates.json contains scope_totals_kg_co2e and grand_total_kg_co2e; (2) crosscheck.xlsx exposes all 16 required columns; (3) co2e_kg, uncertainty_low_kg, uncertainty_high_kg cells are live formulas, not literals |
The 16 required crosscheck columns (in order):
scope, category, sub_category,
activity_data, activity_unit,
emission_factor, ef_unit, ef_source,
gwp, gwp_horizon, gwp_source,
uncertainty_pct, uncertainty_source,
co2e_kg, uncertainty_low_kg, uncertainty_high_kg
The last three are formula cells written by bfev.crosscheck:
co2e_kg = =D{r}*F{r}*I{r} # AD * EF * GWP
uncertainty_low_kg = =N{r}*(1-L{r}/100) # co2e * (1 - σ%)
uncertainty_high_kg = =N{r}*(1+L{r}/100)
Plus a SUMIF row per scope and a grand-total SUM.
| Inputs | aggregates_json, results_json, crosscheck_xlsx, client_yaml |
| Outputs | executive_pdf, scientific_pdf, official_pdf, self_audit_json |
| Hard rules | (1) all 3 PDFs present; (2) self-audit.json reports zero unsourced_numbers and zero unsourced_entities |
| Forbidden | reading filled.xlsx, recomputing any number, introducing entities absent from client.yaml.actors or aggregates.json |
| Inputs | client_yaml, aggregates_json, deliverables_dir |
| Outputs | consistency_audit_json |
| Hard rules | runs three mechanical checks, all must pass: 1. anti-leak: no phrase in ANTI_LEAK_PHRASES appears in any rendered PDF2. crosscheck reconciliation: every scope total in aggregates.json equals the recomputed AD·EF·GWP sum from crosscheck.xlsx within ε (default 0.5 %)3. entity allowlist: every entity in report/self-audit.json.entities_referenced is in client.yaml.actors |
The orchestrator is the only component that sees both aggregates.json
and the rendered PDFs. That separation is what makes the design safe: stage 4
can never silently lie about a number because the orchestrator will catch it.
Drop a new function into bfev.audit that returns a CheckResult. Wire it
into audit.run(). Write a test under tests/test_audit.py with both a
passing and a failing case. No agent prompt changes needed.