Locality of reference — SAP-specific docs, specs, and runtime data
now live alongside the calculator that consumes them, mirroring the
prior packages→domain layout moves.
Move targets:
- Narrative MDs → domain/sap10_calculator/docs/
NEXT_AGENT_PROMPT.md, HANDOVER_NEXT.md, SAP_CALCULATOR.md
- Spec PDFs → domain/sap10_calculator/docs/specs/
RdSAP 10 Specification 10-06-2025.pdf
PCDF_Spec_Rev-06b_12_May_2021.pdf
sap-10-2-full-specification-2025-03-14.pdf
sap-10-3-full-specification-2026-01-13.pdf
- PCDB runtime data → domain/sap10_calculator/tables/pcdb/data/
pcdb10.dat (8.3MB) + 7× pcdb_table_*.jsonl (18MB total)
Path code rewrites (load-bearing):
- tables/pcdb/__init__.py: replaced parents[4]/'docs'/'sap-spec' with
Path(__file__).resolve().parent/'data' for Table 105 JSONL loading.
- tables/pcdb/postcode_weather.py: same rebase for the pcdb10.dat path
read by _postcode_climate_table().
- tables/pcdb/etl.py __main__: same rebase for the manual ETL invocation
(source + output_dir both now point inside the package).
- tests/test_pcdb_etl.py: _PCDB_DAT_PATH now derives from
parents[1]/'tables'/'pcdb'/'data' (was parents[3]/'docs'/'sap-spec').
Citation rewrites:
- 12 .py docstrings and 4 .md docs (ADRs + READMEs + narrative docs)
had `docs/sap-spec/<file>` strings rewritten to their new locations.
- Two cases where the catch-all sed misfired (an ADR-0009 line about a
PCDB extract; the pcdb __init__.py docstring about ETL output) were
hand-corrected to point at tables/pcdb/data/ rather than docs/specs/.
docs/sap-spec/ is now empty (will be removed in a follow-up sweep or
left as a vestigial empty dir for future repurposing). ADRs 0009 and
0010 remain at docs/adr/ — they're part of the chronological
cross-cutting decision log, not calculator-specific narrative.
Verified:
- Calculator's 1e-4 production gate
(test_api_001479_full_chain_sap_matches_worksheet_pdf_exactly) GREEN.
- Wider sweep (domain/sap10_calculator/ + domain/sap10_ml/): 1654
passed / 20 failed — exact pre-move baseline. All 20 failures
pre-existing (10 hand-built skeleton + 4 cohort chain + 6 cohort
diff).
- Pyright net-zero on the 4 touched runtime/test files (0 errors)
and unchanged on heat_transmission.py (13) / cert_to_inputs.py (35) /
mapper.py (33).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
8.4 KiB
Handover — API → SAP integration test
The SAP 10.2 / RdSAP 10 calculator is closed: 930/930 pin tests
green against the 6 Elmhurst U985 worksheet PDFs (Rating cascade for
SAP rating + EI rating; Demand cascade for EPC Current Carbon +
Current Primary Energy). Architecture + public API live in
SAP_CALCULATOR.md — read that first.
Your job: build an integration test that runs API request → cert → SAP scoring end-to-end against this calculator, using the 6 Elmhurst fixtures as the strongest test case in the repo.
What "done" looks like
A test (probably under backend/ somewhere, exact location TBD by
the codebase shape) that:
- Spins up the API (FastAPI or whatever the http surface is).
- Sends a request with a representative
EpcPropertyDatapayload (use one of the 6 Elmhurst fixtures'build_epc()outputs as the reference, or send the upstream JSON shape if that's the boundary). - Receives the 4 EPC-facing outputs back through whatever endpoint the API exposes them on (or invokes the SAP scoring code path the API would use internally).
- Asserts the 4 outputs match the fixture's lodged values at the
stated tolerance:
sap_score(integer, exact match)ei_rating(integer, exact match)current_carbon_kg(abs=1e-4againstDEMAND_LINE_272_TOTAL_CO2)current_pe_kwh(abs=1e-4againstDEMAND_LINE_286_TOTAL_PE)
Parametrise the test over all 6 fixtures so any regression in the plumbing fails loudly.
What's in the box
Public API (the only thing you need from the SAP module)
from domain.sap10_calculator.rdsap.cert_to_inputs import (
cert_to_inputs, # Rating cascade
cert_to_demand_inputs, # Demand cascade
local_climate_for_cert,
environmental_section_from_cert,
primary_energy_section_from_cert,
)
from domain.sap10_calculator.calculator import calculate_sap_from_inputs, SapResult
See SAP_CALCULATOR.md §2 for the recommended dwelling_outputs(epc)
function shape — copy-paste it as your reference scoring path.
Fixture cohort (the most comprehensive test case in the repo)
6 real-world certs with full PDF ground-truth:
| Fixture | TFA | Notable cert-shape features |
|---|---|---|
_elmhurst_worksheet_000474 |
56.79 | Main + 2 ext, gas combi, no secondary |
_elmhurst_worksheet_000477 |
77.58 | RR main-only, electric secondary |
_elmhurst_worksheet_000480 |
84.41 | Main + ext + RR, electric secondary |
_elmhurst_worksheet_000487 |
81.57 | RR + ext + alt-wall, electric shower |
_elmhurst_worksheet_000490 |
66.06 | Main + ext |
_elmhurst_worksheet_000516 |
90.54 | Main only |
Each fixture exposes:
build_epc() -> EpcPropertyData— encode the cert as our domain typeLINE_*— rating-cascade worksheet expected values (Block 1)DEMAND_LINE_*— demand-cascade worksheet expected values (Block 2)SAP_VALUE_CONTINUOUS/LINE_258_SAP_RATING_INTEGER— SAP ratingLINE_274_EI_RATING_INTEGER— EI rating
Expected EPC outputs per fixture:
| sap_score | ei_rating | current_carbon_kg | current_pe_kwh | |
|---|---|---|---|---|
| 000474 | 62 | 60 | 3104.1222 | 16931.7227 |
| 000477 | 65 | 69 | 2879.7824 | 16545.4543 |
| 000480 | 61 | 65 | 3479.1552 | 19953.4189 |
| 000487 | 62 | 69 | 3005.2667 | 17755.3174 |
| 000490 | 57 | 61 | 3250.1703 | 18583.7962 |
| 000516 | 63 | 66 | 3501.4376 | 20087.8232 |
What you'll need to investigate
The SAP calculator side is a pure-Python function chain — easy. The API side is what you need to map out:
- Where does cert data enter the system? Find the FastAPI / Django
/ whatever endpoint that accepts cert input. Look under
backend/for routers. - What's the request payload shape? Is it
EpcPropertyDataJSON directly, or a different upstream representation that gets mapped? Checkdatatypes/epc/domain/mapper.py— the mapper from various schema versions (SAP-Schema-18/19, RdSAP-Schema-18) toEpcPropertyDatalives there. - Is SAP scoring already wired to the API? Search the backend for
imports of
domain.sap10_calculator.rdsap.cert_to_inputsordomain.sap10_calculator.calculator. If it's not yet wired, the integration test is a forcing function for wiring it. - What's the response shape? The 4 outputs above are what the EPC publishes; the API may already expose them, or may expose a wider surface (per-section breakdown for retrofit modelling, etc.).
If the API doesn't yet expose SAP scoring, the integration test scope might include adding the endpoint. Confirm scope with the user before expanding.
Workflow conventions (from the SAP cleanup work)
- AAA tests —
# Arrange / # Act / # Assertheaders on every new test. - One slice = one commit with Co-Authored-By trailer.
pytest.approx(..., abs=1e-4)for the EPC outputs — same bar as the SAP cascade tests. The 4 expected values above are at 4 d.p. so abs=1e-4 is the floor.- Don't widen tolerances. If a pin fails, it's a real bug (probably in the API plumbing, since the calculator is closed).
Files to read on day 1
| File | Why |
|---|---|
domain/sap10_calculator/docs/SAP_CALCULATOR.md |
Module API + architecture (you're heading there) |
domain/sap10_calculator/calculator.py |
SapResult fields you'll assert against |
domain/sap10_calculator/rdsap/cert_to_inputs.py |
The 3 public entry points + the section helpers |
domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_000474.py |
A reference fixture — build_epc() shows the EpcPropertyData shape |
domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py |
The current e2e test pattern — model your integration test on this |
backend/ (explore) |
API entry points |
datatypes/epc/domain/mapper.py |
Schema → EpcPropertyData mappers |
Quick orient
# Confirm SAP calculator is still 930/930 green
python -m pytest \
domain/sap10_calculator/worksheet/tests/test_section_cascade_pins.py \
domain/sap10_calculator/worksheet/tests/test_e2e_elmhurst_sap_score.py \
--no-cov --no-header --tb=no -q
# Show the 4 EPC outputs for fixture 000474
cd packages/domain/src && python -c "
from domain.sap10_calculator.rdsap.cert_to_inputs import (
cert_to_inputs, local_climate_for_cert,
environmental_section_from_cert, primary_energy_section_from_cert,
)
from domain.sap10_calculator.calculator import calculate_sap_from_inputs
from domain.sap10_calculator.worksheet.tests import _elmhurst_worksheet_000474 as w
epc = w.build_epc()
pc = local_climate_for_cert(epc)
rating = calculate_sap_from_inputs(cert_to_inputs(epc))
env_rating = environmental_section_from_cert(epc)
env_demand = environmental_section_from_cert(epc, postcode_climate=pc)
pe_demand = primary_energy_section_from_cert(epc, postcode_climate=pc)
print(f'SAP: {rating.sap_score}') # 62 (UK-avg)
print(f'EI: {env_rating.ei_rating_integer}') # 60 (UK-avg)
print(f'Carbon: {env_demand.total_co2_kg_per_yr:.4f} kg/yr') # 3104.1222 (postcode)
print(f'PE: {pe_demand.total_pe_kwh_per_yr:.4f} kWh/yr') # 16931.7227 (postcode)
"
Important: SAP rating and EI rating use UK-average climate; Current
Carbon and Current Primary Energy use postcode climate. Don't read EI
from the demand-cascade environmental_section_from_cert — that's a
postcode-conditions EI value, not what the EPC publishes.
What's NOT in scope
- Extending the SAP calculator. It's closed at the EPC-output layer.
If you find an additional cert-shape variation that breaks the
calculator, capture it as a new conformance fixture (see
domain/sap10_calculator/README.md) — don't paper over it in the integration test. - BEDF fuel pricing. The Fuel Bill on the EPC uses postcode-specific BEDF prices (PCDB Table 200), which are deferred. The 4 outputs above cover SAP + EI + Carbon + PE; Fuel Bill is a follow-up.
- The Demand-SAP "improved dwelling" cascade. That's Block 3 of the U985 worksheet (retrofit-applied SAP rating). Out of scope.
Good luck. The SAP side is solid; this is purely a plumbing exercise.