8e6ee31596
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.
244 lines
11 KiB
Python
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'
|