WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
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:
+322
@@ -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.
|
||||
Reference in New Issue
Block a user