feat: cross-level xref link scopes via detail_reference and member tag
This commit is contained in:
@@ -30,9 +30,23 @@ def _family(assertion: Dict) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _xref_keys(assertion: Dict) -> List[str]:
|
||||||
|
"""Cross-level join keys: detail references and member tags."""
|
||||||
|
location = assertion.get("location_key") or {}
|
||||||
|
keys = []
|
||||||
|
ref = re.sub(r"\s+", "", str(location.get("detail_reference") or "")).upper()
|
||||||
|
if ref:
|
||||||
|
keys.append(f"detail:{ref}")
|
||||||
|
tag = re.sub(r"\s+", "", str(location.get("tag") or "")).upper()
|
||||||
|
if re.match(r"^[A-Z]{2,}\d", tag): # member marks: HSS16X4X5/8, W12X26, ...
|
||||||
|
keys.append(f"tag:{tag}")
|
||||||
|
return keys
|
||||||
|
|
||||||
|
|
||||||
def build_link_scopes(sheets: List[Dict]) -> List[AgentScope]:
|
def build_link_scopes(sheets: List[Dict]) -> List[AgentScope]:
|
||||||
"""Partition facts by level and object/tag family, then enforce a hard cap."""
|
"""Partition facts by level and object/tag family, then enforce a hard cap."""
|
||||||
buckets: Dict[Tuple[str, str], List[Dict]] = defaultdict(list)
|
buckets: Dict[Tuple[str, str], List[Dict]] = defaultdict(list)
|
||||||
|
xref: Dict[str, List[Dict]] = defaultdict(list)
|
||||||
for sheet in sheets:
|
for sheet in sheets:
|
||||||
for assertion in sheet.get("assertions", []):
|
for assertion in sheet.get("assertions", []):
|
||||||
enriched = {
|
enriched = {
|
||||||
@@ -44,6 +58,8 @@ def build_link_scopes(sheets: List[Dict]) -> List[AgentScope]:
|
|||||||
level = str((assertion.get("location_key") or {}).get("level")
|
level = str((assertion.get("location_key") or {}).get("level")
|
||||||
or sheet.get("level") or "unknown").lower()
|
or sheet.get("level") or "unknown").lower()
|
||||||
buckets[(level, _family(assertion))].append(enriched)
|
buckets[(level, _family(assertion))].append(enriched)
|
||||||
|
for key in _xref_keys(assertion):
|
||||||
|
xref[key].append(enriched)
|
||||||
|
|
||||||
scopes: List[AgentScope] = []
|
scopes: List[AgentScope] = []
|
||||||
cap = max(2, config.AGENT_LINK_MAX_ASSERTIONS)
|
cap = max(2, config.AGENT_LINK_MAX_ASSERTIONS)
|
||||||
@@ -56,6 +72,15 @@ def build_link_scopes(sheets: List[Dict]) -> List[AgentScope]:
|
|||||||
scope_id=f"{level}:{family}:{offset // cap + 1}",
|
scope_id=f"{level}:{family}:{offset // cap + 1}",
|
||||||
payload={"assertions": chunk, "level": level, "family": family},
|
payload={"assertions": chunk, "level": level, "family": family},
|
||||||
))
|
))
|
||||||
|
for key, assertions in sorted(xref.items()):
|
||||||
|
sheets_present = {a.get("sheet_number") for a in assertions}
|
||||||
|
if len(assertions) < 2 or len(sheets_present) < 2:
|
||||||
|
continue
|
||||||
|
scopes.append(AgentScope(
|
||||||
|
scope_id=f"xref:{key}",
|
||||||
|
payload={"assertions": assertions[:cap],
|
||||||
|
"level": "xref", "family": key},
|
||||||
|
))
|
||||||
return scopes
|
return scopes
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from backend.agents.base import AgentScope
|
||||||
|
from backend.agents.linker import build_link_scopes
|
||||||
|
|
||||||
|
|
||||||
|
def _sheet(number, page, level, assertions):
|
||||||
|
return {"sheet_number": number, "page_number": page,
|
||||||
|
"discipline": "Structural", "level": level,
|
||||||
|
"assertions": assertions}
|
||||||
|
|
||||||
|
|
||||||
|
def _assertion(id_, ref=None, tag=None, level=None):
|
||||||
|
return {"id": id_, "attribute": "stud_pack_size", "value": "(5) 2x6",
|
||||||
|
"source_text": "(5) 2x6 STUD PACK",
|
||||||
|
"location_key": {"detail_reference": ref, "tag": tag,
|
||||||
|
"level": level}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_xref_scope_joins_same_detail_reference_across_levels():
|
||||||
|
sheets = [
|
||||||
|
_sheet("S101", 10, "foundation", [_assertion("a1", ref="A/S205")]),
|
||||||
|
_sheet("S205", 20, "roof", [_assertion("a2", ref="A/S205")]),
|
||||||
|
_sheet("S401", 30, "roof", [_assertion("a3", ref="A/S205")]),
|
||||||
|
]
|
||||||
|
scopes = build_link_scopes(sheets)
|
||||||
|
xref = [s for s in scopes if s.scope_id.startswith("xref:")]
|
||||||
|
assert xref, "expected a cross-level detail-reference scope"
|
||||||
|
ids = {a["id"] for s in xref for a in s.payload["assertions"]}
|
||||||
|
assert ids == {"a1", "a2", "a3"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_xref_scope_requires_two_distinct_sheets():
|
||||||
|
sheets = [
|
||||||
|
_sheet("S401", 30, "roof", [_assertion("a1", ref="A/S205"),
|
||||||
|
_assertion("a2", ref="A/S205")]),
|
||||||
|
]
|
||||||
|
scopes = build_link_scopes(sheets)
|
||||||
|
assert not [s for s in scopes if s.scope_id.startswith("xref:")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_xref_scope_joins_shared_member_tag():
|
||||||
|
sheets = [
|
||||||
|
_sheet("S102", 5, "roof", [_assertion("a1", tag="HSS16X4X5/8")]),
|
||||||
|
_sheet("S401", 30, "unknown", [_assertion("a2", tag="HSS16X4X5/8")]),
|
||||||
|
]
|
||||||
|
scopes = build_link_scopes(sheets)
|
||||||
|
xref = [s for s in scopes if s.scope_id.startswith("xref:")]
|
||||||
|
assert xref
|
||||||
Reference in New Issue
Block a user