94 lines
3.4 KiB
Python
94 lines
3.4 KiB
Python
"""sheet_reconcile.py - deterministic sheet-list reconciliation (no LLM).
|
|
|
|
The cover sheet's own sheet index (SHEET LIST / DRAWING INDEX) declares which
|
|
sheets the set is SUPPOSED to contain. Comparing that declaration against the
|
|
sheets wave-1 actually identified answers two early questions:
|
|
|
|
- declared_not_in_set: sheets the index lists but we didn't identify - dark
|
|
pages, misidentification, or disciplines genuinely absent from this PDF.
|
|
- in_set_not_declared: sheet numbers we extracted that the index doesn't
|
|
list - misread title blocks or unlisted sheets.
|
|
|
|
Deterministic complement to the LLM sheet_index stage, which can only infer
|
|
from what extraction already found.
|
|
"""
|
|
|
|
import re
|
|
from typing import Dict, List, Optional
|
|
|
|
# Markers that introduce the drawing set's own sheet index on a cover page.
|
|
_INDEX_MARKERS = (
|
|
"SHEET LIST",
|
|
"DRAWING INDEX",
|
|
"SHEET INDEX",
|
|
"DRAWING LIST",
|
|
"INDEX OF DRAWINGS",
|
|
)
|
|
|
|
# Sheet ids: 1-2 letters, optional hyphen, 2-3 digits, optional decimal suffix.
|
|
# Covers S301, A102, LS101, C-001, C-001.1; excludes dates/project numbers
|
|
# (pure digits) and member marks (W12X26 - letter after digits).
|
|
_SHEET_TOKEN_RE = re.compile(r"\b([A-Z]{1,2}-?\d{2,3}(?:\.\d+)?)\b")
|
|
|
|
# Only cover-front pages carry the set index.
|
|
_MAX_INDEX_PAGE = 5
|
|
|
|
|
|
def _normalize_id(sheet_id: str) -> str:
|
|
return (sheet_id or "").upper().replace("-", "").strip()
|
|
|
|
|
|
def declared_sheet_list(page_texts: Dict[int, Optional[str]]) -> List[str]:
|
|
"""Scrape the declared sheet list off the cover page's text layer.
|
|
|
|
page_texts: {page_number: text_layer_or_None}. Returns the ordered,
|
|
deduped list of declared sheet ids, or [] when no index marker exists.
|
|
Only the FIRST page containing a marker is parsed (later 'sheet list'
|
|
echoes in legends/schedules are ignored).
|
|
"""
|
|
for page_number in sorted(page_texts):
|
|
if page_number > _MAX_INDEX_PAGE:
|
|
break
|
|
text = page_texts.get(page_number) or ""
|
|
upper = text.upper()
|
|
marker_at = -1
|
|
for marker in _INDEX_MARKERS:
|
|
marker_at = upper.find(marker)
|
|
if marker_at >= 0:
|
|
break
|
|
if marker_at < 0:
|
|
continue
|
|
section = text[marker_at:]
|
|
declared: List[str] = []
|
|
for token in _SHEET_TOKEN_RE.findall(section):
|
|
if token not in declared:
|
|
declared.append(token)
|
|
return declared
|
|
return []
|
|
|
|
|
|
def reconcile_sheets(sheets: List[Dict], declared: List[str]) -> Dict:
|
|
"""Compare extracted sheet_numbers against the declared index.
|
|
|
|
Comparison is hyphen/case-normalized; output lists keep the declared /
|
|
extracted originals.
|
|
"""
|
|
found: List[str] = [str(s["sheet_number"]) for s in sheets or []
|
|
if s.get("sheet_number")]
|
|
found_norm = {_normalize_id(n) for n in found}
|
|
declared_norm = {_normalize_id(n) for n in declared}
|
|
|
|
declared_not_in_set = [n for n in declared if _normalize_id(n) not in found_norm]
|
|
# Preserve extraction order, dedupe, keep originals.
|
|
in_set_not_declared: List[str] = []
|
|
for n in found:
|
|
if _normalize_id(n) not in declared_norm and n not in in_set_not_declared:
|
|
in_set_not_declared.append(n)
|
|
|
|
return {
|
|
"declared_total": len(declared),
|
|
"found_total": len(found),
|
|
"declared_not_in_set": declared_not_in_set,
|
|
"in_set_not_declared": in_set_not_declared,
|
|
}
|