WillTools: precast shop-ticket QC and panel handling tools
ci / test (push) Has been cancelled

Public demo snapshot of an in-house pyRevit extension for precast concrete
shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel
handling solver (PCI Chapter 8).

- No client, job, plant or product data: knowledge layers ship as
  structural templates only.
- Ticket QC's model-read escalation targets a LAN model server endpoint
  (configurable), not a cloud provider.
- Built from the private development repo via tools/make_dist.py.
This commit is contained in:
2026-09-03 16:33:47 -05:00
commit 52d94303bc
281 changed files with 155656 additions and 0 deletions
@@ -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