"""Grounding-guard rescue tier, text-layer prompt block, and render hygiene.""" from backend import config from backend.pipeline._stage import render from backend.pipeline.extractor import ( _is_grounded, _normalize_sheet, _text_layer_block, ) from backend.prompts import EXTRACTOR_USER_INSTRUCTION, VERIFY_USER_INSTRUCTION PAGE_TEXT = "NOTES: (5) 2X6 STUD PACK AT BEARING. HSS16X4 BEAM. 7'-0\" AFF." def _parsed(value, source_text): return { "sheet": {"sheet_number": "S401"}, "objects": [{ "object_id": "o1", "object_type": "framing", "name": "stud pack", "attributes": {"count": value}, "source_text": source_text, }], } def test_rescue_tier_keeps_and_stamps(): """Digits absent from source_text but present in the page text layer: kept, stamped grounding=text_layer (vision quoted imperfectly).""" sheet = _normalize_sheet(_parsed("(2)", "(2) 2x6 STUD PACK"), 1, page_text=PAGE_TEXT) # "(2)" is not grounded by its own source_text alone? it is - use a value # whose digits differ from the quote to exercise the rescue path. sheet = _normalize_sheet(_parsed("5", "(2) 2x6 STUD PACK"), 1, page_text=PAGE_TEXT) assert len(sheet["assertions"]) == 1 assert sheet["assertions"][0]["grounding"] == "text_layer" def test_no_rescue_without_page_text(): sheet = _normalize_sheet(_parsed("5", "(2) 2x6 STUD PACK"), 1) assert sheet["assertions"] == [] def test_still_dropped_when_digits_nowhere(): sheet = _normalize_sheet(_parsed("99", "(2) 2x6 STUD PACK"), 1, page_text=PAGE_TEXT) assert sheet["assertions"] == [] def test_is_grounded_backward_compatible(): assert _is_grounded("(5)", "(5) 2x6 STUD PACK") is True # Digit-run guard is a set check: "(3)" has no support anywhere. assert _is_grounded("(3)", "(5) 2x6 STUD PACK") is False assert _is_grounded("(3)", "(5) 2x6 STUD PACK", page_text="(3) 2x6 STUD PACK") is True def test_text_layer_block_empty_without_layer(): assert _text_layer_block({"page_number": 1}) == "" assert _text_layer_block({"page_number": 1, "text_layer": None}) == "" def test_text_layer_block_appends_and_caps(monkeypatch): block = _text_layer_block({"page_number": 1, "text_layer": PAGE_TEXT}) assert "TEXT LAYER" in block and "STUD PACK" in block monkeypatch.setattr(config, "TEXT_LAYER_MAX_CHARS", 50) block = _text_layer_block({"page_number": 1, "text_layer": "x" * 500}) assert len(block.split(":\n", 1)[1]) == 50 def test_verify_instruction_fully_rendered(): """render() silently leaves missing keys as literals - both placeholders must be substituted at the (single) verify render site.""" out = render(VERIFY_USER_INSTRUCTION, {"finding": "FINDING_JSON", "text_layer": "PAGE_TEXT"}) assert "{finding}" not in out and "{text_layer}" not in out assert "FINDING_JSON" in out and "PAGE_TEXT" in out def test_extractor_instruction_fully_substituted(): page = {"page_number": 1, "text_layer": PAGE_TEXT} out = (EXTRACTOR_USER_INSTRUCTION.replace("{sheet_hint}", "") + _text_layer_block(page)) assert "{sheet_hint}" not in out def test_vision_unverified_stamp_when_source_text_not_in_text_layer(): """Digits ground the object against the page text, but its quoted source_text is not actually present in the text layer: kept, stamped vision_unverified (the wave-5b verifier consumes grounding stamps).""" page_text = "WALL: 2X6 WD STUD @ 16\" O.C. WITH R-13 BATT INSULATION" parsed = {"sheet": {}, "objects": [ {"object_id": "x1", "name": "stud pack", "source_text": "(5) 2X6 STUD PACK AT JAMB", # NOT in page text "attributes": {"count": "5"}}]} sheet = _normalize_sheet(parsed, 1, page_text=page_text) assert len(sheet["assertions"]) == 1 assert sheet["assertions"][0]["grounding"] == "vision_unverified" def test_no_unverified_stamp_when_source_text_in_text_layer(): page_text = "WALL: 2X6 WD STUD @ 16\" O.C. WITH R-13 BATT INSULATION" parsed = {"sheet": {}, "objects": [ {"object_id": "x1", "name": "stud note", "source_text": "2X6 WD STUD @ 16\" O.C.", "attributes": {"size": "2x6"}}]} sheet = _normalize_sheet(parsed, 1, page_text=page_text) assert len(sheet["assertions"]) == 1 assert "grounding" not in sheet["assertions"][0] def test_preset_grounding_stamp_survives_normalization(): """Fallback/merge rungs stamp grounding upstream; normalization must preserve a pre-set stamp instead of recomputing it away.""" parsed = {"sheet": {}, "objects": [ {"object_id": "f1", "name": "lumber note", "source_text": "ALL LUMBER SOUTHERN PINE", "grounding": "text_layer_fallback"}]} sheet = _normalize_sheet(parsed, 1, page_text="ALL LUMBER SOUTHERN PINE") assert sheet["assertions"][0]["grounding"] == "text_layer_fallback"