Files
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at 77a82eb.
Code, docs and the public catalogue only: no plant or job knowledge
layers, no client, job or producer names (tools/scrub.py clean against
the widened term list; a second word-level grep clean). The intro deck is
left out because its slide images cannot be checked for job drawings.
2026-09-09 08:35:15 -05:00

244 lines
11 KiB
Python

# -*- 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'