Commit graph

34 commits

Author SHA1 Message Date
Khalim Conn-Kowlessar
ea35bed24c S0380.236: extension party-wall type read independently of "As Main Wall"
RdSAP 10 §3.3: "As Main Wall: Yes" makes an extension inherit the main
dwelling's external wall CONSTRUCTION only — the party wall type is
lodged separately per building part in the Summary §7 block and may
differ. `_extract_extensions` was copying `main_walls.party_wall_type`
into the inherited WallDetails, so every extension reused the main's
party wall U.

On the double_glazing fixture (Summary_001431) the Main lodges party
"CU Cavity masonry unfilled" (SAP10 wall_construction 4 → u_party_wall
0.5) but the 1st Extension lodges "U Unable to determine" (→ 0 → RdSAP
default 0.25). Pre-fix both building parts used 0.5, inflating worksheet
(32) party-wall heat loss by 6.56 W/K (Ext1 26.25 m² × 0.25). After the
fix worksheet (32) is exact: ours 32.573 vs worksheet 32.5725.

Now reads the extension's own "Party Wall Type" from its §7 chunk,
falling back to the main's only when the extension lodges none. Adds a
fixture + test asserting Main=4 / Ext=0 with distinct u_party_wall.
Suite 2413 pass; no cohort regression.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-05 09:19:43 +00:00
Khalim Conn-Kowlessar
fe59c4d8a2 S0380.208: case 7 combi e2e fixture — condensing-oil-combi path validated exact
Adds simulated case 7: case 6 (P960-0001-001431) with the heating swapped
to a CONDENSING OIL COMBI (SAP code 130, Table 4b 82/73) and the cylinder
removed — combi instantaneous DHW (WHC 901), Table 3a keep-hot combi loss
(61) = 600 kWh/yr, no primary/storage loss, boiler interlock PRESENT (no
−5pp). This is the heating archetype golden cert 0240-0200-5706-2365-8010
uses, which case 6 (SAP code 127, a *regular* condensing oil boiler +
cylinder) never exercised.

The cascade reproduces the case-7 worksheet EXACTLY at abs=1e-4 on every
top-level SapResult output with ZERO calculator changes:
  (211) 7865.4304  (213) 7556.9821  (219) 3496.8121  (98c) 12646.3783
  (255) 1123.3372  (257) 1.9631     (272) 5738.9315  (258) 73
This validates the SAP 10.2 Appendix D Eq D1 combi efficiency blend +
Table 3a keep-hot combi loss + Table 4b code 130 (82/73) path, and
exonerates the combi mechanism as the source of 0240's API-path residual
— which therefore lives in 0240's fabric/demand or the API mapper.

Test-only slice (no impl change). New fixture file: 0 pyright errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 17:57:22 +00:00
Khalim Conn-Kowlessar
2b1f90a7de S0380.199: site-notes "Roof of Room" windows → roof windows (cross-mapper parity with S0380.198)
The Elmhurst extractor crashed parsing simulated-case-6's room-in-roof
window rows: the §11 "Location" cell "Roof of Room in Roof" wraps across
the layout prefix/suffix blocks and leaked into the glazing-type phrase
("Double between 2002 Roof of Room and 2021 in Roof" → UnmappedElmhurst-
Label). Fix (`_parse_window_from_anchors`): detect the roof-of-room
location tokens, strip them from the before/after blocks so the glazing
phrase reconstructs cleanly, and set location="Roof of Room".

Mapper: `_is_elmhurst_roof_window` gains a "Roof of Room" location branch
(highest-confidence rooflight signal, above the BP-roof-type / U>3.0
gates); `_ELMHURST_ROOF_WINDOW_U_BY_GLAZING` gains "Double between 2002
and 2021" → 2.30 (case 6 lodges the already-inclined roof-window U, so
the +0.30 inclination adjustment must not double-apply).

This is the site-notes mirror of S0380.198 (API window_wall_type=4):
both paths now route room-in-roof rooflights to (27a) at the inclined U.
Validated against the case-6 P960 worksheet at abs=1e-4:
  (27)  Windows      = 22.7408 (cascade 22.7407)
  (27a) Roof Windows = 13.0375 (cascade 13.0375, EXACT)
  (31)  ext area     = 336.13

Case 6 is pinned only on the §3 window line refs (new standalone test,
not added to the section-pin `_FIXTURES`) because its DUAL main heating
(51% rads + 49% underfloor, oil) makes the §10/§12 per-system lines
non-comparable to SapResult's aggregated fields — documented in the
fixture module. Summary mirrored to Summary_001431_case6.pdf.

Suite: 2355 passed, 1 skipped. New code: 0 pyright errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 12:46:18 +00:00
Khalim Conn-Kowlessar
570df83459 S0380.197: simulated case 5 e2e fixture — detached sandstone RR validates S0380.196 (RdSAP 10 §3.9.1 + Table 4 p.22)
Promotes user-simulated "case 5" (detached, sandstone-walled, room-in-roof
cousin of golden cert 0240) to an e2e worksheet fixture pinning the WHOLE
extractor → mapper → calculator pipeline at abs=1e-4 on all 11 Block-1
line refs. Its worksheet prints the exact RR-gable routing S0380.196
implements, validating that fix against ground truth:

  Roof room Main Gable Wall 1  15.68  U=0.35  (29a)  Exposed → walls @ main-wall U
  Roof room Main remaining area 61.73  U=0.30  (30)  A_RR shell − Σ gables
  External roof Main           14.52  U=0.11  (30)  loft residual
  Roof room Main Gable Wall 2  15.68  U=0.25  (32)  Party → party @ 0.25

gable area = 6.40 × 2.45 (§3.9.1 default RR storey height); A_RR remaining
= 12.5√(83.2/1.5) − 2×15.68 = 93.09 − 31.36 = 61.73 (RdSAP 10 §3.9.1(e)).
Confirms a DETACHED dwelling can lodge a Party RR gable (Table 4 p.22
row 2) — so my S0380.196 mapping (gable_wall_type 0=Party, 1=Exposed) is
correct; do not flip it.

