RICS / backend /tests /test_notes_context_inheritance.py
StormShadow308's picture
Add demo documentation and Docker setup for v2 report generation system
aad7814
Raw
History Blame Contribute Delete
6.23 kB
"""Guard tests for block-level context inheritance + the routing tier lock.
These pin the *structural* contract of how multi-sentence note blocks route β€”
NOT the wording of any particular surveyor's notes. Surveyors pack several
sentences (and several elements) into one paragraph; only the first sentence of
a block usually carries the section label/code. The parser therefore:
* lets a sentence with no signal INHERIT the block's running section,
* lets a STRONG signal (explicit code / element label / high-confidence keyword)
set or override that running section,
* keeps a WEAK general-keyword match from overriding an established section
(so generic "repairs and repointing" boilerplate is not dragged to the walls),
* always files a HAZARD (I-group) mention in its own section without letting it
hijack the surrounding element context.
"""
from __future__ import annotations
from backend.core.notes_keyword_router import (
TIER_EXPLICIT,
TIER_GENERAL,
TIER_HIGH,
TIER_LABEL,
TIER_NONE,
classify_note_cascade,
classify_note_cascade_with_tier,
)
from backend.core.notes_parser import UNASSIGNED, parse_notes_to_sections
from backend.core.rics_canonical_l3 import build_canonical_template_schema
def _schema():
return build_canonical_template_schema()
def _by_id(notes) -> dict[str, str]:
return {n.section_id: n.text.lower() for n in notes}
# ── Routing tier contract (router level, schema-independent) ─────────────────
def test_tier_explicit_code():
sid, tier, _ = classify_note_cascade_with_tier("F6, drainage chamber lifted.")
assert (sid, tier) == ("F6", TIER_EXPLICIT)
def test_tier_element_label():
sid, tier, _ = classify_note_cascade_with_tier("Main roof: slipped slates noted.")
assert (sid, tier) == ("D2", TIER_LABEL)
def test_tier_high_confidence_keyword():
sid, tier, _ = classify_note_cascade_with_tier("Ceilings show some deflection.")
assert (sid, tier) == ("E2", TIER_HIGH)
def test_tier_none_for_nonsense():
sid, tier, _ = classify_note_cascade_with_tier("xyzzy plugh frobnicator quux")
assert (sid, tier) == (UNASSIGNED, TIER_NONE)
def test_cascade_backcompat_scores_unchanged():
"""The legacy (section, score, body) contract still scores 1.0 / 0.0."""
assert classify_note_cascade("Ceilings show some deflection.")[1] == 1.0
assert classify_note_cascade("xyzzy plugh frobnicator quux")[1] == 0.0
def test_bare_repointing_is_weak_but_wall_repointing_is_strong():
"""Unqualified repointing is ambiguous (weak); wall-qualified stays strong."""
sid, tier, _ = classify_note_cascade_with_tier("Repairs and repointing are recommended.")
assert (sid, tier) == ("D4", TIER_GENERAL)
sid2, tier2, _ = classify_note_cascade_with_tier("Wall repointing is recommended.")
assert (sid2, tier2) == ("D4", TIER_HIGH)
# ── Block-level context inheritance (parser level, canonical schema) ─────────
def test_signal_less_continuation_inherits_label_block():
notes = parse_notes_to_sections(
"Chimney stack: brick chimney generally sound. It should be monitored over time.",
_schema(),
)
by_id = _by_id(notes)
assert "monitored over time" in by_id.get("D1", "")
def test_weak_keyword_does_not_override_established_context():
"""The core fix: generic repointing inside a chimney block stays with D1."""
notes = parse_notes_to_sections(
"Chimney stack: brick chimney inspected. Repairs and repointing are recommended.",
_schema(),
)
by_id = _by_id(notes)
assert "repointing" in by_id.get("D1", "")
assert "repointing" not in by_id.get("D4", "")
def test_strong_keyword_overrides_established_context():
notes = parse_notes_to_sections(
"Roof structure: trusses and purlins inspected from the loft. "
"Ceilings throughout show some movement.",
_schema(),
)
by_id = _by_id(notes)
assert "trusses" in by_id.get("E1", "") or "purlins" in by_id.get("E1", "")
assert "ceilings" in by_id.get("E2", "")
def test_explicit_code_mid_block_breaks_context():
notes = parse_notes_to_sections(
"E1 roof structure of traditional design with adequate support. "
"E3 damp was noted to an internal partition wall.",
_schema(),
)
by_id = _by_id(notes)
assert "roof structure" in by_id.get("E1", "")
assert "damp" in by_id.get("E3", "")
def test_weak_hazard_files_to_hazard_without_hijacking_context():
"""A weak (general-tier) hazard is filed to I3 but does not capture the
block: the following signal-less sentence still inherits the element."""
notes = parse_notes_to_sections(
"Staircase: timber staircase inspected. The balustrade is a safety concern. "
"Otherwise it remains secure.",
_schema(),
)
by_id = _by_id(notes)
assert "staircase" in by_id.get("E7", "")
assert "balustrade" in by_id.get("I3", "")
assert "remains secure" in by_id.get("E7", "")
assert "balustrade" not in by_id.get("E7", "")
def test_block_start_weak_keyword_establishes_context():
"""When a block opens with only a weak keyword, it still sets the context so
signal-less continuations are not lost to UNASSIGNED."""
notes = parse_notes_to_sections(
"Some repointing is needed. It should be carried out by a competent contractor.",
_schema(),
)
by_id = _by_id(notes)
assert "repointing" in by_id.get("D4", "")
assert "competent contractor" in by_id.get("D4", "")
def test_paragraph_boundary_resets_context():
notes = parse_notes_to_sections(
"Chimney stack: brick chimney noted. Needs attention in due course.\n\n"
"Windows: timber sash units. Some are painted shut.",
_schema(),
)
by_id = _by_id(notes)
assert "needs attention" in by_id.get("D1", "")
assert "painted shut" in by_id.get("D5", "")
assert "painted shut" not in by_id.get("D1", "")
def test_signal_less_block_with_no_context_stays_unassigned():
notes = parse_notes_to_sections("Xyzzy plugh frobnicator quux.", _schema())
assert any(n.section_id == UNASSIGNED for n in notes)