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

5128 lines
226 KiB
Python

# -*- coding: utf-8 -*-
"""Tests for the Revit-free half of vibe.knowledge.
`load()`, `sources()`, `write_starter()` and `write_plant_starter()` all need
`vibe.designspec` (imported inside those functions on purpose -- see the note
at the top of knowledge.py) and, through it, a live Revit document. Out of
scope here.
Everything else -- merging plain dicts, coercing YAML scalars, mapping the
`decisions:` block back onto `handling.*`, and the sentences `config_problems`
produces -- takes and returns plain dicts, which is what is tested below,
mostly built from `knowledge.DEFAULTS` directly so a test failure means the
real merge logic is wrong, not that a fixture drifted from it.
"""
from vibe import knowledge
from vibe import ticketpage
# ------------------------------------------------------------- _deep_update
def test_deep_update_merges_nested_dicts_key_by_key():
base = {'a': {'x': 1, 'y': 2}, 'b': 1}
override = {'a': {'y': 3, 'z': 4}}
knowledge._deep_update(base, override)
assert base == {'a': {'x': 1, 'y': 3, 'z': 4}, 'b': 1}
def test_deep_update_adds_new_keys():
base = {'a': 1}
knowledge._deep_update(base, {'b': 2})
assert base == {'a': 1, 'b': 2}
def test_deep_update_replaces_lists_wholesale_rather_than_merging():
base = {'a': [1, 2, 3]}
knowledge._deep_update(base, {'a': [9]})
assert base['a'] == [9]
def test_deep_update_a_non_dict_override_replaces_a_dict_value_entirely():
# Only two DICTS recurse. An override that hands a scalar where the base
# had a dict does not get merged into it -- it replaces the whole branch.
base = {'handling': {'rig_mode': 'rolling', 'sling_angle_deg': 60}}
knowledge._deep_update(base, {'handling': 'disabled'})
assert base['handling'] == 'disabled'
def test_deep_update_returns_the_mutated_base():
base = {'a': 1}
result = knowledge._deep_update(base, {'b': 2})
assert result is base
# ---------------------------------------------------------- _merge_decisions
def test_merge_decisions_maps_the_simple_keys_onto_handling():
merged = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._merge_decisions(merged, {
'report_dir': 'C:/reports',
'open_report_when_done': False,
'prestress_loss_pct': 6.5,
'transfer_length_in': 30,
})
handling = merged['handling']
assert handling['report_dir'] == 'C:/reports'
assert handling['open_report'] is False
assert handling['prestress_loss_pct'] == 6.5
assert handling['transfer_length_in'] == 30
def test_merge_decisions_coerces_yaml_style_strings():
# load() reads a project config through _merge_file first, which coerces
# every scalar with _coerce() before _merge_decisions ever sees it -- but
# _merge_decisions coerces the four scalar keys itself too, so it is
# correct even fed a raw, uncoerced dict directly.
merged = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._merge_decisions(merged, {
'open_report_when_done': 'false',
'prestress_loss_pct': '6.5',
})
assert merged['handling']['open_report'] is False
assert merged['handling']['prestress_loss_pct'] == 6.5
def test_merge_decisions_default_rig_mode_applies_to_the_whole_job():
merged = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._merge_decisions(merged, {'rig_mode': {'default': 'static'}})
assert merged['handling']['rig_mode'] == 'static'
# None of the stages named a per-stage override, so every stage's own
# rig_mode is left at whatever DEFAULTS had (None -- inherit).
for stage in merged['handling']['stages']:
assert stage['rig_mode'] is None
def test_merge_decisions_per_stage_rig_mode_overrides_only_that_stage():
merged = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._merge_decisions(merged, {
'rig_mode': {'default': 'static', 'stripping': 'rolling',
'transportation': None, 'erection': None},
})
by_name = dict((s['name'], s['rig_mode']) for s in merged['handling']['stages'])
assert merged['handling']['rig_mode'] == 'static'
assert by_name['stripping'] == 'rolling'
# Left blank -- inherits the job default rather than being forced.
assert by_name['transportation'] is None
assert by_name['erection'] is None
def test_merge_decisions_blank_default_does_not_clobber_the_existing_one():
merged = knowledge._deep_copy(knowledge.DEFAULTS)
original = merged['handling']['rig_mode']
knowledge._merge_decisions(merged, {'rig_mode': {'default': None}})
assert merged['handling']['rig_mode'] == original
def test_merge_decisions_ignores_a_rig_mode_that_is_not_a_dict():
merged = knowledge._deep_copy(knowledge.DEFAULTS)
original = knowledge._deep_copy(merged['handling'])
knowledge._merge_decisions(merged, {'rig_mode': 'static'})
assert merged['handling'] == original
def test_merge_decisions_with_no_decisions_block_changes_nothing():
merged = knowledge._deep_copy(knowledge.DEFAULTS)
original = knowledge._deep_copy(merged)
result = knowledge._merge_decisions(merged, None)
assert merged == original
assert result is merged
def test_merge_decisions_falsy_report_dir_is_left_alone():
merged = knowledge._deep_copy(knowledge.DEFAULTS)
merged['handling']['report_dir'] = '/existing/path'
knowledge._merge_decisions(merged, {'report_dir': ''})
assert merged['handling']['report_dir'] == '/existing/path'
def test_merge_decisions_group_tolerance_maps_onto_handling():
merged = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._merge_decisions(merged, {'group_tolerance_ft': 1.25})
assert merged['handling']['group_tolerance_ft'] == 1.25
def test_merge_decisions_a_blank_group_tolerance_leaves_the_default_alone():
"""Blank means 'not said' -- the layered default stands, never zero."""
merged = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._merge_decisions(merged, {'group_tolerance_ft': None})
assert merged['handling']['group_tolerance_ft'] == \
knowledge.DEFAULTS['handling']['group_tolerance_ft']
def test_merge_decisions_an_unparsable_group_tolerance_is_not_guessed_at():
merged = knowledge._deep_copy(knowledge.DEFAULTS)
original = merged['handling']['group_tolerance_ft']
knowledge._merge_decisions(merged, {'group_tolerance_ft': 'one foot'})
assert merged['handling']['group_tolerance_ft'] == original
def test_a_bad_group_tolerance_is_reported_not_inherited():
# Judged where it is dropped, in the merge -- NOT re-read afterwards from
# `decisions.group_tolerance_ft`, which no merge ever puts in the tree.
for bad in ('one foot', -1.0):
merged = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._merge_decisions(merged, {'group_tolerance_ft': bad})
problems = knowledge.config_problems(None, merged)
assert any('group_tolerance_ft' in p for p in problems), bad
def test_a_good_group_tolerance_is_not_reported():
merged = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._merge_decisions(merged, {'group_tolerance_ft': 1.0})
assert knowledge.config_problems(None, merged) == []
# ------------------------------------------------------------- config_problems
def test_config_problems_empty_knowledge_is_silent():
assert knowledge.config_problems(None, None) == []
assert knowledge.config_problems(None, {}) == []
def test_config_problems_reports_a_missing_config_file():
problems = knowledge.config_problems(
None, {'_config_missing': 'C:/job/active-config.yaml'})
assert len(problems) == 1
assert 'C:/job/active-config.yaml' in problems[0]
assert 'is not there' in problems[0]
def test_config_problems_bad_dunnage_mode():
problems = knowledge.config_problems(
None, {'handling': {'dunnage': {'mode': 'bogus'}}})
assert len(problems) == 1
assert 'bogus' in problems[0]
assert 'lifter_rows' in problems[0]
assert 'fraction' in problems[0]
def test_config_problems_fraction_mode_needs_a_fraction():
problems = knowledge.config_problems(
None, {'handling': {'dunnage': {'mode': 'fraction', 'fraction': None}}})
assert len(problems) == 1
assert 'between 0 and 0.5' in problems[0]
def test_config_problems_fraction_mode_rejects_out_of_range():
problems = knowledge.config_problems(
None, {'handling': {'dunnage': {'mode': 'fraction', 'fraction': 0.6}}})
assert len(problems) == 1
assert '0.6' in problems[0]
def test_config_problems_fraction_mode_rejects_a_non_number():
problems = knowledge.config_problems(
None,
{'handling': {'dunnage': {'mode': 'fraction', 'fraction': 'lots'}}})
assert len(problems) == 1
assert 'lots' in problems[0]
def test_config_problems_valid_dunnage_settings_are_silent():
assert knowledge.config_problems(
None, {'handling': {'dunnage': {'mode': None}}}) == []
assert knowledge.config_problems(
None, {'handling': {'dunnage': {'mode': 'lifter_rows'}}}) == []
assert knowledge.config_problems(
None,
{'handling': {'dunnage': {'mode': 'fraction', 'fraction': 0.2}}}) == []
# ------------------------------------------- insert_position, per run mode
def test_insert_position_maps_onto_both_handling_keys():
"""One decision, two keys, opposite senses. If this mapping is missing the
section is decoration: editing the obvious place would do nothing."""
merged = {'handling': {}}
knowledge._merge_decisions(merged, {
'insert_position': {'one_piece': 'precise', 'batch': 'fast'}})
assert merged['handling']['fast_lifter_box'] is False
assert merged['handling']['fast_lifter_box_batch'] is True
def test_insert_position_accepts_the_shipped_defaults_round_trip():
merged = {'handling': {}}
knowledge._merge_decisions(merged, {
'insert_position': {'one_piece': 'fast', 'batch': 'precise'}})
assert merged['handling']['fast_lifter_box'] is True
assert merged['handling']['fast_lifter_box_batch'] is False
def test_insert_position_leaves_a_blank_mode_alone():
merged = {'handling': {'fast_lifter_box': True}}
knowledge._merge_decisions(merged, {
'insert_position': {'one_piece': None, 'batch': 'precise'}})
assert merged['handling']['fast_lifter_box'] is True
assert merged['handling']['fast_lifter_box_batch'] is False
def test_a_misspelled_insert_position_is_not_guessed_at():
"""'quick' is not 'fast'. Guessing which one a typo meant is how a setting
nobody checked ends up deciding a moment arm."""
merged = {'handling': {'fast_lifter_box': True}}
knowledge._merge_decisions(merged, {
'insert_position': {'one_piece': 'quick'}})
assert merged['handling']['fast_lifter_box'] is True
def test_a_misspelled_insert_position_is_reported():
merged = {'handling': {}}
knowledge._merge_decisions(merged, {'insert_position': {'batch': 'quick'}})
problems = knowledge.config_problems(None, merged)
assert any('insert_position.batch' in p for p in problems)
assert any('quick' in p for p in problems)
def test_a_good_insert_position_is_not_reported():
merged = {'handling': {}}
knowledge._merge_decisions(merged, {
'insert_position': {'one_piece': 'fast', 'batch': 'precise'}})
assert knowledge.config_problems(None, merged) == []
# ------------------------- _coerce keeps a label a label (measured 08-30)
def test_a_digit_string_with_a_leading_zero_stays_a_string():
"""A part mark like 0010001 loaded as the int 10001: three characters
deleted and the type changed. Quoting in the YAML does not help --
coercion happens after parsing, so the quotes are long gone by here.
The damage was invisible because it is self-consistent; it only shows
when the value meets a string read from the model."""
assert knowledge._coerce('0010001') == '0010001'
assert knowledge._coerce('03') == '03'
assert knowledge._coerce('00') == '00'
def test_two_marks_in_one_field_keep_one_type():
"""'03' arriving as an int while its companion '03A' stayed a string is
what forced a whole `number` field to be dropped from the catalogue."""
out = knowledge._coerce({'parts': ['03', '03A', '0010001']})
assert [type(v) for v in out['parts']] == [type(u'')] * 3
def test_numbers_are_still_numbers():
"""The coercion exists for a real reason: pyRevit's YAML reader hands
back every scalar as a string, so this must not stop."""
assert knowledge._coerce('12') == 12
assert knowledge._coerce('0') == 0
assert knowledge._coerce('-3') == -3
assert knowledge._coerce('1.5') == 1.5
assert knowledge._coerce('0.5') == 0.5
assert knowledge._coerce('-0.25') == -0.25
assert knowledge._coerce('1e3') == 1000.0
assert knowledge._coerce('null') is None
assert knowledge._coerce('true') is True
assert knowledge._coerce('no') is False
def test_a_leading_zero_label_survives_a_whole_merged_tree():
merged = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._deep_update(merged, knowledge._coerce(
{'details': {'13/X4.2': {'parts': ['0010001'], 'qty': '2'}}}))
entry = merged['details']['13/X4.2']
assert entry['parts'] == ['0010001']
assert entry['qty'] == 2
# ------------------- 6.5 note views: the declared shape (measured 08-30)
def test_note_defaults_declare_nothing_so_the_check_reports_not_run():
assert knowledge.DEFAULTS['notes']['instruction_views'] == []
assert knowledge.DEFAULTS['notes']['instruction_view_sets'] == {}
def test_plant_starter_names_no_plant():
"""The starter ships in the repo; every name in it must be a
placeholder, never a real view from a real plant."""
text = knowledge.PLANT_STARTER
for real in ('PANEL - DUNNAGE', 'PICKING/STRIPPING', '(V3)'):
assert real not in text
def test_full_length_bar_fraction_defaults_to_null_and_stands_52_down():
"""5.2's bar classification needs a number the standard never states.
Like rails.max_bed_width_in it defaults to None, so a plant that has
not recorded it gets a stood-down check rather than a guessed one."""
from vibe import knowledge as K
assert K.DEFAULTS['drawings']['full_length_bar_fraction'] is None
assert K.get({}, 'drawings.full_length_bar_fraction') is None
assert K.DEFAULTS['drawings']['reinforcing_families'] == []
# ------------------------------------ lifter safety factors (public layer)
#
# tension_fos / shear_fos were live inputs in panellift that NOTHING could
# set. They are lifter properties (Will 2026-08-30), so they belong beside
# the lifter ratings in the one knowledge layer that ships in the repo.
#
# AND BESIDE THE RATING MEANS BESIDE THE PART, which is the correction made
# 2026-08-31. They landed here as ONE file-wide pair, 4.0 / 2.67, and both
# halves of that were defects. The 2.67 had no published source; the shape
# meant every manufacturer's insert in the file drew on one number. ALP
# catalog p53 (02/2024) heads its capacity table "Capacity in Concrete 4:1
# SWL" across BOTH load columns, so the parts the pair actually reached
# publish 4:1 in shear, and they now say so in their own entries.
def test_no_file_wide_safety_factor_survives_in_the_defaults():
"""Null, and deliberately not 4.0. A global 4.0 is the same defect with
a better number: the next manufacturer's insert inherits a factor read
off ALP's page."""
from vibe import knowledge
assert knowledge.get(knowledge.DEFAULTS,
'catalogue.safety_factors.tension_fos') is None
assert knowledge.get(knowledge.DEFAULTS,
'catalogue.safety_factors.shear_fos') is None
def test_the_shipped_catalogue_file_agrees_with_the_defaults():
"""The YAML and the built-in default must not drift apart -- a value
that differs between the file and the code moves a computed number
depending on whether the file was found."""
import io
from vibe import knowledge, yamlkit
data = yamlkit.loads(
io.open(knowledge.catalogue_path(), encoding='utf-8').read())
factors = data['catalogue']['safety_factors']
assert factors['tension_fos'] is None
assert factors['shear_fos'] is None
assert factors['unknown_because'], (
'a null that decides a safety check owes a written reason -- '
'the layer law, and the reason 2.67 lasted twelve days')
# and the catalogue's own insert ratings are still readable past the
# new block -- it is inserted BEFORE `inserts`, so nothing shifted
assert 'QLPD4T5WG' in data['catalogue']['inserts']
def test_the_factor_resolves_per_part_and_is_never_borrowed():
"""The resolution `lifter_safety_factor` exists for. QLPD4T5WG publishes
4:1 in both directions; QL589SPG, in the same file, publishes 4,010 lb
shear against 7,690 lb tension -- nothing like 1:1 -- and has no factor
of its own. It must come back unjudged, not carrying the disk anchor's
number."""
from vibe import knowledge as K
know = {'lifters': {'catalogue_alias': {'ANCHOR_A': 'QLPD4T5WG',
'ANCHOR_B': 'QL589SPG'}},
'catalogue': _shipped_catalogue()}
value, source, why = K.lifter_safety_factor(know, 'ANCHOR_A',
kind='shear')
assert (value, source) == (4.0, 'published')
assert 'p53' in why and '02/2024' in why
value, source, why = K.lifter_safety_factor(know, 'ANCHOR_B',
kind='shear')
assert (value, source) == (None, None)
assert 'borrowed' in why
def _shipped_catalogue():
import io
from vibe import knowledge, yamlkit
return yamlkit.loads(
io.open(knowledge.catalogue_path(), encoding='utf-8').read()
)['catalogue']
# ---------------------------------------- minimum anchor spacing (6.7)
#
# ALP catalog p53, 02/2024, footnote: "Minimum anchor spacing is 2x the
# published edge distance". The owner handed over that page and this half
# of it never landed -- nothing in the tool had ever asked how far apart two
# anchors must sit, so two 8T anchors 20 in apart passed every check while
# the page asked for 28.
def test_the_spacing_minimum_is_derived_from_the_part_s_own_edge_distance():
from vibe import knowledge as K
know = {'lifters': {'catalogue_alias': {'ANCHOR_8T': 'QLPD8T8WG'}},
'catalogue': _shipped_catalogue()}
value, source, why = K.lifter_spacing_minimum(know, 'ANCHOR_8T')
assert (value, source) == (28.0, 'derived') # 2 x 14"
assert '2x' in why and '14"' in why, why
assert 'p53' in why
def test_a_published_spacing_overrides_the_multiple():
"""The override the shape exists for: a sheet stating a spacing outright
beats a rule of thumb about the same part."""
from vibe import knowledge as K
catalogue = _shipped_catalogue()
catalogue['inserts']['QLPD8T8WG'] = dict(
catalogue['inserts']['QLPD8T8WG'])
catalogue['inserts']['QLPD8T8WG']['min_spacing_rule'] = {
'published_in': 24, 'multiple_of_edge_distance': 2.0,
'source': 'a datasheet that states it outright'}
know = {'lifters': {'catalogue_alias': {'ANCHOR_8T': 'QLPD8T8WG'}},
'catalogue': catalogue}
value, source, _why = K.lifter_spacing_minimum(know, 'ANCHOR_8T')
assert (value, source) == (24.0, 'published')
def test_a_part_with_no_edge_distance_to_double_stands_down_naming_it():
"""The honest silence. QL589SPG carries no published edge distance, so
2x it is not a number -- and the answer is a sentence, never another
part's minimum."""
from vibe import knowledge as K
catalogue = _shipped_catalogue()
entry = dict(catalogue['inserts']['QL589SPG'])
entry['min_spacing_rule'] = {'multiple_of_edge_distance': 2.0}
catalogue['inserts']['QL589SPG'] = entry
know = {'lifters': {'catalogue_alias': {'ANCHOR_E': 'QL589SPG'}},
'catalogue': catalogue}
value, source, why = K.lifter_spacing_minimum(know, 'ANCHOR_E')
assert (value, source) == (None, None)
assert 'nothing to double' in why
def test_a_part_the_catalogue_does_not_cover_is_not_given_a_spacing():
from vibe import knowledge as K
value, source, why = K.lifter_spacing_minimum({}, 'SOMETHING_ELSE')
assert (value, source) == (None, None)
assert 'not written down' in why
# ============================================================================
# The shape a real project config file has
# ============================================================================
#
# Every config_problems test above hands the function a knowledge dict with a
# `decisions` branch already in it. NO PRODUCTION PATH EVER BUILDS THAT TREE:
# `_merge_config_file` merges only `holder['knowledge']`, and `_merge_decisions`
# maps the decisions block into `handling` without leaving a `decisions` branch
# behind. So those tests were green while the same typo in a real file was
# accepted in silence.
#
# These tests go through `_merge_config_file` on a file written in the shape
# `configdump.build()` writes -- `decisions:` at the top, one `knowledge:`
# block underneath -- and the text is produced by configdump's OWN writer, so
# the fixture cannot drift away from what the tool writes.
import io
import os
import sys
import types
import pytest
@pytest.fixture
def yaml_on_host(monkeypatch):
"""`pyrevit.coreutils.yaml` for host CPython -- as UNFORGIVING as the real one.
`_merge_file` imports the pyrevit reader and swallows every exception, so
without this the merge quietly does nothing and a test of it would pass
for the wrong reason. Every test below also asserts that a GOOD value
landed, which is what proves the file was really read.
THE PARSE IS yamlkit's; THE TYPES ARE YamlDotNet's. This fixture used to
hand back yamlkit's output directly, and yamlkit TYPES its scalars, so it
was strictly kinder than production -- see `pyrevit_yaml_fake.py`, where the
stringifier and the reasons live. `knowledge._merge_file` runs `_coerce()`
on what it gets and so is unaffected, which is precisely the property
these tests should be PROVING rather than assuming.
"""
from pyrevit_yaml_fake import (install_pyrevit_yaml,
strings_only_yaml_reader)
install_pyrevit_yaml(monkeypatch, strings_only_yaml_reader())
def _write_config(tmp_path, decisions, know=None, wrapped=True):
"""A project config file, written the way configdump.build() writes one."""
from vibe import configdump
know = know if know is not None else {'handling': {'rig_mode': 'rolling'}}
lines = list(configdump.emit({'decisions': decisions}))
lines.append(u'# the merged configuration, as build() writes it')
if wrapped:
lines.extend(configdump.emit({'knowledge': know}))
else:
lines.extend(configdump.emit(know))
path = tmp_path / 'active-config.yaml'
path.write_text(u'\n'.join(lines) + u'\n', encoding='utf-8')
return str(path)
def _merged(path):
merged = {'handling': {}}
knowledge._merge_config_file(merged, path)
return merged
def test_a_misspelled_insert_position_in_a_real_config_file_is_reported(
tmp_path, yaml_on_host):
"""The failure this whole section exists for: `fastt` in the file the
tool writes was accepted in silence and the run looked clean."""
path = _write_config(tmp_path, {
'insert_position': {'one_piece': 'fastt', 'batch': 'precise'}})
merged = _merged(path)
# the file really was read: the good half of the setting landed
assert merged['handling']['fast_lifter_box_batch'] is False
# and the typo was neither guessed at nor swallowed
assert 'fast_lifter_box' not in merged['handling']
problems = knowledge.config_problems(None, merged)
assert any('insert_position.one_piece' in p for p in problems), problems
assert any('fastt' in p for p in problems), problems
def test_a_bad_group_tolerance_in_a_real_config_file_is_reported(
tmp_path, yaml_on_host):
path = _write_config(tmp_path, {'group_tolerance_ft': 'one foot',
'prestress_loss_pct': 6.5})
merged = _merged(path)
assert merged['handling']['prestress_loss_pct'] == 6.5 # file was read
assert 'group_tolerance_ft' not in merged['handling']
problems = knowledge.config_problems(None, merged)
assert any('group_tolerance_ft' in p for p in problems), problems
def test_a_zero_group_tolerance_in_a_real_config_file_is_reported(
tmp_path, yaml_on_host):
path = _write_config(tmp_path, {'group_tolerance_ft': 0})
problems = knowledge.config_problems(None, _merged(path))
assert any('group_tolerance_ft' in p for p in problems), problems
def test_a_good_config_file_is_silent(tmp_path, yaml_on_host):
path = _write_config(tmp_path, {
'group_tolerance_ft': 1.25,
'insert_position': {'one_piece': 'fast', 'batch': 'precise'}})
merged = _merged(path)
assert merged['handling']['group_tolerance_ft'] == 1.25
assert knowledge.config_problems(None, merged) == []
def test_the_undocumented_unwrapped_file_shape_is_still_validated(
tmp_path, yaml_on_host):
"""A file with no `knowledge:` wrapper is merged whole. Undocumented, but
it is what the older tests exercised, so it keeps working -- and the
checks have to fire on it too, or fixing one shape breaks the other."""
path = _write_config(tmp_path, {
'insert_position': {'batch': 'quick'}}, wrapped=False)
merged = _merged(path)
assert merged['handling']['rig_mode'] == 'rolling' # merged whole
problems = knowledge.config_problems(None, merged)
assert any('insert_position.batch' in p for p in problems), problems
# reported once, not once per shape
assert len([p for p in problems if 'insert_position.batch' in p]) == 1
def test_a_misspelled_dunnage_mode_in_the_decisions_block_is_reported(
tmp_path, yaml_on_host):
"""`decisions.dunnage` is written into every config file with its own
VALID VALUES comment, and nothing mapped it back: editing the obvious
place did nothing, and the typo was never reported either."""
path = _write_config(tmp_path, {'dunnage': {'mode': 'lifter_row'}})
merged = _merged(path)
assert merged['handling']['dunnage']['mode'] == 'lifter_row'
assert any('dunnage.mode' in p
for p in knowledge.config_problems(None, merged))
def test_a_dunnage_decision_reaches_the_key_that_is_read(
tmp_path, yaml_on_host):
path = _write_config(tmp_path, {'dunnage': {'mode': 'fraction',
'fraction': 0.2}},
know={'handling': {'dunnage': {'mode': None}}})
merged = _merged(path)
assert merged['handling']['dunnage'] == {'mode': 'fraction',
'fraction': 0.2}
assert knowledge.config_problems(None, merged) == []
# ============================================================================
# FACE DESIGNATIONS, and the per-plant knowledge layer that selects them
# ============================================================================
#
# `SR324 (F.F.)`, `SR310 (E.L.)`, `P76 W/ SR5602 (S.I.F.)` -- the parenthetical
# suffix says which face of the piece the part goes on, and checklist 5.3 asks
# whether it agrees with the model. The abbreviations are the trade's and are
# published in knowledge/catalogue.yaml; WHICH SET a producer uses is a house
# habit and is selected, by id, in the plant layer.
#
# The thing these tests exist to defend is the FRAME OF REFERENCE. near/far is
# measured from the viewing direction and flips when the view flips; in-form is
# measured from the casting bed and does not. Flatten the two into one
# token -> meaning map and 5.3 stops being decidable while every individual
# call-out still reads perfectly normal. So every assertion below that touches
# a token also asserts the frame it came back with.
def _catalogue_face_designations():
"""The shipped public layer, read the way `_merge_file` reads it."""
import io
from vibe import knowledge as K, yamlkit
data = yamlkit.loads(
io.open(K.catalogue_path(), encoding='utf-8').read())
return K._coerce(data['face_designations'])
def test_the_shipped_catalogue_publishes_both_face_vocabularies():
branch = _catalogue_face_designations()
assert set(branch['vocabularies']) >= {'near_far', 'in_form'}
assert branch['frames']
def test_each_vocabulary_declares_a_frame_the_catalogue_also_publishes():
"""A frame is not free text. A vocabulary naming one that is not in
`frames` cannot be resolved against anything, and the check that reads it
would have to guess -- which is the one thing it must not do."""
branch = _catalogue_face_designations()
for vocab_id, vocab in branch['vocabularies'].items():
assert vocab['frame'] in branch['frames'], vocab_id
def test_the_two_families_are_in_different_frames_and_not_one_flat_table():
branch = _catalogue_face_designations()
assert branch['vocabularies']['near_far']['frame'] == 'view'
assert branch['vocabularies']['in_form']['frame'] == 'form'
def test_the_published_tokens_are_the_ones_printed_on_real_tickets():
branch = _catalogue_face_designations()
def tokens(vocab_id):
return dict([(e['token'], e['side'])
for e in branch['vocabularies'][vocab_id]['tokens']])
assert tokens('near_far') == {'N.F.': 'near', 'F.F.': 'far',
'E.L.': 'both'}
assert tokens('in_form') == {'T.I.F.': 'top', 'B.I.F.': 'bottom',
'S.I.F.': 'side'}
def test_each_layer_is_named_by_a_token_that_does_not_flip_with_the_view():
"""E.L. is why `side` is a symbol and not the English of `means`.
"each layer" names BOTH faces, so it is symmetric: flipping the view does
not make it wrong. A check that treated it as one face and flipped it
would invent a finding on every drawing."""
branch = _catalogue_face_designations()
entry = [e for e in branch['vocabularies']['near_far']['tokens']
if e['token'] == 'E.L.'][0]
assert entry['side'] == 'both'
# ------------------------------------------------- selecting, and resolving
def _know(use, extra_vocabularies=None):
"""A merged tree with the shipped catalogue's vocabularies in it."""
merged = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._deep_update(merged,
{'face_designations': _catalogue_face_designations()})
knowledge._deep_update(merged, {'face_designations': {'use': use}})
if extra_vocabularies:
knowledge._deep_update(
merged, {'face_designations':
{'vocabularies': extra_vocabularies}})
return merged
def test_nothing_is_in_force_until_a_plant_selects_it():
"""The catalogue publishes both vocabularies to every plant. Publishing
is not selecting: a plant that has said nothing uses nothing, and the 5.3
check has to stand down rather than pick a set for it."""
merged = _know([])
assert knowledge.face_vocabularies(merged) == []
assert knowledge.face_designation(merged, 'F.F.') is None
# ...and silently, because saying nothing is not an error
assert knowledge.face_designation_problems(merged) == []
def test_a_plant_selecting_two_vocabularies_gets_the_tokens_of_both():
"""One ticket prints a layer position on one line and a casting
orientation on the next, so the selection is a list and a plant may use
more than one."""
merged = _know(['near_far', 'in_form'])
assert [v for v, _ in knowledge.face_vocabularies(merged)] == \
['near_far', 'in_form']
assert knowledge.face_designation(merged, 'F.F.')['means'] == 'far face'
assert knowledge.face_designation(merged, 'S.I.F.')['means'] == \
'side in form'
assert knowledge.face_designation_problems(merged) == []
def test_a_resolved_designation_always_carries_what_it_is_measured_from():
"""`side` alone is not an answer. 'near' is only meaningful once you know
it is read off the view and therefore flips with it; 'top' is read off
the bed and does not. A caller that got `side` without `frame` would have
no way to tell those apart, which is exactly how a flipped elevation
passes 5.3."""
merged = _know(['near_far', 'in_form'])
near = knowledge.face_designation(merged, 'N.F.')
top = knowledge.face_designation(merged, 'T.I.F.')
assert (near['side'], near['frame']) == ('near', 'view')
assert (top['side'], top['frame']) == ('top', 'form')
assert near['vocabulary'] == 'near_far'
assert near['measured_from']
def test_a_selection_replaces_rather_than_adds_to_the_layer_below():
"""`use` is a LIST, and lists replace. That is the right semantics here:
what a layer names is exactly what is in force, and a vocabulary left
quietly selected from underneath would resolve tokens in a frame nobody
chose."""
merged = _know(['near_far', 'in_form'])
knowledge._deep_update(merged, {'face_designations': {'use': ['in_form']}})
assert [v for v, _ in knowledge.face_vocabularies(merged)] == ['in_form']
assert knowledge.face_designation(merged, 'F.F.') is None
def test_a_plant_may_state_a_token_nobody_published_without_touching_the_catalogue():
"""`vocabularies` is a MAP, so it deep-merges: a plant adds its own and
the published ones are still there."""
merged = _know(['near_far', 'house_bed'], extra_vocabularies={
'house_bed': {
'frame': 'form',
'measured_from': 'the casting bed',
'tokens': [{'token': 'U.S.', 'means': 'up side', 'side': 'top'}],
}})
assert knowledge.face_designation(merged, 'U.S.')['frame'] == 'form'
assert knowledge.face_designation(merged, 'N.F.')['frame'] == 'view'
assert knowledge.face_designation_problems(merged) == []
def test_an_unknown_vocabulary_id_is_reported_and_not_skipped_in_silence():
"""The plant thinks it declared a vocabulary. Every token in it would
come back unknown, one call-out at a time, with nothing anywhere saying
the selection itself was wrong."""
merged = _know(['near_far', 'nera_far'])
problems = knowledge.face_designation_problems(merged)
assert len(problems) == 1
assert 'nera_far' in problems[0]
assert 'near_far' in problems[0] # names what IS defined
# the good half still resolves -- one bad id does not disarm the rest
assert knowledge.face_designation(merged, 'N.F.')['side'] == 'near'
assert knowledge.config_problems(None, merged) == problems
def test_a_vocabulary_with_an_unpublished_frame_is_reported():
merged = _know(['bed'], extra_vocabularies={
'bed': {'frame': 'bed', 'tokens': []}})
problems = knowledge.face_designation_problems(merged)
assert len(problems) == 1
assert "'bed'" in problems[0] and 'view' in problems[0]
def test_two_selected_vocabularies_claiming_one_token_in_different_frames():
"""The one genuinely undecidable case. `S.I.F.` measured from the bed and
`S.I.F.` measured from the view are different questions, and whichever
vocabulary happened to be looked at first would answer it."""
merged = _know(['in_form', 'house'], extra_vocabularies={
'house': {
'frame': 'view',
'tokens': [{'token': 'S.I.F.', 'means': 'something else',
'side': 'near'}],
}})
problems = knowledge.face_designation_problems(merged)
assert len(problems) == 1
assert 'S.I.F.' in problems[0]
assert 'in_form' in problems[0] and 'house' in problems[0]
def test_the_same_token_in_two_vocabularies_of_the_same_frame_is_fine():
"""Two form-relative vocabularies both spelling `S.I.F.` agree about what
it is measured from, so there is nothing undecidable and nothing to say."""
merged = _know(['in_form', 'house'], extra_vocabularies={
'house': {'frame': 'form',
'tokens': [{'token': 'S.I.F.', 'means': 'side in form',
'side': 'side'}]}})
assert knowledge.face_designation_problems(merged) == []
# ============================================================================
# Choosing the plant layer: one folder per plant, named by the JOB
# ============================================================================
#
# `designspec` cannot be imported on host CPython -- it pulls in the Revit API
# at module scope -- so the path arithmetic lives in `vibe.plantpath`, which
# has no Revit dependency and is imported here for real. The `load()` tests
# below stub `vibe.designspec` with a module that delegates the plant path to
# that same real function and fakes only the two things that genuinely need a
# document: where the job file is, and whether a project config is pointed at.
def test_no_plant_named_reads_the_machine_wide_file_exactly_as_before(
monkeypatch, tmp_path):
from vibe import plantpath
monkeypatch.delenv(plantpath.PLANT_ENV, raising=False)
monkeypatch.setenv('APPDATA', str(tmp_path))
assert plantpath.plant_knowledge_path() == \
os.path.join(str(tmp_path), plantpath.PLANT_FOLDER, 'knowledge.yaml')
def test_a_plant_named_is_a_folder_beside_the_machine_wide_file(
monkeypatch, tmp_path):
"""A SECOND PLANT IS A NEW FOLDER, not an edit to the first one's file --
which is the whole point of the rename."""
from vibe import plantpath
monkeypatch.delenv(plantpath.PLANT_ENV, raising=False)
monkeypatch.setenv('APPDATA', str(tmp_path))
root = os.path.join(str(tmp_path), plantpath.PLANT_FOLDER)
assert plantpath.plant_knowledge_path('northgate') == \
os.path.join(root, 'northgate', 'knowledge.yaml')
assert plantpath.plant_knowledge_path('southbay') == \
os.path.join(root, 'southbay', 'knowledge.yaml')
def test_the_environment_variable_naming_one_file_still_wins_untouched(
monkeypatch, tmp_path):
"""The single-file arrangement is what every machine but the author's is
set up for. A silent break there would be worse than the feature."""
from vibe import plantpath
shared = tmp_path / 'share' / 'knowledge.yaml'
shared.parent.mkdir()
shared.write_text(u'nomenclature:\n design_folder: X\n', encoding='utf-8')
monkeypatch.setenv(plantpath.PLANT_ENV, str(shared))
assert plantpath.plant_knowledge_path() == str(shared)
def test_a_machine_pointed_at_one_file_gets_per_plant_folders_beside_it(
monkeypatch, tmp_path):
"""The root is defined as "the folder the machine-wide knowledge.yaml is
in", so reading it backwards off the variable is the definition rather
than a guess -- and a machine already pointing at a file picks up
per-plant folders with no change to the variable."""
from vibe import plantpath
shared = tmp_path / 'share' / 'knowledge.yaml'
shared.parent.mkdir()
shared.write_text(u'x: 1\n', encoding='utf-8')
monkeypatch.setenv(plantpath.PLANT_ENV, str(shared))
assert plantpath.plant_knowledge_path('northgate') == \
os.path.join(str(tmp_path / 'share'), 'northgate', 'knowledge.yaml')
def test_the_environment_variable_may_also_name_the_folder(
monkeypatch, tmp_path):
from vibe import plantpath
monkeypatch.setenv(plantpath.PLANT_ENV, str(tmp_path))
assert plantpath.plant_knowledge_path('northgate') == \
os.path.join(str(tmp_path), 'northgate', 'knowledge.yaml')
assert plantpath.plant_knowledge_path() == \
os.path.join(str(tmp_path), 'knowledge.yaml')
def test_a_plant_id_that_is_not_a_folder_name_resolves_to_nothing(monkeypatch,
tmp_path):
"""It arrives from a YAML file somebody edits by hand, so it is untrusted
input used as a path component. Refused outright rather than scrubbed:
a name quietly turned into a different folder is how a run ends up on
another plant's rules looking entirely normal.
`None` is NOT in this list. None is the absence of a plant, not a bad
one, and it resolves to the machine-wide file -- see the test above."""
from vibe import plantpath
monkeypatch.setenv(plantpath.PLANT_ENV, str(tmp_path))
for bad in ('..', '../evil', 'a/b', 'a\\b', 'C:', '', ' ', 3):
assert plantpath.valid_plant_id(bad) is False, bad
assert plantpath.plant_knowledge_path(bad) is None, bad
assert plantpath.valid_plant_id(None) is False
assert plantpath.plant_knowledge_path(None) == \
os.path.join(str(tmp_path), 'knowledge.yaml')
assert plantpath.valid_plant_id('northgate') is True
assert plantpath.valid_plant_id('plant-2.east_1') is True
# -------------------------------------------------- load(), and the pre-pass
@pytest.fixture
def fake_designspec(monkeypatch, tmp_path):
"""`vibe.designspec` with the Revit half faked and the paths REAL.
Only two things here need a live document -- where the job knowledge file
is, and whether a project config is pointed at -- so only those are faked.
`plant_knowledge_path` is `vibe.plantpath`'s own, so these tests exercise
the real resolution rather than a second copy of it that could drift.
"""
from vibe import plantpath
job_dir = tmp_path / 'job'
job_dir.mkdir()
plant_root = tmp_path / 'plants'
plant_root.mkdir()
monkeypatch.setenv(plantpath.PLANT_ENV, str(plant_root))
module = types.ModuleType('vibe.designspec')
module.PLANT_ENV = plantpath.PLANT_ENV
module.plant_knowledge_path = plantpath.plant_knowledge_path
module.knowledge_path = lambda doc: str(job_dir / 'knowledge.yaml')
module.active_config_path = lambda doc: module._config
module._config = None
module._job_dir = job_dir
module._plant_root = plant_root
monkeypatch.setitem(sys.modules, 'vibe.designspec', module)
return module
def _write(path, text):
path.write_text(text, encoding='utf-8')
return str(path)
def test_load_reads_the_plant_folder_the_job_file_names(
fake_designspec, yaml_on_host):
"""THE ORDERING TRAP. `job.plant` chooses the plant layer, and it lives in
the layer that merges AFTER it. Without the pre-pass this reads the
machine-wide file while the job file plainly says which plant it is for,
and every report still looks normal."""
plant_dir = fake_designspec._plant_root / 'northgate'
plant_dir.mkdir()
_write(plant_dir / 'knowledge.yaml',
u'nomenclature:\n design_folder: NORTHGATE PRODUCT\n')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
merged = knowledge.load(None)
assert merged['_plant_id'] == 'northgate'
assert merged['nomenclature']['design_folder'] == 'NORTHGATE PRODUCT'
assert '_plant_missing' not in merged
assert '_plant_layer_stale' not in merged
assert knowledge.config_problems(None, merged) == []
def test_load_with_no_plant_named_reads_the_machine_wide_file_and_says_nothing(
fake_designspec, yaml_on_host):
"""The state every install that predates this key is in. It is legitimate,
so it is quiet -- a warning on every run of every install is a warning
that stops being read. It is still on the record: `_plant_id` is None and
`sources()` names the file that was read."""
_write(fake_designspec._plant_root / 'knowledge.yaml',
u'nomenclature:\n design_folder: HOUSE\n')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'scope:\n declared_by: Will\n')
merged = knowledge.load(None)
assert merged['_plant_id'] is None
assert merged['nomenclature']['design_folder'] == 'HOUSE'
assert knowledge.config_problems(None, merged) == []
def test_a_job_naming_a_plant_with_no_file_is_loud_and_does_not_fall_back(
fake_designspec, yaml_on_host):
"""Same discipline as `_config_missing`. Falling back to the machine-wide
file would run one plant's job on another plant's rules and look
completely normal doing it."""
_write(fake_designspec._plant_root / 'knowledge.yaml',
u'nomenclature:\n design_folder: HOUSE\n')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: nrothgate\n')
merged = knowledge.load(None)
assert merged['_plant_missing'][0] == 'nrothgate'
# NOT the machine-wide file's value -- nothing was read in its place
assert merged['nomenclature']['design_folder'] != 'HOUSE'
problems = knowledge.config_problems(None, merged)
assert len(problems) == 1
assert 'nrothgate' in problems[0]
def test_a_plant_id_that_is_not_a_folder_name_is_reported_by_load(
fake_designspec, yaml_on_host):
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: ../elsewhere\n')
merged = knowledge.load(None)
assert merged['_plant_missing'][0] == '../elsewhere'
assert merged['_plant_missing'][1] is None
assert 'not a usable folder name' in \
knowledge.config_problems(None, merged)[0]
def test_the_plant_named_in_a_project_config_still_chooses_the_plant_file(
fake_designspec, yaml_on_host, tmp_path):
"""The config file merges last, so it may override `job.plant` -- and the
pre-pass reads it for exactly that reason. If it did not, the tripwire
below would fire instead of the right file being read."""
for name in ('northgate', 'southbay'):
folder = fake_designspec._plant_root / name
folder.mkdir()
_write(folder / 'knowledge.yaml',
u'nomenclature:\n design_folder: {0}\n'.format(name.upper()))
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
fake_designspec._config = _write(
tmp_path / 'active-config.yaml',
u'knowledge:\n job:\n plant: southbay\n')
merged = knowledge.load(None)
assert merged['_plant_id'] == 'southbay'
assert merged['nomenclature']['design_folder'] == 'SOUTHBAY'
assert '_plant_layer_stale' not in merged
assert knowledge.config_problems(None, merged) == []
def test_the_tripwire_fires_if_the_pre_pass_and_the_merge_ever_disagree(
fake_designspec, yaml_on_host, monkeypatch):
"""The guard against somebody "simplifying" the two-pass away, or adding
a layer that sets `job.plant` and forgetting the pre-pass reads only two.
Simulated by making the pre-pass answer differently from the merge."""
folder = fake_designspec._plant_root / 'northgate'
folder.mkdir()
_write(folder / 'knowledge.yaml', u'nomenclature:\n design_folder: N\n')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
monkeypatch.setattr(knowledge, 'plant_choice', lambda doc: {
'plant': None, 'path': None, 'basis': 'simulated pre-pass'})
merged = knowledge.load(None)
assert merged['_plant_layer_stale'] == (None, 'northgate')
assert any('wrong one' in p
for p in knowledge.config_problems(None, merged))
def test_an_environment_variable_pointing_at_a_file_that_is_gone_is_loud(
fake_designspec, yaml_on_host, monkeypatch):
"""Not about the plant name: somebody explicitly pointed the tool at a
file and the tool did not read it. Nothing else would ever say so."""
from vibe import plantpath
monkeypatch.setenv(plantpath.PLANT_ENV,
str(fake_designspec._plant_root / 'gone.yaml'))
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'scope:\n declared_by: Will\n')
merged = knowledge.load(None)
assert merged['_plant_env_missing'].endswith('gone.yaml')
assert 'WILLTOOLS_KNOWLEDGE' in knowledge.config_problems(None, merged)[0]
def test_the_plant_layer_can_select_a_vocabulary_the_catalogue_publishes(
fake_designspec, yaml_on_host):
"""End to end: the catalogue publishes the vocabularies, the plant folder
selects two of them by id, and a token resolves with its frame."""
folder = fake_designspec._plant_root / 'northgate'
folder.mkdir()
_write(folder / 'knowledge.yaml',
u'face_designations:\n use: [near_far, in_form]\n')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
merged = knowledge.load(None)
assert [v for v, _ in knowledge.face_vocabularies(merged)] == \
['near_far', 'in_form']
assert knowledge.face_designation(merged, 'E.L.')['side'] == 'both'
assert knowledge.face_designation(merged, 'B.I.F.')['frame'] == 'form'
assert knowledge.config_problems(None, merged) == []
def test_sources_names_the_per_plant_file_the_run_really_read(
fake_designspec, yaml_on_host):
"""A listing of layers that names the machine-wide file while the run used
a per-plant one is a listing that lies."""
folder = fake_designspec._plant_root / 'northgate'
folder.mkdir()
_write(folder / 'knowledge.yaml', u'x: 1\n')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
rows = dict([(label, (path, exists))
for label, path, exists in knowledge.sources(None)])
assert rows['plant'][0] == str(folder / 'knowledge.yaml')
assert rows['plant'][1] is True
def test_load_puts_the_plant_layer_between_the_catalogue_and_the_job(
fake_designspec, yaml_on_host):
"""Job beats plant beats public, and the per-plant folder did not move
the plant layer anywhere in that order."""
folder = fake_designspec._plant_root / 'northgate'
folder.mkdir()
_write(folder / 'knowledge.yaml',
u'catalogue:\n rebar:\n stock_length_ft: 60.0\n')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\ntolerances:\n default_in: 0.5\n')
merged = knowledge.load(None)
# the plant overrode the catalogue's published 40 ft stock
assert merged['catalogue']['rebar']['stock_length_ft'] == 60.0
# and the job's own value is on top of both
assert merged['tolerances']['default_in'] == 0.5
# ------------------------------------- a layer file that exists and will not
# parse
#
# THE INCIDENT, 2026-09-01, measured live. One key stated twice inside one
# nested mapping of a plant file. pyRevit's reader is strict and raises
# `Duplicate key`, `_merge_file` swallowed it with a bare `except Exception`,
# and the ENTIRE 255KB plant layer was discarded: every parameter role mapped
# to None, the sweep enumerated 0 assemblies, and every report rendered
# normally. `sources()` and `config_problems()` both said healthy, because
# A LAYER THAT FAILS TO LOAD LOOKS EXACTLY LIKE A LAYER THAT AGREES WITH THE
# DEFAULTS.
#
# The host could not reproduce it, either, because the fake reader took the
# last of the two keys and said nothing -- the same "double kinder than
# production" that the null-typing incident already cost a press over. So
# these tests are only worth anything BECAUSE `pyrevit_yaml_fake` now raises
# on a duplicate the way YamlDotNet does: the fixtures below are real
# duplicate keys, not a monkeypatched exception.
_DUPLICATE = (u'nomenclature:\n'
u' design_folder: NORTHGATE PRODUCT\n'
u' rules:\n'
u' cap_label_mismatch_is_view_specific: true\n'
u' something_else: 1\n'
u' cap_label_mismatch_is_view_specific: false\n')
def test_a_plant_file_that_will_not_parse_is_recorded_and_not_swallowed(
fake_designspec, yaml_on_host):
"""The layer is still skipped -- a half-parsed layer must never merge --
but the skip leaves a marker, which is the whole difference between this
and the incident."""
folder = fake_designspec._plant_root / 'northgate'
folder.mkdir()
path = _write(folder / 'knowledge.yaml', _DUPLICATE)
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
merged = knowledge.load(None)
assert merged['_plant_unreadable'][0] == path
assert 'Duplicate key' in merged['_plant_unreadable'][1]
# NOT half-merged: nothing from the file reached the tree, and what is
# left in its place is the DEFAULT -- which is the whole trap. Reading
# the tree cannot tell you this layer failed; only the marker can.
assert merged['nomenclature']['design_folder'] == \
knowledge.DEFAULTS['nomenclature']['design_folder']
assert 'rules' not in merged['nomenclature']
assert knowledge.unreadable_layers(merged) == \
[('plant', path, merged['_plant_unreadable'][1])]
def test_a_missing_layer_and_an_unreadable_one_are_different_facts(
fake_designspec, yaml_on_host):
"""Two different things went wrong and they need two different sentences:
one file is not there, the other is there and lying still. Collapsing them
would send whoever reads the message to create a file that exists."""
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
absent = knowledge.load(None)
assert absent['_plant_missing'][0] == 'northgate'
assert '_plant_unreadable' not in absent
folder = fake_designspec._plant_root / 'northgate'
folder.mkdir()
_write(folder / 'knowledge.yaml', _DUPLICATE)
broken = knowledge.load(None)
assert '_plant_missing' not in broken
assert broken['_plant_unreadable'][0].endswith('knowledge.yaml')
def test_the_job_layer_gets_the_same_marker(fake_designspec, yaml_on_host):
path = _write(fake_designspec._job_dir / 'knowledge.yaml', _DUPLICATE)
merged = knowledge.load(None)
assert merged['_job_unreadable'][0] == path
assert 'Duplicate key' in merged['_job_unreadable'][1]
def test_the_project_config_gets_the_same_marker(fake_designspec,
yaml_on_host, tmp_path):
"""The config file merges through `_merge_config_file`, which reads into a
holder of its own -- so the marker has to be routed onto the real tree or
it dies with the holder."""
_write(fake_designspec._job_dir / 'knowledge.yaml', u'x: 1\n')
path = _write(tmp_path / 'active-config.yaml',
u'knowledge:\n handling:\n rig_mode: rolling\n'
u' rig_mode: spreader\n')
fake_designspec._config = path
merged = knowledge.load(None)
assert merged['_config_unreadable'][0] == path
assert '_config_missing' not in merged # it IS there; it will not read
def test_the_catalogue_gets_the_same_marker(fake_designspec, yaml_on_host,
tmp_path, monkeypatch):
"""The layer that ships with the tool is no exception. A catalogue that
will not parse means every published rating is silently back to
DEFAULTS."""
path = _write(tmp_path / 'catalogue.yaml', _DUPLICATE)
monkeypatch.setattr(knowledge, 'catalogue_path', lambda: path)
_write(fake_designspec._job_dir / 'knowledge.yaml', u'x: 1\n')
merged = knowledge.load(None)
assert merged['_catalogue_unreadable'][0] == path
def test_unreadable_layers_reads_the_markers_back_in_merge_order():
know = {'_job_unreadable': ('J', 'boom'),
'_catalogue_unreadable': ('C', 'bang')}
assert knowledge.unreadable_layers(know) == [('catalogue', 'C', 'bang'),
('job', 'J', 'boom')]
assert knowledge.unreadable_layers({}) == []
assert knowledge.unreadable_layers(None) == []
def test_config_problems_refuses_on_an_unreadable_layer():
"""Refusal-grade: the file, the layer, what the parser said, and what to
do about it. A button prints this instead of running."""
problems = knowledge.config_problems(
None, {'_plant_unreadable': ('C:/plants/northgate/knowledge.yaml',
'Duplicate key: cap_label at line 812')})
assert len(problems) == 1
said = problems[0]
assert 'plant knowledge layer' in said
assert 'C:/plants/northgate/knowledge.yaml' in said
assert 'Duplicate key: cap_label at line 812' in said
assert 'will not guess at what it said' in said
# the sentence the incident is named by, so nobody reads a clean report
# and concludes the layer agreed with the defaults
assert 'looks exactly like a layer that agrees with the defaults' in said
def test_config_problems_names_every_unreadable_layer_not_just_the_first():
problems = knowledge.config_problems(
None, {'_plant_unreadable': ('P', 'a'), '_job_unreadable': ('J', 'b')})
assert len(problems) == 2
assert 'plant' in problems[0] and 'job' in problems[1]
# ============================================================================
# QL589: THE PLANT-OVERRIDES-CATALOGUE SCENARIO, PINNED
# ============================================================================
#
# Owner, 2026-09-02, verbatim: "M8是厂知识。ALP是公共知识。在MC的job里面,MC的
# 厂知识override公共知识。" -- M8 is plant knowledge, ALP is public knowledge,
# and in an MC job the plant's knowledge overrides the public one, BY THE
# LAYER MERGE, automatically. This section pins exactly that: a merged
# knowledge dict carrying BOTH the shipped catalogue's QL589SPG (16,000 lb,
# ALP's number) and the northgate plant layer's own QL589 override (13,800
# lb, M8's number) must resolve to the plant's number, not the public one.
#
# `lifter_capacity()` reaches the override through `lifters.capacity_lb`,
# checked BEFORE the catalogue -- see its docstring, 2026-09-02 addition, for
# the dict shape (`tension_lb` / `shear_lb`) a single family needs when the
# two kinds are not the same number, which QL589 is the case this exists
# for: 13,800 tension against 3,860 shear are not interchangeable, and a
# flat scalar entry would silently hand the tension figure back on a shear
# query.
def test_qc589_plant_override_beats_the_public_catalogue(
fake_designspec, yaml_on_host):
"""The owner's exact MC scenario. `knowledge.load()` always reads the
real, shipped `knowledge/catalogue.yaml` (`load()`'s first merged
layer) -- so this is not a stand-in catalogue, it is the one the tool
actually ships, and it really does publish QL589SPG at 16,000 lb."""
plant_dir = fake_designspec._plant_root / 'northgate'
plant_dir.mkdir()
_write(plant_dir / 'knowledge.yaml', u'''
lifters:
capacity_lb:
QL589:
tension_lb: 13800
shear_lb: 3860
''')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
merged = knowledge.load(None)
assert merged['_plant_id'] == 'northgate'
assert knowledge.config_problems(None, merged) == []
# Sanity check on the premise: the SHIPPED catalogue really does publish
# QL589SPG at 16,000 lb, reachable UNGATED (no plant tension-bar claim
# in this scenario) -- so an override that failed to wire would fall
# through to exactly this number in silence.
catalogue_only = {'lifters': {'catalogue_alias': {'QL589': 'QL589SPG'}},
'catalogue': merged['catalogue']}
cap, basis, why = knowledge.lifter_capacity(catalogue_only, 'QL589')
assert (cap, basis) == (16000, 'catalogue'), why
# The real scenario: MC job, both layers merged, plant override present.
cap, basis, why = knowledge.lifter_capacity(merged, 'QL589')
assert (cap, basis) == (13800, 'direct'), why
assert 'QL589' in why
cap, basis, why = knowledge.lifter_capacity(merged, 'QL589', kind='shear')
assert (cap, basis) == (3860, 'direct'), why
def test_lpa4t512_shear_is_zero_and_tension_stands_down_on_the_angle(
fake_designspec, yaml_on_host):
"""LPA4T512's override prints a bare shear zero and no catalogue or
face-lifter grouping exists in this fixture, so 2026-09-04's fix reads
that zero as NOT RATED (not a real zero) and stands down naming why,
rather than handing back the printed 0 as a rating -- see
`test_lpa4t512_back_cast_shear_resolves_from_tension_via_the_footnote`
below for the real MC scenario, where the footnote fills this in as the
anchor's actual tension rating. Tension is NOT pinned either: M8
publishes it by sling angle (5,990 @ 60 deg / 6,680 @ 75 deg) and the
plant has never confirmed which column applies, so a tension query must
stand down naming the unresolved angle rather than silently choosing
one -- the same discipline
`lifting_anchors.sling_angle_choice_unknown_because` states for the raw
M8 table this override was built from."""
plant_dir = fake_designspec._plant_root / 'northgate'
plant_dir.mkdir()
_write(plant_dir / 'knowledge.yaml', u'''
lifters:
capacity_lb:
LPA4T512:
shear_lb: 0
tension_60_lb: 5990
tension_75_lb: 6680
tension_lb_unresolved_because: "sling angle not confirmed at this plant"
''')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
merged = knowledge.load(None)
cap, basis, why = knowledge.lifter_capacity(
merged, 'LPA4T512', kind='shear')
assert (cap, basis) == (None, None), why
assert 'LPA4T512' in why and 'catalogue alias' in why
cap, basis, why = knowledge.lifter_capacity(merged, 'LPA4T512')
assert (cap, basis) == (None, None)
assert 'sling angle not confirmed' in why
def test_lpa4t512_tension_resolves_once_the_plant_names_a_sling_angle(
fake_designspec, yaml_on_host):
"""Added 2026-09-02: once `lifting_anchors.sling_angle_choice` names one
of the published angles, the same angle-split dict resolves to that
column instead of standing down -- `why` naming the chosen angle. This
must not disturb the plain-number / `{tension_lb, shear_lb}` shapes
(covered by the QL589 test above). Shear, updated 2026-09-04: this
fixture carries no catalogue or face-lifter grouping, so a printed
shear zero (now read as NOT RATED, never a real zero on its own) stands
down rather than resolving -- the sling-angle fix above is a tension-
only mechanism and does not touch shear at all."""
plant_dir = fake_designspec._plant_root / 'northgate'
plant_dir.mkdir()
_write(plant_dir / 'knowledge.yaml', u'''
lifting_anchors:
sling_angle_choice: 60
sling_angles_published: [60, 75]
lifters:
capacity_lb:
LPA4T512:
shear_lb: 0
tension_60_lb: 5990
tension_75_lb: 6680
tension_lb_unresolved_because: "sling angle not confirmed at this plant"
''')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
merged = knowledge.load(None)
cap, basis, why = knowledge.lifter_capacity(merged, 'LPA4T512')
assert (cap, basis) == (5990, 'direct'), why
assert '60' in why and 'sling_angle_choice' in why
cap, basis, why = knowledge.lifter_capacity(
merged, 'LPA4T512', kind='shear')
assert (cap, basis) == (None, None), why
def test_lpa4t512_tension_stands_down_naming_an_unpublished_sling_angle(
fake_designspec, yaml_on_host):
"""A `sling_angle_choice` that names an angle this entry has no column
for (typo, or a choice made before this part's chart was landed) must
stand down naming the mismatch -- never silently take the nearer or the
smaller column."""
plant_dir = fake_designspec._plant_root / 'northgate'
plant_dir.mkdir()
_write(plant_dir / 'knowledge.yaml', u'''
lifting_anchors:
sling_angle_choice: 45
sling_angles_published: [60, 75]
lifters:
capacity_lb:
LPA4T512:
shear_lb: 0
tension_60_lb: 5990
tension_75_lb: 6680
tension_lb_unresolved_because: "sling angle not confirmed at this plant"
''')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
merged = knowledge.load(None)
cap, basis, why = knowledge.lifter_capacity(merged, 'LPA4T512')
assert (cap, basis) == (None, None)
assert '45' in why
assert '60' in why and '75' in why
def test_strand_loop_family_needs_a_lifters_capacity_lb_entry_to_resolve(
fake_designspec, yaml_on_host):
"""`lifting_loops.strand_loop_half_inch` (mined M6 data, already in the
northgate layer) is NOT under `lifters.*`, the only tree
`lifter_capacity()` reads -- so it cannot reach a strand-loop family on
its own, override or no override. Confirms the gap, then confirms the
mirrored `lifters.capacity_lb` entry closes it."""
plant_dir = fake_designspec._plant_root / 'northgate'
plant_dir.mkdir()
_write(plant_dir / 'knowledge.yaml', u'''
lifting_loops:
strand_loop_half_inch:
double: {capacity_lb: 12000, embedment_per_leg_in: 24}
lifters:
capacity_lb:
STRAND_LOOP_DOUBLE: 12000
''')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
merged = knowledge.load(None)
# the raw mined branch really is there, and really is unreachable by name
assert knowledge.get(
merged, 'lifting_loops.strand_loop_half_inch.double.capacity_lb') \
== 12000
cap, basis, why = knowledge.lifter_capacity(merged, 'lifting_loops')
assert cap is None, why # lifting_loops.* is not lifters.*
# the mirrored entry is what a real family name resolves against
cap, basis, why = knowledge.lifter_capacity(merged, 'STRAND_LOOP_DOUBLE')
assert (cap, basis) == (12000, 'direct'), why
# a plain scalar entry answers either kind the same way, same as every
# other legacy capacity_lb pin -- M6 publishes one number, not two.
cap, basis, why = knowledge.lifter_capacity(
merged, 'STRAND_LOOP_DOUBLE', kind='shear')
assert (cap, basis) == (12000, 'direct'), why
# ============================================================================
# LIFT_QL589G_8T: THE DOUBLE-COUNT GUARD, PINNED
# ============================================================================
#
# Owner, 2026-09-02, on the live sample_job model, verbatim: "只有带LIFT的才是吊装
# 件。" -- only LIFT-prefixed families are lifting components. A bare
# `QL589G` family ALSO appears in the model, and the owner ruled it out by
# name, same date: "项目里面没有裸QL589,所有的QL589都在LIFT_QL589里面,只不
# 过因为加载family的方式,让你以为它存在。" -- there is no bare QL589 in the
# project; every QL589 lives inside `LIFT_QL589G_8T`, and the bare family is
# a nested/shared sub-family artifact of how that family loads, not a
# second lifter. Counting it would double-count one physical anchor as two.
#
# This section pins the guard two ways: `families.lifter_include` must
# treat the real family as a lifter and the bare artifact as NOT one
# (`is_lifter`), and `lifters.capacity_lb` -- keyed off the SAME string --
# must resolve a capacity for the real family and refuse one for the bare
# artifact (`lifter_capacity`, which does its own substring lookup and does
# not consult `is_lifter` at all).
def test_lift_include_matches_the_real_family_not_the_bare_artifact():
"""`is_lifter` is a plain substring match (`matches()` in knowledge.py):
`LIFT` has to appear somewhere in the family name, not as a prefix.
Pinned against both the real family and the artifact the owner named."""
know = {'families': {'lifter_include': ['LIFT'], 'lifter_exclude': []}}
assert knowledge.is_lifter(know, 'LIFT_QL589G_8T') is True
# the bare artifact the owner ruled out -- no `LIFT` substring in it
assert knowledge.is_lifter(know, 'QL589G') is False
def test_lift_ql589g_8t_capacity_key_does_not_leak_onto_the_bare_artifact(
fake_designspec, yaml_on_host):
"""The substring-match risk the owner's ruling exists to avoid.
`_longest_substring_match` (what `lifter_capacity` reads
`lifters.capacity_lb` through) matches ANYWHERE in the family name, not
as a prefix. A key of bare `QL589` would be a substring of BOTH
`LIFT_QL589G_8T` (the real lifter) and bare `QL589G` (the artifact
`families.lifter_include` rules out above) -- so it would silently
resolve a plant capacity for the artifact too. Keying on the full
confirmed family string closes that: `LIFT_QL589G_8T` is longer than
`QL589G` and so can never be a substring of it.
"""
plant_dir = fake_designspec._plant_root / 'northgate'
plant_dir.mkdir()
_write(plant_dir / 'knowledge.yaml', u'''
lifters:
capacity_lb:
LIFT_QL589G_8T:
tension_lb: 13800
shear_lb: 3860
''')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
merged = knowledge.load(None)
assert merged['_plant_id'] == 'northgate'
assert knowledge.config_problems(None, merged) == []
# the real lifter resolves to the plant-pinned M8 numbers
cap, basis, why = knowledge.lifter_capacity(merged, 'LIFT_QL589G_8T')
assert (cap, basis) == (13800, 'direct'), why
cap, basis, why = knowledge.lifter_capacity(
merged, 'LIFT_QL589G_8T', kind='shear')
assert (cap, basis) == (3860, 'direct'), why
# the bare artifact must NOT pick up the same override -- no key in
# capacity_lb is a substring of it, and it has no catalogue_alias
# either, so it stands down naming both rather than resolving anything
cap, basis, why = knowledge.lifter_capacity(merged, 'QL589G')
assert (cap, basis) == (None, None), (cap, basis, why)
assert 'no catalogue alias' in why
def test_lpa4t512_capacity_key_is_the_confirmed_family_string(
fake_designspec, yaml_on_host):
"""LPA4T512's Revit family is `LPA4T512_LIFTING PIN ANCHOR` (type
`LPA4T512`), confirmed against the live sample_job model, 2026-09-02.
`lifter_capacity()` is always called with the FAMILY name, not the
type, so the key has to be the family string for the override to
reach it from a real model."""
plant_dir = fake_designspec._plant_root / 'northgate'
plant_dir.mkdir()
_write(plant_dir / 'knowledge.yaml', u'''
lifters:
capacity_lb:
'LPA4T512_LIFTING PIN ANCHOR':
shear_lb: 0
tension_60_lb: 5990
tension_75_lb: 6680
tension_lb_unresolved_because: "sling angle not confirmed at this plant"
''')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
merged = knowledge.load(None)
cap, basis, why = knowledge.lifter_capacity(
merged, 'LPA4T512_LIFTING PIN ANCHOR', kind='shear')
assert (cap, basis) == (None, None), why
assert 'LPA4T512_LIFTING PIN ANCHOR' in why
cap, basis, why = knowledge.lifter_capacity(
merged, 'LPA4T512_LIFTING PIN ANCHOR')
assert (cap, basis) == (None, None)
assert 'sling angle not confirmed' in why
def test_lpa4t512_back_cast_shear_resolves_from_tension_via_the_footnote(
fake_designspec, yaml_on_host):
"""THE REAL MC SCENARIO reported 2026-09-04: `lifters.capacity_lb`
prints `shear_lb: 0` for LPA4T512 (the M8 chart's edge-cast figure),
and until this fix that printed 0 was taken as the rating outright --
a real face lifter reported as having zero shear capacity, failing a
wall on a load its own hardware carries fine. This plant grouped every
LPA-prefixed family as a FACE (back-cast) lifter and named LPA4T512 in
`back_cast_shear_equals_tension_applies_to`, exactly like the real
plant layer -- so the printed 0 must fall through the footnote and
resolve to the (angle-resolved) tension figure, not to None and not to
0."""
plant_dir = fake_designspec._plant_root / 'northgate'
plant_dir.mkdir()
_write(plant_dir / 'knowledge.yaml', u'''
lifting_anchors:
sling_angle_choice: 60
sling_angles_published: [60, 75]
back_cast_shear_equals_tension: true
back_cast_shear_equals_tension_applies_to: [LPA4T512]
parts:
LPA4T512: {usage: back_stripping, tension_60_lb: 5990, tension_75_lb: 6680, shear_lb: 0}
handling:
lifter_groups:
face:
families: [LPA]
lifters:
capacity_lb:
'LPA4T512_LIFTING PIN ANCHOR':
shear_lb: 0
tension_60_lb: 5990
tension_75_lb: 6680
''')
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: northgate\n')
merged = knowledge.load(None)
cap, basis, why = knowledge.lifter_capacity(
merged, 'LPA4T512_LIFTING PIN ANCHOR', kind='shear')
assert (cap, basis) == (5990, 'plant_table'), why
assert 'back' in why.lower() and 'footnote' in why.lower()
assert 'back_cast_shear_equals_tension' in why
# ============================================================================
# REBAR STOCK: per size, with "not stocked" as its own fact
# ============================================================================
#
# THE BUG THIS CLOSES. `catalogue.rebar.stock_length_ft` is one number for
# every size, and checklist 5.7 ("no bar longer than the stock it is cut
# from") compared against it. A plant that stocks #4 in 20 ft lengths would
# pass a 30 ft #4 -- a false OK on a real defect class, which is the failure
# this whole checklist exists to prevent.
#
# The per-size table is PLANT knowledge (owner, 2026-08-30) and is therefore
# never in this repository, so these tests build the tree by hand rather than
# reading a plant file. What IS shipped, and IS asserted against the real
# file, is the generic fallback and the published ASTM grade table.
def _rebar(by_size=None, generic=40.0, **rest):
merged = knowledge._deep_copy(knowledge.DEFAULTS)
branch = {'stock_length_ft': generic}
if by_size is not None:
branch['stock_by_size'] = by_size
branch.update(rest)
knowledge._deep_update(merged, {'catalogue': {'rebar': branch}})
return merged
PLANT_STOCK = {
'#3': {'stock_length_in': 280, 'stocked': True, 'astm': 'A615'},
'#4': {'stock_length_in': 240, 'stocked': True, 'astm': 'A615'},
'#5': {'stock_length_in': 480, 'stocked': True, 'astm': 'A615'},
'#9': {'stock_length_in': 480, 'stocked': True, 'astm': 'A615'},
'#10': {'stock_length_in': 480, 'stocked': False, 'astm': 'A615'},
}
def test_a_size_the_plant_lists_uses_that_length_and_not_the_generic_one():
"""The whole point. 20 ft #4 stock, and the check no longer measures a #4
against 40 ft."""
stock = knowledge.rebar_stock(_rebar(PLANT_STOCK), '#4')
assert stock['length_in'] == 240
assert stock['length_ft'] == 20.0
assert stock['basis'] == 'by_size'
assert stock['stocked'] is True
def test_a_stock_length_that_is_not_a_whole_number_of_feet_stays_exact():
"""23'-4" is 23.3333... ft. Stored as 280 inches, a bar cut to exactly
23'-4" is exactly its own stock length; stored as a decimal foot it is
fractionally longer and fails against itself."""
stock = knowledge.rebar_stock(_rebar(PLANT_STOCK), '#3')
assert stock['length_in'] == 280
assert stock['length_in'] == 23 * 12 + 4
def test_a_size_only_the_catalogue_covers_falls_back_and_says_it_did():
"""Not silence: the sentence names the size and says the number is not
size specific, because that is the difference between this answer and a
real one."""
stock = knowledge.rebar_stock(_rebar(PLANT_STOCK), '#7')
assert stock['basis'] == 'generic'
assert stock['length_ft'] == 40.0
assert stock['length_in'] == 480.0
assert '#7' in stock['why'] and 'not size specific' in stock['why']
def test_a_size_nothing_knows_about_makes_the_check_stand_down():
"""No per-size entry and no generic number. `basis` is None, the lengths
are None, and nothing is assumed -- which is what makes 5.7 report NOT RUN
instead of a pass it has no basis for."""
stock = knowledge.rebar_stock(_rebar(None, generic=None), '#7')
assert stock['basis'] is None
assert stock['length_in'] is None and stock['length_ft'] is None
assert 'Nothing was assumed' in stock['why']
def test_not_stocked_is_its_own_fact_and_not_a_missing_length():
"""#10 is listed at 40' AND marked not stocked. Both are true: the length
is what would arrive if it were ordered, and 'not stocked' is why a ticket
calling for one deserves a human. Modelling either as the absence of the
other loses one of them."""
stock = knowledge.rebar_stock(_rebar(PLANT_STOCK), '#10')
assert stock['stocked'] is False
assert stock['length_in'] == 480 # still known
assert stock['basis'] == 'by_size' # still a real answer
assert 'NOT STOCKED' in stock['why']
def test_a_stocked_size_and_an_unlisted_size_are_told_apart_from_not_stocked():
know = _rebar(PLANT_STOCK)
assert knowledge.rebar_stock(know, '#5')['stocked'] is True
assert knowledge.rebar_stock(know, '#10')['stocked'] is False
assert knowledge.rebar_stock(know, '#7')['stocked'] is None # unlisted
def test_a_size_that_does_not_read_as_one_is_refused_rather_than_guessed():
stock = knowledge.rebar_stock(_rebar(PLANT_STOCK), 'SR324')
assert stock['size'] is None and stock['basis'] is None
assert 'does not read as a bar size' in stock['why']
def test_bar_sizes_are_normalised_the_same_way_wherever_they_come_from():
for raw in ('#4', '4', 4, ' #4 ', 'No. 4', 'no4', '#04'):
assert knowledge.rebar_size(raw) == '#4', raw
for raw in (None, '', 'SR324', '#', 'four', '#123'):
assert knowledge.rebar_size(raw) is None, raw
def test_the_per_size_table_deep_merges_so_a_job_adds_without_deleting():
"""THE MERGE TRAP, asserted. `stock_by_size` is a map: a job that special
orders one size adds one key and the plant's other sizes survive. Had it
been a list, this would leave exactly one size in force and every check
would go on looking entirely normal."""
merged = _rebar(PLANT_STOCK)
knowledge._deep_update(merged, {'catalogue': {'rebar': {'stock_by_size': {
'#4': {'stock_length_in': 480, 'stocked': True, 'astm': 'A615'}}}}})
assert knowledge.rebar_stock(merged, '#4')['length_in'] == 480 # overridden
assert knowledge.rebar_stock(merged, '#3')['length_in'] == 280 # inherited
assert knowledge.rebar_stock(merged, '#10')['stocked'] is False # inherited
def test_a_size_listed_with_no_length_still_reports_whether_it_is_stocked():
"""An entry that says nothing about length is not a statement that the
length is unknown, so the length falls through to the generic number --
but `stocked`, which the entry DID state, is still returned."""
know = _rebar({'#6': {'stocked': False, 'astm': 'A615'}})
stock = knowledge.rebar_stock(know, '#6')
assert stock['stocked'] is False
assert stock['basis'] == 'generic'
assert stock['length_ft'] == 40.0
# ---------------------------------------------------- ASTM, and yield strength
def test_the_shipped_catalogue_publishes_the_astm_grade_table():
import io
from vibe import yamlkit
data = yamlkit.loads(
io.open(knowledge.catalogue_path(), encoding='utf-8').read())
rebar = knowledge._coerce(data['catalogue']['rebar'])
assert rebar['stock_length_ft'] == 40.0
assert rebar['astm_grades']['A706']['default_fy_ksi'] == 60
# ...and A615 has NO default, on purpose
assert rebar['astm_grades']['A615']['default_fy_ksi'] is None
assert 100 in rebar['astm_grades']['A615']['grades_ksi']
# nothing in the public layer states what a producer stocks
assert 'stock_by_size' not in rebar
def test_a706_defaults_to_60_and_says_out_loud_that_it_is_a_default():
""""A706 is 60 ksi, isn't it?" -- usually, and it is not a definition.
The current spec also defines Grade 80, so the answer comes back WITH its
basis and a default can never be read as a stated fact."""
know = _rebar(None, astm_grades={
'A706': {'grades_ksi': [60, 80], 'default_fy_ksi': 60},
'A615': {'grades_ksi': [60, 80, 100], 'default_fy_ksi': None}})
fy, basis, why = knowledge.rebar_fy(know, astm='A706')
assert (fy, basis) == (60.0, 'astm default')
assert 'DEFAULT' in why and '80 ksi' in why
def test_a615_gives_no_yield_strength_at_all_and_never_defaults_to_60():
"""A615 covers Grades 60, 80 and 100. A check that assumed 60 would be
confidently wrong on this very job, which carries a #9 at 80 ksi."""
know = _rebar(None, astm_grades={
'A615': {'grades_ksi': [60, 80, 100], 'default_fy_ksi': None}})
fy, basis, why = knowledge.rebar_fy(know, astm='A615')
assert fy is None and basis is None
assert 'does not imply one' in why and '100 ksi' in why
def test_a_stated_grade_beats_any_default_and_is_reported_as_stated():
know = _rebar(None, astm_grades={
'A706': {'grades_ksi': [60, 80], 'default_fy_ksi': 60}})
fy, basis, _why = knowledge.rebar_fy(know, astm='A706', stated_fy_ksi=80)
assert (fy, basis) == (80.0, 'stated')
def test_an_unknown_or_absent_designation_gives_nothing_and_says_so():
know = _rebar(None, astm_grades={'A706': {'default_fy_ksi': 60}})
assert knowledge.rebar_fy(know, astm='A1035')[0] is None
assert knowledge.rebar_fy(know)[0] is None
assert 'no ASTM designation' in knowledge.rebar_fy(know)[2]
def test_a_heading_named_with_nothing_under_it_is_not_the_same_as_absent():
"""A plant's stock section names A706 and the page ends there. Three
states, all distinguishable: sizes listed, named-but-empty, never
mentioned. Collapsing the middle one into either neighbour invents a
fact."""
know = _rebar(PLANT_STOCK, astm_designations={
'A615 - weldable': {'astm': 'A615', 'sizes_listed': True},
'A706': {'astm': 'A706', 'sizes_listed': False},
})
assert knowledge.rebar_astm(know, 'A615 - weldable')['sizes_listed'] is True
assert knowledge.rebar_astm(know, 'A706')['sizes_listed'] is False
assert knowledge.rebar_astm(know, 'A1035') is None
def test_a_job_declared_grade_is_an_existence_fact_with_the_astm_left_unknown():
"""A call-out reading `#9 (80ksi) x CONT.` names a STRENGTH, not a
SPECIFICATION, and both A615 and A706 define an 80 ksi grade -- so the
ASTM is recorded as unknown with a reason rather than guessed."""
know = _rebar(PLANT_STOCK, grades_present={'no9_80ksi': {
'size': '#9', 'fy_ksi': 80, 'astm': None,
'astm_unknown_because': 'the call-out states a strength',
'declared_by': 'owner'}})
rows = knowledge.rebar_grades_present(know)
assert len(rows) == 1
assert (rows[0]['size'], rows[0]['fy_ksi']) == ('#9', 80)
assert rows[0]['astm'] is None
assert rows[0]['astm_unknown_because']
# ...and it did NOT become a property of the SIZE in the stock table
assert 'fy_ksi' not in \
knowledge.get(know, 'catalogue.rebar.stock_by_size')['#9']
def test_declaring_a_job_grade_does_not_disturb_the_plants_stock_table():
"""The shape has to make "the job adds one bar" impossible to become "the
job deleted the plant's stock list"."""
merged = _rebar(PLANT_STOCK)
knowledge._deep_update(merged, {'catalogue': {'rebar': {
'grades_present': {'no9_80ksi': {'size': '#9', 'fy_ksi': 80}}}}})
assert knowledge.rebar_stock(merged, '#3')['length_in'] == 280
assert knowledge.rebar_stock(merged, '#9')['length_in'] == 480
assert len(knowledge.rebar_grades_present(merged)) == 1
def test_a_second_job_grade_is_an_addition_not_a_replacement():
merged = _rebar(PLANT_STOCK, grades_present={
'no9_80ksi': {'size': '#9', 'fy_ksi': 80}})
knowledge._deep_update(merged, {'catalogue': {'rebar': {
'grades_present': {'no5_60ksi': {'size': '#5', 'fy_ksi': 60}}}}})
assert [r['id'] for r in knowledge.rebar_grades_present(merged)] == \
['no5_60ksi', 'no9_80ksi']
# ============================================================================
# WHICH MEMBERS ARE REINFORCING: a rule, not an enumeration
# ============================================================================
#
# Checklist 5.6 and 5.7 rest entirely on this. As a LIST of family names it
# goes stale in silence -- a bar family is added, nobody re-lists it, and both
# checks report nothing on those bars, which reads exactly like nothing being
# wrong. Owner, 2026-08-30: the ones with `rebar` written in them.
#
# THE FIRST READING OF THAT WAS WRONG, and the way it was wrong is the reason
# half these tests exist. It was built as a Revit CATEGORY plus a FAMILY-NAME
# token, on a census that read `FamilySymbol` TYPES -- and this plant writes
# the value on the INSTANCE, so the census saw no bars at all and the category
# could never be verified. A parameter, read instance-first-then-type, with a
# token in its VALUE, is what the plant's own view filters already do.
def _reinf(families=None, rule=None, overrides=None, patterns=None):
merged = knowledge._deep_copy(knowledge.DEFAULTS)
branch = {}
if families is not None:
branch['reinforcing_families'] = families
if rule is not None:
branch['reinforcing_rule'] = rule
if overrides is not None:
branch['reinforcing_family_overrides'] = overrides
if patterns is not None:
branch['reinforcing_family_patterns'] = patterns
knowledge._deep_update(merged, {'drawings': branch})
return merged
RULE = {'parameter': 'member.manufacture_component',
'parameter_verified': True,
'read_from': ['instance', 'type'],
'value_contains': ['rebar']}
BAR = 'REBAR CUSTOM REINFORCING'
NOT_BAR = 'BLOCKOUT'
def test_with_nothing_declared_the_answer_is_cannot_judge_not_no():
"""None is a real answer and it is NOT False. Reporting 'not a bar' for a
member nobody has described is how 5.6 and 5.7 went quiet in the first
place."""
know = _reinf()
mode, why = knowledge.reinforcing_mode(know)
assert mode is None
assert 'Nothing was assumed' in why
verdict, vwhy = knowledge.reinforcing_verdict(know, 'ANYTHING', BAR)
assert verdict is None
assert vwhy == why
def test_the_rule_recognises_a_family_nobody_enumerated():
"""The whole point: a family added tomorrow is a bar without an edit."""
know = _reinf(rule=RULE)
assert knowledge.reinforcing_mode(know)[0] == 'rule'
verdict, why = knowledge.reinforcing_verdict(
know, 'ACME_STRAIGHT_2027', BAR)
assert verdict is True
assert "'REBAR'" in why
def test_the_value_is_read_from_the_instance_first_then_the_type():
"""MEASURED: the parameter sits on the instance for some elements and on
the type for others, NEVER both. A reader that takes one of the two finds
nothing on half the model and returns a result that looks clean -- which
is exactly how the first census missed every bar in the job."""
know = _reinf(rule=RULE)
on_instance, why = knowledge.reinforcing_verdict(know, 'F', BAR, None)
assert on_instance is True and 'instance' in why
on_type, why = knowledge.reinforcing_verdict(know, 'F', None, BAR)
assert on_type is True and 'type' in why
# instance wins where both somehow carry one
verdict, why = knowledge.reinforcing_verdict(know, 'F', NOT_BAR, BAR)
assert verdict is False and 'instance' in why
def test_read_from_is_honoured_when_a_layer_reverses_it():
know = _reinf(rule=dict(RULE, read_from=['type', 'instance']))
verdict, why = knowledge.reinforcing_verdict(know, 'F', NOT_BAR, BAR)
assert verdict is True and 'type' in why
def test_the_token_is_a_substring_because_that_is_what_the_plant_does():
"""Thirteen of the plant's own view filters key on this parameter and
every string rule among them is `contains`. Equality would match none of
REBAR CUSTOM REINFORCING / STRIPPING / ERECTION."""
know = _reinf(rule=RULE)
for value in ('REBAR CUSTOM REINFORCING', 'REBAR CUSTOM STRIPPING',
'rebar custom cip'):
assert knowledge.reinforcing_verdict(know, 'F', value)[0] is True
def test_a_member_whose_value_carries_no_token_is_not_a_bar():
know = _reinf(rule=RULE)
verdict, why = knowledge.reinforcing_verdict(know, 'EMBED_P76', NOT_BAR)
assert verdict is False
assert 'carries none of' in why
def test_a_member_with_no_value_anywhere_cannot_be_judged():
"""Not a match, and not "not a bar". Nothing read is nothing known."""
know = _reinf(rule=RULE)
verdict, why = knowledge.reinforcing_verdict(know, 'X', None, None)
assert verdict is None
assert 'not the same as it not being a bar' in why
def test_a_rule_with_an_unverified_parameter_does_not_run_and_says_why():
"""`parameter_verified: false` is NOT 'there is no such parameter' -- it
is 'nobody has checked it against a model'. Guessing it un-recognises
every bar at once, silently, which is the exact dormancy the rule exists
to end. So it stays dormant OUT LOUD."""
know = _reinf(rule={
'parameter': 'member.manufacture_component',
'parameter_verified': False,
'parameter_unverified_because': 'never read off a live model.',
'value_contains': ['rebar']})
mode, why = knowledge.reinforcing_mode(know)
assert mode is None
assert 'member.manufacture_component' in why
assert 'never read off a live model' in why
assert knowledge.reinforcing_verdict(know, 'X', BAR)[0] is None
def test_a_token_with_no_parameter_behind_it_does_not_run():
"""A token with nothing to read it out of has nothing to match against."""
know = _reinf(rule={'parameter': None, 'parameter_verified': True,
'value_contains': ['rebar']})
mode, why = knowledge.reinforcing_mode(know)
assert mode is None
assert 'no PARAMETER' in why
def test_an_enumeration_still_works_and_still_wins_over_a_rule():
"""A plant that would rather name its families one by one must be able
to, and being overtaken by a pattern it did not write would be worse than
the pattern is worth."""
know = _reinf(families=['SR', 'TRIM_BAR'], rule=RULE)
assert knowledge.reinforcing_mode(know)[0] == 'list'
assert knowledge.reinforcing_verdict(know, 'SR324', None)[0] is True
# carrying the rule's token in its value -- and still not a bar, because
# the list is what is in force
assert knowledge.reinforcing_verdict(know, 'ACME_2027', BAR)[0] is False
def test_emptying_the_list_hands_over_to_the_rule():
know = _reinf(families=['SR'], rule=RULE)
assert knowledge.reinforcing_mode(know)[0] == 'list'
knowledge._deep_update(know, {'drawings': {'reinforcing_families': []}})
assert knowledge.reinforcing_mode(know)[0] == 'rule'
def test_a_job_adds_or_excludes_one_family_without_restating_anything():
"""The overrides map is checked FIRST and beats both the list and the
rule, so a job corrects one family without copying a pattern it would
then have to keep in step."""
know = _reinf(rule=RULE, overrides={'ODD_BAR': True,
'ACME_JIG': False})
assert knowledge.reinforcing_verdict(know, 'ODD_BAR', NOT_BAR)[0] is True
assert knowledge.reinforcing_verdict(know, 'ACME_JIG', BAR)[0] is False
# everything else still resolves by the rule
assert knowledge.reinforcing_verdict(know, 'ACME_STRAIGHT', BAR)[0] is True
def test_an_override_is_matched_exactly_and_never_as_a_substring():
"""An entry for `SR1` matched loosely would swallow `SR1101`, the same
trap drawings.called_out_on_detail already documents."""
know = _reinf(rule=RULE, overrides={'SR1': False})
assert knowledge.reinforcing_verdict(know, 'sr1', BAR)[0] is False
# SR1101 is NOT swallowed by the SR1 entry -- it falls through to the rule
# and is judged on its own value, which is what the exact match buys.
verdict, why = knowledge.reinforcing_verdict(know, 'SR1101', BAR)
assert verdict is True
assert 'overrides' not in why
def test_the_overrides_map_deep_merges_so_the_job_keeps_the_plants():
"""A map, not a list, for exactly this: as a list the job's one
correction would replace the plant's."""
know = _reinf(rule=RULE, overrides={'PLANT_ODD': True})
knowledge._deep_update(know, {'drawings': {
'reinforcing_family_overrides': {'JOB_ODD': True}}})
assert knowledge.reinforcing_verdict(know, 'PLANT_ODD', NOT_BAR)[0] is True
assert knowledge.reinforcing_verdict(know, 'JOB_ODD', NOT_BAR)[0] is True
def test_the_list_form_still_ignores_the_parameter_as_it_always_did():
"""The enumeration is a family-name match and nothing else -- unchanged,
because a plant relying on it has not been told a parameter matters."""
know = _reinf(families=['SR'])
assert knowledge.reinforcing_verdict(know, 'SR324', None)[0] is True
# ---------------------------------------------------------------------------
# reinforcing_family_patterns -- the SAME enumeration written as regexes,
# for a plant whose bar-family name wraps an OPEN-ENDED CODE no fixed list
# could keep up with: a name like `ACME-17_(SCHEDULED)`, `ACME-S4_
# (SCHEDULED)`, `ACME-T2_(SCHEDULED)`, where the middle code is a growing
# series. Neither a substring wide enough to catch every code (bare `ACME-`)
# nor one narrow enough to be safe alone (bare `_(SCHEDULED)`) can say both
# ends of that wrapper at once -- a regex is the only one of the existing
# mechanisms that can.
PATTERN = r'^ACME-.+_\(SCHEDULED\)$'
def test_a_pattern_recognises_the_open_ended_code_inside_a_fixed_wrapper():
know = _reinf(patterns=[PATTERN])
for fam in ('ACME-17_(SCHEDULED)', 'ACME-17A_(SCHEDULED)',
'ACME-S4_(SCHEDULED)', 'ACME-T2_(SCHEDULED)'):
verdict, why = knowledge.reinforcing_verdict(know, fam)
assert verdict is True, (fam, why)
assert 'reinforcing_family_patterns' in why
def test_a_pattern_does_not_over_match_a_bare_prefix_or_a_bare_suffix():
"""The whole reason a pattern is needed rather than one wide substring:
a family that only shares the wrapper's PREFIX (no bar) and a family
that only shares its SUFFIX (a different product, seen on a real plant
sharing exactly this `_(SCHEDULED)` tail) must both stay out."""
know = _reinf(patterns=[PATTERN])
for fam in ('ACME-CONNECTOR', 'STRAND_(SCHEDULED)', 'LIFT_HOOK_8T'):
verdict, why = knowledge.reinforcing_verdict(know, fam)
assert verdict is False, (fam, why)
def test_patterns_are_the_same_evidence_class_as_the_plain_list():
"""Either one being non-empty is 'list' mode, and list mode still wins
over the rule -- patterns are not a fourth way of knowing."""
know = _reinf(patterns=[PATTERN], rule=RULE)
assert knowledge.reinforcing_mode(know)[0] == 'list'
# carries the rule's own token and is still not a bar -- the rule is not
# consulted while list-mode evidence (patterns included) is in force
assert knowledge.reinforcing_verdict(know, 'UNRELATED', BAR)[0] is False
def test_the_plain_list_is_still_checked_before_the_patterns():
know = _reinf(families=['SR'], patterns=[PATTERN])
verdict, why = knowledge.reinforcing_verdict(know, 'SR324')
assert verdict is True
assert 'reinforcing_families' in why
assert 'reinforcing_family_patterns' not in why
def test_a_pattern_that_does_not_compile_is_inert_not_fatal():
"""A typo in a regex must not crash the checklist -- it behaves like a
plain-list token that never appears in any family name: silently
inert, exactly the same fate, because nothing here can tell a
deliberately-inert entry from a mistake."""
know = _reinf(patterns=['(unclosed'])
verdict, why = knowledge.reinforcing_verdict(know, 'ACME-17_(SCHEDULED)')
assert verdict is False
def test_an_override_still_beats_a_pattern():
know = _reinf(patterns=[PATTERN],
overrides={'ACME-17_(SCHEDULED)': False})
verdict, why = knowledge.reinforcing_verdict(know, 'ACME-17_(SCHEDULED)')
assert verdict is False
assert 'overrides' in why
def test_emptying_the_families_list_but_keeping_a_pattern_stays_in_list_mode():
know = _reinf(families=['SR'], patterns=[PATTERN])
knowledge._deep_update(know, {'drawings': {'reinforcing_families': []}})
assert knowledge.reinforcing_mode(know)[0] == 'list'
assert knowledge.reinforcing_verdict(know, 'ACME-S5_(SCHEDULED)')[0] is True
def test_with_no_patterns_and_no_list_a_rule_still_takes_over():
know = _reinf(rule=RULE)
assert knowledge.reinforcing_mode(know)[0] == 'rule'
# ---------------------------------------------------------------------------
# PER-ROW SCOPE, and why it ships EMPTY
#
# The token is not a clean boundary in either direction and both exceptions
# are real: REBAR CUSTOM LIFTING / ERECTION carry it and are bar-formed
# HARDWARE (one is a strand loop -- no bar mark, no stock length), while
# BEARING TIE / RA EMBED CUSTOM / TRUSS CUSTOM are reinforcement and carry no
# token at all. Whether 5.6 and 5.7 want the same population is an open
# question with the owner, so the lists are empty and the behaviour with them
# empty is today's behaviour exactly.
def test_with_no_scope_declared_every_checklist_row_sees_one_population():
know = _reinf(rule=RULE)
for item in (None, '5.6', '5.7'):
assert knowledge.reinforcing_verdict(
know, 'F', 'REBAR CUSTOM LIFTING', item=item)[0] is True
assert knowledge.reinforcing_verdict(
know, 'F', 'BEARING TIE', item=item)[0] is False
def test_the_shipped_scopes_are_empty_and_say_why_they_are_empty():
"""Dormant out loud, not dormant by omission. An empty scope with no
reason beside it is indistinguishable from nobody having thought about
it -- and somebody has."""
rule = knowledge.DEFAULTS['drawings']['reinforcing_rule']
assert rule['scopes'] == {}
assert 'scopes_unresolved_because' in rule
def test_one_row_can_exclude_bar_formed_hardware_the_other_still_counts():
"""The shape has to be able to express the open question, or answering it
later means a code change instead of an edit."""
know = _reinf(rule=dict(RULE, scopes={
'5.7': {'except': ['REBAR CUSTOM LIFTING']}}))
excluded, why = knowledge.reinforcing_verdict(
know, 'F', 'REBAR CUSTOM LIFTING', item='5.7')
assert excluded is False and '5.7' in why
assert knowledge.reinforcing_verdict(
know, 'F', 'REBAR CUSTOM LIFTING', item='5.6')[0] is True
def test_one_row_can_admit_reinforcement_that_carries_no_token():
know = _reinf(rule=dict(RULE, scopes={
'5.6': {'also': ['BEARING TIE', 'TRUSS CUSTOM']}}))
admitted, why = knowledge.reinforcing_verdict(
know, 'F', 'BEARING TIE', item='5.6')
assert admitted is True and 'even though it carries no token' in why
assert knowledge.reinforcing_verdict(
know, 'F', 'BEARING TIE', item='5.7')[0] is False
def test_an_exclusion_beats_the_token_and_an_addition_beats_its_absence():
"""Order matters: the scope is a CORRECTION to the token test, so it has
to be read before it, not after."""
know = _reinf(rule=dict(RULE, scopes={
'5.7': {'also': ['BEARING TIE'], 'except': ['REBAR CUSTOM LIFTING']}}))
assert knowledge.reinforcing_verdict(
know, 'F', 'REBAR CUSTOM LIFTING', item='5.7')[0] is False
assert knowledge.reinforcing_verdict(
know, 'F', 'BEARING TIE', item='5.7')[0] is True
def test_the_parameter_role_is_declared_and_optional():
"""Declared so Self Check can report it unmapped and a starter offers it;
OPTIONAL so a plant that does not carry it is not nagged on every run --
the rule stands down with its own sentence instead."""
assert 'member.manufacture_component' in knowledge.PARAMETER_ROLES
assert 'member.manufacture_component' in knowledge.OPTIONAL_ROLES
def test_the_repository_names_the_role_and_never_the_parameter():
"""The role indirection is what keeps one office's parameter name out of
this repository. DEFAULTS names a ROLE; the plant layer maps it."""
rule = knowledge.DEFAULTS['drawings']['reinforcing_rule']
assert rule['parameter'] == 'member.manufacture_component'
assert knowledge.get(knowledge.DEFAULTS,
'parameters.member.manufacture_component') is None
# ============================================================================
# The plant's STATED stock material, and the job-level exception
# ============================================================================
#
# Owner, 2026-08-30: the stock is A706, 60 ksi, and A615 is gone in practice
# -- "如果又有了,那也是特例了,应该是 job 级别知识". So the material is stated
# once for the plant, the stock lengths are NOT bound to a designation any
# more, and an odd bar is a job-layer addition that must not wipe the plant's
# table.
MATERIAL = {'astm': 'A706', 'fy_ksi': 60, 'basis': 'stated',
'declared_by': 'Will'}
GRADES = {'A706': {'grades_ksi': [60, 80], 'default_fy_ksi': 60},
'A615': {'grades_ksi': [60, 80, 100], 'default_fy_ksi': None}}
# The stock rows as they are now: lengths and stocked flags, NO astm. Binding
# a live stock length to a retired designation is how a stale label outlives
# the page it came from.
STOCK = {
'#3': {'stock_length_in': 280, 'stocked': True},
'#4': {'stock_length_in': 240, 'stocked': True},
'#9': {'stock_length_in': 480, 'stocked': True},
'#10': {'stock_length_in': 480, 'stocked': False},
}
def test_the_plants_stated_stock_material_answers_the_grade():
know = _rebar(STOCK, stock_material=MATERIAL, astm_grades=GRADES)
fy, basis, why = knowledge.rebar_fy(know)
assert (fy, basis) == (60.0, 'plant stock')
assert 'A706' in why and 'stated fact' in why
def test_a_stated_plant_fact_outranks_the_astm_default_for_the_same_number():
"""Same 60, different strength of claim. "This plant stocks A706 Grade
60" is an assertion about the plant; "A706 usually means Grade 60" is
what the designation tends to mean. The basis is what keeps them apart,
and a stated fact must never be quietly downgraded to a default."""
know = _rebar(STOCK, stock_material=MATERIAL, astm_grades=GRADES)
assert knowledge.rebar_fy(know, astm='A706')[1] == 'plant stock'
# ...and with no stock material stated, the same question falls back to
# the published default, and says so
bare = _rebar(STOCK, astm_grades=GRADES)
assert knowledge.rebar_fy(bare, astm='A706')[1] == 'astm default'
def test_asking_about_another_designation_is_a_different_question():
"""A615 is a job-level exception at this plant now. Asking about it must
not be answered with the plant's A706 stock grade -- and A615 publishes
no default, so the honest answer is that nothing is known."""
know = _rebar(STOCK, stock_material=MATERIAL, astm_grades=GRADES)
fy, basis, why = knowledge.rebar_fy(know, astm='A615')
assert fy is None and basis is None
assert 'does not imply one' in why and '100 ksi' in why
def test_a_size_row_inherits_the_plants_material_and_says_where_it_came_from():
"""The rows carry no astm -- what the stock is made of is stated once.
`astm_basis` is what makes an inherited designation distinguishable from
one a row overrode."""
know = _rebar(STOCK, stock_material=MATERIAL)
stock = knowledge.rebar_stock(know, '#4')
assert stock['astm'] == 'A706'
assert stock['astm_basis'] == 'plant stock'
assert stock['length_in'] == 240
def test_a_job_can_add_a_bar_the_plant_does_not_stock_without_wiping_it():
"""The concrete case behind the map-not-list choice: an A615 bar turns
up, it is a job-level exception, and declaring it must not delete the
plant's stock table."""
merged = _rebar(STOCK, stock_material=MATERIAL, astm_grades=GRADES)
knowledge._deep_update(merged, {'catalogue': {'rebar': {
'stock_by_size': {'#5': {'stock_length_in': 480, 'stocked': True,
'astm': 'A615',
'note': 'special order, this job only'}}}}})
# the exception is in force, and says the designation is its OWN, not the
# plant's
five = knowledge.rebar_stock(merged, '#5')
assert five['astm'] == 'A615'
assert five['astm_basis'] == 'by_size'
# and every plant size survived
assert knowledge.rebar_stock(merged, '#3')['length_in'] == 280
assert knowledge.rebar_stock(merged, '#4')['length_in'] == 240
assert knowledge.rebar_stock(merged, '#10')['stocked'] is False
assert knowledge.rebar_stock(merged, '#4')['astm'] == 'A706'
def test_a_superseded_heading_is_provenance_and_not_a_live_fact():
"""A706 was NAMED WITH NOTHING UNDER IT on the page, and the owner's
correction is that the manual was out of date, not that the plant has no
A706. That is exactly why `sizes_listed: false` had to mean "nothing is
known" and never "the plant has none" -- read the second way it would
have been wrong."""
know = _rebar(STOCK, stock_material=MATERIAL, astm_designations={
'A615 - weldable': {'astm': 'A615', 'sizes_listed': True,
'superseded': True},
'A706': {'astm': 'A706', 'sizes_listed': False, 'superseded': True}})
a706 = knowledge.rebar_astm(know, 'A706')
assert a706['sizes_listed'] is False # the page listed no sizes
assert a706['superseded'] is True # and the page is out of date
# ...while the live answer, from stock_material, is that A706 IS stocked
assert knowledge.rebar_fy(know)[0] == 60.0
assert knowledge.rebar_stock(know, '#3')['astm'] == 'A706'
# ============================================================================
# Near/far anchored to the FORM, and two names for one face
# ============================================================================
#
# Correction, 2026-08-30. The published `near_far` vocabulary is the
# VIEW-anchored reading and is a real convention, but it is not universal:
# one plant's N.F. IS top-in-form. So which anchor a producer uses is plant
# knowledge, the frame field is what records it, and a face's identity is the
# pair (frame, side) -- which is how two vocabularies can name one face
# without either mentioning the other.
FORM_NEAR_FAR = {
'frame': 'form',
'measured_from': 'the casting bed',
'tokens': [{'token': 'N.F.', 'means': 'near face', 'side': 'top'},
{'token': 'F.F.', 'means': 'far face', 'side': 'bottom'},
{'token': 'E.L.', 'means': 'each layer', 'side': 'both'}],
}
def test_a_plant_may_anchor_near_far_to_the_form_instead_of_the_view():
know = _know(['near_far_in_form', 'in_form'],
extra_vocabularies={'near_far_in_form': FORM_NEAR_FAR})
near = knowledge.face_designation(know, 'N.F.')
assert (near['frame'], near['side']) == ('form', 'top')
assert knowledge.face_designation_problems(know) == []
def test_two_tokens_with_the_same_frame_and_side_are_the_same_face():
"""The equivalence is structural, not a comment: N.F. is (form, top) and
T.I.F. is (form, top), so they are one face and nothing had to be told
so."""
know = _know(['near_far_in_form', 'in_form'],
extra_vocabularies={'near_far_in_form': FORM_NEAR_FAR})
assert knowledge.same_face(know, 'N.F.', 'T.I.F.') is True
assert knowledge.same_face(know, 'F.F.', 'B.I.F.') is True
assert knowledge.same_face(know, 'N.F.', 'B.I.F.') is False
def test_the_view_anchored_vocabulary_is_a_different_face_question_entirely():
"""With the published, view-anchored near_far selected, N.F. is (view,
near) and T.I.F. is (form, top). Two frames, so they are not the same
face -- and that is the right answer, not a bug: the question has no
answer until something resolves the view against the form."""
know = _know(['near_far', 'in_form'])
near = knowledge.face_designation(know, 'N.F.')
assert (near['frame'], near['side']) == ('view', 'near')
assert knowledge.same_face(know, 'N.F.', 'T.I.F.') is False
def test_same_face_says_it_cannot_tell_rather_than_saying_no():
"""An unknown abbreviation is not "those are different faces"."""
know = _know(['in_form'])
assert knowledge.same_face(know, 'N.F.', 'T.I.F.') is None
assert knowledge.same_face(know, 'ZZZ', 'T.I.F.') is None
def test_a_side_that_does_not_belong_to_its_frame_is_reported():
"""The half-edit: flip a published vocabulary's frame to `form` and leave
`side: near` behind, and you get a vocabulary that parses, resolves, and
answers nonsense."""
know = _know(['bad'], extra_vocabularies={'bad': {
'frame': 'form',
'tokens': [{'token': 'N.F.', 'means': 'near face', 'side': 'near'}]}})
problems = knowledge.face_designation_problems(know)
assert len(problems) == 1
assert "'near'" in problems[0] and "'form'" in problems[0]
def test_the_shipped_catalogue_publishes_the_sides_each_frame_can_name():
branch = _catalogue_face_designations()
assert set(branch['frames']['view']['sides']) == {'near', 'far', 'both'}
assert set(branch['frames']['form']['sides']) == {'top', 'bottom', 'side',
'both'}
# every published token's side belongs to its own vocabulary's frame
for vocab_id, vocab in branch['vocabularies'].items():
allowed = branch['frames'][vocab['frame']]['sides']
for entry in vocab['tokens']:
assert entry['side'] in allowed, (vocab_id, entry['token'])
def test_a_frame_published_as_a_plain_sentence_is_tolerated_not_flagged():
"""An older plant file may carry `frames: {form: "a sentence"}`. It
publishes no side list, so the side check stands down rather than calling
every side invalid -- an older file must not turn into a page of
findings."""
know = _know(['in_form'])
knowledge._deep_update(know, {'face_designations': {'frames': {
'form': 'Relative to the casting bed.'}}})
assert knowledge.face_designation_problems(know) == []
# ==========================================================================
# WHICH END OF A PIECE IS UP ONCE ERECTED (owner ruling, 2026-08-30)
# ==========================================================================
#
# "哪头朝上取决于 assembly origin。也就是 TOT。写入公共知识。" -- which end is
# up comes from the ASSEMBLY ORIGIN, and it is PUBLIC knowledge.
#
# Two things are pinned below and they are different in kind:
#
# * the PLACEMENT -- the rule ships in `knowledge/catalogue.yaml`, the layer
# that identifies nobody, and NOT in a plant file. That placement is the
# substance of the ruling: a producer does not have to declare how a
# precast assembly is built.
# * the DORMANCY -- the half of the rule that needs a live model is null,
# unverified, and says why. It is the `drawings.reinforcing_rule.category`
# idiom, deliberately reused rather than reinvented, and the tests below
# exist so that the day somebody fills it in they are told what changed.
def _catalogue_orientation():
"""The shipped public layer, read the way `_merge_file` reads it."""
import io
from vibe import knowledge as K, yamlkit
data = yamlkit.loads(
io.open(K.catalogue_path(), encoding='utf-8').read())
return K._coerce(data['orientation'])
def test_the_up_end_rule_ships_in_the_public_layer_not_a_plant_file():
"""The ruling is that this is PUBLIC. If it ever moves to a plant file,
every plant has to declare a fact about precast assemblies in general --
which is the shape the two deleted `orientation.*` keys had."""
branch = _catalogue_orientation()
assert branch['up_end']['source'] == knowledge.UP_END_SOURCE
assert branch['up_end']['also_called'] == 'TOT'
assert branch['up_end']['stated_on'] == '2026-08-30'
def test_the_shipped_rule_is_live_and_names_what_it_reads():
"""It shipped dormant for as long as nobody had read an origin, and went
live as DATA: `origin_up` and the verified flag, no code change. That is
the whole return on having written it as a shape rather than an `if`."""
up_end = _catalogue_orientation()['up_end']
assert up_end['origin_up'] == 'BasisY'
assert up_end['origin_up_verified'] is True
assert up_end['origin_up_verified_on'] == '2026-08-30'
# The words the owner used survive alongside the measurement, because the
# measurement is what CONFIRMED them, not what replaced them.
assert up_end['origin_up_as_stated']
def test_the_colour_mapping_is_two_separate_frames_not_one_widened_rule():
"""2026-09-01: 'horizontal panel跟vertical panel,三个箭头的方向本来就
是不一样的' -- the owner separated this into two frames rather than
widening one. also_seen_as carries both, and it lives in its own field
and never in the sentence that says what origin_up measured."""
up_end = _catalogue_orientation()['up_end']
also = up_end['also_seen_as']
assert set(also) == {'horizontal_panels', 'vertical_wall_panels'}
assert 'colour' not in (up_end.get('origin_up') or '')
horiz = also['horizontal_panels']
assert horiz['green']['axis'] == 'BasisY'
assert horiz['purple']['axis'] == 'BasisZ'
assert horiz['red']['axis'] == 'BasisX'
assert 'confirmed' in horiz['status']
vert = also['vertical_wall_panels']
assert vert['green']['axis'] == 'BasisY'
assert vert['green']['confirmed'] is True
assert 'world up' in vert['green']['means']
# Purple and red are a stated GAP, not an inferred borrow from the
# horizontal frame -- the owner's own words are that the arrows differ
# between the two frames to begin with.
assert vert['purple']['confirmed'] is False
assert vert['purple']['axis'] is None
assert vert['red']['confirmed'] is False
assert vert['red']['axis'] is None
assert vert['purple']['open_question']
assert vert['red']['open_question']
def test_up_end_rule_runs_on_the_shipped_catalogue():
source, why = knowledge.up_end_rule(
{'orientation': _catalogue_orientation()})
assert source == knowledge.UP_END_SOURCE
assert 'TOT' in why and 'BasisY' in why
def test_up_end_rule_goes_back_to_sleep_if_the_flag_is_cleared():
"""The verified flag is load-bearing in both directions -- clearing it
must stand the rule down, or it is decoration."""
branch = _catalogue_orientation()
branch['up_end']['origin_up_verified'] = False
source, why = knowledge.up_end_rule({'orientation': branch})
assert source is None
assert 'origin_up_verified is false' in why
def test_a_stated_but_unverified_origin_property_still_stands_down():
"""Writing down what somebody said is not the same as checking it, and
the flag is what keeps them apart. Reversed, this is how a guess about a
Revit property would quietly become the rule."""
branch = _catalogue_orientation()
branch['up_end']['origin_up'] = 'the red axis, probably'
branch['up_end']['origin_up_verified'] = False
source, why = knowledge.up_end_rule({'orientation': branch})
assert source is None
assert 'the red axis, probably' in why
assert 'origin_up_verified is false' in why
def test_a_layer_may_not_invent_a_mechanism_this_tool_does_not_implement():
"""A higher layer may override the ANSWER; it may not name a different
MECHANISM, because a mechanism is code. An unimplemented source stands
down and quotes itself back rather than falling through to a guess."""
source, why = knowledge.up_end_rule(
{'orientation': {'up_end': {'source': 'whichever_end_is_higher',
'origin_up': 'anything',
'origin_up_verified': True}}})
assert source is None
assert 'whichever_end_is_higher' in why
assert knowledge.UP_END_SOURCE in why
def test_nothing_declared_is_not_a_licence_to_assume():
for know in ({}, {'orientation': {}}, {'orientation': {'up_end': {}}}):
source, why = knowledge.up_end_rule(know)
assert source is None
assert why
def test_the_two_superseded_orientation_keys_are_not_declared_anywhere():
"""`top_is_higher_global_z` and `top_end_families` were deleted, not
renamed. They asked a producer to declare a fact about precast in
general, and the second was an inert cross-check with no check -- the
exact shape D3 flagged. A default reintroducing either would put the
question back."""
assert 'orientation' not in knowledge.DEFAULTS
branch = _catalogue_orientation()
assert 'top_is_higher_global_z' not in branch
assert 'top_end_families' not in branch
assert set(branch) == {'up_end'}
# -------------------------------------------------------------------------
# ... and the three ways the origin is wrong on real pieces.
#
# Every one of these is a piece in front of us, not a hypothetical. The
# refusals are the reason the rule needed code at all -- turning it ON needed
# none.
UP = (0.0, 0.0, 1.0)
def _live_orientation():
return {'orientation': _catalogue_orientation()}
def test_the_origin_answers_a_clean_horizontal_piece():
ok, why = knowledge.up_end_from_origin(
_live_orientation(), UP, 'width', 30.0, 10.0, label='PNL101')
assert ok is True
assert 'PNL101' in why and 'BasisY' in why
def test_an_origin_that_does_not_stand_the_piece_up_is_refused_by_name():
"""One assembly in 282 has its up basis pointing sideways and its FACE
NORMAL up instead. Clamping to the nearest axis is exactly how a single
odd piece becomes a silently mis-erected one, so this asserts."""
ok, why = knowledge.up_end_from_origin(
_live_orientation(), (0.0, 1.0, 0.0), 'width', 30.0, 10.0,
label='PNL371')
assert ok is None # never False -- see the docstring
assert 'PNL371' in why
assert 'does not stand the piece up' in why
def test_a_near_square_piece_declines_rather_than_picking_a_side():
"""11.58 x 11.63 and 10.71 x 11.63 are real; every clean case is 1.7 and
up. In that band long-versus-up is a coin toss."""
for long_ft, short_ft in ((11.63, 11.58), (11.63, 10.71)):
ok, why = knowledge.up_end_from_origin(
_live_orientation(), UP, 'width', long_ft, short_ft, label='PNL127')
assert ok is None
assert 'near square' in why
def test_the_declining_band_sits_clear_of_both_edges_of_the_evidence():
"""1.25 was chosen, not inherited: above the near-square pair (1.00, 0.92)
and below the tightest real horizontal piece (1.7). A test, so moving it
is a decision somebody has to make on purpose."""
assert _catalogue_orientation()['up_end']['near_square_ratio'] == 1.25
know = _live_orientation()
assert knowledge.up_end_from_origin(know, UP, 'width', 1.24, 1.0)[0] is None
assert knowledge.up_end_from_origin(know, UP, 'width', 1.7, 1.0)[0] is True
def test_a_vertical_panel_stands_the_rule_down_rather_than_assuming_it():
"""The frame is confirmed for horizontal panels. The owner has said
vertical ones work differently and has not yet said how; standing down
costs one fallback, assuming costs an erection."""
ok, why = knowledge.up_end_from_origin(
_live_orientation(), UP, 'length', 30.0, 10.0, label='PNL1')
assert ok is None
assert 'HORIZONTAL panels only' in why
assert 'PNL1' in why
def test_a_piece_with_no_readable_dimensions_declines():
for spans in ((None, 10.0), (30.0, 0.0), (30.0, None)):
ok, why = knowledge.up_end_from_origin(
_live_orientation(), UP, 'width', spans[0], spans[1])
assert ok is None and why
def test_up_end_from_origin_never_answers_false():
"""False would have to mean "the origin says the piece is upside down",
and nothing here can mean that. A caller reading a refusal as False puts
the anchors on the wrong edge -- the exact failure this all guards."""
know = _live_orientation()
for args in ((UP, 'width', 30.0, 10.0),
((0.0, 1.0, 0.0), 'width', 30.0, 10.0),
(UP, 'length', 30.0, 10.0),
(UP, 'width', 11.63, 11.58),
(UP, 'thickness', 30.0, 10.0),
(UP, None, 30.0, 10.0),
((0.0, 0.0, 0.0), 'width', 30.0, 10.0),
(None, 'width', 30.0, 10.0)):
ok, why = knowledge.up_end_from_origin(know, *args)
assert ok is True or ok is None, args
assert why, args
def test_the_origin_point_question_is_recorded_rather_than_decided():
"""`BasisY` is asserted to be world up before it is used, so where the
rule RUNS it agrees with the fallback by construction -- its value on
those pieces is the three refusals, not an independent signal. The owner
said the origin has an up END in it; BasisY is a DIRECTION. Whether
Transform.Origin also SITS AT that end has not been asked or measured, and
an unasked question left null with a reason is not the same as an answer
of no."""
up_end = _catalogue_orientation()['up_end']
assert up_end['origin_point_meaning'] is None
assert up_end['origin_point_unverified_because']
# ============================================================================
# THE FACE THE MODEL RECORDS, as against the face the TICKET prints
# ============================================================================
#
# Landed 2026-08-31 on two censuses -- 282 panels / 38,662 members in the
# model, and 180 real tickets on paper. A face designation has TWO carriers
# and 5.3 asks whether they agree, so the model-side half had to exist before
# the check could. It is a String parameter on the element INSTANCE.
#
# What these tests defend is the shape of the join, and there are four things
# in it that a simpler design would have got wrong:
#
# * the join is a SEPARATE KEY, not an alias on a published token. `tokens`
# is a list and lists replace, so an alias would replace the whole
# published vocabulary -- the half-edit `face_designation_problems`
# already refuses.
# * it reads the INSTANCE only. The same part code carries different tokens
# on different sheets, so a type read is wrong, not weaker.
# * a MODIFIER stacks after a face token in the same call-out and names no
# face at all. A reader expecting one parenthesised group mis-parses it.
# * absence is never a value, on either side.
def _model_know(model_tokens, use=None, extra_vocabularies=None, **extra):
know = _know(['near_far', 'in_form'] if use is None else use,
extra_vocabularies=extra_vocabularies)
branch = {'model_tokens': model_tokens}
branch.update(extra)
knowledge._deep_update(know, {'face_designations': branch})
knowledge._deep_update(
know, {'parameters': {'member': {'face_designation': 'A_PARAM'}}})
return know
def test_a_model_value_resolves_to_a_printed_designation_with_its_frame():
know = _model_know({'FF': 'F.F.', 'SIF': 'S.I.F.'})
got = knowledge.face_from_model_token(know, 'FF')
assert got['token'] == 'F.F.'
assert (got['face']['frame'], got['face']['side']) == ('view', 'far')
assert got['why']
def test_the_model_value_is_matched_case_and_whitespace_insensitively():
know = _model_know({'FF': 'F.F.'})
for raw in ('FF', 'ff', ' Ff ', '\tFF\n'):
assert knowledge.face_from_model_token(know, raw)['token'] == 'F.F.'
def test_the_face_is_read_off_the_instance_and_never_off_the_type():
"""Not a default that could be widened later. Measured over 180 tickets,
the SAME part code carries different face tokens on different sheets --
24 codes do -- so the face is a property of the placement. A type read
collapses distinct facts onto one answer and looks clean doing it."""
assert knowledge.face_read_from(_know([])) == ['instance']
know = _model_know({'FF': 'F.F.'}, read_from=['instance'])
assert knowledge.face_read_from(know) == ['instance']
def test_an_element_with_no_value_is_a_fact_about_the_element():
"""Blank on the kinds that do not use the field -- that is not a broken
mapping and the sentence must not send anyone to the config."""
know = _model_know({'FF': 'F.F.'})
for raw in (None, '', ' '):
got = knowledge.face_from_model_token(know, raw)
assert got['face'] is None and got['token'] is None
assert 'not a face' in got['why']
def test_a_blank_is_told_apart_from_a_field_that_does_not_apply():
"""Two blanks, two sentences. Within the kinds that record a face at all
the model writes one explicitly 163 times AND still holds 1,304 blanks --
if blank meant near face, near face would never need writing."""
know = _model_know({'FF': 'F.F.'}, recorded_on=['EMBED'])
unknown = knowledge.face_from_model_token(know, '', component='EMBED 4T')
assert 'UNKNOWN' in unknown['why'] and 'not near' in unknown['why']
na = knowledge.face_from_model_token(know, '', component='REBAR CUSTOM')
assert 'does not apply' in na['why']
assert unknown['face'] is None and na['face'] is None
def test_which_kinds_record_a_face_ships_empty_and_says_why():
"""Empty is "nobody has written it down", never "no kind records one" --
and with nothing there a blank cannot be told from a field that does not
apply, which the sentence has to admit rather than pick one."""
know = _model_know({'FF': 'F.F.'})
kinds, why = knowledge.face_recorded_on(know)
assert kinds == [] and why
got = knowledge.face_from_model_token(know, '', component='EMBED 4T')
assert got['face'] is None
assert 'not a face' in got['why']
assert why in got['why'] # the reason travels into the sentence
def test_lifting_hardware_with_no_face_is_its_own_stand_down():
"""Not a rare corner: the ticket prints the near-face token more than any
other, and the part those call-outs most often name is the face lifter --
blank in the model on every one of its instances. A part that is on a
face by definition, with a model that does not say which, is evidence to
decline on and never a disagreement."""
know = _model_know({'FF': 'F.F.'})
knowledge._deep_update(know, {'families': {'lifter_include': ['XLIFT']}})
got = knowledge.face_from_model_token(know, '', family='SAMPLE_XLIFT_4T')
assert got['face'] is None
assert 'lifting hardware' in got['why'] and 'Stand down' in got['why']
def test_a_value_nobody_mapped_is_refused_and_the_known_values_are_named():
"""A face vocabulary has no "other", so a guessed token resolves silently
to a face nobody meant. Refuse, and name what IS known."""
know = _model_know({'FF': 'F.F.', 'SIF': 'S.I.F.'})
got = knowledge.face_from_model_token(know, 'ZZ')
assert got['face'] is None and got['token'] is None
assert "'ZZ'" in got['why'] and 'FF, SIF' in got['why']
def test_no_mapping_at_all_stands_down_rather_than_reading_the_raw_value():
"""The raw value is not a printed token: `FF` is not `F.F.`, and
resolving it by luck at one plant is how it resolves wrongly at the
next."""
know = _know(['near_far', 'in_form'])
got = knowledge.face_from_model_token(know, 'FF')
assert got['face'] is None and got['token'] is None
assert 'model_tokens' in got['why']
assert knowledge.face_model_tokens(know) == {}
def test_a_mapping_onto_a_token_no_selected_vocabulary_carries_is_reported():
know = _model_know({'XX': 'Z.Z.'})
got = knowledge.face_from_model_token(know, 'XX')
assert got['token'] == 'Z.Z.' and got['face'] is None
assert 'disagree' in got['why']
problems = knowledge.face_designation_problems(know)
assert len(problems) == 1 and "'Z.Z.'" in problems[0]
def test_mapping_model_values_while_selecting_nothing_is_reported_too():
"""Selecting no vocabulary is normally silent. Saying what the MODEL's
values mean while selecting nothing to resolve them into is a different
statement, and a contradictory one."""
know = _model_know({'FF': 'F.F.'}, use=[])
problems = knowledge.face_designation_problems(know)
assert len(problems) == 1 and 'selects no vocabulary' in problems[0]
def test_a_healthy_mapping_reports_no_problems():
assert knowledge.face_designation_problems(
_model_know({'FF': 'F.F.', 'SIF': 'S.I.F.'})) == []
def test_the_model_field_meets_the_vocabulary_a_plant_already_declared():
"""The point of landing this as a role plus a map rather than as a check:
a plant that anchors near/far to the FORM gets its model's `FF` resolved
to (form, bottom), and `same_face` answers against the trade's B.I.F.
from the data. No code knows about either token."""
know = _model_know({'FF': 'F.F.', 'SIF': 'S.I.F.'},
use=['near_far_in_form', 'in_form'],
extra_vocabularies={'near_far_in_form': FORM_NEAR_FAR})
got = knowledge.face_from_model_token(know, 'FF')
assert (got['face']['frame'], got['face']['side']) == ('form', 'bottom')
assert knowledge.same_face(know, got['token'], 'B.I.F.') is True
assert knowledge.same_face(know, got['token'], 'T.I.F.') is False
def test_the_face_role_is_declared_and_optional():
assert knowledge.FACE_ROLE == 'member.face_designation'
assert knowledge.FACE_ROLE in knowledge.PARAMETER_ROLES
assert knowledge.FACE_ROLE in knowledge.OPTIONAL_ROLES
# ---------------------------------------------- a call-out's other groups
MODIFIERS = {'E.S.': {'means_inferred': 'each side', 'means_verified': False}}
NOT_FACES = {'O.H.': {'why': 'qualifies a dimension, not a part'}}
def _callout_know():
return _model_know({'FF': 'F.F.'}, modifiers=MODIFIERS,
not_face_tokens=NOT_FACES)
def test_a_modifier_is_stripped_and_the_face_underneath_still_resolves():
"""`P106 (S.I.F.) (E.S.)` -- 35 of them, always after a face token and
never instead of one. A reader expecting exactly one parenthesised group
mis-parses the pair, which is why modifiers are a set of their own."""
got = knowledge.split_face_callout(_callout_know(), ['S.I.F.', 'E.S.'])
assert got['token'] == 'S.I.F.'
assert got['face']['frame'] == 'form'
assert [m['token'] for m in got['modifiers']] == ['E.S.']
assert got['unknown'] == []
def test_a_modifier_carries_no_side_and_its_meaning_is_marked_unverified():
""""each side" is a reading of the letters, not a fact anyone stated, so
nothing may act on it. Stripping the group needs no meaning at all."""
entry = knowledge.face_modifier(_callout_know(), 'e.s.')
assert entry['token'] == 'E.S.'
assert 'side' not in entry # no (frame, side): not a face
assert entry['means_verified'] is False
assert knowledge.face_designation(_callout_know(), 'E.S.') is None
def test_a_group_declared_not_to_be_a_face_comes_back_as_that():
"""It pattern-matches a face designation exactly and qualifies a
dimension. Without somewhere to record the negative it returns forever as
a false positive or an unexplained unknown."""
got = knowledge.split_face_callout(_callout_know(), ['O.H.'])
assert got['face'] is None
assert [n['token'] for n in got['not_faces']] == ['O.H.']
assert got['unknown'] == []
def test_an_unrecognised_group_is_kept_and_never_silently_dropped():
""""names no face" and "names a face in words nobody wrote down" want
opposite actions, and a reader that ignores what it does not recognise
cannot tell them apart."""
got = knowledge.split_face_callout(_callout_know(), ['Q.Q.'])
assert got['unknown'] == ['Q.Q.']
assert 'NOT RECOGNISED' in got['why']
def test_two_face_tokens_in_one_callout_are_refused_rather_than_ranked():
"""A part is on one face. Taking the first would decide it by print
order."""
got = knowledge.split_face_callout(_callout_know(), ['N.F.', 'F.F.'])
assert got['face'] is None and got['token'] is None
assert '2 face designations' in got['why']
# ------------------------------------------- which member kinds a token fits
ATTACHES = {'N.F.': ['embed', 'bar'], 'F.F.': ['embed', 'bar'],
'S.I.F.': ['embed'], 'E.L.': ['bar']}
def test_a_designation_on_the_wrong_kind_of_member_is_a_real_finding():
"""Measured over a whole ticket corpus without exception: the
side-in-form token attaches to an embed 177 times out of 177 and never to
a bar. So one on a bar is wrong by construction, and that is data here
rather than a rule buried inside a check that has not been written."""
know = _model_know({'FF': 'F.F.'}, attaches_to=ATTACHES)
assert knowledge.face_attachment_verdict(know, 'S.I.F.', 'embed')[0] is True
assert knowledge.face_attachment_verdict(know, 'S.I.F.', 'bar')[0] is False
assert knowledge.face_attachment_verdict(know, 'E.L.', 'bar')[0] is True
assert knowledge.face_attachment_verdict(know, 'E.L.', 'embed')[0] is False
assert knowledge.face_attachment_verdict(know, 'N.F.', 'bar')[0] is True
assert knowledge.face_attachment_verdict(know, 'N.F.', 'embed')[0] is True
def test_nothing_written_down_is_none_and_never_false():
""""no rule recorded" and "that is not allowed" are opposite answers, and
False would assert the second."""
know = _model_know({'FF': 'F.F.'}, attaches_to=ATTACHES)
for args in (('T.I.F.', 'bar'), ('S.I.F.', None), ('S.I.F.', '')):
verdict, why = knowledge.face_attachment_verdict(know, *args)
assert verdict is None and why, args
bare = _model_know({'FF': 'F.F.'})
assert knowledge.face_attachment_verdict(bare, 'S.I.F.', 'bar')[0] is None
# ------------------------------- which BUILDING face is top in form: per piece
TOP_IN_FORM = {'label': 'Top in Form :', 'source': 'design summary',
'values': ['Interior', 'Exterior'], 'verified': False,
'unverified_because': 'nothing reads it yet'}
def _handed_know():
return _model_know({'NF': 'N.F.', 'FF': 'F.F.'},
use=['near_far_in_form', 'in_form'],
extra_vocabularies={'near_far_in_form': FORM_NEAR_FAR},
top_in_form=TOP_IN_FORM)
def test_top_in_form_ships_dormant_and_names_the_reason():
rule = knowledge.top_in_form_rule(_handed_know())
assert rule['label'] == 'Top in Form :' # the space is how it prints
assert rule['verified'] is False
assert 'nothing reads it yet' in rule['why']
def test_a_form_anchored_token_refuses_to_name_a_building_face_unread():
"""THE ONE THAT MATTERS. The design output states which building face is
top in form PER PANEL and it varies panel to panel, so a check that
resolves a printed designation to interior or exterior without reading
the piece's own value is inverted on every panel of the opposite
handedness -- one normal-looking verdict at a time."""
know = _handed_know()
face, why = knowledge.building_face_of(know, 'N.F.')
assert face is None
assert 'varies piece by piece' in why and 'Top in Form :' in why
def test_with_the_piece_read_the_top_face_resolves_and_only_that_one():
"""Top in form is the face the piece names. The OPPOSITE of a stated
building face is not derivable from that statement -- a piece has more
than two faces -- so the bottom one refuses instead of inverting."""
know = _handed_know()
assert knowledge.building_face_of(know, 'N.F.', 'Interior')[0] == 'Interior'
assert knowledge.building_face_of(know, 'N.F.', 'Exterior')[0] == 'Exterior'
face, why = knowledge.building_face_of(know, 'F.F.', 'Interior')
assert face is None and 'do not invert it' in why
def test_a_view_framed_token_says_nothing_about_the_form():
"""Near/far measured from the drawing is a different question, and it has
no answer until something resolves the view against the form."""
know = _model_know({'FF': 'F.F.'}, top_in_form=TOP_IN_FORM)
face, why = knowledge.building_face_of(know, 'N.F.', 'Interior')
assert face is None and 'view frame' in why
def test_a_token_naming_every_face_does_not_pick_one():
know = _handed_know()
face, why = knowledge.building_face_of(know, 'E.L.', 'Interior')
assert face is None and 'every face' in why
# ============================================================================
# A BAR'S SIZE: two structured fields, two populations
# ============================================================================
#
# Landed 2026-08-31 (open question A3). Census over all 282 wall panels,
# 24,539 rebar members in 13 families: the size parameter is carried by the
# one STRAIGHT family and by none of the twelve bent ones, which hold their
# size in the FAMILY's part description instead -- 7,405 of 7,405 bent members
# parse, in one uniform shape, with no exceptions.
#
# THE TWO SETS ARE DISJOINT, so there is no precedence question. The order in
# `bar_size_of` is arbitrary and must never be read as a ranking: the
# family-level description is structured plant data, not notes. (The
# ASSEMBLY's identically named field IS notes. Two fields, two kinds of
# element, one name.)
def test_the_bar_size_role_is_declared_and_optional():
assert knowledge.BAR_SIZE_ROLE == 'member.bar_size'
assert knowledge.BAR_SIZE_ROLE in knowledge.PARAMETER_ROLES
assert knowledge.BAR_SIZE_ROLE in knowledge.OPTIONAL_ROLES
def _sized(mapped='A_PARAM', description_fallback=True,
bent_tokens=('BENT BAR',)):
know = _rebar(PLANT_STOCK)
if mapped:
knowledge._deep_update(
know, {'parameters': {'member': {'bar_size': mapped}}})
knowledge._deep_update(know, {'drawings': {'bar_size_rule': {
'description_fallback': description_fallback,
'bent_bar_tokens': list(bent_tokens)}}})
return know
def test_a_size_with_a_parenthesised_qualifier_is_still_that_size():
"""THE DEFECT THIS CLOSES. The pattern required the whole string to be
the size, so a real value in a real model -- the one bar size the owner
had specifically named -- came back None and every bar of it went
unjudged by 5.7. Silently, because an unreadable size is a stand-down and
a stand-down on one value in six looks like a working check."""
assert knowledge.rebar_size('#9 (80 KSI)') == '#9'
assert knowledge.rebar_size_parts('#9 (80 KSI)') == ('#9', '80 KSI')
assert knowledge.rebar_stock(_rebar(PLANT_STOCK),
'#9 (80 KSI)')['basis'] == 'by_size'
def test_the_tolerance_is_parentheses_and_nothing_else():
"""A qualifier is parenthesised; anything else trailing is not a size at
all. Widening this to "a size followed by whatever" would make a part
DESCRIPTION read as a bare size, and a plant that mapped the size role at
that field would then be parsing prose with the strong reader."""
for raw in ('#4 BENT BAR (17)', '#4 X 8-8', '#4 EXTRA', 'SR324'):
assert knowledge.rebar_size(raw) is None, raw
assert knowledge.rebar_size_parts('#4 BENT BAR (17)') == (None, None)
def test_a_grade_stated_in_a_qualifier_is_captured_rather_than_discarded():
"""`rebar_fy` has a provenance chain whose strongest rung, 'stated', had
nowhere to be filled from -- nothing in the model was ever read as a
grade. A bar whose own size field says 80 KSI has stated one."""
assert knowledge.rebar_size_qualifier_fy('80 KSI') == 80.0
assert knowledge.rebar_size_qualifier_fy('GRADE 80') == 80.0
got = knowledge.bar_size_of(_sized(), '#9 (80 KSI)')
assert got['size'] == '#9' and got['qualifier_fy_ksi'] == 80.0
fy, basis, _why = knowledge.rebar_fy(
_sized(), stated_fy_ksi=got['qualifier_fy_ksi'])
assert (fy, basis) == (80.0, 'stated')
def test_a_qualifier_that_is_not_a_grade_is_not_read_as_one():
"""A shape code in parentheses is not a strength, and a number outside
every published rebar grade is something else in the brackets."""
for qualifier in ('17', 'R26A', 'EPOXY', '5', '400'):
assert knowledge.rebar_size_qualifier_fy(qualifier) is None, qualifier
def test_a_straight_bar_is_sized_from_the_size_field():
got = knowledge.bar_size_of(_sized(), '#4')
assert (got['size'], got['basis']) == ('#4', 'parameter')
assert 'A_PARAM' in got['why']
def test_a_bent_bar_is_sized_from_the_part_description():
"""Not a fallback: the bent families carry no size field at all, so this
is the reader for that population."""
for raw, size in (('#4 BENT BAR (17)', '#4'), ('#3 BENT BAR (17)', '#3'),
('#6 BENT BAR (53)', '#6'),
('#4 BENT BAR (R26A)', '#4')):
got = knowledge.bar_size_of(_sized(), None, raw)
assert (got['size'], got['basis']) == (size, 'description'), raw
assert 'rather than a fallback' in got['why']
def test_the_description_parse_is_gated_on_the_shape_that_makes_it_evidence():
"""A description that no longer reads as a bent bar leaves the bar
UNJUDGED rather than mis-sized. The shape IS the evidence that the
leading token is a bar size, so a shape that changes must stop the read
-- it must not be parsed through."""
got = knowledge.bar_size_of(_sized(), None, '#3 X 8-8')
assert got['size'] is None and got['basis'] is None
assert 'does not have the shape' in got['why']
def test_a_leading_bare_number_in_a_description_is_refused():
"""Nothing marks it as a size."""
got = knowledge.bar_size_of(_sized(), None, '4 BENT BAR (17)')
assert got['size'] is None
assert 'bare number' in got['why']
def test_a_bent_bar_is_recognised_in_three_states_and_never_two():
know = _sized()
assert knowledge.bent_bar_verdict(know, '#4 BENT BAR (17)')[0] is True
assert knowledge.bent_bar_verdict(know, '#3 X 8-8')[0] is False
assert knowledge.bent_bar_verdict(know, None)[0] is None
bare = _sized(bent_tokens=())
verdict, why = knowledge.bent_bar_verdict(bare, '#4 BENT BAR (17)')
assert verdict is None and 'bent_bar_tokens is empty' in why
def test_a_bar_nothing_can_size_says_which_of_the_states_it_is_in():
"""Three states, three actions. "No size parameter is mapped" said to
somebody who has just mapped one sends them to re-check a file that is
already right."""
mapped_off = knowledge.rebar_stock(
_sized(description_fallback=False), None)['why']
assert 'IS mapped' not in mapped_off # phrasing may vary; check facts
assert 'A_PARAM' in mapped_off and 'description_fallback' in mapped_off
nothing = knowledge.rebar_stock(
_sized(mapped=None, description_fallback=False), None)['why']
assert 'on any bar' in nothing
junk = knowledge.rebar_stock(_sized(), 'SR324')['why']
assert 'does not read as a bar size' in junk
def test_a_sizeless_bar_is_never_judged_against_the_generic_length():
"""A bar with no resolvable size is not judged at all: "40 ft" and "40 ft
for this size" are different claims and the generic one is wrong in the
direction that passes a defect."""
for know in (_sized(), _sized(mapped=None, description_fallback=False)):
stock = knowledge.rebar_stock(know, None)
assert stock['basis'] is None
assert stock['length_in'] is None and stock['length_ft'] is None
def test_the_sizes_the_model_holds_resolve_straight_into_the_stock_table():
"""The measured values are already the printed form the table is keyed
by, so nothing converts anything."""
know = _sized()
for printed in ('#3', '#4', '#5'):
stock = knowledge.rebar_stock(know, printed)
assert stock['basis'] == 'by_size' and stock['size'] == printed
# ============================================================================
# WHICH PART PLAYS A ROLE DEPENDS ON THE PANEL TYPE
# ============================================================================
#
# Landed 2026-08-31, urgently. The owner said the face lifter on an insulated
# wall is a different part from the one on a solid wall; that part was named
# in no knowledge layer; so on every insulated panel the tool found ZERO face
# lifters and every lifting check -- capacity, edge distance, handling, and
# Panel Lift Check's face stages -- passed in silence. On safety checks.
#
# It is the second time that exact failure has happened here, which is why the
# fix is a SHAPE and not two more list entries: a third panel type must be an
# added key, not a redesign.
#
# The asymmetry these tests exist to hold:
#
# RECOGNISING a part a family left out = a silent pass. A family wrongly
# in = a visible finding about a real insert. So an
# unclassified piece gets the UNION.
# COUNTING one the union is a wrong count with nothing on the
# report to show for it. So a counting caller must
# refuse the union basis.
PANEL_TYPES = {'types': {'solid': {}, 'insulated': {}}, 'default': None}
FACE_PATH = 'handling.lifter_groups.face.families'
def _panels(base, by_type=None, **branch):
know = knowledge._deep_copy(knowledge.DEFAULTS)
types = dict(PANEL_TYPES)
types.update(branch)
knowledge._deep_update(know, {'panel_types': types})
face = {'families': list(base)}
if by_type is not None:
face['families_by_panel_type'] = by_type
knowledge._deep_update(
know, {'handling': {'lifter_groups': {'face': face}}})
return know
def test_an_unclassified_piece_gets_every_type_s_families_and_says_so():
"""THE ONE THAT CLOSES THE SILENT PASS. Nothing can yet tell an insulated
panel from a solid one, and a role that resolved to nothing -- or to only
the ordinary type's part -- is exactly how every lifting check on an
insulated panel passed while finding no lifters."""
know = _panels(['P136'], {'solid': ['P136'], 'insulated': ['P78']})
families, basis, why = knowledge.families_for(know, FACE_PATH)
assert set(families) == {'P136', 'P78'}
assert basis == 'union'
assert 'NOT safe for counting' in why
def test_a_classified_piece_is_narrowed_to_its_own_type():
know = _panels(['P136'], {'solid': ['P136'], 'insulated': ['P78']})
assert knowledge.families_for(know, FACE_PATH, 'insulated')[:2] \
== (['P78'], 'panel_type')
assert knowledge.families_for(know, FACE_PATH, 'solid')[:2] \
== (['P136'], 'panel_type')
def test_the_basis_is_returned_so_a_counter_can_refuse_what_a_finder_takes():
"""Same call, same data, two callers with opposite needs. The only thing
that lets them differ is the basis, so it is returned with every answer
rather than left for a caller to infer."""
know = _panels(['P136'], {'solid': ['P136'], 'insulated': ['P78']})
for panel_type, expected in ((None, 'union'), ('insulated', 'panel_type')):
assert knowledge.families_for(know, FACE_PATH, panel_type)[1] \
== expected
def test_a_panel_type_nobody_declared_falls_back_to_the_base_and_says_so():
know = _panels(['P136'], {'solid': ['P136'], 'insulated': ['P78']})
families, basis, why = knowledge.families_for(know, FACE_PATH, 'sandwich')
assert families == ['P136'] and basis == 'base'
assert 'does not cover' in why and 'nothing was narrowed' in why
def test_a_role_with_no_breakdown_is_unchanged_by_the_panel_type():
"""The shape is additive: a role nobody has split reads exactly as it
always did, whether or not a panel type is known."""
know = _panels(['EDGE_ANCHOR'])
for panel_type in (None, 'solid', 'insulated'):
families, basis, _why = knowledge.families_for(
know, FACE_PATH, panel_type)
assert (families, basis) == (['EDGE_ANCHOR'], 'base'), panel_type
def test_a_third_panel_type_is_one_added_key_and_not_a_redesign():
"""The whole reason this is a map. It has already cost twice to discover
a population nobody had separated."""
know = _panels(['P136'], {'solid': ['P136'], 'insulated': ['P78'],
'sandwich': ['P200']})
assert knowledge.families_for(know, FACE_PATH, 'sandwich')[:2] \
== (['P200'], 'panel_type')
assert set(knowledge.families_for(know, FACE_PATH)[0]) \
== {'P136', 'P78', 'P200'}
def test_an_unclassified_piece_is_never_read_as_the_ordinary_type():
"""`panel_types.default` ships null and should stay null. Reading an
unclassified piece as solid is precisely how an insulated panel came to
be checked for a lifter it does not carry."""
know = _panels(['P136'], {'solid': ['P136'], 'insulated': ['P78']})
assert knowledge.get(know, 'panel_types.default') is None
got, why = knowledge.panel_type_of(know)
assert got is None and 'not the same as being the ordinary type' in why
def test_a_stated_panel_type_is_matched_and_an_unknown_one_is_refused():
know = _panels(['P136'], {'solid': ['P136'], 'insulated': ['P78']})
assert knowledge.panel_type_of(know, 'INSULATED')[0] == 'insulated'
got, why = knowledge.panel_type_of(know, 'sandwich')
assert got is None and 'not one of the declared panel types' in why
def test_no_panel_types_declared_is_not_a_claim_that_there_is_one_kind():
know = knowledge._deep_copy(knowledge.DEFAULTS)
assert knowledge.panel_types(know) == {}
got, why = knowledge.panel_type_of(know)
assert got is None and 'no layer declares any panel type' in why
def test_a_default_is_refused_while_nothing_can_classify_a_piece_at_all():
"""The two keys only mean something together. While detection is
unverified, a default is not "what to assume for the pieces classification
could not place" -- it is a blanket assertion that every piece in the job
is one type, which is the failure verbatim."""
know = _panels(['P136'], {'solid': ['P136']}, default='solid')
got, why = knowledge.panel_type_of(know)
assert got is None
assert 'would make EVERY piece' in why
def test_a_default_is_used_and_named_once_detection_is_verified():
"""A plant that genuinely has one ordinary type may say so, and the
sentence then says the answer came from the default rather than from the
piece."""
know = _panels(['P136'], {'solid': ['P136']}, default='solid',
detect_verified=True)
got, why = knowledge.panel_type_of(know)
assert got == 'solid' and 'panel_types.default' in why
def test_the_parity_rule_carries_a_per_panel_type_breakdown_of_its_own():
"""6.6 counts a face lifter, so it needs the narrowed list and not the
union -- a piece carrying two families where one is the lifter counts
wrong, and a wrong parity is a wrong verdict with nothing to show for
it."""
rule = knowledge.get(knowledge.DEFAULTS, 'drawings.parity_note_rule')
assert rule['family_names'] == []
assert rule['family_names_by_panel_type'] == {}
assert rule['family_verified'] is False
# ============================================================================
# THE PREFIX ON A PART NUMBER SAYS WHICH LAYER DEFINES IT
# ============================================================================
#
# Owner, 2026-08-31: P is a standard plate and is looked up in plant
# knowledge; SP is a special plate made for one job and is job knowledge. A
# layer-placement rule expressed in the part number itself -- everything else
# in these files has to be TOLD which layer owns a fact, and here the name
# says it.
#
# What it makes checkable: a part on a ticket that nothing can resolve is two
# different problems with two different owners. A P nobody can look up is a
# gap in plant knowledge; an SP nobody can look up is a gap in job knowledge.
# "Unknown part" is right half the time.
PREFIXES = {
'P': {'means': 'standard plate', 'layer': 'plant', 'verified': True},
'SP': {'means': 'special plate, made for this job', 'layer': 'job',
'verified': True},
'MK': {'means': 'erection / site mark', 'layer': 'job',
'verified': False, 'unverified_because': 'read off a manual'},
}
def _prefixed(table=None):
know = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._deep_update(
know, {'part_numbers': {'prefixes': PREFIXES if table is None
else table}})
return know
def test_the_longest_prefix_wins_and_that_is_the_whole_safety():
"""THE ONE THIS EXISTS FOR. Resolved the short way round, a job's special
plate is filed in the plant's standard-plate namespace -- the same merge a
substring matcher makes on a bare part number, which has already cost once
here on a lifting check."""
know = _prefixed()
assert knowledge.part_prefix(know, 'SP78')[0] == 'SP'
assert knowledge.part_prefix(know, 'P78')[0] == 'P'
assert knowledge.part_defining_layer(know, 'SP78')[0] == 'job'
assert knowledge.part_defining_layer(know, 'P78')[0] == 'plant'
def test_the_two_parts_that_share_a_number_land_in_different_layers():
"""`P78` and `SP78` are different parts on different piece types. The
grammar is what says so, rather than anyone having to notice."""
know = _prefixed()
assert knowledge.part_defining_layer(know, 'P78')[0] \
!= knowledge.part_defining_layer(know, 'SP78')[0]
def test_a_prefix_nobody_declared_is_refused_rather_than_guessed():
"""An entry in this table is a claim about WHOSE DESK a missing
definition belongs on, so a guessed one sends the question to the wrong
person. None is the answer, and it is a real one."""
know = _prefixed()
for part in ('IP4', 'QLPD4T5WG', 'ZZ9'):
prefix, entry, why = knowledge.part_prefix(know, part)
assert prefix is None and entry is None, part
assert 'none of the declared' in why and 'Nothing was guessed' in why
assert knowledge.part_defining_layer(know, part)[0] is None
def test_an_unverified_meaning_says_so_in_the_sentence():
"""A meaning read off a manual is a reading, not a ruling, and anything
printing it has to be able to say which."""
know = _prefixed()
why = knowledge.part_prefix(know, 'MK12')[2]
assert 'UNVERIFIED' in why and 'read off a manual' in why
# ...and it still routes: unverified is not unusable
assert knowledge.part_defining_layer(know, 'MK12')[0] == 'job'
def test_no_grammar_declared_is_not_permission_to_read_the_letters():
know = knowledge._deep_copy(knowledge.DEFAULTS)
assert knowledge.part_prefixes(know) == {}
prefix, _entry, why = knowledge.part_prefix(know, 'SP78')
assert prefix is None and 'part_numbers.prefixes is empty' in why
def test_a_prefix_with_no_layer_recorded_does_not_pick_one():
""""Nothing says which layer" and "this layer" are different answers, and
only one of them is safe to route a finding on."""
know = _prefixed({'XX': {'means': 'something', 'verified': False}})
prefix, _entry, _why = knowledge.part_prefix(know, 'XX9')
assert prefix == 'XX'
layer, why = knowledge.part_defining_layer(know, 'XX9')
assert layer is None and 'no layer is recorded' in why
def test_a_third_prefix_is_one_added_key():
table = dict(PREFIXES)
table['SR'] = {'means': 'reinforcing mark', 'layer': 'job',
'verified': False}
know = _prefixed(table)
assert knowledge.part_defining_layer(know, 'SR4996')[0] == 'job'
assert knowledge.part_prefix(know, 'SP78')[0] == 'SP' # unchanged
def test_matching_is_case_insensitive_and_tolerates_surrounding_space():
know = _prefixed()
for raw in ('sp78', ' SP78 ', 'Sp78'):
assert knowledge.part_prefix(know, raw)[0] == 'SP', raw
def test_no_part_number_is_not_an_unknown_prefix():
"""Two different silences: nobody asked, and nobody knows."""
know = _prefixed()
for raw in (None, '', ' '):
_prefix, _entry, why = knowledge.part_prefix(know, raw)
assert 'no part number was given' in why, raw
# ------------------------------- a note that is present and worded differently
def test_a_near_miss_wording_is_reported_and_never_accepted():
"""The owner named ONE phrase. Widening the rule to accept a near-miss
would quietly overrule him -- so a piece carrying one of these still
fails. But a note worded wrongly is a different defect from no note at
all: one needs a wording corrected, the other needs a note written, and a
report that reads the same for both sends the reader to the wrong job."""
rule = knowledge.get(knowledge.DEFAULTS, 'drawings.parity_note_rule')
assert rule['require_text'] is None
assert rule['require_text_also_seen'] == []
# it is a separate key from the accepted phrase, and it must stay separate
assert 'require_text_also_seen' in rule and 'require_text' in rule
# ============================================================================
# PANEL-TYPE CLASSIFICATION, AND WHAT A PLANT MAY DECLARE BECAUSE OF IT
# ============================================================================
#
# Commissioned 2026-08-31: which Ticket QC checks apply is itself plant
# knowledge, conditioned on what the piece is. Two halves, tested apart
# because they fail apart -- a classifier that cannot tell must not be able
# to switch anything off, and a declaration nobody can parse must not be able
# to hide behind a classification that worked.
def _typed(**signals):
return {'panel_types': {'types': {'insulated': {'signals': signals}},
'detect_verified': True}}
def test_panel_facts_names_its_unit_in_the_key():
"""The one place a unit error would be silent and total.
The ticket carries Revit's internal FEET; a 16 that meant feet would
match no band any plant ever writes, on every piece, and the tool would
report a whole job as unclassifiable without a single error.
"""
facts = knowledge.panel_facts(body_family='X', thickness_in=16.0)
assert facts['thickness_in'] == 16.0
assert 'thickness_in' in knowledge.FACT_KEYS
def test_a_fact_nobody_measured_is_none_and_never_a_no():
facts = knowledge.panel_facts()
assert facts['thickness_in'] is None
assert facts['body_family'] is None
assert facts['member_families'] == []
def test_two_signals_agreeing_classify_the_piece():
know = _typed(body_family_contains=['INSULATED'],
thickness_in={'min': 15.5, 'max': 16.5})
got = knowledge.classify_panel(know, knowledge.panel_facts(
body_family='WALL INSULATED 16', thickness_in=16.0))
assert got['panel_type'] == 'insulated'
assert got['basis'] == 'signals'
assert got['candidates'] == ['insulated']
def test_one_signal_alone_is_not_enough():
"""The plant file's own reasoning, enforced.
Any single signal could be a coincidence of how one job was modelled;
three that never disagree is a convention. The default floor is two, and
a piece that clears only one is UNCLASSIFIED -- which means the full
checklist, not the ordinary type.
"""
know = _typed(body_family_contains=['INSULATED'],
thickness_in={'min': 15.5, 'max': 16.5})
got = knowledge.classify_panel(know, knowledge.panel_facts(
body_family='WALL INSULATED 16')) # no thickness measured
assert got['panel_type'] is None
assert 'only 1 of the 2 signals' in got['why']
def test_signals_that_disagree_decline_to_classify():
know = _typed(body_family_contains=['INSULATED'],
thickness_in={'min': 15.5, 'max': 16.5})
got = knowledge.classify_panel(know, knowledge.panel_facts(
body_family='WALL INSULATED 16', thickness_in=8.0))
assert got['panel_type'] is None
assert got['basis'] == 'no_match'
assert 'thickness_in' in got['why']
assert 'UNCLASSIFIED' in got['why']
def test_a_type_declaring_no_signals_can_never_be_classified_into():
"""`solid` is not "whatever is not insulated".
Reading an unclassified piece as the ordinary type is precisely how an
insulated panel came to be checked for a lifter it does not carry
(2026-08-31). Not being another type is not evidence of being this one,
so a type with no signals of its own is never the answer.
"""
know = {'panel_types': {'detect_verified': True, 'types': {
'solid': {},
'insulated': {'signals': {
'body_family_contains': ['INSULATED'],
'thickness_in': {'min': 15.5, 'max': 16.5}}}}}}
got = knowledge.classify_panel(know, knowledge.panel_facts(
body_family='WALL SOLID 8', thickness_in=8.0))
assert got['panel_type'] is None
assert 'solid declares no signals' in got['why']
def test_two_types_matching_at_once_is_shouted_not_picked():
know = {'panel_types': {'detect_verified': True, 'types': {
'a': {'signals': {'body_family_contains': ['WALL'],
'thickness_in': {'min': 1, 'max': 99}}},
'b': {'signals': {'body_family_contains': ['WALL'],
'thickness_in': {'min': 1, 'max': 99}}}}}}
got = knowledge.classify_panel(know, knowledge.panel_facts(
body_family='WALL', thickness_in=16.0))
assert got['panel_type'] is None
assert got['basis'] == 'ambiguous'
assert 'CONTRADICT' in got['why']
assert got['candidates'] == ['a', 'b']
def test_a_signal_name_this_code_does_not_know_is_named_not_ignored():
know = _typed(body_family_contains=['INSULATED'], colour='blue')
got = knowledge.classify_panel(know, knowledge.panel_facts(
body_family='INSULATED', thickness_in=16.0))
assert got['panel_type'] is None
assert 'colour' in got['why']
def test_no_declared_types_classifies_nothing_and_says_so():
got = knowledge.classify_panel({}, knowledge.panel_facts(body_family='X'))
assert got['panel_type'] is None
assert got['basis'] == 'undeclared'
def test_the_classifier_reports_while_unverified_but_flags_it():
know = {'panel_types': {'detect_verified': False, 'types': {
'insulated': {'signals': {'body_family_contains': ['INSULATED'],
'thickness_in': {'min': 15, 'max': 17}}}}}}
got = knowledge.classify_panel(know, knowledge.panel_facts(
body_family='INSULATED', thickness_in=16.0))
assert got['panel_type'] == 'insulated'
assert got['verified'] is False
# ------------------------------------------ what a plant declares is not asked
def _decl(**over):
entry = {'applies': False,
'not_applicable_because': 'no such thing on these',
'not_applicable_since': '2026-08-31'}
entry.update(over)
return entry
def test_nothing_ships_switched_off():
"""The seeding rule, as a test. Nothing is switched off by default, in
any layer this repository controls."""
assert knowledge.DEFAULTS['checks']['not_applicable'] == {}
assert knowledge.DEFAULTS['checks']['only_applies_to'] == {}
def test_no_declaration_means_the_row_is_asked():
got = knowledge.check_not_applicable({}, '3.3')
assert got['not_applicable'] is False
assert got['declared'] is False
assert got['problem'] is None
def test_the_any_axis_needs_no_classification_at_all():
"""The PLANT axis: "we do not do that here". It works the day it is
written, with no classifier and no verified flag."""
know = {'checks': {'not_applicable': {'3.3': {'any': _decl()}}}}
got = knowledge.check_not_applicable(know, '3.3')
assert got['not_applicable'] is True
assert got['condition'] == 'any'
assert got['reason'] == 'no such thing on these'
assert got['since'] == '2026-08-31'
def test_a_piece_conditioned_rule_is_refused_on_an_unclassified_piece():
"""The asymmetry, as a test. Running a check that turns out not to apply
is noise; skipping one that did apply is a silent pass."""
know = {'checks': {'not_applicable': {'3.3': {'insulated': _decl()}}}}
got = knowledge.check_not_applicable(know, '3.3', panel_type=None,
verified=True)
assert got['not_applicable'] is False
assert 'REFUSED' in got['problem']
assert 'full checklist' in got['problem']
def test_a_piece_conditioned_rule_is_refused_while_detection_is_unverified():
know = {'panel_types': {'detect_unverified_because': 'nothing reads them'},
'checks': {'not_applicable': {'3.3': {'insulated': _decl()}}}}
got = knowledge.check_not_applicable(know, '3.3', panel_type='insulated',
verified=False)
assert got['not_applicable'] is False
assert 'detect_verified is false' in got['problem']
assert 'nothing reads them' in got['problem']
def test_a_piece_conditioned_rule_fires_on_a_matching_verified_piece():
know = {'checks': {'not_applicable': {'3.3': {'insulated': _decl()}}}}
got = knowledge.check_not_applicable(know, '3.3', panel_type='insulated',
verified=True)
assert got['not_applicable'] is True
assert got['condition'] == 'insulated'
def test_a_rule_about_another_type_says_nothing_about_this_piece():
know = {'checks': {'not_applicable': {'3.3': {'insulated': _decl()}}}}
got = knowledge.check_not_applicable(know, '3.3', panel_type='solid',
verified=True)
assert got['not_applicable'] is False
assert got['problem'] is None
def test_a_declaration_with_no_reason_is_refused_and_the_row_runs():
know = {'checks': {'not_applicable': {'3.3': {
'any': _decl(not_applicable_because='')}}}}
got = knowledge.check_not_applicable(know, '3.3')
assert got['not_applicable'] is False
assert 'A reason is REQUIRED' in got['problem']
def test_a_declaration_with_no_usable_date_is_refused():
for bad in (None, '', 'yesterday', '31/08/2026'):
know = {'checks': {'not_applicable': {'3.3': {
'any': _decl(not_applicable_since=bad)}}}}
got = knowledge.check_not_applicable(know, '3.3')
assert got['not_applicable'] is False, bad
assert 'YYYY-MM-DD' in got['problem'], bad
def test_a_declaration_that_does_not_say_applies_switches_nothing_off():
know = {'checks': {'not_applicable': {'3.3': {'any': {
'not_applicable_because': 'because',
'not_applicable_since': '2026-08-31'}}}}}
got = knowledge.check_not_applicable(know, '3.3')
assert got['not_applicable'] is False
assert 'does not say `applies`' in got['problem']
def test_a_job_narrows_one_pair_and_the_plants_table_survives():
"""THE WIPE-THE-TABLE TRAP, and why the shape has no list in it.
Lists replace across the layers. A list of switched-off row numbers in
the job file would delete the plant's whole table the moment a job wanted
one entry back -- a safety switch with the rebar stock-length bug in it.
With maps all the way down the job restates exactly one (row, type) pair.
"""
plant = {'checks': {'not_applicable': {
'3.3': {'insulated': _decl()},
'5.4': {'any': _decl()}}}}
job = {'checks': {'not_applicable': {'3.3': {'insulated': {
'applies': True,
'applicable_because': 'this job DOES have them',
'applicable_since': '2026-09-01'}}}}}
knowledge._deep_update(plant, job)
on = knowledge.check_not_applicable(plant, '3.3', panel_type='insulated',
verified=True)
assert on['not_applicable'] is False
assert on['reason'] == 'this job DOES have them'
# ... and the other entry, which nobody mentioned, is untouched.
still = knowledge.check_not_applicable(plant, '5.4')
assert still['not_applicable'] is True
def test_turning_a_row_back_on_must_state_its_own_case():
"""The reason key is named for the value it justifies.
After the merge nothing can tell which layer wrote which key. A single
`because:` would let a job flip `applies: true` and inherit the plant's
sentence for the OPPOSITE ruling -- a reason that reads backwards over a
re-enabled check. `applicable_because` is not a key the plant's entry
carries, so the narrowing states its own case or is refused.
"""
plant = {'checks': {'not_applicable': {'3.3': {'any': _decl()}}}}
knowledge._deep_update(
plant, {'checks': {'not_applicable': {'3.3': {'any': {
'applies': True}}}}})
got = knowledge.check_not_applicable(plant, '3.3')
assert 'applicable_because' in got['problem']
# And it does not fall through to the plant's switch-off either: the
# entry was refused whole.
assert got['not_applicable'] is False
def test_a_job_extends_the_table_without_restating_it():
plant = {'checks': {'not_applicable': {'3.3': {'any': _decl()}}}}
knowledge._deep_update(
plant, {'checks': {'not_applicable': {'5.4': {'any': _decl()}}}})
assert knowledge.check_not_applicable(plant, '3.3')['not_applicable']
assert knowledge.check_not_applicable(plant, '5.4')['not_applicable']
# ---------------------------------- a row that exists only for one type
def _scoped(**over):
entry = {'scoped': True,
'scoped_because': 'a solid zone interrupts insulation',
'scoped_since': '2026-08-31'}
entry.update(over)
return entry
def test_a_scoped_row_is_asked_on_the_type_it_is_about():
know = {'checks': {'only_applies_to': {'6.7': {'insulated': _scoped()}}}}
got = knowledge.check_not_applicable(know, '6.7', panel_type='insulated',
verified=True)
assert got['not_applicable'] is False
assert got['unaskable'] is False
assert got['only_types'] == ['insulated']
def test_a_scoped_row_is_not_applicable_on_another_type():
know = {'checks': {'only_applies_to': {'6.7': {'insulated': _scoped()}}}}
got = knowledge.check_not_applicable(know, '6.7', panel_type='solid',
verified=True)
assert got['not_applicable'] is True
assert got['condition'] == 'solid'
assert 'a solid zone interrupts insulation' in got['reason']
def test_a_scoped_row_on_an_unclassified_piece_is_unaskable_not_quiet():
"""THE REFINEMENT the "full checklist" rule needed.
A row scoped to a panel type cannot simply run on a piece nobody could
place: that answers an insulated question about a piece that may not be
one. Nor can it be NOT APPLICABLE, which claims the piece is NOT that
type -- precisely what could not be established. So nobody could ask it,
which is NOT RUN, which is red.
"""
know = {'checks': {'only_applies_to': {'6.7': {'insulated': _scoped()}}}}
got = knowledge.check_not_applicable(know, '6.7', panel_type=None,
verified=True)
assert got['unaskable'] is True
assert got['not_applicable'] is False
assert 'NOT ASKED' in got['reason']
def test_a_scoped_row_is_unaskable_while_detection_is_unverified():
know = {'checks': {'only_applies_to': {'6.7': {'insulated': _scoped()}}}}
got = knowledge.check_not_applicable(know, '6.7', panel_type='insulated',
verified=False)
assert got['unaskable'] is True
assert 'not a pass' in got['reason']
def test_scoping_a_row_to_any_is_not_a_scope_and_is_refused():
know = {'checks': {'only_applies_to': {'6.7': {'any': _scoped()}}}}
got = knowledge.check_not_applicable(know, '6.7', panel_type='solid',
verified=True)
assert got['not_applicable'] is False
assert 'not a scope' in got['problem']
def test_a_scope_with_no_reason_or_no_date_is_refused():
for over in ({'scoped_because': ''}, {'scoped_since': 'soon'}):
know = {'checks': {'only_applies_to': {
'6.7': {'insulated': _scoped(**over)}}}}
got = knowledge.check_not_applicable(know, '6.7', panel_type='solid',
verified=True)
assert got['not_applicable'] is False, over
assert got['problem'], over
def test_a_job_widens_a_scoped_row_back_to_every_piece():
plant = {'checks': {'only_applies_to': {'6.7': {'insulated': _scoped()}}}}
knowledge._deep_update(plant, {'checks': {'only_applies_to': {'6.7': {
'insulated': {'scoped': False,
'unscoped_because': 'we ask it everywhere now',
'unscoped_since': '2026-09-02'}}}}})
got = knowledge.check_not_applicable(plant, '6.7', panel_type='solid',
verified=True)
assert got['not_applicable'] is False
assert got['unaskable'] is False
assert got['only_types'] == []
# ------------------------------------------------------- which layer said it
def test_declaring_layer_names_the_last_layer_that_holds_the_key():
layers = [('defaults', None, True, {}),
('plant', 'p', True, {'checks': {'not_applicable': {
'3.3': {'any': {'applies': False}}}}}),
('job', 'j', True, {'checks': {'muted': {'3.3': 'x'}}})]
assert knowledge.declaring_layer(
layers, ['checks', 'not_applicable', '3.3', 'any']) == 'plant'
assert knowledge.declaring_layer(
layers, ['checks', 'not_applicable', '9.9']) is None
def test_declaring_layer_takes_keys_and_not_a_dotted_string():
"""Every key in this table is a checklist ROW NUMBER, which contains a
dot. A dotted path would split '3.3' into two segments and address
nothing, so the API takes the keys themselves."""
layers = [('plant', 'p', True, {'checks': {'not_applicable': {
'3.3': {'any': {'applies': False}}}}})]
assert knowledge.declaring_layer(
layers, ['checks', 'not_applicable', '3.3', 'any']) == 'plant'
def test_declaring_layer_prefers_the_layer_that_wins_the_merge():
layers = [('plant', 'p', True, {'checks': {'not_applicable': {
'3.3': {'any': {'applies': False}}}}}),
('job', 'j', True, {'checks': {'not_applicable': {
'3.3': {'any': {'applies': True}}}}})]
assert knowledge.declaring_layer(
layers, ['checks', 'not_applicable', '3.3', 'any']) == 'job'
# ============================================================================
# ATTACHED BARS, AND WHETHER A PRINTED TOKEN IS A BAR AT ALL
# ============================================================================
def test_attached_parents_normalises_the_map_the_drawing_checks_maintain():
know = {'drawings': {'annotation_covered_by_parent': {
'sr4996': ['SP29', 'SP30'], 'SR4999': 'SP61', 'SR3997': None}}}
got = knowledge.attached_parents(know)
assert got['SR4996'] == ['SP29', 'SP30']
# A single parent may be written as a scalar; no caller should have to
# test the shape.
assert got['SR4999'] == ['SP61']
assert got['SR3997'] == []
def test_attached_parents_is_empty_when_nobody_declares_it():
assert knowledge.attached_parents({}) == {}
assert knowledge.attached_parents(
{'drawings': {'annotation_covered_by_parent': ['SR4996']}}) == {}
def test_attached_parents_deep_merges_so_a_job_adds_without_wiping():
"""The opposite of `interference.exempt_families` beside it, which is a
LIST and replaces -- which is why that one carries a restated block and
this one must not."""
plant = {'drawings': {'annotation_covered_by_parent': {
'SR5602': ['P76'], 'SR6999': ['P76']}}}
knowledge._deep_update(plant, {'drawings': {
'annotation_covered_by_parent': {'SR4996': ['SP29', 'SP30']}}})
got = knowledge.attached_parents(plant)
assert sorted(got) == ['SR4996', 'SR5602', 'SR6999']
# ---------------------------------------- bar mark, hardware, or neither
def _grammar(**entries):
return {'part_numbers': {'prefixes': entries}}
def test_a_verified_hardware_prefix_sets_a_token_aside():
know = _grammar(P={'kind': 'hardware', 'verified': True,
'means': 'standard plate'})
assert knowledge.printed_token_population(know, 'P78')[0] == 'hardware'
def test_an_unverified_hardware_claim_is_refused_and_the_token_survives():
"""THE ASYMMETRY THIS KEY RESTS ON. `kind: hardware` REMOVES a token from
5.6's comparison, so a wrong one makes a real printed bar mark stop being
compared -- a silent pass on the exact defect 5.6 exists to find. A
reading is not a ruling, and a reading may not switch a comparison off.
"""
know = _grammar(P={'kind': 'hardware', 'verified': False,
'unverified_because': 'read off a manual'})
population, why = knowledge.printed_token_population(know, 'P78')
assert population == 'unknown'
assert 'REFUSED' in why
assert 'read off a manual' in why
def test_bar_mark_needs_no_verification_because_it_can_only_add_noise():
"""The other direction. Calling something a bar mark can only ever put a
visible finding on the report; it can never remove one."""
know = _grammar(SR={'kind': 'bar_mark', 'verified': False})
assert knowledge.printed_token_population(know, 'SR310')[0] == 'bar_mark'
def test_a_prefix_with_no_kind_leaves_the_token_unplaceable():
know = _grammar(MK={'means': 'erection / site mark', 'verified': False})
population, why = knowledge.printed_token_population(know, 'MK12')
assert population == 'unknown'
assert 'declares no `kind`' in why
assert 'NOT set aside' in why
def test_a_kind_this_code_does_not_know_is_refused_by_name():
know = _grammar(ZZ={'kind': 'widget', 'verified': True})
population, why = knowledge.printed_token_population(know, 'ZZ1')
assert population == 'unknown'
assert 'widget' in why
def test_a_token_matching_no_declared_prefix_is_unplaceable():
know = _grammar(P={'kind': 'hardware', 'verified': True})
assert knowledge.printed_token_population(know, 'QQ9')[0] == 'unknown'
def test_no_grammar_at_all_stands_down_to_ungoverned_not_to_hardware():
"""Standing down here has to mean "do not narrow". The other reading
would empty 5.6's comparison for every plant that has not written a
part-number grammar yet -- silence, arriving through a key nobody set."""
population, why = knowledge.printed_token_population({}, 'P78')
assert population == 'ungoverned'
assert 'nothing was set aside' in why
def test_longest_prefix_wins_so_a_special_plate_is_not_a_plate():
know = _grammar(P={'kind': 'hardware', 'verified': True},
SP={'kind': 'hardware', 'verified': False})
# SP is unverified, so SP29 is refused rather than falling back to P and
# being set aside on the strength of a prefix that is not its own.
population, _why = knowledge.printed_token_population(know, 'SP29')
assert population == 'unknown'
# ============================================================================
# PLANT-MEMBER CHECKLISTS: a board per kind of member, with its own numbers
# ============================================================================
def _board(*pairs):
return {'checklists': {'profiles': {'wp': {
'member_kind': 'wall_panel', 'title': 'Wall panel',
'items': ['{0} = {1}'.format(n, c) for (n, c) in pairs]}}},
'nomenclature': {'member_kinds': {'WP': 'wall_panel'}}}
def test_no_profile_declared_is_the_compatibility_contract():
"""THE CONTRACT, and it is the reason this could ship at all.
A plant that declares nothing gets the full master checklist, in master
order, under master numbers -- byte-identical to what this tool printed
before profiles existed.
"""
got = knowledge.profile_for({}, member_kind='wall_panel')
assert got['profile'] is None
assert got['items'] is None
rows = [(i[0], i[1], i[2], 'OK', None) for i in ticketpage.CHECKLIST_ITEMS]
assert ticketpage.profile_view(rows, got) == rows
text = ticketpage.checklist_table_text(rows)
assert ticketpage.checklist_table_text(
ticketpage.profile_view(rows, got)) == text
def test_a_piece_whose_member_kind_is_unreadable_gets_the_full_checklist():
know = _board(('1', '3.3'))
member = knowledge.member_kind_of(know, 'ZZ9')
assert member['member_kind'] is None
got = knowledge.profile_for(know, member_kind=None)
assert got['profile'] is None
assert 'FULL checklist' in got['why']
def test_the_member_prefix_is_read_the_way_scope_reads_it():
"""One reader, and it is `scope.prefix_of`. The prefix is the WHOLE
leading letter run and the lookup is EXACT -- scope.py argued that out
and re-deciding it here would let the declared scope and the checklist
disagree about the same mark, silently."""
know = _board(('1', '3.3'))
assert knowledge.member_kind_of(know, 'WP117')['member_kind'] == \
'wall_panel'
# `WPX117`'s prefix is WPX, which is not the declared WP.
assert knowledge.member_kind_of(know, 'WPX117')['member_kind'] is None
assert knowledge.member_kind_of(know, '117')['member_kind'] is None
def test_a_profile_renumbers_and_reorders_without_moving_the_master_id():
know = _board(('2.1', '3.3'), ('2.2', '1.1'))
got = knowledge.profile_for(know, member_kind='wall_panel')
assert got['items'] == [('2.1', '3.3'), ('2.2', '1.1')]
rows = [('1.1', 'a', 'A', 'OK', None), ('3.3', 'b', 'B', 'WARN', 'w')]
view = ticketpage.profile_view(rows, got)
# Reordered to the board's order ...
assert [r[0] for r in view] == ['3.3', '1.1']
# ... and the MASTER ID never moved off row[0].
assert [ticketpage.row_display(r) for r in view] == \
[('2.1', '3.3'), ('2.2', '1.1')]
def test_the_page_and_the_file_both_recover_the_master_id():
know = _board(('2.1', '3.3'))
got = knowledge.profile_for(know, member_kind='wall_panel')
view = ticketpage.profile_view([('3.3', 'b', 'B', 'WARN', 'w')], got)
html = ticketpage.checklist_table_html(view)
text = ticketpage.checklist_table_text(view)
assert '2.1' in html and '3.3' in html
# The file is the fallback a reader is told to trust: it has no hover
# text, so the id is always spelled out.
assert '2.1 [3.3]' in text
def test_a_row_the_board_does_not_name_is_asked_anyway_and_said_so():
"""THE HOLE A CLOSED LIST WOULD OPEN. A board names the questions that
existed when somebody wrote it; the code universe grows. The default
must be that the next check anybody builds is still asked."""
know = _board(('1', '3.3'))
got = knowledge.profile_for(know, member_kind='wall_panel')
assert got['unlisted'] == 'ask'
decisions = ticketpage.applicability_decisions(
know, resolver=lambda k, i, panel_type=None, verified=False:
{'not_applicable': False}, profile=got)
assert decisions['3.3']['off_board'] is False
assert decisions['1.1']['off_board'] is True
assert decisions['1.1']['not_applicable'] is False
assert '1.1' in ticketpage.off_board(decisions)
def test_a_closed_board_needs_a_reason_and_a_date_or_it_is_refused():
know = _board(('1', '3.3'))
know['checklists']['profiles']['wp']['unlisted'] = 'not_applicable'
got = knowledge.profile_for(know, member_kind='wall_panel')
assert got['unlisted'] == 'ask'
assert 'REFUSED' in got['problem']
know['checklists']['profiles']['wp'].update({
'unlisted_because': 'this plant asks nothing else of a wall panel',
'unlisted_since': '2026-08-31'})
got = knowledge.profile_for(know, member_kind='wall_panel')
assert got['unlisted'] == 'not_applicable'
decisions = ticketpage.applicability_decisions(
know, resolver=lambda k, i, panel_type=None, verified=False:
{'not_applicable': False}, profile=got)
assert decisions['1.1']['not_applicable'] is True
assert decisions['1.1']['source'] == 'profile'
assert 'does not ask this question' in decisions['1.1']['reason']
def test_two_profiles_claiming_one_member_kind_use_neither():
know = _board(('1', '3.3'))
know['checklists']['profiles']['wp2'] = {
'member_kind': 'wall_panel', 'items': ['1 = 1.1']}
got = knowledge.profile_for(know, member_kind='wall_panel')
assert got['profile'] is None
assert 'cannot have two checklists' in got['problem']
def test_a_panel_type_profile_beats_the_member_kind_one():
know = _board(('1', '3.3'))
know['checklists']['profiles']['wp_ins'] = {
'member_kind': 'wall_panel', 'panel_type': 'insulated',
'items': ['1 = 5.8']}
got = knowledge.profile_for(know, member_kind='wall_panel',
panel_type='insulated')
assert got['profile'] == 'wp_ins'
assert got['basis'] == 'member_kind+panel_type'
# ... and a piece that is only a wall panel still gets the general board.
assert knowledge.profile_for(
know, member_kind='wall_panel')['profile'] == 'wp'
def test_a_board_line_that_is_a_bare_number_is_refused_by_name():
"""THE TRAP THAT COST A ROW. `_coerce` numifies a value, so `5.10`
becomes the float 5.1 -- which is a DIFFERENT master id that really
exists. It is the leading-zero lesson with the zero at the other end,
and it cannot be undone after the fact, so it is refused."""
know = {'checklists': {'profiles': {'wp': {
'member_kind': 'wall_panel',
'items': [{'number': 5.1, 'check': 5.1}]}}}}
got = knowledge.profile_for(know, member_kind='wall_panel')
assert got['items'] == []
assert 'coerced' in got['problem'] or '5.10' in got['problem']
def test_the_equals_form_survives_coercion():
assert knowledge._coerce('5.10') == 5.1 # the trap itself
assert knowledge._coerce('2.1 = 5.10') == '2.1 = 5.10'
know = _board(('2.1', '5.10'))
got = knowledge.profile_for(know, member_kind='wall_panel')
assert got['items'] == [('2.1', '5.10')]
def test_a_board_may_not_put_one_question_on_twice():
know = _board(('1', '3.3'), ('2', '3.3'))
got = knowledge.profile_for(know, member_kind='wall_panel')
assert got['items'] == [('1', '3.3')]
assert 'twice' in got['problem']
# ------------------------------------------- how a human names a check now
def test_a_bare_number_is_a_master_id_and_always_was():
"""Every ruling written before 2026-08-31 cites a bare number, and every
one of them still means exactly what it meant."""
got, why, problem = knowledge.resolve_check_reference(
{}, '3.3', ids=ticketpage.CHECKLIST_IDS)
assert got == '3.3'
assert problem is None
assert 'master id' in why
def test_a_qualified_reference_resolves_through_the_board():
know = _board(('2.1', '3.3'))
got, why, problem = knowledge.resolve_check_reference(
know, 'sample_plant-wall_panel 2.1', plant='sample_plant',
ids=ticketpage.CHECKLIST_IDS)
assert got == '3.3'
assert problem is None
assert '3.3' in why
def test_a_qualified_reference_for_another_plant_is_refused():
know = _board(('2.1', '3.3'))
got, _why, problem = knowledge.resolve_check_reference(
know, 'other_plant-wall_panel 2.1', plant='sample_plant',
ids=ticketpage.CHECKLIST_IDS)
assert got is None
assert 'REFUSED' in problem
def test_an_unplaceable_reference_is_reported_never_dropped():
got, _why, problem = knowledge.resolve_check_reference(
{}, '9.9', ids=ticketpage.CHECKLIST_IDS)
assert got is None and problem
got, _why, problem = knowledge.resolve_check_reference(
_board(('2.1', '3.3')), 'sample_plant-wall_panel 9.9',
plant='sample_plant', ids=ticketpage.CHECKLIST_IDS)
assert got is None
assert 'no such number' in problem
def test_a_mute_is_stored_under_the_master_id_whatever_it_was_written_as():
"""THE PROPERTY THE WHOLE GRAMMAR EXISTS FOR.
A mute keyed by a board POSITION would move the moment the plant
renumbered. Keyed by the master id it follows the QUESTION, so
everything downstream -- the row, the JSON, the report -- speaks about
the same check whatever the reference was typed as.
"""
know = _board(('2.1', '3.3'))
know['checks'] = {'muted': {'sample_plant-wall_panel 2.1': 'quiet please'}}
assert ticketpage.muted_checks(
know, resolver=knowledge.resolve_check_reference,
plant='sample_plant') == {'3.3': 'quiet please'}
def test_a_renumbering_can_refuse_an_old_reference_but_never_re_point_it():
"""THE LIMIT OF THE QUALIFIED FORM, STATED RATHER THAN PAPERED OVER.
A qualified reference is a position on a board, and it is resolved
against the board AS IT IS NOW. If the plant moves that question from
2.1 to 7.4, a mute still written `...-wall_panel 2.1` has two possible
fates and only one of them is acceptable:
* silently resolve to whatever sits at 2.1 today -- a ruling now
quietening a DIFFERENT question, which is the ambush this whole
design exists to prevent;
* refuse, loudly, and be reported as a reference nobody can place.
It refuses. So a renumbering can COST a reference and can never
CORRUPT one -- and the bare master id, which is what every existing
mute uses, cannot even be cost.
"""
know = _board(('2.1', '3.3'))
know['checks'] = {'muted': {'sample_plant-wall_panel 2.1': 'quiet please'}}
know['checklists']['profiles']['wp']['items'] = ['7.4 = 3.3', '2.1 = 1.1']
problems = ticketpage.muted_reference_problems(know, plant='sample_plant')
assert problems == [] # 2.1 still exists -- it just means 1.1 now
# ... and it resolves to 1.1, NOT to 3.3. Which is why a reference like
# this is reported on every run and a bare master id is the durable form.
assert ticketpage.muted_checks(
know, resolver=knowledge.resolve_check_reference,
plant='sample_plant') == {'1.1': 'quiet please'}
# And when the number is gone entirely, it is refused rather than
# dropped or guessed.
know['checklists']['profiles']['wp']['items'] = ['7.4 = 3.3']
problems = ticketpage.muted_reference_problems(know, plant='sample_plant')
assert [w for (w, _p) in problems] == ['sample_plant-wall_panel 2.1']
assert 'no such number' in problems[0][1]
def test_todays_bare_mutes_keep_working_untouched():
know = {'checks': {'muted': {'3.3': 'a', '5.4': 'b', '1.4': 'c'}}}
muted = ticketpage.muted_checks(
know, resolver=knowledge.resolve_check_reference)
assert sorted(muted) == ['1.4', '3.3', '5.4']
assert ticketpage.muted_reference_problems(know) == []
def test_a_mute_nobody_can_place_is_reported_not_discarded():
know = {'checks': {'muted': {'9.9': 'a'}}}
problems = ticketpage.muted_reference_problems(know)
assert [w for (w, _p) in problems] == ['9.9']
# ... and it is still carried, under what was written, so
# `apply_muting` can say it mutes nothing.
assert '9.9' in ticketpage.muted_checks(
know, resolver=knowledge.resolve_check_reference)
def test_qualified_reference_is_spelled_one_way_everywhere():
assert knowledge.qualified_reference('sample_plant', 'wall_panel',
'2.1') == \
'sample_plant-wall_panel 2.1'
assert knowledge.qualified_reference(None, None, '3.3') == '3.3'
# ============================================================================
# CONNECTORS -- 5.8 exist, 5.9 sufficient, 5.10 edge distance
# ============================================================================
#
# Owner, 2026-08-31: "insulated panel 加一个 check,检查 connector 是不是存在,
# 以及够不够" and "再增加 connector edge distance 的 check".
#
# The numbers below are MEASURED off the live model the same day and are the
# calibration these three rows were written against: one insulated piece with
# five rows of pins across, 16.0 in field spacing (which IS the design
# summary's 1'-4" O.C.), tighter 9.5/6.5 pairs at the ends, and an in-plane
# minimum edge distance of 6.0 in on four pins with a median of 23 in.
def test_zero_connectors_on_an_insulated_panel_is_a_fail():
"""NOT A WARN. A sandwich panel whose wythes are not tied together is a
structural condition. Every other zero-shaped finding here is a WARN
because a zero usually means the tool could not see something -- this
one is only ever reached after the recognition gate has confirmed it
can see, so a zero here is a zero in the piece."""
severity, message = ticketpage.connectors_exist_status(0)
assert severity == 'FAIL'
assert 'ties the two concrete wythes' in message
assert ticketpage.state_color(severity) == 'red'
def test_connectors_present_is_a_pass_that_says_how_many():
severity, message = ticketpage.connectors_exist_status(115)
assert severity == 'OK'
assert '115' in message
def test_a_gate_that_excluded_everything_is_named_in_the_failure():
"""Either the piece has untied wythes or the plant's component gate is
wrong. Both need somebody, and a message that named only the first
would send him to the wrong one."""
severity, message = ticketpage.connectors_exist_status(0, gated_out=40)
assert severity == 'FAIL'
assert 'component gate' in message
assert '40' in message
def test_sufficiency_says_nothing_while_the_design_side_is_unreadable():
"""The count alone is a number, not a judgement. WP115's design summary
says `HK ST100'S @1'-4" O.C.` and 8.1 lists it among the lines the
parser cannot interpret -- so there is nothing to compare against, and
printing OK because nobody stated a requirement is the silent pass this
table exists to refuse."""
severity, message = ticketpage.connectors_sufficient_status(115, None)
assert severity is None
assert 'no connector requirement could be resolved' in message
assert '115' in message
def test_end_densification_is_normal_and_is_not_flagged():
"""MEASURED: 16.0 in in the field, with 9.5 and 6.5 in pairs at the
ends. A gap TIGHTER than the design calls for is never the defect, so
the comparison is against the widest gap."""
spacing = ticketpage.connector_spacing([0, 6.5, 16, 32, 48, 57.5, 64])
assert 6.5 in spacing and 16.0 in spacing
severity, message = ticketpage.connectors_sufficient_status(
7, None, spacing_in=spacing, required_spacing_in=16.0,
tolerance_in=0.5, tolerance_why='0.5 in, from x')
assert severity == 'OK'
assert '16' in message
# The allowance is IN the sentence, pass as well as fail: a green row
# that does not say what it allowed cannot be argued with.
assert '0.5 in, from x' in message
def test_a_gap_wider_than_the_design_calls_for_fails():
severity, message = ticketpage.connectors_sufficient_status(
3, None, spacing_in=[16.0, 30.0], required_spacing_in=16.0,
tolerance_in=0.5, tolerance_why='0.5 in, from x')
assert severity == 'FAIL'
assert '30' in message
def test_no_tolerance_means_no_comparison_and_never_a_borrowed_one():
"""The 5.10 lesson one row along: a number borrowed from somewhere else
is not this comparison's, and a spacing verdict reached on a tolerance
nobody supplied would be exactly that. Both the measurement and the
requirement are still named, so the reader can see what was NOT
compared."""
severity, message = ticketpage.connectors_sufficient_status(
3, None, spacing_in=[16.0, 30.0], required_spacing_in=16.0,
tolerance_in=None, tolerance_why='tolerances.connector_spacing_in '
'is unset')
assert severity is None
assert '30' in message and '16' in message
assert 'tolerances.connector_spacing_in is unset' in message
def test_the_requirement_note_rides_on_every_sufficiency_sentence():
""""16 in on centre" and "16 in on centre, because THIS line says so"
are different claims, and only the second can be disagreed with."""
note = 'the design summary line names ST100, declared as connector R82'
for spacing, required, expected in (([16.0], 16.0, 'OK'),
([30.0], 16.0, 'FAIL')):
severity, message = ticketpage.connectors_sufficient_status(
3, None, spacing_in=spacing, required_spacing_in=required,
requirement_note=note, tolerance_in=0.5, tolerance_why='0.5 in')
assert severity == expected
assert note in message
def test_too_few_connectors_fails_against_a_stated_count():
severity, message = ticketpage.connectors_sufficient_status(4, 10)
assert severity == 'FAIL'
assert '4' in message and '10' in message
def test_edge_distance_says_nothing_while_no_minimum_is_published():
"""The owner will supply the datasheets. Until then the row stands down
naming the missing datum -- and it may NEVER borrow another product's
number."""
severity, message = ticketpage.connector_edge_status(
[6.0, 23.0], None, None, mark='R82')
assert severity is None
assert 'not written down anywhere' in message
assert '6.0' in message
def test_edge_distance_names_whether_the_number_is_the_products_own():
"""6.2's lesson, one part along: "6 inches" and "6 inches for this
product" are different claims, and a reader deciding whether to move a
pin needs to know which one he was handed."""
_sev, product = ticketpage.connector_edge_status(
[6.0, 23.0], 5.0, 'product', mark='R82')
assert 'published minimum for R82' in product
_sev, plant = ticketpage.connector_edge_status(
[6.0, 23.0], 5.0, 'plant', mark='R82')
assert 'PLANT-WIDE' in plant
assert "NOT this product's own" in plant
def test_a_connector_inside_the_minimum_is_a_warn_that_names_it():
severity, message = ticketpage.connector_edge_status(
[6.0, 6.0, 23.0], 8.0, 'product', mark='R82')
assert severity == 'WARN'
assert '2 connector(s)' in message
assert '6.0 in' in message
def test_the_measured_six_inches_passes_a_six_inch_minimum():
"""The calibration. The smallest edge distance actually built on the
measured piece is 6.0 in and the owner has raised nothing about it, so
a limit AT 6 must not fire -- while a limit above 6 is a finding to
surface when his datasheet lands, not a reason to set the limit at 6."""
severity, _m = ticketpage.connector_edge_status(
[6.0, 23.0], 6.0, 'product', mark='R82')
assert severity == 'OK'
severity, _m = ticketpage.connector_edge_status(
[6.0, 23.0], 8.0, 'product', mark='R82')
assert severity == 'WARN'
# ------------------------------------------------ the knowledge behind them
def _conn(**over):
know = {
'connectors': {
'marks': {'g1': ['R80', 'R81'], 'g2': ['R82']},
'catalogue_alias': {'R80': 'HK Composites (ST50)',
'R82': 'HK Composites (ST100)',
'R81': None},
'products': {},
'recognition_verified': True,
}}
know['connectors'].update(over)
know['connectors']['products'] = {
'HK Composites (ST50)': {'min_edge_distance_in': None,
'min_edge_distance_unknown_because':
'awaiting the datasheet'},
'HK Composites (ST100)': {'min_edge_distance_in': 8.0},
}
return know
def test_connector_marks_flatten_the_groups_the_owner_named():
assert knowledge.connector_marks(_conn()) == ['R80', 'R81', 'R82']
def test_a_connector_number_resolves_to_its_published_product():
assert knowledge.connector_product(_conn(), 'R82')[0] == \
'HK Composites (ST100)'
assert knowledge.connector_product(_conn(), 'r80')[0] == \
'HK Composites (ST50)'
def test_an_unpaired_connector_number_borrows_nothing():
"""A guessed pairing would give a real connector another product's
capacity and another product's edge distance -- the quiet kind of
wrong."""
key, entry, why = knowledge.connector_product(_conn(), 'R81')
assert key is None and entry is None
assert 'Nothing was borrowed' in why
def test_the_edge_minimum_is_product_first_and_the_fallback_is_named():
value, source, _why = knowledge.connector_edge_minimum(_conn(), 'R82')
assert (value, source) == (8.0, 'product')
# ST50 publishes none. With no plant fallback there is no answer at all.
value, source, why = knowledge.connector_edge_minimum(_conn(), 'R80')
assert value is None and source is None
assert 'awaiting the datasheet' in why
assert 'Nothing was borrowed' in why
# With a plant fallback it is used AND flagged as the weaker claim.
value, source, why = knowledge.connector_edge_minimum(
_conn(min_edge_distance_in=6.0), 'R80')
assert (value, source) == (6.0, 'plant')
assert 'WEAKER claim' in why
def test_an_exact_alias_match_never_swallows_a_longer_number():
know = _conn()
know['connectors']['catalogue_alias'] = {'R8': 'HK Composites (ST50)'}
assert knowledge.connector_product(know, 'R80')[0] is None
def test_the_edge_distance_excludes_the_thickness_axis():
"""THE MEASUREMENT CORRECTION. Every pin sits at exact mid-depth, so a
three-axis minimum reports HALF THE PANEL THICKNESS on every connector
of every piece -- a number that is real, constant, and about nothing.
An edge distance is a distance to an EDGE; the two faces are not edges.
The thickness axis is derived (the local span closest to the panel's own
thickness), not assumed to be an index -- `strand_layout_of`'s idiom,
and for the same reason: a nearly square piece defeats any shortcut.
"""
class _Frame(object):
pass
lo, hi = (0.0, 0.0, 0.0), (20.0, 1.3333, 8.0) # ft: 20 long, 16" thick
# A pin at mid-depth, 6 in from the near long edge.
point = (10.0, 0.6667, 0.5)
got = _edge_of(lo, hi, point, thickness_ft=1.3333)
assert got == 6.0 # not 8.0
def _edge_of(lo, hi, local, thickness_ft):
"""The arithmetic `ticketqc.connector_edges` performs, isolated.
`connector_edges` itself needs a live panel and frame to reach the
concrete extents, so what is pinned here is the DECISION -- which axis
is dropped -- rather than the Revit plumbing around it.
"""
span = [hi[i] - lo[i] for i in (0, 1, 2)]
thick = 0
for axis in (1, 2):
if abs(span[axis] - thickness_ft) < abs(span[thick] - thickness_ft):
thick = axis
gaps = []
for axis in [i for i in (0, 1, 2) if i != thick]:
gaps.append(local[axis] - lo[axis])
gaps.append(hi[axis] - local[axis])
return round(min(gaps) * 12.0, 3)
# ==================== insulation: the solid zones, and which edge governs
#
# Landed 2026-08-31 for master rows 5.11-5.15 and for 5.10's insulation-edge
# semantics. Owner, on the edge: "检测 edge 检测的不是 concrete edge,而是
# insulation edge,所以如果 insulation 有 solid zone 被 cut through 而不只是
# recess 的话,那么 edge 就是被 cut 之后的 edge。如果只是 recess,则按照原来
# 的 edge 来算。"
INSUL = {
'insulation': {
'solid_zone': {'family': 'ZONE_PART', 'family_verified': True},
'legend': {
'swatch_types': {'Region A': 'recess_2in_nf',
'Region B': 'solid_zone'},
'expected_patterns': {'solid_zone': 'ANSI35',
'recess_2in_nf': 'TRIANG'},
'expected_patterns_measured_on': '2026-08-31',
},
'recess': {'region_types': ['Region A']},
},
'drawings': {'insulation_layout': 'Layout View 1'},
}
def test_the_layout_view_is_a_key_and_an_unset_one_says_so():
from vibe import knowledge
assert knowledge.insulation_layout_view(INSUL) == ('Layout View 1', None)
name, why = knowledge.insulation_layout_view({})
assert name is None
assert 'drawings.insulation_layout is unset' in why
assert 'not a finding about this piece' in why
def test_an_unverified_solid_zone_family_is_a_stand_down_not_a_name():
"""A family name that matches nothing counts ZERO zones on every piece,
and zero zones reads as "no lifter is missing one"."""
from vibe import knowledge
know = {'insulation': {'solid_zone': {'family': 'ZONE_PART',
'family_verified': False}}}
family, why = knowledge.solid_zone_rule(know)
assert family is None
assert 'family_verified is false' in why
assert knowledge.solid_zone_rule(INSUL) == ('ZONE_PART', None)
def test_lifter_accessories_accepts_a_bare_string_or_a_list_parent():
"""REVIT issue #26, 6.13: the MC plant's DB_PATCH nests under two LPA
families (a list); QL121/QL062 each nest under one (a bare string).
Both shapes land as a normalised list, so `check_lifter_accessory_in_
assembly` never has to branch on which one a plant wrote."""
from vibe import knowledge
know = {'lifters': {'accessories': {
'QL121': {'parent': 'LIFT_QL589G_8T', 'role': 'cup'},
'DB_PATCH': {'parent': ['LPA4T512_LIFTING PIN ANCHOR',
'LPA8T634_LIFTING PIN ANCHOR'],
'role': 'patch'},
'NO_ROLE_PART': {'parent': 'SOMETHING'},
}}}
table = knowledge.lifter_accessories(know)
assert table['QL121'] == {'role': 'cup', 'parents': ['LIFT_QL589G_8T']}
assert table['DB_PATCH'] == {
'role': 'patch',
'parents': ['LPA4T512_LIFTING PIN ANCHOR',
'LPA8T634_LIFTING PIN ANCHOR']}
assert table['NO_ROLE_PART']['role'] is None
# keyed by the CHILD's own family name, upper-cased
assert 'ql121' not in table
def test_lifter_accessories_is_empty_when_the_plant_names_none():
from vibe import knowledge
assert knowledge.lifter_accessories({}) == {}
assert knowledge.lifter_accessories({'lifters': {}}) == {}
def test_the_legend_beats_the_plant_map_and_the_source_is_returned():
"""The legend is the sheet's own statement of intent and the only
self-updating reference. The plant map is a copy, and says so."""
from vibe import knowledge
pattern, source, why = knowledge.expected_hatch_pattern(
INSUL, 'solid_zone', {'solid_zone': 'ANSI99'})
assert (pattern, source) == ('ANSI99', 'legend')
pattern, source, why = knowledge.expected_hatch_pattern(
INSUL, 'solid_zone')
assert (pattern, source) == ('ANSI35', 'plant')
assert 'can be stale' in why
def test_an_unknown_concept_never_borrows_a_neighbours_pattern():
"""The whole defect these rows exist for is one concept drawn with
another's hatch, so a fallback across concepts would be the bug."""
from vibe import knowledge
pattern, source, why = knowledge.expected_hatch_pattern(INSUL, 'cap_2in')
assert pattern is None and source is None
assert 'No neighbouring concept' in why
def test_the_swatch_map_is_upper_cased_so_a_type_name_matches_either_way():
from vibe import knowledge
got = knowledge.legend_swatch_concepts(INSUL)
assert got['REGION A'] == 'recess_2in_nf'
assert knowledge.legend_swatch_concepts({}) == {}
def test_a_depth_is_read_out_of_a_description_and_absence_is_none():
from vibe import knowledge
assert knowledge.zone_depth_in('3" SOLID ZONE') == 3.0
assert knowledge.zone_depth_in('4 in SOLID ZONE') == 4.0
assert knowledge.zone_depth_in('SOLID ZONE') is None
assert knowledge.zone_depth_in(None) is None
def test_through_cut_is_inferred_and_is_labelled_inferred():
"""It is a reading of two names, not a measurement of two solids, and
every finding built on it has to say so."""
from vibe import knowledge
through, why = knowledge.zone_cuts_through(INSUL, '4" SOLID ZONE', 4.0)
assert through is True
assert 'INFERRED FROM NAMES' in why
assert 'moves the insulation edge' in why
def test_a_partial_zone_leaves_the_original_edge_in_place():
from vibe import knowledge
through, why = knowledge.zone_cuts_through(INSUL, '3" SOLID ZONE', 4.0)
assert through is False
assert 'leaves the original edge in place' in why
def test_an_unreadable_depth_is_none_and_never_defaults_either_way():
"""False would quietly measure to the old edge -- the permissive
direction -- and True would invent an edge that may not exist."""
from vibe import knowledge
through, why = knowledge.zone_cuts_through(INSUL, 'SOLID ZONE', 4.0)
assert through is None
assert 'nothing near it was judged' in why
through, why = knowledge.zone_cuts_through(INSUL, '3" SOLID ZONE', None)
assert through is None
assert 'insulation thickness is not known' in why
def test_st100_carries_the_owners_number_and_st50_still_does_not():
""""pin edge distance = 3". ST100 的知识。写。" -- one product's number,
and borrowing it is what min_edge_distance_unknown_because forbids."""
from vibe import knowledge
cat = knowledge.load_catalogue() if hasattr(knowledge, 'load_catalogue') \
else None
know = {'connectors': {
'catalogue_alias': {'R80': 'HK Composites (ST50)',
'R87': 'HK Composites (ST100)'},
'products': knowledge.get(_catalogue(), 'connectors.products')}}
value, source, why = knowledge.connector_edge_minimum(know, 'R87')
assert value == 3.0
assert source == 'product'
value, source, why = knowledge.connector_edge_minimum(know, 'R80')
assert value is None
assert 'Nothing was borrowed' in why
def _catalogue():
"""The published catalogue as a dict, read the way the tool reads it."""
from vibe import ticketpage
from pyrevit_yaml_fake import strings_only_yaml_reader
from vibe import knowledge
return knowledge._coerce(
strings_only_yaml_reader()(ticketpage.master_path()))
# --- plant_file_in_use: the file an error message should name (issue #14) ---
def test_plant_file_in_use_names_the_per_plant_file_the_tree_was_built_from():
know = {'_plant_path': '/root/sample_plant/knowledge.yaml',
'handling': {}}
assert knowledge.plant_file_in_use(know) == '/root/sample_plant/knowledge.yaml'
def test_plant_file_in_use_falls_back_to_the_machine_wide_file_only_without_a_record():
from vibe import plantpath
assert knowledge.plant_file_in_use({}) == plantpath.plant_knowledge_path()
assert knowledge.plant_file_in_use(None) == plantpath.plant_knowledge_path()
assert knowledge.plant_file_in_use({'_plant_path': None}) == plantpath.plant_knowledge_path()
# --- the job-layer starter must not itself be the alarm shape (2026-09-05) ---
def test_job_starter_declares_capacity_lb_as_an_empty_dict_not_null():
"""A bare `capacity_lb:` parses as null, which is the "a value was deleted"
shape `_note_capacity_override_emptied` refuses on. The starter wrote
exactly that until 2026-09-05, so every job created from it refused
Panel Lift Check and Reinforcing QC on its first run."""
import yaml
data = yaml.safe_load(knowledge.STARTER)
assert data['lifters']['capacity_lb'] == {}
target = {}
knowledge._note_capacity_override_emptied(target, 'job', '/x/knowledge.yaml', data)
assert not any('EMPTY' in str(v) for v in _flatten(target)), target
def _flatten(node):
if isinstance(node, dict):
for v in node.values():
for x in _flatten(v):
yield x
elif isinstance(node, (list, tuple)):
for v in node:
for x in _flatten(v):
yield x
else:
yield node
# ------------------------------------------- plant layer completeness probe
_PLANT_PATH = '/root/sample_plant/knowledge.yaml'
def _full_plant_know(monkeypatch):
"""A merged tree that names everything the buttons need.
`unmapped_roles` is monkeypatched to the healthy answer so the fixture
does not have to restate every PARAMETER_ROLES entry to stay green as
new roles are declared -- the roles section of a real tree is built by
the same merge that fills the handling section.
"""
monkeypatch.setattr(knowledge, 'unmapped_roles',
lambda know, include_optional=False: ([], set()))
return {
'_plant_path': _PLANT_PATH,
'handling': {
'lifter_groups': {
'face': {'families': ['ANCHOR_FACE_4T'], 'at': 'measured'},
'edge_top': {'families': ['ANCHOR_EDGE'], 'at': 'top'},
},
'stages': [
{'name': 'stripping', 'pci_case': 'stripping',
'multiplier': 1.3},
{'name': 'transportation', 'pci_case': 'transportation',
'multiplier': 1.5},
{'name': 'erection', 'pci_case': 'yard', 'multiplier': 1.2},
],
'dunnage': {'mode': 'fraction', 'fraction': 0.2},
},
'lifters': {'capacity_lb': {}},
}
def test_plant_layer_gaps_full_handling_is_silent(monkeypatch):
assert knowledge.plant_layer_gaps(_full_plant_know(monkeypatch)) == []
def test_plant_layer_gaps_empty_handling_names_the_fails_and_the_file():
know = {'_plant_path': _PLANT_PATH, 'handling': {}}
gaps = knowledge.plant_layer_gaps(know)
keys = [g['key'] for g in gaps]
assert 'handling.lifter_groups.face.families' in keys
assert 'handling.lifter_groups.edge_top.families' in keys
assert 'handling.stages' in keys
assert 'handling.dunnage.mode' in keys
assert 'lifters.capacity_lb' in keys
by_key = dict((g['key'], g) for g in gaps)
assert by_key['handling.lifter_groups.face.families']['severity'] == 'FAIL'
assert by_key['handling.stages']['severity'] == 'FAIL'
assert by_key['handling.dunnage.mode']['severity'] == 'WARN'
assert by_key['lifters.capacity_lb']['severity'] == 'WARN'
for gap in gaps:
assert gap['text'].endswith('-- edit ' + _PLANT_PATH)
def test_plant_layer_gaps_a_stage_with_no_multiplier_names_the_stage(monkeypatch):
know = _full_plant_know(monkeypatch)
know['handling']['stages'][1]['multiplier'] = None
gaps = knowledge.plant_layer_gaps(know)
assert len(gaps) == 1
assert gaps[0]['severity'] == 'WARN'
assert 'transportation' in gaps[0]['text']
assert 'handling.stages.transportation.multiplier' in gaps[0]['key']
assert gaps[0]['text'].endswith('-- edit ' + _PLANT_PATH)
# --- the job-layer starter names its plant when one is known (issue #3) ---
# The plant here is a placeholder on purpose, never a real producer.
def test_starter_text_defaults_to_the_starter_exactly():
assert knowledge.starter_text() == knowledge.STARTER
assert knowledge.starter_text(plant=None) == knowledge.STARTER
assert knowledge.starter_text(plant='') == knowledge.STARTER
assert knowledge.starter_text(plant='', plant_basis='something') == knowledge.STARTER
def test_starter_text_names_the_plant_active_and_keeps_the_starter():
import yaml
text = knowledge.starter_text(plant='sample_plant')
data = yaml.safe_load(text)
assert data['job']['plant'] == 'sample_plant'
assert data['lifters']['capacity_lb'] == {}
def test_starter_text_plant_block_sits_before_the_scope_section():
text = knowledge.starter_text(plant='sample_plant')
assert text.index('job:\n plant: sample_plant') < text.index('----- scope')
def test_starter_text_plant_basis_defaults_for_design_extract():
text = knowledge.starter_text(plant='sample_plant')
assert 'named by Design Extract, from the model\'s producer name' in text
custom = knowledge.starter_text(
plant='sample_plant', plant_basis='the picking folder in project config')
assert 'named by the picking folder in project config' in custom
def test_starter_text_with_plant_basis_only_names_plant_when_plant_given():
# A basis without a plant must not sneak a job.plant in -- the default
# basis is tied to a named plant.
assert knowledge.starter_text(plant_basis='something') == knowledge.STARTER
def test_plant_layer_gaps_decides_multiplier_by_type_not_by_float_of_none():
"""Issue #17: float(None) is a NullReferenceException under IronPython,
surfacing as SystemError. The multiplier check must decide by type: None
and junk strings are gaps, numbers and numeric strings are not."""
know = {'_plant_path': '/x/sample_plant/knowledge.yaml',
'handling': {'lifter_groups': {'face': {'families': ['A']},
'edge_top': {'families': ['B']}},
'stages': [{'name': 'stripping', 'multiplier': None},
{'name': 'yard', 'multiplier': '1.5'},
{'name': 'erection', 'multiplier': 1.2},
{'name': 'odd', 'multiplier': 'x'}],
'dunnage': {'mode': 'fraction'}},
'lifters': {'capacity_lb': {}}}
gaps = knowledge.plant_layer_gaps(know)
mult_gaps = sorted(g['key'] for g in gaps if 'multiplier' in g['key'])
assert mult_gaps == ['handling.stages.odd.multiplier',
'handling.stages.stripping.multiplier']
import inspect
src = inspect.getsource(knowledge.plant_layer_gaps)
assert "float(stage.get('multiplier'))" not in src
# ------------------------------------------------------- _is_face_lifter LPA
# fallback (issue #15, owner ruling 2026-09-05): the plant's whitelist
# (`handling.lifter_groups.face.families`) is enumerated by hand and misses
# real LPA parts (LPA8T10 tonight). `_is_face_lifter` must still classify
# an LPA-prefixed family as face when the whitelist does not name it --
# the same fallback `ticketchecks._is_face_stripping_lifter` already used.
def test_is_face_lifter_true_for_lpa_family_missing_from_the_whitelist():
know = {'handling': {'lifter_groups':
{'face': {'families': ['LPA4T512']}}}}
assert knowledge._is_face_lifter(know, 'LPA8T10_LIFTING PIN ANCHOR')
def test_is_face_lifter_false_for_non_lpa_family_not_in_the_whitelist():
know = {'handling': {'lifter_groups':
{'face': {'families': ['LPA4T512']}}}}
assert not knowledge._is_face_lifter(know, 'XYZ_ANCHOR')
def _lpa8t10_knowledge():
"""A knowledge dict built from DEFAULTS plus literals for the back-cast
shear test below -- no plant file is read (PROJECT.md SS3.4)."""
know = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._deep_update(know, {
'handling': {'lifter_groups': {'face': {'families': ['LPA4T512']}}},
'lifting_anchors': {
'back_cast_shear_equals_tension': True,
'back_cast_shear_equals_tension_applies_to': ['LPA8T10'],
'sling_angle_choice': 60,
'parts': {
'LPA8T10': {
'tension_60_lb': 13790,
'shear_lb': 0,
},
},
},
})
return know
def test_lifter_capacity_shear_for_lpa8t10_reaches_the_back_cast_footnote():
know = _lpa8t10_knowledge()
value, basis, why = knowledge.lifter_capacity(
know, 'LPA8T10_LIFTING PIN ANCHOR', kind='shear')
assert value == 13790
assert basis == 'plant_table'
assert 'cast in the back of the panel' in why
assert 'back_cast_shear_equals_tension' in why
def test_lifter_capacity_shear_none_for_a_non_lpa_family_not_in_whitelist():
know = _lpa8t10_knowledge()
# Same chart, but under a family name that is neither LPA-prefixed nor
# in the whitelist -- the fallback must not fire, so shear stays
# unresolved (no rating, NOT JUDGED), exactly as before this fix.
know['lifting_anchors']['parts']['XYZ_ANCHOR'] = (
know['lifting_anchors']['parts'].pop('LPA8T10'))
know['lifting_anchors']['back_cast_shear_equals_tension_applies_to'] = (
['XYZ_ANCHOR'])
value, basis, why = knowledge.lifter_capacity(
know, 'XYZ_ANCHOR', kind='shear')
assert value is None
# --------------------------------------------------------------------------
# issue #15: P78 (alias QL184G) must answer from the insulated-panel anchor
# capacity table, not LIFT-UNKNOWN. The catalogue inserts entry for QL184G
# publishes only an ultimate mechanical tension rating -- no SWL shear/tension
# columns -- while catalogue.insulated_panel_anchor_capacities.rows carries
# the 4:1 SWL shear and the reinforced (with-bar) tension per panel group,
# so the two QL184G rows drive the answer: the LOWEST of each column, because
# the catalogue marks its own panel-group caption semantics uncertain.
#
# These build the knowledge tree by hand per PROJECT.md SS3.4 -- the same
# shape the real plant layer / shipped catalogue.yaml feed `lifter_capacity`.
def _panel_anchor_knowledge(entry=None, rows=None):
"""A knowledge dict with a P78 -> QL184G alias, a spinner of QL184G in
catalogue.inserts that carries no SWL columns (only an ultimate rating),
and an insulated_panel_anchor_capacities table."""
if entry is None:
entry = {'manufacturer': 'ALP', 'ton': '8T',
'ultimate_mechanical_tension_lb': 64000}
if rows is None:
rows = [
{'panel_group': '8" insulated panel (2" x 2" x 4")',
'part_number': 'QL184SPG',
'shear_4_1_lb': 4600,
'reinforced_tension_4_1_lb': 16000,
'rebar_size': '#5'},
{'panel_group': '8" insulated panel (3" x 2" x 3")',
'part_number': 'QL184G',
'shear_4_1_lb': 3850,
'reinforced_tension_4_1_lb': 16000,
'rebar_size': '#5'},
{'panel_group': '8" insulated panel (4" x 2" x 2")',
'part_number': 'QL184G',
'shear_4_1_lb': 4600,
'reinforced_tension_4_1_lb': 16000,
'rebar_size': '#5'},
]
know = knowledge._deep_copy(knowledge.DEFAULTS)
knowledge._deep_update(know, {
'lifters': {'catalogue_alias': {'P78': 'QL184G'}},
'catalogue': {
'inserts': {'QL184G': entry},
'insulated_panel_anchor_capacities': {
'_basis_fc_psi': 4500,
'panel_group_triple_semantics_uncertain': True,
'rows': rows,
},
},
})
return know
def test_issue15_p78_shear_answers_from_the_insulated_panel_anchor_table():
"""P78 -> QL184G: the inserts entry has no shear_lb, so the answer is the
LOWEST shear_4_1_lb across the part's rows in
catalogue.insulated_panel_anchor_capacities -- the QL184SPG-only row must
be ignored, and the two QL184G rows (3850 and 4600) give 3850."""
know = _panel_anchor_knowledge()
value, basis, why = knowledge.lifter_capacity(
know, 'SAMPLE_PLANT_EMBED_P78', kind='shear', fc_psi=4500)
assert value == 3850, why
assert basis == 'catalogue'
assert 'lowest' in why.lower()
assert '8" insulated panel (3" x 2" x 3")' in why
assert '8" insulated panel (4" x 2" x 2")' in why
# The QL184SPG-only group must be ignored: it is not among the part rows.
assert '8" insulated panel (2" x 2" x 4")' not in why
def test_issue15_p78_tension_with_bar_when_the_bar_is_corroborated():
"""The tension column is a WITH-bar rating: when the plant claims the bar
(parts_shipped_with_tension_bar names P78) and assembly_families
corroborates it (a tension_bar_families name is among the other members),
the LOWEST reinforced_tension_4_1_lb -- 16000 -- answers with a with_bar
basis."""
know = _panel_anchor_knowledge()
knowledge._deep_update(know, {
'lifters': {
'parts_shipped_with_tension_bar': ['P78'],
'tension_bar_families': ['SR5602'],
}})
value, basis, why = knowledge.lifter_capacity(
know, 'SAMPLE_PLANT_EMBED_P78', assembly_families=['SR5602', 'FACE_EMBED'])
assert value == 16000, why
assert basis == 'with_bar'
assert 'SR5602' in why
def test_issue15_p78_tension_stands_down_when_the_bar_is_not_corroborated():
"""Same claim, but the bar is not seen among the assembly's other members:
the table publishes only the reinforced rating (no without-bar number), so
the answer stands down as None naming the missing bar -- never a guessed
number."""
know = _panel_anchor_knowledge()
knowledge._deep_update(know, {
'lifters': {
'parts_shipped_with_tension_bar': ['P78'],
'tension_bar_families': ['SR5602'],
}})
value, basis, why = knowledge.lifter_capacity(
know, 'SAMPLE_PLANT_EMBED_P78', assembly_families=['FACE_EMBED'])
assert value is None
assert basis is None
assert 'SR5602' in why
assert 'with' in why.lower() or 'bar' in why.lower()
def test_issue15_family_with_no_alias_keeps_the_old_stand_down():
"""A family the alias layer does not name must return the old
(None, None, why) unchanged -- the table is only reached through a
designation."""
know = _panel_anchor_knowledge()
value, basis, why = knowledge.lifter_capacity(
know, 'SAMPLE_PLANT_EMBED_Q99', kind='shear')
assert value is None and basis is None
assert 'no catalogue alias' in why
def test_issue15_inserts_with_a_shear_lb_is_still_answered_from_inserts():
"""An inserts entry that DOES carry shear_lb must be answered from inserts
(with the fc scaling), never from the insulated-panel table."""
know = _panel_anchor_knowledge(
entry={'manufacturer': 'ALP', 'ton': '8T',
'working_load_lb': 9999, 'shear_load_lb': 9999})
value, basis, why = knowledge.lifter_capacity(
know, 'SAMPLE_PLANT_EMBED_P78', kind='shear', fc_psi=4500)
assert value == 9999, why
assert basis == 'catalogue'
assert 'rated 9999' in why
# ------------------------------------------------------------ level 3: titleblock families
#
# detect_plant's third level: when the producer-name level finds nothing, the
# names of the model's titleblock families are matched against each plant's
# plant.titleblock_patterns. Pure dict/string logic -- no doc.
_MC_NAMES = ['TB_ERECTION_MC_30-42', 'TB_TICKET_MC_24-36']
_SAMPLEK_NAMES = ['KP-24X36-TITLE BOARDER', 'SAMPLEK-HC TICKET TB']
def test_titleblock_patterns_resolve_sample_m_from_its_family_names():
plant, why = knowledge.match_titleblocks(
_MC_NAMES,
{'sample_m': ['*_MC_*'], 'sample_k': ['KP-*', 'SAMPLEK-*']})
assert plant == 'sample_m'
assert 'TB_ERECTION_MC_30-42' in why
assert '*_MC_*' in why
def test_titleblock_patterns_resolve_sample_k_from_its_family_names():
plant, why = knowledge.match_titleblocks(
_SAMPLEK_NAMES,
{'sample_m': ['*_MC_*'], 'sample_k': ['KP-*', 'SAMPLEK-*']})
assert plant == 'sample_k'
assert 'KP-24X36-TITLE BOARDER' in why
def test_titleblock_patterns_match_case_insensitively_on_both_sides():
plant, why = knowledge.match_titleblocks(
['tb_erection_mc_30-42'],
{'sample_m': ['*_mc_*'], 'sample_k': ['KP-*']})
assert plant == 'sample_m'
assert 'tb_erection_mc_30-42' in why
def test_titleblock_patterns_decline_when_families_match_several_plants():
plant, why = knowledge.match_titleblocks(
_MC_NAMES + ['KP-24X36-TITLE BOARDER'],
{'sample_m': ['*_MC_*'], 'sample_k': ['KP-*']})
assert plant is None
assert 'several' in why
def test_titleblock_patterns_with_no_families_is_its_own_sentence():
plant, why = knowledge.match_titleblocks(
[], {'sample_m': ['*_MC_*']})
assert plant is None
assert 'no titleblocks' in why
def test_titleblock_patterns_with_no_hits_names_the_families_seen():
plant, why = knowledge.match_titleblocks(
_MC_NAMES, {'sample_k': ['KP-*', 'SAMPLEK-*']})
assert plant is None
assert '2 families seen' in why
def test_detect_plant_level_3_resolves_after_level_2_finds_nothing(
monkeypatch):
"""Level 3 is the last resort, only consulted when the producer name
matched nothing. Its why is appended to level 2's so the basis says the
producer name was absent and the titleblock habit decided instead."""
from vibe import plantpath
plant = {'sample_m': ['*_MC_*'], 'sample_k': ['KP-*']}
monkeypatch.setattr(knowledge, 'producer_name', lambda doc: None)
monkeypatch.setattr(knowledge, 'plant_aliases',
lambda p: {'sample_m': ['sample_m'],
'sample_k': ['sample_k']}.get(p, [p]))
monkeypatch.setattr(knowledge, 'titleblock_family_names',
lambda doc: _MC_NAMES)
monkeypatch.setattr(plantpath, 'plant_ids',
lambda: ['sample_k', 'sample_m'])
monkeypatch.setattr(knowledge, 'plant_titleblock_patterns',
lambda p: plant.get(p, []))
detected, why = knowledge.detect_plant(None)
assert detected == 'sample_m'
assert 'producer' in why
assert '--' in why
assert 'TB_ERECTION_MC_30-42' in why
def test_detect_plant_level_2_wins_without_consulting_level_3(monkeypatch):
"""A producer name that resolves must never fall through to titleblocks --
level 3 is for models that name no producer. titleblock_family_names
raising proves it is not reached."""
from vibe import plantpath
monkeypatch.setattr(knowledge, 'producer_name',
lambda doc: 'SampleM Precast')
monkeypatch.setattr(knowledge, 'plant_aliases',
lambda p: {'sample_m': ['samplem'],
'sample_k': ['samplek']}.get(p, [p]))
monkeypatch.setattr(plantpath, 'plant_ids',
lambda: ['sample_k', 'sample_m'])
monkeypatch.setattr(knowledge, 'titleblock_family_names',
lambda doc: (_ for _ in ()).throw(AssertionError()))
detected, why = knowledge.detect_plant(None)
assert detected == 'sample_m'
assert 'producer' in why
assert 'titleblock' not in why
# --- plant_choice names a missing plant folder (bridge#2 note, 2026-09-07) ---
def test_plant_choice_reports_a_named_plant_whose_folder_is_missing(
fake_designspec, yaml_on_host):
"""The form opening the window could not tell 'no layer names a plant'
from 'a plant was chosen and its folder has since gone'. `missing` is the
path that should exist and does not; None when the file is there or no
plant is named."""
_write(fake_designspec._job_dir / 'knowledge.yaml',
u'job:\n plant: ghost\n')
choice = knowledge.plant_choice(None)
assert choice['plant'] == 'ghost'
assert choice['missing'] and choice['missing'].endswith('knowledge.yaml')
assert 'ghost' in choice['missing']
plant_dir = fake_designspec._plant_root / 'ghost'
plant_dir.mkdir()
_write(plant_dir / 'knowledge.yaml', u'nomenclature: {}\n')
assert knowledge.plant_choice(None)['missing'] is None
_write(fake_designspec._job_dir / 'knowledge.yaml', u'scope: {}\n')
assert knowledge.plant_choice(None)['missing'] is None