WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
# -*- 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
|
||||
Reference in New Issue
Block a user