Files
willtools-demo/WillTools.extension/tests/test_ticketchecks_host.py
T
will 8e6ee31596
ci / test (push) Successful in 6m18s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
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.
2026-09-09 08:32:15 -05:00

4567 lines
196 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""The `ticketchecks` rulings that are pure decision, tested off-model.
WHY THIS FILE NEEDS A TRICK AND `test_ticketpage.py` DOES NOT
--------------------------------------------------------------
`vibe.ticketqc` imports eight names from `Autodesk.Revit.DB` AT MODULE SCOPE,
and `vibe.ticketchecks` imports `Finding` from `ticketqc`, so neither can be
imported on host CPython the way `ticketpage` can. CLAUDE.md names them as
the standing example of code the test conftest cannot reach.
It cannot reach them *by itself*. Eight names is a small enough surface to
stand up as a stub, and the two things tested below never touch Revit at all:
* checklist 4.7's disposition is a decision over a dict
(`_debond_requirement`'s result), and that dict is CACHED ON THE TICKET
-- so a test can put the answer there and the geometry is never asked
for. That cache is not a hook added for testing; it exists because four
callers were reading the panel solid four times.
* checklist 8.1 reads a spec dict and formats a sentence.
* checklist 7.1's PAIR LIST is text and a file path. The geometry was
solved long before `clash_csv_rows` sees a hit -- it gets the same
dicts `member_boxes` built, and reads three keys off them.
The stub is deliberately inert: every name is a bare class that does
nothing. If a future edit makes one of these functions actually call the
Revit API, the test does not quietly pass -- it raises, which is the correct
outcome, because the function would no longer be the pure decision this file
claims it is.
"""
# pylint: disable=import-error,invalid-name,broad-except
import contextlib
import sys
import types
import pytest
def _stub_revit():
"""`Autodesk.Revit.DB`, inert, for a host interpreter.
Returns the names it put into `sys.modules`, so they can be taken back
out again -- see below, and read that comment before reusing this.
"""
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']
# THE STUB IS PUT BACK. `sys.modules` is process-wide and pytest imports
# every test module before it runs any of them, so a stub left behind here
# is a stub present during every other test in the suite --
# `test_reinforceoffline.py` proves the offline path never pulls in Revit by
# asserting that no `Autodesk*` module is loaded, and it would have failed on
# a lie this file told. The imported modules keep their own references to the
# stub classes, so removing the entries costs them nothing.
_ADDED = _stub_revit()
try:
from vibe import ticketchecks as TC # noqa: E402
from vibe import ticketpage as TP # noqa: E402
# `ticketchecks` has already pulled this in (it takes `Finding` from it),
# so this binds a name and imports nothing new. 8.1's other source is
# here: `parse_design_summary` is regexes over text notes, and the notes
# are the only thing it needs a document for -- see `_summary_view`.
from vibe import ticketqc as QC # noqa: E402
finally:
for _name in _ADDED:
sys.modules.pop(_name, None)
def _know():
from vibe import knowledge
return knowledge._deep_copy(knowledge.DEFAULTS)
def _ticket(requirement):
"""A ticket whose 4.7 answer is already solved.
`_debond_requirement` returns its cache when one is present, so nothing
below reaches for a panel, a frame or a solid.
"""
return {'mark': 'TEST-1', '_debond_requirement': requirement}
REASON = 'panel geometry for the debond check is not solvable'
# ================================================================== 4.7
#
# Owner's report of a WP133 run, 2026-08-30: 4.7 read NOT RUN with the
# reason "piece has no through-thickness opening -- no strand run needs
# debonding". That sentence is a JUDGEMENT, not an inability: the geometry
# was solved (`_debond_requirement`'s `reason` was None) and it found zero
# through-thickness openings. A piece that cannot interrupt a strand run
# does not need debonding, and saying so is a pass.
#
# NOT RUN has to keep meaning exactly one thing -- "could not judge" -- or
# the red it is printed in stops meaning anything. Rendering an answered
# question as NOT RUN breaks that rule from the other side, and it is worth
# a fix precisely because the rule is the product.
def test_47_no_opening_is_a_pass_not_a_silence():
findings = TC.check_strand_debond(
_ticket({'runs': [], 'reason': None, 'has_openings': False,
'threshold_ft': 4.0}), {}, _know(), None)
assert len(findings) == 1
assert findings[0].severity == 'OK'
assert findings[0].code == 'STRAND-DEBOND'
assert 'no opening interrupts any strand run' in findings[0].message
def test_47_no_opening_declares_nothing_to_the_skip_probe():
"""The probe is what turns a silence into NOT RUN. It must stay quiet."""
probe = TC._SKIP_PROBES['check_strand_debond']
assert probe(_ticket({'runs': [], 'reason': None, 'has_openings': False,
'threshold_ft': 4.0}), {}, _know(), None) is None
def test_47_unreadable_geometry_still_reports_not_run():
"""The distinction that matters, and the half that must NOT move.
Judged-and-clear is now OK. Could-not-look is still NOT RUN, with the
sentence that says why.
"""
t = _ticket({'runs': None, 'reason': REASON, 'has_openings': False,
'threshold_ft': 4.0})
assert TC.check_strand_debond(t, {}, _know(), None) == []
probe = TC._SKIP_PROBES['check_strand_debond']
assert probe(t, {}, _know(), None) == REASON
def test_47_openings_that_leave_enough_concrete_still_pass_their_own_way():
"""A piece WITH openings that all develop is a different OK sentence.
Two passes that mean different things must read differently, or the
reader learns nothing from either.
"""
findings = TC.check_strand_debond(
_ticket({'runs': [], 'reason': None, 'has_openings': True,
'threshold_ft': 4.0}), {}, _know(), None)
assert findings[0].severity == 'OK'
assert 'still develops' in findings[0].message
assert 'no opening interrupts' not in findings[0].message
def test_47_a_short_run_still_warns():
findings = TC.check_strand_debond(
_ticket({'reason': None, 'has_openings': True, 'threshold_ft': 4.0,
'runs': [{'length_ft': 2.5, 'x0': 10.0, 'x1': 12.5,
'strands_in': [3.0, 9.0]}]}),
{}, _know(), None)
assert findings[0].severity == 'WARN'
assert 'must be debonded' in findings[0].message
def test_47_reaches_the_checklist_as_ok():
"""End to end through the table the reader actually sees."""
findings = TC.check_strand_debond(
_ticket({'runs': [], 'reason': None, 'has_openings': False,
'threshold_ft': 4.0}), {}, _know(), None)
rows = dict((r[0], r) for r in TP.checklist_states(
findings, [('check_strand_debond', 'RAN', None)]))
assert rows['4.7'][3] == 'OK'
assert TP.state_color(rows['4.7'][3]) == 'green'
def test_47_reaches_the_checklist_as_red_when_it_could_not_look():
rows = dict((r[0], r) for r in TP.checklist_states(
[], [('check_strand_debond', 'SKIPPED', REASON)]))
assert rows['4.7'][3] == 'NOT RUN'
assert rows['4.7'][4] == REASON
assert TP.state_color(rows['4.7'][3]) == 'red'
def test_45_still_stands_down_only_on_a_real_failure_to_judge():
"""4.5 consumes 4.7. Flipping 4.7 must not have moved 4.5.
4.5 stands down when 4.7 COULD NOT judge. "No openings" was never that,
and 4.5's probe reads `reason` -- which is why the flip was safe.
"""
know = _know()
know['strand']['debond_callout_tokens'] = ['DEBOND']
probe = TC._SKIP_PROBES['check_debond_callout']
judged = _ticket({'runs': [], 'reason': None, 'has_openings': False,
'threshold_ft': 4.0})
judged['_view_text'] = {'BED PLAN': 'nothing about debonding'}
assert probe(judged, {}, know, None) is None
blind = _ticket({'runs': None, 'reason': REASON, 'has_openings': False,
'threshold_ft': 4.0})
blind['_view_text'] = {'BED PLAN': ''}
assert REASON in probe(blind, {}, know, None)
# ================================================================== 8.1
#
# Owner, reading WP133: "都哪些lines not interpreted?" -- 8.1 printed
# "4 line(s) of the design summary were not interpreted" and then named
# three of them and an ellipsis. A count the reader cannot resolve into the
# things counted is not information; it is a prompt to go and look somewhere
# the tool had already looked.
def _spec(n):
return {'unresolved': [{'text': 'LINE {0}'.format(i), 'reason': 'x'}
for i in range(n)]}
def test_81_names_every_line_not_the_first_three():
findings = TC.check_spec_unresolved({}, {}, _know(), _spec(7))
assert len(findings) == 1
message = findings[0].message
for i in range(7):
assert 'LINE {0}'.format(i) in message
assert '| ...' not in message
def test_81_still_leads_with_the_count():
findings = TC.check_spec_unresolved({}, {}, _know(), _spec(4))
assert findings[0].message.startswith('4 line(s)')
assert findings[0].severity == 'WARN'
# 8.1 was the last row in the table with no skip probe at all: on two live
# tickets it read "NOT RUN -- no probe declared", because BOTH its empty
# returns reached `run_all` as RAN-EMPTY. The two are not the same silence
# and the tests below are one per path.
def test_81_a_clean_spec_is_an_explicit_pass_not_a_silence():
"""A spec that records zero unresolved lines was READ, and every line of
the design summary matched a rule. That is an answer, and 8.1 says so
rather than leaving the row to be inferred from an absence."""
findings = TC.check_spec_unresolved({}, {}, _know(), {'unresolved': []})
assert len(findings) == 1
assert findings[0].severity == 'OK'
assert findings[0].code == 'SPEC-UNRESOLVED'
assert 'every line of the design summary was interpreted' \
in findings[0].message
def test_81_clean_spec_declares_nothing_to_the_skip_probe():
probe = TC._SKIP_PROBES['check_spec_unresolved']
assert probe({}, {}, _know(), {'unresolved': []}) is None
assert probe({}, {}, _know(), {'unresolved': None}) is None
def test_81_no_spec_on_disk_is_not_run_with_a_reason():
"""No spec means nothing recorded which lines went uninterpreted. The
piece was still judged -- off the regex reading of the summary view --
and the row has to say that rather than imply a clean design."""
assert TC.check_spec_unresolved({}, {}, _know(), None) == []
reason = TC._SKIP_PROBES['check_spec_unresolved']({}, {}, _know(), None)
assert reason
assert 'no spec on disk' in reason
assert 'Extract Design' in reason
def test_81_spec_without_an_unresolved_field_is_not_run():
""""The extractor never wrote down what it could not interpret" is not
"the extractor interpreted everything", and only one of those is a pass."""
spec = {'design_number': 'WP1'}
assert TC.check_spec_unresolved({}, {}, _know(), spec) == []
reason = TC._SKIP_PROBES['check_spec_unresolved']({}, {}, _know(), spec)
assert reason
assert 'no readable "unresolved" list' in reason
def test_81_unresolved_of_the_wrong_shape_is_not_counted_letter_by_letter():
"""A hand-edited `unresolved: foo` used to be walked character by
character and reported as "3 line(s) ... f | o | o"."""
spec = {'unresolved': 'foo'}
assert TC.check_spec_unresolved({}, {}, _know(), spec) == []
assert TC._SKIP_PROBES['check_spec_unresolved']({}, {}, _know(), spec)
def test_81_the_three_silences_render_as_three_different_rows():
"""End to end through the table: a pass, and two NOT RUNs that say
different things. None of them may read "no probe declared"."""
def _row(spec):
findings = TC.check_spec_unresolved({}, {}, _know(), spec)
log = []
reason = TC._SKIP_PROBES['check_spec_unresolved'](
{}, {}, _know(), spec)
if findings:
log.append(('check_spec_unresolved', 'RAN', None))
elif reason:
log.append(('check_spec_unresolved', 'SKIPPED', reason))
else:
log.append(('check_spec_unresolved', 'RAN-EMPTY', None))
for row in TP.checklist_states(findings, log):
if row[0] == '8.1':
return row
raise AssertionError('8.1 is not in the checklist')
clean = _row({'unresolved': []})
assert clean[3] == 'OK'
no_spec = _row(None)
assert no_spec[3] == 'NOT RUN'
assert 'no spec on disk' in no_spec[4]
no_field = _row({'design_number': 'WP1'})
assert no_field[3] == 'NOT RUN'
assert 'no readable' in no_field[4]
for row in (clean, no_spec, no_field):
assert 'no probe declared' not in (row[4] or '')
def test_81_a_clean_pass_adds_no_enumerated_detail():
"""`checklist_details` splits SPEC-UNRESOLVED messages on ' | ' to print
one line per entry. The OK message must not be shredded by it."""
findings = TC.check_spec_unresolved({}, {}, _know(), {'unresolved': []})
assert TP.checklist_details(findings) == {}
def test_81_message_splits_back_into_the_lines_the_page_prints():
"""The two modules must agree on the separator, or the detail is lost.
`ticketchecks` joins with SPEC_UNRESOLVED_SEP and `ticketpage` splits on
its own default. This test is the contract between them.
"""
findings = TC.check_spec_unresolved({}, {}, _know(), _spec(5))
detail = TP.checklist_details(findings)
assert detail['8.1'] == ['LINE 0', 'LINE 1', 'LINE 2', 'LINE 3', 'LINE 4']
assert TC.SPEC_UNRESOLVED_SEP == ' | '
def test_81_enumeration_lands_in_the_rendered_table():
findings = TC.check_spec_unresolved({}, {}, _know(), _spec(4))
rows = TP.checklist_states(findings,
[('check_spec_unresolved', 'RAN', None)])
html = TP.checklist_table_html(rows, TP.checklist_details(findings))
for i in range(4):
assert 'LINE {0}'.format(i) in html
# ------------------------------------------------- 8.1's OTHER source
#
# The live post-restart run, 2026-08-30. The probe work above narrowed 8.1
# to spec-backed intent -- and no design has a spec yet, so 8.1 answered
# "no spec on disk" on every ticket in the run. The owner's earlier run had
# printed the four lines of a design summary nobody had classified, and
# those lines never came from a spec: they came from the regex reader,
# which keeps its own list of notes no rule would place.
#
# The rule these tests hold down: 8.1 reports from WHICHEVER parser built
# the intent, and NOT RUN is only for neither.
def _regex_intent(n):
return {'source': 'regex',
'unresolved': [{'text': 'LINE {0}'.format(i), 'reason': 'x'}
for i in range(n)]}
def test_81_reports_the_regex_readers_own_uninterpreted_lines():
"""No spec, but the summary was read -- and the reading knows what it
could not place. That is the enumeration the owner asked for."""
findings = TC.check_spec_unresolved({}, _regex_intent(4), _know(), None)
assert len(findings) == 1
assert findings[0].severity == 'WARN'
assert findings[0].code == 'SPEC-UNRESOLVED'
assert findings[0].message.startswith('4 line(s)')
for i in range(4):
assert 'LINE {0}'.format(i) in findings[0].message
def test_81_the_regex_enumeration_is_worded_exactly_as_the_spec_one():
"""The format is the thing that was lost, so it is spelled out here.
ticketpage splits on ': ' and then on SPEC_UNRESOLVED_SEP; a message
that drifts on either side stops producing detail rows silently.
"""
from_regex = TC.check_spec_unresolved({}, _regex_intent(4), _know(), None)
from_spec = TC.check_spec_unresolved({}, {}, _know(), _spec(4))
assert from_regex[0].message == from_spec[0].message
assert from_regex[0].message == (
'4 line(s) of the design summary were not interpreted: '
'LINE 0 | LINE 1 | LINE 2 | LINE 3')
assert TP.checklist_details(from_regex)['8.1'] == \
['LINE 0', 'LINE 1', 'LINE 2', 'LINE 3']
def test_81_the_regex_path_reaches_the_rendered_table_as_a_warn():
findings = TC.check_spec_unresolved({}, _regex_intent(4), _know(), None)
rows = dict((r[0], r) for r in TP.checklist_states(
findings, [('check_spec_unresolved', 'RAN', None)]))
assert rows['8.1'][3] == 'WARN'
html = TP.checklist_table_html(rows.values(),
TP.checklist_details(findings))
for i in range(4):
assert 'LINE {0}'.format(i) in html
def test_81_a_clean_read_says_which_parser_is_speaking():
""""Every line was interpreted" is a much weaker claim from a handful of
regexes than from a reviewed spec, so the reader is told which one said
it. Both are still an explicit OK, not a silence."""
from_spec = TC.check_spec_unresolved({}, {}, _know(), {'unresolved': []})
from_regex = TC.check_spec_unresolved(
{}, {'source': 'regex', 'unresolved': []}, _know(), None)
for findings in (from_spec, from_regex):
assert len(findings) == 1
assert findings[0].severity == 'OK'
assert 'every line of the design summary was interpreted' \
in findings[0].message
assert TC.SPEC_SOURCE_SPEC in from_spec[0].message
assert TC.SPEC_SOURCE_REGEX in from_regex[0].message
assert TC.SPEC_SOURCE_REGEX not in from_spec[0].message
assert TC.SPEC_SOURCE_SPEC not in from_regex[0].message
def test_81_neither_clean_message_is_shredded_into_detail_rows():
"""Both OK sentences must stay clear of ': ' -- `checklist_details`
partitions on it and would print the tail as enumerated lines."""
for spec, intent in (({'unresolved': []}, {}),
(None, {'source': 'regex', 'unresolved': []})):
findings = TC.check_spec_unresolved({}, intent, _know(), spec)
assert ': ' not in findings[0].message
assert TP.checklist_details(findings) == {}
def test_81_the_regex_path_declares_nothing_to_the_skip_probe():
probe = TC._SKIP_PROBES['check_spec_unresolved']
assert probe({}, _regex_intent(4), _know(), None) is None
assert probe({}, {'source': 'regex', 'unresolved': []},
_know(), None) is None
def test_81_not_run_is_now_only_for_no_record_from_either_parser():
"""The reason has to name both absences, because either one alone is no
longer enough to stand 8.1 down."""
reason = TC._SKIP_PROBES['check_spec_unresolved'](
{}, {'source': 'regex'}, _know(), None)
assert TC.check_spec_unresolved({}, {'source': 'regex'}, _know(), None) == []
assert reason
assert 'no spec on disk' in reason
assert 'Extract Design' in reason
assert 'regex reader could not place' in reason
def test_81_an_intent_unresolved_of_the_wrong_shape_is_not_walked_letter_by_letter():
"""The spec side already refuses to count a stray string character by
character. The intent side inherits the same refusal."""
intent = {'source': 'regex', 'unresolved': 'foo'}
assert TC.check_spec_unresolved({}, intent, _know(), None) == []
assert TC._SKIP_PROBES['check_spec_unresolved']({}, intent, _know(), None)
def test_81_a_spec_still_outranks_the_intent_when_both_are_present():
"""The reviewed record wins, and its silences keep their own meaning --
a spec with no readable field is still NOT RUN even when the intent
beside it would have answered."""
assert TC.check_spec_unresolved({}, _regex_intent(2), _know(),
_spec(5))[0].message.startswith('5 line(s)')
bare = {'design_number': 'WP1'}
assert TC.check_spec_unresolved({}, _regex_intent(2), _know(), bare) == []
assert 'no readable' in TC._SKIP_PROBES['check_spec_unresolved'](
{}, _regex_intent(2), _know(), bare)
# The plumbing that makes the branch above reachable on a live ticket: the
# regex reader has to hand its list forward on the intent dict, or 8.1 has
# nothing to read. `parse_design_summary` is regexes over text notes, and
# `_text_notes` is the only thing in it that needs a document.
class _Coord(object):
def __init__(self, x, y):
self.X, self.Y = x, y
class _Note(object):
def __init__(self, text, x=0.0, y=0.0):
self.Text = text
self.Coord = _Coord(x, y)
class _Id(object):
IntegerValue = 42
class _View(object):
Name = 'PNL1.0 DESIGN SUMMARY'
Id = _Id()
@contextlib.contextmanager
def _summary_view(texts):
"""`parse_design_summary` over exactly these note texts."""
notes = [_Note(t, y=float(i)) for i, t in enumerate(texts)]
original = QC._text_notes
QC._text_notes = lambda doc, view_id: notes
try:
yield _View()
finally:
QC._text_notes = original
def test_the_regex_reader_carries_its_uninterpreted_lines_on_the_intent():
lines = ["USE 5,000 KSI MILD", 'STRAND PATTERN', '1/2" DIA. STR.']
with _summary_view(lines) as view:
intent = QC.parse_design_summary(None, view)
assert intent['mild_ksi'] == 5000 # the reader still reads
texts = [u['text'] for u in intent['unresolved']]
assert texts == ['STRAND PATTERN', '1/2" DIA. STR.']
assert all(u['reason'] for u in intent['unresolved'])
def test_a_summary_the_reader_placed_entirely_carries_an_empty_list():
"""Not a missing key -- the difference between "interpreted everything"
and "never wrote down what it could not interpret" is the whole point of
8.1's third state."""
with _summary_view(["USE 5,000 KSI MILD"]) as view:
intent = QC.parse_design_summary(None, view)
assert intent['unresolved'] == []
assert TC.check_spec_unresolved({}, intent, _know(), None)[0].severity \
== 'OK'
def test_the_reader_and_the_check_meet_end_to_end():
"""The regression, from the notes in the view to the row on the page.
`STRAND PATTERN` is now PLACED by the summary vocabulary as a group
label, so 8.1 reports two uninterpreted lines and not three -- and the
line it placed is still visible, in the inventory clause. That is the
whole contract of the vocabulary: it shrinks the row by understanding
lines, never by dropping them.
"""
with _summary_view(['STRAND PATTERN', '1/2" DIA. STR.',
'STRIPPING PER PLANT STANDARD']) as view:
intent = QC.parse_design_summary(None, view)
intent['source'] = 'regex'
findings, log = TC.run_all({'mark': 'TEST-1'}, intent, _know(), None)
unresolved = [f for f in findings if f.code == 'SPEC-UNRESOLVED']
assert len(unresolved) == 1
assert unresolved[0].message.startswith('2 line(s)')
rows = dict((r[0], r) for r in TP.checklist_states(findings, log))
assert rows['8.1'][3] == 'WARN'
details = TP.checklist_details(findings)['8.1']
assert details[:2] == ['1/2" DIA. STR.', 'STRIPPING PER PLANT STANDARD']
assert 'group label' in details[-1]
# The verbatim record on the intent is UNTOUCHED: the reader still wrote
# down every line its own rules could not place.
assert [u['text'] for u in intent['unresolved']] == [
'STRAND PATTERN', '1/2" DIA. STR.', 'STRIPPING PER PLANT STANDARD']
# ================================================================== 6.4
#
# Handed back 2026-08-30: `check_dunnage_openings` had six `return []` paths
# and no probe, so `run_all` logged it RAN-EMPTY and the row read NOT RUN
# with the reason "no probe declared" -- a sentence about the tool, not
# about the piece. Every silent return now leaves `_dunnage_silence` behind
# and a probe reads it, the same shape 5.2 already used.
#
# THE TRAP, and it is the whole point of the item: "no size notes found" and
# "notes found but no parseable feet" are FAILURES TO READ THE TICKET, not
# clean pieces. They must keep reporting NOT RUN. Only the fall-through
# after the clearance loop is a pass, and it now says so out loud.
@contextlib.contextmanager
def _revit():
"""The stub, for the duration of one call, and gone again afterwards.
`check_dunnage_openings` imports four Revit names at the top of its own
body, before its first early return, so the two config paths below
cannot be reached without it. It is popped again immediately -- see the
module docstring: a stub left in `sys.modules` breaks
`test_reinforceoffline`.
"""
added = _stub_revit()
try:
yield
finally:
for name in added:
sys.modules.pop(name, None)
def _dunnage_probe(t):
return TC._SKIP_PROBES['check_dunnage_openings'](t, {}, _know(), None)
def test_64_a_ticket_with_no_panel_says_so_instead_of_declaring_no_probe():
t = {'mark': 'TEST-1'}
with _revit():
assert TC.check_dunnage_openings(t, {}, _know(), None) == []
assert 'no panel or no piece length' in t['_dunnage_silence']
assert 'no panel or no piece length' in _dunnage_probe(t)
# ----- geometry stand-in: 6.4 now reads the openings BEFORE the sheet ------
#
# Owner, reading a live report, 2026-08-31: "6.4 应该是 OK, 既然没有
# opening" -- a piece with zero openings settles the row on its own,
# because dunnage cannot sit over an opening that does not exist. The
# fixture below stands up just enough of the geometry chain (panel, frame,
# extents, solid, openings) for `check_dunnage_openings` to reach that
# judgement without touching Revit, and controls `solid` separately from
# `rects` so a genuine zero (solid found, no opening) and a genuine
# ungathered (solid never found) are two different tests, not one.
class _FakeTdoc(object):
def __init__(self, panel):
self._panel = panel
def GetElement(self, _eid):
return self._panel
class _FakeCoord(object):
def __init__(self, x, y):
self.X = x
self.Y = y
class _FakeNote(object):
def __init__(self, text, x, y):
self.Text = text
self.Coord = _FakeCoord(x, y)
class _FakeNoteQuery(object):
def __init__(self, notes):
self._notes = notes
def OfCategory(self, _cat):
return self
def ToElements(self):
return self._notes
class _FakeSheet(object):
Id = 'sheet-1'
def _patch_dunnage_geometry(monkeypatch, rects, solid_present=True,
notes=None):
"""Stands the geometry (and, if `notes` is given, the sheet) up for one
`check_dunnage_openings` call, entirely off host fakes:
rects -- the openings `liftexport._openings` hands back.
solid_present -- False models the panel solid itself failing on the
SECOND read (`liftexport.concrete_solid` returns
None) -- the state 6.4 must never call a zero.
notes -- [(text, x, y)] text notes on sheet 1, only reached
once `rects` is non-empty.
"""
db = sys.modules['Autodesk.Revit.DB']
monkeypatch.setattr(db, 'ElementId', lambda v: v, raising=False)
if notes is not None:
monkeypatch.setattr(
db, 'FilteredElementCollector',
lambda _doc, _view_id: _FakeNoteQuery(
[_FakeNote(text, x, y) for (text, x, y) in notes]),
raising=False)
db.BuiltInCategory.OST_TextNotes = 'OST_TextNotes'
monkeypatch.setattr(QC, '_panel_frame', lambda p: ('frame',))
monkeypatch.setattr(QC, 'concrete_extents',
lambda p, f: ((0.0, 0.0, 0.0), (20.0, 4.0, 0.5)))
from vibe import liftexport
solid = object() if solid_present else None
monkeypatch.setattr(liftexport, 'concrete_solid', lambda p: solid)
monkeypatch.setattr(liftexport, '_openings',
lambda solid_, frame, lo, hi, axes: list(rects))
monkeypatch.setattr(
liftexport, 'opening_penetration',
lambda panel, frame, solid_, rects_, axes, lo, hi:
[{'through': True} for _ in rects_])
return object() # the fake panel `_FakeTdoc.GetElement` hands back
def test_64_zero_openings_measured_is_ok_before_any_notes_are_read(
monkeypatch):
"""The vacuous case the owner's correction is for. `sheets` is left
empty and no notes are wired up at all -- if the check reached for
either, it would raise (no `FilteredElementCollector` patched) or read
NOT RUN off the empty-sheets gate, so an OK here proves the openings
settled the row before the stations were ever needed."""
t = {'mark': 'TEST-1', 'panel_id': 1, 'length': 20.0, 'sheets': []}
with _revit():
panel = _patch_dunnage_geometry(monkeypatch, rects=[])
t['doc'] = _FakeTdoc(panel)
out = TC.check_dunnage_openings(t, {}, _know(), None)
assert len(out) == 1
assert out[0].severity == 'OK'
assert out[0].code == 'DUN-OPENING'
assert 'no openings on this piece' in out[0].message
assert 'stations were not needed' in out[0].message
# never touched _dunnage_silence -- this is a Finding, not a silence
assert t.get('_dunnage_silence') is None
def test_64_openings_ungathered_is_not_run_and_distinct_from_zero(
monkeypatch):
"""`solid is None` on the second read is NOT the same answer as zero
openings measured -- both `_openings` and the old code would have
handed back an empty list either way, which is the exact ambiguity the
owner's correction had to resolve without also breaking this case."""
t = {'mark': 'TEST-1', 'panel_id': 1, 'length': 20.0, 'sheets': []}
with _revit():
panel = _patch_dunnage_geometry(monkeypatch, rects=[],
solid_present=False)
t['doc'] = _FakeTdoc(panel)
assert TC.check_dunnage_openings(t, {}, _know(), None) == []
assert t['_dunnage_silence'] is not None
assert 'no claim is made about where the dunnage lands' \
in t['_dunnage_silence']
assert 'no openings on this piece' not in t['_dunnage_silence']
def test_64_a_ticket_with_no_sheet_says_so(monkeypatch):
"""Only reached now once an opening is on the piece -- with none, the
row is already settled and the sheet is never asked for."""
t = {'mark': 'TEST-1', 'panel_id': 1, 'length': 20.0, 'sheets': []}
with _revit():
panel = _patch_dunnage_geometry(monkeypatch,
rects=[(8.0, 10.0, 0.5, 2.0)])
t['doc'] = _FakeTdoc(panel)
assert TC.check_dunnage_openings(t, {}, _know(), None) == []
assert 'no sheet' in t['_dunnage_silence']
def test_64_openings_present_and_dunnage_unparsed_is_not_run(monkeypatch):
"""The trap the owner's report actually hit: a piece WITH an opening
whose printed insets fail to parse still must not pass -- it is a
failure to read the ticket, not a clean piece, and it must not be
confused with the vacuous zero-opening case above."""
notes = [('PLANT STD"', 3.0, 0.59), ('SEE NOTE"', 5.0, 0.59)]
t = {'mark': 'TEST-1', 'panel_id': 1, 'length': 20.0,
'sheets': [_FakeSheet()]}
with _revit():
panel = _patch_dunnage_geometry(monkeypatch,
rects=[(8.0, 10.0, 0.5, 2.0)],
notes=notes)
t['doc'] = _FakeTdoc(panel)
assert TC.check_dunnage_openings(t, {}, _know(), None) == []
assert 'none parsed into feet' in t['_dunnage_silence']
def test_64_openings_present_and_dunnage_over_one_is_warn(monkeypatch):
"""A piece WITH an opening, printed insets that DO parse, and one
station too close to it -- the WP115-style WARN, unchanged by any of
this."""
notes = [('7\'-8"', 3.0, 0.59), ('3\'-0"', 5.0, 0.59)]
t = {'mark': 'TEST-1', 'panel_id': 1, 'length': 20.0,
'sheets': [_FakeSheet()]}
with _revit():
panel = _patch_dunnage_geometry(monkeypatch,
rects=[(8.0, 10.0, 0.5, 2.0)],
notes=notes)
t['doc'] = _FakeTdoc(panel)
out = TC.check_dunnage_openings(t, {}, _know(), None)
assert len(out) == 1
assert out[0].severity == 'WARN'
assert out[0].code == 'DUN-OPENING'
assert 'sits over an opening' not in out[0].message
assert '4.0"' in out[0].message # 7.667 ft to 8.0 ft edge
def test_64_the_probe_never_returns_none_on_a_silence():
"""A probe returning None turns an empty return into a quiet OK. On a
check with six silent paths that is the one outcome that must be
unreachable, so the fallback is a reason, never None."""
assert _dunnage_probe({}) is not None
assert 'not run' in _dunnage_probe({})
def test_64_reads_not_run_with_a_real_reason_on_the_table_the_reader_sees():
reason = 'no size notes ... could not be read'
rows = dict((r[0], r) for r in TP.checklist_states(
[], [('check_dunnage_openings', 'SKIPPED', reason)]))
assert rows['6.4'][3] == 'NOT RUN'
assert rows['6.4'][4] == reason
assert 'no probe declared' not in (rows['6.4'][4] or '')
def test_64_a_clear_piece_reaches_the_table_as_ok_not_as_a_silence():
"""The fall-through after the clearance loop is the ONLY pass, and it
now emits an OK finding rather than an empty list -- 4.7's flip, applied
to the row this check exists for."""
findings = [TC.Finding('OK', 'DUN-OPENING',
'every dunnage station is clear of every opening')]
rows = dict((r[0], r) for r in TP.checklist_states(
findings, [('check_dunnage_openings', 'RAN', None)]))
assert rows['6.4'][3] == 'OK'
assert TP.state_color(rows['6.4'][3]) == 'green'
# ================================================================== 5.4
#
# The owner read 5.4 as "没跑" on a live ticket (2026-08-30). It had not
# failed: the design it was checking records no mesh at all. What was wrong
# was that ONE sentence covered three different silences and pointed at
# Extract Design in all of them -- including the one where re-extracting
# returns exactly the same nothing.
#
# The probe is the whole subject here. It is a pure decision over a spec
# dict and an intent dict and touches no document.
_WWF_PROBE = TC._SKIP_PROBES['check_wwf_designation']
def test_54_a_design_that_has_a_mesh_is_not_a_silence_at_all():
assert _WWF_PROBE({}, {'wwf_mesh': '6X6-W1.4XW1.4'}, _know(), {}) is None
def test_54_no_spec_at_all_sends_the_reader_to_extract_design():
reason = _WWF_PROBE({}, {'source': 'spec'}, _know(), None)
assert 'Extract Design' in reason
assert 'no design summary has been extracted' in reason
def test_54_the_regex_fallback_says_it_was_the_one_that_found_nothing():
"""With no spec on file the intent came off the summary view through
regexes. Blaming Extract Design for that reading is blaming a tool that
was never run."""
reason = _WWF_PROBE({}, {'source': 'regex'}, _know(), None)
assert 'fall-back reader' in reason
def test_54_a_spec_that_records_no_mesh_does_not_blame_the_extractor():
"""THE COMMON CASE, and the one the owner hit: the spec is there, it was
extracted cleanly, and `reinforcing.wwf.mesh` is null because the design
reinforces with bars. The old text told him to go verify in Extract
Design, which would have produced the same null."""
spec = {'reinforcing': {'wwf': {'mesh': None, 'layers': None,
'per_face': None}}}
reason = _WWF_PROBE({}, {'source': 'spec'}, _know(), spec)
assert 'reinforcing.wwf.mesh is empty' in reason
assert 'bars rather than mesh' in reason
assert 're-extracting will not' in reason
assert 'run Extract Design' not in reason
def test_54_a_spec_predating_the_field_is_the_one_worth_re_extracting():
reason = _WWF_PROBE({}, {'source': 'spec'}, _know(),
{'reinforcing': {'mild_grade_ksi': 60}})
assert 'no reinforcing.wwf section' in reason
assert 're-run Extract Design' in reason
def test_54_every_one_of_those_silences_is_still_red_and_never_a_pass():
"""A better sentence is not permission to soften the state. 5.4 cannot
judge a designation nobody has, so the row stays NOT RUN."""
for spec in (None, {}, {'reinforcing': {'wwf': {'mesh': None}}}):
reason = _WWF_PROBE({}, {'source': 'spec'}, _know(), spec)
assert reason
rows = dict((r[0], r) for r in TP.checklist_states(
[], [('check_wwf_designation', 'SKIPPED', reason)]))
assert rows['5.4'][3] == 'NOT RUN'
assert TP.state_color(rows['5.4'][3]) == 'red'
assert rows['5.4'][4] == reason
# ================================================================== 5.7
#
# The two config gates are pure decisions -- read a key, refuse, say why --
# and they are the paths that decide whether this check can speak at all on
# a plant that has written nothing down.
def test_57_stands_down_when_the_stock_length_is_not_written_down():
"""It never assumes 40: the number is published data and a null key is
NOT RUN with the key named."""
know = _know()
know['catalogue']['rebar']['stock_length_ft'] = None
t = {'mark': 'TEST-1'}
assert TC.check_bar_stock_length(t, {}, know, None) == []
assert 'catalogue.rebar.stock_length_ft' in t['_bar_length_silence']
def test_57_stands_down_rather_than_measuring_a_bar_off_its_geometry():
"""The owner's ruling is that the length is READ. With no parameter
mapped there is nothing to read, and a bounding box is not an offer this
check is allowed to make."""
know = _know()
know['catalogue']['rebar']['stock_length_ft'] = 40.0
know.setdefault('parameters', {}).setdefault('member', {})['length'] = None
t = {'mark': 'TEST-1'}
assert TC.check_bar_stock_length(t, {}, know, None) == []
assert 'member.length' in t['_bar_length_silence']
assert 'measure' in t['_bar_length_silence']
def _know_with_bars(bars, stock=40.0, size='#4', unknown=None,
basis='parameter'):
"""A ticket whose bar gather is already solved.
EVERY BAR GETS A SIZE unless the caller pins one, because 5.7 resolves
its stock length PER SIZE (`knowledge.rebar_stock`) as of 2026-08-31 and
a bar whose size cannot be read is deliberately not judged at all -- see
`test_57_a_bar_whose_size_cannot_be_read_is_not_judged_at_all`.
Pass `size=None` to exercise that path.
AND EVERY SIZE GETS A BASIS, because a size travels with the field that
answered for it -- `'parameter'` for the straight population and
`'description'` for the bent one. A gathered bar always carries one; a
row here without it would be a shape the gather cannot produce.
"""
know = _know()
know['catalogue']['rebar']['stock_length_ft'] = stock
know.setdefault('parameters', {}).setdefault(
'member', {})['length'] = 'BAR_LENGTH_PARAM'
know['parameters']['member']['bar_size'] = 'BAR_SIZE_PARAM'
seeded = []
for bar in bars:
row = dict(bar)
row.setdefault('size', size)
if row.get('size'):
row.setdefault('size_basis', basis)
seeded.append(row)
t = {'mark': 'TEST-1',
'_bar_members': {'bars': seeded, 'reason': None,
'unknown': list(unknown or [])}}
return t, know
def test_57_a_long_bar_warns_and_says_the_ruling_is_still_open():
"""Measured: 2,122 elements sit at or over 40 ft on the live model, and
whether a continuous run may is with the owner. The finding ships
honest rather than pre-exempting anything."""
t, know = _know_with_bars(
[{'mark': 'SR1101', 'family': 'F', 'length_ft': 60.0}])
findings = TC.check_bar_stock_length(t, {}, know, None)
assert findings[0].severity == 'WARN'
assert findings[0].code == 'BAR-LENGTH'
assert 'SR1101' in findings[0].message
assert 'stock_exempt_marks' in findings[0].message
def test_57_the_bars_it_could_not_judge_get_their_own_code_not_the_rows():
t, know = _know_with_bars(
[{'mark': 'SR310', 'family': 'F', 'length_ft': 11.0},
{'mark': 'SR9', 'family': 'F', 'length_ft': 0.0},
{'mark': 'SR8', 'family': 'F', 'length_ft': None}])
findings = TC.check_bar_stock_length(t, {}, know, None)
codes = dict((f.code, f) for f in findings)
assert codes['BAR-LENGTH'].severity == 'OK'
assert codes['BAR-LENGTH-UNREAD'].severity == 'OK'
assert 'SR9' in codes['BAR-LENGTH-UNREAD'].message
assert 'SR8' in codes['BAR-LENGTH-UNREAD'].message
# and the row is decided by BAR-LENGTH alone
rows = dict((r[0], r) for r in TP.checklist_states(
findings, [('check_bar_stock_length', 'RAN', None)]))
assert rows['5.7'][3] == 'OK'
def test_57_a_piece_with_no_recognised_bar_names_the_key_that_would_fix_it():
"""This plant models bars as Specialty Equipment, so Revit's category
says nothing and everything rests on drawings.reinforcing_families --
which is empty. The row must say that, not pass."""
t, know = _know_with_bars([])
assert TC.check_bar_stock_length(t, {}, know, None) == []
assert 'reinforcing_families' in t['_bar_length_silence']
def test_57_an_exempt_mark_is_set_aside_and_leaves_nothing_to_judge():
"""Exempt the only bar and the check has compared NOTHING. That is a
NOT RUN naming what was set aside -- never an OK, which would report a
ticket nobody looked at as a clean one."""
t, know = _know_with_bars(
[{'mark': 'SR1101', 'family': 'F', 'length_ft': 60.0}])
know['catalogue']['rebar']['stock_exempt_marks'] = ['SR1101']
assert TC.check_bar_stock_length(t, {}, know, None) == []
assert 'stock_exempt_marks' in t['_bar_length_silence']
assert 'SR1101' in t['_bar_length_silence']
def test_57_an_exemption_alongside_a_judged_bar_is_reported_not_hidden():
t, know = _know_with_bars(
[{'mark': 'SR1101', 'family': 'F', 'length_ft': 60.0},
{'mark': 'SR310', 'family': 'F', 'length_ft': 11.0}])
know['catalogue']['rebar']['stock_exempt_marks'] = ['SR1101']
codes = dict((f.code, f) for f in
TC.check_bar_stock_length(t, {}, know, None))
assert codes['BAR-LENGTH'].severity == 'OK' # the one judged bar
assert 'SR1101' in codes['BAR-LENGTH-UNREAD'].message
# ================================================================== 5.6
def test_56_stands_down_when_no_bar_was_recognised():
t, know = _know_with_bars([])
assert TC.check_bar_mark_uniqueness(t, {}, know, None) == []
assert 'reinforcing_families' in t['_barmark_silence']
# ------------- what 5.6 and 5.7 say when no bar was recognised (08-30)
#
# Both stood down on the same gather and printed the SAME paragraph, one row
# under the other, opening with "no reinforcing found on this piece". The
# owner read the pair as "5.6 5.7 没跑" and he was right that they had not
# run -- but the sentence was also making a claim the tool is in no position
# to make. This plant models bars outside any rebar category, so with
# `drawings.reinforcing_families` empty a piece with three hundred members
# and a bar line in its design summary produced exactly the same words as a
# piece with genuinely no steel in it.
def test_5657_do_not_claim_the_piece_has_no_reinforcing_when_they_cannot_tell():
"""With the key empty the honest statement is about the TOOL, not the
piece: nothing could be recognised."""
t, know = _know_with_bars([])
TC.check_bar_stock_length(t, {}, know, None)
reason = t['_bar_length_silence']
assert 'could be RECOGNISED' in reason
assert 'not the same as the piece carrying none' in reason
assert 'no reinforcing found on this piece' not in reason
def test_5657_name_the_switch_that_would_make_them_run():
"""A NOT RUN a reader cannot act on is half a report."""
t, know = _know_with_bars([])
TC.check_bar_mark_uniqueness(t, {}, know, None)
reason = t['_barmark_silence']
assert 'TO MAKE THIS ROW RUN' in reason
assert 'drawings.reinforcing_families' in reason
def test_5657_each_says_what_IT_lost_rather_than_printing_one_paragraph_twice():
"""Two rows, two sentences. The shared half is shared; the consequence
is the row's own, so a reader sees two checks reporting rather than one
message pasted twice."""
t, know = _know_with_bars([])
TC.check_bar_stock_length(t, {}, know, None)
TC.check_bar_mark_uniqueness(t, {}, know, None)
length, mark = t['_bar_length_silence'], t['_barmark_silence']
assert length != mark
assert 'No bar length was compared against stock' in length
assert 'printed bar mark' in mark
assert 'printed bar mark' not in length
def test_5657_with_the_key_filled_in_it_IS_a_statement_about_the_piece():
"""Both signals were asked and both came back empty: now the tool has
looked, and it may say so -- naming what it looked for."""
t, know = _know_with_bars([])
know['drawings']['reinforcing_families'] = ['BAR_TOKEN_A', 'BAR_TOKEN_B']
TC.check_bar_stock_length(t, {}, know, None)
reason = t['_bar_length_silence']
assert 'appears to carry no reinforcing' in reason
assert 'BAR_TOKEN_A' in reason and 'BAR_TOKEN_B' in reason
assert 'TO MAKE THIS ROW RUN' not in reason
def test_5657_a_better_sentence_does_not_make_either_row_any_less_red():
"""Naming the switch is not flipping it. Nothing here decides a piece
is clean."""
t, know = _know_with_bars([])
TC.check_bar_stock_length(t, {}, know, None)
TC.check_bar_mark_uniqueness(t, {}, know, None)
rows = dict((r[0], r) for r in TP.checklist_states(
[], [('check_bar_stock_length', 'SKIPPED', t['_bar_length_silence']),
('check_bar_mark_uniqueness', 'SKIPPED', t['_barmark_silence'])]))
for num in ('5.6', '5.7'):
assert rows[num][3] == 'NOT RUN', num
assert TP.state_color(rows[num][3]) == 'red', num
assert rows['5.7'][4] == t['_bar_length_silence']
assert rows['5.6'][4] == t['_barmark_silence']
def test_56_a_clean_ticket_reaches_the_table_as_ok():
t, know = _know_with_bars(
[{'mark': 'SR310', 'family': 'F', 'length_ft': 11.0}])
t['_view_text'] = {}
findings = [TC.Finding('OK', 'BARMARK-UNIQUE', 'all marks agree')]
rows = dict((r[0], r) for r in TP.checklist_states(
findings, [('check_bar_mark_uniqueness', 'RAN', None)]))
assert rows['5.6'][3] == 'OK'
# ============================ rows answered by a CODE, not a check function
#
# On a live run, 2026-08-31, six rows came back like this in `qc-run.json`:
#
# {"item": "3.1", "state": "NOT RUN", "judged": false,
# "checks": [], "reason": null, "note": null, "owner_codes": ["FC"]}
#
# `reason` null, `note` null, `checks` empty; in the text report a bare red
# NOT RUN with nothing under it. Those rows name no check function -- they
# are answered by codes `ticketqc.check_ticket` emits -- so the skip probes,
# which are keyed by check function, could not reach them. That is the §3.1
# guarantee failing one level ABOVE where it was installed, and the reasons
# existed all along: the design had no extracted spec.
def _no_spec_intent():
"""The intent table as it arrives with nothing extracted: every column a
dash, and `source` saying which reader produced it."""
return {'source': 'regex'}
def _reasons(t=None, intent=None, know=None, spec=None):
return dict((name[len(TC.ROW_NOTE_PREFIX):], note)
for (name, _state, note) in TC.row_silences(
t if t is not None else {'mark': 'TEST-1'},
_no_spec_intent() if intent is None else intent,
_know() if know is None else know, spec))
def test_every_code_answered_row_can_explain_its_own_silence():
"""THE MECHANICAL GUARANTEE. A row that names no check and declares no
reason of its own must have a row probe, or it can print NOT RUN with
nothing to say -- which is what happened. Adding such a row now fails
here instead of on the owner's screen."""
for item in TP.CHECKLIST_ITEMS:
num, declared, check_slot = item[0], item[4], item[5]
if num in TP.CHECKLIST_FOR_ENGINEER or TP._check_names(check_slot):
continue
assert num in TC._ROW_PROBES or declared, (
'row {0} is answered by finding codes alone and has neither a '
'row probe nor a declared reason'.format(num))
def test_the_six_rows_from_the_live_run_all_speak_now():
said = _reasons()
for num in ('2.1', '2.3', '3.1', '3.2', '4.1', '4.2'):
assert said.get(num), num
assert said[num].strip(), num
def test_the_missing_spec_is_named_as_the_reason_not_the_ticket():
"""Three of those rows are quiet for ONE reason -- nothing was extracted
for this design -- and that is a statement about the design side, never
about the piece. The sentence has to say which, or it reads as a defect
in a ticket nobody checked."""
said = _reasons()
for num in ('3.1', '3.2', '2.3', '4.2'):
assert 'no spec has been extracted for this design' in said[num]
assert 'intent.source = regex' in said[num]
assert 'Run Extract Design' in said[num]
assert 'nothing here is a statement about the ticket' in said[num]
assert "carries no f'c" in said['3.1']
assert "carries no f'ci" in said['3.2']
assert 'carries no unit weight' in said['2.3']
assert 'carries no strand group' in said['4.2']
def test_a_row_probe_goes_quiet_the_moment_its_input_is_there():
"""A probe explains a SILENCE and must never answer for a row that was
answered. With the design value present it says nothing at all."""
said = _reasons(intent={'source': 'spec', 'fc': 6000, 'fci': 3500,
'pcf': 150, 'strands': [{'dia': 0.5, 'qty': 4}],
'envelope': {'MAX_LENGTH': (30.0, "30'")}},
t={'mark': 'T', 'length': 24.9, 'width': 8.0,
'ticket_strand_count': 4,
'design_number': 'PNL1', 'tkt_design_number': 'PNL1.0'},
spec={'concrete': {}})
assert said == {}
def test_41_names_both_halves_because_either_would_have_answered():
"""4.1 needs the ticket's printed count OR the design's strand groups.
Row 6.4 reports that same parse failure in a full sentence on the same
piece; a fact that answers one row and leaves another blank is the
defect."""
said = _reasons()
assert 'did not parse into a count' in said['4.1']
assert 'no strand group to fall back on' in said['4.1']
# the printed count alone is enough to make it stop speaking
assert '4.1' not in _reasons(t={'mark': 'T', 'ticket_strand_count': 6})
def test_21_separates_a_piece_it_could_not_measure_from_a_design_with_no_envelope():
"""Two absences, two different fixes: one is a piece the tool could not
measure, the other a design nobody extracted."""
both = _reasons()['2.1']
assert 'were not measured AND' in both
unmeasured = _reasons(
intent={'source': 'spec', 'envelope': {'MAX_LENGTH': (30.0, "30'")}},
t={'mark': 'T'})['2.1']
assert 'could not be measured off' in unmeasured
no_envelope = _reasons(t={'mark': 'T', 'length': 24.9, 'width': 8.0},
intent={'source': 'spec'},
spec={'x': 1})['2.1']
assert 'no size envelope' in no_envelope
def test_a_row_probe_that_raises_is_reported_and_never_swallowed():
"""A probe whose own failure was silent would reproduce, one level
further out, the exact silence this mechanism exists to close."""
def _boom(t, intent, know, spec):
raise KeyError('nope')
TC._ROW_PROBES['9.9'] = _boom
try:
said = _reasons()
finally:
del TC._ROW_PROBES['9.9']
assert 'KeyError' in said['9.9']
assert 'never as a pass' in said['9.9']
def test_run_all_carries_the_row_reasons_out_in_its_own_log():
"""The log is the one channel that already reaches every consumer, so
the button and `qc-run.json` get this with no edit of their own."""
findings, log = TC.run_all({'mark': 'T'}, _no_spec_intent(), _know(), None)
rows = [entry for entry in log
if str(entry[0]).startswith(TC.ROW_NOTE_PREFIX)]
assert rows, 'run_all recorded no row-level reasons at all'
for (name, state, note) in rows:
assert state == 'NO-CODE'
assert note
table = dict((r[0], r) for r in TP.checklist_states(findings, log))
assert table['3.1'][3] == 'NOT RUN'
assert 'no spec has been extracted' in table['3.1'][4]
# ================================================================== 1.5
def test_15_and_56_and_57_all_declare_a_probe():
"""Every new check answers "why did you say nothing" -- the whole reason
6.4 came back."""
for name in ('check_callout_overlap', 'check_bar_mark_uniqueness',
'check_bar_stock_length', 'check_dunnage_openings'):
assert name in TC._SKIP_PROBES
def test_15_and_56_and_57_are_actually_registered_to_run():
names = [c.__name__ for c in TC.CHECKS]
for name in ('check_callout_overlap', 'check_bar_mark_uniqueness',
'check_bar_stock_length'):
assert name in names
def test_the_new_probes_never_return_none_on_a_silence():
for name in ('check_callout_overlap', 'check_bar_mark_uniqueness',
'check_bar_stock_length'):
assert TC._SKIP_PROBES[name]({}, {}, _know(), None) is not None
def test_15_reaches_the_table_and_the_rotated_note_cannot_decide_it():
findings = [TC.Finding('OK', 'CALLOUT-OVERLAP', 'no two overlap'),
TC.Finding('OK', 'CALLOUT-OVERLAP-UNREAD', '1 rotated')]
rows = dict((r[0], r) for r in TP.checklist_states(
findings, [('check_callout_overlap', 'RAN', None)]))
assert rows['1.5'][3] == 'OK'
# ============================ the green-row trap, on all three new checks
#
# Found by reading `checklist_states` back against the new checks rather than
# by a failing test, and it is worth a test of its own because the shape
# recurs: a check that returns ONLY its set-aside note returns a non-empty
# list, so `run_all` logs it RAN and never asks the probe -- and
# `checklist_states`, finding no OWNER code among the findings on a check
# that RAN, renders the row **OK**. A ticket on which nothing could be
# judged would have printed green.
#
# The fix is the same in all three: when nothing was actually compared, the
# check returns [] and the detail goes into the probe's reason, so the row
# is red and says what was set aside. NOT RUN has to keep meaning exactly
# one thing, in both directions.
def test_57_all_bars_unreadable_is_red_and_never_a_green_row():
t, know = _know_with_bars(
[{'mark': 'SR8', 'family': 'F', 'length_ft': None},
{'mark': 'SR9', 'family': 'F', 'length_ft': 0.0}])
assert TC.check_bar_stock_length(t, {}, know, None) == []
reason = t['_bar_length_silence']
assert 'SR8' in reason and 'SR9' in reason # the detail survives
rows = dict((r[0], r) for r in TP.checklist_states(
[], [('check_bar_stock_length', 'SKIPPED', reason)]))
assert rows['5.7'][3] == 'NOT RUN'
assert TP.state_color(rows['5.7'][3]) == 'red'
def test_56_all_bars_unmarked_is_red_and_never_a_green_row():
t, know = _know_with_bars(
[{'mark': None, 'family': 'BAR_FAMILY_B', 'length_ft': 2.0}])
assert TC.check_bar_mark_uniqueness(t, {}, know, None) == []
assert 'BAR_FAMILY_B' in t['_barmark_silence']
rows = dict((r[0], r) for r in TP.checklist_states(
[], [('check_bar_mark_uniqueness', 'SKIPPED',
t['_barmark_silence'])]))
assert rows['5.6'][3] == 'NOT RUN'
def test_the_set_aside_note_alone_would_have_rendered_green():
"""The bug itself, pinned: this is what the three checks must never
return. If a future edit lets a set-aside note out on its own, this
assertion is the one that explains why the row went green."""
rows = dict((r[0], r) for r in TP.checklist_states(
[TC.Finding('OK', 'BAR-LENGTH-UNREAD', 'nothing could be read')],
[('check_bar_stock_length', 'RAN', None)]))
assert rows['5.7'][3] == 'OK' # <- exactly the wrong outcome
# ================================================================== 6.3 cost
#
# Measured on the live model 2026-08-30: the all-checks wall on WP396 (the
# LEDGE piece) was 41.9 s, and a per-check breakdown put 98.9% of it -- 40.2 s
# -- inside `check_handling_result`. A second call cost 38.8 s, so it is
# per-ticket cost and not cold geometry paid once.
#
# The cut is `recommend=False` on the panellift subprocess, which is 93% of a
# run. It is only allowed because NOTHING ON THIS SIDE READS WHAT IT DROPS:
# `_findings_from_summary` takes the top-level verdict and each stage's
# severity/code/message, all of which panellift builds from `cases` before the
# flag is consulted. The tests below pin both halves of that -- the flag
# really reaches the subprocess, and the consumed set really is
# recommendation-free -- because a cut justified by an invariant nobody
# asserts is a cut that quietly stops being safe.
def _summary(recommendation):
return {
'verdict': 'WARN',
'stages': [{'name': 'STRIP', 'findings': [
{'severity': 'WARN', 'code': 'TENSION',
'message': 'peak tension 512 psi against 480 allowable'}]}],
'recommendation': recommendation,
}
_SEARCHED = {'searched': True, 'worth_moving': True,
'rows': [{'x_ft': 4.0, 'count': 2}],
'moves': [{'mark': 'L1', 'dx_in': 6.0, 'moved': True}],
'as_built_ratio': 1.07, 'recommended_ratio': 0.91}
_NOT_SEARCHED = {'searched': False, 'worth_moving': None, 'rows': [],
'moves': [], 'as_built_ratio': None,
'recommended_ratio': None}
def test_63_the_ticket_path_asks_panellift_not_to_search():
"""The flag has to reach the subprocess. A flag that silently does not is
indistinguishable from a ticket run that is still 41 seconds."""
from vibe import liftexport
seen = {}
def _fake_run(python_exe, job_path, out_path, **kw):
seen.update(kw)
seen['out_path'] = out_path
return 0, _summary(_NOT_SEARCHED), ''
real = liftexport.run
liftexport.run = _fake_run
try:
summary, stderr = TC._run_panellift_json('py.exe', 'j.json', 'WP396')
finally:
liftexport.run = real
assert seen.get('recommend') is False
assert summary['verdict'] == 'WARN'
# Still a throwaway report path, still not beside a reviewed one.
assert 'willtools_ticketqc' in seen['out_path']
def test_63_the_findings_do_not_change_when_the_search_is_off():
"""The whole justification for the cut, as an assertion. If a future
panellift ever lets `--no-recommend` move a verdict or drop a finding,
this is the test that says the ticket path may no longer force it."""
on = TC._findings_from_summary(_summary(_SEARCHED), 'WP396',
TC._ONSPOT_TAG)
off = TC._findings_from_summary(_summary(_NOT_SEARCHED), 'WP396',
TC._ONSPOT_TAG)
assert [(f.severity, f.code, f.message) for f in on] == \
[(f.severity, f.code, f.message) for f in off]
assert len(on) == 1 and on[0].severity == 'WARN'
def test_63_nothing_on_this_side_reads_the_recommendation_block():
"""Belt and braces: a summary with NO recommendation key at all must
still produce the same findings, which is only true if the block is
never touched."""
bare = _summary(_SEARCHED)
del bare['recommendation']
assert [(f.severity, f.code, f.message)
for f in TC._findings_from_summary(bare, 'WP396', TC._ONSPOT_TAG)] \
== [(f.severity, f.code, f.message)
for f in TC._findings_from_summary(_summary(_SEARCHED), 'WP396',
TC._ONSPOT_TAG)]
def test_63_the_reader_is_told_the_positions_were_not_computed():
"""Ticket QC has no 'Recommended insert positions' heading to print NOT
COMPUTED under -- it never printed positions -- so the fact rides on the
provenance tag, which is the one thing a reader of a handling row sees."""
assert 'no position search' in TC._ONSPOT_TAG
findings = TC._findings_from_summary(_summary(_NOT_SEARCHED), 'WP396',
TC._ONSPOT_TAG)
assert 'no position search' in findings[0].message
# The reviewed tag is deliberately NOT changed: it points at a report that
# does carry its positions.
assert 'no position search' not in TC._REVIEWED_TAG
def test_63_the_tag_did_not_grow_the_pressure_on_the_note_cap():
"""`checklist_states` caps a row note at 180 characters and the tag is
spent before the finding's own message starts. The new tag says more in
fewer characters; if it ever grows past the old one, the row starts
truncating the actual problem instead."""
assert len(TC._ONSPOT_TAG) <= 106
assert 'good enough to find a problem, not good enough to sign' \
in TC._ONSPOT_TAG # the button's handling section quotes this
def test_63_a_swept_row_carries_the_same_provenance():
"""`_findings_from_problems_row` reuses _ONSPOT_TAG, so the cached sweep
path says the same thing as the on-the-spot one."""
row = {'verdict': 'OK', 'stages': [{'name': 'STRIP', 'findings': []}]}
findings = TC._findings_from_problems_row(row, 'WP396')
assert len(findings) == 1
assert findings[0].severity == 'OK'
assert 'no position search' in findings[0].message
# ================================================================== 7.1
#
# The interference pair list, as a file. Owner, reading his WP296 run,
# 2026-08-30: "interference check 如果是 warning,那么 note 那边应该是一个
# 文件的链接,单击打开那个文件,写成 csv,里面包含干涉的 mark#,ID,X,mark#,ID
# 这样。带表头。"
#
# Everything below is pure: `clash_csv_rows` is handed the same dicts
# `member_boxes` builds, and no Revit name is touched -- which is why these
# run under the inert stub like the rest of this file.
#
# THE MARKS IN THESE TESTS ARE SYNTHETIC. The real ones are the client's and
# live in the CSV beside the model, never in this repo.
_A = {'id': 111, 'mark': 'AA111', 'family': 'BAR_(SCHEDULED)'}
_B = {'id': 222, 'mark': 'BB222', 'family': 'ANCHOR'}
_NOMARK = {'id': 333, 'mark': None, 'family': 'MESH'}
def test_71_the_five_columns_are_his_five_in_his_order():
"""MARK, ID, X, MARK, ID -- and nothing inserted between them."""
assert TC.CLASH_CSV_COLUMNS[:5] == ('MARK', 'ID', 'X', 'MARK', 'ID')
def test_71_the_middle_column_holds_a_literal_x():
"""`X` is the versus between the two sides, not data about the clash."""
row = TC.clash_csv_rows([(0.15, 0.08, _A, _B)])[0]
assert row[2] == 'x'
assert TC.CLASH_CSV_X == 'x'
def test_71_a_pair_becomes_one_row_of_mark_id_x_mark_id():
row = TC.clash_csv_rows([(0.15, 0.08, _A, _B)])[0]
assert row[:5] == ['AA111', '111', 'x', 'BB222', '222']
def test_71_what_the_check_also_knows_comes_after_his_five():
row = TC.clash_csv_rows([(0.15, 0.08, _A, _B)])[0]
assert row[5:] == ['BAR_(SCHEDULED)', 'ANCHOR']
assert TC.CLASH_CSV_COLUMNS[5:] == ('FAMILY_1', 'FAMILY_2')
def test_71_the_two_measurement_columns_are_gone_from_the_file():
"""Owner, 2026-08-30: keep THICKNESS_IN / VOLUME_IN3 only if they can
ship hidden by default in Excel, otherwise turn them off. A .csv is
plain text and has no column formatting, so they are off.
Seven columns, and the numbers still arrive in `hits` and are still
dropped -- a row that grew back a tenth cell would be this test.
"""
assert len(TC.CLASH_CSV_COLUMNS) == 7
for gone in ('THICKNESS_IN', 'VOLUME_IN3'):
assert gone not in TC.CLASH_CSV_COLUMNS
assert len(TC.clash_csv_rows([(0.15, 0.08, _A, _B)])[0]) == 7
def test_71_the_file_keeps_the_deepest_pair_first_now_that_no_column_says():
"""Order is the surviving trace of THICKNESS_IN. The caller sorts
deepest-first; this must not reshuffle it."""
deep = {'id': 1, 'mark': 'DEEP', 'family': 'F'}
shallow = {'id': 2, 'mark': 'SHALLOW', 'family': 'F'}
rows = TC.clash_csv_rows([(0.9, 0.5, deep, _B), (0.13, 0.01, shallow, _B)])
assert [r[0] for r in rows] == ['DEEP', 'SHALLOW']
# --------------- where the dropped thickness went: the note's depth band
def test_71_the_note_carries_the_depth_band_the_csv_no_longer_does():
"""THICKNESS_IN was the quantity the filter tests, so it was the
reader's only per-pair answer to "why is this pair here at all". It
cannot simply be deleted; it is a clause on the row that links to the
file."""
band = TC.clash_depth_band(
[(0.85, 0.4, _A, _B), (0.13, 0.01, _A, _B)], 0.125)
assert '0.13"-0.85"' in band
assert '0.125"' in band # the filter it cleared
assert 'deepest row first' in band # how to read the file
assert 'shared material' in band
def test_71_the_depth_band_stays_short_enough_to_read_folded():
"""Nothing truncates a note any more, so this is not about survival --
it is about the COLLAPSED height. 7.1's row folds to its item's two
lines, and a band that ran to a paragraph would put the interesting
half behind a click for no reason."""
band = TC.clash_depth_band(
[(12.345, 9.0, _A, _B), (0.125, 0.01, _A, _B)], 0.125)
assert '0.125"-12.345"' in band
assert len(band) < 100
# and the path still reaches the reader, outside the fold
note = ('35 interference(s) among the 301 member(s) of this piece\'s '
'own assembly; ' + band)
full = note + TP.NOTE_FILE_SEP + r'C:\jobs\a job\interference-PNL1.csv'
html = TP.checklist_table_html(
[('7.1', 'Interference check', 'B', 'WARN', full)], table_id='X')
assert 'wt-ck-note-path' in html
assert 'interference-PNL1.csv</a>' in html
def test_71_one_pair_reads_as_one_number_not_a_band_of_it_to_itself():
band = TC.clash_depth_band([(0.5, 0.1, _A, _B)], 0.125)
assert '0.5"' in band
assert '-' not in band.split(',')[0]
def test_71_an_inexact_run_says_box_overlap_and_never_shared_material():
"""`interference.exact` off measures a bounding box, and calling that
shared material claims a boolean nobody ran."""
band = TC.clash_depth_band([(0.5, None, _A, _B)], 0.125, exact=False)
assert 'box overlap' in band
assert 'shared material' not in band
def test_71_no_pairs_means_no_band_rather_than_an_empty_clause():
assert TC.clash_depth_band([], 0.125) == ''
def test_71_a_member_with_no_mark_says_so_rather_than_leaving_a_blank():
"""None is "there is no mark", not "the mark could not be read"."""
row = TC.clash_csv_rows([(0.2, 0.01, _NOMARK, _A)])[0]
assert row[0] == '-'
assert row[1] == '333'
def test_71_a_missing_volume_no_longer_has_a_cell_to_be_wrong_in():
"""This was `test_71_no_volume_is_empty_and_never_zero`: with
`interference.exact` off there is no volume, and a 0 in that cell would
have claimed the shared material was measured and found to be nothing.
The cell is gone, so the trap is gone with it -- and a row that grows it
back must not quietly write a zero, which the length assertion catches.
"""
row = TC.clash_csv_rows([(0.5, None, _A, _B)])[0]
assert len(row) == 7
assert '0' not in row[5:]
def test_71_every_pair_is_written_not_just_the_reported_ones():
"""The note's `interference.max_reported` limit is a NOTE limit.
The file is the place the pairs went to stop being truncated, so it
cannot inherit the cap that made them unreadable.
"""
hits = [(0.1 + i / 100.0, 0.01, _A, _B) for i in range(40)]
assert len(TC.clash_csv_rows(hits)) == 40
def test_71_the_csv_has_a_header_row():
""""带表头"."""
text = TC.csv_text(TC.CLASH_CSV_COLUMNS,
TC.clash_csv_rows([(0.15, 0.08, _A, _B)]))
first = text.split('\r\n')[0]
assert first == 'MARK,ID,X,MARK,ID,FAMILY_1,FAMILY_2'
def test_71_a_field_with_a_comma_is_quoted_and_a_quote_is_doubled():
odd = {'id': 9, 'mark': 'A,1', 'family': 'SAY "HI"'}
text = TC.csv_text(TC.CLASH_CSV_COLUMNS,
TC.clash_csv_rows([(0.15, 0.08, odd, _B)]))
body = text.split('\r\n')[1]
assert body.startswith('"A,1",9,x,BB222,222,"SAY ""HI"""')
def test_71_the_rows_end_in_crlf_for_a_spreadsheet():
text = TC.csv_text(('A',), [['1']])
assert text == 'A\r\n1\r\n'
def test_71_the_file_is_named_after_the_piece():
"""A run checks many pieces; one shared file would be whichever was
checked last."""
assert TC.clash_csv_name('PNL1') == 'interference-PNL1.csv'
assert TC.clash_csv_name('PNL 1/A') == 'interference-PNL 1_A.csv'
def test_71_the_file_is_written_where_the_qc_data_lives(tmp_path):
path = TC.write_clash_csv(str(tmp_path), 'PNL1',
TC.clash_csv_rows([(0.15, 0.08, _A, _B)]))
assert path == str(tmp_path / 'interference-PNL1.csv')
body = open(path, 'rb').read().decode('utf-8')
assert body.split('\r\n')[0].startswith('MARK,ID,X,MARK,ID')
assert 'AA111,111,x,BB222,222' in body
def test_71_a_now_clean_piece_does_not_keep_the_last_runs_list(tmp_path):
"""A stale file is a list of clashes that will be believed."""
TC.write_clash_csv(str(tmp_path), 'PNL1',
TC.clash_csv_rows([(0.15, 0.08, _A, _B)]))
assert TC.clear_clash_csv(str(tmp_path), 'PNL1') is None
assert not (tmp_path / 'interference-PNL1.csv').exists()
def test_71_clearing_a_file_that_was_never_written_is_silent(tmp_path):
assert TC.clear_clash_csv(str(tmp_path), 'PNL1') is None
def test_71_a_stale_file_that_survives_is_reported_back(tmp_path):
"""It is open in Excel, say. Swallowing that leaves the defect in place.
The path comes back so the note can name it; nothing else can tell.
"""
import os as _os
path = TC.write_clash_csv(str(tmp_path), 'PNL1', [])
real_remove = _os.remove
def _refuse(target):
raise OSError(13, 'in use')
_os.remove = _refuse
try:
assert TC.clear_clash_csv(str(tmp_path), 'PNL1') == path
finally:
_os.remove = real_remove
assert _os.path.exists(path)
def test_71_the_note_marker_is_the_one_the_page_splits_on():
"""The contract between the check that writes the path and the table
that turns it into a link -- one spelling, owned by the page."""
note = ('4 interference(s) among the 142 member(s) of this piece\'s own '
'assembly' + TP.NOTE_FILE_SEP + r'C:\jobs\x\interference-PNL1.csv')
head, sep, path = TP.note_parts(note)
assert sep == TP.NOTE_FILE_SEP
assert path == r'C:\jobs\x\interference-PNL1.csv'
assert '142 member(s) of this piece' in head
html = TP.checklist_table_html(
[('7.1', 'Interference check', 'B', 'WARN', note)], table_id='X')
assert 'href="file:///C:/jobs/x/interference-PNL1.csv"' in html
# ==================================================== 4.3 tolerances.it_lb
#
# The key was declared, documented and offered in the config form while the
# check compared kips against a hardcoded `0.1`. The literal is gone and the
# key is read; the owner set the key at 100 lb so that wiring it moves
# nothing today. Everything about the COMPARISON is in test_ticketpage.py --
# what is tested here is the resolution of the key and the silence it can
# now produce.
def test_43_with_nothing_declared_the_check_still_uses_100_lb():
"""The call-site fallback IS the deleted literal: 100 lb = 0.1 kips.
A knowledge layer that declares nothing must not change the check."""
assert TC._jacking_tolerance_lb({}) == 100.0
def test_43_a_layer_turns_the_knob_which_is_the_point_of_wiring_it():
know = _know()
know['tolerances']['it_lb'] = 250.0
assert TC._jacking_tolerance_lb(know) == 250.0
know['tolerances']['it_lb'] = 1
assert TC._jacking_tolerance_lb(know) == 1.0
def test_43_no_module_constant_shadows_the_key_any_more():
"""A constant sitting beside a knowledge key of the same meaning is how
`tolerances.it_lb` came to be declared and read by nothing. The only
100 left is the fallback INSIDE the getter, so there is nowhere for a
second source of truth to hide."""
import inspect
src = inspect.getsource(TC.check_jacking_force)
body = src.split('"""', 2)[-1] # past the docstring
body = '\n'.join(l.split('#')[0] for l in body.splitlines())
assert '0.1' not in body
for name in dir(TC):
if name.isupper() and 'TOL' in name and 'IT' in name:
raise AssertionError('a constant shadows the key: ' + name)
def test_43_a_tolerance_that_is_not_a_tension_is_could_not_judge():
"""Unusable is not 'fall back to a number nobody chose'."""
for bad in ('tight', -1, []):
know = _know()
know['tolerances']['it_lb'] = bad
assert TC._jacking_tolerance_lb(know) is None, bad
reason = TC._SKIP_PROBES['check_jacking_force']({}, {}, know, None)
assert reason and 'tolerances.it_lb' in reason
def test_43_the_check_stands_down_rather_than_guessing_a_tolerance():
know = _know()
know['tolerances']['it_lb'] = 'tight'
t = {}
assert TC.check_jacking_force(t, {}, know, None) == []
assert 'tolerances.it_lb' in t['_jack_force_skip']
rows = dict((r[0], r) for r in TP.checklist_states(
[], [('check_jacking_force', 'SKIPPED', t['_jack_force_skip'])]))
assert rows['4.3'][3] == 'NOT RUN'
assert 'tolerances.it_lb' in rows['4.3'][4]
# ======================== 5.1's detail-sheet half: detail_sheet_prefix
#
# The key is read now. The half it belongs to is NOT built -- nothing reads
# what a detail sheet draws -- and the check says so instead of passing.
def test_51_the_detail_half_is_registered_and_declares_a_probe():
assert 'check_detail_callouts' in [c.__name__ for c in TC.CHECKS]
assert 'check_detail_callouts' in TC._SKIP_PROBES
def test_51_the_probe_never_returns_none_on_a_silence():
assert TC._SKIP_PROBES['check_detail_callouts'](
{}, {}, _know(), None) is not None
def test_51_an_undeclared_prefix_stands_the_half_down_by_name():
"""`knowledge.DEFAULTS` declares no detail_sheet_prefix -- the plant
layer does -- so the stock configuration reads NOT RUN, naming the key
a plant has to write down."""
t = {}
assert TC.check_detail_callouts(t, {}, _know(), None) == []
reason = TC._SKIP_PROBES['check_detail_callouts'](t, {}, _know(), None)
assert 'nomenclature.detail_sheet_prefix' in reason
def _know_with_prefix(bed_text):
"""A plant that names its detail series, and a BED PLAN that says
`bed_text`. The view lookup is the only Revit-touching part of the
path, so it is the only thing replaced."""
know = _know()
know.setdefault('nomenclature', {})['detail_sheet_prefix'] = 'X'
real = TC._bed_plan_text
TC._bed_plan_text = lambda t, k: bed_text
return know, real
def test_51_a_plan_that_cites_a_detail_is_not_run_and_names_the_sheet():
know, real = _know_with_prefix('P106 (S.I.F.)\n06/X1')
try:
t = {}
assert TC.check_detail_callouts(t, {}, know, None) == []
reason = TC._SKIP_PROBES['check_detail_callouts'](t, {}, know, None)
finally:
TC._bed_plan_text = real
assert '06/X1' in reason and 'X1' in reason
rows = dict((r[0], r) for r in TP.checklist_states(
[TC.Finding('OK', 'BOM-MEMBERS', '9 named BOM item(s) match')],
[('check_bom_members', 'RAN', None),
('check_detail_callouts', 'SKIPPED', reason)]))
# THE POINT OF THE WHOLE ROW: the BOM half passed and 5.1 is still not
# answered, because the half that could have failed it was never asked.
assert rows['5.1'][3] == 'NOT RUN'
assert 'X1' in rows['5.1'][4]
def test_51_a_plan_that_cites_no_detail_is_a_pass_and_the_row_goes_green():
know, real = _know_with_prefix('P106 (S.I.F.)(E.S.)\nSR309 (E.L.)')
try:
t = {}
out = TC.check_detail_callouts(t, {}, know, None)
finally:
TC._bed_plan_text = real
assert [f.severity for f in out] == ['OK']
assert [f.code for f in out] == ['DETAIL-CALLOUT']
rows = dict((r[0], r) for r in TP.checklist_states(
list(out) + [TC.Finding('OK', 'BOM-MEMBERS', 'match')],
[('check_bom_members', 'RAN', None),
('check_detail_callouts', 'RAN', None)]))
assert rows['5.1'][3] == 'OK'
# ------------------------------------------------- 6.2, the per-part minimum
#
# The catalogue publishes a minimum edge distance PER PART; the plant-wide
# `lifters.min_edge_distance_in` is one number for everything. 6.2 used to
# read only the second, so it disagreed with Panel Lift Check -- which has
# always gone through `knowledge.lifter_edge_distance` -- about the same
# insert on the same model.
#
# EVERY PART NUMBER BELOW IS INVENTED. A fixture pinned to a real catalogue
# entry breaks the day a published rating is corrected, which is a change to
# product data and not to this decision.
_FAR_PART = 'INVENTED_FAR_ANCHOR' # publishes 14", above the default
_NEAR_PART = 'INVENTED_NEAR_ANCHOR' # publishes 6", below the default
_UNPUBLISHED_PART = 'INVENTED_NO_ENTRY' # in no catalogue, no alias
def _edge_know(plant_default=9):
"""A knowledge tree holding two invented parts and a plant-wide default."""
return {
'lifters': {
'min_edge_distance_in': plant_default,
'catalogue_alias': {_FAR_PART: 'INVENTED_FAR_ITEM',
_NEAR_PART: 'INVENTED_NEAR_ITEM'},
},
'catalogue': {
'inserts': {
'INVENTED_FAR_ITEM': {'min_edge_distance_in': 14},
'INVENTED_NEAR_ITEM': {'min_edge_distance_in': 6},
},
},
}
def _edge_ticket(family, gap_ft):
"""A 20 ft piece with one insert `gap_ft` in from the near end."""
return {'lifters': [{'family': family, 'local': (gap_ft, 0.0, 0.0)}],
'panel_extents': ((0.0, 0.0, 0.0), (20.0, 4.0, 0.5))}
def _edge_row(out, log_state='RAN', note=None):
rows = dict((r[0], r) for r in TP.checklist_states(
list(out), [('check_lifter_edge_distance', log_state, note)]))
return rows['6.2']
def test_62_a_part_whose_published_minimum_is_above_the_plant_default_fails():
"""10 inches from the end. The plant-wide 9" said OK; this part publishes
14", so it is short -- and short of a number that IS this part's own, so
the verdict is FAIL and not a hedge."""
know = _edge_know(plant_default=9)
out = TC.check_lifter_edge_distance(
_edge_ticket(_FAR_PART, 10.0 / 12.0), {}, know, None)
assert [f.code for f in out] == ['LIFT-EDGE']
assert out[0].severity == 'FAIL'
assert '14"' in out[0].message
assert 'published minimum' in out[0].message
assert _FAR_PART in out[0].message
assert _edge_row(out)[3] == 'FAIL'
def test_62_a_part_whose_published_minimum_is_below_the_plant_default_passes():
"""7 inches from the end. The plant-wide 9" flagged it; this part
publishes 6", so there is nothing to report and the row goes green."""
know = _edge_know(plant_default=9)
out = TC.check_lifter_edge_distance(
_edge_ticket(_NEAR_PART, 7.0 / 12.0), {}, know, None)
assert [f.severity for f in out] == ['OK']
assert '6"' in out[0].message
assert _edge_row(out)[3] == 'OK'
def test_62_an_unpublished_part_falls_back_and_says_it_leaned_on_the_default():
"""A part the catalogue does not publish is held to the plant-wide number
-- the same fallback `liftexport` uses -- and the finding says so, because
"9 inches" and "14 inches for this part" are different claims."""
know = _edge_know(plant_default=9)
out = TC.check_lifter_edge_distance(
_edge_ticket(_UNPUBLISHED_PART, 5.0 / 12.0), {}, know, None)
# Short of the fallback, so it is reported -- but as a WARN, because the
# limit may not belong to this insert. FAIL would assert that it does.
assert out[0].severity == 'WARN'
assert 'plant-wide default' in out[0].message
assert 'lifters.min_edge_distance_in' in out[0].message
assert _edge_row(out)[3] == 'WARN'
def test_62_an_unknown_part_with_no_plant_default_is_could_not_judge():
"""Not in the catalogue and no plant-wide number: nobody knows how far
this insert must sit from an end. That is NOT RUN with a reason, never a
pass -- and the reason names the part and the key, so it can be fixed."""
know = _edge_know(plant_default=None)
t = _edge_ticket(_UNPUBLISHED_PART, 1.0 / 12.0)
out = TC.check_lifter_edge_distance(t, {}, know, None)
assert out == []
reason = TC._SKIP_PROBES['check_lifter_edge_distance'](t, {}, know, None)
assert reason
assert _UNPUBLISHED_PART in reason
assert 'lifters.min_edge_distance_in' in reason
row = _edge_row(out, log_state='SKIPPED', note=reason)
assert row[3] == 'NOT RUN'
assert TP.state_color(row[3]) == 'red'
assert _UNPUBLISHED_PART in row[4]
def test_62_never_reports_a_pass_over_an_insert_it_could_not_judge():
"""One insert the catalogue publishes and one it does not, with no plant
default. The judged one is clear, and the row still may not go green: a
green row is a claim about every insert on the piece."""
know = _edge_know(plant_default=None)
t = {'lifters': [{'family': _NEAR_PART, 'local': (5.0, 0.0, 0.0)},
{'family': _UNPUBLISHED_PART, 'local': (9.0, 0.0, 0.0)}],
'panel_extents': ((0.0, 0.0, 0.0), (20.0, 4.0, 0.5))}
out = TC.check_lifter_edge_distance(t, {}, know, None)
assert [f.severity for f in out] == ['WARN']
assert 'NOT judged' in out[0].message
assert _UNPUBLISHED_PART in out[0].message
assert _edge_row(out)[3] == 'WARN'
def test_62_asks_the_catalogue_the_same_question_panel_lift_check_does():
"""The point of the fix: two outputs of one tool, one answer. This holds
`_edge_limit` to `liftexport`'s own resolution order rather than to a
hand-copied number, so the two cannot drift apart again."""
from vibe import knowledge
know = _edge_know(plant_default=9)
for family, expected in ((_FAR_PART, 14), (_NEAR_PART, 6)):
liftexport_would_use = (knowledge.lifter_edge_distance(know, family) or
knowledge.get(know,
'lifters.min_edge_distance_in'))
assert TC._edge_limit(know, family)[0] == liftexport_would_use
assert liftexport_would_use == expected
# And the fallback hop is the same hop, for a part with no entry.
assert TC._edge_limit(know, _UNPUBLISHED_PART) == (9, 'plant')
# ------------------------------------ 6.7, the minimum spacing between two
#
# ALP catalog p53, 02/2024, footnote: "Minimum anchor spacing is 2x the
# published edge distance". The owner handed over that page; its edge
# distances became 6.2 and this sentence on the same page never landed, so
# two 8T anchors 20 in apart passed every row on the board while the page
# asked for 28.
#
# EVERY PART NUMBER BELOW IS INVENTED, for `_edge_know`'s reason: a fixture
# pinned to a real catalogue entry breaks the day a published rating is
# corrected, which is a change to product data and not to this decision.
def _spacing_know(published=None, multiple=2.0, edge=14):
rule = {'source': 'an invented datasheet, p1, 01/2099'}
if multiple is not None:
rule['multiple_of_edge_distance'] = multiple
if published is not None:
rule['published_in'] = published
return {
'lifters': {
# A plant-wide edge distance, present ON PURPOSE: it must not
# become a spacing. 6.2 may lean on it; 6.7 may not, because
# doubling a general number produces a specific-looking claim
# about a part nobody measured.
'min_edge_distance_in': 9,
'catalogue_alias': {_FAR_PART: 'INVENTED_FAR_ITEM',
_NEAR_PART: 'INVENTED_NEAR_ITEM'},
},
'catalogue': {
'inserts': {
'INVENTED_FAR_ITEM': {'min_edge_distance_in': edge,
'min_spacing_rule': rule},
'INVENTED_NEAR_ITEM': {'min_edge_distance_in': 6,
'min_spacing_rule': dict(rule)},
},
},
}
def _spacing_ticket(pairs):
"""A 20 ft x 4 ft x 6 in piece carrying inserts at (family, x_ft)."""
return {'lifters': [{'mark': 'L{0}'.format(i), 'family': fam,
'local': (x, 2.0, 0.25)}
for i, (fam, x) in enumerate(pairs)],
'panel_extents': ((0.0, 0.0, 0.0), (20.0, 4.0, 0.5)),
'thickness': 0.5}
def _spacing_row(out, log_state='RAN', note=None):
rows = dict((r[0], r) for r in TP.checklist_states(
list(out), [('check_lifter_anchor_spacing', log_state, note)]))
return rows['6.7']
def test_67_two_anchors_closer_than_twice_the_edge_distance_fail():
"""The case the page names and nothing measured: 20 in apart on a part
publishing 14 in edge distance, so the minimum is 28 in."""
know = _spacing_know(edge=14)
out = TC.check_lifter_anchor_spacing(
_spacing_ticket([(_FAR_PART, 0.0), (_FAR_PART, 20.0 / 12.0)]),
{}, know, None)
assert [f.code for f in out] == ['LIFT-SPACING']
assert out[0].severity == 'FAIL'
assert '20"' in out[0].message # what was measured
assert '28"' in out[0].message # what the page requires
assert '2x' in out[0].message and '14"' in out[0].message
assert 'p1' in out[0].message # the source, named
assert _spacing_row(out)[3] == 'FAIL'
def test_67_the_same_pair_one_inch_further_apart_passes():
know = _spacing_know(edge=14)
out = TC.check_lifter_anchor_spacing(
_spacing_ticket([(_FAR_PART, 0.0), (_FAR_PART, 29.0 / 12.0)]),
{}, know, None)
assert [f.severity for f in out] == ['OK']
assert '28"' in out[0].message
assert _spacing_row(out)[3] == 'OK'
def test_67_a_mixed_pair_is_held_to_the_LARGER_of_the_two_minimums():
"""A 14 in anchor beside a 6 in one does not get the 6 in rule. Taking
the smaller passes exactly the pair this check exists for."""
know = _spacing_know(edge=14)
out = TC.check_lifter_anchor_spacing(
_spacing_ticket([(_FAR_PART, 0.0), (_NEAR_PART, 20.0 / 12.0)]),
{}, know, None)
assert out[0].severity == 'FAIL'
assert '28"' in out[0].message # 2 x 14, not 2 x 6
assert '12"' not in out[0].message
def test_67_a_published_spacing_overrides_the_multiple():
"""A sheet that states a spacing outright beats a rule of thumb about
the same part -- and the arithmetic disappears from the message with it,
because there is none to show."""
know = _spacing_know(published=18, edge=14)
out = TC.check_lifter_anchor_spacing(
_spacing_ticket([(_FAR_PART, 0.0), (_FAR_PART, 20.0 / 12.0)]),
{}, know, None)
assert [f.severity for f in out] == ['OK'] # 20" clears 18"
assert '18"' in out[0].message
assert '2x' not in out[0].message
def test_67_an_unknown_part_is_could_not_judge_and_never_a_pass():
"""The honesty state. An insert the catalogue does not cover has no
spacing, and the plant-wide edge distance in the fixture must not be
doubled into one."""
know = _spacing_know()
t = _spacing_ticket([(_UNPUBLISHED_PART, 0.0), (_UNPUBLISHED_PART, 0.5)])
out = TC.check_lifter_anchor_spacing(t, {}, know, None)
assert out == []
reason = TC._SKIP_PROBES['check_lifter_anchor_spacing'](t, {}, know, None)
assert reason
assert _UNPUBLISHED_PART in reason
row = _spacing_row(out, log_state='SKIPPED', note=reason)
assert row[3] == 'NOT RUN'
assert TP.state_color(row[3]) == 'red'
def test_67_a_part_with_no_published_edge_distance_stands_down_naming_it():
""""2x the published edge distance" is not a number when nothing is
published to double, and the sentence says exactly that."""
know = _spacing_know(edge=None)
know['catalogue']['inserts']['INVENTED_FAR_ITEM'].pop(
'min_edge_distance_in')
t = _spacing_ticket([(_FAR_PART, 0.0), (_FAR_PART, 0.5)])
out = TC.check_lifter_anchor_spacing(t, {}, know, None)
assert out == []
reason = TC._SKIP_PROBES['check_lifter_anchor_spacing'](t, {}, know, None)
assert 'nothing to double' in reason
assert _spacing_row(out, log_state='SKIPPED', note=reason)[3] == 'NOT RUN'
def test_67_never_reports_a_pass_over_a_pair_it_could_not_judge():
"""A judged pair that is clear plus an insert nobody can resolve. The
row may not go green: green is a claim about every anchor on the piece.
"""
know = _spacing_know(edge=14)
t = _spacing_ticket([(_FAR_PART, 0.0), (_FAR_PART, 3.0),
(_UNPUBLISHED_PART, 6.0)])
out = TC.check_lifter_anchor_spacing(t, {}, know, None)
assert [f.code for f in out] == ['LIFT-SPACING']
assert out[0].severity == 'WARN'
assert 'Not every insert could be judged' in out[0].message
assert _UNPUBLISHED_PART in out[0].message
assert _spacing_row(out)[3] == 'WARN'
def test_67_one_insert_is_no_pair_and_says_so():
know = _spacing_know()
t = _spacing_ticket([(_FAR_PART, 0.0)])
assert TC.check_lifter_anchor_spacing(t, {}, know, None) == []
reason = TC._SKIP_PROBES['check_lifter_anchor_spacing'](t, {}, know, None)
assert 'no pair' in reason
def test_67_measures_in_the_plane_and_ignores_the_thickness_axis():
"""Two anchors at the same in-plane point in different wythes are 0 in
apart for this rule, not half a panel thickness apart. In-plane is the
smaller distance, which is the conservative read, and it is what a
spacing rule about overlapping concrete cones means."""
know = _spacing_know(edge=14)
t = _spacing_ticket([(_FAR_PART, 0.0), (_FAR_PART, 0.0)])
t['lifters'][1]['local'] = (0.0, 2.0, 0.45) # 2.4" away in thickness
out = TC.check_lifter_anchor_spacing(t, {}, know, None)
assert out[0].severity == 'FAIL'
assert '0"' in out[0].message
# ...and the axis was DERIVED, not assumed to be index 2.
assert TC._in_plane_axes(t) == [0, 1]
tall = dict(t, panel_extents=((0.0, 0.0, 0.0), (0.5, 4.0, 20.0)),
thickness=0.5)
assert TC._in_plane_axes(tall) == [1, 2]
def test_67_unreadable_geometry_is_a_warn_and_not_a_silence():
know = _spacing_know()
t = _spacing_ticket([(_FAR_PART, 0.0), (_FAR_PART, 0.5)])
t['panel_extents'] = None
out = TC.check_lifter_anchor_spacing(t, {}, know, None)
assert [f.severity for f in out] == ['WARN']
assert 'not measured' in out[0].message
# ------------------------------------- 6.9, lifting-point count vs length
#
# MC PLANT RULE, owner verbatim 2026-09-02 (see ticketchecks.py above
# `check_lifting_points` for the full quote and the arithmetic). The plant
# id, family name and every dimension in these fixtures are invented --
# `sample_plant` shape, never the real plant name -- because the point of
# the test is the ARITHMETIC (count vs length), not any real job.
_MC_LIFT_KNOW = {
'lifters': {
'lifting_points': {
'two_point_max_length_ft': 20,
'two_point_absolute_max_ft': 25,
},
},
}
def _points_ticket(length_ft, xs, family='LIFT_SAMPLE_8T'):
"""A `length_ft` x 4 ft x 6 in panel carrying one lifter at each x in
`xs` (feet from the panel origin, along its own long axis)."""
return {
'lifters': [{'family': family, 'id': i, 'local': (x, 2.0, 0.25)}
for i, x in enumerate(xs)],
'panel_extents': ((0.0, 0.0, 0.0), (length_ft, 4.0, 0.5)),
'length': length_ft,
'thickness': 0.5,
}
def _points_row(out, log_state='RAN', note=None):
rows = dict((r[0], r) for r in TP.checklist_states(
list(out), [('check_lifting_points', log_state, note)]))
return rows['6.9']
def test_69_18ft_two_points_ok():
"""Under the 20 ft nominal limit -- 2-point is simply fine here."""
t = _points_ticket(18.0, [3.0, 15.0])
out = TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None)
assert [f.code for f in out] == ['LIFT-POINTS']
assert out[0].severity == 'OK'
assert '2 lifting point' in out[0].message
assert _points_row(out)[3] == 'OK'
def test_69_18ft_four_points_ok():
"""More points is never worse: still OK well under the nominal limit."""
t = _points_ticket(18.0, [2.0, 6.0, 12.0, 16.0])
out = TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None)
assert out[0].severity == 'OK'
assert '4 lifting point' in out[0].message
def test_69_22ft_two_points_warn():
"""Over the nominal 20 ft rule but inside the real 25 ft ceiling --
the grey zone the owner named explicitly ("你按照20-25给warn")."""
t = _points_ticket(22.0, [3.0, 19.0])
out = TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None)
assert out[0].severity == 'WARN'
assert '2 lifting point' in out[0].message
assert '20' in out[0].message and '25' in out[0].message
assert _points_row(out)[3] == 'WARN'
def test_69_22ft_four_points_ok():
t = _points_ticket(22.0, [2.0, 8.0, 14.0, 20.0])
out = TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None)
assert out[0].severity == 'OK'
def test_69_27ft_two_points_fail():
"""Past the real 25 ft ceiling -- 2-point no longer holds, per the
owner's "25以上给fail"."""
t = _points_ticket(27.0, [3.0, 24.0])
out = TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None)
assert out[0].severity == 'FAIL'
assert '2 lifting point' in out[0].message
assert '25' in out[0].message
assert _points_row(out)[3] == 'FAIL'
def test_69_27ft_four_points_ok():
"""4-point clears any length -- the rule is about too FEW points, never
too many."""
t = _points_ticket(27.0, [2.0, 9.0, 18.0, 25.0])
out = TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None)
assert out[0].severity == 'OK'
assert '4-point rule' in out[0].message
def test_69_boundary_20ft_exactly_is_still_the_two_point_zone():
""""小于20ft则2p" / "25以内都可以2p" pin the boundaries inclusive: at
exactly 20 ft, 2-point is still simply fine, not the grey zone."""
t = _points_ticket(20.0, [3.0, 17.0])
out = TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None)
assert out[0].severity == 'OK'
def test_69_boundary_25ft_exactly_is_the_warn_ceiling_not_fail():
"""At exactly 25 ft 2-point still just barely holds -- WARN, not FAIL."""
t = _points_ticket(25.0, [3.0, 22.0])
out = TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None)
assert out[0].severity == 'WARN'
def test_69_a_pair_of_anchors_at_one_station_is_one_point_not_two():
"""Two anchors a few inches apart are ONE pick point, not two -- the
whole reason this reuses `liftexport.rows_of` instead of counting
`len(t['lifters'])`. Proven the way that matters: with 4 modelled
anchors forming 2 real points on a 27 ft panel, counting anchors
would read 4-point and pass; counting points must read 2-point and
FAIL."""
t = _points_ticket(27.0, [2.0, 2.3, 24.0, 24.3])
assert len(t['lifters']) == 4
out = TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None)
assert out[0].severity == 'FAIL'
assert '2 lifting point' in out[0].message
def test_69_non_mc_layer_stands_down_naming_the_missing_rule():
"""A plant that has not declared MC's numbers never gets judged against
them -- any non-MC layer."""
t = _points_ticket(27.0, [3.0, 24.0])
know = {}
assert TC.check_lifting_points(t, {}, know, None) == []
reason = TC._SKIP_PROBES['check_lifting_points'](t, {}, know, None)
assert reason
assert 'two_point_max_length_ft' in reason
assert 'two_point_absolute_max_ft' in reason
row = _points_row([], log_state='SKIPPED', note=reason)
assert row[3] == 'NOT RUN'
assert TP.state_color(row[3]) == 'red'
def test_69_length_unreadable_is_a_warn_finding():
t = _points_ticket(27.0, [3.0, 24.0])
t['length'] = None
out = TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None)
assert [f.code for f in out] == ['LIFT-POINTS-UNREAD']
assert out[0].severity == 'WARN'
assert 'length' in out[0].message
assert _points_row(out)[3] == 'WARN'
def test_69_zero_lifters_defers_to_61_and_never_passes():
"""No recognised lifter at all is 6.1's (LIFT-CAPACITY) territory --
this check stands down silently and its OWN probe points at 6.1 by
name rather than re-deriving the families.lifter_include story."""
t = {'lifters': [], 'panel_extents': ((0.0, 0.0, 0.0), (20.0, 4.0, 0.5)),
'length': 20.0}
assert TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None) == []
reason = TC._SKIP_PROBES['check_lifting_points'](t, {}, _MC_LIFT_KNOW, None)
assert reason
assert '6.1' in reason
row = _points_row([], log_state='SKIPPED', note=reason)
assert row[3] == 'NOT RUN'
def test_69_lifters_with_no_readable_position_is_unread_not_a_pass():
"""Recognised lifters exist, but none carry a resolved local position --
the panel frame could not be solved. A could-not-judge, not a pass, and
it is named on the row rather than left for the probe."""
t = _points_ticket(27.0, [3.0, 24.0])
for lift in t['lifters']:
lift['local'] = None
out = TC.check_lifting_points(t, {}, _MC_LIFT_KNOW, None)
assert [f.code for f in out] == ['LIFT-POINTS-UNREAD']
assert out[0].severity == 'WARN'
assert 'position' in out[0].message
def test_69_a_bad_threshold_value_is_treated_as_not_set():
"""A non-numeric threshold must not raise into CHECK-ERROR -- it stands
down the same as an absent one."""
t = _points_ticket(27.0, [3.0, 24.0])
know = {'lifters': {'lifting_points': {
'two_point_max_length_ft': 'not-a-number',
'two_point_absolute_max_ft': 25}}}
assert TC.check_lifting_points(t, {}, know, None) == []
# ------------------------------------- 6.10, lift-point POSITION vs PCI
#
# MC PLANT RULE, owner verbatim 2026-09-02 (see ticketchecks.py above
# `check_lift_positions` for the full quote). DISTINCT from 6.9's tests
# above: this is never about how many points there are, only where they
# sit. `_points_ticket` is reused unchanged -- same fixture shape, same
# invented `sample_plant`-grade family name and dimensions, nothing real.
_MC_POS_KNOW = {
'lifters': {
'lifting_points': {
'pci_position_tolerance_in': 6,
},
},
}
def _pci_stations(length_ft):
"""The same PCI two-point stations the check itself computes, via the
check module's own constant -- so a test fixture can never quietly
drift from what the check considers "on PCI"."""
near = TC.PCI_TWO_POINT_FRACTION * length_ft
return near, length_ft - near
def test_610_2point_both_on_pci_ok():
length = 20.0
s1, s2 = _pci_stations(length)
t = _points_ticket(length, [s1, s2])
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert [f.code for f in out] == ['LIFT-POS']
assert out[0].severity == 'OK'
def test_610_2point_one_point_8in_off_warns_the_other_stays_ok():
"""One point 8" off PCI warns and names it; the other, exactly on PCI,
reads OK in the same finding."""
length = 20.0
s1, s2 = _pci_stations(length)
t = _points_ticket(length, [s1 + 8.0 / 12.0, s2])
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert [f.code for f in out] == ['LIFT-POS']
assert out[0].severity == 'WARN'
msg = out[0].message
assert 'point 1' in msg and 'WARN' in msg
assert 'point 2' in msg and 'OK' in msg
assert '8' in msg
def test_610_boundary_exactly_6in_off_is_ok():
""""相差大于6\"" is strictly greater-than -- exactly 6" off passes."""
length = 20.0
s1, s2 = _pci_stations(length)
t = _points_ticket(length, [s1 + 0.5, s2]) # 0.5 ft == 6.0 in, exact
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert out[0].severity == 'OK'
def test_610_boundary_6_01in_off_warns():
length = 20.0
s1, s2 = _pci_stations(length)
t = _points_ticket(length, [s1 + 6.01 / 12.0, s2])
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert out[0].severity == 'WARN'
def _pci_four_stations(length_ft):
"""The same PCI four-point stations the check itself computes, via the
check module's own constant -- so a test fixture can never quietly
drift from what the check considers "on PCI"."""
return [f * length_ft for f in TC.PCI_FOUR_POINT_FRACTIONS]
def test_610_4point_all_on_pci_ok():
"""Owner-confirmed four-point stations (0.104L/0.395L/0.605L/0.896L,
2026-09-02) are now wired in -- a 4-point pick sitting exactly on all
four is judged OK, not stood down."""
length = 40.0
s1, s2, s3, s4 = _pci_four_stations(length)
t = _points_ticket(length, [s1, s2, s3, s4])
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert [f.code for f in out] == ['LIFT-POS']
assert out[0].severity == 'OK'
def test_610_4point_one_point_8in_off_warns_the_others_stay_ok():
"""One of the four points 8" off PCI warns and names it; the other
three, exactly on PCI, read OK in the same finding."""
length = 40.0
s1, s2, s3, s4 = _pci_four_stations(length)
t = _points_ticket(length, [s1, s2 + 8.0 / 12.0, s3, s4])
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert [f.code for f in out] == ['LIFT-POS']
assert out[0].severity == 'WARN'
msg = out[0].message
assert 'point 1' in msg and 'point 3' in msg and 'point 4' in msg
assert 'point 2' in msg and 'WARN' in msg
assert '8' in msg
def test_610_4point_boundary_exactly_6in_off_is_ok():
"""Same strictly-greater-than-6" boundary as the 2-point case."""
length = 40.0
s1, s2, s3, s4 = _pci_four_stations(length)
t = _points_ticket(length, [s1 + 0.5, s2, s3, s4]) # 0.5 ft == 6.0 in
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert out[0].severity == 'OK'
def test_610_4point_boundary_6_01in_off_warns():
length = 40.0
s1, s2, s3, s4 = _pci_four_stations(length)
t = _points_ticket(length, [s1 + 6.01 / 12.0, s2, s3, s4])
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert out[0].severity == 'WARN'
def test_610_2point_still_uses_0207_0793_unchanged():
"""The 4-point branch landing must not perturb the 2-point case --
still the 0.207L/0.793L fractions, named verbatim in the finding."""
length = 20.0
s1, s2 = _pci_stations(length)
t = _points_ticket(length, [s1, s2])
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert out[0].severity == 'OK'
assert '0.207L' in out[0].message
assert '0.793L' in out[0].message
def test_610_3point_stands_down_not_configured():
"""Any count other than 2 or 4 (e.g. 3) is still not known -- a
could-not-judge, never a pass and never a fail."""
length = 20.0
t = _points_ticket(length, [2.0, 10.0, 18.0])
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert [f.code for f in out] == ['LIFT-POS-UNREAD']
assert out[0].severity == 'WARN'
assert '3-point' in out[0].message
assert 'not configured' in out[0].message
def test_610_non_mc_layer_stands_down_naming_the_missing_key():
length = 20.0
s1, s2 = _pci_stations(length)
t = _points_ticket(length, [s1, s2])
know = {}
assert TC.check_lift_positions(t, {}, know, None) == []
reason = TC._SKIP_PROBES['check_lift_positions'](t, {}, know, None)
assert reason
assert 'pci_position_tolerance_in' in reason
def test_610_length_unreadable_is_a_warn_finding():
length = 20.0
s1, s2 = _pci_stations(length)
t = _points_ticket(length, [s1, s2])
t['length'] = None
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert [f.code for f in out] == ['LIFT-POS-UNREAD']
assert out[0].severity == 'WARN'
assert 'length' in out[0].message
def test_610_zero_lifters_defers_to_61_and_never_passes():
t = {'lifters': [], 'panel_extents': ((0.0, 0.0, 0.0), (20.0, 4.0, 0.5)),
'length': 20.0}
assert TC.check_lift_positions(t, {}, _MC_POS_KNOW, None) == []
reason = TC._SKIP_PROBES['check_lift_positions'](t, {}, _MC_POS_KNOW, None)
assert reason
assert '6.1' in reason
def test_610_lifters_with_no_readable_position_is_unread_not_a_pass():
length = 20.0
s1, s2 = _pci_stations(length)
t = _points_ticket(length, [s1, s2])
for lift in t['lifters']:
lift['local'] = None
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert [f.code for f in out] == ['LIFT-POS-UNREAD']
assert out[0].severity == 'WARN'
assert 'position' in out[0].message
def test_610_anchor_pair_at_one_station_is_one_point_on_pci():
"""Two anchors a few inches apart cluster into ONE pick point, same
grouping 6.9 relies on -- proven the way that matters: 4 modelled
anchors forming 2 real points, each pair straddling a PCI station,
must be judged as a 2-point lift (OK against PCI) rather than
stood down as an unconfigured 4-point lift."""
length = 20.0
s1, s2 = _pci_stations(length)
t = _points_ticket(length, [s1 - 0.1, s1 + 0.1, s2 - 0.1, s2 + 0.1])
assert len(t['lifters']) == 4
out = TC.check_lift_positions(t, {}, _MC_POS_KNOW, None)
assert [f.code for f in out] == ['LIFT-POS']
assert out[0].severity == 'OK'
# ------------------------------------- 6.11, dunnage-point count vs
# stripping-point count
#
# MC PLANT RULE, owner verbatim 2026-09-02 (see ticketchecks.py above
# `check_dunnage_stripping_match` for the full quotes and every design
# decision). `_points_ticket`'s shape is reused for the lifter/geometry
# half; the dunnage half goes through `_dunnage_station_reader`, which is
# monkeypatched directly here rather than standing 6.4's whole
# sheet-reading fixture chain back up a second time -- that reader's OWN
# correctness (sheet notes, printing order, "not a pass" silences) is 6.4's
# tests' job, proven above; this row's tests are the COMPARISON, not the
# read. `sample_plant`-grade family names throughout, same as 6.9/6.10.
_MC_DUN_STRIP_KNOW = {
'lifters': {
'lifting_points': {
'dunnage_matches_stripping': True,
},
},
}
def _dun_strip_ticket(length_ft, face_xs, other_xs=(),
face_family='LPA_FACE_4T', other_family='QL_TOP_8T'):
"""`_points_ticket`'s shape plus a second (default non-face) family of
lifters and the bits `check_dunnage_stripping_match` needs to reach for
the dunnage stations. `face_family` starts with LPA on purpose -- the
fallback identification -- so most tests below never need a
`usage`-tagged knowledge fixture. `doc` is any non-None placeholder:
`_dunnage_station_reader` is monkeypatched in these tests and never
actually dereferences it."""
lifters = [{'family': face_family, 'id': 'F{0}'.format(i),
'local': (x, 2.0, 0.25)} for i, x in enumerate(face_xs)]
lifters += [{'family': other_family, 'id': 'O{0}'.format(i),
'local': (x, 2.0, 0.25)} for i, x in enumerate(other_xs)]
return {
'lifters': lifters,
'panel_extents': ((0.0, 0.0, 0.0), (length_ft, 4.0, 0.5)),
'length': length_ft,
'thickness': 0.5,
'panel_id': 1,
'doc': object(),
}
def _patch_dun_stations(monkeypatch, stations, reason=None):
monkeypatch.setattr(TC, '_dunnage_station_reader',
lambda t, tdoc: (stations, reason))
def _dun_strip_row(out, log_state='RAN', note=None):
rows = dict((r[0], r) for r in TP.checklist_states(
list(out), [('check_dunnage_stripping_match', log_state, note)]))
return rows['6.11']
def test_611_usage_tag_identifies_a_non_lpa_family_as_face():
"""The primary identification -- `usage: back_stripping`, read through
the same resolver `lifter_capacity()` uses -- works on a family that
does not even start with LPA."""
know = {'lifters': {'capacity_lb': {
'QL846': {'usage': 'back_stripping', 'tension_lb': 3080}}}}
assert TC._is_face_stripping_lifter(know, 'QL846') is True
def test_611_usage_tag_overrides_the_lpa_prefix_fallback():
"""An LPA-prefixed family explicitly tagged something OTHER than
back_stripping is not a face lifter -- a stated usage tag is
authoritative and the bare-prefix fallback never overrides it."""
know = {'lifters': {'capacity_lb': {
'LPA_SPECIAL': {'usage': 'erection_stripping'}}}}
assert TC._is_face_stripping_lifter(know, 'LPA_SPECIAL') is False
def test_611_lpa_prefix_is_only_a_fallback_for_no_usage_tag_at_all():
know = {}
assert TC._is_face_stripping_lifter(know, 'LPA4T512_LIFTING PIN ANCHOR') \
is True
assert TC._is_face_stripping_lifter(know, 'LIFT_QL589G_8T') is False
def test_611_2v2_matches_is_ok(monkeypatch):
t = _dun_strip_ticket(27.0, [3.0, 24.0])
_patch_dun_stations(monkeypatch, [5.0, 17.0])
out = TC.check_dunnage_stripping_match(t, {}, _MC_DUN_STRIP_KNOW, None)
assert [f.code for f in out] == ['DUN-STRIP']
assert out[0].severity == 'OK'
assert '2 dunnage point' in out[0].message
assert '2 stripping' in out[0].message
assert _dun_strip_row(out)[3] == 'OK'
def test_611_4_face_2_dunnage_warns_naming_4_vs_2(monkeypatch):
t = _dun_strip_ticket(27.0, [2.0, 9.0, 18.0, 25.0])
_patch_dun_stations(monkeypatch, [5.0, 17.0])
out = TC.check_dunnage_stripping_match(t, {}, _MC_DUN_STRIP_KNOW, None)
assert [f.code for f in out] == ['DUN-STRIP']
assert out[0].severity == 'WARN'
assert '2 dunnage' in out[0].message and '4 stripping' in out[0].message
assert _dun_strip_row(out)[3] == 'WARN'
def test_611_2_face_4_dunnage_warns_naming_2_vs_4(monkeypatch):
t = _dun_strip_ticket(27.0, [3.0, 24.0])
_patch_dun_stations(monkeypatch, [2.0, 8.0, 18.0, 25.0])
out = TC.check_dunnage_stripping_match(t, {}, _MC_DUN_STRIP_KNOW, None)
assert out[0].severity == 'WARN'
assert '4 dunnage' in out[0].message and '2 stripping' in out[0].message
def test_611_mixed_only_face_lifters_count_toward_stripping(monkeypatch):
"""A top/side lifter sharing the ticket with two face lifters must not
be counted toward N_strip -- proven the way that matters: 3 real
points on the piece (2 face, 1 top/side) but the verdict must read as
2-vs-2, not 3-vs-2."""
t = _dun_strip_ticket(27.0, [3.0, 24.0], other_xs=[13.5])
_patch_dun_stations(monkeypatch, [5.0, 17.0])
out = TC.check_dunnage_stripping_match(t, {}, _MC_DUN_STRIP_KNOW, None)
assert out[0].severity == 'OK'
assert '2 stripping' in out[0].message
def test_611_a_pair_of_anchors_at_one_station_is_one_point(monkeypatch):
"""Same grouping fixture as 6.9/6.10: 4 modelled face anchors forming 2
real points must read as 2-point, not 4-point."""
t = _dun_strip_ticket(27.0, [2.0, 2.3, 24.0, 24.3])
assert len(t['lifters']) == 4
_patch_dun_stations(monkeypatch, [5.0, 17.0])
out = TC.check_dunnage_stripping_match(t, {}, _MC_DUN_STRIP_KNOW, None)
assert out[0].severity == 'OK'
assert '2 stripping' in out[0].message
def test_611_non_mc_layer_stands_down_naming_the_missing_key():
t = _dun_strip_ticket(27.0, [3.0, 24.0])
know = {}
assert TC.check_dunnage_stripping_match(t, {}, know, None) == []
reason = TC._SKIP_PROBES['check_dunnage_stripping_match'](
t, {}, know, None)
assert reason
assert 'dunnage_matches_stripping' in reason
row = _dun_strip_row([], log_state='SKIPPED', note=reason)
assert row[3] == 'NOT RUN'
assert TP.state_color(row[3]) == 'red'
def test_611_zero_lifters_defers_to_61_and_never_passes():
t = {'lifters': [], 'panel_extents': ((0.0, 0.0, 0.0), (20.0, 4.0, 0.5)),
'length': 20.0}
assert TC.check_dunnage_stripping_match(
t, {}, _MC_DUN_STRIP_KNOW, None) == []
reason = TC._SKIP_PROBES['check_dunnage_stripping_match'](
t, {}, _MC_DUN_STRIP_KNOW, None)
assert reason
assert '6.1' in reason
def test_611_zero_face_lifters_is_not_judged_not_a_pass(monkeypatch):
"""Only a top/side lifter on this piece -- no stripping-point count can
be established, and this must NOT read as a quiet 0-vs-0 pass."""
t = _dun_strip_ticket(27.0, [], other_xs=[3.0, 24.0])
_patch_dun_stations(monkeypatch, []) # must never even be reached
out = TC.check_dunnage_stripping_match(t, {}, _MC_DUN_STRIP_KNOW, None)
assert [f.code for f in out] == ['DUN-STRIP-UNREAD']
assert out[0].severity == 'WARN'
assert 'back_stripping' in out[0].message or 'face' in out[0].message
def test_611_lifters_with_no_readable_position_is_unread_not_a_pass():
t = _dun_strip_ticket(27.0, [3.0, 24.0])
for lift in t['lifters']:
lift['local'] = None
out = TC.check_dunnage_stripping_match(t, {}, _MC_DUN_STRIP_KNOW, None)
assert [f.code for f in out] == ['DUN-STRIP-UNREAD']
assert out[0].severity == 'WARN'
assert 'position' in out[0].message
def test_611_dunnage_unreadable_is_dun_strip_unread(monkeypatch):
t = _dun_strip_ticket(27.0, [3.0, 24.0])
_patch_dun_stations(monkeypatch, None,
reason='no size notes found on sheet 1')
out = TC.check_dunnage_stripping_match(t, {}, _MC_DUN_STRIP_KNOW, None)
assert [f.code for f in out] == ['DUN-STRIP-UNREAD']
assert out[0].severity == 'WARN'
assert 'no size notes found on sheet 1' in out[0].message
# ==================================================================
# 5.6 / 5.7: the corrected reinforcing rule, and the per-size stock
# ==================================================================
#
# Both of these were a day of knowledge work that landed and was never
# connected: `knowledge.reinforcing_verdict` and `knowledge.rebar_stock`
# existed, were tested, and had no production caller, while the superseded
# shapes kept running next door. Wired 2026-08-31.
def _rule_know(verified=True, tokens=('BAR',), listed=None):
"""A knowledge tree whose reinforcing RULE can run (or deliberately not).
The token and the parameter name are invented for the fixture. The real
ones are a plant fact and belong in the plant layer, never here.
"""
know = _know()
know.setdefault('parameters', {}).setdefault(
'member', {})['manufacture_component'] = 'INVENTED_COMPONENT_PARAM'
know.setdefault('drawings', {})['reinforcing_rule'] = {
'parameter': 'member.manufacture_component',
'value_contains': list(tokens),
'read_from': ['instance', 'type'],
'parameter_verified': verified,
}
know['drawings']['reinforcing_families'] = list(listed or [])
return know
def test_could_not_judge_no_longer_collapses_into_not_a_bar():
"""THE BUG. `_is_reinforcing` returned a bool, so a member nothing could
judge came back False -- indistinguishable from a member positively
identified as not reinforcing. `reinforcing_verdict`'s own docstring names
that collapse as how 5.6 and 5.7 went quiet."""
know = _rule_know()
# A member the rule can read and rejects: a real False.
assert TC._reinforcing_verdict('SOME_FAMILY', 'Specialty Equipment', know,
'A PLATE')[0] is False
# A member carrying no value at all: could not judge, and it must NOT be
# False.
verdict, why = TC._reinforcing_verdict('SOME_FAMILY',
'Specialty Equipment', know)
assert verdict is None
assert verdict is not False
assert 'not the same as it not being a bar' in why
def test_the_dormant_rule_stays_dormant_and_says_why():
"""The parameter's real name is verified but the key ships
`parameter_verified: false` pending the owner. Wiring the call must not
turn a pending question into a guess: every member comes back None, and
the reason is the one `reinforcing_mode` already writes."""
know = _rule_know(verified=False)
verdict, why = TC._reinforcing_verdict('SOME_FAMILY',
'Specialty Equipment', know,
'A BAR')
assert verdict is None, 'a dormant rule must not judge anything'
mode, mode_why = knowledge_module().reinforcing_mode(know)
assert mode is None
assert why == mode_why
def knowledge_module():
from vibe import knowledge
return knowledge
def test_revits_own_category_still_answers_first():
"""A recorded category is a fact in the model, not an inference from a
name, and it is the only live signal at a plant that has declared
nothing. An enumeration must not be able to turn a rebar-category member
into 'not a bar'."""
know = _rule_know(verified=False, listed=['SOMETHING_ELSE'])
verdict, why = TC._reinforcing_verdict('ANY', 'Structural Rebar', know)
assert verdict is True
assert 'category' in why
# ------------------------------------------------------------------
# `_reinforcing_values` must keep instance and type GENUINELY separate.
#
# `ticketqc.pstr`/`rstr` started unioning instance-then-type on 2026-09-02
# (a role mapped to a TYPE parameter used to read back `None` forever,
# silently). That union is correct for almost every caller, but
# `_reinforcing_values` reports its two return values downstream as which
# one WON ('source': 'instance' or 'type' in the finding text) and a
# plant's `read_from` states which one it trusts. These tests pin that
# `_reinforcing_values` uses `rstr_instance_only`/`rstr_instance_only(el.
# Symbol, ...)`, NOT the unioning `rstr`, so a type-only value can never
# surface as the instance value.
QC.StorageType.String = 'String'
class _RVParam(object):
def __init__(self, value):
self.StorageType = QC.StorageType.String
self._value = value
def AsString(self):
return self._value
def AsValueString(self):
return self._value
class _RVElement(object):
"""LookupParameter only -- no `GetTypeId`/`Document` -- because
`rstr_instance_only` never needs a type walk of its own; the caller
supplies `.Symbol` directly, the same way Revit does."""
def __init__(self, params=None, symbol=None):
self._params = params or {}
self.Symbol = symbol
def LookupParameter(self, name):
return self._params.get(name)
def test_reinforcing_values_type_only_value_reports_as_type_not_instance():
"""THE COLLISION. A value that exists ONLY on the type must come back as
`(None, value)`, never `(value, value)` or `(value, None)` -- either of
those would mean the unioning reader leaked the type's value into the
instance slot, which is the exact wrong-attribution bug (CLAUDE.md
Sec.1: an answer from the wrong request is indistinguishable from a
right one)."""
know = _rule_know()
symbol = _RVElement(params={'INVENTED_COMPONENT_PARAM': _RVParam('BAR')})
el = _RVElement(params={}, symbol=symbol)
instance_value, type_value = TC._reinforcing_values(el, know)
assert instance_value is None
assert type_value == 'BAR'
def test_reinforcing_values_instance_only_value_reports_as_instance_not_type():
"""The mirror case: instance-only must never leak into the type slot."""
know = _rule_know()
symbol = _RVElement(params={})
el = _RVElement(params={'INVENTED_COMPONENT_PARAM': _RVParam('BAR')},
symbol=symbol)
instance_value, type_value = TC._reinforcing_values(el, know)
assert instance_value == 'BAR'
assert type_value is None
def test_reinforcing_values_both_set_stay_genuinely_distinct():
"""Both carry a value, and they may differ -- proving neither read
quietly copied the other's answer."""
know = _rule_know()
symbol = _RVElement(params={'INVENTED_COMPONENT_PARAM': _RVParam('TYPE-BAR')})
el = _RVElement(params={'INVENTED_COMPONENT_PARAM': _RVParam('INST-BAR')},
symbol=symbol)
instance_value, type_value = TC._reinforcing_values(el, know)
assert instance_value == 'INST-BAR'
assert type_value == 'TYPE-BAR'
def test_a_plant_preferring_instance_is_not_satisfied_by_a_type_only_value():
"""`read_from` is the plant's promise about WHICH field it trusts. A
plant that names only 'instance' must get `None` (could not judge) for
a member whose value sits solely on the type -- not the type's value
served up under the instance's name."""
know = _rule_know()
know['drawings']['reinforcing_rule']['read_from'] = ['instance']
verdict, why = TC._reinforcing_verdict(
'SOME_FAMILY', 'Specialty Equipment', know,
instance_value=None, type_value='BAR')
assert verdict is None
assert 'not the same as it not being a bar' in why
def test_5657_say_how_many_members_they_could_not_judge_and_why():
"""The `None`s must reach `_no_bars_reason` -- it is the only place that
can tell 'this piece carries no steel' from 'nothing here could recognise
any'. A boolean filter threw them away before they got there."""
unknown = [{'family': 'INVENTED_FAM_A', 'why': 'no value for the role'},
{'family': 'INVENTED_FAM_A', 'why': 'no value for the role'},
{'family': 'INVENTED_FAM_B', 'why': 'no value for the role'}]
t, know = _know_with_bars([], unknown=unknown)
TC.check_bar_stock_length(t, {}, know, None)
reason = t['_bar_length_silence']
assert 'COULD NOT BE JUDGED' in reason
assert '3 member(s)' in reason
assert 'INVENTED_FAM_A' in reason
assert 'INVENTED_FAM_B' in reason
assert 'not counted as "not a bar"' in reason
def test_5657_print_the_reason_the_rule_is_dormant():
"""`reinforcing_mode`'s sentence is the difference between 'nothing was
recognised' and 'nothing was recognised BECAUSE the parameter behind the
rule has not been verified yet' -- only the second can be acted on."""
t, _ = _know_with_bars([])
know = _rule_know(verified=False)
know['catalogue']['rebar']['stock_length_ft'] = 40.0
know.setdefault('parameters', {}).setdefault(
'member', {})['length'] = 'BAR_LENGTH_PARAM'
TC.check_bar_mark_uniqueness(t, {}, know, None)
reason = t['_barmark_silence']
assert 'HOW THE TOOL IS DECIDING' in reason
_mode, mode_why = knowledge_module().reinforcing_mode(know)
assert mode_why in reason
def test_the_bar_gather_is_cached_per_checklist_row():
"""5.6 and 5.7 may legitimately see different populations once a per-row
scope is declared, so one cache serving both would hand the second row
the first row's answer."""
t, know = _know_with_bars([{'mark': 'SR1', 'family': 'F',
'length_ft': 10.0}])
t['_bar_members:5.6'] = {'bars': [], 'reason': None, 'unknown': []}
bars_56, _r, _u = TC._bar_members(t, know, item='5.6')
bars_57, _r, _u = TC._bar_members(t, know, item='5.7')
assert bars_56 == []
assert [b['mark'] for b in bars_57] == ['SR1']
# ------------------------------------------------- 5.7, per-size stock
_SMALL_SIZE = '#4'
_BIG_SIZE = '#9'
def _size_know(bars, by_size=None, generic=40.0):
t, know = _know_with_bars(bars, stock=generic)
know['catalogue']['rebar']['stock_by_size'] = dict(by_size or {})
return t, know
def test_57_uses_the_per_size_table_the_moment_a_size_is_available():
"""THE FALSE OK the per-size table was built to fix, and which was still
live because nothing called `rebar_stock`: at a plant stocking #4 in 20 ft
lengths, a 30 ft #4 passed the generic 40 ft comparison."""
t, know = _size_know(
[{'mark': 'SR-SMALL', 'family': 'F', 'length_ft': 30.0,
'size': _SMALL_SIZE}],
by_size={_SMALL_SIZE: {'stock_length_in': 240}}) # 20 ft
findings = TC.check_bar_stock_length(t, {}, know, None)
codes = dict((f.code, f) for f in findings)
assert codes['BAR-LENGTH'].severity == 'WARN'
assert 'SR-SMALL' in codes['BAR-LENGTH'].message
# ... and against the generic 40 ft alone it would have passed.
t2, know2 = _size_know(
[{'mark': 'SR-SMALL', 'family': 'F', 'length_ft': 30.0,
'size': _SMALL_SIZE}], by_size={})
assert dict((f.code, f) for f in TC.check_bar_stock_length(
t2, {}, know2, None))['BAR-LENGTH'].severity == 'OK'
def test_57_a_bar_whose_size_cannot_be_read_is_not_judged_at_all():
"""Never a pass, and never silently judged against the generic number:
"40 ft" and "40 ft for this size" are different claims. Which parameter
carries a bar's size is question A3 with the owner."""
t, know = _size_know(
[{'mark': 'SR-NOSIZE', 'family': 'F', 'length_ft': 30.0,
'size': None}], by_size={_SMALL_SIZE: {'stock_length_in': 240}})
assert TC.check_bar_stock_length(t, {}, know, None) == []
reason = t['_bar_length_silence']
assert 'SR-NOSIZE' in reason
assert 'nothing was judged' in reason
rows = dict((r[0], r) for r in TP.checklist_states(
[], [('check_bar_stock_length', 'SKIPPED', reason)]))
assert rows['5.7'][3] == 'NOT RUN'
assert TP.state_color(rows['5.7'][3]) == 'red'
def test_57_with_neither_reader_available_it_names_both_of_them():
"""The old sentence said the size role was unmapped and sent the reader
to the owner's open question A3. A3 was answered by measurement on
2026-08-31 and the role is declared, so that sentence pointed at a closed
question -- worse than no message. What is true now is that there are TWO
readers over two disjoint populations, and only when BOTH are off can
nothing read a size on any bar."""
t, know = _size_know([{'mark': 'SR-NOSIZE', 'family': 'F',
'length_ft': 30.0, 'size': None}])
del know['parameters']['member']['bar_size']
assert TC.check_bar_stock_length(t, {}, know, None) == []
reason = t['_bar_length_silence']
assert 'member.bar_size' in reason
assert 'drawings.bar_size_rule.description_fallback' in reason
assert 'DISJOINT' in reason
assert 'A3' not in reason
def test_57_says_so_when_it_fell_back_to_the_generic_number():
"""A size the per-size table does not list is judged against the generic
length -- and the note has to say the number is not size specific."""
t, know = _size_know(
[{'mark': 'SR-BIG', 'family': 'F', 'length_ft': 10.0,
'size': _BIG_SIZE}],
by_size={_SMALL_SIZE: {'stock_length_in': 240}})
findings = TC.check_bar_stock_length(t, {}, know, None)
codes = dict((f.code, f) for f in findings)
assert codes['BAR-LENGTH'].severity == 'OK'
assert 'BAR-LENGTH-GENERIC' in codes
assert _BIG_SIZE in codes['BAR-LENGTH-GENERIC'].message
assert 'not size specific' in codes['BAR-LENGTH-GENERIC'].message
def test_57_names_the_length_per_mark_once_two_sizes_disagree():
"""One number in a sentence is easier to read than the same number
repeated -- but a sentence saying "the 40 ft stock length" while half the
bars were judged against 20 would be worse than no sentence."""
findings, _u, _z, _e = TP.bar_stock_length_status(
[{'mark': 'SR-A', 'family': 'F', 'length_ft': 30.0, 'stock_ft': 20.0},
{'mark': 'SR-B', 'family': 'F', 'length_ft': 45.0,
'stock_ft': 40.0}], 40.0)
assert findings[0][0] == 'WARN'
assert '20 ft stock' in findings[0][1]
assert '40 ft stock' in findings[0][1]
# With one length everywhere, the old single-number wording is kept.
same, _u, _z, _e = TP.bar_stock_length_status(
[{'mark': 'SR-A', 'family': 'F', 'length_ft': 45.0}], 40.0)
assert 'the 40 ft stock length' in same[0][1]
# ------------------------------- 5.7, the two size readers (2026-08-31)
#
# THE POPULATIONS ARE DISJOINT AND THE READERS ARE NOT A CHAIN. Censused over
# 24,539 rebar members in 13 families: the one STRAIGHT family carries the
# size parameter and not one of the twelve bent families does; all twelve
# bent families carry their part description on the TYPE (instance count
# zero) and no straight member carries one, with 7,405 of 7,405 bent
# descriptions parsing in one uniform shape. No bar has both fields, so the
# order `bar_size_of` tries them in has never decided anything and must not
# be read as a ranking.
#
# The bent half was the half still unwired after the size role landed, and it
# is roughly 30% of the reinforcing in this model -- not a rounding error.
def _two_reader_know():
"""A plant with BOTH size readers switched on."""
_t, know = _know_with_bars([])
know['parameters']['member']['identity_description'] = 'DESC_PARAM'
rule = know['drawings']['bar_size_rule']
rule['description_fallback'] = True
rule['bent_bar_tokens'] = ['BENT BAR']
return know
def _ticket_of(bars):
return {'mark': 'TEST-1',
'_bar_members': {'bars': list(bars), 'reason': None,
'unknown': []}}
def _gathered(know, mark, length_ft, parameter_value=None, description=None):
"""One bar row, sized exactly the way the gather sizes it."""
row = {'mark': mark, 'family': 'F', 'length_ft': length_ft}
row.update(TC._bar_size(know, parameter_value, description))
return row
def test_57_a_straight_bar_is_sized_by_the_dedicated_parameter():
got = TC._bar_size(_two_reader_know(), '#4', None)
assert got['size'] == '#4'
assert got['size_basis'] == 'parameter'
assert 'member.bar_size' in got['size_why']
def test_57_a_bent_bar_is_sized_off_the_front_of_its_part_description():
"""The half that was still standing down. The bent families carry no size
field at all, so this is the reader for that population and not a
fallback for the other one."""
got = TC._bar_size(_two_reader_know(), None, '#5 BENT BAR (17)')
assert got['size'] == '#5'
assert got['size_basis'] == 'description'
assert 'part description' in got['size_why']
def test_57_a_bent_bar_is_judged_against_the_stock_for_its_own_size():
"""End to end: a bent bar whose size only its description knows is held
to the per-size table like any other bar. Against the generic 40 ft it
would have passed."""
know = _two_reader_know()
know['catalogue']['rebar']['stock_by_size'] = {'#5': {'stock_length_in': 240}}
t = _ticket_of([_gathered(know, 'SR-BENT', 30.0,
description='#5 BENT BAR (17)')])
codes = dict((f.code, f)
for f in TC.check_bar_stock_length(t, {}, know, None))
assert codes['BAR-LENGTH'].severity == 'WARN'
assert 'SR-BENT' in codes['BAR-LENGTH'].message
assert '#5' in codes['BAR-LENGTH'].message # not merely "20 ft"
def test_57_a_description_that_does_not_parse_leaves_the_bar_unjudged():
"""The parse is strict, and the reason is not that the field is
unreliable -- it is structured plant data. The shape `#N BENT BAR` IS the
evidence that the leading token is a size, so a reworded description must
leave the bar unjudged rather than mis-sized."""
got = TC._bar_size(_two_reader_know(), None, 'REWORDED, NO LONGER A SHAPE')
assert got['size'] is None
assert got['size_basis'] is None
assert 'REWORDED, NO LONGER A SHAPE' in got['size_why']
def test_57_an_unparseable_description_is_set_aside_with_ITS_OWN_reason():
"""Not against the generic stock length, and not with a sentence about a
mapping that is already right: the reason has to name what was missing
for THAT bar, or a reader re-checks a file that is not the problem."""
know = _two_reader_know()
t = _ticket_of([_gathered(know, 'SR-BENT', 30.0,
description='REWORDED, NO LONGER A SHAPE')])
assert TC.check_bar_stock_length(t, {}, know, None) == []
reason = t['_bar_length_silence']
assert 'SR-BENT' in reason
assert 'REWORDED, NO LONGER A SHAPE' in reason
assert 'unjudged rather than mis-sized' in reason
assert 'no parameters.member.bar_size is mapped' not in reason
rows = dict((r[0], r) for r in TP.checklist_states(
[], [('check_bar_stock_length', 'SKIPPED', reason)]))
assert rows['5.7'][3] == 'NOT RUN'
assert TP.state_color(rows['5.7'][3]) == 'red'
def test_57_the_two_size_readers_never_both_answer_for_one_bar():
"""Two populations, two readers -- never a race and never a merge. The
census says no bar carries both fields; what the code guarantees is that
even if one did, exactly ONE field is named as having answered."""
know = _two_reader_know()
straight = TC._bar_size(know, '#4', None)
bent = TC._bar_size(know, None, '#5 BENT BAR (17)')
assert straight['size_basis'] == 'parameter'
assert bent['size_basis'] == 'description'
assert straight['size_basis'] != bent['size_basis']
both = TC._bar_size(know, '#4', '#5 BENT BAR (17)')
assert both['size_basis'] == 'parameter' # one answer, not two
assert both['size'] == '#4'
assert '#5' not in both['size_why'] # the other field is not read
# ...and with the parameter empty the description is not "second choice",
# it is the only reader for the population that has no parameter at all.
assert TC._bar_size(know, '', '#5 BENT BAR (17)')['size_basis'] \
== 'description'
class _Recorder(object):
"""An element that answers no parameter and remembers what was asked.
`ticketqc.pstr` stops at a falsy parameter, so nothing here reaches the
inert Revit stub at the top of this file -- which is exactly why WHERE a
field is read from is testable off-model at all.
"""
def __init__(self, symbol=None):
self.Symbol = symbol
self.asked = []
def LookupParameter(self, pname):
self.asked.append(pname)
return None
def test_57_the_description_is_read_off_the_type_and_never_the_instance():
"""One parameter name over two kinds of element: the ASSEMBLY's identity
description is the owner's freeform notes and the FAMILY's is structured
plant data. `description_read_from` is what keeps them apart, and reading
the wrong one gets prose where structure was expected."""
know = _two_reader_know()
symbol = _Recorder()
el = _Recorder(symbol=symbol)
assert TC._bar_size_values(el, know) == (None, None)
assert 'DESC_PARAM' in symbol.asked
assert 'DESC_PARAM' not in el.asked
# The size parameter stays a union over instance and type -- `_bar_mark`'s
# precedence, for `_bar_mark`'s measured reason.
assert el.asked == ['BAR_SIZE_PARAM']
assert symbol.asked[0] == 'BAR_SIZE_PARAM'
def test_57_the_finding_pins_which_field_answered():
"""PROVENANCE IS THE PART A REFACTOR DROPS QUIETLY. "40 ft" and "40 ft
for a #4" are different claims, and so are "read from a dedicated size
parameter" and "parsed off the front of a description" -- a verdict that
cannot say what it read cannot be audited."""
t, know = _size_know(
[{'mark': 'SR-STRAIGHT', 'family': 'F', 'length_ft': 10.0,
'size': '#4', 'size_basis': 'parameter'},
{'mark': 'SR-BENT', 'family': 'F', 'length_ft': 12.0,
'size': '#5', 'size_basis': 'description'}])
codes = dict((f.code, f)
for f in TC.check_bar_stock_length(t, {}, know, None))
note = codes['BAR-LENGTH-SIZE-BASIS']
assert note.severity == 'OK'
assert ('read from the size field behind parameters.member.bar_size'
in note.message)
assert ('parsed from the part description behind '
'parameters.member.identity_description' in note.message)
assert '#4' in note.message and '#5' in note.message
def test_57_the_provenance_note_can_never_decide_the_row():
"""Its own code, unowned -- 5.2's BED-REVERSE-UNCLASSIFIED shape. It may
neither turn 5.7 green nor disappear from the report."""
owners = []
for item in TP.CHECKLIST_ITEMS:
owners.extend(item[3] or ())
assert 'BAR-LENGTH-SIZE-BASIS' not in owners
t, know = _size_know([{'mark': 'SR-STRAIGHT', 'family': 'F',
'length_ft': 10.0, 'size': '#4'}])
findings = TC.check_bar_stock_length(t, {}, know, None)
assert 'BAR-LENGTH-SIZE-BASIS' in [f.code for f in findings]
rows = dict((r[0], r) for r in TP.checklist_states(
[f for f in findings if f.code != 'BAR-LENGTH'],
[('check_bar_stock_length', 'RAN', None)]))
assert rows['5.7'][3] == 'OK' # off the log, not off this note
def test_57_the_two_sentences_about_a_closed_question_are_gone():
"""Tombstone. Question A3 -- which parameter carries a bar's size -- was
answered by measurement on 2026-08-31 and `member.bar_size` is declared
in `knowledge.PARAMETER_ROLES`. Two strings in this module still said the
opposite: `_bar_size`'s docstring and `BAR_SIZE_UNMAPPED`. A message that
sends a reader to a closed question is worse than no message."""
assert 'A3' not in TC.BAR_SIZE_UNMAPPED
assert 'question A3' not in TC.check_bar_stock_length.__doc__
doc = ' '.join((TC._bar_size.__doc__ or '').split())
assert 'is question A3 with the owner' not in doc
assert 'has not been answered' not in doc
assert 'NOT IN `knowledge.PARAMETER_ROLES`' not in doc
assert 'question A3 was answered' in doc
assert TC.BAR_SIZE_ROLE in knowledge_module().PARAMETER_ROLES
# ================================================== 5.4, the mesh word
#
# Three definitions of "mesh" existed. `takeoffgather._is_mesh` reads
# `families.wwf_prefix` defaulting to `''`, `ticketqc` reads it defaulting to
# `'WWF'`, and `check_wwf_designation` -- the check that OWNS row 5.4 -- read
# no key at all and hardcoded the two English words.
_MESH_FAMILY = 'INVENTED_FABRIC_6x6' # matches neither built-in word
class _StubSymbol(object):
def __init__(self, family_name):
self.Family = type(str('F'), (object,), {'Name': family_name})()
class _StubMember(object):
def __init__(self, family_name):
self.Symbol = _StubSymbol(family_name)
class _StubAssembly(object):
def __init__(self, families):
self._members = [_StubMember(f) for f in families]
def GetMemberIds(self):
return list(range(len(self._members)))
_ASM_ID = 'THE-ASSEMBLY'
class _StubDoc(object):
"""Just enough document to walk one assembly's members.
The assembly answers to its own id and members answer to their index, so
a mix-up between the two cannot silently return the wrong element -- the
first version of this stub returned a MEMBER for the assembly id, the
check's `try` swallowed the IndexError, and every assertion below passed
against the "no assembly" branch.
"""
def __init__(self, families):
self._asm = _StubAssembly(families)
def GetElement(self, ref):
if ref == _ASM_ID:
return self._asm
return self._asm._members[ref]
def _mesh_ticket(families, prefix='WWF'):
know = _know()
know.setdefault('families', {})['wwf_prefix'] = prefix
t = {'asm_id': _ASM_ID, 'doc': _StubDoc(families)}
return t, know
def _wwf(t, mesh, know):
"""5.4 with the Revit stub in place, and with an `ElementId` that can
actually be constructed.
The check does `tdoc.GetElement(ElementId(t['asm_id']))` inside a try, so
with the inert stub the construction raises, the assembly comes back None
and the walk never happens -- every assertion below would then be testing
the "no assembly" branch while looking like it tested the mesh rule. The
id here is a pass-through, which is exactly what the stub document
expects.
"""
with _revit():
db = sys.modules['Autodesk.Revit.DB']
was = db.ElementId
db.ElementId = lambda value: value
try:
return TC.check_wwf_designation(t, {'wwf_mesh': mesh}, know, None)
finally:
db.ElementId = was
def test_54_reads_the_plants_word_instead_of_two_hardcoded_english_ones():
"""A plant that calls its mesh anything else got a silent pass: no family
matched the built-in words, so 5.4 reported "no mesh family is modelled"
as a fact about the MODEL rather than as the tool not knowing the word."""
t, know = _mesh_ticket([_MESH_FAMILY], prefix='INVENTED_FABRIC')
out = _wwf(t, '6x6-W2.9xW2.9', know)
codes = dict((f.code, f) for f in out)
assert 'WWF' in codes
# It found the family, so the message is a comparison and not a "nothing
# is modelled" claim.
assert 'no modelled family matches' not in codes['WWF'].message
def test_54_names_the_key_when_nothing_matches_it():
t, know = _mesh_ticket(['SOMETHING_ELSE'], prefix='INVENTED_FABRIC')
out = _wwf(t, '6x6', know)
assert out[0].severity == 'WARN'
assert 'families.wwf_prefix' in out[0].message
assert 'INVENTED_FABRIC' in out[0].message
def test_54_a_family_that_looks_like_mesh_but_misses_the_key_is_reported():
"""The signature of a prefix set to the wrong word. Neither a pass nor a
comparison -- and it must not be able to decide the row or to vanish."""
t, know = _mesh_ticket(['A MESH FAMILY'], prefix='INVENTED_FABRIC')
out = _wwf(t, '6x6', know)
assert out[0].severity == 'WARN'
assert 'read like mesh by name' in out[0].message
assert 'A MESH FAMILY' in out[0].message
assert 'Nothing was compared' in out[0].message
def test_54_keeps_todays_behaviour_when_the_key_is_null():
"""What a null prefix should mean plant-wide is an open owner question --
the two other readers of this key disagree about it -- so nothing is
invented: the built-in words still apply, and the finding says so."""
t, know = _mesh_ticket(['A MESH FAMILY'], prefix='')
out = _wwf(t, '6x6', know)
codes = dict((f.code, f) for f in out)
assert 'WWF' in codes
assert 'WWF-UNMATCHED' not in codes
t2, know2 = _mesh_ticket(['SOMETHING_ELSE'], prefix='')
out2 = _wwf(t2, '6x6', know2)
assert 'unset' in out2[0].message
assert 'MESH/WWF' in out2[0].message
def test_54_a_mismatched_family_beside_a_matched_one_cannot_decide_the_row():
t, know = _mesh_ticket([_MESH_FAMILY, 'A MESH FAMILY'],
prefix='INVENTED_FABRIC')
out = _wwf(t, '6x6', know)
codes = dict((f.code, f) for f in out)
assert codes['WWF-UNMATCHED'].severity == 'OK'
assert 'A MESH FAMILY' in codes['WWF-UNMATCHED'].message
# WWF-UNMATCHED owns no checklist row, so it can neither set nor sink 5.4.
owners = set()
for item in TP.CHECKLIST_ITEMS:
for code in (item[3] or ()):
owners.add(code)
assert 'WWF-UNMATCHED' not in owners
# ============================================ 5.2, the bed plan view name
def test_52_reads_the_plants_bed_plan_name_and_never_a_literal():
"""Triage B1. `_bed_plan_text` read `drawings.bed_plan` while
`check_bed_plan_reverse`, ten screens down in the same file, compared
against a literal English view name."""
know = _know()
know.setdefault('drawings', {})['bed_plan'] = 'INVENTED VIEW NAME'
name, why = TC._bed_plan_name(know)
assert name == 'INVENTED VIEW NAME'
assert why is None
# WALKED WITH `ast`, not grepped. Prose mentions survive -- a comment
# recording a measurement off the live model, and the docstrings that
# explain this very fix -- and none of them can decide a checklist row.
# What may not survive is a STRING THE CODE EVALUATES, so that is what is
# looked for, and every string constant that is not a docstring is
# checked rather than a list of the ones somebody remembered.
for text in _code_strings(TC.__file__.replace('.pyc', '.py')):
assert 'ELEVATION TOP' not in text.upper(), (
'a plant view name is still a string this code evaluates: '
'{0!r}'.format(text))
def _code_strings(path):
"""Every string constant in a module that is NOT a docstring."""
import ast
import io as _io
tree = ast.parse(_io.open(path, encoding='utf-8').read())
docstrings = set()
for node in ast.walk(tree):
if isinstance(node, (ast.Module, ast.FunctionDef, ast.ClassDef)):
body = getattr(node, 'body', None) or []
if body and isinstance(body[0], ast.Expr) and isinstance(body[0].value, ast.Constant) and isinstance(body[0].value.value, str):
docstrings.add(id(body[0].value))
out = []
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, str) and id(node) not in docstrings:
out.append(node.value)
return out
def test_52_stands_down_with_a_reason_when_the_key_is_unset():
"""Not a fallback to the name this tool shipped with: that is a guess
about the plant dressed as a default, and the row it feeds would report
on a view nobody chose."""
know = _know()
know.setdefault('drawings', {})['bed_plan'] = ''
name, why = TC._bed_plan_name(know)
assert name is None
assert 'drawings.bed_plan' in why
assert 'will not guess' in why
rows = dict((r[0], r) for r in TP.checklist_states(
[], [('check_bed_plan_reverse', 'SKIPPED', why)]))
assert rows['5.2'][3] == 'NOT RUN'
assert TP.state_color(rows['5.2'][3]) == 'red'
def test_52_both_readers_of_the_bed_plan_name_go_through_one_resolver():
"""The bug was two readers of one fact disagreeing. One resolver is what
stops them drifting apart again."""
know = _know()
know.setdefault('drawings', {})['bed_plan'] = 'INVENTED VIEW NAME'
assert TC._bed_plan_name(know)[0] == 'INVENTED VIEW NAME'
assert TC._bed_plan_text({'doc': None}, know) is None # no document
know['drawings']['bed_plan'] = None
assert TC._bed_plan_name(know)[0] is None
# ================================================================== 6.6
#
# Owner's ruling, 2026-08-31: when the count of one part on a piece is ODD,
# the ticket's BED PLAN must carry a particular phrase.
#
# WHAT THESE TESTS REACH, AND WHY THEY CAN. `check_parity_note` has exactly
# two reads that need a document -- the member census and the BED PLAN's
# text -- and both are seams. The census is CACHED ON THE TICKET (the same
# shape `_bar_members` uses, and for the same reason: four callers were
# reading the same thing four times), so a test puts the answer there and
# no assembly is ever opened. The BED PLAN's text comes through one
# function. Everything between them is arithmetic over a dict.
#
# THE PART NUMBER IS INVENTED. Asserting on the plant's real one would pin
# the plant's rule into the repository from the test side, which is the
# same defect as writing it into the check. The WORDING is the real one and
# is doing a job: the last test in this block asserts that this exact
# string appears in neither module, so the day somebody hardcodes the
# phrase the suite says so.
PART = 'ACME_PART_9'
PHRASE = 'FOR ERECTION ONLY'
def _parity_know(bed_plan='ELEVATION TEST', **overrides):
know = _know()
rule = {'family_names': [PART],
'family_verified': True,
'family_unverified_because': None,
'parity': 'odd',
'require_text': PHRASE}
rule.update(overrides)
know.setdefault('drawings', {})['parity_note_rule'] = rule
if bed_plan is not None:
know['drawings']['bed_plan'] = bed_plan
return know
def _parity_ticket(count, total=None, unknown=None, reason=None):
"""A ticket whose member census is already solved."""
return {'mark': 'TEST-1',
'_parity_members': {'count': count,
'total': total if total is not None else 40,
'unknown': list(unknown or []),
'reason': reason}}
def _bed(monkeypatch, text):
"""Point the one BED PLAN read at a string, and record that it happened."""
calls = []
def fake(t, know):
calls.append(True)
return text
monkeypatch.setattr(TC, '_bed_plan_text', fake)
return calls
def test_66_odd_count_with_the_note_is_ok(monkeypatch):
_bed(monkeypatch, 'DUNNAGE PER PLANT STANDARD. FOR ERECTION ONLY.')
out = TC.check_parity_note(_parity_ticket(3), {}, _parity_know(), None)
assert len(out) == 1
assert out[0].severity == 'OK'
assert out[0].code == 'PARITY-NOTE'
def test_66_odd_count_without_the_note_is_the_defect(monkeypatch):
_bed(monkeypatch, 'DUNNAGE PER PLANT STANDARD.')
t = _parity_ticket(3)
out = TC.check_parity_note(t, {}, _parity_know(), None)
assert [f.severity for f in out] == ['FAIL']
assert t['_parity_note_silence'] is None
def test_66_even_count_is_ok_and_never_opens_the_bed_plan(monkeypatch):
"""The gate order is the design: a count whose parity does not demand
the note is fully judged without the plan, so requiring a readable plan
there would print NOT RUN for a piece this answers completely."""
calls = _bed(monkeypatch, None)
out = TC.check_parity_note(_parity_ticket(4), {}, _parity_know(), None)
assert [f.severity for f in out] == ['OK']
assert calls == []
def test_66_zero_count_is_a_pass_and_names_the_members_it_read(monkeypatch):
_bed(monkeypatch, None)
out = TC.check_parity_note(_parity_ticket(0, total=112), {},
_parity_know(), None)
assert [f.severity for f in out] == ['OK']
assert '112' in out[0].message
def test_66_zero_and_unrecognised_are_not_the_same_answer(monkeypatch):
_bed(monkeypatch, PHRASE)
know = _parity_know()
zero = TC.check_parity_note(_parity_ticket(0, total=112), {}, know, None)
blind = _parity_ticket(0, total=112,
unknown=[{'family': '', 'why': 'no family name'}])
silent = TC.check_parity_note(blind, {}, know, None)
assert zero[0].severity == 'OK'
assert silent == []
assert 'not trustworthy' in blind['_parity_note_silence']
def test_66_an_unrecognisable_part_is_not_run_with_a_reason(monkeypatch):
_bed(monkeypatch, PHRASE)
t = _parity_ticket(3, unknown=[{'family': '', 'why': 'no family name'}])
assert TC.check_parity_note(t, {}, _parity_know(), None) == []
reason = TC._probe_parity_note(t, {}, _parity_know(), None)
assert 'no family name' in reason
def test_66_an_unreadable_bed_plan_is_not_run_with_its_own_reason(
monkeypatch):
_bed(monkeypatch, None)
t = _parity_ticket(3)
assert TC.check_parity_note(t, {}, _parity_know(), None) == []
reason = TC._probe_parity_note(t, {}, _parity_know(), None)
assert 'ELEVATION TEST' in reason
assert TC.BED_PLAN_KEY in reason
def test_66_no_bed_plan_name_declared_is_its_own_third_reason(monkeypatch):
"""Three silences and they are different things: the plant's bed-plan
view is not named, the named view is not on this ticket, or the count
could not be made. None of them may read the same."""
calls = _bed(monkeypatch, PHRASE)
t = _parity_ticket(3)
assert TC.check_parity_note(t, {}, _parity_know(bed_plan=None),
None) == []
assert calls == []
assert TC.BED_PLAN_KEY in t['_parity_note_silence']
def test_66_the_phrase_key_unset_is_not_run(monkeypatch):
_bed(monkeypatch, PHRASE)
t = _parity_ticket(3)
know = _parity_know(require_text=None)
assert TC.check_parity_note(t, {}, know, None) == []
assert 'require_text' in TC._probe_parity_note(t, {}, know, None)
def test_66_stands_down_loudly_on_stock_defaults(monkeypatch):
"""The keys are not in DEFAULTS yet. Until they land the row must read
NOT RUN naming the key that is missing -- never OK, and never silence."""
_bed(monkeypatch, PHRASE)
t = _parity_ticket(3)
assert TC.check_parity_note(t, {}, _know(), None) == []
assert 'family_names' in TC._probe_parity_note(t, {}, _know(), None)
def test_66_an_unverified_family_name_stands_down(monkeypatch):
"""A family name nobody has confirmed matches nothing, counts zero on
every piece, and zero is even -- a permanent invisible OK. The same
guard `reinforcing_rule.parameter_verified` carries."""
_bed(monkeypatch, PHRASE)
t = _parity_ticket(3)
know = _parity_know(family_verified=False,
family_unverified_because='not read off a model yet')
assert TC.check_parity_note(t, {}, know, None) == []
reason = TC._probe_parity_note(t, {}, know, None)
assert 'family_verified' in reason
assert 'not read off a model yet' in reason
def test_66_a_census_that_could_not_be_made_carries_its_own_reason(
monkeypatch):
_bed(monkeypatch, PHRASE)
t = _parity_ticket(None, reason='this ticket has no assembly')
assert TC.check_parity_note(t, {}, _parity_know(), None) == []
assert TC._probe_parity_note(t, {}, _parity_know(),
None) == 'this ticket has no assembly'
def test_66_the_probe_never_returns_none_on_a_silence():
"""With a probe registered, a None return turns a silence into a quiet
OK -- a worse failure than the one the probe fixes."""
assert TC._probe_parity_note({}, {}, _know(), None)
assert TC._probe_parity_note({'_parity_note_silence': None}, {}, _know(),
None)
def test_66_the_check_is_registered_and_carries_a_probe():
assert TC.check_parity_note in TC.CHECKS
assert 'check_parity_note' in TC._SKIP_PROBES
def test_66_no_part_number_or_wording_is_written_into_the_code():
"""The three plant facts are read, never held. A literal in either
module is the exact shape this project keeps having to remove."""
import inspect
for module in (TC, TP):
source = inspect.getsource(module)
assert 'parity_note_rule' in source
assert PART not in source
assert PHRASE not in source
# ============================================================================
# A CHECK IS SKIPPED ONLY WHEN NO ROW IT ANSWERS CAN BE ASKED
# ============================================================================
#
# `knowledge.check_not_applicable` decides per ROW; `CHECKS` is per FUNCTION,
# and the two are not one-to-one -- 4.4 and 4.6 both name
# `check_strand_layout`, and 5.1 names two functions. Skipping a check
# because ONE of its rows was declared inapplicable would take the other row
# with it: a row nobody switched off, going quiet, because of a declaration
# about a different row. That is a silently-disabled safety check arriving
# through the mechanism built to make disabling one visible.
def _switch_off(item, condition='any'):
know = _know()
know['checks']['not_applicable'] = {item: {condition: {
'applies': False,
'not_applicable_because': 'declared for the test',
'not_applicable_since': '2026-08-31'}}}
return know
def test_nothing_is_skipped_when_nothing_is_declared():
assert TC._applicability_skips({'mark': 'X'}, _know()) == {}
def test_a_check_whose_only_row_is_switched_off_does_not_run():
know = _switch_off('3.3') # 3.3 is check_release_ratio's only row
skips = TC._applicability_skips({'mark': 'X'}, know)
assert 'check_release_ratio' in skips
assert 'declared for the test' in skips['check_release_ratio']
def test_a_check_shared_with_a_row_that_still_applies_keeps_running():
"""4.4 and 4.6 both name `check_strand_layout`. Switching 4.4 off must
not take 4.6 with it -- the row is repainted, the check is not stopped."""
know = _switch_off('4.4')
skips = TC._applicability_skips({'mark': 'X'}, know)
assert 'check_strand_layout' not in skips
know = _switch_off('4.6')
assert 'check_strand_layout' not in TC._applicability_skips(
{'mark': 'X'}, know)
def test_switching_both_shared_rows_off_does_stop_the_check():
know = _know()
entry = {'applies': False,
'not_applicable_because': 'declared for the test',
'not_applicable_since': '2026-08-31'}
know['checks']['not_applicable'] = {'4.4': {'any': entry},
'4.6': {'any': dict(entry)}}
skips = TC._applicability_skips({'mark': 'X'}, know)
assert 'check_strand_layout' in skips
assert '4.4' in skips['check_strand_layout']
assert '4.6' in skips['check_strand_layout']
def test_a_ticket_with_no_classification_resolves_as_unclassified():
"""Every host test and every caller older than 2026-08-31 builds `t` by
hand. That resolves to UNCLASSIFIED and unverified -- the safe default,
where only a plant-wide `any` declaration can switch anything off."""
know = _switch_off('3.3', condition='insulated')
assert TC._applicability_skips({'mark': 'X'}, know) == {}
decisions, classification, member, profile = TC.applicability_decisions_for({'mark': 'X'}, know)
assert member['member_kind'] is None
assert profile['profile'] is None
assert classification == {}
assert decisions['3.3']['not_applicable'] is False
assert 'REFUSED' in decisions['3.3']['problem']
def test_a_classified_ticket_lets_a_piece_conditioned_rule_fire():
know = _switch_off('3.3', condition='insulated')
t = {'mark': 'X', 'panel_type': {'panel_type': 'insulated',
'verified': True}}
skips = TC._applicability_skips(t, know)
assert 'check_release_ratio' in skips
def test_run_all_logs_a_skipped_check_rather_than_dropping_it():
"""The disposition log is the one channel that reaches every consumer.
A check that was not run and said nothing would be indistinguishable
from one that ran and found nothing -- the founding failure."""
know = _switch_off('3.3')
_findings, log = TC.run_all({'mark': 'X'}, {}, know, None)
named = dict([(name, (state, note)) for (name, state, note) in log])
assert named['check_release_ratio'][0] == TC.NOT_APPLICABLE_DISPOSITION
assert 'declared for the test' in named['check_release_ratio'][1]
def test_a_row_scoped_to_a_type_is_not_run_on_an_unplaceable_piece():
"""The other direction: a row that exists ONLY for one panel type.
On a piece nothing could classify it is NOT ASKED -- and the row ends up
NOT RUN, in red, rather than quietly answered as though the piece were
the ordinary type.
"""
know = _know()
know['checks']['only_applies_to'] = {'3.3': {'insulated': {
'scoped': True,
'scoped_because': 'only insulated pieces have one',
'scoped_since': '2026-08-31'}}}
skips = TC._applicability_skips({'mark': 'X'}, know)
assert 'check_release_ratio' in skips
assert 'NOT ASKED' in skips['check_release_ratio']
decisions = TC.applicability_decisions_for({'mark': 'X'}, know)[0]
assert decisions['3.3']['unaskable'] is True
rows, notes = TP.apply_applicability(
[('3.3', 'release ratio', 'B', 'OK', None)], decisions)
assert rows[0][3] == 'NOT RUN'
assert notes == []
# ============================================================================
# THE MASTER CHECKLIST LIVES IN YAML NOW, SO IT CAN DRIFT FROM THE CODE
# ============================================================================
#
# Owner, 2026-08-31: "做一个总的 checklist 放 public knowledge 里面,然后 plant
# 里面做一层映射,job 里面用静音按键去控制。"
#
# A tuple in `ticketpage.py` could not disagree with the functions beside it.
# A YAML list can, in two directions, and BOTH ARE SILENT:
#
# * a master entry naming a check function nobody wrote -> the row is NOT
# RUN forever and reads like an unbuilt feature rather than a typo;
# * a registered check function no master entry names -> the check RUNS,
# spends the time, and can never reach a row. Its findings land in
# `qc-run.json` under a code no row owns and nobody looks at them.
#
# This pair is the guard that replaces what the tuple used to give for free,
# and it is why the move was safe to make at all.
# TWO CHECKS THAT RUN AND REACH NO CHECKLIST ROW. Found 2026-08-31 by the
# test below, on the day the master checklist moved into the public layer --
# a defect that had been there the whole time and that a tuple could never
# have surfaced, because nothing had ever asked the question.
#
# Neither is named by a master entry's `checks:` slot, AND neither emits a
# finding code any master entry owns:
#
# check_bom_strand_count -> BOM-STRAND. Row 4.1 asks the same question
# ("printed strand count vs modelled") and owns
# STR-QTY, which `ticketqc.check_ticket` emits
# directly. So the question IS on the checklist
# and this second, independent answer to it is
# not -- it can FAIL, it lands in qc-run.json,
# and no row turns red.
# check_picking_match -> PICK-STATIONS / PICK-UNREADABLE. Nothing on
# the checklist asks about picking stations at
# all.
#
# BOTH WERE WIRED UP ON 2026-08-31 AND THIS LIST IS NOW EMPTY. Giving a
# check a row is a decision about what the report claims and it was the
# owner's to make; he made it -- "看看能跑的,能做的,现在都做了去" -- and
# they landed as master ids 4.8 and 6.8. The argument for a row of its own
# rather than a second check on a neighbouring row is written out beside
# each entry in `knowledge/catalogue.yaml`; the short version is that
# hanging either off an existing row would have made that row NOT RUN
# whenever the new check went quiet, which is the opposite of what was
# asked for.
#
# THE TUPLE STAYS, EMPTY, AND SO DO THE THREE TESTS. It is an allowlist
# with two guards pointing in opposite directions -- one fails if a check
# reaches no row and is not listed, the other fails if a listed check gains
# one -- and an empty allowlist under a working pair of guards is exactly
# what "there are none" looks like. Deleting it would delete the mechanism
# that found these two in the first place.
ROWLESS_CHECKS = ()
def test_every_master_entry_names_a_check_that_exists():
missing = []
for item in TP.CHECKLIST_ITEMS:
for name in TP._check_names(item[5]):
if not hasattr(TC, name):
missing.append('{0} -> {1}'.format(item[0], name))
assert not missing, (
'the published master checklist names check function(s) that do not '
'exist in vibe.ticketchecks: {0}. Either the function was renamed '
'without the catalogue, or the entry was written before the code.'
.format('; '.join(missing)))
def test_every_registered_check_is_reachable_from_a_master_entry():
"""The silent half. A check that runs and can never set a row is worse
than one that is missing: it costs the time and produces nothing a
reader will ever see."""
named = set()
for item in TP.CHECKLIST_ITEMS:
for name in TP._check_names(item[5]):
named.add(name)
orphans = sorted([fn.__name__ for fn in TC.CHECKS
if fn.__name__ not in named
and fn.__name__ not in ROWLESS_CHECKS])
assert not orphans, (
'these check functions are registered in CHECKS and no master '
'checklist entry names them, so they run and can never reach a row: '
'{0}. If that is deliberate, add it to ROWLESS_CHECKS with a reason.'
.format(', '.join(orphans)))
def test_the_rowless_exemptions_are_still_really_rowless():
"""The other half of an allowlist: it must not rot into a place where
things go to be forgotten. If somebody gives one of these a row, this
fails and the exemption comes out."""
named = set()
for item in TP.CHECKLIST_ITEMS:
for name in TP._check_names(item[5]):
named.add(name)
stale = sorted([name for name in ROWLESS_CHECKS if name in named])
assert not stale, (
'these are listed as reaching no checklist row and a master entry '
'now names them: {0}. Remove them from ROWLESS_CHECKS.'
.format(', '.join(stale)))
def test_every_check_function_a_master_entry_names_is_registered():
"""The other silent half: a function that exists, is named by a row, and
is not in CHECKS never runs -- and the row then reads NOT RUN with the
generic 'no probe declared' rather than saying nobody wired it."""
registered = set([fn.__name__ for fn in TC.CHECKS])
unwired = []
for item in TP.CHECKLIST_ITEMS:
for name in TP._check_names(item[5]):
if hasattr(TC, name) and name not in registered:
unwired.append('{0} -> {1}'.format(item[0], name))
assert not unwired, (
'these master entries name a function that exists but is not in '
'TC.CHECKS, so it never runs: {0}'.format('; '.join(unwired)))
def test_the_master_ids_are_frozen():
"""A WRITTEN-DOWN GOLDEN, and it must stay written down.
A plant may now renumber its own checklist in one YAML edit. These ids
are what everything ELSE names: the owner's mutes, `checks.muted`,
`checks.not_applicable`, `checks.only_applies_to`, the QNA records,
`qc-run.json` and its consumers. A renumbering that reached them would
either break those references or -- far worse -- leave them working and
pointing at a different question.
So this list is TYPED OUT and compared. A pin that recomputes both sides
pins nothing; it agrees with any refactor, including a wrong one. Adding
a question means adding its id here, deliberately, which is the one
keystroke that makes somebody notice they are minting an identity rather
than a row number.
REMOVING one is a DELETION, not a renumbering, and it orphans every
ruling that names it. A retired id is never reused.
"""
assert list(TP.CHECKLIST_IDS) == [
'1.1', '1.2', '1.3', '1.4', '1.5',
'2.1', '2.2', '2.3', '2.4',
'3.1', '3.2', '3.3',
'4.1', '4.2', '4.3', '4.4', '4.5', '4.6', '4.7',
# 4.8, minted 2026-08-31 for a check that had been RUNNING and
# reaching no row -- `check_bom_strand_count`, whose BOM-STRAND
# finding can FAIL. Typed out here on purpose: this pin is the
# keystroke that makes minting an identity deliberate.
'4.8',
'5.1', '5.2', '5.3', '5.4', '5.5', '5.6', '5.7',
'5.8', '5.9', '5.10',
# SOLID ZONES AND INSULATION RECESSES, minted 2026-08-31 on the
# owner's second asking. Five new IDENTITIES, typed out here
# deliberately -- which is the keystroke this pin exists to force.
'5.11', '5.12', '5.13', '5.14', '5.15',
# 5.16, minted 2026-09-03: door-opening diagonal edge bars.
'5.16',
'6.1', '6.2', '6.3', '6.4', '6.5', '6.6', '6.7',
# 6.8, the other one: `check_picking_match`, which asked a question
# nothing on the checklist asked at all.
'6.8',
# 6.9, minted 2026-09-02: `check_lifting_points`, an MC plant rule
# (owner verbatim in ticketchecks.py above the check) grading
# lifting-point count against panel length. Typed out here
# deliberately, same as every id above it.
'6.9',
# 6.10, minted the same day as 6.9: `check_lift_positions`, a
# second MC plant rule -- lifting-point POSITION vs PCI, distinct
# from 6.9's COUNT vs length. Typed out here deliberately, same as
# every id above it.
'6.10',
# 6.11, minted the same day: `check_dunnage_stripping_match`, a
# third MC plant rule -- dunnage-point count vs stripping-point
# (face-lifter) count. Typed out here deliberately, same as every
# id above it.
'6.11',
# 6.13, minted 2026-09-08 (REVIT issue #26): lifter accessory
# (cup/patch) assembly membership. 6.12 is skipped on purpose --
# see `FLEXURAL_CHECKS_ALREADY_ASKED` in ticketchecks.py. Typed
# out here deliberately, same as every id above it.
'6.13',
'7.1',
'8.1', '8.2',
]
def test_a_master_id_is_never_reused():
assert len(TP.CHECKLIST_IDS) == len(set(TP.CHECKLIST_IDS))
def test_the_checklist_refuses_rather_than_shrinking(tmp_path):
"""A checklist that quietly lost rows would report every ticket it
looked at as fine. Refusing to load is the cheap outcome."""
missing = str(tmp_path / 'nope.yaml')
with pytest.raises(TP.ChecklistUnavailable) as caught:
TP.load_master(missing)
assert 'nope.yaml' in str(caught.value)
empty = tmp_path / 'empty.yaml'
empty.write_text('checklist:\n master: []\n', encoding='utf-8')
with pytest.raises(TP.ChecklistUnavailable):
TP.load_master(str(empty))
dupe = tmp_path / 'dupe.yaml'
dupe.write_text(
"checklist:\n master:\n"
" - {id: '1.1', name: a, grade: A, owner_codes: null,"
" not_run_reason: null, checks: null}\n"
" - {id: '1.1', name: b, grade: A, owner_codes: null,"
" not_run_reason: null, checks: null}\n", encoding='utf-8')
with pytest.raises(TP.ChecklistUnavailable) as caught:
TP.load_master(str(dupe))
assert 'identity' in str(caught.value)
nameless = tmp_path / 'nameless.yaml'
nameless.write_text(
"checklist:\n master:\n"
" - {id: '1.1', grade: A, owner_codes: null,"
" not_run_reason: null, checks: null}\n", encoding='utf-8')
with pytest.raises(TP.ChecklistUnavailable):
TP.load_master(str(nameless))
def test_the_for_engineer_rows_still_derive_from_the_published_table():
"""The 5.3 defect, one layer out. `CHECKLIST_FOR_ENGINEER` is derived
from the row's own declared reason, and the reason now comes out of
YAML -- so an entry that loses `not_run_reason` in the move would turn a
person's row back into a red NOT RUN on every ticket."""
assert '5.3' in TP.CHECKLIST_FOR_ENGINEER
assert '5.5' in TP.CHECKLIST_FOR_ENGINEER
for num in TP.CHECKLIST_FOR_ENGINEER:
item = [i for i in TP.CHECKLIST_ITEMS if i[0] == num][0]
assert item[4], num
assert not item[3], num
assert not TP._check_names(item[5]), num
# ================== 5.11 - 5.15: the gates, and every one of the silences
#
# Owner, 2026-08-31: "没有 solid zone 相关的检查 ... 这些都没有啊" and "不光是
# solid zone,insulation recess 也得加上。"
#
# WHAT IS ASSERTED HERE IS THAT NONE OF THE FIVE CAN GO QUIET. Every empty
# return leaves a sentence behind and a probe reads it -- the shape 6.4 had
# to be retrofitted with, built in from the first commit this time.
#
# EVERY PLANT STRING BELOW IS INVENTED. A test that pinned this plant's real
# type names would put its vocabulary into the repository from the test side,
# which is the same defect as writing it into the check.
SOLID_ZONE_KNOW = {
'insulation': {
'solid_zone': {'family': 'ZONE_PART', 'family_verified': True},
'legend': {
'swatch_types': {'RECESS TYPE A': 'recess_2in_nf'},
'expected_patterns': {'solid_zone': 'ANSI35',
'recess_2in_nf': 'TRIANG'},
'expected_patterns_measured_on': '2026-08-31',
},
'recess': {'region_types': ['RECESS TYPE A'],
'modelled_path_verified': False,
'modelled_path_unverified_because': 'nobody has ruled'},
},
'drawings': {'insulation_layout': 'Layout View 1'},
}
def _zone_ticket(**kw):
t = {'solid_zones': [{'id': 1, 'lifter_id': 2, 'member': True}],
'lifters': [{'id': 2}], 'lifters_without_zone': 0,
'lifters_unread': 0, 'sheets': [{'name': 'PNL101'}]}
t.update(kw)
return t
def test_511_an_unverified_solid_zone_family_stands_the_row_down():
"""The 6.6 guard, and it is doing more work than usual here: a family
name that matches nothing counts ZERO zones, and zero zones reads as
"no lifter is missing one" -- a permanent invisible pass."""
know = {'insulation': {'solid_zone': {'family': 'ZONE_PART',
'family_verified': False}}}
t = _zone_ticket()
assert TC.check_solid_zone_members(t, {}, know, {}) == []
assert 'family_verified is false' in t['_solid_zone_silence']
def test_511_an_unnamed_family_stands_down_naming_the_key():
t = _zone_ticket()
assert TC.check_solid_zone_members(t, {}, {}, {}) == []
assert 'insulation.solid_zone.family is unset' in t['_solid_zone_silence']
def test_511_zones_never_gathered_is_told_apart_from_zones_measured_at_zero():
"""6.4's lesson, landed the same day. `None` and `[]` reach the check as
different values so that no later edit can collapse them."""
ungathered = _zone_ticket(solid_zones=None)
assert TC.check_solid_zone_members(
ungathered, {}, SOLID_ZONE_KNOW, {}) == []
assert 'not gathered on this run' in ungathered['_solid_zone_silence']
measured = _zone_ticket(solid_zones=[])
assert TC.check_solid_zone_members(measured, {}, SOLID_ZONE_KNOW, {}) == []
assert 'no solid zone was found' in measured['_solid_zone_silence']
assert ungathered['_solid_zone_silence'] != measured['_solid_zone_silence']
def test_511_a_non_member_fails_on_its_own_code():
t = _zone_ticket(solid_zones=[{'id': 1, 'lifter_id': 2, 'member': False}])
findings = TC.check_solid_zone_members(t, {}, SOLID_ZONE_KNOW, {})
assert [(f.severity, f.code) for f in findings] == [
('FAIL', 'SOLID-ZONE-ASM')]
def test_511_a_lifter_with_no_zone_rides_a_second_code_on_the_same_row():
t = _zone_ticket(lifters_without_zone=1)
codes = [f.code for f in
TC.check_solid_zone_members(t, {}, SOLID_ZONE_KNOW, {})]
assert 'SOLID-ZONE-ABSENT' in codes
assert 'SOLID-ZONE-ASM' in codes
def test_511_an_absent_zone_survives_an_unjudgeable_membership():
"""The row is answered by the absent FAIL, so the unjudged half rides as
an OK-severity note rather than swallowing the verdict."""
t = _zone_ticket(lifters_without_zone=1,
solid_zones=[{'id': 1, 'lifter_id': 2, 'member': None}])
findings = TC.check_solid_zone_members(t, {}, SOLID_ZONE_KNOW, {})
codes = dict([(f.code, f.severity) for f in findings])
assert codes['SOLID-ZONE-ABSENT'] == 'FAIL'
assert codes['SOLID-ZONE-UNJUDGED'] == 'OK'
def test_512_no_sheet_issued_is_not_run_and_is_neither_a_pass_nor_a_failure():
"""12 of the 27 insulated pieces have no sheet yet (2026-08-31). A piece
with no ticket has not got a ticket defect, and has not got a clean
ticket either."""
t = _zone_ticket(sheets=[])
assert TC.check_solid_zone_on_ticket(t, {}, SOLID_ZONE_KNOW, {}) == []
assert 'no sheet has been issued' in t['_solid_zone_ticket_silence']
def test_512_an_unnamed_layout_view_stands_down_naming_the_key():
know = dict(SOLID_ZONE_KNOW)
know['drawings'] = {}
t = _zone_ticket()
assert TC.check_solid_zone_on_ticket(t, {}, know, {}) == []
assert ('drawings.insulation_layout is unset'
in t['_solid_zone_ticket_silence'])
def test_512_a_non_member_is_reported_as_missing_from_the_ticket():
t = _zone_ticket(solid_zones=[{'id': 1, 'lifter_id': 2, 'member': False}])
findings = TC.check_solid_zone_on_ticket(t, {}, SOLID_ZONE_KNOW, {})
assert findings[0].severity == 'FAIL'
assert findings[0].code == 'SOLID-ZONE-TICKET'
assert 'Layout View 1' in findings[0].message
def test_513_falls_back_to_the_plant_map_and_says_it_is_a_fallback():
t = _zone_ticket(solid_zone_pattern='TRIANG')
findings = TC.check_solid_zone_hatch(t, {}, SOLID_ZONE_KNOW, {})
assert len(findings) == 1
assert findings[0].severity == 'FAIL'
assert 'plant layer' in findings[0].message
assert 'TRIANG' in findings[0].message and 'ANSI35' in findings[0].message
def test_513_prefers_the_legend_read_off_this_very_sheet():
"""The legend is the sheet's own statement of intent and the only
self-updating reference; the plant map is a copy of it."""
t = _zone_ticket(solid_zone_pattern='ANSI35',
legend_patterns={'solid_zone': 'ANSI35'})
findings = TC.check_solid_zone_hatch(t, {}, SOLID_ZONE_KNOW, {})
assert findings[0].severity == 'OK'
assert 'own legend' in findings[0].message
def test_513_an_ungathered_pattern_is_a_named_silence_not_a_pass():
t = _zone_ticket()
assert TC.check_solid_zone_hatch(t, {}, SOLID_ZONE_KNOW, {}) == []
assert 'could not be read on this run' in t['_solid_zone_hatch_silence']
def test_514_stands_down_and_the_probe_carries_both_unblockers():
t = _zone_ticket(recess_regions={'RECESS TYPE A': 2})
assert TC.check_recess_regions(t, {}, SOLID_ZONE_KNOW, {}) == []
said = TC._SKIP_PROBES['check_recess_regions'](t, {}, SOLID_ZONE_KNOW, {})
assert 'NOTHING WAS COMPARED' in said
assert 'NOT consulted' in said
def test_514_an_ungathered_region_list_names_the_gather_it_needs():
t = _zone_ticket(recess_regions=None)
assert TC.check_recess_regions(t, {}, SOLID_ZONE_KNOW, {}) == []
assert 'FilledRegion' in t['_recess_silence']
def test_515_reports_every_recess_type_in_ONE_finding():
"""`checklist_states` keeps the LAST finding per code, so a second
RECESS-HATCH would silently replace the first and the row would print
half the answer. 6.7's ruling, obeyed rather than rediscovered."""
know = {'insulation': {
'legend': {'swatch_types': {'RECESS TYPE A': 'recess_2in_nf',
'RECESS TYPE B': 'recess_3in_nf'},
'expected_patterns': {'recess_2in_nf': 'TRIANG',
'recess_3in_nf': 'TUCKPOINT'}},
'recess': {'region_types': ['RECESS TYPE A', 'RECESS TYPE B']}}}
t = _zone_ticket(recess_patterns={'RECESS TYPE A': 'STARS',
'RECESS TYPE B': 'STARS'})
findings = TC.check_recess_hatch(t, {}, know, {})
assert len(findings) == 1
assert findings[0].severity == 'FAIL'
assert 'RECESS TYPE A' in findings[0].message
assert 'RECESS TYPE B' in findings[0].message
def test_515_carries_the_size_caveat_without_answering_it():
know = {'insulation': {
'legend': {'swatch_types': {'RECESS TYPE A': 'recess_2in_nf'},
'expected_patterns': {'recess_2in_nf': 'TRIANG'}},
'recess': {'region_types': ['RECESS TYPE A']}}}
t = _zone_ticket(recess_patterns={'RECESS TYPE A': 'TRIANG'})
findings = TC.check_recess_hatch(t, {}, know, {})
assert findings[0].severity == 'OK'
assert 'question for the owner' in findings[0].message
def test_all_five_probes_return_a_reason_and_never_none_on_a_silence():
"""A probe that can return None on a silent path converts the silence
into a quiet OK -- a worse failure than the one it exists to fix."""
for name in ('check_solid_zone_members', 'check_solid_zone_on_ticket',
'check_solid_zone_hatch', 'check_recess_regions',
'check_recess_hatch'):
assert name in TC._SKIP_PROBES
said = TC._SKIP_PROBES[name]({}, {}, {}, {})
assert said and 'not as a pass' in said
def test_the_five_rows_are_wired_end_to_end():
pairs = (('5.11', 'SOLID-ZONE-ASM', 'check_solid_zone_members'),
('5.12', 'SOLID-ZONE-TICKET', 'check_solid_zone_on_ticket'),
('5.13', 'SOLID-ZONE-HATCH', 'check_solid_zone_hatch'),
('5.14', 'RECESS-PRESENT', 'check_recess_regions'),
('5.15', 'RECESS-HATCH', 'check_recess_hatch'))
for num, code, fn in pairs:
item = [i for i in TP.CHECKLIST_ITEMS if i[0] == num][0]
assert code in item[3], num
assert fn in TP._check_names(item[5]), num
assert fn in [f.__name__ for f in TC.CHECKS], num
assert num not in TP.CHECKLIST_FOR_ENGINEER
# ============ 6.13: lifter accessory (cup/patch) assembly membership =====
#
# REVIT issue #26. Every plant string below is invented, same rule as the
# solid-zone fixtures above -- this plant's real family names (QL121,
# QL062, DB_PATCH) belong in the plant layer, not in a test.
_ACCESSORY_KNOW = {
'lifters': {
'accessories': {
'CUP_PART': {'parent': 'LIFT_PART', 'role': 'cup'},
'PATCH_PART': {'parent': ['LIFT_PART', 'LIFT_PART_2'],
'role': 'patch'},
},
},
}
def _accessory_ticket(**kw):
t = {'lifters': [{'id': 2, 'family': 'LIFT_PART'}],
'lifter_children': [], 'lifter_children_unread': 0}
t.update(kw)
return t
def test_613_no_lifter_recognised_defers_to_61():
t = _accessory_ticket(lifters=[])
assert TC.check_lifter_accessory_in_assembly(t, {}, _ACCESSORY_KNOW, {}) == []
assert 'see 6.1' in t['_lifter_accessory_silence']
def test_613_children_never_gathered_is_told_apart_from_gathered_at_zero():
"""The same `None` vs `[]` discipline 5.11 uses for `solid_zones`."""
ungathered = _accessory_ticket(lifter_children=None)
assert TC.check_lifter_accessory_in_assembly(
ungathered, {}, _ACCESSORY_KNOW, {}) == []
assert 'not gathered on this run' in ungathered['_lifter_accessory_silence']
gathered = _accessory_ticket(lifter_children=[])
findings = TC.check_lifter_accessory_in_assembly(
gathered, {}, _ACCESSORY_KNOW, {})
assert findings[0].severity == 'OK'
assert 'no nested child' in findings[0].message
assert gathered['_lifter_accessory_silence'] is None
def test_613_a_classified_accessory_absent_from_the_assembly_fails():
t = _accessory_ticket(lifter_children=[
{'id': 10, 'family': 'CUP_PART', 'lifter_id': 2,
'lifter_family': 'LIFT_PART', 'member': False}])
findings = TC.check_lifter_accessory_in_assembly(t, {}, _ACCESSORY_KNOW, {})
assert [(f.severity, f.code) for f in findings] == [
('FAIL', 'LIFT-ACCESSORY-NOT-IN-ASSEMBLY')]
assert 'lifter 2' in findings[0].message
assert 'child 10' in findings[0].message
assert 'CUP_PART' in findings[0].message
assert 'cup' in findings[0].message
def test_613_a_classified_accessory_present_is_ok():
t = _accessory_ticket(lifter_children=[
{'id': 10, 'family': 'CUP_PART', 'lifter_id': 2,
'lifter_family': 'LIFT_PART', 'member': True}])
findings = TC.check_lifter_accessory_in_assembly(t, {}, _ACCESSORY_KNOW, {})
assert [(f.severity, f.code) for f in findings] == [
('OK', 'LIFT-ACCESSORY-NOT-IN-ASSEMBLY')]
def test_613_parent_as_a_list_is_accepted():
"""DB_PATCH's own shape: `parent` is a list, not a bare string."""
t = _accessory_ticket(lifter_children=[
{'id': 11, 'family': 'PATCH_PART', 'lifter_id': 2,
'lifter_family': 'LIFT_PART_2', 'member': False}])
findings = TC.check_lifter_accessory_in_assembly(t, {}, _ACCESSORY_KNOW, {})
assert findings[0].severity == 'FAIL'
assert 'patch' in findings[0].message
from vibe import knowledge as K
entry = K.lifter_accessories(_ACCESSORY_KNOW)['PATCH_PART']
assert entry['parents'] == ['LIFT_PART', 'LIFT_PART_2']
assert entry['role'] == 'patch'
def test_613_an_unclassified_child_not_in_the_assembly_warns():
t = _accessory_ticket(lifter_children=[
{'id': 12, 'family': 'MYSTERY_PART', 'lifter_id': 2,
'lifter_family': 'LIFT_PART', 'member': False}])
findings = TC.check_lifter_accessory_in_assembly(t, {}, _ACCESSORY_KNOW, {})
assert [(f.severity, f.code) for f in findings] == [
('WARN', 'LIFT-ACCESSORY-UNCLASSIFIED')]
assert 'MYSTERY_PART' in findings[0].message
def test_613_an_unclassified_member_is_a_quiet_pass():
"""Unclassified AND already in the assembly: nothing to flag, nothing
to claim -- an empty return that is a genuine ran-and-found-nothing,
not a silence."""
t = _accessory_ticket(lifter_children=[
{'id': 12, 'family': 'MYSTERY_PART', 'lifter_id': 2,
'lifter_family': 'LIFT_PART', 'member': True}])
assert TC.check_lifter_accessory_in_assembly(t, {}, _ACCESSORY_KNOW, {}) == []
assert t['_lifter_accessory_silence'] is None
def test_613_unreadable_membership_is_never_a_pass():
t = _accessory_ticket(lifter_children=[
{'id': 10, 'family': 'CUP_PART', 'lifter_id': 2,
'lifter_family': 'LIFT_PART', 'member': None}])
assert TC.check_lifter_accessory_in_assembly(t, {}, _ACCESSORY_KNOW, {}) == []
assert 'readable assembly membership' in t['_lifter_accessory_silence']
assert 'nothing was judged' in t['_lifter_accessory_silence']
def test_613_a_fail_and_a_warn_coexist_on_different_codes():
t = _accessory_ticket(lifter_children=[
{'id': 10, 'family': 'CUP_PART', 'lifter_id': 2,
'lifter_family': 'LIFT_PART', 'member': False},
{'id': 12, 'family': 'MYSTERY_PART', 'lifter_id': 2,
'lifter_family': 'LIFT_PART', 'member': False}])
findings = TC.check_lifter_accessory_in_assembly(t, {}, _ACCESSORY_KNOW, {})
codes = dict([(f.code, f.severity) for f in findings])
assert codes['LIFT-ACCESSORY-NOT-IN-ASSEMBLY'] == 'FAIL'
assert codes['LIFT-ACCESSORY-UNCLASSIFIED'] == 'WARN'
def test_613_probe_never_returns_none_on_a_silent_path():
t = _accessory_ticket(lifters=[])
assert TC.check_lifter_accessory_in_assembly(t, {}, _ACCESSORY_KNOW, {}) == []
said = TC._SKIP_PROBES['check_lifter_accessory_in_assembly'](
t, {}, _ACCESSORY_KNOW, {})
assert said and 'see 6.1' in said
def test_613_is_wired_end_to_end():
item = [i for i in TP.CHECKLIST_ITEMS if i[0] == '6.13'][0]
assert 'LIFT-ACCESSORY-NOT-IN-ASSEMBLY' in item[3]
assert 'check_lifter_accessory_in_assembly' in TP._check_names(item[5])
assert 'check_lifter_accessory_in_assembly' in [
f.__name__ for f in TC.CHECKS]
assert 'check_lifter_accessory_in_assembly' in [
f.__name__ for f in TC.REINFORCING_CHECKS]
assert 'check_lifter_accessory_in_assembly' not in TC.REINFORCING_EXCLUDED
assert 'check_lifter_accessory_in_assembly' not in TC.REINFORCING_APPLIES_TO
def test_no_plant_vocabulary_reached_the_code():
"""The filled-region type names, the layout view name and the solid-zone
family are PLANT facts. A literal in the code would be the `bed_plan`
mistake repeated, and it is the mistake this whole layer exists against.
"""
import io as _io
import os as _os
here = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))
for name in ('ticketchecks.py', 'ticketpage.py', 'ticketqc.py',
'knowledge.py'):
path = _os.path.join(here, 'lib', 'vibe', name)
text = _io.open(path, encoding='utf-8').read()
assert 'KP SOLID' not in text, name
assert 'KP 2in' not in text, name
assert 'KP 3in' not in text, name
assert 'Elevation Top 1' not in text, name
assert 'SOLID_ZONE_RECTANGULAR' not in text, name
# ==================================================================== probes
#
# THE `RAN-EMPTY -> "no probe declared"` CLASS, closed 2026-08-31 on the
# owner's standing order: "qc别再给我出这么多not-run。看看能跑的,能做的,现在
# 都做了去。" Seven checks still had no probe -- `check_lifter_capacity`,
# `check_handling_result`, `check_member_interference`, `check_picking_match`,
# `check_ticket_set`, `check_sheet_name`, `check_invisible` -- and any one of
# them returning empty printed a red NOT RUN carrying a sentence about the
# TOOL on a row that may well have been fine.
#
# The guard below is deliberately universal rather than a list: a list would
# have to be edited by the same person who forgot the probe.
def test_every_registered_check_has_a_skip_probe():
"""The one guard that makes "a new check owes a probe" enforceable.
`CLAUDE.md` ss5 has said it in prose since the mechanism was built, and
prose is what let the count sit at seven. A check with no probe is not a
check that works -- it is a NOT RUN row nobody can act on, shipped.
"""
orphans = sorted([fn.__name__ for fn in TC.CHECKS
if fn.__name__ not in TC._SKIP_PROBES])
assert not orphans, (
'these registered checks have no entry in _SKIP_PROBES, so an empty '
'return from any of them renders NOT RUN reading "no probe '
'declared" -- a sentence about the tool, in red, on a row that may '
'be fine: {0}'.format(', '.join(orphans)))
def test_no_probe_is_registered_for_a_check_that_does_not_exist():
"""The other direction, and it is silent: a probe keyed by a renamed or
deleted function never fires, and the row it was written for is back to
"no probe declared" with nothing saying why."""
registered = set([fn.__name__ for fn in TC.CHECKS])
stray = sorted([name for name in TC._SKIP_PROBES
if name not in registered])
assert not stray, (
'these skip probes are keyed by a name that is not in TC.CHECKS, so '
'they can never run: {0}'.format(', '.join(stray)))
def test_every_probe_returns_text_or_none_and_never_an_empty_string():
"""`''` is the one return the contract forbids: it is falsy, so `run_all`
reads it as "ran and found nothing" -- a probe that meant to explain a
silence turning that silence into a green row."""
checked = 0
for name in sorted(TC._SKIP_PROBES):
try:
answer = TC._SKIP_PROBES[name]({}, {}, _know(), None)
except Exception:
# A probe that reaches the Revit API cannot be asked here, and
# `run_all` already treats a raising probe as its own reported
# state ('probe raised'). It is skipped, not excused: this test
# counts what it managed to ask so it cannot rot into a loop
# over nothing.
continue
checked += 1
assert answer is None or (isinstance(answer, str) and answer.strip()), (
'{0}: a probe returns a reason or None, never {1!r}'
.format(name, answer))
assert checked > len(TC._SKIP_PROBES) // 2
def test_the_probes_for_the_three_checks_that_cannot_be_silent_say_so():
"""1.1, 1.3 and 8.2 answer with a severity and a sentence for every
input, including no sheets at all. Their probes therefore never fire in
normal running -- and when one does, the row must name the check and say
the silence is a defect in the tool rather than a verdict."""
for name, row in (('check_sheet_name', '1.1'),
('check_ticket_set', '1.3'),
('check_invisible', '8.2')):
said = TC._SKIP_PROBES[name]({}, {}, _know(), None)
assert said and row in said and name in said
assert 'defect in the tool' in said
def test_the_lifter_capacity_probe_separates_none_from_unrecognised():
""""this piece carries no insert" and "none of its inserts matches the
plant's family rule" are different answers with different repairs, and
the sentence names both."""
said = TC._SKIP_PROBES['check_lifter_capacity']({}, {}, _know(), None)
assert 'families.lifter_include' in said
assert TC._SKIP_PROBES['check_lifter_capacity'](
{'lifters': [{'family': 'X'}]}, {}, _know(), None) is None
def test_the_interference_probe_names_the_switch_and_stays_quiet_when_on():
"""The clean path -- geometry solved, nothing interfering -- is also an
empty return, and the probe MUST return None for it or a correct piece
reads NOT RUN."""
off = TC._SKIP_PROBES['check_member_interference']({}, {}, {}, None)
assert 'interference.check is off' in off
know = {'interference': {'check': True}}
assert 'panel' in TC._SKIP_PROBES['check_member_interference'](
{}, {}, know, None)
assert TC._SKIP_PROBES['check_member_interference'](
{'panel_id': 7}, {}, know, None) is None
def test_the_picking_probe_names_the_reading_failure_as_the_tools():
"""Six silent paths, and the last one is a READING failure: the printed
band did not parse. "the ticket is wrong" and "we could not read it" are
the two answers this whole tool exists to keep apart."""
know = {'handling': {'lifter_groups': {'face': {'families': ['P136']}}}}
assert 'no sheet has been issued' in TC._SKIP_PROBES[
'check_picking_match']({}, {}, know, None)
assert 'length' in TC._SKIP_PROBES['check_picking_match'](
{'sheets': [object()]}, {}, know, None)
assert 'face.families is empty' in TC._SKIP_PROBES['check_picking_match'](
{'sheets': [object()], 'length': 24.9}, {}, {}, None)
said = TC._SKIP_PROBES['check_picking_match'](
{'sheets': [object()], 'length': 24.9}, {}, know, None)
assert 'could not be recovered' in said
assert 'Nothing about the modelled inserts is claimed' in said
# ============================================================================
# MK0625: INTERFERENCE-EXEMPT THE SAME WAY SR5602 IS FOR P76
# ============================================================================
#
# Owner, 2026-09-02, on the live sample_job model, verbatim: "MC厂知识:MK0625是
# QL589的tension bar,不与任何东西干涉。" -- MK0625 is QL589's tension bar
# and interferes with nothing. Same shape as the other plant's SR5602/
# SR6999 being the bars welded to the P76 embed: fully exempt via
# `interference.exempt_families`, and also grouped with its lifter under
# `interference.component_groups` so the assembly's own members are
# explained even in a shape that does not lean on the exemption alone.
#
# `_exempt` and `_grouped` are pure decisions over dicts -- no Revit, no
# geometry solved -- so this is a direct unit test, same as the rest of
# this file.
def test_mk0625_is_interference_exempt_the_same_way_sr5602_is():
know = {'interference': {
'exempt_families': ['MK0625'],
'component_groups': [
{'name': 'LIFT_QL589G_8T ASSEMBLY',
'families': ['LIFT_QL589G_8T', 'MK0625'],
'interferes': 'within_group'}]}}
boxes = [{'id': 1, 'family': 'MK0625'},
{'id': 2, 'family': 'LIFT_QL589G_8T'},
{'id': 3, 'family': 'SOME_OTHER_EMBED'}]
# exempt_families alone already keeps MK0625 out of every pair, exactly
# the mechanism SR5602/SR6999 rely on at the other plant
exempt, unexplained = TC._exempt({}, boxes, know, None)
assert exempt == set([1])
assert unexplained == []
# the component group also explains the assembly's own members
# touching each other -- belt-and-suspenders with the exemption above,
# mirroring the "EMBED P76" group exactly
groups = TC._component_groups(know)
assert TC._grouped('MK0625', 'LIFT_QL589G_8T', groups) is True
# but it is not a blanket exemption for the lifter itself against a
# THIRD family outside the group
assert TC._grouped('LIFT_QL589G_8T', 'SOME_OTHER_EMBED', groups) is False
# ============================================================================
# 2.2: reconstruction NOT APPLICABLE (WP100, 2026-09-08, `_scratch/wp100_
# weight.md`) -- `liftexport._reconstructed_ft3` can find a wythe whose
# thickness is not constant (a step or taper it has no way to represent) and
# stand ONLY the reconstruction leg down, naming why, while the measured leg
# (which never depends on it) is still read normally. `check_weight_three_way`
# must then compare printed vs measured through
# `ticketpage.weight_agreement_no_reconstruction` -- which, unlike the
# generic two-way path, CAN say OK -- rather than the ordinary `WGT-READ`
# ("suspect the tool first") that fired live on WP100 before this fix.
#
# `_weight_sources` caches its answer on the ticket (`t['_weight_sources']`),
# exactly the seam `_ticket()` above already uses for 4.7 -- so these tests
# prime that cache directly and never touch a panel, a frame or a solid,
# the same trick this file's docstring describes for 4.7 and 8.1.
def _weight_ticket(sources, weight_lb=None):
t = {'mark': 'TEST-1', 'weight_lb': weight_lb}
full = {'printed': None, 'reconstructed': None, 'measured': None,
'stated': weight_lb, 'reconstruction_reason': None, 'why': {}}
full.update(sources)
t['_weight_sources'] = full
return t
def test_222_reconstruction_not_applicable_and_printed_measured_agree_is_ok():
"""The WP100 shape itself: printed 9,045 vs measured 9,115 (0.77% apart,
inside 2%) with the reconstruction stood down as not applicable."""
reason = ('wythe 1 of 2 (its own solid measures 41.22 ft3) does not '
'reconcile ... stepped or tapered wythe')
t = _weight_ticket({'printed': 9045.0, 'measured': 9115.0,
'stated': 9115.0, 'reconstruction_reason': reason,
'why': {'reconstructed': reason}})
out = TC.check_weight_three_way(t, {}, _know(), None)
assert len(out) == 1
assert out[0].severity == 'OK'
assert out[0].code == 'WGT-OK'
assert 'not applicable' in out[0].message
assert 'stepped or tapered' in out[0].message
def test_222_reconstruction_not_applicable_but_printed_measured_disagree_warns():
reason = 'stepped or tapered wythe'
t = _weight_ticket({'printed': 8000.0, 'measured': 9115.0,
'stated': 9115.0, 'reconstruction_reason': reason,
'why': {'reconstructed': reason}})
out = TC.check_weight_three_way(t, {}, _know(), None)
assert out[0].severity == 'WARN'
assert out[0].code == 'WGT-TWO-WAY-NA'
assert 'not applicable' in out[0].message
def test_222_reconstruction_missing_for_an_ordinary_reason_still_never_ok():
"""A reconstruction absent for any OTHER reason (concrete unreadable, the
geometry switch, ...) must still go through the generic two-way path,
which is deliberately never OK -- only the NAMED not-applicable state
(`reconstruction_reason` set) gets the new treatment."""
t = _weight_ticket({'printed': 9045.0, 'measured': 9115.0,
'stated': 9115.0, 'reconstruction_reason': None,
'why': {'reconstructed': 'some unrelated reason'}})
out = TC.check_weight_three_way(t, {}, _know(), None)
assert out[0].severity == 'WARN'
assert out[0].code == 'WGT-TWO-WAY'
def test_222_three_way_is_unchanged_when_reconstruction_is_present():
"""Regression: a normal three-way (reconstruction present and used)
must not be touched by any of the above -- same OK it always gave."""
t = _weight_ticket({'printed': 9115.0, 'measured': 9115.0,
'reconstructed': 9115.0, 'stated': 9115.0})
out = TC.check_weight_three_way(t, {}, _know(), None)
assert out[0].severity == 'OK'
assert out[0].code == 'WGT-OK'
assert 'not applicable' not in out[0].message