From ccf7aa21181f090cc7336b4fcef1e34a23d3ba26 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Sun, 24 May 2026 17:40:06 +0000 Subject: [PATCH] =?UTF-8?q?Scaffold:=20end-to-end=20Summary=E2=86=92EpcPro?= =?UTF-8?q?pertyData=20chain=20test=20for=20000474=20(xfail)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../tests/fixtures/Summary_000474.pdf | Bin 0 -> 80569 bytes .../tests/test_summary_pdf_mapper_chain.py | 105 ++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 backend/documents_parser/tests/fixtures/Summary_000474.pdf create mode 100644 backend/documents_parser/tests/test_summary_pdf_mapper_chain.py diff --git a/backend/documents_parser/tests/fixtures/Summary_000474.pdf b/backend/documents_parser/tests/fixtures/Summary_000474.pdf new file mode 100644 index 0000000000000000000000000000000000000000..be39243bb88725eb2c3fd418be5809bda0136fc6 GIT binary patch literal 80569 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 AnE(I) literal 0 HcmV?d00001 diff --git a/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py new file mode 100644 index 00000000..ed32dafc --- /dev/null +++ b/backend/documents_parser/tests/test_summary_pdf_mapper_chain.py @@ -0,0 +1,105 @@ +"""End-to-end scaffold for the Elmhurst Summary→EpcPropertyData chain. + +The 6 Elmhurst worksheet fixtures in `domain.sap.worksheet.tests` +build their `EpcPropertyData` synthetically — they validate the +calculator + cascade in isolation from the mapper. This file pins +the OTHER half of the chain: `from_elmhurst_site_notes` must produce +a calculator-equivalent `EpcPropertyData` when fed the Summary +PDF the worksheet was generated from. If the two halves agree, the +WHOLE pipeline (extractor + mapper + cascade + calculator) is +validated end-to-end against authoritative Elmhurst documents. + +Status: xfail. Today's audit (2026-05-24) surfaced a 28-field diff +between `from_elmhurst_site_notes(Summary_000474)` and the hand- +built `_elmhurst_worksheet_000474.build_epc()`. The load-bearing +gaps (calculator-relevant): + - sap_building_parts: 1 instead of 3 — mapper produces a single + bp via `[_map_elmhurst_building_part(survey)]` at [mapper.py:288](datatypes/epc/domain/mapper.py#L288) + - sap_windows: 0 instead of 5 — mapper plumbs no windows + - renewable_heat_incentive: None instead of RenewableHeatIncentive + - sap_heating / sap_ventilation differ in details + +Preprocessing: the existing `ElmhurstSiteNotesExtractor` was written +against Textract-style output (label\\nvalue pairs in spatial +reading order). We don't have Textract in the test environment, so +this helper converts `pdftotext -layout` output (label-whitespace- +value on a single line) into the Textract-style sequence the +extractor expects. Test-only preprocessing; production runs through +Textract directly. +""" + +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + +import pytest + +from backend.documents_parser.elmhurst_extractor import ElmhurstSiteNotesExtractor +from datatypes.epc.domain.mapper import EpcPropertyDataMapper + +_FIXTURES = Path(__file__).parent / "fixtures" +_SUMMARY_000474_PDF = _FIXTURES / "Summary_000474.pdf" + + +def _summary_pdf_to_textract_style_pages(pdf_path: Path) -> list[str]: + """Convert a Summary PDF into the per-page text format the existing + `ElmhurstSiteNotesExtractor` expects (label\\nvalue sequences). + + `pdftotext -layout` preserves the spatial pairing of label and value + on each line; we split each line on 2+ spaces to surface the + label/value tokens, then concatenate them back into a single + newline-delimited stream per page. + """ + info = subprocess.run( + ["pdfinfo", str(pdf_path)], capture_output=True, text=True, check=True + ).stdout + m = re.search(r"Pages:\s+(\d+)", info) + if m is None: + raise RuntimeError(f"Could not parse page count from {pdf_path}") + page_count = int(m.group(1)) + + pages: list[str] = [] + for i in range(1, page_count + 1): + layout = subprocess.run( + [ + "pdftotext", "-layout", "-f", str(i), "-l", str(i), + str(pdf_path), "-", + ], + capture_output=True, text=True, check=True, + ).stdout + tokens: list[str] = [] + for line in layout.splitlines(): + if not line.strip(): + tokens.append("") + continue + parts = [p for p in re.split(r"\s{2,}", line.strip()) if p] + tokens.extend(parts) + pages.append("\n".join(tokens)) + return pages + + +@pytest.mark.xfail( + reason=( + "Elmhurst mapper `from_elmhurst_site_notes` currently produces a " + "single SapBuildingPart regardless of the cert's actual count; " + "cert 000474 lodges Main + Extension 1 + Extension 2 (3 bps). " + "See module docstring for full punch list." + ), + strict=True, +) +def test_summary_000474_mapper_produces_three_building_parts() -> None: + # Arrange — cert U985-0001-000474 is a mid-terrace with 3 building + # parts (Main + 2 extensions) per the hand-built worksheet fixture + # at packages/domain/src/domain/sap/worksheet/tests/ + # _elmhurst_worksheet_000474.py. Routing the Summary PDF through + # extractor + mapper must yield the same count. + pages = _summary_pdf_to_textract_style_pages(_SUMMARY_000474_PDF) + site_notes = ElmhurstSiteNotesExtractor(pages).extract() + + # Act + epc = EpcPropertyDataMapper.from_elmhurst_site_notes(site_notes) + + # Assert + assert len(epc.sap_building_parts) == 3