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,366 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""The design-summary vocabulary -- rows 8.1 and 5.9.
|
||||
|
||||
WHAT THIS FILE IS PINNING. Row 8.1 reported SEVEN uninterpreted lines on a
|
||||
real insulated piece and row 5.9 stood down on every press because one of
|
||||
those seven WAS the requirement it needed. The grammar in
|
||||
`ticketpage.parse_summary_line` closes both, and every production in it was
|
||||
drawn from a corpus rather than from one line:
|
||||
|
||||
(a) the live design-summary lines the evidence files recorded as
|
||||
unresolved -- 9 distinct, 7 of them on the piece the owner read;
|
||||
(b) 54 distinct `... @ <spacing> O.C. ...` call-outs over 75 occurrences
|
||||
in the plant standards manual extractions.
|
||||
|
||||
Both counted 2026-08-31. The lines below are quoted for GRAMMAR only: a bar
|
||||
size, a product code and a pitch are naming conventions, not identities
|
||||
(CLAUDE.md ss3).
|
||||
|
||||
THE TWO INVARIANTS THIS FILE EXISTS TO KEEP, and they are the ones a future
|
||||
"tidy up" will attack first:
|
||||
|
||||
* a line the grammar cannot place STAYS an 8.1 finding, verbatim;
|
||||
* a line it places but cannot VALUE (`@ "S" O.C.`, `@ __ O.C.`) is not a
|
||||
line it read -- it carries `spacing_in: None` and a reason, and nothing
|
||||
may compare a piece against it.
|
||||
"""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
import conftest # noqa: F401 -- installs the bare `vibe` package stub
|
||||
|
||||
from vibe import ticketpage as TP # noqa: E402
|
||||
from vibe import knowledge as K # noqa: E402
|
||||
|
||||
|
||||
# The seven lines of one real design summary, as the evidence file recorded
|
||||
# them. This is the golden the whole exercise is measured against.
|
||||
LIVE_SUMMARY = [
|
||||
u'STRAND PATTERN',
|
||||
u'CL STRANDS',
|
||||
u'#3S @ 18" O.C. +(2)@3" EA. END',
|
||||
u'CL #7 BARS NEEDED WHEN THERE ARE OPENINGS',
|
||||
u'CL STRANDS & #7 BARS',
|
||||
u"HK ST100'S @1'-4\" O.C.",
|
||||
u'CL #3 BARS@ TOP WYTHE',
|
||||
]
|
||||
|
||||
|
||||
def test_the_seven_lines_the_owner_read_are_all_placed():
|
||||
"""8.1 went from seven uninterpreted lines to none, and it did it by
|
||||
understanding them -- not by dropping any."""
|
||||
placed, unplaced = TP.classify_summary_lines(LIVE_SUMMARY)
|
||||
assert unplaced == []
|
||||
assert len(placed) == 7
|
||||
kinds = sorted([p['kind'] for p in placed])
|
||||
assert kinds == ['conditional', 'label', 'placement', 'placement',
|
||||
'placement', 'spacing', 'spacing']
|
||||
|
||||
|
||||
def test_the_inventory_names_what_each_line_was_taken_to_be():
|
||||
"""A row that went from seven red lines to green with no sentence under
|
||||
it would have swapped one silence for a quieter one."""
|
||||
placed, _ = TP.classify_summary_lines(LIVE_SUMMARY)
|
||||
said = TP.summary_requirement_inventory(placed)
|
||||
assert '2 spacing requirements' in said
|
||||
assert '3 placement statements' in said
|
||||
assert '1 conditional' in said
|
||||
assert '1 group label' in said
|
||||
|
||||
|
||||
# ----------------------------------------------------- the spacing production
|
||||
|
||||
def test_the_field_spacing_is_the_first_at_and_the_rest_is_a_qualifier():
|
||||
"""`#3S @ 18" O.C. +(2)@3" EA. END` states ONE field spacing and then an
|
||||
end densification. A reader that took the LAST `@` would report the end
|
||||
pair as the requirement and fail every piece that is correct."""
|
||||
req = TP.parse_summary_line(u'#3S @ 18" O.C. +(2)@3" EA. END')
|
||||
assert req['kind'] == 'spacing'
|
||||
assert req['spacing_in'] == 18.0
|
||||
assert req['product'] == '#3S'
|
||||
assert req['qualifiers'] == [u'+(2)@3" EA. END']
|
||||
|
||||
|
||||
def test_feet_and_inches_and_bare_inches_and_fractions_all_read():
|
||||
"""22 of the 54 manual call-outs write feet-and-inches, 18 write bare
|
||||
inches, and several of those carry a vulgar fraction glyph -- which is
|
||||
the one that silently turns 1 1/2 into 1 if nobody spells it out."""
|
||||
cases = [(u"HK ST100'S @1'-4\" O.C.", 16.0),
|
||||
(u'#3 E.L. @ 4\'-0" O.C.', 48.0),
|
||||
(u'#4 REBAR @ 48" O.C.', 48.0),
|
||||
(u'(2) #4 E.L. @ 1½" O.C.', 1.5),
|
||||
(u'(2) #4 E.L. @ 1 1/2" O.C.', 1.5),
|
||||
(u'30 SPACES @ 7½" O.C.', 7.5)]
|
||||
for text, inches in cases:
|
||||
req = TP.parse_summary_line(text)
|
||||
assert req is not None and req['kind'] == 'spacing', text
|
||||
assert req['spacing_in'] == inches, text
|
||||
|
||||
|
||||
def test_a_symbolic_spacing_is_recognised_and_never_valued():
|
||||
"""`@ "S" O.C.` is a COLUMN OF A TABLE on the sheet -- 7 occurrences over
|
||||
5 distinct call-outs in the corpus. A reader that scanned for the first
|
||||
digits would answer it from the plate size to the left of the `@`."""
|
||||
req = TP.parse_summary_line(u'PLATE 3/8"x4" x 0\'-4" (P410) @ "S" O.C.')
|
||||
assert req['kind'] == 'spacing'
|
||||
assert req['spacing_in'] is None
|
||||
assert 'table' in req['spacing_unread_because']
|
||||
|
||||
|
||||
def test_a_blank_spacing_is_recognised_and_never_valued():
|
||||
"""The sheet carries a literal `__`. The design has stated the SHAPE of
|
||||
the requirement and not its value, and those are different."""
|
||||
req = TP.parse_summary_line(u'#4 REBAR U-BEND @ __ O.C.')
|
||||
assert req['kind'] == 'spacing'
|
||||
assert req['spacing_in'] is None
|
||||
assert 'blank' in req['spacing_unread_because']
|
||||
|
||||
|
||||
def test_an_unvalued_spacing_is_called_out_in_the_inventory():
|
||||
"""It must not hide inside a count of lines the tool "read"."""
|
||||
placed, _ = TP.classify_summary_lines(
|
||||
[u'PLATE (P410) @ "S" O.C.', u'#4 @ 12" O.C.'])
|
||||
said = TP.summary_requirement_inventory(placed)
|
||||
assert 'carry no numeric spacing' in said
|
||||
assert 'P410' in said
|
||||
|
||||
|
||||
# --------------------------------------------- placement, conditional, label
|
||||
|
||||
def test_cl_is_a_placement_and_the_wythe_is_its_zone():
|
||||
"""`CL #3 BARS@ TOP WYTHE` is a bar size, a placement (centred) and a
|
||||
wythe -- QNA/proposed-checks-2026-08-31.md P12. The `@` here carries the
|
||||
zone and not a spacing, which is why the spacing production must not
|
||||
match a line with no `O.C.` on it."""
|
||||
req = TP.parse_summary_line(u'CL #3 BARS@ TOP WYTHE')
|
||||
assert req['kind'] == 'placement'
|
||||
assert req['placement'] == 'centred'
|
||||
assert req['items'] == ['#3 BARS']
|
||||
assert req['zone'] == 'TOP WYTHE'
|
||||
|
||||
|
||||
def test_a_placement_naming_two_things_keeps_both():
|
||||
req = TP.parse_summary_line(u'CL STRANDS & #7 BARS')
|
||||
assert req['items'] == ['STRANDS', '#7 BARS']
|
||||
assert req['zone'] is None
|
||||
|
||||
|
||||
def test_a_conditional_wraps_the_requirement_it_qualifies():
|
||||
"""The tool STATES this and does not evaluate it. What it must not do is
|
||||
lose the requirement inside the condition."""
|
||||
req = TP.parse_summary_line(u'CL #7 BARS NEEDED WHEN THERE ARE OPENINGS')
|
||||
assert req['kind'] == 'conditional'
|
||||
assert req['condition'] == 'THERE ARE OPENINGS'
|
||||
assert req['requirement']['kind'] == 'placement'
|
||||
assert req['requirement']['items'] == ['#7 BARS']
|
||||
|
||||
|
||||
def test_the_conditionals_are_printed_and_marked_as_unevaluated():
|
||||
placed, _ = TP.classify_summary_lines(LIVE_SUMMARY)
|
||||
said = TP.summary_requirement_inventory(placed)
|
||||
assert 'does not evaluate' in said
|
||||
assert 'NEEDED WHEN THERE ARE OPENINGS' in said
|
||||
|
||||
|
||||
def test_a_deferral_is_not_a_label_and_stays_unplaced():
|
||||
"""`REINFORCING PER DESIGN` is three capitalised words with no digits --
|
||||
exactly the shape of a heading -- and it is a DEFERRAL. Filing it as a
|
||||
label would be the tool claiming to have understood a line that says the
|
||||
answer is somewhere else."""
|
||||
assert TP.parse_summary_line(u'REINFORCING PER DESIGN') is None
|
||||
placed, unplaced = TP.classify_summary_lines(
|
||||
[u'STRAND PATTERN', u'REINFORCING PER DESIGN'])
|
||||
assert [p['kind'] for p in placed] == ['label']
|
||||
assert unplaced == [u'REINFORCING PER DESIGN']
|
||||
|
||||
|
||||
def test_an_unplaceable_line_comes_back_verbatim_and_in_its_own_shape():
|
||||
"""8.1 prints it and a reader has to be able to find it on the sheet, so
|
||||
a `{text, reason}` entry must come back as that entry and not as a
|
||||
string this function invented."""
|
||||
entry = {'text': u'SOMETHING NOBODY HAS TAUGHT THIS 42', 'reason': u'why'}
|
||||
placed, unplaced = TP.classify_summary_lines([entry])
|
||||
assert placed == []
|
||||
assert unplaced == [entry]
|
||||
assert unplaced[0] is entry
|
||||
|
||||
|
||||
# ----------------------------------------------- 5.9: the design side, joined
|
||||
|
||||
def _plant():
|
||||
"""A plant that declares connectors the way the real one does: part
|
||||
numbers of its own, and an alias onto a published product."""
|
||||
return {
|
||||
'connectors': {
|
||||
'marks': {'group_1': ['R80', 'R81', 'R82'],
|
||||
'group_2': ['R87']},
|
||||
'catalogue_alias': {'R80': 'HK Composites (ST50)',
|
||||
'R82': 'HK Composites (ST100)',
|
||||
'R81': None, 'R87': None},
|
||||
},
|
||||
'tolerances': {'connector_spacing_in': 0.5},
|
||||
}
|
||||
|
||||
|
||||
def test_the_summary_line_joins_to_the_plants_connector_through_its_code():
|
||||
"""The whole point of the exercise: the design says `HK ST100'S`, the
|
||||
plant says `R82 = HK Composites (ST100)`, and ST100 is what joins them.
|
||||
Nothing in the code knows what an ST100 is."""
|
||||
placed, _ = TP.classify_summary_lines(LIVE_SUMMARY)
|
||||
req, why = K.connector_spacing_requirement(_plant(), placed)
|
||||
assert req['spacing_in'] == 16.0
|
||||
assert 'ST100' in why and 'R82' in why
|
||||
|
||||
|
||||
def test_the_join_is_exact_so_one_product_never_answers_for_another():
|
||||
"""`ST50` and `ST100` are both 'HK Composites' and they are not the same
|
||||
part. A substring join would let either answer for the other, which is
|
||||
the `connector_product` lesson one join along."""
|
||||
placed, _ = TP.classify_summary_lines([u"HK ST50'S @2'-0\" O.C."])
|
||||
req, why = K.connector_spacing_requirement(_plant(), placed)
|
||||
assert req['spacing_in'] == 24.0
|
||||
assert 'ST50' in why and 'R80' in why
|
||||
assert 'R82' not in why
|
||||
|
||||
|
||||
def test_a_spacing_that_is_not_about_a_connector_is_refused_by_name():
|
||||
"""A bar's pitch read as a connector's would be a confident wrong
|
||||
answer, and 5.9 FAILS on a shortfall."""
|
||||
placed, _ = TP.classify_summary_lines([u'#3S @ 18" O.C.'])
|
||||
req, why = K.connector_spacing_requirement(_plant(), placed)
|
||||
assert req is None
|
||||
assert 'none of them names a part number' in why
|
||||
assert '#3S @ 18" O.C.' in why
|
||||
|
||||
|
||||
def test_a_connector_line_with_no_number_stands_the_row_down_saying_so():
|
||||
"""The one case a reader must not mistake for agreement: the
|
||||
requirement is real, it is about a connector, and it has no value."""
|
||||
placed, _ = TP.classify_summary_lines([u'HK ST100 PINS @ "S" O.C.'])
|
||||
req, why = K.connector_spacing_requirement(_plant(), placed)
|
||||
assert req is None
|
||||
assert 'ST100' in why
|
||||
assert 'no number to hold this piece to' in why
|
||||
|
||||
|
||||
def test_a_design_that_states_nothing_says_that_and_not_something_else():
|
||||
req, why = K.connector_spacing_requirement(_plant(), [])
|
||||
assert req is None
|
||||
assert 'no line this tool could read as a requirement' in why
|
||||
|
||||
|
||||
def test_a_plant_that_declares_no_connector_cannot_recognise_one():
|
||||
placed, _ = TP.classify_summary_lines(LIVE_SUMMARY)
|
||||
req, why = K.connector_spacing_requirement({}, placed)
|
||||
assert req is None
|
||||
assert 'no layer declares a connector part number' in why
|
||||
|
||||
|
||||
def test_the_tolerance_is_a_key_with_its_provenance_in_the_sentence():
|
||||
"""It was a `+ 0.5` literal inside the comparison. A tolerance that
|
||||
cannot be named cannot be argued with, and one that cannot be resolved
|
||||
must stop the comparison rather than fall back on a constant."""
|
||||
value, why = K.connector_spacing_tolerance(_plant())
|
||||
assert value == 0.5
|
||||
assert 'tolerances.connector_spacing_in' in why
|
||||
value, why = K.connector_spacing_tolerance({'tolerances': {}})
|
||||
assert value is None
|
||||
assert 'tolerances.connector_spacing_in is unset' in why
|
||||
|
||||
|
||||
def test_the_measured_piece_passes_end_to_end():
|
||||
"""THE CALIBRATION, and it is the reason the comparison is against the
|
||||
WIDEST gap. Measured 2026-08-31 on one insulated piece: 16.0 in across
|
||||
the field -- which IS the summary's 1'-4" -- with 9.5 and 6.5 in pairs
|
||||
at the ends. End densification is normal and must never be flagged."""
|
||||
placed, _ = TP.classify_summary_lines(LIVE_SUMMARY)
|
||||
know = _plant()
|
||||
req, why = K.connector_spacing_requirement(know, placed)
|
||||
tolerance, tolerance_why = K.connector_spacing_tolerance(know)
|
||||
severity, message = TP.connectors_sufficient_status(
|
||||
115, None, spacing_in=[9.5, 16.0, 16.0, 16.0, 6.5],
|
||||
required_spacing_in=req['spacing_in'], requirement_note=why,
|
||||
tolerance_in=tolerance, tolerance_why=tolerance_why)
|
||||
assert severity == 'OK'
|
||||
assert '16.0' in message
|
||||
assert 'ST100' in message
|
||||
assert 'tolerances.connector_spacing_in' in message
|
||||
|
||||
|
||||
# ------------------------------------------------- 8.1's two clean answers
|
||||
|
||||
def _spec(lines):
|
||||
return {'unresolved': [{'text': t, 'reason': 'r'} for t in lines]}
|
||||
|
||||
|
||||
def _checks():
|
||||
"""`ticketchecks` needs the inert Revit stub; borrow it and put it back.
|
||||
|
||||
`sys.modules` is process-wide and `test_reinforceoffline.py` asserts that
|
||||
no `Autodesk*` module is loaded, so leaving it behind would make this
|
||||
file tell that test a lie.
|
||||
"""
|
||||
import types
|
||||
added = []
|
||||
if 'Autodesk.Revit.DB' not in sys.modules:
|
||||
autodesk = types.ModuleType('Autodesk')
|
||||
revit = types.ModuleType('Autodesk.Revit')
|
||||
db = types.ModuleType('Autodesk.Revit.DB')
|
||||
for name in ('FilteredElementCollector', 'AssemblyInstance', 'View',
|
||||
'ViewSheet', 'ViewType', 'BuiltInCategory', 'Element',
|
||||
'StorageType', 'ElementId', 'XYZ', 'Transform',
|
||||
'Options', 'Solid', 'BoundingBoxXYZ'):
|
||||
setattr(db, name, type(str(name), (object,), {}))
|
||||
autodesk.Revit = revit
|
||||
revit.DB = db
|
||||
sys.modules['Autodesk'] = autodesk
|
||||
sys.modules['Autodesk.Revit'] = revit
|
||||
sys.modules['Autodesk.Revit.DB'] = db
|
||||
added = ['Autodesk.Revit.DB', 'Autodesk.Revit', 'Autodesk']
|
||||
try:
|
||||
from vibe import ticketchecks
|
||||
return ticketchecks
|
||||
finally:
|
||||
for name in added:
|
||||
sys.modules.pop(name, None)
|
||||
|
||||
|
||||
def test_81_tells_its_two_clean_answers_apart():
|
||||
""""the extractor placed every line" and "the extractor could not place
|
||||
seven and this vocabulary recognised all seven" are different claims,
|
||||
and the second is a reading. The reader is entitled to know which one he
|
||||
is looking at."""
|
||||
TC = _checks()
|
||||
strong = TC.check_spec_unresolved({}, {}, {}, _spec([]))[0]
|
||||
assert strong.severity == 'OK'
|
||||
assert 'was interpreted' in strong.message
|
||||
weaker = TC.check_spec_unresolved({}, {}, {}, _spec(LIVE_SUMMARY))[0]
|
||||
assert weaker.severity == 'OK'
|
||||
assert 'the summary vocabulary categorised those' in weaker.message
|
||||
assert '7 line(s) categorised' in weaker.message
|
||||
|
||||
|
||||
def test_81_still_warns_and_still_enumerates_what_it_could_not_place():
|
||||
"""The row's job is unparsed lines and it must never learn to shrug."""
|
||||
TC = _checks()
|
||||
found = TC.check_spec_unresolved(
|
||||
{}, {}, {}, _spec([u'STRAND PATTERN', u'REINFORCING PER DESIGN',
|
||||
u'SOMETHING ELSE ENTIRELY 42']))[0]
|
||||
assert found.severity == 'WARN'
|
||||
assert found.message.startswith('2 line(s)')
|
||||
assert 'REINFORCING PER DESIGN' in found.message
|
||||
assert 'SOMETHING ELSE ENTIRELY 42' in found.message
|
||||
# and what it DID place is still visible on the same row
|
||||
assert 'group label' in found.message
|
||||
|
||||
|
||||
def test_81_leaves_the_specs_verbatim_record_alone():
|
||||
"""`spec['unresolved']` is the EXTRACTOR's account of what its own rules
|
||||
could not place. A later reader's opinion written back over it would
|
||||
stop it being evidence."""
|
||||
TC = _checks()
|
||||
spec = _spec(LIVE_SUMMARY)
|
||||
before = [dict(entry) for entry in spec['unresolved']]
|
||||
TC.check_spec_unresolved({}, {}, {}, spec)
|
||||
assert spec['unresolved'] == before
|
||||
Reference in New Issue
Block a user