diff --git a/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py b/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py new file mode 100644 index 00000000..86fef5d5 --- /dev/null +++ b/packages/domain/src/domain/sap/worksheet/tests/test_section_cascade_pins.py @@ -0,0 +1,90 @@ +"""Section-by-section cascade pins against U985 PDF line refs. + +Each pin walks the actual cert→inputs cascade (not the per-section +isolation tests) and asserts the produced value matches the worksheet +PDF line ref to abs=1e-4 for every fixture in the cohort. Tests run in +worksheet order (§1, §2, §3, ..., §12) — when a pin fails the residual +localises by section. Bottom-up discipline: a failing §3 pin gets +fixed before §10a is touched. + +Per `[[feedback-e2e-validation-philosophy]]`: tolerances are NOT +widened to mask drift. A failing pin is a named calculator bug. + +Reference: SAP 10.2 specification (14-03-2025). +""" +from typing import Final + +import pytest + +from domain.sap.rdsap.cert_to_inputs import cert_to_inputs +from domain.sap.worksheet.dimensions import dimensions_from_cert +from domain.sap.worksheet.tests import ( + _elmhurst_worksheet_000474 as _w000474, + _elmhurst_worksheet_000477 as _w000477, + _elmhurst_worksheet_000480 as _w000480, + _elmhurst_worksheet_000487 as _w000487, + _elmhurst_worksheet_000490 as _w000490, + _elmhurst_worksheet_000516 as _w000516, +) + + +_FIXTURES: Final[dict[str, object]] = { + "000474": _w000474, + "000477": _w000477, + "000480": _w000480, + "000487": _w000487, + "000490": _w000490, + "000516": _w000516, +} + +_FLOAT_PIN_ABS: Final[float] = 1e-4 + + +def _pin(actual: float, expected: float, tag: str) -> None: + """Assert `actual` matches `expected` to abs=1e-4. The PDF lodges + every worksheet line ref to 4 d.p.; anything looser is drift.""" + diff = abs(actual - expected) + assert diff < _FLOAT_PIN_ABS, ( + f"{tag}: actual={actual}, expected={expected}, diff={diff:.6f}" + ) + + +# ============================================================================ +# §1 Overall dwelling dimensions — LINE_4 TFA, LINE_5 Volume +# ============================================================================ + + +@pytest.mark.parametrize("fixture_name", list(_FIXTURES), ids=lambda x: x) +def test_section_1_line_4_total_floor_area_matches_pdf(fixture_name: str) -> None: + """§1 (4) — total floor area Σ across heated storeys.""" + # Arrange + mod = _FIXTURES[fixture_name] + epc = mod.build_epc() # type: ignore[attr-defined] + + # Act + dim = dimensions_from_cert(epc) + + # Assert + _pin( + dim.total_floor_area_m2, + mod.LINE_4_TFA_M2, # type: ignore[attr-defined] + f"§1 (4) {fixture_name}", + ) + + +@pytest.mark.parametrize("fixture_name", list(_FIXTURES), ids=lambda x: x) +def test_section_1_line_5_volume_matches_pdf(fixture_name: str) -> None: + """§1 (5) — dwelling internal volume Σ across storeys.""" + # Arrange + mod = _FIXTURES[fixture_name] + epc = mod.build_epc() # type: ignore[attr-defined] + + # Act + dim = dimensions_from_cert(epc) + + # Assert + _pin( + dim.volume_m3, + mod.LINE_5_VOLUME_M3, # type: ignore[attr-defined] + f"§1 (5) {fixture_name}", + )