Files
willtools-demo/WillTools.extension/lib/vibe/reinforcereport.py
T
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

1599 lines
73 KiB
Python

# -*- coding: utf-8 -*-
"""Turning a reinforcing suggestion into the words the button prints.
No Revit here, and no matching either. This module knows nothing about
geometry: it is handed the `(suggestion, info)` pair that
`vibe.reinforcebind.propose` / `suggest_for_piece` returns, plus the
`explain()` rows for one proposal, and it decides how that reads. It exists
so the wording -- which is the part that can quietly lie -- is unit-tested on
host CPython instead of being trapped inside a pyRevit script that can only
be exercised by restarting Revit.
WHAT THIS MODULE IS PROTECTING
------------------------------
Distinctions that the layers below take care to keep apart, and that a
report is the easiest place in the whole feature to collapse by accident:
1. `NO_DONORS` -- nothing eligible was offered, so NOTHING WAS JUDGED --
against `NO_COVERAGE` -- donors were examined and none of them covers
anything. The first is ignorance, the second is a finding. They get
different headlines and different words, never "no suggestions".
2. `features == []` (measured, genuinely nothing to reinforce) against
`features is None` (never measured, could not be judged). Same rule:
different headlines.
3. PARTIAL is an ANSWER, not a failure. "Here is a cover of five of your six
features and this is the one nobody has" is the useful shape of that
answer, so a partial cover prints its proposals first and its gap second,
rather than printing an apology.
4. `master` -- a donor the owner has typed MASTER against in the model,
meaning REVIEWED, not merely reinforced. Every other donor in the pool is
marked `TICKET_IS_REINFORCED`, which says somebody drew bars and says
nothing about whether anybody checked them. That difference is the only
provenance this feature has, so a MASTER is marked wherever it is printed
and the footnote says what the mark means -- and also says what it does
NOT mean, because MASTER is a tie-break and never beats a better fit.
5. `outline_approximate` -- the 13 of 255 WP donors whose real outline is not
the rectangle the index stores. A match involving one of those may never
be printed as a plain "exact". It is printed as `exact*` with the footnote
attached, every time, so the word "exact" in this report always means
measured.
6. Same-elevation AFFINITY against MATCH. Ruled 2026-09-02, after WP425
ranked five donors in a dead tie and the one sharing its own building
elevation printed last, invisibly, by mark order -- and RULED AGAIN the
same day: elevation is a PURELY GEOMETRIC fact (a thickness-axis band,
`reinforcecore.elevation_band`), never a hand-typed parameter. It is the
LAST tie-break -- after fewest donors, most exact, most reviewed and
total distance -- and this module marks it the same way it marks
MASTER: on the donor, in the proposals table, with a footnote that says
plainly it never outranks a better fit. A band that could not be
determined is a stated could-not-judge (`elevation_note`), never a
silent absence that reads as "elevation was never a factor here".
INCHES, TWO DECIMALS -- AND WHAT THAT DOES NOT MEAN
---------------------------------------------------
Owner's ruling, 2026-08-30: "all actual measurements use inches, to two
decimal places". Every measurement
this module prints is in INCHES to two decimals, and never a bare number
without a unit.
The stored geometry is still FEET -- Revit's internal unit, which is what the
index caches. Two kinds of number arrive here and they cross the unit seam at
different places, so they get two different formatters and the names say
which is which:
* `length_in(feet)` -- a COORDINATE or a strip width, still feet on the
record. It converts here, at the moment of printing, through
`reinforcecore.inches()`.
* `distance_in(inches)` -- a `covers` distance. It is ALREADY inches:
`reinforcebind.covers` converted it at the seam. This one must never
convert, and a second conversion is the 12x error this project has
already paid for once.
Passing the wrong one is a factor of twelve, so they are named for what they
take rather than for what they print.
TWO DECIMALS IS DISPLAY, NOT PRECISION. 0.01 in is an order of magnitude
finer than any tolerance this tool uses -- dimensions 3/8", interference
1/8". So `reinforcebind` quantizes every non-exact distance to the nearest
1/8 in before it ever reaches here, which means a distance printed on this
page can only ever be a multiple of 1/8 in. `GRAIN_NOTE` says so out loud;
the granularity of the printed values says it again to anyone who skips
footnotes.
"""
# pylint: disable=invalid-name
from vibe import reinforcecore as core
from vibe import reinforcematch as match
# Feature kinds, copied rather than imported: `reinforcebind` is under active
# rewrite and this module is deliberately not coupled to its internals. These
# are the string values that arrive on a feature record.
OPENING = 'opening'
STRIP_X = 'strip_x'
STRIP_Y = 'strip_y'
VOID_VOID = 'void-void'
VOID_EDGE = 'void-edge'
APPROX_FOOTNOTE = (
'`*` — this match involves a piece whose real outline is not the '
'rectangle the index stores (a slope or a corner cut the index cannot '
'see). Its **edge** conditions are approximated, so a starred match is '
'never a measured exact match. Check the edge on the drawing before '
'trusting it.')
# The mark on a reviewed donor. An HTML entity, not a literal character: the
# extension runs on IronPython 2.7 where a non-ASCII source literal is a byte
# string, and the report is already rendered as markdown (`—` throughout).
# `*` is taken -- it means "outline approximated" -- and the two can and do
# appear on the same donor, so they must not be the same symbol.
MASTER_MARK = '†'
MASTER_FOOTNOTE = (
'`' + MASTER_MARK + '` — this donor is typed **MASTER** in the '
'model, which means it has been **reviewed**. Every other donor here is '
'marked `TICKET_IS_REINFORCED`, which only means somebody drew '
'reinforcing on it — not that anyone checked it. MASTER is the one '
'provenance signal this tool has. It is a **tie-break only**: a MASTER '
'never outranks a donor that fits better, it only wins between donor '
'sets the geometry cannot tell apart.')
# The mark on a donor sharing the target's building elevation. A double
# dagger, distinct from `*` (outline approximated) and MASTER_MARK's single
# dagger -- all three can land on the same donor. Owner ruling, 2026-09-02,
# on WP425: five donors tied on every other criterion and the one on the
# target's own elevation (WP234) printed last, invisibly, because nothing
# marked it. This mark is the fix: it is what makes the tie AND what broke
# it visible on the same row, rather than a silent sort by mark number.
ELEVATION_MARK = '‡'
ELEVATION_FOOTNOTE = (
'`' + ELEVATION_MARK + '` — this donor shares the target\'s '
'**building elevation**. It is the **last** tie-break in the ranking '
'— after fewest donors, most exact, most reviewed and total '
'distance — so it never outranks a donor that fits better; it '
'only decides who wins among donors everything above it left dead '
'level. A donor whose elevation could not be read never carries this '
'mark, even when the target\'s is known.')
GRAIN_NOTE = (
'> **How to read the numbers.** Every measurement here is in **inches**. '
'Distances (`off 0.25 in`) are rounded to the nearest **1/8 in**, which '
'is the finest tolerance this tool works to anywhere — dimensions '
'are checked at 3/8 in and interference at 1/8 in. Two decimals are '
'printed as asked, but nothing here is measured to 0.01 in, and two '
'donors that differ by less than 1/16 in are ranked as **equal** rather '
'than separated by noise. `exact` is the one exception: it is not '
'rounded, and it means measured-identical to within 1/32 in. Positions '
'and strip widths come from geometry cached to 0.036 in.')
CAP_NOTE = (
'> **A proposal may name at most {0} donors**, by ruling. When '
'{0} cannot account for every feature, this tool returns the best {0} '
'and **names what they miss** rather than growing to a fifth. A feature '
'left out by the cap is not a feature nobody has — somebody in the '
'pool covers it, and it is listed separately below.')
TITLE = 'Reinforcing Suggestion'
# ------------------------------------------------------------- small format
def length_in(value_ft, places=2):
"""A stored length -- FEET IN -- printed as inches. Or an em dash.
This is the converting formatter. Give it a coordinate or a strip width
off a feature record, which are feet. Do NOT give it a `covers` distance:
those are already inches and would come out twelve times too big.
"""
got = core.inches(value_ft)
if got is None:
return '—' if value_ft is None else str(value_ft)
return ('{0:.' + str(places) + 'f} in').format(got)
def distance_in(value_in, places=2):
"""A `covers` distance -- ALREADY INCHES -- printed. Or an em dash.
The non-converting formatter, and the reason the two are named apart:
`reinforcebind.covers` crossed the unit seam already, and converting a
second time here is exactly the 12x mistake this project has made before.
"""
if value_in is None:
return '—'
try:
return ('{0:.' + str(places) + 'f} in').format(float(value_in))
except (TypeError, ValueError):
return str(value_in)
def rect(box):
"""A feature's rectangle as `x0..x1 x y0..y1`, absolute INCHES.
The box arrives in feet, straight off the record; every one of the four
numbers converts here through `length_in`'s helper.
"""
if not box:
return '—'
x0, x1, y0, y1 = box
return '{0}..{1} x {2}..{3}'.format(
_n(x0), _n(x1), _n(y0), _n(y1))
def _n(value_ft, places=2):
"""One coordinate of a rectangle: feet in, bare inches out (no unit --
`rect` carries the unit once, in the column heading)."""
got = core.inches(value_ft)
if got is None:
return '?'
return ('{0:.' + str(places) + 'f}').format(got)
def feature_label(row):
"""What a target feature IS, in words an engineer can point at.
A strip is named by what bounds it, because that is the whole definition
of a ligament -- there is no radius or neighbourhood involved.
"""
kind = (row or {}).get('kind')
if kind == OPENING:
return 'opening'
if kind not in (STRIP_X, STRIP_Y):
return str(kind or 'feature')
across = 'along length' if kind == STRIP_X else 'across height'
if (row or {}).get('between') == VOID_EDGE:
edge = {'x0': 'left end', 'x1': 'right end',
'y0': 'bottom', 'y1': 'top'}.get(row.get('edge'), 'edge')
return 'strip, opening to {0} ({1})'.format(edge, across)
return 'strip between two openings ({0})'.format(across)
def short_label(row):
"""The three-word version of `feature_label`, for a tally column."""
kind = (row or {}).get('kind')
if kind == OPENING:
return 'opening'
if kind not in (STRIP_X, STRIP_Y):
return str(kind or 'feature')
if (row or {}).get('between') == VOID_EDGE:
return 'strip to an edge'
return 'strip between openings'
def mark(name, master_names=None, approximate=False,
same_elevation_names=None):
"""A donor name with its marks. Never silently drops one."""
out = str(name)
if approximate:
out += '*'
if name in set(master_names or ()):
out += MASTER_MARK
if name in set(same_elevation_names or ()):
out += ELEVATION_MARK
return out
def exactness(row):
"""`exact` / `exact*` / `off 0.25 in` / `--`.
The star is not decoration. An approximated outline can never be reported
as a measured exact match -- see the module docstring.
"""
if not row or row.get('donor') is None:
return '--'
star = '*' if row.get('approximate') else ''
if row.get('exact'):
return 'exact' + star
dist = row.get('distance')
if dist is None:
return 'covered' + star
# A non-zero distance that rounds to 0.00 in must not print as `off
# 0.00 in`, which reads as exact to everyone who has ever seen a rounded
# number. Only `exact` means zero. In practice the production `covers`
# floors every non-exact distance at 1/32 in, which prints as 0.04 in, so
# this branch only fires for a caller that supplies its own comparison --
# and it must still be right for that caller.
if 0.0 < dist < 0.005:
return 'off <0.01 in' + star
return 'off {0}{1}'.format(distance_in(dist), star)
# ------------------------------------------------------- the index and when
def index_line(cache, refresh_report, rebuilt_all, refreshed, offline=False):
"""One line saying how current the answer is. Always printed.
The build stamp is not a nicety: the staleness signature cannot see a
geometry edit that leaves the member set alone, so a reader who does not
know when the index was built cannot judge whether that matters to him.
Offline it is the ONLY thing standing between the reader and a snapshot of
unknown age, so `offline=True` says so in the same line rather than
leaving it to a footnote.
The donor count is the count of ELIGIBLE records, not of records. Since
schema 5 the file also holds in-scope pieces measured so they can be
targets, and calling those donors would overstate the pool that was
actually searched by exactly the number of pieces nobody may copy from.
"""
built = (cache or {}).get('built_at') or 'never'
pieces = (cache or {}).get('pieces') or {}
donor_count = 0
for rec in pieces.values():
rec = rec or {}
if core.is_eligible(rec.get('reinforced'), rec.get('on_hold')):
donor_count += 1
others = len(pieces) - donor_count
if offline:
how = ('read from the snapshot on disk; the model was never opened, '
'so nothing was re-checked against it')
elif rebuilt_all:
how = 'FULL REBUILD this press'
elif refreshed:
built_now = (refresh_report or {}).get('built')
how = 'refreshed this press ({0} piece(s) rebuilt)'.format(
0 if built_now is None else built_now)
else:
how = 'read from cache, nothing needed rebuilding'
tail = ('' if not others else
' (+ **{0}** piece(s) measured as possible targets, which may not '
'donate)'.format(others))
return ('Index: **{0}** indexed donor(s){1}, built **{2}** &mdash; {3}.'
.format(donor_count, tail, built, how))
# The exact sentence an offline answer opens with. Short on purpose: it is the
# one line a reader must not be able to skim past, and it says the two things
# that matter -- that this is a snapshot, and that the model was not consulted.
OFFLINE_BANNER = 'OFFLINE &mdash; answered from the snapshot, not the model'
def offline_banner(cache):
"""The offline headline, with the snapshot's age attached to it.
The age belongs HERE rather than only in `index_line`, because offline the
build stamp is not a detail about the index -- it is the date of the
answer. Everything below is as true as the model was at that moment and no
truer.
"""
built = (cache or {}).get('built_at') or 'never'
return '**{0}.** Index built **{1}**.'.format(OFFLINE_BANNER, built)
def staleness_note(rebuilt_all, offline=False):
"""The known limitation, said out loud unless it has just been ruled out.
KNOWN LIMITATION. The change signature is
`reinforced | on_hold | master | member-count | checksum(member ids)`. It
sees a member added, removed or swapped, and a piece typed up as MASTER;
it does NOT see an opening moved or resized in place, or a panel
stretched, because those leave the member set alone. Only a full rebuild
does.
OFFLINE IS A STRICTLY WEAKER POSITION and gets its own words. In Revit the
cheap signature pass runs on every press, so the first half of that list
IS caught. With no model open nothing can be recomputed at all, so both
halves are invisible: not one of the model's assemblies was looked at, and
a piece added, deleted, reinforced or typed MASTER since the build is as
invisible as an opening dragged two feet left. The only remedy is to open
the model and refresh, and the note says so instead of offering a
shift-click that means nothing at a command line.
"""
if offline:
return ('> **Nothing here was checked against the model.** This answer '
'is built entirely from the index file, which was written the '
'last time somebody refreshed it inside Revit. Anything that '
'has changed since then is invisible: an opening moved or '
'resized, a panel stretched, a piece added or deleted, a piece '
'newly reinforced, newly held or newly typed MASTER. In Revit '
'the first of those is the only blind spot, because the cheap '
'signature pass catches the rest on every press; offline there '
'is no pass and no model, so all of them are blind. To rule '
'any of it out, open the model and refresh the index (a full '
'rebuild of this model measured ~10-16 s).')
if rebuilt_all:
return ('Every donor was re-measured from the model on this press, so '
'nothing here is stale.')
return ('> **The index may be stale in one specific way.** It notices a '
'member added, removed or swapped, a piece newly reinforced, a '
'piece newly typed MASTER, and a piece put on or taken off hold. '
'It does **not** notice an '
'opening moved or resized in place, or a panel stretched &mdash; '
'those leave the member set unchanged and the cached measurement '
'standing. If anyone has been editing panel geometry, '
'**shift-click this button** to force a full rebuild before '
'trusting the answer.')
def coverage_note(info):
"""What the index could not see, in the caller's own terms.
A search over a partial index that does not say what it missed is
claiming a completeness it does not have.
"""
out = []
unjudged = (info or {}).get('coverage_unjudged') or []
if unjudged:
out.append(
'> **{0} indexed piece(s) could not be measured** and took no part '
'in this search. They are not "pieces with nothing to offer" '
'&mdash; nobody knows what they have: {1}'
.format(len(unjudged), ', '.join(sorted(unjudged)[:20]) +
(' ...' if len(unjudged) > 20 else '')))
if (info or {}).get('audited') is False:
out.append(
'> **The outline audit has never been run on this model**, so no '
'piece is flagged as having an outline the index cannot see. That '
'is "nobody checked", not "every outline is a clean rectangle". '
'Shift-click to run it.')
return out
# ---------------------------------------------------------------- the target
# ------------------------------------------------------ the design refusal
#
# THE ONE RULE ON THIS PAGE THAT REFUSES RATHER THAN RANKS.
#
# Owner, 2026-09-01: "选择母版的目标,必须得是跟自己的design number一样的panel。
# 我刚在一个WP2.0上面跑了reinforce,给我俩WP1.0的母版,压根用不了。" A donor of
# another design is unusable, not merely worse -- so when there is none of the
# right design this report says so and offers NOTHING. No nearest-design
# fallback, not even a labelled one: a label is precisely what a reader in a
# hurry skips, and the page would then look exactly like an answer.
#
# Every one of these four says the same three things, because that is what
# makes a refusal actionable rather than annoying: WHICH DESIGN was wanted,
# WHAT WAS ACTUALLY THERE (counted, so the reader can tell "the index is
# empty" from "this job has no second WP2 yet"), and THE WAY OUT.
DESIGN_RULE_NOTE = (
'A template is only usable if it carries the **same design number** as '
'the piece being reinforced &mdash; owner ruling, 2026-09-01. That is a '
'**filter**, not a preference: this tool will not offer a donor of '
'another design, and will not offer one labelled as such either. Packet '
'suffixes do not split a design: `{0}.0` and `{0}.A` are both design '
'`{0}`.')
# The design the suffix rule is illustrated with when the target's own could
# not be read. A placeholder mark, per this repository's convention -- never a
# design number from a job, and never one invented to look like this job's.
DESIGN_RULE_EXAMPLE = 'PNL1'
def designs_seen_text(seen):
"""`WP1 (300), WP3 (110), unreadable (2)` -- biggest first, then by name.
An empty tally reads as "none", never as an empty string: a sentence that
trails off after 'the designs seen were' is a sentence a reader fills in
for himself.
"""
seen = seen or {}
if not seen:
return 'none'
order = sorted(seen, key=lambda k: (-seen[k], k))
return ', '.join(
'{0} ({1})'.format('unreadable' if k == core.DESIGN_UNREADABLE else k,
seen[k])
for k in order)
def design_refusal(info):
"""(sentence, [paragraph, ...]) when the design rule refuses, else None.
Kept out of `verdict()` on purpose: `verdict` reports what a SEARCH found,
and none of these ran a search. They belong with the other could-not-judge
answers, which is `target_block`'s half of the page -- so the first return
is a **bold sentence** in the shape the other refusals there use, not a
heading. `render` prints `## NOT JUDGED` above it.
"""
design = (info or {}).get('design') or {}
if not design.get('refused'):
return None
name = (info or {}).get('target') or '?'
want = design.get('target_key')
raw = design.get('target')
considered = design.get('considered')
candidates = design.get('candidates')
status = design.get('status')
if status == core.DESIGN_INDEX_NOT_RECORDED:
return ('**This index predates design-number recording, so no donor '
'could be checked against {0}.**'.format(name),
['The index does not carry the field at all. Nothing was '
'judged and nothing is offered &mdash; an index that never '
'recorded the field is "nobody looked", **not** "every piece '
'matches".',
'**Re-run the snapshot.** Shift-click this button with the '
'model open to rebuild the index from scratch, then try '
'again.',
DESIGN_RULE_NOTE.format(want or DESIGN_RULE_EXAMPLE)])
if status == core.DESIGN_INDEX_UNREAD:
return ('**No indexed piece carries a design number, so no donor '
'could be checked against {0}.**'.format(name),
['All {0} candidate piece(s) &mdash; of {1} in the index '
'&mdash; carry an **empty** design number. Nothing was read; '
'this is **not** "they all match".'
.format(candidates if candidates is not None else '?',
considered if considered is not None else '?'),
'**Re-run the snapshot.** The index was built without a '
'knowledge layer, or this plant has not mapped '
'`parameters.panel.design_number`. Shift-click this button '
'with the model open to rebuild it. If the numbers are still '
'empty afterwards the role is genuinely unmapped &mdash; map '
'it in the plant knowledge file.',
DESIGN_RULE_NOTE.format(want or DESIGN_RULE_EXAMPLE)])
if status == core.DESIGN_TARGET_UNKNOWN:
return ('**{0} has no readable design number, so nothing could be '
'matched to it.**'.format(name),
['This piece\'s own design number reads {0}, which the '
'nomenclature pattern could not turn into a design. Nothing '
'can be filtered against a design nobody knows, so nothing '
'was judged and nothing is offered.'
.format('`{0}`'.format(raw) if raw else '**empty**'),
'Check the piece carries a value for the '
'`panel.design_number` role, and that '
'`nomenclature.design_number_re` reads the way this job '
'writes them. The designs the pool does carry: {0}.'
.format(designs_seen_text(design.get('designs_seen')))])
# DESIGN_NONE_MATCH -- everything was read, and the answer is no.
return ('**No donor with design number {0} exists in this index, so {1} '
'was not judged.**'.format(want, name),
['{0} is design **{1}**. Of {2} indexed piece(s), {3} got as far '
'as the design test and **not one of them is {1}**. The designs '
'they do carry: {4}.'
.format(name, want,
considered if considered is not None else '?',
candidates if candidates is not None else '?',
designs_seen_text(design.get('designs_seen'))),
DESIGN_RULE_NOTE.format(want),
'**The way out:** detail one {0} piece by hand. The moment it is '
'marked `TICKET_IS_REINFORCED` and the index is rebuilt, it '
'becomes the template for every other {0} on the job. If you '
'expected a {0} donor to exist, check the design number on the '
'piece you have in mind &mdash; and check the rejection table '
'below, in case it was dropped for scope or thickness before the '
'design test ever saw it.'.format(want)])
# ------------------------------------------------- the featureless target
#
# Owner, 2026-09-01, after being handed a report that said nothing about it:
# "如果扫描到目标没有opening,那么就找另一块没有opening的板子给target当doner
# ... 这件儿不应该你告诉我,应该reinforce告诉我。" A target measured to have no
# openings used to read as "NOT JUDGED" and stop there -- true as far as it
# went (there is nothing to set-cover), but silent about the search that
# `reinforcebind.shape_search_for` now actually runs. This is that search,
# in words: what it means that the pool inverted, what it found, and -- the
# owner's own next sentence -- that the honest answer can be "these are
# indistinguishable" rather than a false ranking.
def shape_search_lines(name, search):
"""Paragraphs for `target_block`'s `no_features` case. `search` is
`reinforcebind`'s `shape_search_for` result, or None if it never ran
(an info dict built by hand, or a caller that predates this)."""
if not search or not search.get('ran'):
return []
eligible = search.get('eligible') or []
secondary = search.get('secondary') or []
considered = search.get('considered')
lines = [
'> **The search inverted.** A donor that has no ligaments has '
'nothing to offer a target that has openings to cover -- that is '
'why `require_features` drops one. But {0} has no openings either, '
'so the rule reads the other way here: a panel with no openings IS '
'the right donor for a panel with no openings, and the pool below '
'is exactly that search, not a gap where one used to be. Design '
'number, thickness, scope and self-exclusion all still apply -- '
'only the featureless rule inverted.'.format(name)]
if not eligible and not secondary:
lines.append(
'> **Nobody survived the filter.** Of {0} piece(s) considered, '
'not one is the same design and thickness as {1} -- featureless '
'or not. There is no donor to propose; detail this piece by '
'hand.'.format(considered if considered is not None else '?',
name))
return lines
if not eligible:
lines.append(
'> **No donor in the pool is also featureless.** {0} donor(s) '
'of the right design and thickness DO have openings of their '
'own. Their reinforcing around those openings has nothing to do '
'with this target, but the rest of the cage may still be a '
'usable template, so they are listed below rather than dropped '
'-- ranked last, because a featureless twin would have been the '
'better answer and there is not one here.'.format(len(secondary)))
return lines
if search.get('degenerate'):
lines.append(
'> **{0} featureless donor(s) found, and they cannot be told '
'apart on shape.** Same design, and thickness, length and width '
'equal within this tool\'s own measuring tolerance. Listed below '
'in name order -- that is alphabetical, not a ranking; nothing '
'here says one fits better than another.'.format(len(eligible)))
else:
lines.append(
'> **{0} featureless donor(s) found**, ranked by thickness, '
'then length, then width -- the only shape left to compare once '
'there are no ligaments to measure.'.format(len(eligible)))
if secondary:
lines.append(
'> A further {0} donor(s) of the right design and thickness DO '
'have openings of their own, listed after the featureless ones. '
'Their own opening reinforcing is irrelevant here, but the rest '
'of the cage may still be worth a look.'.format(len(secondary)))
return lines
def shape_search_rows(search):
"""One row per donor the inverted search considered, featureless tier
first -- `shape_search_for` already ranked within each tier."""
if not search:
return []
out = []
for row in (search.get('eligible') or ()):
out.append(_shape_row_cells(row, 'no openings'))
for row in (search.get('secondary') or ()):
out.append(_shape_row_cells(row, 'has openings'))
return out
def _shape_row_cells(row, openings_label):
return [
mark(row.get('name'), [row.get('name')] if row.get('master') else ()),
openings_label,
distance_in(row.get('thickness_diff_in')),
length_in(row.get('length_diff_ft')),
length_in(row.get('width_diff_ft')),
row.get('design_number') or '?',
]
SHAPE_SEARCH_COLUMNS = ['donor', 'openings', 'thickness diff', 'length diff',
'width diff', 'design']
def target_block(info, reinforced=None, on_hold=None):
"""What the button can say about the piece the engineer picked.
`reinforced` / `on_hold` are the target's own flags, 1 / 0 / None, read
off the assembly by the caller. They do not disqualify a target -- the
piece being reinforced is normally neither -- but a target that is
already reinforced, or on hold, is worth saying out loud rather than
answering as though it were an ordinary request.
"""
status = (info or {}).get('target_status')
name = (info or {}).get('target') or '?'
lines = []
if status == 'not_in_model':
return ('**{0} is not in this model.** Nothing was judged.'
.format(name), lines, False)
if status == 'not_in_index':
# OFFLINE ONLY, and deliberately not the sentence above. With no model
# open nobody can say whether the piece exists; all that is known is
# that the snapshot does not hold it. Three reasons, all worth acting
# on differently, so all three are named.
return ('**{0} is not in the index.** Nothing was judged &mdash; and '
'this is **not** "the piece does not exist": with no model '
'open nothing here can tell. The snapshot does not hold it, '
'which happens when the piece is outside the declared scope, '
'when it was added to the model after the index was last '
'built, or when the index was built by a version that stored '
'donors only. Open the model and refresh the index to find '
'out which.'.format(name), lines, False)
if status == 'no_target':
return ('**No target piece was resolved.** Nothing was judged.',
lines, False)
if status == 'unjudged':
return ('**{0} could not be measured, so nothing was judged.** This '
'is not "no suggestions" &mdash; its geometry would not read, '
'so the search never ran. Check the piece has a structural '
'framing member with readable concrete geometry.'.format(name),
lines, False)
if status == 'no_features':
# THE OWNER'S OWN COMPLAINT, ANSWERED HERE: "这件儿不应该你告诉我,应该
# reinforce告诉我。" A featureless target used to stop at the sentence
# below. It still says that sentence first -- it is still true, there
# is still nothing to set-cover -- but it no longer stops: whatever
# `shape_search_for` found is appended, so the pool that inverted for
# exactly this target is not left unsaid a second time.
lines = list(lines) + shape_search_lines(
name, (info or {}).get('shape_search'))
return ('**{0} was measured and has nothing to reinforce.** No '
'openings, and therefore no strips of concrete between them. '
'That is a measured answer, not a failure to look.'
.format(name), lines, False)
# LAST, and after the measurement answers on purpose. A piece whose
# geometry would not read is a different, more fundamental finding than a
# piece measured fine that has no same-design donor, and the reader needs
# the first one first.
refusal = design_refusal(info)
if refusal is not None:
return (refusal[0], list(refusal[1]), False)
n = len((info or {}).get('target_features') or [])
head = '**{0}** &mdash; {1} feature(s) to cover.'.format(name, n)
if (info or {}).get('target_approximate'):
lines.append(
'> **This target\'s own outline is approximated.** Its real shape '
'is not the rectangle the index stores, so its edge conditions '
'are not measured. Nothing about its edges may be called exact.')
if reinforced == 1:
lines.append(
'> This piece is already marked `TICKET_IS_REINFORCED`. It is '
'being used as a target anyway, as asked &mdash; but it is also '
'a donor for everyone else, so check you meant this one.')
if on_hold == 1:
lines.append(
'> This piece carries `ON_HOLD = 1`. A held piece may not be a '
'donor; nothing stops it being a target, but its own reinforcing '
'is not settled.')
return head, lines, True
# --------------------------------------------------------------- the verdict
def verdict(suggestion, info):
"""(headline, [paragraph, ...]) for the result of an actual judgement.
`suggestion is None` never reaches here: that case is the target block's,
and it means the target could not be judged, not that nothing was found.
"""
if suggestion is None:
return ('Nothing was judged.', [])
n = suggestion.target_feature_count
donors = (info or {}).get('donors')
considered = (info or {}).get('considered')
if suggestion.status == match.NO_DONORS:
# A COLD DONOR DB is a distinct flavour of NO_DONORS: the index holds
# no pieces at all, so there was nothing to filter, let alone judge.
# The fix is "run a Snapshot", not "no donor matches" -- and it must
# NEVER read as a silent all-clear, which is the cardinal sin here.
if (info or {}).get('cold_db'):
reason = (info or {}).get('cold_reason')
why = (' The previous index was discarded (**{0}**), so it has to '
'be rebuilt.'.format(reason) if reason else
' No snapshot has populated it yet.')
return ('NOT JUDGED &mdash; the donor DB is cold, run a Snapshot '
'first',
['The donor index holds **no pieces at all**, so **not one '
'comparison was made** -- this is not "no donor matches" '
'and it is not "all clear".' + why,
'Press **Snapshot** to index the model, then run this '
'again. Until then, {0} feature(s) of the target remain '
'un-judged.'.format(n)])
return ('NOT JUDGED &mdash; no eligible donor was offered',
['Of {0} indexed piece(s), **none** survived the donor filter, '
'so **not one comparison was made**. This is not "no piece '
'matches"; nothing was examined. The rejection table below '
'says why each one was dropped.'
.format(considered if considered is not None else '?'),
'{0} feature(s) of the target were never looked at.'
.format(n)])
if suggestion.status == match.NO_COVERAGE:
return ('JUDGED &mdash; no donor covers anything',
['{0} donor(s) were compared against all {1} target '
'feature(s) and **not one of them covers a single feature**. '
'This is a finding, not a gap in the search.'
.format(donors if donors is not None else
suggestion.donor_count, n)])
if suggestion.status == match.CAPPED:
reached = n - len(suggestion.capped_out)
return ('CAPPED &mdash; {0} donor(s) account for {1} of {2} '
'feature(s)'.format(suggestion.donors_needed, reached, n),
['Every feature of this piece **is** covered by somebody in '
'the pool, but no set of {0} donors or fewer reaches all of '
'them, and a proposal may name at most {0}. The {1} '
'feature(s) left out are listed under "Left out by the '
'{0}-donor cap" &mdash; a donor for each one exists; go and '
'find it, or accept these and detail the rest by hand.'
.format(suggestion.donor_cap, len(suggestion.capped_out))])
covered = n - len(suggestion.uncovered) - len(suggestion.capped_out)
if suggestion.status == match.COMPLETE:
return ('COMPLETE COVER &mdash; {0} donor(s) account for all {1} '
'feature(s)'.format(suggestion.donors_needed, n),
['{0} cover(s) of that size were found; the {1} best are '
'below. Fewest donors first, then most exact.'
.format(suggestion.cover_count, len(suggestion.proposals))])
lines = ['This is an answer, not a failure. The donors below cover {0} of '
'the {1} feature(s); {2} are covered by **no indexed donor at '
'all**, in any combination, and are listed under "Nobody has '
'these".'.format(covered, n, len(suggestion.uncovered))]
if suggestion.capped_out:
lines.append(
'A further **{0} feature(s) are covered by somebody**, but not '
'within the {1}-donor cap a proposal is allowed. Those are a '
'different problem from the ones nobody has, and they are listed '
'separately.'.format(len(suggestion.capped_out),
suggestion.donor_cap))
return ('PARTIAL COVER &mdash; {0} donor(s) account for {1} of {2} '
'feature(s)'.format(suggestion.donors_needed, covered, n), lines)
# ----------------------------------------------------------------- the rows
def proposal_rows(suggestion, master_names=None, same_elevation_names=None):
"""One row per proposal: the donor set and how well it does.
`master_names` / `same_elevation_names` may be left out and the table
still reads correctly -- the proposals carry their own lists. They are
accepted so a caller that already has `info['master_donors']` /
`info['same_elevation_donors']` need not reach into a Proposal.
"""
rows = []
for i, p in enumerate(suggestion.proposals if suggestion else ()):
names = set(master_names or ()) | set(p.masters or ())
elev_names = (set(same_elevation_names or ()) |
set(p.same_elevation or ()))
rows.append([
str(i + 1),
', '.join(mark(d, names, same_elevation_names=elev_names)
for d in p.donors),
str(len(p.donors)),
'{0} / {1}'.format(p.covered_count,
suggestion.target_feature_count),
str(p.exact_count),
str(len(p.masters or ())),
str(len(p.same_elevation or ())),
distance_in(p.total_distance),
])
return rows
PROPOSAL_COLUMNS = ['#', 'donors', 'how many', 'features covered',
'exact', 'reviewed (MASTER)', 'same elevation',
'total distance']
def donor_rows(explain_rows, approximate_names=None, master_names=None,
same_elevation_names=None):
"""One row per donor in the shown proposal: what that donor is bringing.
Grouped by donor rather than by feature, because the engineer's next
action is to open one donor's drawing and copy from it, and he needs to
know what he is going there for.
"""
approximate_names = set(approximate_names or ())
master_names = set(master_names or ())
same_elevation_names = set(same_elevation_names or ())
order, by = [], {}
for row in explain_rows or []:
key = row.get('donor')
if key is None:
continue
if key not in by:
order.append(key)
by[key] = {'n': 0, 'exact': 0, 'approx': False, 'master': False,
'kinds': {}}
d = by[key]
d['n'] += 1
if row.get('donor_master') or key in master_names:
d['master'] = True
if row.get('exact'):
d['exact'] += 1
if row.get('approximate') or key in approximate_names:
d['approx'] = True
label = short_label(row)
d['kinds'][label] = d['kinds'].get(label, 0) + 1
out = []
for key in order:
d = by[key]
kinds = ', '.join('{0} x {1}'.format(v, k)
for k, v in sorted(d['kinds'].items()))
out.append([
mark(key, [key] if d['master'] else (), d['approx'],
same_elevation_names),
'MASTER -- reviewed' if d['master'] else 'reinforced only',
str(d['n']),
str(d['exact']),
kinds,
])
return out
DONOR_COLUMNS = ['donor', 'provenance', 'features it covers',
'of those, exact', 'what']
def feature_id_cell(row):
"""The shared-vocabulary feature-ID pairing for one explain row.
'target #k' names the registry id of the TARGET feature's shape;
when a donor covers it, '<-> donor #j' names the donor feature's id too --
equal when the two shapes are identical at the index grain, which is what
lets a reader see the numbering the registry assigned. The ids are an INDEX
only (see `reinforcecore.Registry`): they never rank and never enter
`covers()`; this column just exposes them.
A target shape NO donor ever produced has no registry id and reads 'not in
donor vocabulary' -- the informative "nobody carries this shape" signal,
surfaced as uncovered/PARTIAL elsewhere, never an error and never blank.
"""
tid = row.get('target_feature_id')
left = 'target #{0}'.format(tid) if tid is not None else \
'not in donor vocabulary'
if row.get('donor') is None:
return left
did = row.get('donor_feature_id')
if did is None:
return '{0} &harr; donor (unregistered)'.format(left)
return '{0} &harr; donor #{1}'.format(left, did)
def feature_rows(explain_rows, suggestion=None):
"""One row per TARGET feature. Every feature gets a row, always.
A feature this proposal does not cover still occupies a line, and the
line says which kind of not-covered it is: another donor has it and this
set simply did not use it, or nobody has it at all.
"""
nobody = set((suggestion.uncovered if suggestion else ()) or ())
capped = set((suggestion.capped_out if suggestion else ()) or ())
cap = suggestion.donor_cap if suggestion else None
out = []
for row in explain_rows or []:
i = row.get('target_index')
if row.get('donor') is not None:
got = '{0} #{1}'.format(row['donor'], row.get('donor_index'))
elif i in nobody:
got = 'NOBODY HAS IT'
elif i in capped:
# Somebody has it; the cap is why it is not here. Two different
# findings, two different next actions, never the same words.
got = 'LEFT OUT BY THE {0}-DONOR CAP'.format(cap)
else:
got = 'not in this set (another donor has it)'
note = ''
if row.get('through_unjudged'):
note = 'penetration not judged'
out.append([
str(i),
feature_label(row),
rect(row.get('rect')),
(length_in(row.get('strip_ft'))
if row.get('strip_ft') is not None else '--'),
got,
exactness(row),
note,
feature_id_cell(row),
])
return out
FEATURE_COLUMNS = ['#', 'feature', 'rectangle (in from start of piece)',
'strip width', 'covered by', 'how close', 'note',
'feature-ID']
def uncovered_rows(explain_rows, suggestion):
"""The features NOBODY has. The most useful half of a partial answer."""
nobody = set((suggestion.uncovered if suggestion else ()) or ())
out = []
for row in explain_rows or []:
if row.get('target_index') not in nobody:
continue
out.append([
str(row.get('target_index')),
feature_label(row),
rect(row.get('rect')),
(length_in(row.get('strip_ft'))
if row.get('strip_ft') is not None else '--'),
])
return out
UNCOVERED_COLUMNS = ['#', 'feature', 'rectangle (in from start of piece)',
'strip width']
def capped_rows(explain_rows, suggestion):
"""The features some donor HAS but this proposal could not reach.
A separate table from "Nobody has these" on purpose: the two look
identical on a feature-by-feature line -- uncovered either way -- and they
call for opposite actions. Nobody-has means detail it from scratch.
Capped-out means the donor exists and the engineer can go and open it.
"""
left = set((suggestion.capped_out if suggestion else ()) or ())
out = []
for row in explain_rows or []:
if row.get('target_index') not in left:
continue
out.append([
str(row.get('target_index')),
feature_label(row),
rect(row.get('rect')),
(length_in(row.get('strip_ft'))
if row.get('strip_ft') is not None else '--'),
])
return out
CAPPED_COLUMNS = ['#', 'feature', 'rectangle (in from start of piece)',
'strip width']
def cap_note(suggestion):
"""`CAP_NOTE` filled in, when the cap actually bit. Empty otherwise --
a rule that changed nothing is noise on a page that is already long."""
if suggestion is None or not suggestion.capped_out:
return []
return [CAP_NOTE.format(suggestion.donor_cap)]
def rejection_rows(info):
"""Why the donor pool is the size it is.
A pool that shrank from hundreds to a handful without saying why is not a
filter, it is a silent exclusion.
"""
counts = (info or {}).get('rejected') or {}
why = {
'self': 'the target itself',
'unjudged': 'never measured -- could not be judged, not "has nothing"',
'no_features': 'measured, genuinely no openings and so no strips',
'not_eligible': 'not marked reinforced, or on hold',
'prefix': 'outside the declared scope -- another engineer\'s piece',
'design': 'a DIFFERENT DESIGN NUMBER -- unusable as a template, '
'however well it fits',
'design_unknown': 'its own design number could not be read -- not '
'judged, and never assumed to match',
'design_target_unknown': 'the TARGET has no readable design number, '
'so nothing could be compared to it',
'thickness': 'different panel thickness',
'thickness_unknown': 'thickness could not be compared',
'width': 'panel height outside the allowed difference',
'width_unknown': 'panel height could not be compared',
}
out = []
for key in sorted(counts, key=lambda k: -counts[k]):
out.append([key, str(counts[key]), why.get(key, '')])
return out
REJECTION_COLUMNS = ['rejected for', 'pieces', 'what that means']
def master_note(info, suggestion=None):
"""What the MASTER mark is, and what it is not. Printed once, when it
can matter -- which is whenever the pool contains one, whether or not a
proposal happened to pick it.
Also says the opposite thing out loud when the pool contains NONE. A
reader who sees no daggers has to be able to tell "no reviewed template
was available" from "the mark is not being shown", and only the first is
a fact about this job.
"""
names = list((info or {}).get('master_donors') or [])
pool = (info or {}).get('donors')
if (info or {}).get('master_unjudged'):
return ['> **The reviewed-template check did not run.** This plant '
'has not mapped `parameters.member.identity_description`, so '
'no piece was read for the MASTER marker and the ranking '
'below has no provenance in it at all. That is "nobody '
'looked", **not** "no donor is reviewed". Map the role in '
'the plant knowledge file to turn it on.']
if not names:
if not pool:
return []
return ['> **No donor in this pool is typed MASTER.** Every one of '
'the {0} is marked `TICKET_IS_REINFORCED`, which means '
'somebody drew reinforcing on it and **not** that anybody '
'reviewed it. Nothing below carries a review behind it.'
.format(pool)]
shown = ', '.join(sorted(names)[:20]) + (' ...' if len(names) > 20 else '')
return ['> **Typed MASTER in this pool: {0} of {1} donor(s)** '
'&mdash; reviewed, not merely reinforced: {2}. A MASTER is '
'preferred **only between donor sets that fit equally well**; it '
'never outranks a better fit.'
.format(len(names), pool if pool is not None else len(names),
shown)]
def needs_master_footnote(explain_rows, info=None, suggestion=None):
"""True when a MASTER mark is actually printed somewhere."""
if (info or {}).get('master_donors'):
return True
for row in explain_rows or []:
if row.get('donor_master'):
return True
for p in (suggestion.proposals if suggestion else ()):
if p.masters:
return True
return False
def band_label(band):
"""A geometric elevation band, human-readable: its axis plus the
coordinate range on it, in INCHES -- this module's stated print
convention (see the unit-seam note in `reinforcecore`: a stored
coordinate converts to inches only at the moment of printing, never
earlier). None for a band that is None or malformed -- the same
could-not-judge answer either way.
"""
try:
axis = band.get('axis')
lo, hi = band.get('min'), band.get('max')
except AttributeError:
return None
if axis is None or lo is None or hi is None:
return None
return '{0} @ {1} to {2}'.format(axis, length_in(lo), length_in(hi))
def elevation_signal_label(signal):
"""A target's own elevation signal -- `{'group': ..., 'band': ...}`,
`info['target_elevation']`'s shape -- rendered for a reader.
The group, printed as-is, when the piece carries one (the ACCELERATOR:
a plant's own name for the wall plane, e.g. a drafting sheet). Falls
back to the GEOMETRIC band otherwise -- the floor every piece has an
answer from, or None if neither could be determined. Neither is
privileged over the other in what it MEANS; this only picks which is
more informative to print when both happen to be present.
"""
signal = signal or {}
group = signal.get('group')
if group:
return 'sheet {0}'.format(group)
return band_label(signal.get('band'))
def elevation_source_label(source):
"""`core.ELEVATION_BY_GROUP` / `core.ELEVATION_BY_BAND` -> a short
phrase naming which source decided one donor. None for neither."""
if source == core.ELEVATION_BY_GROUP:
return 'same sheet'
if source == core.ELEVATION_BY_BAND:
return 'same geometry'
return None
def elevation_note(info):
"""What the SAME-ELEVATION mark is, and what it is not. Mirrors
`master_note`: printed whenever elevation could matter, which is
whenever the target's OWN elevation -- by EITHER source -- was
determined at all.
THREE ANSWERS, not two, same discipline `master_note` and
`design_refusal` already use. Neither source could determine the
target's elevation (nobody could be preferred, and that is "nobody
looked" -- not "no donor is on this elevation"); it was determined and
nothing in the pool shares it (a real finding: the tie-break simply had
nothing to prefer); or it was determined and some donor(s) share it,
named the same way MASTER donors are -- each tagged with WHICH source
answered for it (`info['same_elevation_sources']`), because "same
sheet" and "same geometry" are different enough claims that a reader
should not have to guess which one fired.
Elevation has TWO SOURCES since the owner's 2026-09-02 ruling (see
`reinforcecore.elevation_match`) -- geometry is the ALWAYS-ON floor, a
plant-declared grouping parameter (MC's `Tagged_Sheet`) an OPTIONAL
accelerator most jobs do not map. `info['target_elevation']` carries
both halves of what the TARGET itself has; `elevation_signal_label`
turns it back into words.
"""
target = (info or {}).get('target')
target_signal = (info or {}).get('target_elevation') or {}
same = list((info or {}).get('same_elevation_donors') or [])
sources = (info or {}).get('same_elevation_sources') or {}
pool = (info or {}).get('donors')
if not target_signal.get('group') and not target_signal.get('band'):
return ['> **{0}\'s own building elevation could not be determined.'
'** Neither a plant-declared grouping parameter (most jobs '
'map none) nor the geometry answered -- the panel is likely '
'near-square (which dimension is the thickness is '
'ambiguous) or its thickness is not cleanly aligned to a '
'global axis. Nothing in the pool could be preferred on it '
'-- that is "nobody looked", **not** "no donor is on this '
'elevation". Every other tie-break above it (fewest donors, '
'most exact, most reviewed, total distance) still applies '
'in full.'
.format(target or 'This piece')]
label = elevation_signal_label(target_signal) or 'an undetermined signal'
if not same:
if not pool:
return []
return ['> **No donor in this pool shares {0}\'s building elevation '
'({1}).** It is the **last** tie-break -- after fewest '
'donors, most exact, most reviewed and total distance -- so '
'this never excluded a better-fitting donor; there simply '
'was not one on this wall plane to prefer.'
.format(target or 'the target', label)]
shown = ', '.join(
'{0} ({1})'.format(name, elevation_source_label(sources.get(name))
or 'elevation')
for name in sorted(same)[:20]) + (' ...' if len(same) > 20 else '')
return ['> **Same building elevation as {0} ({1}): {2} of {3} donor(s)** '
'&mdash; {4}. It is the **last** tie-break in the ranking: it '
'never outranks a donor that fits better, it only decides who '
'wins among donors everything above it left dead level.'
.format(target or 'the target', label, len(same),
pool if pool is not None else len(same), shown)]
def needs_elevation_footnote(info=None, suggestion=None):
"""True when a same-elevation mark is actually printed somewhere."""
if (info or {}).get('same_elevation_donors'):
return True
for p in (suggestion.proposals if suggestion else ()):
if p.same_elevation:
return True
return False
def needs_footnote(explain_rows, info=None):
"""True when anything printed carries a star and the footnote must show."""
if (info or {}).get('target_approximate'):
return True
if (info or {}).get('approximate_in_pool'):
return True
for row in explain_rows or []:
if row.get('approximate'):
return True
return False
def scope_lines(info):
"""What the DECLARED SCOPE kept out of the donor pool. Always printed.
A pool bounded by a scope and a pool bounded by nothing produce the same
shaped answer, so the report says which it had every time -- and, when
the target itself is outside the scope, that the answer rests on
knowledge nobody has validated for that kind of piece.
"""
scope = (info or {}).get('scope') or {}
if not scope:
return []
out = ['> ' + scope['line']]
if not scope.get('target_in_scope') and scope.get('target_annotation'):
out.append('> ' + scope['target_annotation'])
for note in (scope.get('declaration') or []):
out.append('_{0}_'.format(note))
return out
def read_only_line():
"""Said on every report, because a suggestion tool is exactly the thing
people ask to 'just copy it in for me'."""
return ('This tool **reads** the model and proposes. It writes nothing '
'&mdash; no parameter, no rebar, no copy. A human decides and a '
'human draws it.')
# --------------------------------------------------------- when it goes stale
#
# A caveat that is always printed is a caveat nobody reads. `vibe.foldreport`
# folds the routine ones and opens the ones that bite, so something has to
# decide which this is -- and that decision is a fact about the index, not a
# formatting choice, so it lives here beside the words it governs.
#
# ASCII ONLY IN A SECTION'S title / summary / problem. Those three strings go
# through `foldreport._section_html`, which escapes and does NOT restore
# entities -- an `&mdash;` there renders as the literal text `&mdash;` in the
# panel. Entities belong in the markdown BLOCKS, which are restored. The
# convention across the tab is a plain ASCII `--` in those three fields.
STALE_AFTER_HOURS = 24
def stale_problem(cache, offline=False, rebuilt_all=False, now=None):
"""A sentence when the staleness caveat actually bites. None when routine.
OFFLINE ALWAYS BITES: nothing was checked against the model at all, and
that is the single most important thing on an offline page.
LIVE, a full rebuild rules it out entirely, and an index built minutes ago
by the press the reader is looking at is not a caveat worth opening a
section for -- the standing note is still printed inside it, folded, which
is what the owner asked for. What DOES bite is an index whose stamp is a
day or more old: geometry edited since then is invisible and nothing on
the press catches it.
An index with no stamp at all bites too, and reads as could-not-tell
rather than as fine.
"""
built = (cache or {}).get('built_at')
if offline:
return ('nothing was checked against the model -- built {0}'
.format(built or 'never'))
if not built:
return 'this index carries no build stamp, so its age is unknown'
if rebuilt_all:
return None
hours = _hours_since(built, now)
if hours is None:
return 'the build stamp could not be read: {0}'.format(built)
if hours >= STALE_AFTER_HOURS:
return ('the index is {0} day(s) old -- geometry edited since then is '
'invisible'.format(int(hours // 24)))
return None
def _hours_since(stamp, now=None):
"""Hours between an index stamp and now, or None if it will not parse.
`time.strptime` and a manual difference in seconds rather than
`datetime`: this file loads under IronPython 2.7 inside Revit, where the
fewer library corners touched the better.
"""
import time
try:
then = time.mktime(time.strptime(str(stamp), '%Y-%m-%d %H:%M:%S'))
except (ValueError, TypeError, OverflowError):
return None
return ((time.time() if now is None else now) - then) / 3600.0
# ------------------------------------------------------------- the whole page
#
# THE SHAPE THE OWNER ASKED FOR, 2026-08-30: the target and the donor
# proposals are the page -- biggest, first. Everything else follows, smaller,
# and FOLDED BY DEFAULT, because most of it is information he will not read
# unless something is wrong. A section that has a problem in it opens itself,
# which turns the page into a contract: if nothing below the headline is open,
# nothing needs him.
#
# The rule itself lives in `vibe.foldreport` rather than here. It is not about
# reinforcing, and the same treatment is wanted across the rest of the tab.
#
# WHAT `render` RETURNS IS SECTIONS, NOT A PAGE. Three renderings of the same
# content exist and none of them may reword it: the button prints the headline
# through pyRevit (which can linkify an element id) and the rest as folding
# HTML; the offline CLI prints plain text with the routine sections folded to
# one line each; and both write the whole thing out unfolded for the record.
# Sections are what keep those the same report.
#
# EVERY SENTENCE THE OLD FLAT REPORT PRINTED IS STILL HERE. Folded is not
# gone: a folded section prints its title and what is inside it, and the full
# text is written somewhere the page names.
def render(suggestion, info, explain_rows, cache, model=None,
refresh_report=None, rebuilt_all=False, refreshed=False,
offline=False, reinforced=None, on_hold=None, full_path=None,
now=None, donor_link=None):
"""The report as `vibe.foldreport.Section`s, headline first. Pure.
`explain_rows` is `reinforcebind.explain(suggestion, info, best)`, passed
in rather than computed, so this module stays uncoupled from `bind`.
`full_path` is where the unfolded text was written, if anywhere -- named
on the page so a folded section is never the only copy.
`donor_link(name, record)` returns extra markup for a donor's own line, or
None. It is a parameter because only the button can make one: pyRevit's
`output.linkify` produces a link that selects the assembly in the model,
and there is nothing to click at a command line. Everything else on the
line is identical either way.
"""
from vibe import foldreport as fold
name = (info or {}).get('target') or '?'
rows = explain_rows or []
head = fold.Section('headline', 'Answer', headline=True)
head.md('# {0} &mdash; {1}'.format(TITLE, name))
if offline:
head.md(offline_banner(cache))
if model:
head.md('Model **{0}**'.format(model))
head.md(index_line(cache, refresh_report, rebuilt_all, refreshed,
offline=offline))
target_head, target_lines, judged = target_block(
info, reinforced=reinforced, on_hold=on_hold)
if not judged:
search = (info or {}).get('shape_search')
# A FEATURELESS TARGET IS STILL "NOT JUDGED" IN THE SET-COVER SENSE --
# there is nothing to cover -- but it is not silent any more, so it
# does not get the same bare heading as a piece nobody could measure
# at all. The table is on the headline section (never folded) rather
# than in the folded tail, because this IS the answer the owner asked
# for, not a routine detail.
head.md('## {0}'.format(
'MEASURED -- NO OPENINGS' if search and search.get('ran')
else 'NOT JUDGED'))
head.md(target_head)
# A refusal that names WHY, WHAT WAS THERE and THE WAY OUT is three
# paragraphs, not one. They used to be dropped here -- harmless while
# every not-judged case was a single sentence, and silently lossy the
# moment one was not.
for line in target_lines:
head.md(line)
if search and search.get('ran'):
shape_rows = shape_search_rows(search)
if shape_rows:
head.table(SHAPE_SEARCH_COLUMNS, shape_rows)
return [head] + _tail_sections(suggestion, info, rows, cache, offline,
rebuilt_all, full_path, now)
head.md('## Target')
head.md(target_head)
for line in target_lines:
head.md(line)
verdict_head, verdict_lines = verdict(suggestion, info)
head.md('## {0}'.format(verdict_head))
for line in verdict_lines:
head.md(line)
if suggestion is not None:
head.md('> ' + suggestion.summary())
best = suggestion.best if suggestion is not None else None
if suggestion is not None and suggestion.proposals:
head.md('## Proposals &mdash; fewest donors first, then most exact, '
'then most reviewed, then same elevation')
head.table(PROPOSAL_COLUMNS,
proposal_rows(suggestion, info.get('master_donors'),
info.get('same_elevation_donors')))
head.md('## What each donor brings &mdash; proposal 1')
head.table(DONOR_COLUMNS,
donor_rows(rows, info.get('approximate_in_pool'),
info.get('master_donors'),
info.get('same_elevation_donors')))
for donor in (best.donors if best else ()):
record = ((cache or {}).get('pieces') or {}).get(donor) or {}
head.md(_donor_line(donor, record, donor_link))
return [head] + _tail_sections(suggestion, info, rows, cache, offline,
rebuilt_all, full_path, now)
def _tail_sections(suggestion, info, rows, cache, offline, rebuilt_all,
full_path, now):
"""Everything below the answer. Folded unless it has a problem in it."""
from vibe import foldreport as fold
out = []
info = info or {}
# ---- what nobody has, and what the cap dropped. Always a problem: these
# are the two things on the page that name work somebody has to do.
uncovered = uncovered_rows(rows, suggestion) if suggestion else []
capped = capped_rows(rows, suggestion) if suggestion else []
if uncovered or capped:
gaps = fold.Section(
'gaps', 'What this proposal does not cover',
summary='{0} nobody has, {1} left out by the cap'
.format(len(uncovered), len(capped)),
problem='{0} feature(s) still need somebody'
.format(len(uncovered) + len(capped)))
if uncovered:
gaps.md('**Nobody has these.** No indexed donor covers them, in '
'any combination. This is the part of the piece that has '
'to be detailed from scratch.')
gaps.table(UNCOVERED_COLUMNS, uncovered)
if capped:
gaps.md('**Left out by the {0}-donor cap.** Somebody in the pool '
'**does** cover these; they are not in the proposal above '
'only because a proposal may name at most {0} donors.'
.format(suggestion.donor_cap))
gaps.table(CAPPED_COLUMNS, capped)
for line in cap_note(suggestion):
gaps.md(line)
out.append(gaps)
# ---- feature by feature. The longest table on the page, and the one the
# owner is least likely to read. Routine by construction.
if rows:
out.append(fold.Section(
'features', 'Feature by feature -- proposal 1',
summary='{0} target feature(s)'.format(len(rows)),
blocks=[('md', 'Every feature of the target gets a line, '
'including the ones this set does not cover. '
'Positions are absolute INCHES from the start of '
'the piece &mdash; never a fraction of panel '
'length.'),
('table', FEATURE_COLUMNS,
feature_rows(rows, suggestion))]))
# ---- provenance: the MASTER signal, same-elevation affinity, and
# approximated outlines. A check that did not run is a problem; a check
# that ran and found nothing is not.
prov = fold.Section('provenance', 'Provenance -- reviewed templates, '
'building elevation and approximated '
'outlines')
for line in master_note(info, suggestion):
prov.md(line)
for line in elevation_note(info):
prov.md(line)
if needs_footnote(rows, info):
prov.md(APPROX_FOOTNOTE)
if needs_master_footnote(rows, info, suggestion):
prov.md(MASTER_FOOTNOTE)
if needs_elevation_footnote(info, suggestion):
prov.md(ELEVATION_FOOTNOTE)
if prov.blocks:
prov.summary = '{0} reviewed donor(s) in the pool'.format(
len(info.get('master_donors') or []))
if info.get('master_unjudged'):
prov.problem = ('the reviewed-template check DID NOT RUN -- '
'nobody was read for the MASTER marker')
elif _approximate_in_answer(rows, info):
prov.problem = ('this answer rests on a piece whose outline the '
'index cannot see')
out.append(prov)
# ---- what the index could not see at all.
notes = coverage_note(info)
if notes:
unjudged = len(info.get('coverage_unjudged') or [])
cover = fold.Section(
'coverage', 'What the index could not see',
summary='{0} piece(s) unmeasured'.format(unjudged),
problem=('{0} indexed piece(s) could not be measured'
.format(unjudged) if unjudged else
'the outline audit has never been run on this model'))
for line in notes:
cover.md(line)
out.append(cover)
# ---- the pool: scope and rejections. Routine, unless the target itself
# is outside the scope, which makes every verdict on the page an opinion.
scope = info.get('scope') or {}
pool = fold.Section(
'pool', 'The donor pool -- who was considered, and who was not',
summary='{0} looked at, {1} became candidates'
.format(info.get('considered'), info.get('donors')))
for line in scope_lines(info):
pool.md(line)
rejections = rejection_rows(info)
if rejections:
pool.table(REJECTION_COLUMNS, rejections)
if scope and not scope.get('target_in_scope'):
pool.problem = ('the target is OUTSIDE the declared scope -- these '
'verdicts rest on unvalidated knowledge')
if pool.blocks:
out.append(pool)
# ---- how to read it, and the standing reminder. Never a problem.
howto = fold.Section('howto', 'How to read the numbers, and what this '
'tool will not do',
summary='inches, on a 1/8 in grain; read only')
howto.md(GRAIN_NOTE)
howto.md(read_only_line())
howto.md('**Reminder:** nothing here was written to the model, and the '
'middle band of a panel vertical reinforcing is PER DESIGN '
'&mdash; an engineer calculation for that one piece. It is not '
'a thing to copy across from a donor.')
if full_path:
howto.md('The whole report, nothing folded, was written to `{0}`.'
.format(full_path))
out.append(howto)
# ---- staleness. Folded when it is the standing note; open when it bites.
stale = fold.Section(
'staleness', 'How current this is',
summary='built {0}'.format((cache or {}).get('built_at') or 'never'),
problem=stale_problem(cache, offline=offline,
rebuilt_all=rebuilt_all, now=now))
stale.md(staleness_note(rebuilt_all, offline=offline))
out.append(stale)
return out
def _approximate_in_answer(rows, info):
"""An approximated outline in the proposal being SHOWN, not merely in the
pool. A donor nobody used is not a caveat on this answer."""
for row in rows or []:
if row.get('donor') is not None and row.get('approximate'):
return True
return bool((info or {}).get('target_approximate'))
def _donor_line(name, record, donor_link=None):
"""The donor's own line. The button hands in a linkify that selects the
assembly; with no Revit there is nothing to click, so the id is printed as
text -- which is what a reader types into Revit's Select by ID."""
design = (record or {}).get('design_number')
bits = ['- **{0}**'.format(name)]
if design:
bits.append('design {0}'.format(design))
link = None
if donor_link is not None:
try:
link = donor_link(name, record)
except Exception: # pylint: disable=W0703
link = None
if link:
bits.append(link)
elif (record or {}).get('asm_id'):
bits.append('assembly id {0}'.format(record['asm_id']))
return ' &nbsp; '.join(bits)