Model/backend/documents_parser
Khalim Conn-Kowlessar 647c1aad0e Slice S0380.85: Curtain Wall §5.18 dispatch closes BP[2] Ext2 cascade gap
RdSAP 10 §5.18 (PDF p.48) "Curtain wall - U-value and other parameters":

  "If documentary evidence is available, use calculated U-value of the
   whole curtain wall. Otherwise for the purpose of RdSAP, U= 2.0 W/m²K
   for pre-2023 curtain walls, And for post-2023 (2024 in Scotland)
   U-values as for windows given in Notes below Table 24."

Table 24 row "Double or triple glazed England/Wales: 2022 or later"
PVC/wood column = 1.4 W/m²K. Whole-wall curtain walls use Frame
Factor=1 per the §5.18 closer.

Pre-S0380.85 `WALL_CURTAIN=9` was defined at rdsap_uvalues.py:116 but
NOT included in `known_types`, so `u_wall(construction=9)` fell through
to `_DEFAULT_WALL_BY_AGE.get(band, WALL_CAVITY)` → cavity table at age
H = 0.60. Cert 000565 BP[2] Ext2 lodges `Type: CW Curtain Wall` +
`Curtain Wall Age: Post 2023` per Summary PDF §7; worksheet pins U=1.40
(matching the §5.18 Post-2023 PVC/wood row). Cascade under-counted
walls by Δ U=0.80 × area = −112.2 W/K on this BP — 70% of the
post-S0380.84 BP main-wall residual (−161 W/K total).

§5.18 keys the curtain-wall U-value on the per-BP installation age,
NOT on the dwelling-wide `construction_age_band` — cert 000565 is
age H (1991-1995) but the curtain wall itself was installed
Post-2023. Plumb a new optional field through the extractor → datatype
→ mapper → cascade so the §5.18 dispatch sees it.

Files touched (5-layer slice span):

  - backend/documents_parser/elmhurst_extractor.py:
      `_wall_details_from_lines` reads "Curtain Wall Age" via
      `_local_val` so absent lines stay None (not "").
  - datatypes/epc/surveys/elmhurst_site_notes.py:WallDetails:
      `curtain_wall_age: Optional[str] = None` field added.
  - datatypes/epc/domain/epc_property_data.py:SapBuildingPart:
      `curtain_wall_age: Optional[str] = None` field added.
  - datatypes/epc/domain/mapper.py:_map_elmhurst_building_part:
      threads `walls.curtain_wall_age` onto SapBuildingPart.
  - domain/sap10_ml/rdsap_uvalues.py:
      new `_u_curtain_wall(curtain_wall_age)` helper + WALL_CURTAIN
      dispatch in `u_wall` before the `known_types` lookup.
      "Post 2023" / "Post-2023" → 1.4; everything else (incl. None)
      → 2.0 per §5.18 fallback.
  - domain/sap10_calculator/worksheet/heat_transmission.py:
      passes `curtain_wall_age=part.curtain_wall_age` to `u_wall`
      on the main-wall path. (Alt-wall path unchanged — cert 000565
      lodges CW only as a main wall, never as an alt sub-area; alt
      coverage is a follow-up slice if a future cert exercises it.)

Tests (6 new, AAA-structure):

  - 3 in domain/sap10_ml/tests/test_rdsap_uvalues.py — `u_wall` direct
    unit tests for Post 2023 (1.4), Pre 2023 (2.0), and absent
    lodging fallback (2.0).
  - 3 in backend/documents_parser/tests/test_summary_pdf_mapper_chain
    .py — extractor pin (BP[2] Ext2 surfaces "Post 2023", non-CW BPs
    stay None), mapper pin (curtain_wall_age threaded to BP[2]
    SapBuildingPart), cascade pin (`heat_transmission_from_cert`
    walls subtotal ≥ 540 W/K — pre-S0380.85 was 443).

Cert 000565 cascade walls: 443 → 555.93 W/K (worksheet 604.07; 70%
closer). Test baseline: 558 pass (was 555 + 3 new) + 9 expected
`test_sap_result_pin[000565-*]` fails unchanged.

Per [[feedback-verify-handover-claims]]: the post-S0380.84 handover
predicted SH residual would close +2591 → ~+800 kWh after this slice,
but the cascade is actually OVER-counting SH despite walls being
UNDER-counted. Closing the wall under-count makes the SH residual
*larger* (+2591 → +6348). The wall fix is spec-correct; the SH
over-count is a separate channel that surfaces more sharply now. Per
[[feedback-spec-citation-in-commits]] + [[feedback-spec-floor-skepticism]]
+ the S0380.84 precedent, ship the spec-correct change and document
the surfaced gap for the next slice rather than reverting to the
compensating-bugs state.

Pyright net-zero on every touched file (existing pre-existing errors
unchanged). Cohort + golden + cert 9501 unaffected — curtain_wall_age
defaults to None on those certs and `u_wall` ignores it unless
`construction == WALL_CURTAIN`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 23:55:49 +00:00
..
handler address JTK review comments 2026-04-20 15:11:17 +00:00
tests Slice S0380.85: Curtain Wall §5.18 dispatch closes BP[2] Ext2 cascade gap 2026-05-29 23:55:49 +00:00
__init__.py Map to RdSapSiteNotes from site notes JSON 🟥 2026-04-16 13:54:03 +00:00
db_writer.py include updating epc_property_data to pashub to ara workflow 2026-04-29 09:55:14 +00:00
elmhurst_extractor.py Slice S0380.85: Curtain Wall §5.18 dispatch closes BP[2] Ext2 cascade gap 2026-05-29 23:55:49 +00:00
extractor.py Handle wall thickness "Unmeasurable" 🟩 2026-04-30 16:41:16 +00:00
local_runner.py update local runner to work for elmhurst 2026-04-24 14:01:36 +00:00
parser.py load ecmk site notes to db 2026-04-29 11:20:47 +00:00
pdf.py update local runner to work for elmhurst 2026-04-24 14:01:36 +00:00