WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
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.
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
title: Job Recon
|
||||
# No tooltip and no author on purpose. pyRevit renders both on hover;
|
||||
# the owner wants nothing there. Guarded by
|
||||
# WillTools.extension/tests/test_button_tooltips.py -- do not add them back.
|
||||
#
|
||||
# One line: this button is a row in a `.pulldown`, and a pulldown row renders
|
||||
# as text beside a 16px icon. A `\n` in it has nowhere to go. The dropdown
|
||||
# grows to fit the longest row, so the title may say what it means.
|
||||
#
|
||||
# Back on the ribbon on 2026-08-30 after a day in `parked/`. Nothing about
|
||||
# the tool changed while it was out: only its ribbon entry was ever parked,
|
||||
# and `vibe.jobrecon` / `vibe.jobreconscan` and their tests never moved.
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
+207
@@ -0,0 +1,207 @@
|
||||
# -*- 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 '
|
||||
'— 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
|
||||
Reference in New Issue
Block a user