Commit graph

5 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
d8cdee4e53 Slice S0380.125: map Elmhurst Summary "18 Hour" meter_type to EIGHTEEN_HOUR
The Elmhurst Summary §14.2 Meters section lodges the electricity meter
type as the bare RdSAP enum form "18 Hour", but `_METER_STR_TO_INT`
only carried the legacy "off-peak 18 hour" alias. All 41 P960-format
heating-system fixtures at `sap worksheets/heating systems examples/`
lodge meter_type "18 Hour", so `cert_to_inputs` strict-raised on every
one of them before this slice.

Per RdSAP 10 Specification §17 page 85 (Electricity meter row 10-2):

  > "Electricity meter: Dual/single/10-hour/18-hour/24-hour/unknown"

Per RdSAP 10 §12 page 62:

  > "if the meter is dual 18-hour/24-hour it is 18-hour/24-hour tariff"

So the bare "18 Hour" lodging routes directly to enum 5 (Off-peak 18
hour) → `Tariff.EIGHTEEN_HOUR`, bypassing the §12 Rules 1-4 dispatch
(which only fires for Dual meters that aren't 18-hour or 24-hour).

After this slice the heating-system corpus probe (`/tmp/probe_*.py`
across 41 variants of the same property × different heating systems)
shifts from "32 raises + 7 mapper gaps + 2 emitter gaps" to
"32 cascade-OK + 7 community-heating + 2 underfloor-emitter + 1
cylinder-size 'No Access'". The 32 newly-OK variants surface a
positive ΔSAP cluster (cascade SAP_c > worksheet SAP_c by +0.87..+30
across boiler types) — that residual layer is queued for the next
slice.

Extended handover suite at HEAD post-slice: **829 pass, 0 fail**
(baseline 775 + test_table_12a.py's 54 incl. the new "18 Hour" entry).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 23:51:47 +00:00
Khalim Conn-Kowlessar
9bfb852483 Slice S0380.90: 6 strict-raise dispatches + UnmappedSapCode promoted to shared module
Bundled slice closing the next 6 silent-fallback dispatch sites flagged
by the post-S0380.89 audit per [[reference-unmapped-sap-code]]:

  1. PV pitch (RdSAP 10 §11.1 — codes 1..5 → 0/30/45/60/90°)
  2. PV overshading (SAP 10.2 Table M1 — codes 1..4 → 1.0/0.8/0.5/0.35)
  3. Meter type (RdSAP cert enum 1..5 → Tariff enum)
  4. Tariff → (high, low) rate (RdSAP 10 Table 32 — 4 of 5 Tariffs)
  5. Heat-network DLF by age band (SAP 10.2 Table 12c — A..M)
  6. Secondary heating fraction by main_heating_category (SAP Table 11)

Each dispatch follows the established strict / total split:
  - Absent lodging (None / 0 / "") → cascade's modal-default value
  - Lodging present but unmapped → `UnmappedSapCode(field, value)`

`UnmappedSapCode` promoted from `cert_to_inputs.py` to new module
`domain/sap10_calculator/exceptions.py` so `tables/table_12a.py` can
raise it too (the meter-type dispatch lives there). `cert_to_inputs`
re-exports it for backward compat with existing test imports.

Corpus audit at HEAD 6d02d205 (full JSON sweep):

  PV pitch codes:           {2, 3}        — covered
  PV overshading codes:     {1, 2}        — covered
  meter_type codes:         {1, 2, 3}     — covered (incl. digit-string '2')
  main_heating_category:    {2, 4, 6, 7, 10} — covered

All corpus codes already in dispatch dicts — no production regression
expected.

**One silent runtime fix surfaced by the strict-raise rollout**: the
GOV.UK API lodges `meter_type` as a digit-string (e.g. `'2'`) on many
certs, but the original `_METER_STR_TO_INT` dict only had word aliases
("single", "dual", "unknown"). Pre-S0380.90 the digit-string fell
through to the silent `return Tariff.STANDARD` default. Adding a
`key.isdigit() → int(key)` short-circuit routes these through the int
enum correctly. Confirmed 125 golden cert fixtures previously running
on this silent default — all now passing with explicit STANDARD via
the int dispatch path (not via the silent fallback).

Tests (6 new, AAA-structure):

  - `test_pv_pitch_deg_full_table_coverage_per_rdsap_10_section_11_1`
  - `test_pv_overshading_factor_full_table_m1_coverage`
  - `test_meter_type_dispatch_full_table_12a_coverage` (incl. digit-string)
  - `test_tariff_high_low_rates_full_dispatch_coverage`
  - `test_heat_network_dlf_full_table_12c_age_band_coverage`
  - `test_secondary_heating_fraction_for_category_full_table_11_coverage`

Each test pins: spec-correct codes → expected dispatch result; absent
lodging → modal default; lodging present but unmapped → `UnmappedSapCode`
with field + value attached.

Test baseline: 574 pass (was 568 + 6 new) + 9 expected
`test_sap_result_pin[000565-*]` fails unchanged. Cohort + golden +
cert 9501 unaffected. Pyright net-zero per touched file.

Open silent-fallback inventory now empty per
[[reference-unmapped-sap-code]] — the cascade dispatch boundary is
now fully strict-raise-gated for code translations. Cascade VALUE
defaults (u_wall, u_floor, etc.) remain total per RdSAP §6.2.3.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-30 09:46:55 +00:00
Khalim Conn-Kowlessar
488492a927 Slice S0380.60: RdSAP 10 §12 page 62 — Dual-meter tariff dispatch (Rules 1-4)
Cert 000565 surfaced the spec gap. Worksheet shows "Electricity
Tariff: 10 Hour Off Peak" while the Summary PDF only lodges
"Electricity meter type: Dual" — no separate tariff-hour field is
exported. Elmhurst SAP picks 10-hour because RdSAP 10 §12 page 62
contains a published inference algorithm:

  > If the meter is dual 18-hour/24-hour it is 18-hour/24-hour tariff.
  > Otherwise the choice between 7-hour and 10-hour is determined as
  > follows.
  > 1. If the main heating system (or main system if there are two)
  >    is an electric CPSU (192) it is 10-hour tariff.
  > 2. Otherwise, if … electric storage heaters (401 to 409), or
  >    electric dry core or water storage boiler (193 or 195), or
  >    electric underfloor heating (421 or 422) — it is 7-hour tariff.
  > 3. If that has not resolved it then if … direct-acting electric
  >    boiler (191), or heat pump (211 to 224, 521 to 524, or
  >    database), or electric room heaters — it is 10-hour tariff.
  > 4. If none of the above applies it is 7-hour tariff.

Cert 000565 Main 1 SAP code 224 (ASHP) + Dual meter → Rule 3 →
10-hour. Matches the worksheet exactly.

New `rdsap_tariff_for_cert(meter_type, main_1_sap_code=...,
main_2_sap_code=..., main_1_is_heat_pump_database=...,
main_2_is_heat_pump_database=...)` implements the dispatch.
"or database" branch covers PCDB Table 362 heat-pump lodgements per
the spec's "or database" wording. Callers compute the boolean via
`heat_pump_record(main_heating_index_number) is not None`.

The pre-existing `tariff_from_meter_type(meter_type)` keeps its
contract for legacy call sites — returns SEVEN_HOUR as the Dual
default (the §12 Rule 4 fallback). Docstring updated to point at the
new helper for callers that need spec-correct dispatch.

Code sets (verbatim §12 page 62):
- `_RULE_1_CPSU_CODES` = {192}
- `_RULE_2_STORAGE_CODES` = {401..409, 193, 195, 421, 422}  (NOT 423/424/425)
- `_RULE_3_TEN_HOUR_CODES` = {191, 211..224, 521..524}
- electric room heater codes (Table 4a 6xx) deferred with TODO until a
  fixture surfaces them — Rule 4 fallback is correct in the interim
  (electric room heater certs would currently get 7-hour, biasing
  their cost residual; not on the active fixture front).

This commit is the FOUNDATIONAL change — no cost helpers are wired
to the new dispatch yet, so cohort/golden tests are unchanged
(354 pass + 10 expected 000565 fails). The next slice wires
`_space_heating_fuel_cost_gbp_per_kwh` / `_hot_water_fuel_cost_gbp_
per_kwh` / `_other_fuel_cost_gbp_per_kwh` to use the new dispatch +
Table 12a high-rate fractions for off-peak certs.

Spec source: `domain/sap10_calculator/docs/specs/RdSAP 10
Specification 10-06-2025.pdf` §12 page 62. Verified verbatim per
[[feedback-verify-handover-claims]] before implementing.

Pyright net-zero (0 / 0).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 23:42: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
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
Renamed from packages/domain/src/domain/sap/tables/table_12a.py (Browse further)