feat: coverage ladder + vision-unverified stamping (classic path)
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
"""Coverage-driven extraction retry ladder — classic path (pipeline/extractor.py)."""
|
||||
|
||||
from backend import config
|
||||
from backend.pipeline import extractor
|
||||
|
||||
PAGE_TEXT = ("1. \nALL SAWN LUMBER IN CONTACT WITH SOIL TO BE SOUTHERN PINE, "
|
||||
"PRESSURE TREATED.\n2. \nROOF SHEATHING: 5/8\" PLYWOOD, C-D GRADE, "
|
||||
"STRUCTURAL I.")
|
||||
|
||||
|
||||
def _page(n=8, text=PAGE_TEXT):
|
||||
return {"page_number": n, "base64": "AAAA", "text_layer": text}
|
||||
|
||||
|
||||
def test_classic_fallback_when_vision_returns_nothing(monkeypatch):
|
||||
"""Vision pass returns an unusable bare list; text retry disabled ->
|
||||
deterministic fallback stubs make a dark sheet impossible."""
|
||||
monkeypatch.setattr(extractor, "call_json",
|
||||
lambda **kw: [{"name": "general notes", "value": "notes"}])
|
||||
monkeypatch.setattr(config, "EXTRACT_TEXT_RETRY_ENABLED", False)
|
||||
sheet = extractor._extract_one(_page())
|
||||
assert sheet["assertions"], "dark sheet must be impossible with fallback enabled"
|
||||
assert all(a.get("grounding") == "text_layer_fallback"
|
||||
for a in sheet["assertions"])
|
||||
assert sheet["coverage"]["ratio"] >= 0.6
|
||||
|
||||
|
||||
def test_classic_merge_preserves_graphical_objects(monkeypatch):
|
||||
"""Rung-2 merge must never drop vision-only graphical objects."""
|
||||
def fake(**kw):
|
||||
if kw.get("images_b64"):
|
||||
return {"sheet": {}, "objects": [
|
||||
{"object_id": "g1", "object_type": "lighting_fixture",
|
||||
"name": "pendant at grid C-4", "source_text": None,
|
||||
"graphical_basis": "16in pendant symbol at grid C-4"}]}
|
||||
return {"sheet": {}, "objects": [
|
||||
{"object_id": "t1", "object_type": "general_note",
|
||||
"source_text": "ALL SAWN LUMBER IN CONTACT WITH SOIL TO BE "
|
||||
"SOUTHERN PINE, PRESSURE TREATED.",
|
||||
"name": "lumber note"}]}
|
||||
|
||||
monkeypatch.setattr(extractor, "call_json", fake)
|
||||
sheet = extractor._extract_one(_page())
|
||||
assert any(a.get("graphical_basis") for a in sheet["assertions"])
|
||||
assert any("SAWN LUMBER" in (a.get("source_text") or "")
|
||||
for a in sheet["assertions"])
|
||||
|
||||
|
||||
def test_classic_recovers_sheet_number(monkeypatch):
|
||||
text = ("REFLECTED CEILING PLAN\n"
|
||||
"GYP. BD. CEILING 8'-11 3/8\" A.F.F. TYP. FOR ALL STOREFRONT\n"
|
||||
"LED TAPE LIGHT. SEE ELEC. SCONCE 8'-0\" A.F.F., SEE ELEC.\n"
|
||||
"A102")
|
||||
|
||||
def fake(**kw):
|
||||
if kw.get("images_b64"):
|
||||
return {"sheet": {}, "objects": [
|
||||
{"object_id": "o1", "name": "RCP ceiling note",
|
||||
"source_text": "GYP. BD. CEILING 8'-11 3/8\" A.F.F. TYP. "
|
||||
"FOR ALL STOREFRONT",
|
||||
"attributes": {"height": "8'-11 3/8\""}}]}
|
||||
return {"sheet": {}, "objects": []}
|
||||
|
||||
monkeypatch.setattr(extractor, "call_json", fake)
|
||||
sheet = extractor._extract_one(_page(18, text))
|
||||
assert sheet["sheet_number"] == "A102"
|
||||
|
||||
|
||||
def test_classic_skips_retry_when_coverage_healthy(monkeypatch):
|
||||
calls = []
|
||||
|
||||
def fake(**kw):
|
||||
calls.append(kw)
|
||||
return {"sheet": {"sheet_number": "S202"}, "objects": [
|
||||
{"object_id": "o1", "name": "lumber note",
|
||||
"source_text": "ALL SAWN LUMBER IN CONTACT WITH SOIL TO BE "
|
||||
"SOUTHERN PINE, PRESSURE TREATED.",
|
||||
"attributes": {"species": "southern pine"}},
|
||||
{"object_id": "o2", "name": "sheathing note",
|
||||
"source_text": "ROOF SHEATHING: 5/8\" PLYWOOD, C-D GRADE, "
|
||||
"STRUCTURAL I.",
|
||||
"attributes": {"sheathing": "5/8 plywood"}}]}
|
||||
|
||||
monkeypatch.setattr(extractor, "call_json", fake)
|
||||
sheet = extractor._extract_one(_page())
|
||||
assert len(calls) == 1, "healthy coverage must not trigger the text-only rung"
|
||||
assert sheet["coverage"]["ratio"] == 1.0
|
||||
assert sheet["sheet_number"] == "S202"
|
||||
|
||||
|
||||
def test_classic_scanned_page_keeps_failed_sheet_shape(monkeypatch):
|
||||
"""No text layer (scanned page): total parse failure keeps the existing
|
||||
'extraction failed' empty-sheet return — ladder is text-layer-only."""
|
||||
monkeypatch.setattr(extractor, "call_json", lambda **kw: None)
|
||||
sheet = extractor._extract_one({"page_number": 4, "base64": "AAAA",
|
||||
"text_layer": None})
|
||||
assert sheet["assertions"] == []
|
||||
assert "extraction failed" in (sheet.get("sheet_title") or "")
|
||||
@@ -84,3 +84,39 @@ def test_extractor_instruction_fully_substituted():
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user