# -*- coding: utf-8 -*- # # The shared engine behind Ticket QC and Reinforcing QC. # # Factored out of Tools.panel/Modeling.pulldown/TicketQC.pushbutton/script.py # on 2026-09-05, when the owner asked for a second, cut-down button -- # Reinforcing QC -- that judges a panel's MODELLED reinforcing before its # ticket exists, sharing everything about Ticket QC that is not actually # about a ticket. `run(doc, uidoc, mode=...)` is that whole button, called # with `mode='ticket'` from one script.py and `mode='reinforcing'` from the # other, so the 765 lines this replaced are not copy-pasted twice over -- # a fix to one would silently not be a fix to the other. # # `mode='ticket'` IS THE OLD SCRIPT, UNCHANGED. Every branch below that # reads `mode` takes the 'ticket' arm unless said otherwise, and every call # made under that arm is the exact call the old script made, in the same # order, with the same arguments. The owner's rule for this refactor was # explicit: Ticket QC's own behaviour must not change. Where the two modes # really differ: # # * `mode='reinforcing'` never calls `ticketqc.check_ticket` -- its inline # comparisons (design number, f'c/f'ci/pcf, strand count, WWF, envelope) # are ALL ticket-vs-design comparisons by construction, and none of them # belong at a stage with no ticket. It calls # `ticketchecks.run_reinforcing` instead, through `_reinforcing_checker` # below and `ticketqc.run_for_assemblies`'s new `checker=` parameter. # * the checklist rows owned ENTIRELY by a check # `ticketchecks.REINFORCING_EXCLUDED` names are pulled out of the # coloured table -- not rendered as an alarming red NOT RUN for a # question this stage was never going to ask -- and listed once, in # their own section, with the reason. See `_excluded_row_numbers`. # * the vision escalation (reads a rendered ticket sheet) never runs. # * the machine-readable files get their own names, beside but never on # top of Ticket QC's. # # Read-only, exactly as the button it replaces was. Nothing here opens a # transaction, including the image export. # # RE-CUT 2026-09-07, against a real bridge run on three panels (see the # module note above `ticketchecks.REINFORCING_EXCLUDED`): `check_cast_extras` # and `check_weight_three_way` moved from included to excluded, and a # MEMBER-CLASS split was added (`ticketchecks.member_class_of` / # `REINFORCING_APPLIES_TO`) so a wall-panel-only rule (`check_lifting_points`' # 20/25 ft point-count ceiling) is not asked of a flexural member. This file # gained two report pieces for `mode='reinforcing'`: a "not asked for this # member class" section (per piece, reading each result's own `run_log` for # `ticketchecks.NOT_FOR_CLASS_DISPOSITION` entries) and a static "flexural # checks not built yet" spec table (`ticketchecks.FLEXURAL_CHECKS_NOT_BUILT`) # so a flexural-member reader sees the gap named rather than assuming the # checklist already covers it. `mode='ticket'` is untouched by any of this. # pylint: disable=import-error,invalid-name,broad-except import os from Autodesk.Revit.DB import AssemblyInstance, ElementId from pyrevit import script, forms from vibe import ticketqc as Q from vibe import configdump from vibe import designspec, knowledge from vibe import scope as scopelib from vibe import ticketchecks as TC from vibe import ticketvision as TV from vibe import ticketpage as TP from vibe import foldreport as fold from vibe import runlog from vibe import compat MODES = ('ticket', 'reinforcing') TITLES = {'ticket': 'Ticket QC', 'reinforcing': 'Reinforcing QC'} # The stage line, printed under the headline so a reader who lands on this # page from either button knows which question it is answering. STAGE_LINE = { 'ticket': ('checks a shop ticket against its design spec, once the ' 'ticket is drawn.'), 'reinforcing': ("checks a panel's modelled reinforcing against its " 'design spec, before ticketing has started -- see the ' "'Not checked at this stage' section for what that " 'leaves out.'), } # Distinct machine-readable filenames per mode, so a reinforcing-stage run # never overwrites -- or is overwritten by -- a ticket-stage run of the same # model. Ticket mode keeps the exact names the old script always used. _JSON_NAME = {'ticket': 'qc-run.json', 'reinforcing': 'reinforcingqc-run.json'} _TEXT_NAME = {'ticket': 'last-ticketqc.txt', 'reinforcing': 'last-reinforcingqc.txt'} _EVIDENCE_FOLDER = {'ticket': Q.EVIDENCE_FOLDER, 'reinforcing': 'evidence-reinforcing'} def _reinforcing_checker(doc, ticket, intent, know, spec): """The `checker` `ticketqc.run_for_assemblies` calls per piece in reinforcing mode -- `ticketchecks.run_reinforcing` instead of `ticketqc.check_ticket`. `doc` is unused (the signature is shared with every checker `run_for_assemblies` could call); ticket/intent/know/spec are exactly what `check_ticket` itself receives. """ findings, log, excluded = TC.run_reinforcing(ticket, intent, know, spec) return findings, log, excluded def _excluded_row_numbers(excluded_names): """{checklist row number} for every row owned ENTIRELY by a check in `excluded_names`. A row naming two checks where only one is excluded still belongs in the coloured table -- it is honestly NOT RUN because half of it was never asked, which is a true statement about this stage, not a stage-scope exclusion. Pulled out here rather than left to `checklist_states`, which has no notion of "this stage never intended to ask this" and would otherwise print the same alarming NOT RUN a genuine gap gets. Thin wrapper over `ticketpage.rows_owned_by` -- shared with `_plant_scoped_row_numbers` below, and with a host test (`test_ticketpage.py`) that does not need this module's Revit imports to exercise the row-matching logic itself. """ return TP.rows_owned_by(excluded_names) def _excluded_section_blocks(excluded_names): """One block per excluded check, with the checklist row(s) it would have owned and the reason it does not run at this stage. """ blocks = [] for name in sorted(excluded_names): nums = [num for (num, _n, _g, _o, _r, check_name) in TP.CHECKLIST_ITEMS if name in TP._check_names(check_name)] where = ' ({0})'.format(', '.join(nums)) if nums else '' blocks.append((fold.MD, '- **{0}**{1} -- {2}' .format(name, where, excluded_names[name]))) return blocks def _plant_scoped_row_numbers(plant_scoped): """Same shape as `_excluded_row_numbers`, for checks `ticketchecks.plant_scoped_out` names -- a check gated on a knowledge key this job's layer has not set, never a piece-level silence. """ return TP.rows_owned_by(plant_scoped) def _plant_scoped_section_blocks(plant_scoped): """One block per row a plant-gated check owns, naming the row id, its catalogue name, and the missing key(s) (already inside `plant_scoped`'s own reason text -- see `ticketchecks.plant_gate_reason`). Grouped by ROW, not by check name like `_excluded_section_blocks` -- the owner asked for "the row id, name and the missing key", and every plant-gated row today owns exactly one check, so this reads the row's own catalogue name rather than a bare function name. """ blocks = [] seen = set() for (num, name, _grade, _owners, _reason, check_name) in TP.CHECKLIST_ITEMS: owned = [n for n in TP._check_names(check_name) if n in plant_scoped] if not owned or num in seen: continue seen.add(num) reasons = sorted(set(plant_scoped[n] for n in owned)) blocks.append((fold.MD, '- **{0}** {1} -- {2}' .format(num, name, '; '.join(reasons)))) return blocks def run(doc, uidoc, mode='ticket'): """Run Ticket QC (`mode='ticket'`) or Reinforcing QC (`mode='reinforcing'`) against `doc`, using `uidoc`'s current selection. See the module docstring for exactly what differs between the two. """ if mode not in MODES: raise ValueError('unknown ticketqc_run mode: {0!r}'.format(mode)) TITLE = TITLES[mode] output = script.get_output() output.close_others() # ------------------------------------------------------------ context # The published checklist has to exist before anything else here is # worth doing: every row this button ever prints comes off it. if TP.CHECKLIST_UNAVAILABLE_REASON: runlog.record(TITLE, doc_title=doc.Title, outcome='refused', report_text=TP.CHECKLIST_UNAVAILABLE_REASON) forms.alert(TP.CHECKLIST_UNAVAILABLE_REASON, title=TITLE, exitscript=True) know = knowledge.load(doc) for problem in knowledge.config_problems(doc, know): runlog.record(TITLE, doc_title=doc.Title, outcome='refused', report_text=problem) forms.alert(problem, title=TITLE, exitscript=True) missing, not_applicable = knowledge.unmapped_roles(know) if missing: _unmapped_msg = ( 'These parameter roles are not mapped in knowledge.yaml, so the ' 'checks that need them cannot run:\n\n {0}\n\n' 'Edit the "parameters:" section of\n{1}\n\n' 'Run Self Check for the full picture.' .format('\n '.join(missing), designspec.knowledge_path(doc))) if 'panel.design_number' in missing: runlog.record(TITLE, doc_title=doc.Title, outcome='refused', report_text=_unmapped_msg) forms.alert(_unmapped_msg, title=TITLE, exitscript=('panel.design_number' in missing)) # ------------------------------------------------------------- selection # Assemblies picked straight through, a picked member resolves to its # owning assembly, and nothing picked means a checklist of every # assembly. No "batch or single" question: this is always a batch of one # or more pieces, ticket mode or reinforcing mode alike. selection = [doc.GetElement(i) for i in uidoc.Selection.GetElementIds()] assemblies = [el for el in selection if isinstance(el, AssemblyInstance)] if not assemblies: owners = [] for el in selection: try: owner = doc.GetElement(el.AssemblyInstanceId) except Exception: owner = None if isinstance(owner, AssemblyInstance) and owner not in owners: owners.append(owner) assemblies = owners specs = designspec.list_specs(doc) if doc.PathName else {} summaries, _sheets = Q.collect_context(doc) grouped = Q.assemblies_by_design(doc, know=know) # ------------------------------------------------------------- the scope the_scope = scopelib.of(know) scope_line = None # what was left off the checklist scope_annotation = None # what was judged outside the scope anyway if not assemblies: # Nothing preselected: a checklist of every assembly. items = [] for design, asm_list in sorted(grouped.items()): for asm in sorted(asm_list, key=lambda a: a.AssemblyTypeName): if design in specs: state = 'spec' elif design in summaries: state = 'regex fallback' else: state = 'NO spec/summary' items.append((asm, '{0} - {1} - {2}' .format(asm.AssemblyTypeName, design, state))) items.sort(key=lambda r: r[1]) offered, left_alone = [], [] for row in items: (offered if the_scope.includes(row[0].AssemblyTypeName) else left_alone).append(row) scope_line = the_scope.line( [r[0].AssemblyTypeName for r in left_alone], total=len(items), unit='piece') items = offered if not items: _nothing_msg = 'There is nothing to check.\n\n{0}'.format(scope_line) runlog.record(TITLE, doc_title=doc.Title, outcome='refused', report_text=_nothing_msg) forms.alert(_nothing_msg, title=TITLE, exitscript=True) names = [r[1] for r in items] picked = forms.SelectFromList.show( names, multiselect=True, title='{0} - choose pieces'.format(TITLE), button_name=('Check pieces' if mode == 'reinforcing' else 'Check tickets')) if not picked: script.exit() picked_set = set(picked) assemblies = [r[0] for r in items if r[1] in picked_set] if not assemblies: script.exit() if not grouped and not assemblies: _no_design_msg = ( 'No assembly carries a value for the "panel.design_number" role ' '(mapped to: {0}).\n\nEither the mapping is wrong or this model ' 'does not group pieces that way.' .format(Q.role_label(know, 'panel.design_number'))) runlog.record(TITLE, doc_title=doc.Title, outcome='refused', report_text=_no_design_msg) forms.alert(_no_design_msg, exitscript=True) # --------------------------------------------------------------------- run context = Q.collect_context(doc) results, missing = [], [] total = len(assemblies) checker = None if mode == 'ticket' else _reinforcing_checker bar_title = ('Checking tickets ({0} pieces) - first of a design can be slow' if mode == 'ticket' else 'Checking reinforcing ({0} pieces) - first of a design can ' 'be slow') with forms.ProgressBar(title=bar_title.format(total), cancellable=True) as pb: for i, asm in enumerate(assemblies): if pb.cancelled: break pb.update_progress(i, total) batch, miss = Q.run_for_assemblies( doc, [asm], specs=specs, know=know, context=context, checker=checker) results.extend(batch) missing.extend(miss) pb.update_progress(total, total) # ------------------------------------------- the escalation, ticket only # Reinforcing QC never reads a rendered sheet: there is no sheet yet. _vision_on = False if mode == 'ticket': _vision_on, _ = TV.run_settings(designspec.root_dir(doc)) for r in results: r['model_read'] = [] if _vision_on and results: with forms.ProgressBar(title='Escalating to a model reading the drawing', cancellable=True) as pb: for i, r in enumerate(results): if pb.cancelled: break pb.update_progress(i, len(results)) try: r['model_read'] = TV.read_for_ticket( r['ticket'], know, designspec.root_dir(doc)) except Exception as _ex: r['model_read'] = [TV.not_run_row( 'AI-BARMARK', 'the drawing', 'the escalation itself failed ({0})' .format(type(_ex).__name__))] # ------------------------------------------------------------ report _excluded_names = dict(TC.REINFORCING_EXCLUDED) if mode == 'reinforcing' else {} _excluded_nums = (_excluded_row_numbers(_excluded_names) if _excluded_names else set()) # PLANT-SCOPED-OUT rows, both modes alike: a check gated on a knowledge # key (`ticketchecks.PLANT_GATE_KEYS`) this job's layer has not set -- # e.g. 6.9/6.10/6.11, MC-only rules keyed on `lifters.lifting_points.*`. # Owner report, 2026-09-08: a Ticket QC on another plant's job showed "a pile # of MC checks" -- these three printed inline as alarming red NOT RUN # on any plant that never declared MC's numbers. Pulled out of the # coloured table entirely, into "Not applicable at this plant" below, # the same treatment `_excluded_nums` gives a stage-scoped-out row. _plant_scoped = TC.plant_scoped_out(know) _plant_scoped_nums = (_plant_scoped_row_numbers(_plant_scoped) if _plant_scoped else set()) _plant_id = knowledge.plant_id(doc) muted = TP.muted_checks(know, resolver=knowledge.resolve_check_reference, plant=_plant_id) _mute_problems = TP.muted_reference_problems(know, plant=_plant_id) try: _layers = configdump.layer_values(doc) except Exception: _layers = [] def _declaring_layer(item, condition): if not _layers or not condition: return None for branch in ('not_applicable', 'only_applies_to'): found = knowledge.declaring_layer( _layers, ['checks', branch, item, condition]) if found: return found return None for r in results: _rows = TC.checklist_states(r['findings'], r.get('run_log')) _rows = TV.annotate_rows(_rows, r.get('model_read')) _decisions, _classification, _member, _profile = \ TC.applicability_decisions_for(r['ticket'], know) for _num in _decisions: _d = _decisions[_num] if _d.get('not_applicable'): _d['layer'] = _declaring_layer(_num, _d.get('condition')) _rows, _na_notes = TP.apply_applicability(_rows, _decisions) r['applicability_notes'] = _na_notes r['applicability_problems'] = TP.applicability_problems(_decisions) r['classification'] = _classification r['member'] = _member r['profile'] = _profile r['off_board'] = TP.off_board(_decisions) _kept, _muted_notes = TP.apply_muting(_rows, muted) r['rows'] = _kept r['rows_unmuted'] = _rows r['muted_notes'] = _muted_notes r['rows'] = TP.profile_view(r['rows'], _profile) r['rows_unmuted'] = TP.profile_view(r['rows_unmuted'], _profile) if _excluded_nums: # NOT AT THIS STAGE, not NOT RUN: pulled out of the coloured # table entirely -- see `_excluded_row_numbers`. The master id # (row[0]) is stable through `profile_view`, so filtering here # is filtering by the same number the module note above keys on. r['rows'] = [row for row in r['rows'] if row[0] not in _excluded_nums] r['rows_unmuted'] = [row for row in r['rows_unmuted'] if row[0] not in _excluded_nums] if _plant_scoped_nums: # NOT APPLICABLE AT THIS PLANT, not NOT RUN -- same treatment, # both modes. See `_plant_scoped_row_numbers` above. r['rows'] = [row for row in r['rows'] if row[0] not in _plant_scoped_nums] r['rows_unmuted'] = [row for row in r['rows_unmuted'] if row[0] not in _plant_scoped_nums] r['details'] = TP.checklist_details(r['findings']) r['counts'] = TP.checklist_tally(r['rows']) def _loudness(r): c = r['counts'] return (c.get('FAIL', 0), c.get('NOT RUN', 0), c.get('WARN', 0), c.get('FOR ENGINEER', 0)) loud, all_green = [], [] for r in results: (loud if any(_loudness(r)) else all_green).append(r) loud.sort(key=lambda r: (-_loudness(r)[0], -_loudness(r)[1], -_loudness(r)[2], r['ticket']['mark'])) clean, failed, warned = [], [], [] tally = {} for r in results: bad = [f for f in r['findings'] if f.severity != 'OK'] for f in bad: tally[f.code] = tally.get(f.code, 0) + 1 if not bad: clean.append(r) elif any(f.severity == 'FAIL' for f in bad): failed.append(r) else: warned.append(r) output.add_style(TP.CHECKLIST_STYLE) output.add_style(fold.STYLE) output.inject_script(fold.SCRIPT) # ---- the headline, then the tables. Nothing folded above this line. output.print_md('# {0}'.format(TITLE)) output.print_md('_{0}_'.format(STAGE_LINE[mode])) output.print_md('Model **{0}** · {1} piece(s) across {2} design ' 'number(s) · {3} clean · {4} failing ' '· {5} warning only' .format(doc.Title, len(results), len(set([r['design'] for r in results])), len(clean), len(failed), len(warned))) output.print_html(TP.checklist_key_html()) for r in loud: t = r['ticket'] head = '**{0}** [{1}] {2} x {3}  —  {4}'.format( t['mark'], r['design'], Q.fmt_feet(t.get('length')), Q.fmt_feet(t.get('width')), TP.checklist_headline(r['counts'])) if mode == 'ticket' and t.get('sheet') is not None: head += '   ' + output.linkify(t['sheet'].Id, 'open ticket') if t.get('panel_id'): head += '   ' + output.linkify(ElementId(t['panel_id']), 'panel') output.print_md(head) output.print_html(TP.checklist_table_html(r['rows'], r['details'])) if all_green: _quiet = sum([r['counts'].get(TP.NOT_APPLICABLE_STATE, 0) for r in all_green]) output.print_md('**{0} piece(s) with nothing to read on the checklist:** ' '{1}{2}' .format(len(all_green), ', '.join([r['ticket']['mark'] for r in all_green]), (' — and {0} row(s) across them were NOT ' 'ASKED because a knowledge layer declares they ' 'do not apply; see below'.format(_quiet)) if _quiet else ' — every row green')) # ------------------------------------------------- everything else, folded sections = [] def _section(key, title, summary, blocks, problem=None): if not blocks: return sections.append(fold.Section(key, title, summary=summary, blocks=blocks, problem=problem)) if missing: _section('missing', 'Pieces that could not be checked', '{0} piece(s)'.format(len(missing)), [(fold.MD, '- **{0}** (id {1}): {2}' .format(m.get('mark', '?'), m.get('id', '?'), m.get('why', 'unknown'))) for m in missing], problem='{0} piece(s) were not judged at all'.format(len(missing))) if _excluded_names: _section('not_at_this_stage', 'Not checked at this stage', '{0} check(s) this stage never asks'.format(len(_excluded_names)), [(fold.MD, 'These checks all need a ticket artefact -- a ' 'sheet, a placed view, a printed call-out, a ' 'BOM table -- that does not exist yet at this ' 'stage. Run Ticket QC once the ticket is drawn ' 'to have them judged.')] + _excluded_section_blocks(_excluded_names), problem=None) if _plant_scoped: _section('not_applicable_at_this_plant', 'Not applicable at this plant', '{0} row(s) this plant\'s knowledge has not turned on' .format(len(_plant_scoped_nums)), [(fold.MD, 'These rows are a PLANT rule, not a universal ' 'one -- another plant wrote a number into its own ' 'knowledge layer, and this job\'s layer has never ' 'set it, so the rule was never asked to apply ' 'here. Set the named key(s) in this job\'s ' 'knowledge to have them judged; leaving them ' 'unset is the correct choice for a plant that ' 'does not follow the rule.')] + _plant_scoped_section_blocks(_plant_scoped), problem=None) if mode == 'reinforcing': _class_blocks = [] _class_piece_count = 0 for r in results: _mark = r['ticket'].get('mark') or '(no mark)' _class_skips = [(name, note) for (name, state, note) in (r.get('run_log') or []) if state == TC.NOT_FOR_CLASS_DISPOSITION] if not _class_skips: continue _class_piece_count += 1 _class_blocks.append( (fold.MD, '**{0}** -- {1}'.format(_mark, _class_skips[0][1]))) _class_blocks.extend( (fold.MD, ' - {0}'.format(name)) for (name, _note) in _class_skips) _section('not_for_class', 'Not asked for this member class', '{0} piece(s) with a check narrowed by member class' .format(_class_piece_count), [(fold.MD, 'A wall panel (WPA/WPB) and a flexural member ' '(SMD/FSA/DT) answer a different checklist -- ' 'see `ticketchecks.REINFORCING_APPLIES_TO`. A ' 'piece whose member class could not be judged ' 'still gets every check; nothing here is a ' 'silent narrowing.')] + _class_blocks, problem=None) _flex_rows = [[name, evidence, rule] for (name, evidence, rule) in TC.FLEXURAL_CHECKS_NOT_BUILT] _asked_rows = [[concern, '{0} ({1})'.format(row_id, check)] for (concern, check, row_id) in TC.FLEXURAL_CHECKS_ALREADY_ASKED] _section('flexural_gaps', 'Flexural-member checks not built yet', '{0} check(s) named, none of them coded' .format(len(_flex_rows)), [(fold.MD, "SMD/FSA/DT are flexural members -- simply " 'supported gravity spans -- and the checklist ' 'above does not yet ask any of these. Listed ' 'so a flexural-member reader sees the gap ' 'rather than assuming it is covered; filed ' 'separately, not built here.'), (fold.TABLE, ['check', 'evidence in the model', 'verdict rule'], _flex_rows), (fold.MD, 'Already asked on a flexural piece, by checks ' 'that were never wall-only -- not gaps:'), (fold.TABLE, ['concern', 'checklist row (check)'], _asked_rows)], problem=None) regex_designs = sorted(set([r['design'] for r in results if r['spec_source'] == 'regex'])) if regex_designs: _section('regex', 'Designs read with regexes, not an extracted spec', ', '.join(regex_designs), [(fold.MD, '**{0}** had no extracted spec, so the summary was ' 'read with regexes instead. That reading is weaker ' '-- run **Extract Design** on them.' .format(', '.join(regex_designs)))], problem='{0} design(s) are being judged against a weaker ' 'reading'.format(len(regex_designs))) _na_notes, _na_problems, _profile_lines = [], [], [] _off_board = [] _seen_na = set() for r in results: _profile_lines.append(TP.profile_line( r['ticket'].get('mark') or '(no mark)', _plant_id, r.get('member'), r.get('profile'), r.get('off_board'))) for _num in (r.get('off_board') or []): if _num not in _off_board: _off_board.append(_num) _profile_lines.append(TP.classification_line( r['ticket'].get('mark') or '(no mark)', r.get('classification'), r.get('applicability_notes'))) for note in (r.get('applicability_notes') or []): if note[0] not in _seen_na: _seen_na.add(note[0]) _na_notes.append(note) for problem in (r.get('applicability_problems') or []): if problem not in _na_problems: _na_problems.append(problem) _pp = (r.get('profile') or {}).get('problem') if _pp and ('profile', _pp) not in _na_problems: _na_problems.append(('profile', _pp)) for _written, _why in _mute_problems: _entry = ('mute {0!r}'.format(_written), _why) if _entry not in _na_problems: _na_problems.append(_entry) _na_blocks = [(fold.MD, line) for line in _profile_lines] if _na_notes: _na_blocks.append((fold.MD, '**Rows not asked, and who said so:**')) _na_blocks.extend([(fold.MD, line) for line in TP.applicability_lines(_na_notes)]) if _na_problems: _na_blocks.append((fold.MD, '**Declarations that were REFUSED, so their ' 'rows RAN:**')) _na_blocks.extend([(fold.MD, '**{0}** — {1}'.format(num, text)) for (num, text) in _na_problems]) _na_blocks.append( (fold.MD, '_A row shown as NOT APPLICABLE was **not run at all**. That ' 'is not muting (a muted check runs and is repainted grey), not ' 'scope (which sets aside a whole piece), and not dormancy (a ' 'check whose enabling knowledge is unset). It is a knowledge ' 'layer declaring that this question is not about this kind of ' 'piece. A row that FAILED is never switched off -- the ' 'declaration is declined and says so. A row scoped to a panel ' 'type, on a piece nothing could classify, is NOT RUN in red, ' 'never quiet: nobody could ask it._')) _section('applicability', 'Which checklist ran on each piece, and what it ' 'did not ask', '{0} row(s) not asked; {1} declaration(s) refused' .format(len(_na_notes), len(_na_problems)), _na_blocks, problem=TP.applicability_problem(_na_notes, _na_problems)) _all_muted = [] _seen_muted = set() for r in results: for note in r['muted_notes']: if note[0] not in _seen_muted: _seen_muted.add(note[0]) _all_muted.append(note) if _all_muted: _section('muted', 'Checklist items muted on this job', '{0} item(s): {1}'.format( len(_all_muted), ', '.join([n[0] for n in _all_muted])), [(fold.MD, line) for line in TP.muting_lines(_all_muted)] + [(fold.MD, '_Muting does not remove a row -- it stays on the ' 'table, repainted grey as MUTED, with this note ' 'saying what the check really reported. The check ' 'still runs every time; only the presentation ' 'changes. It never removes a finding: everything ' 'below still reaches the machine-readable JSON ' 'and still counts in the header line, and a muted ' 'item that FAILED keeps its own red on the table ' '-- muting never softens a failure._')], problem=TP.muting_problem(_all_muted)) if tally: _section('tally', 'Issues by code', '{0} code(s) across {1} piece(s)' .format(len(tally), sum(tally.values())), [(fold.TABLE, ['issue', 'pieces'], [[c, str(n)] for c, n in sorted(tally.items(), key=lambda kv: -kv[1])])]) seen, intent_rows = [], [] for r in results: intent = r['intent'] if intent['name'] in seen: continue seen.append(intent['name']) env = intent.get('envelope') or {} def bound(key, _env=env): return _env[key][1] if key in _env else '-' intent_rows.append([ intent['name'], r['spec_source'], Q._fmt_num(intent.get('fc')), Q._fmt_num(intent.get('fci')), Q._fmt_num(intent.get('pcf')), Q.strand_signature(intent.get('strands') or []), intent.get('wwf_mesh') or '-', '{0} .. {1}'.format(bound('MIN_LENGTH'), bound('MAX_LENGTH')), '{0} .. {1}'.format(bound('MIN_WIDTH'), bound('MAX_WIDTH')), ]) _section('intent', 'Design intent being enforced', '{0} design(s), from {1}'.format( len(intent_rows), ', '.join(sorted(set([r[1] for r in intent_rows]))) or '-'), [(fold.TABLE, ['design', 'from', "f'c", "f'ci", 'pcf', 'strands', 'W.W.F.', 'length', 'width'], intent_rows)]) if scope_annotation is None: scope_annotation = the_scope.annotation( [str(a.AssemblyTypeName) for a in assemblies if not the_scope.includes(a.AssemblyTypeName)], unit='piece') _scope_blocks = [(fold.MD, '> ' + (scope_line or the_scope.line([], unit='piece')))] if scope_annotation: _scope_blocks.append((fold.MD, '> ' + scope_annotation)) for _note in the_scope.declaration_lines(): _scope_blocks.append((fold.MD, '_{0}_'.format(_note))) if doc.PathName: _scope_blocks.append((fold.MD, 'Knowledge file: `{0}`' .format(designspec.knowledge_path(doc)))) _section('scope', 'Scope and knowledge', 'what this run was and was not asked to judge', _scope_blocks, problem=(scope_annotation or None)) if any(f.code.startswith('HANDLE-') for r in results for f in r['findings']): _section('handling', 'How the lifting/handling result was obtained', '`HANDLE-*` rows on this run', [(fold.MD, 'A current **Panel Lift Check** or sweep result is reused when ' 'one is on file; otherwise a default-stage check runs on the ' 'spot, rate limited so this does not hang on a large design. ' '`HANDLE-STALE` means a saved result no longer matches the ' 'piece and was not used. A result not tagged "reviewed Panel ' 'Lift Check" is good enough to find a problem, not good enough ' 'to sign -- see docs/lift-check.md.')]) _read = [(r['ticket']['mark'], f) for r in results for f in (r.get('model_read') or [])] if _read: _head, _read_rows = TV.report_block(_read) _section('modelread', 'Read by a model (never a verdict)', '{0} row(s) a person must judge'.format(len(_read_rows)), [(fold.MD, _head), (fold.TABLE, list(TV.REPORT_COLUMNS), _read_rows)], problem='a model was asked to look at {0} thing(s); nothing ' 'here is a pass or a fail'.format(len(_read_rows))) elif _vision_on: output.print_md('> The vision escalation was on and produced nothing, ' 'which should not happen - every escalation reports a ' 'reason even when it does not run.') # ---- THE MACHINE-READABLE COPY. Written on EVERY press. try: _doc = TV.qc_json([f for r in results for f in r['findings']], [f for r in results for f in (r.get('model_read') or [])], tickets=results, envelope=Q.run_envelope(TV.RUN_SCHEMA, doc=doc)) _path = TV.write_run_json(designspec.root_dir(doc), _doc, name=_JSON_NAME[mode]) output.print_md('_Machine-readable: `{0}`_'.format(_path)) except Exception as _ex: output.print_md('_(the JSON copy could not be written: {0})_' .format(type(_ex).__name__)) # ---- THE EVIDENCE FILE. try: _ev_doc = Q.run_evidence(results, doc=doc) _ev_path = Q.write_evidence( designspec.sub_dir(doc, _EVIDENCE_FOLDER[mode]), _ev_doc) output.print_md('_Evidence (nothing reads it yet): `{0}`_' .format(_ev_path)) except Exception as _ex: output.print_md('_(the evidence file could not be written: {0})_' .format(type(_ex).__name__)) # ---- FOLDED IS NOT GONE. full_path = None try: full_path = os.path.join(designspec.root_dir(doc), _TEXT_NAME[mode]) _lines = [TITLE.upper() + ' -- ' + str(doc.Title), ''] for r in loud + all_green: _t = r['ticket'] _lines.append('') _lines.append('{0} [{1}] {2}'.format( _t['mark'], r['design'], TP.checklist_headline(r['counts']).replace('·', '-'))) _lines.append('-' * 78) _lines.append(TP.checklist_table_text(r['rows'], r['details'])) _lines.append('') _lines.append(fold.text(sections, show_all=True)) payload = compat.as_text('\n'.join(_lines)) handle = open(full_path, 'wb') try: handle.write(payload.encode('utf-8')) finally: handle.close() except Exception as _exc: full_path = None output.print_md('> The full report could not be written to disk ({0}). ' 'Everything is still on this page; use the fold headers.' .format(_exc)) if sections: opened = fold.sections_with_problems(sections) output.print_md('---') if opened: output.print_md('**{0} of the {1} section(s) below opened themselves ' '— they have something in them that needs you.** ' 'The rest are folded; click a header to open one.' .format(len(opened), len(sections))) else: output.print_md('**Nothing below the tables needs you.** Every section ' 'is folded; click a header to open one.') if full_path: output.print_md('_The whole report, nothing folded, is in_ `{0}`' .format(full_path)) output.print_html(fold.html(sections)) runlog.record(TITLE, doc_title=doc.Title, marks=[r['ticket'].get('mark') for r in results], outcome='ok', report_path=full_path)