# -*- coding: utf-8 -*- """`ticketchecks.REINFORCING_CHECKS` / `REINFORCING_EXCLUDED` / `run_reinforcing`. Same trick as `test_ticketchecks_host.py`, and the same reason: `ticketqc` imports `Autodesk.Revit.DB` at module scope and `ticketchecks` takes `Finding` from it, so neither imports on host CPython without an inert stub standing in for the Revit API. See that file's own docstring for why the stub is safe here -- nothing this file exercises touches Revit either. WHAT THIS FILE IS FOR, beyond the mechanical assertions: the two sets below (`EXPECTED_INCLUDE_REASONS` / `EXPECTED_EXCLUDE_REASONS`) are the check-by-check classification this task was asked to produce, written down so a change to either list in `ticketchecks.py` is a visible test failure here rather than a silent drift between the code and the report the owner was handed. Adjusting the classification is expected to mean editing BOTH `ticketchecks.py` and this file's tables together. RE-CUT 2026-09-07 against a real bridge run on three panels (WPA655, WPB530E, SMD516 -- see the module note above `ticketchecks.REINFORCING_EXCLUDED`). Two checks moved from INCLUDE to EXCLUDE (`check_cast_extras`, `check_weight_three_way`) because the run showed both to be, in practice, a ticket-vs-model comparison rather than a reinforcing-stage one. This file also gained coverage for the MEMBER-CLASS split added the same day (`ticketchecks.member_class_of` / `REINFORCING_APPLIES_TO` / `NOT_FOR_CLASS_DISPOSITION`). """ # pylint: disable=import-error,invalid-name,broad-except import sys import types import pytest def _stub_revit(): if 'Autodesk.Revit.DB' in sys.modules: return [] 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 return ['Autodesk.Revit.DB', 'Autodesk.Revit', 'Autodesk'] _ADDED = _stub_revit() try: from vibe import ticketchecks as TC # noqa: E402 finally: for _name in _ADDED: sys.modules.pop(_name, None) # --------------------------------------------------------------------- # THE CLASSIFICATION TABLE. Every one of the 41 functions in `TC.CHECKS`, # one bucket or the other, with the checklist row(s) it owns (from # `knowledge/catalogue.yaml`'s `checklist.master`, read 2026-09-05) noted in # a comment beside it for a human cross-checking this against the report. # INCLUDED: evidence is the panel's modelled content -- geometry, assembly # membership, nested sub-components, and the design intent (a spec or a # design-summary view, which describe the DESIGN, not a drawn ticket). EXPECTED_INCLUDE = frozenset([ 'check_connectors_exist', # 5.8 modelled connectors 'check_connectors_sufficient', # 5.9 modelled connector count/spacing 'check_connector_edge_distance', # 5.10 modelled connector position 'check_solid_zone_members', # 5.11 modelled assembly membership 'check_opening_diagonal_bars', # 5.16 modelled bar-type flags 'check_wwf_designation', # 5.4 modelled mesh family 'check_bar_stock_length', # 5.7 modelled bar length parameter 'check_lifter_capacity', # 6.1 modelled lifters vs weight 'check_lifter_edge_distance', # 6.2 modelled lifters vs geometry 'check_handling_result', # 6.3 geometry + design intent 'check_lifter_anchor_spacing', # 6.7 modelled lifter spacing 'check_lifting_points', # 6.9 (MC) modelled lifter count 'check_lift_positions', # 6.10 (MC) modelled lifter position 'check_lifter_accessory_in_assembly', # 6.13 modelled nested-child # assembly membership 'check_member_interference', # 7.1 modelled geometry clash 'check_release_ratio', # 3.3 design intent only 'check_spec_unresolved', # 8.1 design spec/summary, no ticket 'check_strand_layout', # 4.4 / 4.6 modelled strands vs intent 'check_strand_debond', # 4.7 modelled strands + geometry ]) # EXCLUDED: at least half of what the check compares is a ticket artefact -- # a sheet, a placed view's name or text, a printed call-out, a hatch pattern # rendered on a view, a BOM schedule, a bed-plan note. EXPECTED_EXCLUDE = frozenset([ 'check_solid_zone_on_ticket', # 5.12 ticket's insulation layout view 'check_solid_zone_hatch', # 5.13 legend swatch off ticket views 'check_recess_regions', # 5.14 drafted regions on ticket views 'check_recess_hatch', # 5.15 rendered pattern off ticket views 'check_dunnage_openings', # 6.4 TextNotes on the ticket sheet 'check_dunnage_stripping_match', # 6.11 (MC) same TextNote read as 6.4 'check_bom_strand_count', # 4.8 BOM row + section print (ticket) 'check_bom_members', # 5.1 BOM schedule rows (ticket) 'check_debond_callout', # 4.5 a call-out IS ticket content 'check_picking_match', # 6.8 PICKING text-note band (ticket) 'check_detail_callouts', # 5.1 BED PLAN callout text (ticket) 'check_ticket_set', # 1.3 ticket sheet count 'check_jacking_force', # 4.3 TICKET NOTE view text 'check_bed_plan_reverse', # 5.2 bed-plan view text (ticket) 'check_sheet_name', # 1.1 the sheet's Name (ticket) 'check_invisible', # 8.2 sheet title-block strings 'check_view_name_design', # 1.4 names of views placed on ticket 'check_note_views', # 6.5 note-view content on ticket 'check_callout_overlap', # 1.5 printed call-out bounding boxes 'check_bar_mark_uniqueness', # 5.6 marks printed on the ticket 'check_parity_note', # 6.6 a phrase on the ticket BED PLAN 'check_cast_extras', # 2.4 moved 2026-09-07: a real run # on SMD516 (has a ticket) shows # it reading the ticket's BOM/ # view text directly, not just # standing down without one 'check_weight_three_way', # 2.2 moved 2026-09-07: reported # WGT-TWO-WAY (never OK) on all # three real panels because the # printed leg is a ticket field # this plant never maps ]) def test_expected_tables_partition_all_checks(): """Sanity on the tables ABOVE, before comparing them to the module: no overlap, and every one of the 41 checks lands in exactly one bucket.""" overlap = EXPECTED_INCLUDE & EXPECTED_EXCLUDE assert not overlap, 'named in both buckets: {0}'.format(sorted(overlap)) assert len(EXPECTED_INCLUDE) + len(EXPECTED_EXCLUDE) == len(TC.CHECKS) def test_reinforcing_checks_is_subset_of_checks(): all_names = set(c.__name__ for c in TC.CHECKS) reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS) assert reinforcing_names <= all_names def test_reinforcing_checks_and_excluded_partition_checks(): """Every check is included XOR excluded -- never both, never neither.""" all_names = set(c.__name__ for c in TC.CHECKS) reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS) excluded_names = set(TC.REINFORCING_EXCLUDED) assert reinforcing_names & excluded_names == set() assert reinforcing_names | excluded_names == all_names def test_every_excluded_check_has_a_nonempty_reason(): for name, reason in TC.REINFORCING_EXCLUDED.items(): assert isinstance(reason, str) and reason.strip(), ( '{0} has no reason, or a blank one'.format(name)) def test_classification_matches_the_report(): """The actual module data equals the table above, name for name. A mismatch here means `ticketchecks.py` changed without this file being updated to match (or vice versa) -- the whole point of writing the classification down twice. """ reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS) excluded_names = set(TC.REINFORCING_EXCLUDED) assert reinforcing_names == EXPECTED_INCLUDE assert excluded_names == EXPECTED_EXCLUDE def test_run_reinforcing_returns_findings_log_and_excluded(): findings, log, excluded = TC.run_reinforcing({'mark': 'X'}, {}, {}, None) assert isinstance(findings, list) assert isinstance(log, list) assert excluded == dict(TC.REINFORCING_EXCLUDED) def test_run_reinforcing_only_logs_reinforcing_checks(): """The log never names a check `run_reinforcing` did not actually run (or explicitly stand down) -- no leakage from the excluded set.""" _findings, log, _excluded = TC.run_reinforcing({'mark': 'X'}, {}, {}, None) logged_names = set(name for (name, _state, _note) in log) reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS) assert logged_names <= reinforcing_names def test_run_reinforcing_never_calls_check_ticket_only_row_probes(): """`run_reinforcing` deliberately does not append `row_silences` -- those rows are all `check_ticket`'s own ticket-vs-design territory, which this run never touches. No `row:` entry should appear.""" _findings, log, _excluded = TC.run_reinforcing({'mark': 'X'}, {}, {}, None) assert not any(name.startswith(TC.ROW_NOTE_PREFIX) for (name, _state, _note) in log) # --------------------------------------------------------------------- # MEMBER CLASS, added 2026-09-07 (owner's addendum to the same task): a # wall panel and a flexural member answer a different checklist, and # blurring the two is a real defect the real run showed, not a theoretical # one -- `check_lifting_points`'s 20/25 ft point-count ceiling reported a # FAIL against SMD516 (flexural) that a flexural member was never subject # to. def test_member_class_of_reads_the_mark_prefix(): assert TC.member_class_of({'mark': 'WPA655'}) == ( 'wall', "mark 'WPA655' starts with 'WPA'") assert TC.member_class_of({'mark': 'WPB530E'}) == ( 'wall', "mark 'WPB530E' starts with 'WPB'") assert TC.member_class_of({'mark': 'SMD516'}) == ( 'flexural', "mark 'SMD516' starts with 'SMD'") assert TC.member_class_of({'mark': 'FSA501'}) == ( 'flexural', "mark 'FSA501' starts with 'FSA'") cls, _basis = TC.member_class_of({'mark': 'CLA509'}) assert cls == 'column_beam' def test_member_class_of_falls_back_to_geometry_then_gives_up(): # No recognised prefix, no plant mapping, but a long/narrow shape. cls, basis = TC.member_class_of({'mark': 'ZZZ1', 'length': 40.0, 'width': 8.0}) assert cls == 'flexural' assert 'geometry' in basis # No recognised prefix, and no geometry either -- never a guess. cls, basis = TC.member_class_of({'mark': 'ZZZ1'}) assert cls is None assert basis def test_member_class_of_prefers_mark_over_geometry(): # A wall-prefixed mark is trusted even if the geometry alone would have # read as flexural -- the mark is the cheaper, more specific signal. cls, basis = TC.member_class_of({'mark': 'WPA1', 'length': 40.0, 'width': 8.0}) assert cls == 'wall' assert 'mark' in basis def test_reinforcing_applies_to_restricts_only_named_checks(): # A named check is restricted... assert not TC._reinforcing_applies_to_class( 'check_lifting_points', 'flexural') assert TC._reinforcing_applies_to_class('check_lifting_points', 'wall') # ...and a check named nowhere in the table applies to every class. assert TC._reinforcing_applies_to_class('check_strand_layout', 'wall') assert TC._reinforcing_applies_to_class('check_strand_layout', 'flexural') def test_reinforcing_applies_to_never_narrows_an_unclassified_piece(): # member_class is None (could not judge) -- every check still runs. assert TC._reinforcing_applies_to_class('check_lifting_points', None) def test_reinforcing_applies_to_names_only_real_checks(): reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS) for name in TC.REINFORCING_APPLIES_TO: assert name in reinforcing_names, ( '{0} is in REINFORCING_APPLIES_TO but not in ' 'REINFORCING_CHECKS'.format(name)) def test_run_reinforcing_stands_down_a_wall_only_check_on_a_flexural_mark(): _findings, log, _excluded = TC.run_reinforcing( {'mark': 'SMD516'}, {}, {}, None) named = dict((name, (state, note)) for (name, state, note) in log) state, note = named['check_lifting_points'] assert state == TC.NOT_FOR_CLASS_DISPOSITION assert 'flexural' in note def test_run_reinforcing_runs_a_wall_only_check_on_a_wall_mark(): _findings, log, _excluded = TC.run_reinforcing( {'mark': 'WPA655'}, {}, {}, None) named = dict((name, state) for (name, state, _note) in log) assert named['check_lifting_points'] != TC.NOT_FOR_CLASS_DISPOSITION def test_flexural_checks_not_built_is_name_evidence_rule_triples(): assert len(TC.FLEXURAL_CHECKS_NOT_BUILT) >= 5 for row in TC.FLEXURAL_CHECKS_NOT_BUILT: assert len(row) == 3 for cell in row: assert isinstance(cell, str) and cell.strip() # --- what a flexural piece is ALREADY asked (issue #25, 2026-09-07) ------- # # The first draft of FLEXURAL_CHECKS_NOT_BUILT listed "pick points near # 0.2L" and "eccentric strand pattern vs design" as unbuilt. Both are asked # today by checks REINFORCING_APPLIES_TO never narrowed: 6.10 compares the # lifter stations to PCI with no point-count ceiling, and 4.4/4.6 compare # strand across-positions and row elevations to the design. These tests # pin that, so the table cannot drift back into calling a running check a # gap, and so nobody narrows either check to walls without this failing. def test_flexural_already_asked_names_real_checks_that_apply_to_flexural(): reinforcing_names = set(c.__name__ for c in TC.REINFORCING_CHECKS) assert TC.FLEXURAL_CHECKS_ALREADY_ASKED for concern, check, row_id in TC.FLEXURAL_CHECKS_ALREADY_ASKED: assert concern.strip() and row_id.strip() assert check in reinforcing_names, check assert TC._reinforcing_applies_to_class(check, 'flexural'), ( '{0} is listed as already asked on a flexural piece but ' 'REINFORCING_APPLIES_TO narrows it away'.format(check)) assert check in TC._SKIP_PROBES def test_pick_position_and_strand_pattern_are_asked_not_gaps(): asked = dict((check, row_id) for (_c, check, row_id) in TC.FLEXURAL_CHECKS_ALREADY_ASKED) assert asked['check_lift_positions'] == '6.10' assert asked['check_strand_layout'] == '4.4 / 4.6' names = [row[0] for row in TC.FLEXURAL_CHECKS_NOT_BUILT] # Tombstones: a "6.12" would be 6.10 twice, and the strand row now # names only the half the design extract cannot carry. assert not [n for n in names if n.startswith('pick point') or '0.2L' in n] assert not [n for n in names if 'eccentric' in n] assert [n for n in names if 'harp' in n and 'debond' in n] def test_lift_positions_has_no_point_count_ceiling(): # 6.9's 20/25 ft ceiling must never leak into 6.10: a flexural piece # loses 6.9 by class and keeps 6.10 precisely because 6.10 judges WHERE, # never HOW MANY. Inspect the code object rather than run the Revit- # shaped gather. names = TC.check_lift_positions.__code__.co_names consts = [c for c in TC.check_lift_positions.__code__.co_consts if isinstance(c, str)] assert 'two_point_max_length_ft' not in ' '.join(consts) assert 'two_point_absolute_max_ft' not in ' '.join(consts) assert 'pci_position_tolerance_in' in ' '.join(consts) assert '_pci_recommended_stations' in names