Spaces:
Runtime error
Runtime error
| """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) | |