Two extractor/mapper gaps surfaced and fixed (case 5 is the forcing test):
- Sandstone wall label "SS Stone: sandstone or limestone" had no
  `_ELMHURST_WALL_CODE_TO_SAP10` entry (raised UnmappedElmhurstLabel).
  Added "SS" → 2 (WALL_STONE_SANDSTONE), matching 0240's API
  wall_construction=2 (cross-mapper parity).
- Roof "Insulation Thickness 400+ mm" was silently dropped: the four
  thickness parsers used `.split()[0].isdigit()`, which rejects the
  trailing "+" → None → u_roof fell back to the age-J default 0.16
  instead of 0.11 (+1.09 W/K roof, the whole 0.12 SAP gap). Added
  `_parse_thickness_mm` (strips to leading digits) and applied it at all
  four sites (walls / alt-wall / roof / floor). The only existing fixture
  with "400+ mm" (000565 Stud Wall) routes via the RIR regex, unaffected.

Result: case 5 cascade ≡ worksheet at 1e-4 on SAP/ECF/cost/CO2 + every
energy stream. Neither gap affects 0240 (its API path captures both the
sandstone code and "400mm+"); 0240's residual is therefore non-fabric.

Suite: 2353 passed, 1 skipped. New code: 0 pyright errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 11:41:16 +00:00
Khalim Conn-Kowlessar
4a21717de6 S0380.195: pin sim case 4 (6035 floor geometry) e2e at 1e-4 — 6035 +19 PE is lodged divergence
Adds the user-simulated case-4 worksheet as e2e fixture `001431_6035` —
reproduces golden cert 6035's full floor geometry (Main ground-floor HLP
15.99 + first-floor HLP 8.32, the asymmetric upper storey) and 8 windows.
All 11 Block-1 line refs pin at abs=1e-4 against the worksheet (SAP 68,
ECF 2.2802, cost 937.2341, CO2 4682.3494, space 15745.3260, main fuel
18744.4357).

This is the 4th independent 1e-4 confirmation across the 6035 archetype
(sim cases 1-4). Case 4 matches 6035 on floors + window areas; the
residual ~50 kWh / £11 cascade delta vs 6035 is two lodged inputs only
(largest window orientation N vs S; meter type "Dual" vs API 2), not
calculator behaviour.

Conclusion: the cascade reproduces the spec engine exactly for 6035's
geometry, so 6035's +19 PE vs the lodged register is lodged-register
divergence (the gov.uk register's rounded value vs the spec-exact
worksheet), NOT a calculator gap. 6035 is a "pin-forever" lodged-only
cert. Bugs surfaced + fixed along the way: S0380.192 (Simplified-RR
remaining area) and S0380.193 (suspended-floor sealed rule).

2341 passed (+11), 0 failed; pyright net-zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:56:39 +00:00
Khalim Conn-Kowlessar
e7a0c9885e S0380.194: pin sim case 3 (near-exact 6035 replica) e2e at 1e-4
Adds the user-simulated case-3 worksheet as e2e fixture `001431_rr8` —
Main + Extension + Simplified room-in-roof with 8 windows (≈14.15 m²,
reproducing golden cert 6035's glazing) and Main ground-floor HLP 15.99.
All 11 Block-1 line refs pin at abs=1e-4 against the worksheet (SAP 68,
cost 951.3425, CO2 4767.4862, space 16086.3557, main fuel 19150.4235,
HW 3307.2639, lighting 262.0885).

This is the third independent 1e-4 confirmation that the cascade
reproduces the spec engine for the 6035 archetype (after S0380.192
Simplified-RR + S0380.193 suspended-floor). It differs from 6035 in one
input only — the Main first-floor HLP (15.99 here vs 6035's 8.32) — so
6035's +19 PE vs the lodged register is lodged-register divergence, not
a calculator gap. A byte-identical 6035 replica (first-floor HLP 8.32)
would let 6035 itself be pinned directly to close that out.

2330 passed (+11), 0 failed; pyright net-zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:46:56 +00:00
Khalim Conn-Kowlessar
62fc27a5cc S0380.193: suspended-floor (12) sealed rule fires only on a SUPPLIED U-value
RdSAP 10 §5 (PDF p.29) "Floor infiltration (suspended timber ground
floor only)", age band A-E, splits on whether a floor U-value is
supplied:
  a) [U-value supplied] if floor U-value < 0.5 → "sealed", (12) = 0.1
  b) [no U-value supplied] retro-fitted insulation → "sealed" 0.1;
     otherwise "unsealed", (12) = 0.2

`_has_suspended_timber_floor_per_spec` fed the cascade's COMPUTED default
U into rule (a), so an as-built/uninsulated suspended-timber floor whose
default U happens to be < 0.5 was marked "sealed" (0.1) where Elmhurst
uses "unsealed" (0.2). That dropped (18) infiltration 0.85 → 0.75, (25)
effective ACH, HTC, and understated space heating ~450 kWh.

