Files
will 8e6ee31596
ci / test (push) Successful in 6m18s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
History-free snapshot of the private development repository at cbe45c5
(2026-09-09). Code, docs and public catalogue only; no plant or job
knowledge layers, no client or job names (tools/scrub.py clean). The
intro deck is left out of this snapshot because its slide images could
not be checked for job drawings.
2026-09-09 08:32:15 -05:00

208 lines
8.9 KiB
Python

# -*- coding: utf-8 -*-
#
# Reconcile the issued ticket set against the whole job.
#
# Every other check on this panel judges one ticket at a time, and no amount of
# per-ticket checking can find a ticket that is not there. This button asks the
# question about the SET: every piece printed on something, everything printed
# about a piece that exists, and the counts reconciling mark by mark.
#
# It reads. It never writes, and it opens no transaction.
#
# THE SHAPE OF THE PAGE, AND WHOSE IT IS. Owner, 2026-08-30, now standing
# across this tab: the answer is the headline and is never folded; everything
# else follows it, collapsed; a section that carries a problem opens itself.
# The reader's contract is that if nothing below the headline is open, nothing
# needs him.
#
# Here the answer is the VERDICT IN NUMBERS -- pieces and tickets accounted
# for, and a count per finding -- not the findings themselves. On a job with
# four hundred uncovered pieces the lists are thousands of lines and the
# verdict is invisible inside them. So:
#
# * the headline is the totals and the finding counts, printed through
# pyRevit's own markdown and tables;
# * every non-empty finding is a section below it, OPEN, because there is no
# finding in this reconciliation that is merely interesting;
# * coverage by design, the page-set inventory, the categories swept, the
# checks that found nothing and the timings are folded;
# * and the two sections that say what this run could NOT see -- out of
# scope, and blind spots -- are separate, always, and carry their counts
# on their headers with the fold shut. A blind spot that is a REFUSAL
# (nothing reconciled) opens itself and is repeated in the headline,
# because a refusal reported quietly reads as a clean job.
#
# WHY THE HTML IS WRITTEN BY HAND AND LOOKS DATED. This output panel is a
# `System.Windows.Forms.WebBrowser` -- the IE engine, in IE7 document mode.
# `<details>` does not exist there and fails SILENTLY. The fold is an inline
# `onclick` plus `style.display`, which is IE5-era DOM; a section with a
# problem in it renders open as plain HTML and needs no script at all; and the
# whole report, nothing folded and nothing capped, is written to
# `last-report.txt` beside the model's other job data on every press, with the
# page saying where. A fold that will not open therefore costs a file open,
# never a lost sentence. All of that lives in `vibe.foldreport`.
# pylint: disable=import-error,invalid-name,broad-except
# `__title__` mirrors bundle.yaml's `title:` and is otherwise dead --
# pyRevit reads it only `if not self.meta`. It is kept in step so that
# trimming a bundle file cannot bring an old name back. One line, no
# `\n`: this button is a row in a `.pulldown`, and a row has no second
# line to put one on.
__title__ = 'Job Recon'
import os
import traceback
from pyrevit import revit, script
from vibe import compat, designspec, knowledge, jobrecon, jobreconscan
from vibe import foldreport as fold
from vibe import runlog
doc = revit.doc
output = script.get_output()
output.close_others()
REQUIRED = ('member.control_number', 'sheet.control_numbers')
# The cap on each list ON THE PAGE. The file written beside the model has no
# cap at all -- a page is read, a file is searched.
PAGE_LIMIT = 60
HOW_TO_MAP = """
This reconciliation needs roles the older checks never used. Add them to the
plant knowledge file (the layer that describes the office TEMPLATE), naming
whatever your title block and your pieces actually call these things:
parameters:
member:
control_number: <the piece's control number>
ticket_created: <optional -- the model's own 'a ticket exists' flag>
ticket_detailed: <optional>
sheet:
total_required: <the quantity the title block states>
sheet_count: <how many pages the ticket says it has>
# either list the control-number slots explicitly ...
control_numbers: [<slot 1>, <slot 2>, ...]
nomenclature:
# ... or, for numbered slots, match them with one expression:
control_slot_re: '^<PREFIX>_\\d+$'
Until then this button refuses rather than reporting a job it cannot see.
"""
def write_full(recon, full_path):
"""The whole report, unfolded, beside the model. Returns True if written.
Written BEFORE anything is printed, so the file exists even if the
printing throws. `as_text` first and encode second: under IronPython 2.7
the report is a byte string and a mark carrying a non-ASCII character
would make a bare `.encode('utf-8')` raise on the implicit ASCII decode.
"""
payload = compat.as_text(
'\n'.join(jobrecon.format_report(recon, full_path=full_path)))
handle = open(full_path, 'wb')
try:
handle.write(payload.encode('utf-8'))
finally:
handle.close()
return True
def main():
know = knowledge.load(doc)
problems = knowledge.config_problems(doc, know)
if problems:
for line in problems:
output.print_md('**' + line + '**')
runlog.record('Job Recon', doc_title=doc.Title, outcome='refused',
report_text=('\n'.join(problems) +
'\n\nJob Recon cannot run until the '
'configuration is fixed.'))
return
names = knowledge.param_names(know, 'member.control_number')
slots = (knowledge.param_names(know, 'sheet.control_numbers')
or knowledge.get(know, 'nomenclature.control_slot_re', None))
if not names or not slots:
output.print_md('## Job Recon cannot run')
output.print_md('Nothing was reconciled, and that is **not** a clean '
'result -- it is a refusal.')
print(HOW_TO_MAP)
runlog.record('Job Recon', doc_title=doc.Title, outcome='refused',
report_text=('Job Recon cannot run: the control-number '
'roles are not mapped.\n' + HOW_TO_MAP))
return
recon = jobreconscan.run(doc, know=know)
full_path = os.path.join(designspec.sub_dir(doc, 'jobrecon'),
'last-report.txt')
written = False
try:
written = write_full(recon, full_path)
except Exception as exc:
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))
sections = jobrecon.render(recon, limit=PAGE_LIMIT,
full_path=full_path if written else None)
# ---- the answer, through pyRevit, big and first.
for block in sections[0].blocks:
if block[0] == fold.TABLE:
output.print_table(block[2], columns=block[1])
else:
output.print_md(block[1])
# ---- everything else, folded.
tail = sections[1:]
if not tail:
# The headline alone was the whole page -- recorded the same as a
# full page, with the file copy's path when one was written.
runlog.record('Job Recon', doc_title=doc.Title, outcome='ok',
report_path=full_path if written else None,
report_text='Job Recon ran -- nothing below the headline '
'to report.')
return
output.add_style(fold.STYLE)
output.inject_script(fold.SCRIPT)
opened = fold.sections_with_problems(tail)
output.print_md('---')
if opened:
output.print_md('**{0} of the {1} section(s) below opened themselves '
'&mdash; they have something in them that needs you.** '
'The rest are folded; click a header to open one.'
.format(len(opened), len(tail)))
else:
output.print_md('**Nothing below needs you.** Every section is folded; '
'click a header to open one.')
if written:
output.print_md('The whole report, nothing folded and nothing capped, '
'is in `{0}`.'.format(full_path))
output.print_html(fold.html(tail))
# The page is complete and the file copy exists. The verdict in numbers
# is the headline, which `sections[0]` carried as markdown -- this short
# summary says what reconciled, and points at the file for the rest.
runlog.record('Job Recon', doc_title=doc.Title, outcome='ok',
report_path=full_path if written else None,
report_text='Job Recon ran; full report at {0}'
.format(full_path if written else 'this page'))
try:
main()
except Exception as e:
# The deliberate exits above never reach here (script.exit() raises
# SystemExit, which Exception does not catch) -- this is the
# unexpected-exception path, and pyRevit shows its own dialog after
# the record is written.
runlog.record('Job Recon', doc_title=doc.Title,
outcome='error', error=str(e),
traceback_text=traceback.format_exc())
raise