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.
16 KiB
Handoff
For an agent picking this repo up cold.
Read in this order: README.md for what the tool is and which buttons exist, CLAUDE.md for the rules and the layout, docs/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.