Fix: gate rule (a) on `floor_u_value_known` — a computed default U is not
a supplied value, so it falls through to (b). Verified against the
cert 001431 sim-case-2 worksheet: floor "As built", U=0.43 (matches the
worksheet's (28a) 0.4300 exactly), (12)=0.2 unsealed. Golden cert 6035
(also a suspended uninsulated floor) is unaffected — its U=0.63 ≥ 0.5
already routed to unsealed.

Promotes sim case 2 to the e2e harness as `001431_rr` (Main + Extension
+ Simplified room-in-roof — the 6035 archetype). All 11 Block-1 line
refs pin at abs=1e-4, locking BOTH this fix and S0380.192 (Simplified-RR
remaining area) end-to-end: SAP 69, cost 920.5046, CO2 4566.7090, space
15269.8593, main fuel 18178.4039. 2319 passed (+11), 0 failed; pyright
net-zero.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 09:16:25 +00:00
Khalim Conn-Kowlessar
896b5740c3 S0380.191: pin simulated 001431 gas-combi end-to-end at 1e-4 (e2e harness)
Adds the user-simulated 001431 case (the cert that drove S0380.189/.190)
as an Elmhurst-only e2e fixture: Summary PDF → extractor → mapper →
calculator, every Block-1 SapResult field pinned against the
P960-0001-001431 worksheet at abs=1e-4. All 11 pins pass with zero
residual — the case is clean, confirming the S0380.190 gas-combi fuel
derivation closes the Summary path natively.

Verified the handover's flagged "+0.0007 SAP" was a target artifact, not
a cascade gap: the worksheet displays ECF (257) rounded to 1.6047 and
integer SAP (258)=78; the cascade's continuous SAP is computed from the
UNROUNDED ECF = (255)*(256)/((4)+45) = 660.9750*0.4200/173.0, giving
77.6147 — which matches the worksheet's own unrounded value. Pinning the
continuous SAP from the display-rounded ECF (→ 77.6144) was the wrong
target. Block-1 line refs all match exactly: (211) 10699.7225, (219)
3327.1592, (231) 86.0, (232) 283.2229, (255) 660.9750, (272) 3000.1664,
Σ(98) 8987.7669.

Summary mirrored into the tracked fixtures dir as
Summary_001431_gas_combi.pdf (distinct name — the corpus reuses cert
001431 across every heating variant); source Summary + worksheet tracked
under sap worksheets/golden fixture debugging/ as the pin ground truth.

2302 passed (+11), 0 failed; pyright net-zero on new/changed files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 22:44:32 +00:00
Khalim Conn-Kowlessar
e51fcb74ca Slice S0380.52: cert 000565 Elmhurst-only mapper-driven cascade pin + glazing-label coverage
User pivot at end of prior session: don't hand-build EpcPropertyData
fixtures — route Summary PDFs through `EpcPropertyDataMapper.from_
elmhurst_site_notes` so the pin grid exercises extractor + mapper +
calculator, and each new Elmhurst doc grows mapper coverage instead
of bespoke fixture code.

New fixture cert 000565 is a stress-test cert (5 building parts, age
mix A→J, conservatory with heaters, curtain wall, basement walls,
mixed party-wall constructions) that surfaces many uncommon cascade
paths absent from the cohort-2 + ASHP corpus.

Mapper coverage extended for 3 Elmhurst §11 glazing labels surfaced
on this cert (per RdSAP-Schema-21.0.1, `datatypes/epc/domain/
epc_codes.csv` glazed_type rows):

  "Triple between 2002 and 2021": 9  (RdSAP-21 schema row 9 — triple
       glazing, installed 2002-2022 in EAW; `_G_PERPENDICULAR_BY_
       GLAZING_TYPE[9] = 0.68`, `_G_LIGHT_BY_GLAZING_CODE[9] = 0.70`)
  "Single glazing": 1                (alias of bare "Single"; cascade
       g_L = 0.90, g⊥ = 0.85 per SAP 10.2 Table 6b)
  "Double glazing, known data": 3    (Elmhurst lodgement of RdSAP-21
       schema row 7 "double, known data"; manufacturer U-value and
       g-value lodged via WindowTransmissionDetails override the
       cascade's defaults — grouped under code 3 with other unknown-
       date DG variants for cascade-equivalence on g_L/g⊥)

Per [[feedback-e2e-validation-philosophy]] + [[feedback-zero-error-
strict]]: pin tolerances are abs=1e-4 against U985-0001-000565.pdf
Block 1 line refs (pinned: SAP int + SAP continuous + ECF + total
fuel cost + CO2 + space heating + main 1 fuel + secondary fuel +
hot water + lighting + pumps/fans).

Outcome: 1/11 pin green (`secondary_heating_fuel_kwh_per_yr = 0`);
10 pins are now named calculator-gap residuals to fix in subsequent
slices:

  main_heating_fuel_kwh_per_yr  +27,665.01 kWh/yr  (heat-pump SAP code
      224 + gas combi via WHC 914 "from second main"; cascade probably
      runs ASHP for DHW instead of routing through gas combi)
  hot_water_kwh_per_yr             +164.88 kWh/yr  (FGHRS / solar HW /
      Table 3a no-keep-hot for the gas combi DHW path)
  lighting_kwh_per_yr              -236.19 kWh/yr  (RdSAP §12-1 bulb-
      count cascade; 27 total / 7 low-energy / 20 incandescent lodged)
  pumps_fans_kwh_per_yr            -122.52 kWh/yr  (cascade defaults
      to 130; expected 252.52 = MEV PCDF 500755 + flue + solar pump)

Cohort regression check: 472 pass + 10 expected 000565 failures.
Pyright net-zero (32 errors before, 32 after).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 22:03:52 +00:00
Khalim Conn-Kowlessar
1f8a070f66 Slice S0380.19: count Elmhurst shower outlets by type (no more hardcoded 1)
Surfaces the lodged shower multiplicity from the Elmhurst Summary §16
on the EPC. Previously `_map_elmhurst_sap_heating` hardcoded:

  electric_shower_count = 1 if has_electric_shower else None
  mixer_shower_count    = 0 if has_electric_shower else None

losing the count for any cert with ≥ 2 outlets. Cert
7800-1501-0922-7127-3563 lodges TWO instantaneous electric showers
("Shower 01" + "Shower 11") but the mapper produced
`electric_shower_count=1`. After this slice:

  electric_shower_count = Σ(s for s in showers if s.outlet_type
                              == "Electric shower")
  mixer_shower_count    = Σ(s for s in showers if s.outlet_type
                              != "Electric shower")

**Cascade SAP effect:** None on cert 7800. Appendix J's eq J16
(`N_ES,per_outlet = N_shower / N_outlets`) and eq J18 (Σ_j E_ES,j)
are symmetric in N_electric_showers when there are no mixer outlets,
so the lodged (64a) kWh and (247a) cost are unchanged. The fix is
correctness-by-construction, not a delta-closer for the negative-band
certs (their +0.69 GBP total-cost gap traces to the gas hot-water
kWh path — separate slice).

**Hand-built fixture updates (5):** the cohort-1 hand-builts at
`domain/sap10_calculator/worksheet/tests/_elmhurst_worksheet_*.py`
previously omitted `electric_shower_count` / `mixer_shower_count`
(implicitly None), which matched the mapper's pre-slice None
sentinel. Updated each to the lodged counts the mapper now surfaces:
  000474: 1 mixer  → (0, 1)
  000477: 1 mixer  → (0, 1)
  000480: 1 mixer  → (0, 1)
  000490: 1 mixer  → (0, 1)
  000516: 1 mixer  → (0, 1)
000487 (already at (1, 0) for an electric-shower lodging) unchanged.

Tests:
- `test_summary_7800_two_electric_showers_count_as_two_not_one` —
  pins the multi-shower mapping for cert 7800 (Summary_000890.pdf).
- 5 hand-built field-parity tests
  (`test_from_elmhurst_site_notes_matches_hand_built_*`) now pass at
  the new integer counts instead of None.

Pyright net-zero per file:
- datatypes/epc/domain/mapper.py: 32 (baseline 32)
- backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0

Regression baseline: 699 pass + 10 fail (= prior 698 + 10 + 1 new).

Spec refs:
- SAP 10.2 Appendix J §1a — outlet counting drives `N_outlets` used
  in eq J6/J7 (mixer shower water draw) and eq J16/J17/J18 (electric
  shower energy).
- Cert 7800-1501-0922-7127-3563 Summary §16 "Showers" lodgement.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 07:16:32 +00:00
Khalim Conn-Kowlessar
57fbf83b1e Slice S0380.18: u_party_wall flat default per RdSAP10 Table 15 footnote*
Closes cert 0036-6325-1100-0063-1226 (the cohort's first FLAT fixture)
from Δ -0.3737 → +0.2987 by applying the RdSAP 10 Table 15 footnote *
rule: flats/maisonettes with unknown party-wall construction default
to U=0.0 W/m²K (both sides are heated dwellings, no heat loss).

Worksheet dr87-0001-000910.pdf line ref (32) lodges:
    Party walls Main   24.13 m²   U=0.00   A×U = 0.0000 W/K
matching the Table 15 footnote *. The cascade was applying the U=0.25
*house* default to this lodging because:
  - Elmhurst Summary lodged `party_wall_type='U Unable to determine'`
  - mapper translated it to `party_wall_construction=0` (the cross-
    mapper-parity "unknown" sentinel)
  - `u_party_wall(0)` fell through to `return 0.25` (the final-branch
    default — same path as `u_party_wall(None)`)

That produced cascade `party_walls_w_per_k = 24.13 × 0.25 = 6.03` W/K
of heat-loss excess, propagating through (39) HTC → (97)..(98c) space
heat demand → (211) main fuel kWh → (255) total cost → (257) ECF →
(258) SAP rating. Net effect: cascade SAP 62.3734 vs worksheet 62.7471.

Two-part fix:

1. `domain/sap10_ml/rdsap_uvalues.py:u_party_wall` — add
   `is_flat: bool = False` keyword argument. When True AND
   `party_wall_construction in (None, 0)` (both the API-mapper None
   path and the Elmhurst-mapper 0 sentinel for "Unable to determine"),
   return 0.0 instead of the house default 0.25. Spec citation: RdSAP
   10 Table 15 footnote * ("for flats and maisonettes with unknown
   party-wall construction").

2. `domain/sap10_calculator/worksheet/heat_transmission.py` — wire
   the cascade to pass `is_flat=_is_flat_or_maisonette(epc.property
   _type)`. Adds a new helper `_is_flat_or_maisonette` distinct from
   the existing `_is_house` (which excludes bungalows from
   *cantilever* detection — bungalows ARE houses for party-wall
   purposes per the spec). The new helper checks both the descriptive
   form ("Flat" / "Maisonette") and the SAP schema enum-as-string
   form ("2" / "3" — per `datatypes/epc/domain/epc_codes.csv
   property_type` rows: 0=House, 1=Bungalow, 2=Flat, 3=Maisonette,
   4=Park home).

The schema-enum collision was the bug-fix-with-a-bug: an initial
implementation used "1"/"2" (Flat/Maisonette per intuition) but those
are actually Bungalow/Flat per the schema, which routed all 10
bungalow certs onto the flat path. Corrected pre-commit.

Cohort-2 Summary-path delta after slice:

  cert 0036  (Flat)      Δ -0.3737  →  Δ +0.2987   ✓ improved by +0.67
  10 bungalow certs                  unchanged (correctly NOT flat)
  5 non-flat house certs in band     unchanged (different root cause —
                                     next slice)

Bungalow certs (cohort 1 + 2) verified unchanged at delta ≤ +0.04 each.

Tests added (5):
- `test_u_party_wall_unknown_for_flat_returns_table15_footnote_zero`
  pins the spec rule on the helper.
- `test_u_party_wall_unknown_sentinel_zero_treated_as_unknown_for_flat`
  pins the Elmhurst-mapper `0` sentinel parity.
- `test_u_party_wall_known_solid_still_returns_zero_when_is_flat_false`
  pins precedence: explicit Solid code overrides the is_flat flag.
- `test_summary_0036_flat_unknown_party_wall_routes_to_u_zero` chain-
  test through `from_elmhurst_site_notes` + cert_to_inputs +
  calculate_sap_from_inputs to assert `party_walls_w_per_k == 0` at
  1e-4 tolerance.

Pyright net-zero per file:
- domain/sap10_ml/rdsap_uvalues.py: 1 (baseline 1)
- domain/sap10_calculator/worksheet/heat_transmission.py: 13 (baseline 13)
- domain/sap10_ml/tests/test_rdsap_uvalues.py: 66 (baseline 66)
- backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0

Regression baseline: 698 pass + 10 fail (= prior 694 + 10 + 4 new).

Note: the remaining +0.2987 residual on cert 0036 is in (30) external
roof — worksheet lodges Ext1 flat roof Plasterboard insulated U=2.30
giving 2.51 W/K; cascade has roof_w_per_k=0 (Ext1 roof contribution
missing). Separate slice.

Spec refs:
- RdSAP 10 Table 15 ("U-values of party walls") row 4 — house unknown
  default 0.25 W/m²K.
- RdSAP 10 Table 15 footnote * — flat/maisonette unknown default
  0.0 W/m²K.
- `datatypes/epc/domain/epc_codes.csv` rows
  `property_type,{0..4},...` — SAP/RdSAP schema property-type enum.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:24:58 +00:00
Khalim Conn-Kowlessar
dab59ccfd8 Slice S0380.17: map Elmhurst §11 glazing-type labels to SAP10 codes
Closes a systematic +0.02..+0.07 SAP over-prediction on every triple-
glazed cert in cohort 2 (13 of 38) and removes a silent-default
failure mode flagged via cert 3336-2825-9400-0512-8292 (+0.0674 Δ).

Root cause: `_map_elmhurst_window` (datatypes/epc/domain/mapper.py)
was passing the Elmhurst-lodged glazing-type string verbatim into
`SapWindow.glazing_type` (declared `Union[int, str]`). The §5 (66)..
(67) daylight-factor cascade at
`domain/sap10_calculator/worksheet/internal_gains.py:512` requires
`isinstance(w.glazing_type, int)` to look up Table 6b col light g_L —
string lodgings silently fell through to the `_G_LIGHT_DEFAULT = 0.80`
(double-glazed) branch. Cert 3336 (Triple glazed, worksheet "Window,
Triple glazed") got g_L = 0.80 instead of the correct 0.70, inflating
C_daylight from 1.072 to 1.041 → lighting kWh under-predicted by
−4.53 kWh/yr → total fuel cost under by −1.17 GBP → ECF Δ −0.0049 →
SAP continuous over by +0.0674.

Fix: `_ELMHURST_GLAZING_LABEL_TO_SAP10` dict + `_elmhurst_glazing_
type_code` helper translate the Elmhurst Summary §11 lodged strings
to the SAP 10.2 Table U2 integer codes the cascade keys on:

  "Single"                                          → 1
  "Double pre 2002"                                 → 2
  "Double between 2002 and 2021"                    → 3
  "Double with unknown install date"                → 3
  "Double with unknown 16 mm or install date more"  → 3
  "Double post or during 2022"                      → 5
  "Triple post or during 2022"                      → 6
  "Triple post or during"                           → 6  (year-trunc.)
  "Secondary"                                       → 7

Two regex passes strip the layout noise the extractor sometimes folds
into the glazing-type token: a `(?:Part )?value value Proofed Shutters`
prefix (from adjacent column headers) and a ` Summary Information` /
` Alternative wall…` suffix. Verified against the union of cohort-1
(7 certs) + cohort-2 (38 certs) + test-fixture (9 PDFs) glazing
labels: 18 distinct surface forms, all closed by the dict + noise
patterns; one window in cert 2636's Summary_000898.pdf lodged the
year-truncated "Triple post or during" — added as an alias for code 6
per worksheet "Triple glazed" lodging.

Strict-enum gate: `_elmhurst_glazing_type_code` raises
`UnmappedElmhurstLabel("glazing_type", label)` (Slice S0380.15
pattern, extended to the new helper) when the label is None or not
in the dict — surfaces mapper-coverage gaps at extraction time rather
than masking them as a SAP precision floor.

Cohort-2 Summary-path delta progression (38 certs):
  bucket          before slice 2    after slice 2
  exact (<1e-4)   11                11
  <0.005          0                 5     ← 9421 +0.0012, 2536 +0.0016, 9370 +0.0017, 0100 +0.0028, 2800 +0.0044
  0.005-0.07      15                10    ← all triple-glazed
  0.07-0.5        5                 5
  0.5-1           4                 4
  1-5             1                 1
  5+              2                 2
  RAISES          0                 0

3336 (user's flag) closes from +0.0674 → +0.0400 — the residual is
the remaining systematic offset the next slice will investigate.

Tests added (3):
- `test_summary_3336_triple_glazed_windows_route_to_code_6` — pins
  the mapper output for the user's flagged cert.
- `test_summary_000474_double_glazed_windows_route_to_code_3` —
  exercises the DG branch + the year-unknown alias mapping.
- `test_summary_mapper_raises_on_unmapped_glazing_type_label` —
  strict-enum coverage gate via mutated site notes.

Tests updated (1):
- `test_first_window_glazing_type` (test_elmhurst_end_to_end.py):
  asserts int code 5 (DG low-E argon — "Double post or during 2022")
  not the string verbatim. The string-passthrough behaviour was
  always a latent bug; this test was the only direct pin on it.

Pyright net-zero per file:
  - datatypes/epc/domain/mapper.py: 32 (baseline 32)
  - backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0
  - backend/documents_parser/tests/test_elmhurst_end_to_end.py: 0

Regression baseline: 694 pass + 10 fail (= prior 691 + 10 + 3 new).
Triple-glazed original-cohort certs are now closer to worksheet too;
the ±0.07 chain tests on the original cohort still hold, and a future
slice tightens them once the next-largest residual is closed.

Spec refs:
- SAP 10.2 Table U2 — glazing-type integer enum.
- SAP 10.2 Table 6b col light — light-transmission g_L by glazing
  type (triple 0.70, double-glazed variants 0.80, single 0.90).
- RdSAP 10 §11 Windows — Summary lodging of glazing type as a
  type+install-date phrase.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:05:52 +00:00
Khalim Conn-Kowlessar
6b1cdd64bc Slice S0380.16: add 'Normal' → cylinder_size=2 (110 L) for cohort 2
Unblocks two 38-cert-cohort certs that previously raised
`UnmappedElmhurstLabel("cylinder_size", 'Normal')` at extraction:
  cert 2536-2525-0600-0788-2292  ws SAP=79.7264
  cert 9421-3045-3205-1646-6200  ws SAP=87.4495

Both Summary §15.1 lodgements read "Cylinder Size: Normal"; both dr87
worksheets lodge line ref (47) "Store volume = 110.0000" L (extracted
from `Hot Water Cylinder → Cylinder Volume 110.00`). RdSAP 10 §10.5
Table 28 documents the "Normal (90-130 litres)" descriptor whose
midpoint is 110 L — the canonical Elmhurst label string in
`datatypes/epc/surveys/elmhurst_site_notes.py` is "Normal (90-130
litres)", and the worksheet's exact 110 L matches the midpoint.

Two-line fix:
  +    "Normal": 2,           in `_ELMHURST_CYLINDER_SIZE_LABEL_TO_SAP10`
  +    2: 110.0,              in `_CYLINDER_SIZE_CODE_TO_LITRES`

The cascade enum 2 is consistent with the existing
`cert_to_inputs.py` docstring's documented (but not-yet-observed)
code 2 → Normal slot, alongside code 3 (Medium / 160 L) and code 4
(Large / 210 L) added in earlier slices.

Slice keeps tight: two mapping unit tests pinning `cylinder_size == 2`
for both certs at extraction. Post-fix the first-attempt cascade
deltas vs worksheet are:
  cert 2536  Δ +0.0244   (was: RAISES)
  cert 9421  Δ +0.0296   (was: RAISES)

Both deltas now sit in the same systematic +0.02..+0.07 small-gap
band as ~12 other first-attempt certs in cohort 2 — chain test +
±0.07 pin would just paper over a known systematic residual that the
user has explicitly asked to drive towards 1e-4, not toward ±0.07.
Following slice will investigate the shared systematic offset and
close cert 2536 / 9421 along with the rest of the +0.04 band on
the chain.

Pyright net-zero per file:
  - datatypes/epc/domain/mapper.py: 32 (baseline 32)
  - domain/sap10_calculator/rdsap/cert_to_inputs.py: 35 (baseline 35)
  - backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0

Regression baseline: 691 pass + 10 fail (= prior 689 + 10 + 2 new GREEN).

Spec refs:
- RdSAP 10 §10.5 Table 28 — "Cylinder Volume" Normal band 90-130 L,
  midpoint 110 L (also the canonical Elmhurst label suffix).
- Cert 2536 worksheet `dr87-0001-000889.pdf` line ref (47) = 110.0000.
- Cert 9421 worksheet `dr87-0001-000884.pdf` line ref (47) = 110.0000.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 22:44:02 +00:00
Khalim Conn-Kowlessar
f546bd5ddc Slice S0380.10: pin certs 3800 + 9285 Summary chain tests — first-try closure
Adds two Layer-4 chain tests for the ASHP cohort, both pinning at the
±0.07 spec-floor tolerance with **zero new mapper slices required**.
The structural debt paid down in S0380.2..S0380.9 (HP routing,
cylinder block, composite walls, multi-array PV, multi-bp extension
wall_insulation_thickness inheritance) was already sufficient for
these two certs — they close first-try.

First-attempt probe results across the 5 remaining ASHP cohort certs:

  cert  Worksheet  Summary-cascade  Δ          in floor?
  2225  88.7921    88.4842         -0.3079     no
  2636  86.2641    86.7514         +0.4873     no
  3800  86.1458    86.1900         +0.0442     **YES**  ← this slice
  9285  84.1369    84.1871         +0.0502     **YES**  ← this slice
  9418  84.6305    87.2278         +2.5973     no       (Daikin)

This is the strongest evidence yet that the Summary mapper has
amortized its variant-debt for standard single-bp / single-array
Mitsubishi-cohort ASHPs. Per the [[project-summary-path-cohort-
closure]] memory: 0380 needed 6 slices; 0350 needed 2; 3800 and 9285
need ZERO; 2225 / 2636 / 9418 each need ≤2-3 small slices to close.

Also adds the 5 remaining ASHP cohort Summary PDFs as fixtures
(Summary_000898, 000900, 000901, 000902, 000904) — copied from
`sap worksheets/Additional data with api/<cert>/`. The 3 not-yet-
closed certs (2225, 2636, 9418) will pick up chain tests in
subsequent slices once their per-cert gaps are paid down.

Pyright: 0 errors on the test file (no other code touched).

Regression suite: 679 pass + 10 fail (= handover baseline 669 + 10
+ 10 new GREEN tests across Slices S0380.2..S0380.10). Of the 10
new tests, 7 are unit-level mapper-boundary pins and 4 are chain
tests at ±0.07 (certs 0380, 0350, 3800, 9285).

Spec / precedent refs:
- Slice 102f (commit c0086660) — same disposition on the API path
  for the same 7 ASHP cohort certs.
- SAP 10.2 Appendix N3.6 — PSR-interpolation precision floor
  (calculator-side limit, not mapper).
- Project memory `project-summary-path-cohort-closure` tracks the
  closure status table for all 7 cohort certs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:47:51 +00:00
Khalim Conn-Kowlessar
43a86d66c2 Slice S0380.9: multi-array PV support + close cert 0350 to ASHP spec floor
Refactors Elmhurst `Renewables` PV detail from four scalar fields
(pv_peak_power_kw / pv_orientation / pv_elevation_deg / pv_overshading
— single-array shape) to `pv_arrays: List[ElmhurstPvArray]`, then
walks the §19.0 PV Panel block in 4-tuples so dwellings with multiple
PV arrays surface every array.

Forced by cert 0350-2968-2650-2796-5255 (Summary_000903.pdf), the
second ASHP cohort cert through the Summary path and first to lodge
multiple PV arrays — the dr87 worksheet pins 2 arrays at 1.50 kWp
each (one SE at 45°, one NW at 45°). Pre-slice the extractor's
hardcoded "break at len(values) == 4" capped output at one array
regardless of how many the PDF lodged.

Three-layer end-to-end change:

1. `datatypes/epc/surveys/elmhurst_site_notes.py` — add
   `ElmhurstPvArray` dataclass (kw, orientation, elevation_deg,
   overshading); replace four `Renewables.pv_*` scalars with
   `pv_arrays: List[ElmhurstPvArray] = field(default_factory=list)`.
2. `backend/documents_parser/elmhurst_extractor.py` — rename
   `_extract_pv_array_detail` → `_extract_pv_arrays`; walk values
   after the "Photovoltaic panel details" anchor in 4-tuples until a
   stop token ("batteries"/"export"/etc.) or a §-header closes the
   block. §-header regex tightened to `\d{1,2}\.\d\s+\w` so kWp
   values like "1.50" don't trip the close (without the `\s+\w` the
   regex matched both "20.0 Wind Turbine" AND "1.50").
3. `datatypes/epc/domain/mapper.py` — `_elmhurst_pv_arrays` iterates
   the list and emits one `PhotovoltaicArray` per row; collapses
   empty list → None so the cascade keeps its no-PV fallback.

Forcing function: cert 0350 first-attempt Summary SAP closes from
Δ -4.5829 (Slice 8 baseline) to Δ **+0.0458** — within the ±0.07
ASHP-cohort spec-precision floor. PV export credit GBP moves from
158.91 (one array surfaced) to 265.99 (both arrays surfaced) — the
extra ~107 GBP of avoided cost lifts cert 0350's SAP by ~4.6 points.

This validates the structural-debt-amortizes hypothesis: cert 0350
needed only TWO new slices (S0380.8 inheritance + S0380.9 multi-PV)
beyond the cert 0380 closure work, vs cert 0380's 6 slices from
scratch. Subsequent cohort certs should converge similarly fast as
fixture-specific gaps are paid down.

Added two tests:
- `test_summary_0350_surfaces_two_pv_arrays` — unit test pinning
  the multi-array contract on the mapper boundary.
- `test_summary_0350_full_chain_sap_within_spec_floor_of_worksheet`
  — chain test pinning Δ < ±0.07 (matches cert 0380's chain test).

Cert 0380 (single-array, 3 kWp) continues to pass its chain test +
all 6 unit-level pins — the refactor preserves single-array behaviour.

Pyright net-zero across all four edited files:
  datatypes/epc/domain/mapper.py:                32 (baseline)
  datatypes/epc/surveys/elmhurst_site_notes.py:   0
  backend/documents_parser/elmhurst_extractor.py: 0
  backend/documents_parser/tests/test_summary_pdf_mapper_chain.py: 0

Regression suite: 677 pass + 10 fail (= handover baseline 669 + 10
+ 8 new GREEN unit+chain tests across Slices S0380.2..S0380.9).

Fixtures added: `backend/documents_parser/tests/fixtures/Summary_
000903.pdf` (copied from `sap worksheets/Additional data with api/
0350-2968-2650-2796-5255/`).

Spec refs:
- SAP 10.2 Appendix M (PDF p.103) — multiple PV arrays sum to total
  electricity generation per Equation M-1 (each array's surface flux
  computed independently per Appendix U3.3).
- SAP 10.2 Appendix U3.3 (PDF p.124) — per-array surface flux keyed
  on orientation + tilt + overshading.
- Cert 0350 worksheet `dr87-0001-000903.pdf` (29a Main 19.4575 W/K
  + Ext1 1.3025 W/K = 20.7600 ≡ Summary cascade walls_w_per_k; (39)
  avg HTC 173.4202 ≡ Summary cascade; (64) HW 2084.66 ÷ (216) HW eff
  1.7285 = 1206.04 ≡ Summary cascade hot_water_kwh_per_yr).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 20:44:13 +00:00
Khalim Conn-Kowlessar
5d1778ac4e chore: stage cert 9501 fixtures (second boiler validation cert)
API JSON + Summary PDF for cert 9501-3059-8202-7356-0204. RR/Mid-
terrace flat, 4 building storeys, TFA 113.08 m², mains gas boiler
(PCDB idx 19007), age band B. Worksheet target unrounded SAP
**68.5252**.

Second boiler cert per the per-cert mapper validation workflow:
Summary path proves itself against the worksheet (Layer 2 1e-4 pin),
then the API path catches up (Layer 4 1e-4 pin) — mirrors the cert
0330 cycle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 18:53:08 +00:00
Khalim Conn-Kowlessar
17646c8ae9 chore: stage cert 0380 fixtures (HP pilot — deferred workstream)
Adds the (API JSON + Summary PDF) fixtures for cert
0380-2471-3250-2596-8761 — the Air Source Heat Pump pilot
identified in the handover. Property: 16 Beech Lea, WIGTON CA7 5JY
(semi-detached bungalow, ASHP PCDB idx 104568).

Source: API JSON fetched via EpcClientService. Summary PDF copied
from `sap worksheets/Additional data with api/
0380-2471-3250-2596-8761/Summary_000899.pdf`.

Worksheet target: SAP 88.5104 (continuous), from `dr87-0001-000899
.pdf`.

**This is the HP pilot, intentionally deferred.** Initial probe on
these fixtures (uncommitted before this slice):
  - Summary mapper cascade SAP: 18.08 (Δ -70.43 vs worksheet)
  - API mapper cascade SAP:     70.14 (Δ -18.37 vs worksheet)

Both paths are catastrophically RED. The mapper has never been
validated against an ASHP cert and there's substantial cascade
plumbing required:

  - API mapper correctly identifies the HP (COP 2.3) but fabric HLC
    is 104 W/K vs the ~50 W/K needed for SAP 88.51.
  - Summary mapper misreads the HP as an 80%-efficient boiler
    (catastrophic).
  - 7 of 9 newly-staged certs are ASHPs (6 share PCDB idx 104568,
    cert 9418 uses 102421), so a shared HP-cascade fix will likely
    close most of them at once.

Stashed here so the next agent can pick up the HP workstream
without needing to refetch from the EPB API. Recommend not
attempting these slices until the boiler workflow (cert 0330) is
proven; the boiler cascade is the reference shape and HP work
should build on a known-good baseline. Handover §"Heat-pump
workstream sketch" outlines the likely 15-30 slice queue.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 17:37:34 +00:00
Khalim Conn-Kowlessar
460f17352a chore: stage cert 0330 fixtures (boiler pilot)
Adds the (API JSON + Summary PDF) fixtures for cert
0330-2249-8150-2326-4121 — the boiler pilot identified in the
handover. Property: 17 Summerfield Road, MANCHESTER M22 1AE
(mid-terrace house, mains gas boiler PCDB idx 10241, age D).

Source: API JSON fetched via EpcClientService from
https://api.get-energy-performance-data.communities.gov.uk
(OPEN_EPC_API_TOKEN). Summary PDF copied from
`sap worksheets/Additional data with api/0330-2249-8150-2326-4121/
Summary_000897.pdf` (where the user provided the triple).

Worksheet target: SAP 61.5993 (continuous), from `dr87-0001-000897
.pdf` in the same source directory.

Current state on these fixtures (uncommitted before this slice):
  - Summary mapper cascade SAP: 62.0660 (Δ +0.4667 vs worksheet)
  - API mapper cascade SAP:     63.7446 (Δ +2.1453 vs worksheet)

Both paths RED at 1e-4. Two specific cascade-component gaps
identified in the handover for follow-up slices:

  1. Windows HLC +6.71 W/K (API vs Summary) — likely glazing_type=14
     not in Slice 93's `_API_GLAZING_TYPE_TO_TRANSMISSION` (only
     codes 3 and 13 mapped).
  2. HW kWh +1060 (API 3172.65 vs Summary 2112.00) — §4 subsystem
     gap; needs occupancy/shower/cylinder probe.

This commit stages the fixtures only — no tests added yet. The
follow-up slice should add a RED Layer 2 test (Summary path 1e-4
vs 61.5993) and proceed slice-by-slice.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 17:37:14 +00:00
Khalim Conn-Kowlessar
4427b58a44 Slice 54: Elmhurst mapper sets extensions_count from len(survey.extensions)
`from_elmhurst_site_notes` hard-coded `extensions_count=0` regardless of
how many extensions the survey lodged. The 6 cohort certs from Slices
47-53 all happened to have 0-2 extensions whose count nothing
load-bearing read, so this latent bug was invisible. Cert 001479
(Summary_001479.pdf, GOV.UK EPB cert 0535-9020-6509-0821-6222) has Main
+ Extension 1 + Extension 2 and is the first cohort cert with a real
API counterpart — accurate `extensions_count` becomes load-bearing the
moment the cross-mapper parity assertion compares API vs Elmhurst
EpcPropertyData side by side.

No SAP-cascade impact (the cascade iterates `sap_building_parts`, not
`extensions_count`) — but a real data-integrity bug surfaced by the
cross-mapper diff. Adds Summary_001479.pdf as a new chain-test fixture
and `_SUMMARY_001479_PDF` constant for follow-up slices that will
land per-bp ages, exposed floors, secondary-heating SAP codes, etc.

All 9 chain tests green; 321 mapper/site-notes/rdsap tests green;
pyright net-zero (35-error baseline preserved on mapper.py).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 22:15:47 +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
ccf7aa2118 Scaffold: end-to-end Summary→EpcPropertyData chain test for 000474 (xfail)
The 6 worksheet fixtures build EpcPropertyData by hand, validating the cascade in isolation from the mapper. This commit lands the first half of the OTHER validation: Summary_000474.pdf → ElmhurstSiteNotesExtractor → from_elmhurst_site_notes → EpcPropertyData, asserting it produces the same shape as the hand-built fixture. Test is strict-xfail on sap_building_parts count (mapper produces 1, cert lodges 3). Includes a pdftotext-layout preprocessor that converts spatial label/value layout into the Textract-style sequence the existing extractor expects (test-only). Full punch list of 28 mapper-output diffs captured in project memory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 17:40:06 +00:00
Daniel Roth
6c70c5a535 Extract address when Property photo element is missing from PDF 🟩 2026-04-30 16:25:41 +00:00
Daniel Roth
8f94bb5435 extract window frame details from elmhurst site notes 🟥 2026-04-27 15:50:25 +00:00
Daniel Roth
a8579db4d9 elmhurst site notes fixture 2026-04-24 13:09:30 +00:00
Daniel Roth
e15646c341 rename example site notes to PasHub_ and add Elmhurst example 2026-04-24 13:01:51 +00:00
Daniel Roth
146b5999dc additional fields mapped from pdf 6 🟩 2026-04-23 11:23:29 +00:00
Daniel Roth
2bf8bc6d5e additional fields mapped from pdf 5 🟩 2026-04-23 11:12:25 +00:00
Daniel Roth
c5c3f3fc83 additional fields mapped from pdf 4 🟥 2026-04-23 10:36:15 +00:00
Daniel Roth
7700aac5bb extract cylinder thermostat 🟥 2026-04-21 15:03:12 +00:00
Daniel Roth
da26e4a4cb extract no extensions 🟥 2026-04-21 10:53:14 +00:00
Daniel Roth
ac854f161a extract water heating cylinder thickness 🟥 2026-04-21 10:40:07 +00:00
Daniel Roth
0a8b9e0767 Include inspection metadata in output 2026-04-20 09:04:54 +00:00
Daniel Roth
bc527a039f site notes pdf to json 🟥 2026-04-16 14:45:28 +00:00
Daniel Roth
4f3c7894ae Map to RdSapSiteNotes from site notes JSON 🟥 2026-04-16 13:54:03 +00:00