# -*- coding: utf-8 -*- """Plant extraction rule packs. The property every test here circles is the one the module rests on: a pack may re-describe a note or move it to a field a person reads, and NOTHING it can say makes a note disappear. Everything else -- which regex fires, what sentence comes back -- is detail beside that. The seven notes used throughout are the real ones, verbatim, from `WPA1.0` (sample_job, 8" horizontal wall) read on 2026-09-01: the run that prompted this module, where all seven landed in `unresolved` under one blanket reason. """ import pytest from vibe import extractrules from vibe.extractrules import mc # The seven, in sheet order. Whitespace-normalised the way # `ticketqc.unresolved_in_view` normalises before anything sees them. WPA1_NOTES = [ '*USE MC STANDARD REINFORCEMENT FOR CUT DOWN PANELS AND AROUND ALL OPENINGS', 'STRIPPING & BLOCKING DETAIL', 'USE FOR BACK OR EDGE PICK (UNO)', 'STRAND PATTERN', 'STRAND', "#5 BAR X CONT ONLY NEEDED WHEN LAST PAIR OF STRANDS ARE MORE THAN 1'-0 " 'FROM EDGE OF WALL.', "STRIPPING & BLOCKING DETAIL < 25'", ] def entries(texts, reason='no extraction rule matched this note'): return [{'text': t, 'reason': reason} for t in texts] def know_with(packs): """A knowledge stand-in and a getter over it, so these tests never need the real layered loader (which wants a Revit doc).""" store = {'extract.rule_packs': packs} return store, lambda know, key, default=None: know.get(key, default) # --------------------------------------------------------------- the gate def test_no_declaration_loads_no_pack_and_changes_nothing(): """The honest default: told nothing about an office, recognise nothing.""" know, get = know_with([]) packs, problems = extractrules.load(know, get) assert packs == [] assert problems == [] given = entries(WPA1_NOTES) notes, unresolved = extractrules.apply_to(given, packs) assert notes == [] assert unresolved == given def test_declared_pack_loads(): know, get = know_with(['mc']) packs, problems = extractrules.load(know, get) assert problems == [] assert [p.NAME for p in packs] == ['mc'] def test_a_bare_string_is_accepted_as_one_name(): know, get = know_with('mc') packs, problems = extractrules.load(know, get) assert problems == [] assert [p.NAME for p in packs] == ['mc'] def test_unknown_pack_is_refused_loudly_and_nothing_loads(): """All or nothing. A half-loaded rule set classifies some notes and not others, and the reader cannot tell which sentences to trust.""" know, get = know_with(['mc', 'no-such-plant']) packs, problems = extractrules.load(know, get) assert packs == [] assert len(problems) == 1 assert 'no-such-plant' in problems[0] assert 'mc' in problems[0] # says what IS available def test_empty_and_duplicate_names_are_refused(): know, get = know_with(['mc', 'mc']) _, problems = extractrules.load(know, get) assert problems and 'twice' in problems[0] know, get = know_with(['']) _, problems = extractrules.load(know, get) assert problems and 'empty' in problems[0] def test_knowledge_default_is_empty(): from vibe import knowledge assert knowledge.DEFAULTS['extract']['rule_packs'] == [] # ------------------------------------------------- the load-bearing property def test_no_note_is_ever_dropped(): """The whole module in one assertion.""" know, get = know_with(['mc']) packs, _ = extractrules.load(know, get) given = entries(WPA1_NOTES) notes, unresolved = extractrules.apply_to(given, packs) assert len(notes) + len(unresolved) == len(given) assert (sorted(n['text'] for n in notes + unresolved) == sorted(WPA1_NOTES)) def test_every_placement_names_the_rule_that_made_it(): """An unattributable classification is one nobody can argue with.""" know, get = know_with(['mc']) packs, _ = extractrules.load(know, get) notes, unresolved = extractrules.apply_to(entries(WPA1_NOTES), packs) for entry in notes: assert entry['rule'].startswith('mc/') for entry in unresolved: if 'rule' in entry: assert entry['rule'].startswith('mc/') def test_a_placement_must_declare_a_known_kind_and_a_rule(): with pytest.raises(ValueError): extractrules.Placement('vanish', 'mc/x', 'because') with pytest.raises(ValueError): extractrules.Placement(extractrules.NOTE, '', 'because') # ------------------------------------------------------- the mc pack itself def test_the_seven_notes_land_where_they_should(): know, get = know_with(['mc']) packs, _ = extractrules.load(know, get) notes, unresolved = extractrules.apply_to(entries(WPA1_NOTES), packs) assert sorted(n['text'] for n in notes) == sorted([ '*USE MC STANDARD REINFORCEMENT FOR CUT DOWN PANELS AND AROUND ALL ' 'OPENINGS', 'USE FOR BACK OR EDGE PICK (UNO)', "#5 BAR X CONT ONLY NEEDED WHEN LAST PAIR OF STRANDS ARE MORE THAN " "1'-0 FROM EDGE OF WALL.", "STRIPPING & BLOCKING DETAIL < 25'", ]) assert sorted(u['text'] for u in unresolved) == sorted([ 'STRIPPING & BLOCKING DETAIL', 'STRAND PATTERN', 'STRAND', ]) def test_a_conditional_detail_title_is_a_note_and_the_bare_one_is_not(): """The asymmetry this pack exists to get right: the condition is the part that could change what gets built, so it is never called furniture.""" bare = mc.classify('STRIPPING & BLOCKING DETAIL') cond = mc.classify("STRIPPING & BLOCKING DETAIL < 25'") assert bare.kind == extractrules.FURNITURE assert cond.kind == extractrules.NOTE assert bare.rule != cond.rule def test_furniture_keeps_its_place_in_unresolved(): """`furniture` is not an ignore list -- the line is still written out, only the sentence beside it changes.""" know, get = know_with(['mc']) packs, _ = extractrules.load(know, get) given = entries(['STRAND PATTERN']) notes, unresolved = extractrules.apply_to(given, packs) assert notes == [] assert len(unresolved) == 1 assert unresolved[0]['text'] == 'STRAND PATTERN' assert unresolved[0]['reason'] != given[0]['reason'] # better sentence assert 'legend' in unresolved[0]['reason'] def test_the_pack_recognises_nothing_it_was_not_shown(): """A rule with no observation behind it does not belong in the pack, and a sentence from another producer's sheet must fall straight through.""" for text in ('SEE ARCHITECTURAL DRAWINGS', 'ALL DIMENSIONS ARE NOMINAL', 'USE 6X6 -D3.0/D3.0 W.W.F.', '', 'STRANDS'): assert mc.classify(text) is None def test_classify_is_case_and_padding_tolerant(): assert mc.classify(' strand pattern ') is not None assert mc.classify('Strand Pattern').kind == extractrules.FURNITURE