Files
willtools-demo/docs/handoff.md
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

16 KiB
Raw Blame History

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.