WillTools: precast shop-ticket QC and panel handling tools
ci / test (push) Has been cancelled

Public demo snapshot of an in-house pyRevit extension for precast concrete
shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel
handling solver (PCI Chapter 8).

- No client, job, plant or product data: knowledge layers ship as
  structural templates only.
- Ticket QC's model-read escalation targets a LAN model server endpoint
  (configurable), not a cloud provider.
- Built from the private development repo via tools/make_dist.py.
This commit is contained in:
2026-09-03 16:33:47 -05:00
commit 52d94303bc
281 changed files with 155656 additions and 0 deletions
@@ -0,0 +1,206 @@
# -*- coding: utf-8 -*-
"""`tools/sweep.py` refuses job files measured from a different model.
THE INCIDENT, 2026-09-01
------------------------
A whole sweep -- 184 job files -- carried `source.model` naming one model,
while the document open in Revit was a detached copy that had since DIVERGED
from it: a notch measured 3.5 in in one and 2.5 in in the other. Nothing
looked wrong. Every report rendered, every weight reconciled, and it
reconciled because a job file is the complete input to the check: the numbers
agree with the file they came from whether or not that file is about the piece
in front of you. The two were told apart by hand, and the field that would
have answered it in one line was sitting in every job file unread.
So the guard is a REFUSAL with both model names in it, not a warning line, and
"could not ask Revit" is a third answer that is printed and never mistaken for
agreement.
`tools/sweep.py` is plain host CPython -- it drives Revit through `rvt.py` as
a subprocess and imports nothing from the API -- so unlike `sweep_in_revit.py`
it can simply be loaded by path and called.
"""
import importlib.util
import sys
import pytest
from pathlib import Path
REPO = Path(__file__).resolve().parents[2]
SWEEP_PY = REPO / 'tools' / 'sweep.py'
def _load():
spec = importlib.util.spec_from_file_location('_sweep_under_test',
str(SWEEP_PY))
module = importlib.util.module_from_spec(spec)
sys.modules[spec.name] = module
spec.loader.exec_module(module)
return module
sweep = _load()
def job(model, mark='PNL101'):
return (mark, {'panel': {'mark': mark},
'source': ({} if model is None else {'model': model})})
# ----------------------------------------------------------- the comparison
@pytest.mark.parametrize('name,stem', [
('sample_job.rvt', 'sample_job'),
('sample_job', 'sample_job'),
('SAMPLE_JOB.RVT', 'sample_job'),
(r'C:\models\sample_job.rvt', 'sample_job'),
(' sample_job.rvt ', 'sample_job'),
])
def test_the_extension_and_the_path_are_not_part_of_the_name(name, stem):
"""`source.model` is a basename with `.rvt` on it and `doc.Title` is not,
so a literal comparison would call every job file a mismatch."""
assert sweep.model_stem(name) == stem
@pytest.mark.parametrize('empty', [None, '', ' ', '.rvt'])
def test_a_nameless_model_is_none_and_never_a_match(empty):
assert sweep.model_stem(empty) is None
# ------------------------------------------------------------- the mismatch
def test_a_job_from_another_model_is_a_mismatch():
mismatched, unstamped, reason = sweep.check_source_model(
[job('sample_job.rvt')], 'sample_job_detached')
assert reason is None and unstamped == []
assert mismatched == [('PNL101', 'sample_job.rvt')]
def test_the_same_model_is_not_a_mismatch():
mismatched, unstamped, reason = sweep.check_source_model(
[job('sample_job.rvt')], 'sample_job')
assert (mismatched, unstamped, reason) == ([], [], None)
def test_the_refusal_names_both_models():
"""The one thing the message must do. A refusal that says 'wrong model'
without saying which two leaves the reader exactly where they were."""
mismatched, _unstamped, _reason = sweep.check_source_model(
[job('sample_job.rvt'), job('sample_job.rvt', 'PNL102')],
'sample_job_detached')
message = sweep.model_refusal(mismatched, 'sample_job_detached', 2)
assert 'sample_job.rvt' in message
assert 'sample_job_detached' in message
assert '2 of 2' in message
assert 'PNL101' in message
assert 'Nothing was checked' in message
assert '--any-model' in message
def test_a_job_with_no_source_model_is_unjudged_not_cleared():
""""Written before the field existed" is not "measured from this model".
It comes back in its own list so the caller can say so."""
mismatched, unstamped, reason = sweep.check_source_model(
[job(None)], 'sample_job')
assert mismatched == [] and reason is None
assert unstamped == ['PNL101']
def test_no_open_document_is_could_not_judge_and_says_why():
"""The empty `mismatched` here means DID NOT ASK. Reading it as agreement
is the whole failure this guard exists for, one level up."""
mismatched, unstamped, reason = sweep.check_source_model(
[job('sample_job.rvt')], None)
assert mismatched == []
assert unstamped == ['PNL101']
assert reason and 'no open document' in reason
# ------------------------------------------------ the refusal actually fires
class _Recorder(object):
"""A sweep folder on disk is a lot of scaffolding for one question, so
`check()` is exercised against a jobs dir and stopped before it ever
reaches panellift -- the refusal is meant to happen first."""
def __init__(self):
self.ran = 0
def _sweep_folder(tmp_path, model):
jobs = tmp_path / 'jobs'
jobs.mkdir()
(tmp_path / 'meta').mkdir()
(jobs / 'PNL101.job.json').write_text(
'{"panel": {"mark": "PNL101"}, "source": {"model": "%s"}}' % model,
encoding='utf-8')
(tmp_path / 'meta' / 'PNL101.json').write_text(
'{"mark": "PNL101", "status": "exported"}', encoding='utf-8')
return tmp_path
def test_check_refuses_before_it_computes_anything(tmp_path, monkeypatch):
root = _sweep_folder(tmp_path, 'sample_job.rvt')
seen = _Recorder()
def explode(*args, **kwargs):
seen.ran += 1
raise AssertionError('panellift must not run on a mismatched sweep')
monkeypatch.setattr(sweep.subprocess, 'run', explode)
with pytest.raises(SystemExit) as excinfo:
sweep.check(str(root), 'sample_job_detached')
assert 'sample_job.rvt' in str(excinfo.value)
assert 'sample_job_detached' in str(excinfo.value)
assert seen.ran == 0
def test_any_model_proceeds_and_carries_the_caveat_into_the_report(tmp_path,
monkeypatch):
"""The override is not a way to make the sentence go away: it goes to the
top of problems.txt, where `render` puts it."""
root = _sweep_folder(tmp_path, 'sample_job.rvt')
class _Result(object):
stdout = '{"verdict": "OK", "stages": []}'
stderr = ''
monkeypatch.setattr(sweep.subprocess, 'run',
lambda *a, **k: _Result())
rows, notes = sweep.check(str(root), 'sample_job_detached',
any_model=True)
assert len(rows) == 1
assert any('--any-model' in note for note in notes)
assert notes[0].startswith('!!')
assert notes[0] in sweep.render(rows, [], notes)
def test_an_unreachable_revit_says_the_model_was_not_verified(tmp_path,
monkeypatch):
"""It does not refuse -- there is nothing to refuse against -- and it does
not stay quiet either."""
root = _sweep_folder(tmp_path, 'sample_job.rvt')
class _Result(object):
stdout = '{"verdict": "OK", "stages": []}'
stderr = ''
monkeypatch.setattr(sweep.subprocess, 'run',
lambda *a, **k: _Result())
_rows, notes = sweep.check(str(root), None)
assert any('NOT verified' in note for note in notes)
def test_a_matching_model_adds_no_caveat_at_all(tmp_path, monkeypatch):
"""The ordinary run is unchanged, down to the absence of a banner."""
root = _sweep_folder(tmp_path, 'sample_job.rvt')
class _Result(object):
stdout = '{"verdict": "OK", "stages": []}'
stderr = ''
monkeypatch.setattr(sweep.subprocess, 'run',
lambda *a, **k: _Result())
_rows, notes = sweep.check(str(root), 'sample_job')
assert notes == []