8e6ee31596
ci / test (push) Successful in 6m18s
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.
323 lines
16 KiB
Markdown
323 lines
16 KiB
Markdown
# 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.
|