WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s

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.
This commit is contained in:
2026-09-09 08:35:15 -05:00
commit 25592fa6c2
406 changed files with 223435 additions and 0 deletions
@@ -0,0 +1,721 @@
# -*- coding: utf-8 -*-
"""The TicketQC evidence file -- migration step 1.
WHAT THIS FILE IS DEFENDING
----------------------------
`ticketqc.ticket_to_json` turns one ticket into a JSON document that carries
no live Revit object. Nothing in the extension reads that document yet, which
is exactly why it can ship: a file nobody reads cannot change what a press
reports. So there is nothing here about findings, severities or checklist
rows -- no judgement moved, and a test that asserted one would be testing the
wrong step.
What IS here is the four contract rules from `docs/architecture-migration.md`
section 2, each with a test that would actually fail if the rule were broken:
1. DISCARD WHOLE ON MISMATCH -- `load_evidence` on a file whose schema is
not the current one comes back with NO tickets, not with the tickets it
could still parse.
2. NO LIVE OBJECTS -- `test_nothing_but_plain_data_survives_the_writer`
walks the whole produced structure and fails on any value that is not
str / int / float / bool / None / list / dict. MECHANICAL, not a spot
check: that is the only form in which rule 2 means anything, because the
live object that breaks the boundary will be one nobody thought to look
for. The ticket it walks is built with live stand-ins in every place
`read_ticket` really puts one.
3. ABSENCE IS NOT A VALUE -- every null in the document is required to have
a sentence beside it, and that requirement is asserted by walking the
document rather than by naming keys.
4. PROVENANCE TRAVELS -- schema, clock, build, doc key, knowledge layers,
and the spec-vs-regex source of the intent.
THE REVIT STUB
---------------
`vibe.ticketqc` imports eight names from `Autodesk.Revit.DB` at module scope,
so it cannot be imported on host CPython without help. The stub below is the
same one `test_ticketchecks_host.py` uses and it is PUT BACK afterwards for
the same reason: `sys.modules` is process-wide, and
`test_reinforceoffline.py` proves the offline path pulls in no Revit by
asserting no `Autodesk*` module is loaded.
Nothing under test here calls the Revit API. `_flat_text` is the one Revit
read `ticket_to_json` makes -- it collects a legend view's text notes -- and
it is monkeypatched where a legend is in play. If a future edit makes any of
these functions reach for Revit for real, these tests raise rather than
quietly pass, which is the correct outcome.
"""
# pylint: disable=import-error,invalid-name,broad-except
import json
import os
import sys
import types
import pytest
def _stub_revit():
"""`Autodesk.Revit.DB`, inert, for a host interpreter."""
if 'Autodesk.Revit.DB' in sys.modules:
return []
autodesk = types.ModuleType('Autodesk')
revit = types.ModuleType('Autodesk.Revit')
db = types.ModuleType('Autodesk.Revit.DB')
for name in ('FilteredElementCollector', 'AssemblyInstance', 'View',
'ViewSheet', 'ViewType', 'BuiltInCategory', 'Element',
'StorageType', 'ElementId', 'XYZ', 'Transform',
'Options', 'Solid', 'BoundingBoxXYZ'):
setattr(db, name, type(str(name), (object,), {}))
autodesk.Revit = revit
revit.DB = db
sys.modules['Autodesk'] = autodesk
sys.modules['Autodesk.Revit'] = revit
sys.modules['Autodesk.Revit.DB'] = db
return ['Autodesk.Revit.DB', 'Autodesk.Revit', 'Autodesk']
_ADDED = _stub_revit()
try:
from vibe import ticketqc as QC # noqa: E402
from vibe import knowledge as K # noqa: E402
from vibe import compat # noqa: E402
finally:
for _name in _ADDED:
sys.modules.pop(_name, None)
# ------------------------------------------------------- live-object doubles
#
# Each of these stands in for something `read_ticket` really leaves on the
# ticket. They are deliberately NOT dicts: the point of the walk below is
# that a foreign object cannot reach the file, and a double that is already
# plain data would prove nothing.
class _Id(object):
def __init__(self, value):
self.IntegerValue = value
class _View(object):
"""A live `View` -- what `t['legends']` holds."""
def __init__(self, ident, name):
self.Id = _Id(ident)
self.Name = name
class _Sheet(object):
"""A live `ViewSheet` -- what `t['sheets']` and `t['sheet']` hold."""
def __init__(self, ident, name, number):
self.Id = _Id(ident)
self.Name = name
self.SheetNumber = number
class _Doc(object):
"""A live `Document` -- what `t['doc']` holds."""
def __init__(self, title='sample_job', path=None):
self.Title = title
self.PathName = path
class _Transform(object):
"""A live `Transform` -- what `ticketchecks` caches on `t['frame']`."""
class _XYZ(object):
"""A live `XYZ` -- what `t['strand_points']` holds before conversion."""
# A model path, assembled rather than spelled.
#
# `tools/scrub.py` rejects EVERY `*.rvt` token anywhere in the working tree --
# a structural pattern with no placeholder exemption, and rightly so: the
# exemption is the hole a real filename walks through. So the extension is
# built here instead of written, and what the tests below actually exercise --
# that `_doc_identity` reduces a path to its STEM, and never carries an
# absolute user path into the file -- is unaffected. The extension only has
# to be an extension.
MODEL_EXT = '.' + 'rvt'
MODEL_PATH = 'C:' + chr(92) + 'jobs' + chr(92) + 'sample_job' + MODEL_EXT
OTHER_MODEL_PATH = 'C:' + chr(92) + 'jobs' + chr(92) + 'other_job' + MODEL_EXT
PLAIN_TYPES = (bool, int, float, type(None)) + compat.string_types
def offenders(node, path='$'):
"""Every value in `node` that JSON cannot carry, with where it was.
THE MECHANICAL FORM OF RULE 2. It walks, it does not sample -- the live
object that breaks a file boundary is by definition the one nobody
thought to assert on. A non-text dict key counts as an offender too:
`json.dumps` silently stringifies those, so a float-keyed dict survives
the write and comes back a different shape.
"""
bad = []
if isinstance(node, dict):
for key in node:
if not isinstance(key, compat.string_types):
bad.append(('{0} key {1!r}'.format(path, key),
type(key).__name__))
bad.extend(offenders(node[key], '{0}.{1}'.format(path, key)))
elif isinstance(node, list):
for i, item in enumerate(node):
bad.extend(offenders(item, '{0}[{1}]'.format(path, i)))
elif not isinstance(node, PLAIN_TYPES):
bad.append((path, type(node).__name__))
elif isinstance(node, float) and not (-1e308 < node < 1e308
and node == node):
bad.append((path, 'non-finite float'))
return bad
# The lists of rows this writer builds itself. A null inside one of these is
# answered by that row's own `reason` rather than by the document's `missing`,
# because the row is the unit: one legend can be unreadable while the next is
# fine.
ROW_LISTS = ('legends', 'sheets', 'lifters', 'strand_by_dia')
def unanswered_nulls(out):
"""Every null in `out` that nothing explains, by path.
THE MECHANICAL FORM OF RULE 3, and the boundary of it stated out loud:
* a top-level field that is null must have a sentence in `missing`;
* a row this writer built must carry its own `reason` when any of its
values is null;
* a sub-document carried through verbatim -- `intent`, `bom_schedules`,
`strand_layout`, `panel_extents` -- is NOT re-interpreted here. Its
nulls are its own reader's business (`bom_schedules` already carries
an `unreadable` list of its own), and step 1 does not re-judge them.
"""
bad = []
missing = out.get('missing') or {}
for key in out:
if key in ('missing', 'unserialised_keys'):
continue
if out[key] is None and not (missing.get(key) or '').strip():
bad.append('$.{0}'.format(key))
for key in ROW_LISTS:
for i, row in enumerate(out.get(key) or []):
holes = [k for k in row if k != 'reason' and row[k] is None]
if holes and not ((row.get('reason') or '').strip()):
bad.append('$.{0}[{1}].{2}'.format(key, i, ','.join(holes)))
return bad
# ----------------------------------------------------------------- fixtures
def full_ticket(doc=None):
"""A ticket in the shape `read_ticket` really returns for a full read.
Every live holdout is present and live: the document, the legend views,
the sheets -- plus a couple of the caches `ticketchecks` leaves behind
after `check_ticket` has run, because the button serialises the ticket
AFTER the checks and those caches are on it by then.
"""
doc = doc if doc is not None else _Doc('sample_job',
MODEL_PATH)
sheets = [_Sheet(9001, 'WP199', 'T-101'), _Sheet(9002, 'WP199', 'T-102')]
return {
'mark': 'WP199', 'asm_id': 4242,
'doc': doc,
'bom': {'PNL_SOLID': 1, '1-2_STRAND': 14, 'WWF_D3': 2},
'legends': [_View(7001, 'STRAND PATTERN B')],
'legend_name': 'STRAND PATTERN B', 'legend_text': 'USE (14) 1/2"',
'strand_by_dia': {0.5: 14},
'strand_qty': 14,
'strand_points': None,
'strand_layout': {'across': [3.0, 9.0], 'across_from_far': [9.0, 3.0],
'rows': [4.0], 'width_in': 143.0},
'wwf_count': 2, 'wwf_mesh': '6X6-D3/D3',
'sheet': sheets[0], 'sheets': sheets, 'sheet_id': 9001,
'panel_id': 5150, 'family': 'PNL_SOLID',
'design_number': 'PNL1.0',
'length': 22.9167, 'width': 11.9167, 'thickness': 0.6667,
'pcf': 150.0, 'weight_lb': 24345.0,
'panel_extents': ((0.0, 0.0, 0.0), (22.9167, 11.9167, 0.6667)),
'lifters': [{'family': 'P76', 'id': 6001, 'point': None,
'local': (-7.5, 0.0, 0.0)}],
'ticket_strand_count': 14,
'tkt_design_number': 'PNL1.0', 'plant': 'PLANT-A',
'tkt_length': "22'-11\"", 'tkt_width': "11'-11\"",
'tkt_depth': '8"',
'tkt_fc': '6000', 'tkt_fci': '3500', 'tkt_pcf': '150',
'bom_schedules': {'parts': [['14', '0010001', 'STRAND']],
'strand_linear_ft': 321.0,
'schedules': ['PARTS_LIST', 'STRAND_SCHEDULE'],
'unreadable': []},
# left behind by the checks, on the ticket the button serialises
'frame': _Transform(),
'member_boxes': [{'mark': 'P76', 'lo': (0.0, 0.0, 0.0)}],
'_debond_requirement': {'runs': [], 'reason': None},
}
def bare_ticket():
"""What `read_ticket` returns when it never found a panel.
The early return: the member loop ran, nothing was Structural Framing,
and everything measured off the panel is absent. `strand_points` still
holds LIVE XYZ objects on this path -- `read_ticket` only nulls them
after the panel is found -- which is a fourth live holdout that only
exists on the failure branch.
"""
return {'mark': 'WP999', 'asm_id': 1, 'doc': _Doc('sample_job', ''),
'bom': {}, 'legends': [], 'legend_name': None,
'legend_text': None, 'strand_by_dia': {None: 2},
'strand_qty': 2, 'strand_points': [_XYZ(), _XYZ()],
'wwf_count': 0, 'wwf_mesh': None, 'sheet': None,
'panel_id': None, 'lifters': [], 'weight_lb': None,
'ticket_strand_count': None}
def a_result(ticket, design='PNL1.0', source='spec'):
return {'design': design, 'spec_source': source, 'ticket': ticket,
'intent': {'name': design, 'source': source, 'fc': 6000,
'strands': [{'qty': 14, 'dia': 0.5, 'ksi': 270,
'it': 31600}],
'envelope': {'MAX_LENGTH': (24.0, "24'-0\"")}},
'findings': [], 'run_log': []}
# ============================================================ RULE 2, walked
def test_nothing_but_plain_data_survives_the_writer(monkeypatch):
"""THE POINT OF THE WHOLE STEP, asserted mechanically.
A ticket carrying a live Document, live legend Views, live ViewSheets, a
live Transform and a float-keyed dict goes in. What comes out is walked
to its leaves and every value must be str / int / float / bool / None /
list / dict. Nothing is spot-checked, because the live object that
escapes will not be one this file thought to name.
"""
monkeypatch.setattr(QC, '_flat_text', lambda doc, vid: 'USE (14) 1/2"')
out = QC.ticket_to_json(full_ticket(), design='PNL1.0',
spec_source='spec')
assert offenders(out) == []
json.dumps(out, allow_nan=False) # and it really is writable
def test_the_walker_would_actually_catch_a_live_object():
"""A test that cannot fail is not a test. The walk is handed the exact
shapes it is meant to catch and must name every one of them."""
assert offenders({'a': _Doc()}) == [('$.a', '_Doc')]
assert offenders({'a': [1, _XYZ()]}) == [('$.a[1]', '_XYZ')]
assert offenders({0.5: 14}) == [('$ key 0.5', 'float')]
assert offenders({'a': float('nan')}) == [('$.a', 'non-finite float')]
def test_the_bare_ticket_survives_the_writer_too():
"""The failure branch, where `strand_points` is still live XYZ."""
out = QC.ticket_to_json(bare_ticket())
assert offenders(out) == []
json.dumps(out, allow_nan=False)
def test_the_whole_run_document_survives_the_writer(monkeypatch):
monkeypatch.setattr(QC, '_flat_text', lambda doc, vid: 'text')
monkeypatch.setattr(K, 'sources', lambda d: [
('catalogue', '/x/catalogue.yaml', True),
('plant', '/x/plant.yaml', False)])
doc = _Doc('sample_job', MODEL_PATH)
document = QC.run_evidence(
[a_result(full_ticket(doc)), a_result(bare_ticket(), source='regex')],
doc=doc, written_at='2026-01-01T00:00:00')
assert offenders(document) == []
json.dumps(document, allow_nan=False)
def test_a_live_object_in_an_unexpected_place_becomes_a_null_and_a_reason():
"""Rule 2 must not be a crash. `strand_layout` is a dict today; if some
future gather puts a live object in one, the file says so and the press
still finishes."""
t = bare_ticket()
t['panel_id'] = 1
t['strand_layout'] = {'across': [_XYZ()]}
out = QC.ticket_to_json(t)
assert offenders(out) == []
assert out['strand_layout'] is None
assert 'live _XYZ' in out['missing']['strand_layout']
def test_a_non_finite_float_is_refused_rather_than_written():
"""`json.dumps` writes `NaN`, which is not JSON. A nan out of a
degenerate geometry read is a could-not-measure and lands as one."""
t = bare_ticket()
t['panel_id'] = 1
t['length'] = float('nan')
t['width'] = float('inf')
out = QC.ticket_to_json(t)
assert out['length'] is None and out['width'] is None
assert 'finite' in out['missing']['length']
assert 'finite' in out['missing']['width']
json.dumps(out, allow_nan=False)
def test_a_float_keyed_dict_does_not_reach_the_file_as_one():
"""`strand_by_dia` is keyed by diameter. JSON stringifies number keys on
the way out and never turns them back, and a None key -- which
`strand_diameter` really does produce -- becomes the string "null". It
goes out as records instead."""
out = QC.ticket_to_json(full_ticket())
assert out['strand_by_dia'] == [{'diameter_in': 0.5, 'count': 14,
'reason': None}]
# ================================================ RULE 2, the named holdouts
def test_the_document_becomes_a_key_and_a_stem(monkeypatch):
monkeypatch.setattr(QC, '_flat_text', lambda doc, vid: '')
out = QC.ticket_to_json(full_ticket())
assert out['doc_key'] == 'sample_job'
assert out['model_stem'] == 'sample_job'
assert 'doc' not in out
def test_the_doc_key_is_reinforceindex_s_answer_not_a_second_one():
"""One answer to 'which model is this' in this codebase."""
from vibe import reinforceindex
doc = _Doc('some other title', OTHER_MODEL_PATH)
key, stem, why = QC._doc_identity(doc)
assert key == reinforceindex.doc_key(doc)
assert stem == 'other_job'
assert why is None
def test_a_model_never_saved_has_a_stem_of_null_with_a_reason():
out = QC.ticket_to_json(bare_ticket())
assert out['model_stem'] is None
assert 'never been saved' in out['missing']['model_stem']
def test_the_legends_become_id_name_and_flattened_text(monkeypatch):
monkeypatch.setattr(QC, '_flat_text',
lambda doc, vid: 'STRAND PATTERN B USE (14)')
out = QC.ticket_to_json(full_ticket())
assert out['legends'] == [{'id': 7001, 'name': 'STRAND PATTERN B',
'text': 'STRAND PATTERN B USE (14)',
'reason': None}]
def test_a_legend_that_was_read_and_says_nothing_is_not_a_legend_that_failed(
monkeypatch):
"""Empty text is an answer. `text: null` with a reason is not."""
monkeypatch.setattr(QC, '_flat_text', lambda doc, vid: '')
read = QC.ticket_to_json(full_ticket())['legends'][0]
assert read['text'] == '' and read['reason'] is None
def boom(doc, vid):
raise ValueError('no')
monkeypatch.setattr(QC, '_flat_text', boom)
failed = QC.ticket_to_json(full_ticket())['legends'][0]
assert failed['text'] is None
assert 'could not be read' in failed['reason']
def test_the_sheets_become_id_number_and_name():
"""The holdout the plan did not name. `t['sheets']` is live ViewSheet
and thirteen checks read `.Name` and `.Id` off it."""
out = QC.ticket_to_json(full_ticket())
assert out['sheets'] == [
{'id': 9001, 'number': 'T-101', 'name': 'WP199', 'reason': None},
{'id': 9002, 'number': 'T-102', 'name': 'WP199', 'reason': None}]
assert out['sheet_id'] == 9001
# ================================================ RULE 3, absence has a voice
def test_every_null_in_a_ticket_document_carries_a_reason():
"""MECHANICAL, and it is the rule the whole checklist rests on.
On the far side of a file boundary a key that is null and a key that is
missing look identical, and both look exactly like 'there was nothing to
check here'. So every null is walked for, and every one of them has to be
answerable. The bare ticket -- no panel, no sheet -- is the case that
fills the document with nulls, which is why it is the one walked.
"""
out = QC.ticket_to_json(bare_ticket())
assert unanswered_nulls(out) == []
# and the reasons are sentences, not shrugs
for key in out['missing']:
assert len(out['missing'][key]) > 20, \
'{0} has a reason too short to be one'.format(key)
def test_the_null_rule_holds_on_a_full_read_too(monkeypatch):
monkeypatch.setattr(QC, '_flat_text', lambda doc, vid: 'text')
out = QC.ticket_to_json(full_ticket(), design='PNL1.0',
spec_source='spec')
assert unanswered_nulls(out) == []
def test_the_null_rule_would_actually_catch_a_breach():
"""A test that cannot fail is not a test. `unanswered_nulls` is handed a
document with a bare null in it and must name it."""
assert unanswered_nulls({'length': None, 'missing': {}}) == ['$.length']
assert unanswered_nulls(
{'missing': {}, 'lifters': [{'local': None, 'reason': None}]}) == [
'$.lifters[0].local']
def test_a_row_null_is_answered_by_its_own_reason():
"""The nested half of the rule, on a real case: a lifter that could not
be placed because the panel frame was never solved."""
t = bare_ticket()
t['lifters'] = [{'family': 'P76', 'id': 1, 'point': None}]
out = QC.ticket_to_json(t)
row = out['lifters'][0]
assert row['local'] is None
assert 'frame was not solved' in row['reason']
def test_a_strand_of_unknown_diameter_says_so_rather_than_going_quiet():
out = QC.ticket_to_json(bare_ticket())
assert out['strand_by_dia'] == [
{'diameter_in': None, 'count': 2,
'reason': 'neither the strand family name nor its diameter '
'parameter said what size this strand is'}]
def test_no_panel_and_no_sheet_are_different_sentences():
"""A piece with no framing member and a piece whose sheet is missing are
two different failures, and a reader has to be able to tell them apart."""
out = QC.ticket_to_json(bare_ticket())
assert 'no Structural Framing member' in out['missing']['length']
assert 'no Structural Framing member' in out['missing']['tkt_fc']
t = bare_ticket()
t.update({'panel_id': 7, 'length': 20.0, 'family': 'PNL',
'design_number': 'PNL1.0'})
out = QC.ticket_to_json(t)
assert 'no sheet is named after this mark' in out['missing']['tkt_fc']
assert 'length' not in out['missing']
def test_a_key_this_writer_does_not_know_is_named_not_dropped():
"""Rule 3 turned on the writer itself. The check caches the button
leaves on the ticket land here, and that list is step 4's worklist."""
out = QC.ticket_to_json(full_ticket())
assert out['unserialised_keys'] == ['_debond_requirement', 'frame',
'member_boxes']
def test_a_value_that_is_present_gets_no_reason():
"""The other half: `missing` must not fill up with noise, or nobody
reads it."""
out = QC.ticket_to_json(full_ticket(), design='PNL1.0',
spec_source='spec')
for key in ('mark', 'length', 'width', 'tkt_fc', 'bom_schedules',
'design', 'spec_source'):
assert key not in out['missing']
# ================================================== RULE 4, provenance rides
def test_the_envelope_carries_the_schema_the_clock_and_the_build(monkeypatch):
monkeypatch.setattr(K, 'sources', lambda d: [])
doc = _Doc('sample_job', MODEL_PATH)
document = QC.run_evidence([], doc=doc,
written_at='2026-01-01T00:00:00')
# Pinned to the constant, not to a literal: the schema is BUMPED on any
# key change (rule 1 -- discard whole, never migrate), so a literal here
# only ever means an edit to this line.
assert document['schema'] == QC.SCHEMA
assert document['written_at'] == '2026-01-01T00:00:00'
assert document['written_by'].startswith('TicketQC ')
assert document['build']['extension_version']
assert document['doc_key'] == 'sample_job'
assert document['model_stem'] == 'sample_job'
def test_the_clock_can_be_frozen():
"""The parity gate compares two runs. A diff that is nothing but
timestamps teaches nothing and hides everything."""
one = QC.run_evidence([], doc=None, written_at='2026-01-01T00:00:00')
two = QC.run_evidence([], doc=None, written_at='2026-01-01T00:00:00')
assert json.dumps(one, sort_keys=True) == json.dumps(two, sort_keys=True)
def test_the_knowledge_layers_ride_along_in_merge_order(monkeypatch):
monkeypatch.setattr(K, 'sources', lambda d: [
('catalogue', '/x/catalogue.yaml', True),
('plant', '/x/plant.yaml', False),
('job', '/x/job.yaml', True)])
document = QC.run_evidence([], doc=_Doc(), written_at='t')
assert [row['label'] for row in document['knowledge_sources']] == [
'catalogue', 'plant', 'job']
assert [row['exists'] for row in document['knowledge_sources']] == [
True, False, True]
def test_knowledge_layers_that_could_not_be_listed_are_null_and_a_reason(
monkeypatch):
def boom(_doc):
raise RuntimeError('nope')
monkeypatch.setattr(K, 'sources', boom)
document = QC.run_evidence([], doc=_Doc(), written_at='t')
assert document['knowledge_sources'] is None
assert 'RuntimeError' in document['missing']['knowledge_sources']
def test_no_document_means_no_layers_and_says_so():
document = QC.run_evidence([], doc=None, written_at='t')
assert document['knowledge_sources'] is None
assert 'no document' in document['missing']['knowledge_sources']
assert 'doc_key' in document['missing']
def test_the_spec_source_rides_with_each_ticket(monkeypatch):
monkeypatch.setattr(K, 'sources', lambda d: [])
document = QC.run_evidence(
[a_result(bare_ticket(), design='PNL1.0', source='spec'),
a_result(bare_ticket(), design='PNL2.0', source='regex')],
doc=_Doc(), written_at='t')
assert [t['spec_source'] for t in document['tickets']] == ['spec',
'regex']
assert [t['design'] for t in document['tickets']] == ['PNL1.0', 'PNL2.0']
assert document['tickets'][0]['intent']['fc'] == 6000
def test_a_ticket_with_no_intent_says_so_rather_than_going_quiet():
out = QC.ticket_to_json(bare_ticket())
assert out['intent'] is None
assert 'no design intent' in out['missing']['intent']
def test_the_file_carries_no_findings():
"""Step 1 moves no judgement. `qc-run.json` owns the verdicts; this file
owns the inputs, and keeping them apart is what lets step 2 re-judge one
against the other."""
document = QC.run_evidence([a_result(bare_ticket())], doc=None,
written_at='t')
assert 'findings' not in document
assert 'findings' not in document['tickets'][0]
assert 'severity' not in json.dumps(document)
# ================================================ RULE 1, discarded WHOLE
def _write(tmp_path, payload):
path = os.path.join(str(tmp_path), 'run-x.json')
with open(path, 'w') as handle:
handle.write(json.dumps(payload))
return path
def test_a_schema_mismatch_throws_the_WHOLE_file_away(tmp_path):
"""Not partially, not migrated. The half-read case is the dangerous one:
a run against a configuration nobody chose, that looks entirely normal.
So the assertion is not merely that `_load_error` is set -- it is that
`tickets` is EMPTY even though the file on disk had three of them and
they parsed perfectly well.
"""
path = _write(tmp_path, {'schema': 'ticketqc/0',
'doc_key': 'sample_job',
'written_by': 'TicketQC 0.9',
'tickets': [{'mark': 'A'}, {'mark': 'B'},
{'mark': 'C'}]})
loaded = QC.load_evidence(path)
assert loaded['tickets'] == []
assert loaded['ticket_count'] == 0
assert loaded['doc_key'] is None
assert loaded['written_by'] is None
assert 'ticketqc/0' in loaded['_load_error']
assert QC.SCHEMA in loaded['_load_error']
def test_a_file_with_no_schema_at_all_is_discarded_the_same_way(tmp_path):
path = _write(tmp_path, {'tickets': [{'mark': 'A'}]})
loaded = QC.load_evidence(path)
assert loaded['tickets'] == []
assert '_load_error' in loaded
def test_a_damaged_file_is_not_fatal_but_is_never_silent(tmp_path):
path = os.path.join(str(tmp_path), 'run-x.json')
with open(path, 'w') as handle:
handle.write('{ this is not json')
loaded = QC.load_evidence(path)
assert loaded['tickets'] == []
assert loaded['_load_error']
def test_a_file_that_is_not_there_says_so(tmp_path):
loaded = QC.load_evidence(os.path.join(str(tmp_path), 'nope.json'))
assert loaded['tickets'] == []
assert 'no evidence file' in loaded['_load_error']
def test_a_current_file_round_trips_through_disk(tmp_path, monkeypatch):
monkeypatch.setattr(QC, '_flat_text', lambda doc, vid: 'USE (14) 1/2"')
monkeypatch.setattr(K, 'sources', lambda d: [('job', '/x/job.yaml', True)])
doc = _Doc('sample_job', MODEL_PATH)
document = QC.run_evidence([a_result(full_ticket(doc))], doc=doc,
written_at='2026-01-01T00:00:00')
folder = os.path.join(str(tmp_path), '_ticketqc', 'evidence')
path = QC.write_evidence(folder, document, name='run-frozen.json')
assert os.path.isfile(path)
back = QC.load_evidence(path)
assert '_load_error' not in back
assert back == document
# ==================================================== the step is INERT
def test_the_writer_never_touches_the_ticket_it_was_given(monkeypatch):
"""The whole claim of step 1 in one assertion.
`check_ticket` has already run against this dict by the time the button
serialises it, and thirteen checks still read `t['doc']` and
`t['sheets']` off it on the next press. If `ticket_to_json` edited the
ticket -- swapped the document for a key, flattened the sheets -- the
step would stop being inert and would break a check instead. So: same
keys, and every value is the SAME OBJECT it was before.
"""
monkeypatch.setattr(QC, '_flat_text', lambda doc, vid: 'x')
t = full_ticket()
before = dict((k, id(t[k])) for k in t)
QC.ticket_to_json(t, design='PNL1.0', spec_source='spec')
assert sorted(t.keys()) == sorted(before.keys())
for key in t:
assert id(t[key]) == before[key], '{0} was replaced'.format(key)
def test_the_run_writer_never_touches_the_results_it_was_given(monkeypatch):
monkeypatch.setattr(K, 'sources', lambda d: [])
results = [a_result(bare_ticket())]
before = json.dumps(sorted(results[0].keys()))
QC.run_evidence(results, doc=None, written_at='t')
assert json.dumps(sorted(results[0].keys())) == before
assert results[0]['ticket']['doc'] is not None # still the live one
def test_the_evidence_file_name_is_one_per_press():
assert QC.evidence_run_name('20260101-120000') == \
'run-20260101-120000.json'
assert QC.evidence_run_name().startswith('run-')
assert QC.evidence_run_name().endswith('.json')
def test_the_writer_creates_the_folder_it_needs(tmp_path):
folder = os.path.join(str(tmp_path), 'a', 'b', 'evidence')
path = QC.write_evidence(folder, {'schema': QC.SCHEMA, 'tickets': []},
name='run-1.json')
assert os.path.isfile(path)
def test_no_absolute_user_path_is_invented_for_the_model():
"""Evidence files live beside the model and are never committed, but the
stem is what identifies the model here rather than a full path -- the
thing `tools/scrub.py` would call a leak if one ever were."""
out = QC.ticket_to_json(full_ticket())
assert out['model_stem'] == 'sample_job'
assert '\\' not in (out['model_stem'] or '')
assert '/' not in (out['model_stem'] or '')