123 lines
5.0 KiB
Python
123 lines
5.0 KiB
Python
"""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"
|