# -*- coding: utf-8 -*- """Rows 5.13, 5.14 and 5.15 once the hatch gather exists. WHAT CHANGED ON 2026-08-31. These three rows shipped with their arithmetic built and their inputs deliberately un-gathered: `ticketqc.read_ticket` set no `solid_zone_pattern`, `recess_patterns`, `recess_regions` or `legend_patterns`, so all three stood down naming the gather. That was the right call while nobody had run the reads. The reads have now been taken on the live model, id by id, and `ticketqc._read_insulation_hatch` is the path they exercised -- so the rows run. NO PLANT SPELLING IN THIS FILE. The real filled-region type names carry a plant initialism and live in the plant layer; the placeholders below have the same SHAPE and none of the identity, which is all the mechanism needs (CLAUDE.md ss3). """ import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent)) import conftest # noqa: F401 # THE INERT REVIT STUB, AND IT IS PUT BACK. `ticketqc` imports # `Autodesk.Revit.DB` at MODULE scope, so `conftest`'s package trick cannot # reach it -- `test_ticketchecks_host.py` explains the whole shape and this # is the same borrowing. It is removed again immediately, because # `sys.modules` is process-wide and `test_reinforceoffline.py` asserts that # no `Autodesk*` module is loaded. _ADDED = [] if 'Autodesk.Revit.DB' not in sys.modules: import types _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 ticketpage as TP # noqa: E402 from vibe import ticketchecks as TC # noqa: E402 finally: for _name in _ADDED: sys.modules.pop(_name, None) RECESS_A = 'RECESS 2IN NF' RECESS_B = 'RECESS 3IN NF' def _know(**over): """A plant armed the way the real one now is: a solid-zone family, a legend it reads its reference off, two recess types, and a plant-side copy of the legend for the piece whose legend could not be read.""" know = { 'drawings': {'insulation_layout': 'Elevation Top 1'}, 'insulation': { 'solid_zone': {'family': 'SOLID_ZONE_RECTANGULAR', 'family_verified': True, 'view_filter': 'SOLID ZONE'}, 'legend': { 'view_name': 'TICKET NOTE - INS. LEGEND', 'name_tokens': ['LEGEND'], 'swatch_types': {'SOLID ZONE SWATCH': 'solid_zone', RECESS_A: 'recess_2in_nf', RECESS_B: 'recess_3in_nf'}, 'expected_patterns': {'solid_zone': 'ANSI35', 'recess_2in_nf': 'TRIANG', 'recess_3in_nf': 'TUCKPOINT'}, }, 'recess': {'region_types': [RECESS_A, RECESS_B], 'modelled_path_verified': True, 'modelled_path_unverified_because': None}, }, } know.update(over) return know def _ticket(**over): t = {'mark': 'PNL101', 'sheets': [object()], 'solid_zones': [{'id': 1, 'member': True, 'lifter_id': 9}], 'lifters': [{'family': 'F'}], 'legend_patterns': {'solid_zone': 'ANSI35', 'recess_2in_nf': 'TRIANG', 'recess_3in_nf': 'TUCKPOINT'}, 'recess_regions': {}, 'recess_patterns': {RECESS_A: 'TRIANG', RECESS_B: 'TUCKPOINT'}, 'solid_zone_pattern': 'TRIANG', 'insulation_hatch_notes': []} t.update(over) return t # ----------------------------------------------------------------- 5.13 def test_513_runs_and_fails_on_the_measured_job(): """THE MEASURED ANSWER. The solid zones draw the pattern the view template's `SOLID ZONE` filter sets, and the legend's own swatch for a solid zone is a different pattern element entirely. The row FAILS, and both patterns and both provenances are in the sentence -- "the hatch is wrong" is not actionable, "it draws X and your own legend shows Y" is. """ t = _ticket() found = TC.check_solid_zone_hatch(t, {}, _know(), None) assert len(found) == 1 assert found[0].severity == 'FAIL' assert found[0].code == 'SOLID-ZONE-HATCH' assert 'TRIANG' in found[0].message and 'ANSI35' in found[0].message assert 'legend' in found[0].message assert t['_solid_zone_hatch_silence'] is None def test_513_passes_when_the_filter_sets_the_legends_own_pattern(): found = TC.check_solid_zone_hatch( _ticket(solid_zone_pattern='ANSI35'), {}, _know(), None) assert found[0].severity == 'OK' def test_513_stands_down_carrying_the_gathers_own_sentences(): """A row that stood down saying only "it was not gathered" would throw away the reason the gather recorded, and the reader would have to reproduce the read by hand to find out why.""" t = _ticket(solid_zone_pattern=None, insulation_hatch_notes=[ 'the insulation layout view is not placed on this ticket']) found = TC.check_solid_zone_hatch(t, {}, _know(), None) assert found == [] said = t['_solid_zone_hatch_silence'] assert 'what the gather recorded' in said assert 'not placed on this ticket' in said # ----------------------------------------------------------------- 5.14 def test_514_stands_down_and_refuses_to_imply_the_pieces_were_judged(): """ZERO PLACED, JOB-WIDE. The only recess regions that exist in this model are legend swatches, and the gather excludes legend views on purpose. So a zero here is a real zero -- and it is still not a pass: this reader has never seen a job that DOES draw a recess, so it cannot tell a piece that needs none from a piece whose recesses were forgotten, and the sentence says exactly that.""" t = _ticket() found = TC.check_recess_regions(t, {}, _know(), None) assert found == [] said = t['_recess_silence'] assert 'no recess region is drawn on any ticket view' in said assert 'legend views are excluded' in said assert 'cannot tell a piece that needs no recess' in said def test_514_no_longer_hedges_about_a_modelled_path_that_was_settled(): """The one candidate for a modelled recess was measured and ruled out, so `modelled_path_verified` is true and the caveat goes. A tool that kept hedging about a closed question is noise on a red row.""" t = _ticket() TC.check_recess_regions(t, {}, _know(), None) assert 'modelled-recess path was NOT consulted' not in t['_recess_silence'] know = _know() know['insulation']['recess']['modelled_path_verified'] = False know['insulation']['recess']['modelled_path_unverified_because'] = 'asked' t2 = _ticket() TC.check_recess_regions(t2, {}, know, None) assert 'NOT consulted' in t2['_recess_silence'] def test_514_becomes_a_real_comparison_the_day_a_design_side_arrives(): """No new plumbing: the row already compares when something states what this piece should have.""" t = _ticket(recess_regions={RECESS_A: 2}) found = TC.check_recess_regions(t, {'recesses': {RECESS_A: 2}}, _know(), None) assert found[0].severity == 'OK' found = TC.check_recess_regions( _ticket(recess_regions={}), {'recesses': {RECESS_A: 2}}, _know(), None) assert found[0].severity == 'FAIL' # ----------------------------------------------------------------- 5.15 def test_515_runs_clean_for_both_recess_types(): """The type-level patterns match their legend swatches. One finding for BOTH types, never one per type -- `checklist_states` keeps the last finding per code and a second RECESS-HATCH would silently replace the first.""" t = _ticket() found = TC.check_recess_hatch(t, {}, _know(), None) assert len(found) == 1 assert found[0].severity == 'OK' assert RECESS_A in found[0].message and RECESS_B in found[0].message # The size question is named and explicitly NOT decided here. assert 'not this row' in found[0].message def test_515_fails_on_the_type_that_drifted_and_still_reports_the_other(): t = _ticket(recess_patterns={RECESS_A: 'STARS', RECESS_B: 'TUCKPOINT'}) found = TC.check_recess_hatch(t, {}, _know(), None) assert found[0].severity == 'FAIL' assert 'STARS' in found[0].message assert 'TUCKPOINT' in found[0].message def test_515_stands_down_when_a_filter_could_override_the_type(): """THE CONDITION UNDER WHICH A TYPE-LEVEL READ IS AUTHORITATIVE, and it is checked on every run rather than recorded once. A filled region is a Detail Item; a filter selecting that category beats the type, and then the type says what the region WOULD have drawn with. Measured 2026-08-31: no filter on this template carries the category -- which is a fact about today's template, not a property of Revit.""" t = _ticket(recess_patterns=None, insulation_hatch_notes=[ 'filter(s) INSULATION select the Detail Items category on the ' 'insulation layout view, so a filled region\'s TYPE no longer says ' 'what it drew with -- the recess patterns were not read']) found = TC.check_recess_hatch(t, {}, _know(), None) assert found == [] said = t['_recess_hatch_silence'] assert 'Detail Items category' in said assert 'what the gather recorded' in said def test_the_sheets_own_legend_beats_the_plant_map_and_the_finding_says_so(): """"the legend says ANSI35" and "a YAML file written on 2026-08-31 says ANSI35" are different claims. The legend is the self-updating one.""" from_sheet = TC.check_solid_zone_hatch(_ticket(), {}, _know(), None)[0] assert "ticket's own legend" in from_sheet.message from_plant = TC.check_solid_zone_hatch( _ticket(legend_patterns={}), {}, _know(), None)[0] assert 'plant layer' in from_plant.message def test_a_gather_that_recorded_nothing_leaves_the_sentence_alone(): """`_hatch_gather_note` must add, never replace: the row keeps its own words when the gather has nothing to add.""" assert TC._hatch_gather_note({}, 'because X') == 'because X' assert TC._hatch_gather_note({'insulation_hatch_notes': []}, 'because X') == 'because X'