# -*- 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 `... @ 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