Commit graph

95 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
9ed4ccc28e docs(modelling): handover for the Modelling stage rebuild
Captures issue status (#1153-#1161), the built compute spine, key
facts/gotchas (hand-built 000490 fixture, calculator entry, worktree-vs-main
import trap, test/commit conventions), and the two gates (parser fix -> wire
Elmhurst cascade pins; #1157 persist-Plan HITL schema review). For picking
the work back up in a fresh session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:18:31 +00:00
Khalim Conn-Kowlessar
0ba45a09cc docs(modelling): record stage design — CONTEXT terms + ADR-0016
Reframe Recommendation as a target surface (partitions the EpcPropertyData
surface, so selected overlays never collide); add Measure Option,
Simulation Overlay (EpcSimulation), Product, Cost, Contingency, and
Measure Dependency. ADR-0016 fixes the scoring/optimisation approach
(warm-start grouped-knapsack MILP -> deterministic package re-score ->
greedy repair, with a final-package marginal cascade for display
attribution), resolving the open question in ADR-0005 §14.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 22:13:51 +00:00
Khalim Conn-Kowlessar
f179950519 feat(baseline): wire BillDerivation into the orchestrator and persist the Bill (ADR-0014)
The PropertyBaselineOrchestrator now reads the current Fuel Rates snapshot
once per batch, builds a BillDerivation, and prices each scored property's
SapResult -> EnergyBreakdown into a Bill carried on PropertyBaselinePerformance
(None only on the stub no-calculator path). The Bill is flattened onto nullable
bill_* flat columns (per-section kwh+cost, standing charges, SEG credit, total)
on the postgres table, with bill_total_annual_bill_gbp as the not-null
discriminator on read-back. Section absent from the bill stays None, not 0.

Updated all four orchestrator construction sites to inject the FuelRatesRepository
port (handler + three test sites), and the FE migration doc to reflect the
prefixed columns and that they are now populated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 18:51:18 +00:00
Khalim Conn-Kowlessar
5e75fb474c feat(baseline): EnergyBreakdown.from_sap_result + COOLING section
The SapResult -> EnergyBreakdown adapter (ADR-0014), a classmethod on the
target mirroring Performance.from_sap_result. Folds each positive per-end-use
delivered kWh into a billable EnergyLine: main/main-2/secondary heating and
hot water at their resolved fuel (sap_code_to_fuel); lighting/pumps-fans/
appliances/cooking/cooling as electricity. PV export carries to exported_kwh
for the SEG credit. Zero-kWh end uses emit no line; a positive kWh with no
fuel code raises rather than billing at a default (strict, mirrors the
calculator).

Adds BillSection.COOLING (electricity, from space_cooling_fuel_kwh_per_yr).
BillDerivation already prices any section it is given, so no change there.

Also corrects the ADR-0014 amendment: SapResult carries the calculator's own
fuel codes (raw API or Table-32 per mapper, ADR-0015); sap_fuel normalizes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 18:28:22 +00:00
Khalim Conn-Kowlessar
19a56461ba docs(baseline): Bill Derivation design — fuel as calculator output + rebaselining is assemble-and-score
Captures a /grill-with-docs session resolving how BillDerivation gets the
fuel each end use burns, and what Rebaselining actually is.

- ADR-0014 amendment: per-end-use fuel is a calculator OUTPUT (resolved
  Table-32 codes on SapResult: main-1/main-2/secondary/HW + pv_exported_kwh);
  the adapter is a pure SapResult->EnergyBreakdown map. Corrects stale §3
  (is_gas_code... -> sap_fuel.sap_code_to_fuel). Adds COOLING section.
  Interim, pending ADR-0015.
- ADR-0013 amendment: the calculator is the SCORING ENGINE within
  Rebaselining (assemble the Effective EPC picture, then score), not the
  whole of it; the Rebaseliner exposes its SapResult so the orchestrator
  composes Effective Performance AND the Bill from one scoring.
- ADR-0015 (new): mappers own cert normalization; EpcPropertyData becomes a
  strict type. Explains why fuel resolution sits in the calculator today.
- CONTEXT.md: Effective EPC = the assembled picture; Rebaselining = assemble
  (overrides / neighbour-estimation / old-schema remap) then score.
- EpcPropertyData docstring points at ADR-0015.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 18:04:55 +00:00
Khalim Conn-Kowlessar
69995edec8 Merge branch 'main' of https://github.com/Hestia-Homes/Model into feature/per-cert-mapper-validation 2026-06-02 16:10:41 +00:00
Khalim Conn-Kowlessar
2c8c299fde docs(migration): add the Bill Derivation block to the property_baseline table (ADR-0014)
Slice 5b: update the FE-owned migration spec so the other repo can create the
bill columns in parallel.

- Bill block: per-section delivered kWh + cost (heating, hot water, lighting,
  appliances, cooking, pumps/fans, cooling) + standing_charges_gbp,
  seg_credit_gbp, total_annual_bill_gbp, fuel_rates_period.
- space_heating_kwh / water_heating_kwh (RHI recorded demand) marked SUPERSEDED
  by heating_kwh / hot_water_kwh (calculator delivered fuel); kept until the bill
  populates, then dropped.
- Cooling section kept (mostly 0 but affects the bill, cheap to store).
- Records the calculator-load-bearing posture (effective_* may differ from
  lodged_* for pre-10.2) and that columns are defined now / populated when the
  SapResult->EnergyBreakdown adapter + BillDerivation wiring land.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 10:13:23 +00:00
Khalim Conn-Kowlessar
5f65b9be62 feat(baseline): SAP fuel-code -> Fuel mapping for billing (ADR-0014)
Slice 3 of Bill Derivation. sap_code_to_fuel(code) maps a SAP 10.2 / Table 32
fuel code to the canonical billing Fuel — bounded to the ~47 Table 32 codes (the
carrier, orthogonal to the PCDB product index, so all PCDB heat pumps share one
electricity code). Mains gas / LPG / oil+bioliquids / coal / smokeless / wood /
electricity (standard + off-peak) / heat-network groupings; an unmapped code
(dual fuel, grid-export) raises UnmappedSapCode rather than guessing.

Also: ADR-0014 deferred/TODO section records the stubbed appliances+cooking
(pending the SapResult fields), the off-peak day/night split, the heat-network
rate gap, and regional rates / ETL.

The SapResult -> EnergyBreakdown adapter (next slice) is gated on the
appliances/cooking fields landing on SapResult.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 09:50:10 +00:00
Khalim Conn-Kowlessar
57867832f6 docs(adr): Bill Derivation (ADR-0014) + calculator goes load-bearing (ADR-0013 amend)
Pin the bills design from a /grill-with-docs session:
- ADR-0014: whole-home annual bill from SAP10 Calculation's delivered kWh per
  end use, re-priced at real Fuel Rates (NOT the calculator's SAP-notional
  total_fuel_cost_gbp, which is RdSAP Table 32 standardised prices ~half real
  electricity). Fuel enum + FuelRates + FuelRatesRepository static snapshot;
  per-section + total flat columns; raise on unpriced fuel (house coal /
  heat network are the named gaps).
- ADR-0013 amendment: the shadow stepping-stone is collapsed — the calculator
  is load-bearing now. effective=calculated for sap_version<10.2 (StubRebaseliner
  floor 10.0->10.2); >=10.2 keeps lodged + logs divergence; a strict-raise
  aborts the batch (load-bearing for bills regardless of version).
- CONTEXT: EPC Energy Derivation -> Bill Derivation (no "service" suffix);
  Baseline Performance energy block = per-end-use kWh + per-section bill + total;
  Fuel Rates = committed static snapshot; Rebaselining trigger threshold 10.2.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 09:20:50 +00:00
Khalim Conn-Kowlessar
561e1b8b49 feat(baseline): run Sap10Calculator in shadow on Property Baseline (ADR-0013)
Wire Sap10Calculator into PropertyBaselineOrchestrator as a non-load-bearing
shadow runner. For each property it scores the Effective EPC beside the
load-bearing Lodged/Effective write, catches any strict-raise -> log.error
(never aborts the batch), and on success log.warning's divergence from Lodged:
SAP |continuous - lodged| > 0.5; PEUI/CO2 > 1% relative (CO2 after kg->tonnes).
Every line is tagged with sap_version so SAP-10.2 signal separates from
older-spec drift (ADR-0010 Validation Cohort).

Per ADR-0013, Calculated SAP10 Performance is not a persisted third value-set:
effective = calculated in every baselining scenario, so the calculator IS the
mechanism that produces Effective Performance (the Rebaseliner). It runs in
shadow only while being hardened; when overrides/estimation land it is promoted
to drive Effective and the failure posture flips to abort (ADR-0012, calculator
now load-bearing). No table change.

- ADR-0013 + CONTEXT (Calculated SAP10 Performance / Effective Performance /
  Rebaselining) record the decision.
- CalculatorShadow port + LoggingCalculatorShadow + Calculator protocol.
- FakeCalculatorShadow for orchestrator unit tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 08:01:47 +00:00
Khalim Conn-Kowlessar
ce33cd94ef docs: correct SAP calculator path in CONTEXT (domain/sap → domain/sap10_calculator)
Factual staleness fix flagged in the handover; the calculator lives in
domain/sap10_calculator/calculator.py. Glossary term 'Baseline Performance'
deliberately left unchanged (concept vs PropertyBaselinePerformance class).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 18:56:41 +00:00
Khalim Conn-Kowlessar
50914e8aae refactor(property-baseline): units on co2 / PEUI columns (PR #1139 review)
Make the stored units explicit on the property_baseline_performance columns:
- `*_co2_emissions` → `*_co2_emissions_t_per_yr` (tonnes CO₂/yr, whole dwelling)
- `*_primary_energy_intensity` → `*_primary_energy_intensity_kwh_per_m2_yr`

Column names only; the domain `Performance` VO stays unit-suffix-free (units are
a storage concern, mapped in from_domain/to_domain). Migration doc updated.
Round-trip stays green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:28:48 +00:00
Khalim Conn-Kowlessar
457d959b1f refactor(property-baseline): rename baseline → property_baseline aggregate (PR #1139 review)
Wholesale rename of the Baseline aggregate to PropertyBaseline for clarity /
to disambiguate from baselines that appear elsewhere in Modelling. Scoped to
this aggregate only — the distinct Rebaselining term (rebaseline_reason,
StubRebaseliner, RebaselineNotImplemented) is deliberately untouched.

- domain/baseline → domain/property_baseline; BaselinePerformance →
  PropertyBaselinePerformance.
- repositories/baseline → repositories/property_baseline; BaselineRepository
  / BaselinePostgresRepository → PropertyBaseline*.
- orchestration/baseline_orchestrator.py → property_baseline_orchestrator.py;
  BaselineOrchestrator → PropertyBaselineOrchestrator. BaselineStage →
  PropertyBaselineStage.
- infrastructure/postgres: baseline_performance_table.py →
  property_baseline_performance_table.py; table `baseline_performance` →
  `property_baseline_performance`; Model renamed.
- UnitOfWork attribute `.baseline` → `.property_baseline`.
- Docs: ADR-0004 references + migration doc (renamed to
  property-baseline-performance-table.md) updated.

CONTEXT.md glossary term ("Baseline Performance") left as-is pending a
ubiquitous-language call (raised on the PR). 123 tests pass; pyright strict
clean (only the unrelated pre-existing moto import errors remain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:28:48 +00:00
Khalim Conn-Kowlessar
7275850c9e refactor(orchestration): wire stages onto the UnitOfWork; per-stage commit (#1138)
Replaces the handler's whole-pipeline Session (one transaction across all
three stages, connection pinned during Ingestion's external IO) with a
Unit-of-Work per stage (ADR-0012, added here). Each stage runs its batch in
one unit and commits once; any property raising aborts the batch and the
subtask fails noisily.

- BaselineOrchestrator(unit_of_work, rebaseliner): one unit for the batch,
  commit once. Raise on a pre-SAP10 property leaves the unit uncommitted.
- IngestionOrchestrator(unit_of_work, epc_fetcher, geospatial_repo,
  solar_fetcher): fetch/write split — phase 1 fetches the whole batch (EPC /
  coords / solar) with NO unit open; phase 2 writes in one unit and commits.
  The connection is never held during external IO. Geospatial S3 repo stays
  injected (reference data, not transactional).
- Handler: module-scoped engine (pool reused across warm invocations) + a UoW
  factory; whole-pipeline `with Session` gone. `build_first_run_pipeline`
  composes on the factory. Source clients still behind the raising seam.
- ADR-0012 records the decision (per-stage boundary, all-or-nothing batch,
  idempotent re-run, fetch/write split, module-scoped engine). Modelling stub
  left untouched (no-op, no DB) per the ADR.

Tests: orchestrators on a shared FakeUnitOfWork (assert persisted batch +
exactly-once commit + no-commit-on-raise). New real-DB E2E integration test:
real PostgresUnitOfWork, Ingestion writes the EPC → Baseline reads it back
through the repo → re-run replaces, not duplicates (1 EPC row, 1 baseline row
after two runs). 121 pass in tests/; pyright strict clean; AAA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:28:48 +00:00
Khalim Conn-Kowlessar
9f22b0aae8 feat(baseline): BaselineOrchestrator + BaselinePerformance aggregate (#1135)
Stage 2 of First Run. Establishes each Property's Baseline Performance
from persisted source data and writes it back — reads only from repos,
never a Fetcher or HTTP (ADR-0003), so it is byte-identical whether
Ingestion ran milliseconds ago or last week.

Domain (`domain/baseline/`):
- `Performance` VO — the four rated quantities: SAP / EPC Band / CO2 /
  Primary Energy Intensity. `lodged_performance(epc)` reads them off the
  EPC's recorded fields (PEUI = `energy_consumption_current`).
- `BaselinePerformance` (ADR-0004) — the paired `lodged` + `effective`
  Performance + `rebaseline_reason`, plus the no-derivation part of the
  energy block (`space_heating_kwh` / `water_heating_kwh`, off the RHI,
  deterministic per ADR-0006). Both halves always populated.
- `Rebaseliner` port + `StubRebaseliner`: the re-score-on-override seam
  (ADR-0011). SAP10 certs pass through (effective == lodged, reason
  "none"); a pre-SAP10 cert raises `RebaselineNotImplemented` rather
  than fabricating a plausible-but-wrong "none" — ML rebaselining is not
  wired yet. Mirrors the repo's strict-raise culture.

Persistence: new `BaselineRepository` port + `BaselinePostgresRepository`
+ flat-column `baseline_performance` SQLModel (one row per Property). Per
ADR-0004's amendment this is a standalone table, NOT columns on the
retiring `property_details_epc`. Production migration is FE-owned
(Drizzle) — docs/migrations/baseline-performance-table.md.

Docs (grill-with-docs): corrected CONTEXT.md Lodged/Effective Performance
to Primary Energy Intensity (the term collided with its own _Avoid_ entry
under "heat demand") + fixed stale RHI field names; amended ADR-0004
Consequences for the standalone-table decision.

Fuel split + bills (rest of EPC Energy Derivation) deferred to a
follow-up — they need a Fuel Rates source (Ofgem-cap ETL) that does not
exist yet.

TDD, one test -> one impl: 7 tests (lodged read, rebaseliner pass-through
+ raise, orchestrator establish-and-persist + pre-SAP10 raise, Postgres
round-trip + absent). pyright strict clean; AAA layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:28:48 +00:00
Khalim Conn-Kowlessar
3e1d3acfbf feat(epc): persist renewable_heat_incentive — full round-trip equality (#1137)
Add epc_renewable_heat_incentive table (space_heating_kwh, water_heating_kwh +
the three insulation-impact kWh fields), wired into EpcPostgresRepository
save/get. This is the P0 gap: RenewableHeatIncentive carries the baseline
space-heating/hot-water kWh that EPC Energy Derivation consumes.

The round-trip test now asserts full deep-equality (dropped the
renewable_heat_incentive exclusion) and passes for RdSAP 21.0.0 + 21.0.1.
DB migration for the new table documented in
docs/migrations/epc-property-round-trip-fidelity.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:28:48 +00:00
Khalim Conn-Kowlessar
559616d3bb feat(epc): EPC persistence round-trip fidelity + JSONB code columns (Slice 1 #1129)
Relocate EpcPropertyModel + child tables from the dying backend/ tree to
infrastructure/postgres/epc_property_table.py (re-export shim keeps
documents_parser working). Add EpcRepository port + EpcPostgresRepository with
a full reverse mapper (epc_property tables -> EpcPropertyData).

Round-trip test surfaced two fidelity gaps:
 1. Union[int,str] SAP code fields were str()-coerced on save, losing the int
    (API) vs str (Site Notes) distinction. Now stored as JSONB (type-preserving).
 2. The schema was a partial projection. Closed the cheap gaps on the model
    (heating shower/bath counts, roof_construction_type, curtain_wall_age,
    addendum, mechanical_vent_duct_insulation_level, SAP 10.2 §2 ventilation
    fields + a ventilation_present flag). Structural gaps tracked as follow-ups;
    renewable_heat_incentive (P0, #1137) excluded from the assertion until landed.

Round-trip passes for RdSAP-Schema-21.0.0 and 21.0.1; pyright strict clean.
Migration inventory for the DB: docs/migrations/epc-property-round-trip-fidelity.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:28:48 +00:00
Khalim Conn-Kowlessar
8291f29721 docs(ara): composable stage-orchestrator design (ADR-0011 + ADR-0003 amend + CONTEXT)
Records the grill-with-docs outcomes for the ara_first_run rebuild: three
composable stage orchestrators (Ingestion/Baseline/Modelling), one lambda per
use case chaining them through repos (not in-memory), and the Fetcher-vs-Repo
data-source taxonomy. Amends ADR-0003's chaining rule to generalise beyond
RefreshOrchestrator. Adds the pipeline-composition + First Run vocabulary to
CONTEXT.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:28:48 +00:00
Khalim Conn-Kowlessar
ce12b114c7 docs(ara): next-agent handover for Property Baseline (SAP calc) + Modelling
Orientation for the next chat picking up the two open fronts after the
ara_first_run rebuild shipped:
- where things stand (merged to main via per-cert; branch/worktree layout;
  PRs into per-cert), authoritative ADRs/CONTEXT to read,
- current architecture + key files (post baseline→property_baseline /
  FirstRun→AraFirstRun rename),
- conventions + gotchas (TDD, ephemeral PG, FakeUnitOfWork, pyright noise to
  ignore, gh-credential push workaround),
- Task 1: wire Sap10Calculator into PropertyBaselineOrchestrator (Calculated
  SAP10 Performance as a third value-set; failure-posture decision),
- Task 2: Modelling (stubs to build out; MaterialsRepository naming open;
  needs a UoW when writing Plans),
- the raising/no-op seams not to mistake for done,
- known doc drift flagged (CONTEXT term vs PropertyBaselinePerformance class;
  stale domain/sap/ path → domain/sap10_calculator).

Also banners ara_backend_design.md as superseded (architecture) by ADR-0011/0012.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 16:20:06 +00:00
Jun-te Kim
5470fa1d93 move landlord overrides 2026-06-01 15:46:46 +00:00
Jun-te Kim
8a9d14a45c landlord overrids moved into one repo 2026-06-01 15:16:23 +00:00
Khalim Conn-Kowlessar
3cad599fd1 refactor(property-baseline): units on co2 / PEUI columns (PR #1139 review)
Make the stored units explicit on the property_baseline_performance columns:
- `*_co2_emissions` → `*_co2_emissions_t_per_yr` (tonnes CO₂/yr, whole dwelling)
- `*_primary_energy_intensity` → `*_primary_energy_intensity_kwh_per_m2_yr`

Column names only; the domain `Performance` VO stays unit-suffix-free (units are
a storage concern, mapped in from_domain/to_domain). Migration doc updated.
Round-trip stays green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:57:00 +00:00
Khalim Conn-Kowlessar
c3691d9af2 refactor(property-baseline): rename baseline → property_baseline aggregate (PR #1139 review)
Wholesale rename of the Baseline aggregate to PropertyBaseline for clarity /
to disambiguate from baselines that appear elsewhere in Modelling. Scoped to
this aggregate only — the distinct Rebaselining term (rebaseline_reason,
StubRebaseliner, RebaselineNotImplemented) is deliberately untouched.

- domain/baseline → domain/property_baseline; BaselinePerformance →
  PropertyBaselinePerformance.
- repositories/baseline → repositories/property_baseline; BaselineRepository
  / BaselinePostgresRepository → PropertyBaseline*.
- orchestration/baseline_orchestrator.py → property_baseline_orchestrator.py;
  BaselineOrchestrator → PropertyBaselineOrchestrator. BaselineStage →
  PropertyBaselineStage.
- infrastructure/postgres: baseline_performance_table.py →
  property_baseline_performance_table.py; table `baseline_performance` →
  `property_baseline_performance`; Model renamed.
- UnitOfWork attribute `.baseline` → `.property_baseline`.
- Docs: ADR-0004 references + migration doc (renamed to
  property-baseline-performance-table.md) updated.

CONTEXT.md glossary term ("Baseline Performance") left as-is pending a
ubiquitous-language call (raised on the PR). 123 tests pass; pyright strict
clean (only the unrelated pre-existing moto import errors remain).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 14:54:59 +00:00
Khalim Conn-Kowlessar
48a488d1e9 refactor(orchestration): wire stages onto the UnitOfWork; per-stage commit (#1138)
Replaces the handler's whole-pipeline Session (one transaction across all
three stages, connection pinned during Ingestion's external IO) with a
Unit-of-Work per stage (ADR-0012, added here). Each stage runs its batch in
one unit and commits once; any property raising aborts the batch and the
subtask fails noisily.

- BaselineOrchestrator(unit_of_work, rebaseliner): one unit for the batch,
  commit once. Raise on a pre-SAP10 property leaves the unit uncommitted.
- IngestionOrchestrator(unit_of_work, epc_fetcher, geospatial_repo,
  solar_fetcher): fetch/write split — phase 1 fetches the whole batch (EPC /
  coords / solar) with NO unit open; phase 2 writes in one unit and commits.
  The connection is never held during external IO. Geospatial S3 repo stays
  injected (reference data, not transactional).
- Handler: module-scoped engine (pool reused across warm invocations) + a UoW
  factory; whole-pipeline `with Session` gone. `build_first_run_pipeline`
  composes on the factory. Source clients still behind the raising seam.
- ADR-0012 records the decision (per-stage boundary, all-or-nothing batch,
  idempotent re-run, fetch/write split, module-scoped engine). Modelling stub
  left untouched (no-op, no DB) per the ADR.

Tests: orchestrators on a shared FakeUnitOfWork (assert persisted batch +
exactly-once commit + no-commit-on-raise). New real-DB E2E integration test:
real PostgresUnitOfWork, Ingestion writes the EPC → Baseline reads it back
through the repo → re-run replaces, not duplicates (1 EPC row, 1 baseline row
after two runs). 121 pass in tests/; pyright strict clean; AAA.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 09:54:47 +00:00
Khalim Conn-Kowlessar
76717dfc3a feat(baseline): BaselineOrchestrator + BaselinePerformance aggregate (#1135)
Stage 2 of First Run. Establishes each Property's Baseline Performance
from persisted source data and writes it back — reads only from repos,
never a Fetcher or HTTP (ADR-0003), so it is byte-identical whether
Ingestion ran milliseconds ago or last week.

Domain (`domain/baseline/`):
- `Performance` VO — the four rated quantities: SAP / EPC Band / CO2 /
  Primary Energy Intensity. `lodged_performance(epc)` reads them off the
  EPC's recorded fields (PEUI = `energy_consumption_current`).
- `BaselinePerformance` (ADR-0004) — the paired `lodged` + `effective`
  Performance + `rebaseline_reason`, plus the no-derivation part of the
  energy block (`space_heating_kwh` / `water_heating_kwh`, off the RHI,
  deterministic per ADR-0006). Both halves always populated.
- `Rebaseliner` port + `StubRebaseliner`: the re-score-on-override seam
  (ADR-0011). SAP10 certs pass through (effective == lodged, reason
  "none"); a pre-SAP10 cert raises `RebaselineNotImplemented` rather
  than fabricating a plausible-but-wrong "none" — ML rebaselining is not
  wired yet. Mirrors the repo's strict-raise culture.

Persistence: new `BaselineRepository` port + `BaselinePostgresRepository`
+ flat-column `baseline_performance` SQLModel (one row per Property). Per
ADR-0004's amendment this is a standalone table, NOT columns on the
retiring `property_details_epc`. Production migration is FE-owned
(Drizzle) — docs/migrations/baseline-performance-table.md.

Docs (grill-with-docs): corrected CONTEXT.md Lodged/Effective Performance
to Primary Energy Intensity (the term collided with its own _Avoid_ entry
under "heat demand") + fixed stale RHI field names; amended ADR-0004
Consequences for the standalone-table decision.

Fuel split + bills (rest of EPC Energy Derivation) deferred to a
follow-up — they need a Fuel Rates source (Ofgem-cap ETL) that does not
exist yet.

TDD, one test -> one impl: 7 tests (lodged read, rebaseliner pass-through
+ raise, orchestrator establish-and-persist + pre-SAP10 raise, Postgres
round-trip + absent). pyright strict clean; AAA layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 21:21:34 +00:00
Khalim Conn-Kowlessar
311d1e751a feat(epc): persist renewable_heat_incentive — full round-trip equality (#1137)
Add epc_renewable_heat_incentive table (space_heating_kwh, water_heating_kwh +
the three insulation-impact kWh fields), wired into EpcPostgresRepository
save/get. This is the P0 gap: RenewableHeatIncentive carries the baseline
space-heating/hot-water kWh that EPC Energy Derivation consumes.

The round-trip test now asserts full deep-equality (dropped the
renewable_heat_incentive exclusion) and passes for RdSAP 21.0.0 + 21.0.1.
DB migration for the new table documented in
docs/migrations/epc-property-round-trip-fidelity.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 19:30:18 +00:00
Khalim Conn-Kowlessar
5f0a3b8f65 feat(epc): EPC persistence round-trip fidelity + JSONB code columns (Slice 1 #1129)
Relocate EpcPropertyModel + child tables from the dying backend/ tree to
infrastructure/postgres/epc_property_table.py (re-export shim keeps
documents_parser working). Add EpcRepository port + EpcPostgresRepository with
a full reverse mapper (epc_property tables -> EpcPropertyData).

Round-trip test surfaced two fidelity gaps:
 1. Union[int,str] SAP code fields were str()-coerced on save, losing the int
    (API) vs str (Site Notes) distinction. Now stored as JSONB (type-preserving).
 2. The schema was a partial projection. Closed the cheap gaps on the model
    (heating shower/bath counts, roof_construction_type, curtain_wall_age,
    addendum, mechanical_vent_duct_insulation_level, SAP 10.2 §2 ventilation
    fields + a ventilation_present flag). Structural gaps tracked as follow-ups;
    renewable_heat_incentive (P0, #1137) excluded from the assertion until landed.

Round-trip passes for RdSAP-Schema-21.0.0 and 21.0.1; pyright strict clean.
Migration inventory for the DB: docs/migrations/epc-property-round-trip-fidelity.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 19:26:18 +00:00
Khalim Conn-Kowlessar
5aebd90ef7 docs(ara): composable stage-orchestrator design (ADR-0011 + ADR-0003 amend + CONTEXT)
Records the grill-with-docs outcomes for the ara_first_run rebuild: three
composable stage orchestrators (Ingestion/Baseline/Modelling), one lambda per
use case chaining them through repos (not in-memory), and the Fetcher-vs-Repo
data-source taxonomy. Amends ADR-0003's chaining rule to generalise beyond
RefreshOrchestrator. Adds the pipeline-composition + First Run vocabulary to
CONTEXT.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 19:26:17 +00:00
Jun-te Kim
429e138ea7 merge conflicts 2026-05-28 14:00:19 +00:00
Jun-te Kim
8422041215 landlord overrid orchestration 2026-05-26 15:27:45 +00:00
Khalim Conn-Kowlessar
a7b08a4e8f refactor: move docs/sap-spec/ contents into domain/sap10_calculator/
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>
2026-05-26 13:17:18 +00:00
Khalim Conn-Kowlessar
68401c517a refactor: lift-and-shift packages/domain/src/domain/ml → domain/sap10_ml
Sibling migration to the sap10_calculator move — `domain.ml` now lives
at the root-level layout (`domain/sap10_ml/`) matching the pattern
already used by `domain.addresses`, `domain.tasks`, `domain.postcode`,
and `domain.sap10_calculator`.

Changes:

- `git mv packages/domain/src/domain/ml → domain/sap10_ml` (19 files;
  history preserved).
- Subpackage rename: `domain.ml` → `domain.sap10_ml`. 32 references
  rewritten across .py and .md files: 11 internal + 21 external
  (datatypes/epc/domain/mapper.py, 14 files in domain/sap10_calculator,
  2 backend tests, 2 ADRs, 1 README, 1 design doc).
- Path-string updates: `pytest.ini` testpath
  `packages/domain/src/domain/ml/tests` → `domain/sap10_ml/tests` so
  ML tests stay in the default auto-discovered sweep. `CONTEXT.md`
  also updated.

`packages/domain/src/domain/` is now empty — the workspace `domain/`
tree has been fully migrated. Together with the `domain/__init__.py`
deletions from the sap10_calculator commit (29ac35cc), `domain` is
now a single root-level namespace package with subpackages
{addresses, sap10_calculator, sap10_ml, tasks} + the standalone
`postcode.py` module.

Verified:

- Focused sweep (backend mapper-chain + sap10_calculator worksheet
  e2e + golden fixtures): 99 passed / 19 failed — identical baseline.
- Wider sweep (all sap10_calculator + sap10_ml): 1654 passed / 20
  failed (same pre-existing failures).
- domain/sap10_ml/tests: 210/210 PASSED at new path.
- Pyright net-zero: heat_transmission.py 13, cert_to_inputs.py 35,
  mapper.py 33, rdsap_uvalues.py 1 (all unchanged from baseline).

Note: `packages/domain/pyproject.toml` still declares
`packages = ["src/domain"]` for the hatchling wheel — that target
directory is now empty and the wheel build is effectively a no-op.
Retiring the workspace package or repointing the wheel is a follow-up.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 13:01:35 +00:00
Khalim Conn-Kowlessar
29ac35ccbe refactor: lift-and-shift packages/domain/src/domain/sap → domain/sap10_calculator
Migration of the SAP 10.2 calculator package from the uv-workspace
src-layout (`packages/domain/src/domain/sap`) to the root-level layout
(`domain/sap10_calculator`), matching the pattern already used by
`domain.addresses` / `domain.tasks` / `domain.postcode`.

Changes:

- `git mv packages/domain/src/domain/sap → domain/sap10_calculator`
  (92 files; git auto-detected all as renames so blame/history is
  preserved).
- Subpackage rename: `domain.sap` → `domain.sap10_calculator`. 48
  Python files rewritten (`from domain.sap.X` → `from domain.sap10_
  calculator.X`); zero remaining `domain.sap` refs after the sed pass.
- Path-string updates: 3 .py files (test fixtures + xlsx loader) +
  6 markdown docs (CONTEXT.md, 2 ADRs, 3 sap-spec docs, sap10_
  calculator/README.md) had hard-coded `packages/domain/src/domain/
  sap/...` paths rewritten to `domain/sap10_calculator/...`.
- `Path(__file__).parents[N]` rebasing: the old tree was 3 levels
  deeper than the new one (`packages/domain/src/`), so 4× `parents[7]`
  became `parents[4]` and 1× `parents[6]` became `parents[3]` across
  `tables/pcdb/{__init__.py, postcode_weather.py, etl.py}`,
  `worksheet/tests/_xlsx_loader.py`, and `tests/test_pcdb_etl.py`.
- PEP 420 namespace package: deleted both `domain/__init__.py`
  (root + workspace, both load-bearing only as empty/docstring) so
  Python combines `domain.sap10_calculator` (root) and `domain.ml`
  (workspace) into one namespace package. Confirmed via
  `domain.__path__ == ['/workspaces/model/domain',
  '/workspaces/model/packages/domain/src/domain']`. Without this,
  the root `domain/__init__.py` shadowed the workspace one and
  `domain.ml` was unreachable.

Verified:

- Full sweep (`backend/documents_parser/tests/test_summary_pdf_
  mapper_chain.py + domain/sap10_calculator/worksheet/tests/test_
  e2e_elmhurst_sap_score.py + domain/sap10_calculator/rdsap/tests/
  test_golden_fixtures.py`): 99 passed / 19 failed — exact same
  counts as pre-refactor. All 19 failures pre-existing (9 hand-built
  001479 + 6 cohort diff + 4 cohort chain non-spec).
- Wider sweep (all sap10_calculator + domain.ml): 1654 passed /
  20 failed (the +1 vs the focused sweep is the pre-existing
  `test_roof_insulated_assumed_with_ni_thickness_uses_50mm_per_
  section_5_11_4` which was already failing on the previous baseline).
- Pyright net-zero on the three load-bearing baselines:
  `heat_transmission.py` 13, `cert_to_inputs.py` 35, `mapper.py` 33.

Lift-and-shift only — no semantic renames (`Sap10Calculator` stays
`Sap10Calculator`), no testpaths edits in pytest.ini (sap tests
continue to be invoked by explicit pytest paths).

Note: `domain.ml` still lives at `packages/domain/src/domain/ml/`.
Migrating it would close out the dual-`domain/` layout but is
out of scope for this commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 12:22:37 +00:00
Khalim Conn-Kowlessar
a75052dcca chore: commit cert 001479 fixture + RdSAP/PCDF spec PDFs
Three load-bearing files that the post-Slice-95 tests and docs cite
but were never tracked:

1. `packages/domain/src/domain/sap/rdsap/tests/fixtures/golden/
   0535-9020-6509-0821-6222.json` — API JSON for cert 001479
   (Elmhurst worksheet P960-0001-001479, lodged 31 Oct 2025).
   Required by `test_api_001479_full_chain_sap_matches_worksheet_pdf_
   exactly` (Slice 95's Layer 4 1e-4 gate) and by
   `test_golden_cert_residual_matches_pin` (residual-from-integer
   pin path). Without this committed, both tests fail to find the
   fixture file.

2. `docs/sap-spec/RdSAP 10 Specification 10-06-2025.pdf` — replaces
   the previously-tracked `rdsap-10-specification-2025-06-10.pdf`
   (same content, cleaner filename). Cited from 5 source files
   (`table_32.py`, `pcdb/parser.py`, README.md, SAP_CALCULATOR.md,
   NEXT_AGENT_PROMPT.md) and every spec-citation commit message
   in Slices 87-95. Git auto-detected the rename.

3. `docs/sap-spec/PCDF_Spec_Rev-06b_12_May_2021.pdf` — cited from
   `pcdb/parser.py:69` and the §4-water-heating combi-loss
   docstrings; needed to validate the PCDB Table 3a/3b/3c routing
   logic.

Also fixes the one stale reference in `test_dimensions.py:471`
that still pointed to the old `rdsap-10-specification-2025-06-10
.pdf` filename — now points to the renamed file.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:36:12 +00:00
Khalim Conn-Kowlessar
b2c6a57247 docs: refresh handover + cert 0240 notes after Slice 95
Status: Slice 95 closed Layer 4 (API → cascade SAP) on cert 001479 at
< 1e-4 vs worksheet 69.0094. Production goal MET; the
`test_api_001479_full_chain_sap_matches_worksheet_pdf_exactly` test
formalises this gate. Updates to keep the next agent honest:

- NEXT_AGENT_PROMPT: header + status table + cumulative SAP delta table
  + "First action" + epilogue all reflect Slice 95's close-out.
- NEXT_AGENT_PROMPT §4 (Outlier golden cert investigations): rewrote
  the cert 0240 entry. The earlier "Type-1 RR gable_wall_lengths not
  extracted" claim is stale — mapper.py:1349-1369 already extracts
  them (Slices 71-86). The -15 SAP residual is a mix, dominated by
  the windows subsystem (11 windows × 18.28 m² with default U≈2.27
  because Slice 93's `_API_GLAZING_TYPE_TO_TRANSMISSION` only covers
  glazing codes 3 and 13; cert 0240 lodges code 2). Surfacing
  glazing_type=2 (and likely other unmapped codes) is the biggest
  single-slice leverage point — and would touch 6035 too.
- test_golden_fixtures.py cert 0240 `notes:` field: replaced the
  stale RR hypothesis with the actual cascade subsystem breakdown
  and the glazing_type-2 surfacing recommendation.

No production code changed; docs and a `_GoldenExpectation.notes`
string only. test_golden_fixtures.py stays GREEN (14 passed).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 10:32:18 +00:00
Khalim Conn-Kowlessar
985a59e1f9 docs: rewrite NEXT_AGENT_PROMPT for Slice 87-94 state
Cert 001479 API path closed from +3.08 → +0.0006 SAP delta vs
worksheet 69.0094 in Slices 87-94. Fabric heat loss is now EXACT
across all 6 components. Replaced the prior handover (which assumed
the Elmhurst path was still RED with a 0.26 SAP gap on cohort 000474)
with the current state:

- Acceptance criterion corrected: 1e-4 against worksheet continuous
  SAP (not ±0.5 against API integer) when a worksheet is available.
- Validation layer status table reflects current GREEN/RED state.
- Slice 87-94 progression captured with each fix's SAP delta impact.
- Diagnostic probe + queue documented for next agent: close 001479's
  residual +0.0006 (HW + gains), write Layer 3 diff test, then
  process new cert pairs as user sources them.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 08:41:15 +00:00
Khalim Conn-Kowlessar
86eff23f08 Handover: Layer-2 cohort 000474 GREEN; reframe with production end-goal first
User reframed the end goal explicitly: the production flow is
`API JSON → EpcPropertyDataMapper.from_api_response → SAP calculator`
landing within ±0.5 of the API-published SAP. The Elmhurst-site-notes
work is the cross-validation route — same dwelling, independent path
into EpcPropertyData. Once both routes agree on cert 001479, the API
mapper is validated by transitivity.

Restructure the handover around four nested validation layers:

  Layer 1 (hand-built cascade pin):  6 cohort certs GREEN; 001479 partial
  Layer 2 (Elmhurst ≡ hand-built):   cohort 000474 GREEN; 5 others pending
  Layer 3 (API ≡ Elmhurst):          test doesn't exist yet
  Layer 4 (API cascade ±0.5):        72.08 vs 69 (delta +3.08)

Each layer validates the one below. Closing inner-most first means
upper layers can lean on it as reference.

Documents tools/patterns built in slices 63-70:
- `_LOAD_BEARING_FIELDS` allow-list (~40 cascade/semantic fields)
- `_NON_LOAD_BEARING_WINDOW_SUBFIELDS` deny-list (descriptive int/str
  encoding noise)
- `_diff_load_bearing` recursive helper (strict-pyright-clean)
- `test_from_elmhurst_site_notes_matches_hand_built_NNNNNN` tracer-
  bullet pattern (000474 is the worked example)

Next-step ordering: parametrize over 5 other cohort certs, complete
001479 hand-built (currently 2/11 cascade pins green; gap −3.02 SAP),
add cert 001479 to diff test, then add API mapper → hand-built diff
test, then the production-flow acceptance pin in test_golden_fixtures
for cert 001479.

Lists source-data caveats (the M-vs-L Ext1 age discrepancy on 001479).
Conventions to honour (AAA, abs(diff)<=tol, one slice=one commit,
1e-4 Elmhurst / 0.5 API, no widening, pyright net-zero). Cached
artefacts (golden JSON, Summary PDF, worksheet PDF) noted.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 17:35:28 +00:00
Khalim Conn-Kowlessar
7e1269fc8e Handover: hand-built fixture skeleton landed (Slice 62); 2/11 pins green
Update NEXT_AGENT_PROMPT.md with the pivot to the rigorous cohort
pattern: cert 001479's hand-built `_elmhurst_worksheet_001479.py`
becomes the ground-truth EpcPropertyData. Cross-mapper parity work
then collapses to "both mappers produce hand-built-equivalent
EpcPropertyData".

Two parallel workstreams documented:

1. Iterate the hand-built skeleton (Slice 62) until all 11 cascade
   pins hit 1e-4. Current state: 2/11 green (pumps_fans, lighting);
   sap_score_continuous gap −3.02 SAP. Likely next slices: HW demand
   routing, §2 ventilation tuning, thermal mass parameter, multiple-
   glazed proportion.

2. Once hand-built is GREEN, add `test_elmhurst_mapper_matches_hand_
   built` + `test_api_mapper_matches_hand_built` over the 7-cert
   cohort (000474..000516 + 001479). Every field diff = mapper bug
   to close. Cross-parity collapses to "both mappers produce
   hand-built-equivalent".

Documents the M-vs-L Ext1 age-band source-data conflict (hand-built
uses worksheet's L; Elmhurst mapper trusts Summary's M) — surfaces
as a known caveat in cross-mapper diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 08:12:30 +00:00
Khalim Conn-Kowlessar
0e4f4c051a Handover: TDD red-green session — 4 more slices (58-60) + RED chain pin
Update NEXT_AGENT_PROMPT.md for the TDD session that landed 3 more
slices on top of Session 1's fabric work:

  58: secondary fuel cost routes through lodged secondary_fuel_type
      (closes the biggest single gap on cert 001479 — 9 SAP)
  59: heat_transmission apportions windows per bp via window_location
  60: thermal bridging y uses primary bp's age (dwelling-wide)

Chain pin `test_summary_001479_full_chain_sap_matches_worksheet_pdf_
exactly` is committed RED as the load-bearing TDD forcing function:

  Pre-workstream: delta +5.84 SAP (cascade 63.17 vs target 69.0094)
  Post-Slice 60: delta −1.19 SAP (cascade 70.20 vs target 69.0094)

Per-bp fabric U-values all match the worksheet exactly. Remaining
1.19 SAP overshoot maps to ~3 W/K of HLC undercount in roof + floor:

- Ext2 PS sloping-ceiling roof area uses floor projection (1.92 m²)
  instead of slant area (2.22 m²). −0.81 W/K.
- Main ground-floor U: `u_floor` Table 19 returns 0.60 for age C;
  worksheet expects 0.65 (same as age B). −1.52 W/K.
- (31) external area under-count drives bridging gap. −2.08 W/K.

Slice 61 (SapFloorDimension.floor_lodged_u_value override using
Summary §9 "Default U-value") was attempted and reverted: closed
001479 floor gap exactly but broke 000474 cohort's 1e-4 pin (its
cascade calibration uses u_floor age-B 0.77 vs Summary's lodged
0.75). Next session needs a different fix — Table 19 audit for
age C, or selective override.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 23:54:29 +00:00
Khalim Conn-Kowlessar
a0d9d09410 Handover: 4 cert-001479 slices in (54-57); gap at +7.62 SAP; non-fabric next
Update NEXT_AGENT_PROMPT.md with current branch state for cert 001479
work. Slices 54-57 closed Elmhurst-side mapper gaps surfaced by the
cross-mapper diff against the new GOV.UK API counterpart:

  54: extensions_count from len(survey.extensions)
  55: party-wall code "CU" → cavity unfilled U=0.5
  56: floor "E To external air" → u_exposed_floor (Table 20)
  57: PS sloping-ceiling + As Built + pre-1950 → thickness=0 → U=2.30

Per-bp fabric U-values all match worksheet exactly now. Cascade SAP
went 63.17 → 61.39 (gap widened to +7.62) as each fix exposed
previously-masked over-counting elsewhere; per-data-correct moves.

Remaining ~15 W/K HLC gap (HLP cascade 2.235 vs worksheet 3.127)
lives in non-fabric: living_area_fraction TFA convention, internal
gains, secondary heating SAP-code wiring, possibly thermal bridging
and ventilation HLC.

Documents one source-data caveat: Summary §3 says Ext1 age "M 2023
onwards", worksheet header says "Ext1: L" — assessor inconsistency;
trust Summary per session policy.

758 cohort tests + cert-001479 structural pins green; pyright net-zero
on touched files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 22:41:24 +00:00
Khalim Conn-Kowlessar
a756114aed Handover: all 6 Elmhurst Summary→SAP chains closed at 1e-4
Final state across Slices 47-53:

  000474   0.0000  ✓ Slice 47
  000477   0.0000  ✓ Slice 52
  000480   0.0000  ✓ Slice 50
  000487   0.0000  ✓ Slice 53
  000490   0.0000  ✓ Slice 49
  000516   0.0000  ✓ Slice 51

758 tests pass; pyright net-zero (35 baseline). Updates the handover
doc with a summary of each slice's contribution and a pointer to
likely next workstreams.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 21:43:40 +00:00
Khalim Conn-Kowlessar
ec4916b5a7 Handover: 2/6 Elmhurst chains closed at 1e-4; per-cert diagnoses for remaining 4
Updates NEXT_AGENT_PROMPT.md after Slices 47/48/49. State at hand-off:

  000474   Δ=0.0000  ✓ Slice 47
  000477   Δ=2.6555     Room-in-Roof support needed (15.06 m² 3rd storey)
  000480   Δ=4.1955     diagnosis pending
  000487   Δ=4.4553     extractor drops most §11 windows on this layout
  000490   Δ=0.0000  ✓ Slice 49
  000516   Δ=1.5162     roof-window separation (1 of 6 extracted windows
                        is actually a roof window per handbuilt fixture)

Each remaining cert needs its own schema/extractor/mapper extension —
documented with file/method pointers and recommended slice ordering.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 20:21:27 +00:00
Khalim Conn-Kowlessar
00a27efd87 Slice 48: Elmhurst extractor handles 3 new layout quirks; 5 fixture PDFs added
The §11 Windows table in the Summary PDF doesn't lay out identically
across the cohort. Three new quirks added to the layout-style parser
so the remaining 5 certs can be debugged with windows actually
extracted:

1. `Wood 0.70` combined frame_type+frame_factor line — previously the
   parser expected them on separate lines (data+1 / data+2) and
   rejected the window when the joined form appeared.
2. Trailing glazing-type on the data line — `1.22 1.76 2.15 Double
   pre 2002` is the joined-cell variant in 000516; the W/H/Area
   anchor now captures the trailing phrase as an optional 4th group
   and feeds it through as `inline_glazing_type`, bypassing the
   separate-line glazing-prefix scan.
3. Cross-window gap with no glazing marker — `_partition_after_manuf`
   now falls back to "second orientation token in gap" when no
   glazing-type-prefix word appears. Covers the 000516 layout where
   each window has prefix+suffix orient tokens (no inline orient)
   and the glazing-type is joined-to-data.

The 5 remaining Summary PDFs are copied into
`backend/documents_parser/tests/fixtures/` ready for per-cert mapper
work. Mirror pin tests deferred — each cert still has its own diff
to close (handover in NEXT_AGENT_PROMPT.md documents the per-cert
state, e.g. 000477 needs secondary-heating extraction, 000516 needs
roof-window separation).

Current cohort SAP deltas vs the U985 worksheet PDFs (target 1e-4):

  000474   0.0000  ✓
  000477  +6.3655     secondary heating + lighting
  000480  +8.2695     diagnosis pending
  000487  +8.1433     extractor still drops windows
  000490  +5.6551     diagnosis pending
  000516  +5.9812     roof-window separation

Wider regression stays green (754 pass). Pyright net-zero on
touched files.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 19:17:59 +00:00
Khalim Conn-Kowlessar
b6544e1cd1 Handover: tighten Summary→SAP chain pin to 1e-4 + brief next agent
Slice 46c left the chain at SAP Δ=0.26 vs the Elmhurst worksheet PDF's 62.2584. The user rejected the 0.5 tolerance: because the cascade reproduces Elmhurst exactly on hand-built inputs and the Summary PDF carries the same source-of-truth data, the mapped path must hit 1e-4 like every other Elmhurst worksheet pin.

This commit:
- Tightens `test_summary_000474_full_chain_sap_matches_worksheet_pdf_exactly` from 0.5 to 1e-4. Currently fails with Δ=0.2611 — the forcing function for the next slice.
- Replaces the stale `docs/sap-spec/NEXT_AGENT_PROMPT.md` with a fresh handover identifying the two remaining diffs:
  * pumps_fans_kwh_per_yr 130 vs 160 (30 kWh; likely `central_heating_pump_age` not plumbed)
  * Window [4] mis-classified as SE (4) instead of E (3); `_compose_window_descriptors` over-joins suffix tokens
- Documents the architectural smell (3-schema chain ElmhurstSiteNotes → EpcPropertyData → CalculatorInputs may be over-engineered).
- Lists end-goal: API-path < 0.5 SAP (rounded integers), Elmhurst-path < 1e-4 SAP (unrounded worksheet pins), then replicate for the other 5 Summary PDFs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 18:43:14 +00:00
Khalim Conn-Kowlessar
d44af109a9 Docs: SAP calculator module README + API integration test handover
The SAP 10.2 / RdSAP 10 calculator is closed at 930/930 pin tests green.
Tidying the docs for hand-off to the API-integration agent.

New: docs/sap-spec/SAP_CALCULATOR.md
  Canonical module overview — public API surface, two-cascade
  architecture (Rating UK-avg, Demand postcode), simulator-use-case
  example, file map, validation contract + hard rules, fixture cohort
  notes, spec page references. Replaces the scattered "what's the
  shape" knowledge that was previously only in commit messages.

Rewritten: docs/sap-spec/HANDOVER_NEXT.md
  Old handover (work queue for slices 26-36) is obsolete. Replaced
  with the next agent's brief: build an API → SAP scoring integration
  test using the 6 Elmhurst fixtures. Includes a copy-paste reference
  scoring path, expected outputs per fixture, list of files to read
  on day 1, and scope guardrails.

Refreshed module docstrings:
  - cert_to_inputs.py: now describes both cascades, the deferred-edge-
    case list reflects current state (RR/secondary/§15 living-area
    rounding all DONE; thermal-mass and control-temp adjustment still
    deferred).
  - calculator.py: per-end-use CO2/PE factor machinery documented;
    stale "single-fuel approximation" claim removed (closed in slice 32).
  - sap/README.md: validation paragraph now says "930/930 green" and
    points to SAP_CALCULATOR.md instead of the obsolete HANDOVER_NEXT.

Verified the API examples in both docs produce the expected per-fixture
outputs (SAP=62, EI=60, Carbon=3104.1222, PE=16931.7227 for 000474).
Wider regression: 1585/1585 PASS, zero failures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 10:04:34 +00:00
Khalim Conn-Kowlessar
144f08533f Docs: rewrite HANDOVER_NEXT.md for fresh agent pickup post-slice-25d
§1-§6 fully close (252/252). §7 closes 52/60 (LINE_92/93 marginal on 4
fixtures). §8-§12 not yet pinned. Handover now reads top-to-bottom with
current scoreboard, per-section work queue, spec page reference index,
and the section helper map for the new agent to extend.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:17:43 +00:00
Khalim Conn-Kowlessar
147da90a5a Slice 25d: 000487 §4 LINE_65 closure — derive LINE_64A from cert (App J step 8)
Closes the final §4 cascade fail. SAP10.2 Appendix J step 8 (p.82)
specifies the electric-shower kWh formula:

  N_ES = N_shower / N_outlets             (eq J16)
  EES,j,m = N_ES × f_beh × P_ES,j × 0.1 × n_m   (eq J17)
  EES,m = Σ EES,j,m                       (eq J18)

where P_ES,j defaults to Table J4 (p.83) row "Instantaneous electric
shower" = 9.3 kW for assessments of existing dwellings, and 0.1 = the
6-minute shower duration in hours.

For 000487 (N=2.492, has_bath, 1 electric shower, 0 mixer outlets):
  N_shower = 0.45 × 2.492 + 0.65 = 1.7714
  N_outlets = 1 (just the electric)
  N_ES = 1.7714 / 1 = 1.7714
  Jan: 1.7714 × 1.035 × 9.3 × 0.1 × 31 = 52.86 kWh ≈ PDF LINE_64A[1] = 52.8566 ✓

LINE_65 (heat gains from water heating) was undercounting by 25% of
the missing LINE_64A (the recovery factor for instantaneous electric
showers per the heat-gains formula); deriving LINE_64A from cert
closes it.

Changes:
- water_heating.py: new `electric_shower_monthly_kwh` function +
  `electric_shower_count` parameter to `water_heating_from_cert`.
  When count > 0 and no override, derives LINE_64A from N_outlets +
  Table J4 default P_ES.
- cert_to_inputs.py: `_electric_shower_count_from_cert` helper +
  plumb through both the §4 section helper and internal cascade.

Per-fixture cluster status (was/now):
  §3   24/24 → 24/24  ✓ all 6 fixtures
  §4   53/54 → 54/54  ✓ all 6 fixtures
  §5   52/54 → 54/54  ✓ all 6 fixtures
  §6   11/12 → 12/12  ✓ all 6 fixtures
  §7   45/60 → 52/60  (000487 cascade closed; LINE_92/93 marginal on
                       000474/477/480/490 remains)

Scoreboard:
  section_cascade_pins: 293 → 304 PASS (+11; 97.4% closure)
  e2e SapResult:         32 →  33 PASS (+1, water_heating closure cascades)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:08:32 +00:00
Khalim Conn-Kowlessar
8520a52ee9 Slice 25c: 000477 §4/§5/§6 closure — Table 3c (p.162) M+L lower bound
Fixed a single-character spec adherence bug: SAP10.2 Table 3c (p.162)
specifies the M+L profile's DVF lower bound as `V_d,m < 100.2`, not
`< 100.0`. The 0.2 L/day window matters when V_d,m sits between 100.0
and 100.2 — exactly where 000477's May lodgement lands (100.16 L/day).

For V_d,m = 100.16:
  Spec:    DVF = 0 → (61) = E × r1 × fu = 134.84 × 0.015 × 1.0 = 2.0225 ✓
  Buggy:   DVF = 100.2 - 100.16 = 0.04 → (61) = 2.0233 (off by 0.0008)

The cascade through the missing 0.0008 W on May LINE_61 propagated to
LINE_62/64/65 and then §5 LINE_72/73 + §6 LINE_84 — clearing one
constant unblocks the entire 000477 §4-§6 cluster.

Per-fixture cluster status (was/now):
  §3   24/24 → 24/24
  §4   46/54 → 53/54   (only 000487 LINE_65 remains)
  §5   50/54 → 52/54   (only 000487 LINE_72/73)
  §6   10/12 → 11/12   (only 000487 LINE_84)

All remaining cascade failures cluster on 000487 (slice 25d — derive
LINE_64A electric-shower kWh from cert per Appendix J step 8) plus §7
LINE_92/93 marginal residuals on 4 fixtures (precision artefact).

Scoreboard:
  section_cascade_pins: 286 → 293 PASS (+7)
  e2e SapResult:         32 →  32 PASS (still cascade-blocked by 000487
    LINE_65 + downstream §8-§12 pins not yet asserted)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:04:23 +00:00
Khalim Conn-Kowlessar
ca56fdee5b Slice 25b: 000487 §4 closure (7/8) — has_electric_shower routes Nbath
Closes §4 LINE_43 + LINE_44/45/46/61/62/64 for 000487 (7 of 8 fails).
LINE_65 still fails — needs Appendix J step 8 (electric-shower kWh
derivation from cert) to land before LINE_65 heat gains close.

Spec citation: SAP10.2 Appendix J (p.81) step 2a: `Nbath = 0.13N + 0.19
if shower also present; = 0.35N + 0.50 if no shower present`. The
"shower also present" branch fires when ANY shower is lodged — mixer OR
electric — per the implicit reading that step 1a's Noutlets includes
electric showers in the count.

Changes:
- SapHeating gains `electric_shower_count` + `mixer_shower_count`.
- `water_heating_from_cert` gains `has_electric_shower: bool = False`;
  combined with mixer-flow-rate presence to drive `has_shower`.
- `_mixer_shower_flow_rates_from_cert` honors `mixer_shower_count`
  (default 1 vented when unlodged — preserves legacy behaviour).
- `_has_electric_shower_from_cert` new helper.
- `water_heating_section_from_cert` plumbs `has_electric_shower`
  through bootstrap + final call (and the internal cert_to_inputs path).
- 000487 fixture: `electric_shower_count=1, mixer_shower_count=0`.

§4 per-fixture:
  fixture | LINE_42 | LINE_43 | LINE_44-46 | LINE_61-65
  000474  |   ✓     |   ✓     |    ✓       |   ✓ (9/9)
  000477  |   ✓     |   ✓     |    ✓       |   ✗ LINE_61/62/64/65 (slice 25c)
  000480  |   ✓     |   ✓     |    ✓       |   ✓ (9/9)
  000487  |   ✓     |   ✓     |    ✓       |   ✓ except LINE_65 (8/9)
  000490  |   ✓     |   ✓     |    ✓       |   ✓ (9/9)
  000516  |   ✓     |   ✓     |    ✓       |   ✓ (9/9)

Scoreboard:
  section_cascade_pins: 279 → 286 PASS (+7)
  e2e SapResult:         32 →  32 PASS (unchanged — LINE_65 cascade still
    open, blocks downstream §5 LINE_72/73 + §6 LINE_84 + §7 + downstream)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 22:44:40 +00:00
Khalim Conn-Kowlessar
015144361a Slice 25a: 000487 §3 full closure — RR detailed surfaces + gable_wall_external + roof-area-as-max + half-up rounding
§3 cascade pins now close at abs=1e-4 for all 6 fixtures (was 5 of 6 with
000487 the holdout). Five spec-grounded changes:

1. SapRoomInRoofSurface gains optional `u_value` override + new kind
   `gable_wall_external` per RdSAP10 Table 4 (p.22) row 1 (exposed gable,
   U "as common wall" with assessor-lodged override). Routes to (29a)
   walls + LINE_31 external area.

2. SapAlternativeWall gains optional `u_value` override — assessor-lodged
   measured U bypasses the Table 6 cascade. 000487 Ext1 has a 9-mm
   TimberWallOneLayer at U=1.90 outside the Table 6 buckets.

3. _part_geometry uses MAX of floor areas (not top) for roof area, per
   RdSAP10 §3.8 (p.20): "Roof area is the greatest of the floor areas
   on each level". Fixes 000487 Ext1 where ground=7.13 m² > first=5.63.

4. Replace Python `round()` (banker's) with `_round_half_up` for §15
   element-area rounding. Banker's rounds 17.125 → 17.12; SAP convention
   rounds half-up → 17.13. Boundary case appears in 000487 Ext1 party
   wall area (party_length 6.25 × height 2.74 = 17.125).

5. 000487 fixture lodges 5 detailed RR surfaces (party gable, external
   gable @ U=0.86, flat ceiling, stud wall, slope), roof_insulation_
   thickness=300 (both parts → U=0.14), is_exposed_floor=True on Ext1
   floor 0, and u_value=1.90 on the Ext1 alt wall.

§3 cascade per-fixture:
  field    | 474 | 477 | 480 | 487 | 490 | 516
  LINE_31  |  ✓  |  ✓  |  ✓  |  ✓  |  ✓  |  ✓
  LINE_33  |  ✓  |  ✓  |  ✓  |  ✓  |  ✓  |  ✓
  LINE_36  |  ✓  |  ✓  |  ✓  |  ✓  |  ✓  |  ✓
  LINE_37  |  ✓  |  ✓  |  ✓  |  ✓  |  ✓  |  ✓

Scoreboard:
  section_cascade_pins: 274 → 279 PASS (+5: §3 +4 for 000487, §7 +1
    cascade)
  e2e SapResult:         32 →  32 PASS (unchanged — downstream §8-§12
    pins not yet asserted)

§4 (000487) deferred to slice 25b — needs has_electric_shower routing
through the §4 cascade so Nbath uses the "0.13N+0.19" branch when only
electric showers are present.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 22:32:41 +00:00