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

History-free snapshot of the private development repository at 77a82eb.
Code, docs and the public catalogue only: no plant or job knowledge
layers, no client, job or producer names (tools/scrub.py clean against
the widened term list; a second word-level grep clean). The intro deck is
left out because its slide images cannot be checked for job drawings.
This commit is contained in:
2026-09-09 08:35:15 -05:00
commit 25592fa6c2
406 changed files with 223435 additions and 0 deletions
+322
View File
@@ -0,0 +1,322 @@
# Handoff
For an agent picking this repo up cold.
Read in this order: [README.md](../README.md) for what the tool is and which
buttons exist, [CLAUDE.md](../CLAUDE.md) for the rules and the layout,
[docs/lift-check.md](lift-check.md) for the mechanics of the handling check,
then this.
**What this file is now.** It was written as the orientation document and it
is no longer the best one for that — the README carries the current button set
and `docs/architecture-migration.md` carries the current state of the code.
What it still holds, and nothing else does, is the **engineering** part: the
ranked open questions, what is knowingly approximate, what the last full sweep
found, and what to pick up first. Read it for that. Where it describes the
shape of the tool, check the README before believing it.
Corrected 2026-08-31; a stale section is marked below rather than deleted.
Nothing here repeats CLAUDE.md. The one rule worth saying twice anyway:
**never reload pyRevit.** It kills Revit outright, our code never runs, and no
amount of care downstream helps. Restart Revit instead.
---
## Where the work stands
**Reorganised 2026-09-05.** `Tools.panel` now holds one pulldown,
`Modeling` (**Reinforce Suggestion**, **Reinforcing QC**, **Ticket QC**), plus
**Snapshot** as the one remaining large icon. **Panel Lift Check** moved onto
`Design.panel`'s `DesignHub` pulldown, alongside **Panel Analysis** — both
finished, grouped there because the owner wants the analysis tools together,
not because either is unfinished. Five set-up commands still live behind the
`Config` pulldown on `Setup.panel` — Project Config, Design Extract, Self
Check, Job Recon, Takeoff. `Gravity`, `Lateral`, `Beam Analysis` and
`Connection Analysis` are the four unfinished rows left on `DesignHub`, and
they are quarantined there with `(WIP)` in their titles. The README's table
is the authority on that list.
**Reinforcing QC** is new: a cut-down Ticket QC for a panel whose reinforcing
is finished but whose ticketing has not started. It shares its engine with
Ticket QC through `lib/vibe/ticketqc_run.py` (a `mode` parameter) and runs
only the `ticketchecks.CHECKS` whose evidence is the panel's modelled
content — see `ticketchecks.REINFORCING_CHECKS` / `REINFORCING_EXCLUDED` for
the check-by-check classification.
Design Extract and Ticket QC are arithmetic against a human-corrected spec,
and they are boring in the way finished things are. **Panel Lift Check has
since joined them**: `docs/architecture-migration.md`'s "Never" table lists it
as already migrated and as the model the whole migration plan copies, and the
batch path, the `--no-recommend` flag and the multi-worker run are all
shipped. (Until 2026-08-31 this section called it "the one under development".
That was a release out of date.)
What follows is still mostly about the handling check, because that is where
the open engineering questions are. It is a PCI Chapter 8 handling check:
stripping,
transportation, erection, with prestress credited and blockouts cutting
strands. It runs in two halves across a language boundary — `vibe.liftexport`
measures the piece in IronPython inside Revit and writes a job file;
`panellift` reads that job file on CPython and does every piece of mechanics.
The job file is kept beside the report because it is the complete input: any
number in any report can be re-run on a machine with no Revit and no licence.
That boundary is the single most useful decision in the repo. Keep it.
Anything about *mechanics* goes on the CPython side where there are tests;
anything about *this model* goes on the Revit side.
## What the check will not tell you
It refuses more than it answers, on purpose -- see "When it will not answer" in
`lift-check.md` for the full list. What remains knowingly approximate:
**The plant's rigging choice is data, not a guess.** Per-stage `rig_mode` is
built (`11d626d`) and the mechanics of `rolling` and `static` are different
solvers with tests. The tool refuses to choose between them: a plant file
that leaves a stage blank inherits the global mode, and the report's load
path column says which solver produced each number.
**Transportation without a dunnage rule.** When a plant's knowledge file has
no `handling.dunnage` entry, transportation still runs the old lift path and
the report says so in a warning box. A plant that states `lifter_rows` or
`fraction` gets a real support solve instead. See question 2.
The full-width section question (old question 3) is settled -- see below.
The direction that matters has not changed: every remaining approximation
below is labelled rather than silent.
---
## Open engineering questions, ranked
### 1. Per-stage rigging mode -- settled mechanically, plant answer outstanding
**Known.** Ours is `rolling` everywhere. A commercial check (Eriksson Wall)
uses Static Lift for stripping and transport and Rolling Block for erection.
On the same piece with the same inserts, ours gives a stripping moment of
**23.8 k-ft** against their **14.1** — a factor of 1.7, and by far the largest
known discrepancy in the tool.
**Suspected.** The whole of that gap is the rigging idealisation, not the
section or the load factor. It is the first thing to fix because every
stripping verdict in the tool is currently conservative by roughly that
factor, which is the fastest way to lose a user's trust: a tool that fails
pieces the plant has been shipping for years gets switched off.
**The mechanism is built** (`11d626d`). `handling.stages[]` carries `rig_mode`
alongside `multiplier`, `pci_case`, `strength` and `groups`; blank inherits
the job's mode, so nothing moved when it landed; anything that is neither
`rolling` nor `static` nor blank is refused rather than quietly inherited.
It is surfaced in the project config file under `decisions.rig_mode`, and the
report's stage table carries a **load path** column so two stages solved on
different mechanics cannot look alike on the page.
**What is left is not code.** Somebody has to say how this plant actually rigs
each stage, and it is deliberately not defaulted to Eriksson's answer — their
combination is their default, not a fact about this yard. Once it is set,
re-run the piece above: if stripping lands near 14.1 k-ft on `static`, the
1.7× is explained and the question is closed.
### 2. Transportation is supports, not a lift -- settled and built
**Settled.** Where the piece sits on the trailer is plant knowledge, read from
`handling.dunnage` in the plant layer:
| mode | meaning |
| --- | --- |
| `lifter_rows` | dunnage under the same lines the stripping rows use |
| `fraction` | two lines inset `fraction * L` from each end |
One plant sits on the stripping rows; another insets a fixed fraction of the
span from each end. The numbers themselves live in the plant knowledge files,
not in this repository.
When either mode is configured, the transportation stage carries a `supports`
list instead of `lifters`. `panellift` solves it as a continuous beam on rigid
supports -- two lines are determinate, more follow stiffness, no sling angle,
no cable groups, no insert capacity check -- and the report shows dunnage
lines and their reactions instead of a pick table. A blank `handling.dunnage`
keeps the old lift path, and the report says so in a warning box; the button
refuses on a misconfigured mode rather than falling back quietly.
**What is left is the plant files themselves.** Each plant needs its own
`dunnage:` block in its plant knowledge file -- one rule may be the
stripping rows, another a fixed fraction -- because that is exactly the
layer that must not be published.
### 3. Effective width when the piece spans its short direction
**Known.** `panellift/src/panellift/section.py` builds the section from the
full panel width minus blockouts, and `statics.py` distributes load across
that same width. There is no reduction anywhere for how far apart the inserts
are.
**The worry was** that this makes a panel look safer than it is. A horizontal
panel erected spans its width, say 11.5 ft, while being 26.8 ft across, and it
hangs from a handful of anchors along one edge rather than off a continuous
line. The strip between two anchors has to carry its load sideways to reach
one, and that transverse bending is nowhere in the model.
**Settled, and no FE was needed.** With a line support every 1-ft strip
carries its own load straight to the support and the full width is right by
inspection; the only question is what the discrete anchors cost. That path is
a beam whose **depth is the span dimension** — which is why it is so stiff
that the answer is not close.
Worked on a real piece, 26.83 × 11.50 ft × 8 in, four erection anchors on the
top row at 3.5 / 10.5 / 16.33 / 24.08 ft:
| | |
| --- | --- |
| worst gap between anchors | 8.0 ft |
| weight per ft of edge | 1,150 lb/ft |
| transverse beam | 138 in deep × 8 in wide, `S` = 25,392 in³ |
| `M = wL²/8` | 9,200 lb-ft |
| transverse tension | **4.3 psi**, against 444 psi allowable |
One percent of allowable. The full width is effective — not "probably", by two
orders of magnitude — and no plate solution changes a number that small.
**Where it does bite: an opening that interrupts the transverse path.** The
one piece with a 9.3 × 8.3 ft blockout has that 143 in depth cut to about
43 in over 9.3 ft of its edge, and the two remaining strips are separated.
That is still roughly ten times the margin, but it is the geometry to redo
this arithmetic on rather than assume. Note that the *main span* section at an
opening is already modelled — `NetSection` — so this is only about the path
sideways to an anchor.
Worth turning into a check rather than leaving as a note in a document; it is
one span, one section modulus, and the numbers are already in the job file.
Related and already known: "Transverse equilibrium" and "Flexural capacity
(`φMn`)" in the Open list at the bottom of `lift-check.md`.
---
## The sweep
`tools/sweep.py` checks every WP assembly in the open model and ranks what is
wrong with it. Two halves:
```
python tools/sweep.py measure in Revit, then check
python tools/sweep.py --check re-check the saved job files, no Revit
```
The measuring half drives `tools/sweep_in_revit.py` through the bridge, ten
pieces a call, and only reads the model. It is resumable — a meta file means
that piece is done — so stop it and start it again freely. Output goes beside
the model in `<model>/_ticketqc/sweep/`, not into this checkout.
**The `--check` half is the one to reach for.** A job file is the complete
input, so after any change to the mechanics the whole model is re-checked in
about a minute with Revit closed. That is what the job files are for.
The stages come from `liftexport.default_stages` — what the grouping rules
imply with nobody having clicked anything. Good enough to find what breaks
across a model, **not** good enough to sign. See the next section.
### What the last full sweep found
228 assemblies. 194 measured cleanly; 34 refused because their design has no
spec yet, so there is no initial tension and prestress cannot be credited —
run Extract Design on those designs and they unblock. Of the 194, **184 are
horizontal** (the assembly origin stands the width up) and 10 are vertical.
The sweep immediately earned itself. Every one of the 184 horizontal panels
failed erection on the aspect-ratio check, which turned out to be a bug rather
than a fact about the panels: the check read `width/length`, `transpose()`
swaps those two to stand a piece up, so the ratio came back inverted — above
1.00 for every piece longer than it is wide — and a 2.33-to-1 panel was
refused as "close to square". Fixed in `2663cbc` by taking short over long,
which has the property the check was missing: turning a piece cannot change
whether it is square.
Behind that, the findings worth chasing:
| | |
| --- | --- |
| **WP139, WP345** | genuinely over the stripping tension limit — 1.42 and 1.24 of allowable, both with `CRACK`. These span their length with prestress credited, so they are the most trustworthy numbers in the sweep. |
| **WP109, WP114, WP121, WP314** | no lifting inserts modelled at all. The check raises rather than refuses politely, which is worth tidying, but the finding is real: WP314 is a full-size piece with nothing to pick it up by. |
| **WP187, WP192, WP313, WP409, WP411** | the weight gate fired — measured geometry does not weigh what the model says. Usually a blockout or a thickness read wrong, and it makes the section modulus wrong in the direction that says a panel is fine. |
| **WP215, WP217, WP318, WP322, WP326** | `INSERT` findings at erection. |
---
## Deliberately not automated
**The user picks the inserts, by hand, in Revit, once per stage.** The button
isolates the assembly in the view they are already in, ghosts everything that
is not an insert to 85% transparency, and asks them to click. It does not
switch views and it does not create one.
This replaced a checklist of rows the tool had grouped itself, and the reason
matters: a row is the tool's *guess* about which inserts share a hook, and its
label named one family when the row held two. Picking three lines where four
were meant is unconservative and the report looks entirely normal — that is
how a panel came back at 184% on three rows when it has four.
`stage_audit` still says when the defaults look wrong, but it is advisory now,
not a veto: with every insert clicked, a bad default costs clicks rather than
an answer.
So: the sweep uses the defaults because 228 pieces cannot be clicked, and its
output is a map of where to look — never a signed check.
## The Eriksson comparison
One piece was checked against Eriksson Wall in full, and the agreement on the
parts that overlap is close:
| | ours | theirs |
| --- | --- | --- |
| stress at the governing station | 417.4 psi | 417.6 |
| compression at stripping | 667.5 psi | 667.4 |
Those are with *their* moment fed into *our* section, which is the comparison
that isolates the section from the rigging — and it is why question 1 above is
about rigging and not about geometry.
The gap that cannot be closed by matching them harder: **Eriksson Wall cannot
model a horizontal panel at all.** Its file format carries one longitudinal
axis for the whole file and has no per-stage span field, so a piece that
spans one way at stripping and the other at erection has nowhere to say so.
Rotating the piece into a second file is unconservative, because strands are
implicitly along the longitudinal axis and would come with it. 184 of the 194
pieces in this model are exactly that case, which is the reason this tool
exists rather than a licence.
Eriksson Sync (the Revit exporter) was also evaluated and is not usable here
as-is: its family map is a template from another job, so families in this
model do not match, strands export as nothing, and the lifters in the output
were invented by a default layout rather than read from the model.
---
## Start here
**1. Get `decisions.rig_mode` and `handling.dunnage` set in each plant's
knowledge.** The fields
exist and work; what is missing are the plants' answers. Rigging first
because it is the
largest known error — a 1.7× factor on every stripping number — and nothing
else is worth calibrating underneath it. This is a question for the plant, not
a task for whoever reads this: ask, do not pick.
**2. Run Extract Design on the designs that have no spec, then
`python tools/sweep.py`.** Second because it costs no design work and turns 34
refusals into 34 checked pieces — including every insulated horizontal panel,
which is a shape the tool has never been run against. Expect it to find
something; that is what happened last time.
**3. Turn section 3's arithmetic into a check.** Third because the engineering
is already settled — the transverse path to a discrete anchor costs about 1%
of allowable on a solid panel — and what is left is making the tool say so on
the pieces where it might not hold, which is the ones with a large opening
across the anchor line. One span, one section modulus, both already in the
job file.
Do not start by adding features. `span`/`transpose` is the most load-bearing
logic in the tool and until the last sweep it had only really been exercised
on one piece.