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.
912 lines
63 KiB
Markdown
912 lines
63 KiB
Markdown
# Plan — Strands Auto Layout, a design-summary pattern checked against every piece that carries the design
|
||
|
||
**Status: proposed. Nothing here is built. No module was changed to write it.**
|
||
**No code was written and nothing was committed.**
|
||
|
||
The owner's shape, in his words:
|
||
|
||
> "多加一个按键,在design hub底下,叫strands auto layout. 大致就是用户设计一个
|
||
> design summary, 里面有所有的strand pattern, 按下以后读这个strand pattern,在所
|
||
> 有的这个design number的framing element 底下,找这个strand pattern可能造成的问
|
||
> 题。比较常见的是干涉,或者距离洞/concrete edge太近太远。所以需要用户额外输入
|
||
> max_deflection, bed_offset, max_bed_width, min_clear_cover 等等的参数。"
|
||
|
||
> "这玩意儿我预计一旦跑起来得跑半天,所以你先写写文档啥的"
|
||
|
||
That second sentence is why this file exists before any code, and it is also a
|
||
design input, not an aside: a half-day run is a run that will be interrupted,
|
||
resumed, and overtaken by an edit to the model it is measuring. Everything in
|
||
§6 follows from it.
|
||
|
||
---
|
||
|
||
## 0. Names
|
||
|
||
This repository carries no plant or client name. The placeholders are the ones
|
||
`docs/plan-panellift-three-stage.md` §0 already uses, and CLAUDE.md's table:
|
||
|
||
| placeholder | what it means here |
|
||
| --- | --- |
|
||
| `plant_a` | the producer in production use today. Wall panels. |
|
||
| `plant_b` | the second producer, being onboarded. No job yet. |
|
||
| `sample_job` / `sample_client` | a job / a client, where one must be named |
|
||
|
||
**Plant numbers are cited by KEY PATH and never repeated here.** `strand.deflection.max_in`
|
||
names a value that lives in one file with a manual page cite beside it; writing
|
||
the number into a public document creates a second home for it, and two homes
|
||
for one number is how they drift (`plan-panellift-three-stage.md` §4.3, and the
|
||
`lifters.capacity_lb` incident of 2026-08-31 that argument is drawn from). Every
|
||
threshold below is a path or a question, never a figure.
|
||
|
||
---
|
||
|
||
## 1. What the feature does
|
||
|
||
**Strands Auto Layout** takes the strand pattern an engineer has authored on a
|
||
design summary — the `reinforcing.strand_layout` matrix in
|
||
`knowledge/job/<job>/specs/<design>.yaml`, which already exists and is already
|
||
hand-correctable — and asks, for **every framing element in the model carrying
|
||
that design number**, what problems that one pattern would cause on *that*
|
||
piece. The pieces of one design are not identical: they share a section and a
|
||
detail package, and they differ in width, length and openings (the spec's own
|
||
`envelope.length` / `envelope.width` bands say so), so a pattern that is right
|
||
on the design sheet can still be wrong on a particular piece. It reports
|
||
interference between a strand and anything else cast into the piece, strands
|
||
too close to or too far from an opening or the concrete edge, strands landing
|
||
where the casting bed has no hole, strand runs too short to develop, and the
|
||
debond or deflection those conditions call for. It **writes nothing to the
|
||
model** and produces a per-piece report plus one roll-up. It is expected to run
|
||
for hours, so it is resumable, per-piece, on disk.
|
||
|
||
---
|
||
|
||
## 2. What already exists, and is reused rather than rebuilt
|
||
|
||
Nothing in §2 is new work. It is listed because a design that quietly rewrites
|
||
any of it introduces a second answer to a question this project has already
|
||
answered once.
|
||
|
||
### 2.1 "Every piece with this design number"
|
||
|
||
`ticketqc.assemblies_by_design(doc, know)` already returns `{design key ->
|
||
[AssemblyInstance]}`, keyed on the **reduced** design number, read off the
|
||
assembly's **Structural Framing** member — which is exactly the "framing
|
||
element" the owner names. The reduction is `specformat.design_key`, this
|
||
codebase's one answer to "are these the same design", and it carries the
|
||
project's nomenclature ruling: **design number = sheet number; a packet page
|
||
suffix and a `*.A` note are not separate designs** (`PNL1`, `PNL1.0`, `PNL1.A`
|
||
and `PNL1.B1` all reduce to `PNL1`), and it does **not** merge `PNL1` with
|
||
`PNL2`. The pattern is plant knowledge (`nomenclature.design_number_re`), and
|
||
an unreadable number is `None`, which is **could not read**, never "no design"
|
||
— `reinforcebind.DonorFilter._design_reject` states the rule that two `None`s
|
||
are not a match, and the same rule holds here.
|
||
|
||
`reinforcebind.DonorFilter(match_design=True)` is the same rule as a filter.
|
||
This tool does not need the donor half of it, but it must not re-implement the
|
||
design comparison: it calls `specformat.design_key` and nothing else.
|
||
|
||
**View names do not matter.** The design is read off the framing member's
|
||
parameter, resolved by role (`panel.design_number`), never off a view name.
|
||
|
||
### 2.2 The design summary, and where the strand pattern already lives
|
||
|
||
`specformat.SPEC_TEMPLATE` already carries the pattern in full:
|
||
|
||
```
|
||
reinforcing.strand_layout:
|
||
datum 'left_rail' | 'right_rail' | 'centerline'
|
||
columns_in strand positions across the piece, cumulative from the datum
|
||
rows [{from_bottom_in, strands: [...]}] — one per layer through the
|
||
thickness, marking which columns it fills at that height
|
||
total_in the chain total; should equal the piece width
|
||
note
|
||
reinforcing.strand_groups[] {qty, dia_in, grade_ksi, it_lb, role, note}
|
||
```
|
||
|
||
plus `envelope`, `source.extracted` / `source.extractor` /
|
||
`source.reviewed_by` / `source.reviewed_on` (**null until a person checks it**),
|
||
`unresolved[]` for anything the reader would not guess at, and `confidence{}`.
|
||
|
||
**Argument: the pattern belongs there and nowhere new.** Four reasons, and the
|
||
fourth is the one that decides it.
|
||
|
||
1. It is already the shape a detailer reads — the file's own comment says
|
||
`columns_in` holds "the same numbers a detailer would read off the dimension
|
||
chain", and `rows` is a matrix rather than a count because "a correct strand
|
||
COUNT in the wrong PLACE is still a wrong piece".
|
||
2. It is already consumed. Ticket QC item 4.4 (`check_strand_layout`) reads
|
||
`reinforcing.strand_layout` today via `specformat.strand_positions`, which
|
||
accepts both the matrix (`columns_in`) and the older flat shape
|
||
(`positions_in`). A second file would give this project two strand patterns
|
||
per design and no rule about which wins.
|
||
3. It is per **design**, which is the granularity the owner asked for — one
|
||
pattern, many pieces.
|
||
4. **It already carries provenance and a review state.** A half-day run against
|
||
a machine-read, never-reviewed pattern is a half day spent checking a guess.
|
||
`source.reviewed_by: null` is that fact, already recorded, already
|
||
per-design. A new file would have to grow the same field, and until it did,
|
||
the run would have no way to say what it had trusted. (What the run *does*
|
||
with an unreviewed pattern is §5.4 and open question 3.)
|
||
|
||
**What the spec does not carry, and would have to gain.** Three additive
|
||
fields, under `cli.py`'s versioning rule as restated in
|
||
`plan-panellift-three-stage.md` §5.3: *a key may be added without a schema bump
|
||
only when its absence has an explicit stand-down* — absence must read as
|
||
"nobody said", never as a value.
|
||
|
||
| proposed field | why | absence reads as |
|
||
| --- | --- | --- |
|
||
| `reinforcing.strand_layout.bed` | which casting bed this design is laid out for. The plant layer's `beds:` table is keyed by bed sheet; nothing today says which one a design uses, and the rationale is explicit that **which bed a piece is scheduled on is job knowledge**. | not stated → the bed-station and abutment checks stand down by name, never pass |
|
||
| `reinforcing.strand_layout.debond[]` | which stations the design already debonds, and over what run. Ticket QC 4.5 reads a debond *call-out* off the ticket; the design's own intent has no home. | `[]` = the design debonds nothing (a fact); key absent = nobody recorded it (a stand-down) — and **these are different**, see §5.5 |
|
||
| `reinforcing.strand_layout.deflect[]` | which strands the design deflects, where, and by how much. | same pair |
|
||
|
||
Note the `{}` / `null` convention the knowledge layer already uses and the
|
||
inversion `plan-panellift-three-stage.md` §7.2 warns about: here an **empty
|
||
list is a judgement** ("looked, none") and a **missing key is a stand-down**.
|
||
Say that at the top of the schema block or somebody will apply the other
|
||
file's convention to this one.
|
||
|
||
### 2.3 Through-hole versus blind pocket — settled, and not to be re-litigated
|
||
|
||
`liftexport.opening_penetration()` shoots a line along the thickness axis
|
||
through each opening's centre and intersects it with the concrete solid;
|
||
`split_openings()` sorts the result into `blockouts` (through), `recesses`
|
||
(blind, carrying `concrete_in` and `depth_in`) and a count of `unjudged`.
|
||
|
||
**A strand check must not treat a blind pocket as a hole.** That exact mistake
|
||
— every rectangle `_openings` returned going into `blockouts` — produced a
|
||
genuine-looking capacity FAIL on WP128, a piece whose model has no opening in
|
||
it at all, and it was fixed on 2026-09-01. `ticketpage.strand_debond_runs`
|
||
already documents the reasoning from the strand's own side: behind a recess
|
||
"the concrete and the strand are both still there", so there is no burst-prone
|
||
stub. This tool receives the **through** set pre-filtered, exactly as
|
||
`strand_debond_runs` requires, and it inherits `split_openings`'s third
|
||
outcome unchanged: **`unjudged` stays on the through side**, because "could not
|
||
tell" is not "blind", and standing it down on a failed solve would quietly make
|
||
a piece pass. The count is reported, not only acted on.
|
||
|
||
### 2.4 Interference — the pipeline exists and is measured
|
||
|
||
`check_member_interference` (`ticketchecks.py`, geometry in `ticketqc.py`),
|
||
with the reasoning in `docs/interference.md`. Five stages, each cheap enough to
|
||
earn the next: sweep-and-prune along the length → top-in-form → front → right →
|
||
one box **per solid** → boolean intersection → exemptions. Measured on three
|
||
real pieces: 12,090 pairs down to 27 real, **under a second per piece**.
|
||
|
||
The verdict is a **mean thickness**, `t = 2V/A`, not a volume and not the box
|
||
of the intersection — boxes reported 0.51" of interference for a sliver of
|
||
0.0166 in³, and two bars grazing share a curved lens that fills 10–15% of its
|
||
own box. Configuration is `interference.tolerance_in` (default 1/8"), `exact`,
|
||
`exempt_families`, `ignore_family_pairs`, `component_groups`.
|
||
|
||
**What is different here, and it is the whole difficulty.** Ticket QC's clash
|
||
check tests bodies that are *in the model*. This tool tests a pattern that is
|
||
**proposed** — its strands may not be modelled at all, or may be modelled
|
||
differently from the design. So the strand bodies have to be **synthesised**
|
||
from the pattern (a cylinder of `strand_groups[].dia_in` at each station, run
|
||
along the piece's length axis) and fed into the same pipeline. Three
|
||
consequences, all named as units in §5:
|
||
|
||
- the synthesis is a PRE unit with its own could-not-judge (no diameter, no
|
||
axis, no placement);
|
||
- the mean-thickness verdict and the tolerance are reused **as they are** — a
|
||
second interference criterion in this codebase would be a second answer to a
|
||
question `docs/interference.md` closed after several wrong versions that
|
||
"looked right";
|
||
- `docs/interference.md`'s own bias note applies unchanged: mean thickness
|
||
**under-reports a chunky intersection**, so the tolerance is set knowing
|
||
that.
|
||
|
||
### 2.5 Strand-versus-opening rules already in the plant layer
|
||
|
||
| key | what it says | consumer today |
|
||
| --- | --- | --- |
|
||
| `strand.debond_below_continuous_ft` | along the strand direction, a run of concrete shorter than this must be debonded | Ticket QC 4.7 `check_strand_debond` / `ticketpage.strand_debond_runs` |
|
||
| `strand.debond_by_blockout` | a decision table on the **blockout's own** L and W, and on whether the opening is inside the panel or at its edge; plus `also_applies_to_top_of_panel` and `engineering_must_consider_strand_count` | **none. Landed with no consumer.** |
|
||
| `strand.deflection` | `is_last_option: true`, `max_in`, `min_cover_in` | **none** |
|
||
| `strand.debond_callout_tokens` | the words a ticket uses to call a debond out | Ticket QC 4.5 |
|
||
| `strand.standard_patterns`, `strand.edge_strand_in`, `strand.both_wythes_same_stations` | the plant's own published chains | — |
|
||
| `strand.detensioning` | cutting sequence | none, deliberately |
|
||
|
||
The plant rationale records the hazard between the first two in its own words,
|
||
and it is a **gap in the tool, not a redundancy**: the 4-foot rule measures the
|
||
continuous run of concrete **along the strand**; the decision table's L and W
|
||
are the **blockout's own dimensions**. Same geometry from opposite sides. *They
|
||
agree at the 4-foot boundary and nowhere else,* and **the L-vs-W ratio rows
|
||
have no counterpart in the tool at all** — a blockout longer than 4 ft but
|
||
narrow relative to its width is one the tool passes today and the manual
|
||
debonds. That is a real hole and this feature is the natural place to close it
|
||
(unit `POST-1.11`).
|
||
|
||
**`max_deflection` and the plant's deflection rule.** The owner's parameter
|
||
almost certainly *is* `strand.deflection.max_in`, and there is a conflict worth
|
||
stating before anything is built:
|
||
|
||
1. **It is already plant knowledge, with a manual cite and a confidence
|
||
grade.** Putting it in a per-run dialog means it gets retyped, differently,
|
||
every run — the owner's own stated concern about Project Config. §4 rules it
|
||
plant, with a Project Config field that may only make it **stricter**.
|
||
2. **`is_last_option: true` is the sharper conflict.** The plant note reads
|
||
`DEFLECTING STRAND IS THE LAST OPTION`. A tool named "auto layout" that
|
||
proposes deflection whenever a strand meets an obstruction would invert a
|
||
ranking the plant has stated: debond first, deflect last. So `max_deflection`
|
||
is **not a knob that decides a layout**. It is a **ceiling on a deflection
|
||
the design already calls for**, and the tool's own verdict when it finds an
|
||
obstruction is *"this station needs relief; the plant's ranked options are
|
||
debond, then deflect"* — never *"deflect it by 1.75 inches"*. Open question 5
|
||
asks him to confirm the ranking is a rule and not a preference.
|
||
3. `strand.deflection.min_cover_in` is a cover that applies **to the deflected
|
||
condition** (`DEFLECT STRAND INTO PANEL 1 1/2" MIN. COVER`). It is **not** a
|
||
general clear cover from the concrete edge, and the owner's `min_clear_cover`
|
||
probably means the general one, which **has no key today**. See §4 and open
|
||
question 6.
|
||
|
||
### 2.6 Strands are dimensioned from a RAIL, and the rails/beds branches exist
|
||
|
||
Project knowledge: **strands always come with a dimension chain from a rail, and
|
||
which rail is fixed is plant knowledge, never inferred.** The plant rationale
|
||
states the chain shape exactly: *rail → first strand, then strand to strand,
|
||
then last strand → far edge; N segments locate N−1 strands and the total is the
|
||
piece width.*
|
||
|
||
| key | layer today | what it holds |
|
||
| --- | --- | --- |
|
||
| `rails.strand_datum` | plant | which edge the chain starts from. Read from the wrong end and the whole pattern mirrors. |
|
||
| `rails.left_rail_is_fixed` | **plant, deliberately `null`**; **the job sets it** | whether that rail is the bed's fixed rail. Null in the plant file because it is not a plant-wide truth — the manual labels a FIXED RAIL on the beam beds only; every wall-panel bed labels both sides SIDE RAIL. That one *exists* for panels is stated; **which side is not.** |
|
||
| `rails.max_bed_width_in` | plant, **contested** | a single scalar. See below. |
|
||
| `rails.narrow_panel` | plant | a narrow panel is **held to the fixed rail**, so the expected offset is zero from it; an asymmetric pattern on such a panel is **expected, not a finding**; floating between the rails is a stated last resort. |
|
||
| `beds.<sheet>` | plant | per bed: `panel_width_in`, `strand_ordinates_in` + `ordinates_count` + `ordinates_confidence`, `strand_rows`, `strand_rows_min_in` / `_max_in`, `abutment_kips`, `abutment_cg_in`, `max_strand_cg_in`, `max_strand_dia_in`, `max_kips_per_strand`, interior-bay and outer-bay kips and per-row kips, `jacking_plate_spacing_min_in`, `bed_surface_in` |
|
||
|
||
**`max_bed_width` is a parked contradiction and this feature must not resolve
|
||
it by accident.** The rationale files it as open question A6.1: the live scalar
|
||
disagrees with the manual, no wall-panel bed at `plant_a` is that width, the
|
||
figure the scalar was reasoned from is a dimension across the bay uprights on
|
||
one abutment rather than a panel width, and **a plant with beds of three
|
||
different widths cannot be described by one scalar whichever number that scalar
|
||
holds.** The cost if the manual is right is stated: on the narrowest bed the
|
||
offset budget is fractions of an inch of noise; on the widest it is over two
|
||
feet of real slack reported as a failure. `ticketchecks.py` still reads the
|
||
scalar for `BED-WIDTH` / `BED-OFFSET` and that was left unchanged on purpose.
|
||
|
||
**Ruling for this document: the new tool reads the per-bed table, keyed by the
|
||
design's declared bed, and reports NOT RUN when no bed is declared.** It does
|
||
not read the scalar, and it does not change what Ticket QC reads. That keeps
|
||
one contradiction rather than creating a second, and it means the day the owner
|
||
rules on A6.1, one check changes and this one does not. Open question 7.
|
||
|
||
**`beds.strand_ordinates_in` buys a check nothing else can make**, and the
|
||
rationale says so outright: the bed sheets print the available strand positions
|
||
as a discrete list of drilled ordinates, the section sheets print the layouts as
|
||
dimensions from the panel edge, and **the two halves reconcile exactly** — the
|
||
resolved standard chain's stations are all in one bed's ordinate list and the
|
||
chain sums to that bed's own standard panel width. *A strand at a station this
|
||
bed has no hole for is IMPOSSIBLE, not merely unusual.* One caution rides with
|
||
it: only one bed's ordinate list is transcribed at confidence A; the others are
|
||
**reconstructions at confidence B**, checked against the stated position counts,
|
||
and the rationale asks for them to be re-transcribed before anything measures a
|
||
real strand against them. So `POST-1.6` reports the confidence of the list it
|
||
judged against, in the finding, every time.
|
||
|
||
### 2.7 The run shape — `tools/sweep_in_revit.py`
|
||
|
||
Reused whole, and stated in §6. `todo.json`, `scope.json`, `meta/<mark>.json`,
|
||
`CHUNK` pieces per bridge call, **a piece with a meta file is done**, delete the
|
||
folder to start over. Read only: no transaction, no view change, no selection,
|
||
nothing in the undo stack. Scope from `vibe.scope`, and **out of scope is a
|
||
third state, not an absence.**
|
||
|
||
`tools/sweep.py`'s `check_source_model` is the model-mismatch precedent: it
|
||
compares each job file's `source.model` stem against the open `doc.Title`,
|
||
returns `(mismatched, unstamped, reason)`, treats an unstamped file as
|
||
**unjudged and not thereby cleared**, and refuses with a message naming **both**
|
||
models. It exists because a whole reference sweep was once re-checked against a
|
||
detached copy that had diverged, every report rendered, every number reconciled,
|
||
and the two models were told apart by hand.
|
||
|
||
### 2.8 The overlap with Ticket QC section 4, and the seam between them
|
||
|
||
This is the single largest reuse risk, so it is stated flatly. `docs/checklist-code-map.md`
|
||
already carries:
|
||
|
||
| item | codes | what it asks |
|
||
| --- | --- | --- |
|
||
| 4.4 | `STRAND-POS` | modelled strand positions vs the design chain from the stated rail |
|
||
| 4.6 | `STRAND-SPACING` / `STRAND-ROWS` / `STRAND-GRID` / `BED-WIDTH` / `BED-OFFSET` | row count and spacing plausible; the pattern's offset against the bed's slack |
|
||
| 4.7 | `STRAND-DEBOND` | strand debonded wherever concrete along it runs short |
|
||
| 4.5 | `STRAND-CALLOUT` | the ticket calls the debond out |
|
||
|
||
**The seam: Ticket QC asks "does what is drawn match the design?" This tool
|
||
asks "will this design work on this piece?"** Ticket QC compares two documents;
|
||
this compares a document against geometry. They overlap in the *quantities* they
|
||
compute and not in the question they answer, which is precisely the condition
|
||
that produces two implementations that disagree.
|
||
|
||
So, as a rule with a test behind it: **where the two tools compute the same
|
||
quantity, they call the same function.** Named, now, so nobody writes a second
|
||
one:
|
||
|
||
- `ticketpage.bed_offset_status(offset_in, width_in, bed_width_in, left_rail_is_fixed)`
|
||
— pure, host-testable, already carries the three-way `left_rail_is_fixed`
|
||
handling (False = no fixed rail, slack is spendable; True = a fixed rail, so
|
||
the slack is **not** slack, and *no fixed-rail tolerance has been recorded*,
|
||
so the offset is **reported, not judged**; None = unrecorded, and the message
|
||
says so). This tool passes a different `bed_width_in` (per-bed, §2.6) and gets
|
||
the same logic.
|
||
- `ticketpage.strand_debond_runs(length_ft, through_openings, strand_positions_in, threshold_ft)`
|
||
— already returns `None` for could-not-judge, `[]` for judged-and-clean, and a
|
||
run list otherwise, and already documents why a per-strand question needs no
|
||
hole-vs-notch fraction of its own.
|
||
- `ticketchecks._fit_chain` — see §5.3; it is the existing answer to a problem
|
||
this tool has in a harder form.
|
||
- `specformat.design_key`, `specformat.strand_positions`.
|
||
|
||
Where a function has to move to be shared, it moves to the pure side
|
||
(`specformat` / a new pure `strandlayout` module) and both callers follow it.
|
||
It does not get copied.
|
||
|
||
---
|
||
|
||
## 3. Where the button lives
|
||
|
||
`WillTools.extension/WillTools.tab/Design.panel/DesignHub.pulldown/StrandsAutoLayout.pushbutton/`,
|
||
added to the pulldown's `layout:` list — *anything not listed there is not
|
||
created.*
|
||
|
||
House style, read off both existing rows:
|
||
|
||
- `bundle.yaml` carries **`title:` only**. No tooltip, no author: pyRevit
|
||
renders both on hover and the owner wants nothing there, and
|
||
`tests/test_button_tooltips.py` guards it. Do not add them back.
|
||
- The title is **one line** — a pulldown row is text beside a 16px icon and has
|
||
nowhere to put a `\n`. The dropdown grows to the longest row, so the title may
|
||
say what it means.
|
||
- **`(WIP)` while it is buttons-and-docs only.** Both existing rows carry it and
|
||
both say why in their own bundle files: the suffix is what stops a placeholder
|
||
being mistaken for a finished tool by anyone who opens the dropdown. Proposed
|
||
title: `Strands (WIP)`.
|
||
- `__title__` in `script.py` mirrors `bundle.yaml`'s `title:` and is kept in
|
||
step, so trimming a bundle file cannot bring an old name back.
|
||
- The placeholder script's job, per `GravityLoadChase`: **state what is settled,
|
||
state that the rest is not, print the open questions.** No arithmetic, no
|
||
knowledge key, no schema field, no data structure invented ahead of the
|
||
method. The person who presses the button is in Revit, not in a text editor,
|
||
so "see the file" is how a placeholder becomes a dead end — it carries the
|
||
substance and a pointer to this document.
|
||
|
||
---
|
||
|
||
## 4. The parameters, and where each one lives
|
||
|
||
The rule applied to every row, stated once so each verdict can be argued
|
||
against it:
|
||
|
||
> **A number with a source — a manual page, a published standard, an
|
||
> equipment rating — is knowledge and belongs in a layer. A dialog is for what
|
||
> genuinely changes between two presses of the same button on the same job.**
|
||
> A plant standard in a dialog is a plant standard that gets retyped, slightly
|
||
> differently, every run; and the run that produced last week's report can no
|
||
> longer be reproduced, because what it was run against was typed and thrown
|
||
> away.
|
||
|
||
Layer order is the existing one: `defaults → catalogue → plant → job → project
|
||
config` (`knowledge._FILE_LAYERS`), deep-merged, most specific wins.
|
||
|
||
### 4.1 The four the owner named
|
||
|
||
| parameter | home | meaning and units | absent → |
|
||
| --- | --- | --- | --- |
|
||
| `max_deflection` | **plant**, `strand.deflection.max_in`. Project Config may make it **stricter only**. | The maximum a strand may be pushed off its bed station, inches. It is a **ceiling on a deflection the design already calls for**, not an amount the tool is licensed to apply — `strand.deflection.is_last_option` ranks deflection below debonding (§2.5). | `POST-1.12` reports NOT RUN naming the missing key. It never defaults: a wrong ceiling silently passes a deflection the plant forbids. |
|
||
| `bed_offset` | **measured output, not an input** — plus a **job** fact and an optional **per-run what-if**. | How far the pattern sits off the design chain, inches, signed from `rails.strand_datum`. The tool derives it per piece by fitting the chain (§5.3). The **job** already holds `rails.left_rail_is_fixed`, which decides whether an offset is slack to spend or a finding. A per-run *what-if* offset ("place every piece at +2 in and tell me what breaks") is a legitimate dialog input **and a different quantity** — it must not share the name. | Derived offset unavailable → `POST-1.7` NOT RUN. What-if absent → the tool uses the fitted offset, which is the ordinary case. |
|
||
| `max_bed_width` | **plant, per bed** — `beds.<sheet>.panel_width_in`, with `bed_surface_in` beside it. **Which bed** is **job** (proposed `reinforcing.strand_layout.bed`, §2.2). | The clear width available across the bed, inches; the pattern is stressed across the whole bed, so a piece narrower than it has slack `bed width − panel width`. | No bed declared → the bed-station, bed-width, offset and abutment units all report NOT RUN **naming the missing declaration**. The scalar `rails.max_bed_width_in` is **not** used as a fallback (§2.6). |
|
||
| `min_clear_cover` | **plant — and the key does not exist yet.** Proposed `strand.min_clear_cover_in`. | Minimum clear concrete between a strand's surface and a free face: the concrete edge, an opening's jamb, and the outer face of the wythe. Inches. **Distinct from `strand.deflection.min_cover_in`**, which applies only to a deflected strand (§2.5). | `POST-1.4` NOT RUN with the key named. **No default. This is a number only the owner can give** — open question 6, and it may be three numbers, not one. |
|
||
|
||
### 4.2 The "等等" — everything else the survey says is needed
|
||
|
||
| parameter | home | meaning | absent → |
|
||
| --- | --- | --- | --- |
|
||
| `rails.strand_datum` | plant *(exists)* | which edge the chain is measured from | fitting is attempted from both edges and the reading is **named in every finding**; see §5.3 |
|
||
| `rails.left_rail_is_fixed` | **job** *(exists; plant leaves it null on purpose)* | whether the datum rail is the bed's fixed rail | `bed_offset_status`'s None path: verdicts unchanged, message says the flag is unrecorded |
|
||
| `rails.narrow_panel.*` | plant *(exists)* | narrow panels held to the fixed rail; asymmetry expected | the narrow-panel expectation stands down and `POST-1.7` says so |
|
||
| `beds.<sheet>.strand_ordinates_in` (+ `ordinates_confidence`) | plant *(exists)* | the drilled stations that physically exist | `POST-1.6` NOT RUN. `null` with `strand_ordinates_unknown_because` is a **declared** absence and reports as NOT APPLICABLE-with-reason, not as a gap |
|
||
| `beds.<sheet>.strand_rows_min_in` / `_max_in`, `max_strand_cg_in` | plant *(exists)* | legal row heights through the thickness, and the highest C.G. the abutment is rated for | `POST-1.9` NOT RUN |
|
||
| `beds.<sheet>.abutment_kips`, `abutment_cg_in`, `max_kips_per_strand`, `max_strand_dia_in`, interior/outer bay kips and per-row kips | plant *(exists)* | the bed's force ratings | `POST-1.8` NOT RUN. One bed's `abutment_kips` is **`null` with a stated reason** (the sheet shipped with the blank unfilled) — that is a declared unknown and must report as one, never inferred from another bed |
|
||
| `beds.<sheet>.jacking_plate_spacing_min_in` | plant *(exists; C.G.-dependent)* | minimum station spacing | `POST-1.13` NOT RUN |
|
||
| `strand.debond_below_continuous_ft` | plant *(exists)* | shortest developable run | `POST-1.10` NOT RUN — `strand_debond_runs` already refuses to default it, "a wrong default would silently decide what is this plant's rule to set" |
|
||
| `strand.debond_by_blockout` | plant *(exists, no consumer)* | the L/W decision table | `POST-1.11` NOT RUN |
|
||
| `strand.edge_strand_in` | plant *(exists)* | the plant's own first-strand dimension | informational; quoted beside `POST-1.5` |
|
||
| `interference.tolerance_in`, `exempt_families`, `ignore_family_pairs`, `component_groups`, `within_category_only` | catalogue default + plant + job *(all exist)* | the clash criterion and its exemptions | defaults hold; `interference.check` being **False by default** is a live trap — see open question 9 |
|
||
| **`strand.min_clear_to_opening_in`** | **plant — proposed, does not exist** | how close a strand may run to a through opening's jamb | NOT RUN. **Owner's number.** |
|
||
| **`strand.max_clear_to_opening_in`** | **plant — proposed, does not exist** | the "too far" half the owner named. *What it means is not obvious and is not guessed here* — open question 4 | NOT RUN |
|
||
| **`strand.max_edge_strand_in`** | **plant — proposed, does not exist** | how far in from the concrete edge the outermost strand may sit before the edge is unreinforced | NOT RUN |
|
||
| `tolerances.dimension_in` | plant *(exists)* | chain-fit tolerance, already read by 4.4 | its existing default holds |
|
||
| design selection, resume/restart, report destination, open-on-finish, the what-if offset | **per-run dialog** | what changes between two presses | dialog defaults; recorded in the run manifest |
|
||
|
||
**Project Config surface.** `configmodel.SETTINGS` is a hand-maintained table
|
||
today with an `edit` layer per row and `configform.unmapped()` + its test failing
|
||
on anything unmapped — *a setting added cannot quietly fail to appear.* Under
|
||
`plan-panellift-three-stage.md` §10 that table becomes generated from the units
|
||
that read each field. Whichever way that lands, three rows here are Project
|
||
Config candidates and the rest are not: `max_deflection` (stricter only), the
|
||
what-if offset, and the report destination. **Every other row above is a plant
|
||
or job fact and must not get a text box.**
|
||
|
||
---
|
||
|
||
## 5. The check catalogue
|
||
|
||
### 5.1 Numbering, and a finding about the scheme
|
||
|
||
`docs/plan-panellift-three-stage.md` §1.1 sets the grammar
|
||
`<SEGMENT>-<group>.<n>` with four mandatory segment tags (`PRE`, `ENG`, `POST`,
|
||
`INT`), and defers the tool prefix with a stated condition:
|
||
|
||
> "If a third tool is ever numbered, the namespace grows a tool prefix
|
||
> (`handling/PRE-2.5`). Not now — one level of namespace for one tool is enough,
|
||
> and an unused level is a level people put things in."
|
||
|
||
**That condition is met by this document, and it is met one tool earlier than
|
||
the text expects.** Ticket QC numbers its checks bare (`3.3`, `4.6`) and the
|
||
handling units are to be `PRE-*` / `ENG-*` / `POST-*` / `INT-*`. This is a
|
||
**second** segment-tagged tool, so a bare `PRE-2.5` would be ambiguous between
|
||
handling and strands the day both exist — and `checks.muted` is a deep-merged
|
||
map keyed on those strings, so the ambiguity would land in the one place that
|
||
silently stops a check from checking.
|
||
|
||
**Ruling for this document: units here are `strands/PRE-1.1`, and handling's are
|
||
`handling/PRE-2.5`.** That is a rename of ids that are, today, written down and
|
||
not yet built, so it costs a text edit now and an id migration later — and
|
||
renaming an id is the one thing that scheme is built to avoid.
|
||
|
||
**RESOLVED, 2026-09-01 — the coordinator decided rather than leaving it as a
|
||
recommendation, owner informed, no objection: the tool prefix is adopted from
|
||
day one, not deferred to a third tool.** `docs/plan-panellift-three-stage.md`
|
||
§1.1 now states this directly, so what follows in this section is the finding
|
||
that forced the ruling, not a still-open recommendation. Open question 1
|
||
puts it to the owner. Everything below carries the prefix.
|
||
|
||
**Two further findings about the scheme, both from trying to fit this feature
|
||
into it:**
|
||
|
||
1. **Composition is preset by `(plant, member_type)` — and that pair is not
|
||
unique across tools.** `pipelines.profiles.wall_panel_plant_a` names one
|
||
ordered unit list for `plant_a` × wall panel. This tool needs its own list
|
||
for the *same pair*. So the profile key needs a tool axis
|
||
(`pipelines.profiles.strands.wall_panel_plant_a`), or profiles live under a
|
||
per-tool namespace. Not a defect in the idea; a defect in the key. Open
|
||
question 2.
|
||
2. **The scheme's PRE→ENG seam is a file crossing a process boundary** (Revit's
|
||
IronPython to host CPython) and **this tool has no such boundary** — every
|
||
unit runs in-process, driven in chunks over the bridge. The scheme's own
|
||
§3 already says the three-stage split is a *different axis* from the
|
||
in-process/host split and the two must not be conflated, so the segments
|
||
still apply: PRE measures, ENG computes plant-blind geometry, POST turns a
|
||
number into a verdict against a plant threshold, INT owns the files. What is
|
||
genuinely open is whether a tool that never leaves Revit still owes a *deck*.
|
||
**Recommendation: yes, for the record reason and not the boundary reason** —
|
||
a half-day run whose inputs were not frozen and stamped cannot be reproduced,
|
||
and §6.4 needs exactly that stamp to detect a mid-run change. Open question 8.
|
||
|
||
**Finding codes get an `SL-` prefix.** `CAST-EXTRA` is the precedent
|
||
(`plan-panellift-three-stage.md` PRE-2.6: *already a Ticket QC code — do not
|
||
reuse*). `BED-WIDTH`, `BED-OFFSET`, `STRAND-SPACING`, `STRAND-POS`,
|
||
`STRAND-DEBOND` and `CLASH` are **all live Ticket QC codes** owned by rows in
|
||
`CHECKLIST_ITEMS`, and reusing one would mate this tool's findings to Ticket
|
||
QC's mutes. So every code below is `SL-*`, uniformly, including the ones with no
|
||
collision — a prefix that is applied "only where it is needed" is a prefix
|
||
nobody can predict.
|
||
|
||
**Every unit owns a verdict code and a could-not-judge partner.** Registration
|
||
refuses a unit with only one — that is the project's cardinal rule turned into a
|
||
gate, and `docs/checklist-code-map.md`'s pairs (`CALLOUT-OVERLAP` /
|
||
`CALLOUT-OVERLAP-UNREAD`) are the model. **"Could not judge" is not "no problem
|
||
found."** Every unit also owns a **skip probe** on the ticketqc contract: it
|
||
returns the *reason* it was quiet, or `None` meaning "it ran and found nothing",
|
||
and never `''`.
|
||
|
||
Provenance column: **[W]** = named by the owner. **[P]** = proposed by this
|
||
survey.
|
||
|
||
### 5.2 `strands/PRE` — reading the design and the piece
|
||
|
||
| # | unit | codes | could-not-judge case |
|
||
| --- | --- | --- | --- |
|
||
| `strands/PRE-1.1` | Which pieces carry this design number | `SL-SCOPE` / `SL-SCOPE-UNREAD` | the framing member's design parameter is unmapped or unreadable, or the role `panel.design_number` has no name behind it. **`None` is not a design**, and a piece whose number could not be read is listed as unread, never quietly excluded |
|
||
| `strands/PRE-1.2` | The strand pattern, read from the design summary | `SL-PATTERN` / `SL-PATTERN-UNREAD` | no spec for this design; `strand_layout.columns_in` empty; `strand_groups[]` missing `dia_in`. `specformat.validate_spec`'s complaints ride into the finding |
|
||
| `strands/PRE-1.3` | The pattern's review state | `SL-REVIEWED` / `SL-UNREVIEWED` | `source.reviewed_by` null → **`SL-UNREVIEWED` on every piece and at the top of the roll-up.** Not a stand-down: the run proceeds and says what it trusted (§5.4) |
|
||
| `strands/PRE-1.4` | Which casting bed this design is laid out for | `SL-BED` / `SL-BED-UNDECLARED` | the proposed `strand_layout.bed` absent, or naming a bed the plant table does not have |
|
||
| `strands/PRE-1.5` | Which rail is the datum, and whether it is fixed | `SL-RAIL` / `SL-RAIL-UNREAD` | `rails.strand_datum` unmapped. `left_rail_is_fixed: null` is **not** this unit's could-not-judge — it is a recorded unknown that `bed_offset_status` already handles by name |
|
||
| `strands/PRE-2.1` | Concrete extents, axes, thickness, layup | `SL-AXES` / `SL-AXES-UNREAD` | `liftexport._axes` / `concrete_extents` / `choose_concrete` cannot resolve a body |
|
||
| `strands/PRE-2.2` | Openings, split through vs blind | `SL-OPENING` / `SL-OPENING-UNJUDGED` | `opening_penetration`'s solve failed for a rectangle. **The unjudged rectangle stays on the through side** and the count is reported (§2.3) |
|
||
| `strands/PRE-2.3` | The bodies a strand could hit | `SL-BODIES` / `SL-BODIES-UNREAD` | a member's geometry will not solve; an exempt/ignore list is unset where the plant needs one |
|
||
| `strands/PRE-2.4` | The synthesised strand bodies | `SL-SYNTH` / `SL-SYNTH-UNREAD` | no diameter on the group, no length axis, or the pattern could not be placed (`PRE-2.5` failed) |
|
||
| `strands/PRE-2.5` | **Which of the pattern's stations this piece actually carries** | `SL-SUBSET` / `SL-SUBSET-UNJUDGED` | **the hardest unit in the tool. See §5.3.** |
|
||
| `strands/PRE-3.1` | Declare what this run did not produce for this piece | `SL-DECLARE` / `SL-DECLARE-MISSING` | a unit with no declaration row reads NOT RUN — `plan-panellift-three-stage.md` §7.2's word-not-shape rule, adopted whole |
|
||
|
||
### 5.3 `strands/ENG` — plant-blind geometry
|
||
|
||
Nothing in this segment reads a plant threshold. Every unit produces a number
|
||
or a documented could-not-judge; the word FAIL belongs to POST.
|
||
|
||
| # | unit | codes | could-not-judge case |
|
||
| --- | --- | --- | --- |
|
||
| `strands/ENG-1.1` | Resolve the chain to stations from the datum | `SL-CHAIN` / `SL-CHAIN-UNREAD` | segments missing; `total_in` disagrees with the chain's own sum |
|
||
| `strands/ENG-1.2` | Fit the pattern onto the piece; derive the offset | `SL-FIT` / `SL-FIT-UNJUDGED` | no fit within `tolerances.dimension_in` from **either** edge reading |
|
||
| `strands/ENG-2.1` | Shared thickness, strand vs every other body | `SL-XSECT` / `SL-XSECT-UNJUDGED` | a boolean fails; a body has no solid |
|
||
| `strands/ENG-2.2` | Clear distance, strand to each **through** opening's jamb | `SL-DOPEN` / `SL-DOPEN-UNJUDGED` | opening set unjudged; strand station unplaced |
|
||
| `strands/ENG-2.3` | Clear distance, strand to each concrete edge and face | `SL-DEDGE` / `SL-DEDGE-UNJUDGED` | extents unread; a non-prismatic edge the box does not describe |
|
||
| `strands/ENG-2.4` | Continuous concrete runs along each strand | `SL-RUNS` / `SL-RUNS-UNJUDGED` | `strand_debond_runs` returns `None` — length, positions or threshold missing. **`[]` is a judgement, not a stand-down** |
|
||
| `strands/ENG-2.5` | Row heights and the pattern's C.G. through the thickness | `SL-CG` / `SL-CG-UNJUDGED` | rows unread |
|
||
| `strands/ENG-2.6` | Station spacing across the width | `SL-PITCH` / `SL-PITCH-UNJUDGED` | fewer than two stations |
|
||
| `strands/ENG-3.1` | Total force, per row and per bay, from `it_lb` × qty | `SL-FORCE` / `SL-FORCE-UNJUDGED` | `it_lb` absent from a group; a group whose stations could not be placed |
|
||
| `strands/ENG-3.2` | Geometry of the relief an obstruction would need | `SL-RELIEF` / `SL-RELIEF-UNJUDGED` | the obstruction's extent could not be measured. **Produces a required offset; does not choose debond vs deflect** — that ranking is plant policy and lives in POST |
|
||
|
||
**§5.3a — `strands/PRE-2.5`, and why it is the hardest thing here.**
|
||
|
||
The job knowledge layer records, for this job, in its own words: *the printed
|
||
strand count is the design's full-width maximum, not a per-piece quantity — of
|
||
166 pieces on one design, only 86 carry all 14 strands*
|
||
(`printed_strand_count_is_per_piece: false`). **So the pattern the owner authors
|
||
is a maximum, and each piece carries a subset of it.** A tool that checks all 14
|
||
stations on a piece that carries 9 will report five phantom problems per piece,
|
||
several hundred times, in a half-day run — and every one of them will look
|
||
exactly like a real finding.
|
||
|
||
What exists to build on: `ticketchecks._fit_chain` already slides the design
|
||
chain onto what was **modelled** and keeps *the best contiguous run*, trying
|
||
both edge readings and keeping the better fit. That is the mechanism, and it
|
||
carries an assumption worth naming: **a subset is contiguous.** Nobody has
|
||
confirmed that a piece never drops an interior station.
|
||
|
||
What is different and harder here: `_fit_chain` has a modelled layout to fit
|
||
*against*. This tool may be run on pieces whose strands are not modelled at all
|
||
— that is much of the point of running it — and then there is nothing to fit and
|
||
the subset must be **derived**, from the piece's width, the datum rail, the
|
||
narrow-panel rule (held to the fixed rail, offset zero, asymmetry expected) and
|
||
the bed's ordinate list. Three possible answers and **this document does not
|
||
choose between them**:
|
||
|
||
1. derive it, and report every derived station as derived;
|
||
2. refuse to derive it and report `SL-SUBSET-UNJUDGED` on any piece whose
|
||
strands are not modelled — honest, and possibly quiet on most of the model;
|
||
3. take the subset from the modelled strands where they exist and derive it
|
||
where they do not, saying per piece which it did.
|
||
|
||
Recommendation is (3), because it is the only one that is useful on a
|
||
part-modelled job and it never hides which mode it used. But **a wrong subset
|
||
rule is a wrong finding on every piece of the design**, so this is open question
|
||
10 and it is the one to answer first.
|
||
|
||
### 5.4 `strands/POST` — the verdicts
|
||
|
||
| # | unit | source | codes | could-not-judge |
|
||
| --- | --- | --- | --- | --- |
|
||
| `strands/POST-1.1` | Strand interferes with another cast-in body | **[W]** 干涉 | `SL-CLASH` / `SL-CLASH-UNJUDGED` | `ENG-2.1` unjudged; a body whose geometry did not solve; an exemption list the plant has not filled in — reported as *not asked*, never as clean |
|
||
| `strands/POST-1.2` | Strand too close to a through opening | **[W]** 距离洞太近 | `SL-OPEN-NEAR` / `SL-OPEN-NEAR-UNREAD` | `strand.min_clear_to_opening_in` unset (**owner's number**), or `ENG-2.2` unjudged |
|
||
| `strands/POST-1.3` | Strand too far from a through opening | **[W]** 距离洞太远 | `SL-OPEN-FAR` / `SL-OPEN-FAR-UNREAD` | key unset **and the question itself unsettled** — open question 4 |
|
||
| `strands/POST-1.4` | Strand too close to the concrete edge or face (cover) | **[W]** 距离edge太近 | `SL-COVER` / `SL-COVER-UNREAD` | `strand.min_clear_cover_in` unset (**owner's number**); `ENG-2.3` unjudged |
|
||
| `strands/POST-1.5` | Outermost strand too far in from the concrete edge | **[W]** 距离edge太远 | `SL-EDGE-FAR` / `SL-EDGE-FAR-UNREAD` | `strand.max_edge_strand_in` unset |
|
||
| `strands/POST-1.6` | A station the bed has no drilled hole for | **[P]** | `SL-BED-STATION` / `SL-BED-STATION-UNREAD` | no bed declared; the bed's `strand_ordinates_in` is `null` with `strand_ordinates_unknown_because` (a **declared** absence → NOT APPLICABLE with that reason). **The finding always states the list's `ordinates_confidence`** (§2.6) |
|
||
| `strands/POST-1.7` | Piece wider than the bed; pattern offset beyond the slack; narrow panel not held to the fixed rail | **[P]** | `SL-BED-WIDTH` / `SL-BED-OFFSET` / `SL-BED-OFFSET-UNJUDGED` | via `bed_offset_status`, which already returns `None` for "nothing to judge" and already **reports rather than judges** when a fixed rail exists and no fixed-rail tolerance has been recorded |
|
||
| `strands/POST-1.8` | Force exceeds the abutment, a bay, a row, or a strand | **[P]** | `SL-ABUTMENT` / `SL-ABUTMENT-UNRATED` | the bed's rating is `null` **with a stated reason** — one bed's sheet shipped with the blank unfilled. **Never inferred from another bed.** `INSERT-NO-FOS` is the precedent: unrated means *not judged*, and it says so by name |
|
||
| `strands/POST-1.9` | Row height outside the bed's limits, or C.G. above the rated height | **[P]** | `SL-ROW-HEIGHT` / `SL-ROW-HEIGHT-UNREAD` | limits unset; `ENG-2.5` unjudged |
|
||
| `strands/POST-1.10` | A run of concrete too short to develop the strand in it | **[P]** (existing 4.7 logic, new question) | `SL-DEBOND-RUN` / `SL-DEBOND-RUN-UNREAD` | `strand_debond_runs` returned `None`. `[]` = ran and clean, reported as such |
|
||
| `strands/POST-1.11` | The blockout's own L/W calls for a debond | **[P]** — **closes a hole** | `SL-DEBOND-LW` / `SL-DEBOND-LW-UNREAD` | `strand.debond_by_blockout` unset; the inside-vs-edge classification unjudged. Carries the table's own note that **engineering shall consider the number of strands in the blockout** — so the finding shortlists, it does not rule |
|
||
| `strands/POST-1.12` | A deflection the design calls for exceeds the plant's ceiling, or is proposed where debond is available | **[W]** `max_deflection` | `SL-DEFLECT` / `SL-DEFLECT-UNREAD` | `strand.deflection.max_in` unset. **`is_last_option` is honoured: relief is reported with the plant's ranking, never as a chosen deflection** (§2.5) |
|
||
| `strands/POST-1.13` | Stations closer together than the bed's jacking-plate minimum | **[P]** | `SL-PITCH-MIN` / `SL-PITCH-MIN-UNREAD` | the minimum is C.G.-dependent and the C.G. is unjudged; or the key is unset |
|
||
| `strands/POST-1.14` | The chain total disagrees with the piece width | **[P]** | `SL-CHAIN-SUM` / `SL-CHAIN-SUM-UNJUDGED` | width unread; `total_in` absent. On a **narrow** panel this is expected, not a defect — `rails.narrow_panel` decides, and where it is unset the unit stands down rather than reporting every narrow piece |
|
||
| `strands/POST-1.15` | A strand crosses a through opening at all | **[P]** | `SL-CUT` / `SL-CUT-UNJUDGED` | opening set unjudged. Distinct from `POST-1.2`: this is *through the hole*, not *near it*, and `panellift`'s `prestress.cuts_for` already models what that does to development |
|
||
| `strands/POST-2.1` | Per-piece report | — | | |
|
||
| `strands/POST-2.2` | The basis block: every threshold, its value's layer, and its source | — | | |
|
||
| `strands/POST-2.3` | The unit table: every unit, its state, its reason | `SL-UNIT-TABLE` | | |
|
||
| `strands/POST-2.4` | The roll-up across the design: which pieces, which findings, how many NOT RUN and why | `SL-ROLLUP` | | |
|
||
| `strands/POST-2.5` | Mute roll-up: how many, by which layer, and which matched nothing | `SL-MUTE-COUNT` / `SL-MUTE-STALE` | | |
|
||
|
||
**State vocabulary** is `ticketpage.STATE_VOCABULARY`'s, unchanged: `NOT
|
||
APPLICABLE` (blue, not judged), `NOT RUN` (red, not judged), `FAIL` (red,
|
||
judged), `MUTED` (grey, judged — it ran). `state_color`'s red fallthrough — an
|
||
unrecognised word renders **red, never grey** — is what makes a grey state safe
|
||
to have, and building a second vocabulary would be a mistake this project has
|
||
already made once.
|
||
|
||
**Mute** follows §8 of the handling plan exactly and gains nothing new:
|
||
namespaced keys (`checks.muted: {'strands/POST-1.3': '<reason>'}`), reason
|
||
mandatory and refused by `configmodel.validate`, muted rows stay on the table
|
||
carrying what they really reported, **muting never softens a FAIL**, and stale
|
||
mutes are named. A bare number keeps meaning a Ticket QC master id.
|
||
|
||
**`SL-UNREVIEWED` and the top of the page.** `strands/PRE-1.3` is a
|
||
deliberate departure from "stand down when the input is not trustworthy": the
|
||
run **proceeds** on an unreviewed pattern, because the tool is read-only and
|
||
refusing would help nobody, and it **says so on every piece and at the head of
|
||
the roll-up**. That choice is open question 3 — the alternative is a hard
|
||
refusal, which is what the weight gate does and what `sandwich.REFUSAL_CODE`
|
||
does, and the owner may prefer it.
|
||
|
||
### 5.5 `strands/INT` — the boundary
|
||
|
||
| # | unit | codes |
|
||
| --- | --- | --- |
|
||
| `strands/INT-1.1` | The run manifest: pattern, thresholds, layer shas, engine version, model identity | `SL-MANIFEST` / `SL-MANIFEST-STALE` |
|
||
| `strands/INT-1.2` | Model identity — is this the model the run was started against | `SL-MODEL` / `SL-MODEL-UNSTAMPED` |
|
||
| `strands/INT-1.3` | Per-piece progress files, and resume | `SL-RESUME` / `SL-RESUME-MIXED` |
|
||
| `strands/INT-1.4` | Per-piece staleness — did this piece change after it was measured | `SL-PIECE-STALE` / `SL-PIECE-STALE-UNJUDGED` |
|
||
|
||
---
|
||
|
||
## 6. The run model
|
||
|
||
> **Owner ruling, 2026-09-01 (Discord):** "strand auto layout这个写完了,记得直接在刚dump的模型里面跑一跑试试。" --
|
||
> first trial runs of this feature happen inside a freshly detached
|
||
> snapshot model (the sandbox the snapshot cycle produces), never against
|
||
> the live model. That is the bring-up environment, on top of the
|
||
> standing read-only rule.
|
||
|
||
### 6.1 Shape on disk
|
||
|
||
`sweep_in_revit.py`'s shape, in `designspec.sub_dir(doc, 'strands')` — beside
|
||
the model, under the same `_ticketqc` root the specs and the reinforcing index
|
||
already use, gitignored, and outside the repository entirely when
|
||
`WILLTOOLS_JOBS` is set. **It holds job geometry and may never enter the code
|
||
repository.**
|
||
|
||
```
|
||
strands/
|
||
manifest.json the frozen inputs: design(s), the pattern as read, every
|
||
threshold with its value and its layer, the merged
|
||
knowledge sha, the spec sha, doc key, doc title, engine
|
||
version, started_at
|
||
scope.json the declared scope, the line, and what was left alone
|
||
todo.json every in-scope piece with this design: id, mark, design
|
||
meta/<mark>.json what happened to one piece: status, findings, the unit
|
||
table, the piece signature at the time it was measured,
|
||
seconds
|
||
report/ the rendered per-piece pages and the roll-up
|
||
```
|
||
|
||
`CHUNK` pieces per bridge call, the call returning how many are left. **A piece
|
||
with a meta file is done.** Delete the folder to start over.
|
||
|
||
### 6.2 Cancellation and resume
|
||
|
||
- **Cancel** = stop pressing the button, or close the dialog. There is no
|
||
in-flight state to unwind because there is no transaction: the current chunk
|
||
finishes or is abandoned mid-piece, and an abandoned piece simply has no meta
|
||
file, so it is re-measured on resume. A chunk of ten is the unit of loss.
|
||
- **Resume** = press again. `todo.json` is enumerated **once** and is not
|
||
re-read from the scope — so a scope change mid-run does not silently alter
|
||
what a resumed run covers, and the way to apply one is to delete the folder,
|
||
which is the same rule `sweep_in_revit.py` already states.
|
||
- **Resume against different inputs is refused, not silently honoured.**
|
||
`manifest.json` freezes the pattern and every threshold at enumeration. If the
|
||
spec or a knowledge layer has been edited since, the sha moves and the resume
|
||
refuses with `SL-MANIFEST-STALE`, naming **which layer** moved. Half a run
|
||
under one rule and half under another produces a report that is internally
|
||
inconsistent and looks fine.
|
||
|
||
### 6.3 Model-change refusal
|
||
|
||
Two different failures, and only the first has a precedent:
|
||
|
||
1. **Wrong model.** `sweep.py`'s `check_source_model` / `model_refusal`,
|
||
adopted whole: compare the manifest's model stem against `doc.Title`,
|
||
case-insensitively, `.rvt` trimmed; a **missing** stamp is `unstamped` and is
|
||
**not thereby cleared**; a mismatch refuses, **naming both models**, and
|
||
checks nothing. That refusal exists because a whole sweep was once re-checked
|
||
against a diverged detached copy and every report rendered.
|
||
2. **Same model, changed under the run.** Four hours in, someone moves an
|
||
opening. `doc.Title` is unchanged and `check_source_model` passes. **This is
|
||
the failure the existing precedent does not cover**, and a half-day run is
|
||
the case that produces it.
|
||
|
||
The nearest mechanism is `reinforcecore.signature` — flags plus the member-id
|
||
set, cheap enough to recompute for every assembly in seconds — **but it is
|
||
explicitly documented as blind to an opening moved or resized without a
|
||
member changing.** `rebuild_all` exists as the escape hatch for exactly that.
|
||
So a signature is necessary and not sufficient here.
|
||
|
||
**Design: stamp per piece, verify at the end, and report — do not pretend
|
||
the run was atomic.** Each `meta/<mark>.json` records the piece's signature
|
||
*and* a cheap geometry stamp (concrete extents plus the opening rectangle
|
||
list, both already read by `PRE-2.1` / `PRE-2.2`, so it costs nothing extra).
|
||
On resume and again at completion, the stamps are recomputed and every piece
|
||
whose stamp moved is reported as `SL-PIECE-STALE` **with what moved** — its
|
||
findings are still shown, marked measured-against-a-previous-state. The
|
||
alternative, invalidating and re-measuring, is defensible and is open
|
||
question 12; what is not defensible is a report that does not say.
|
||
|
||
`SL-PIECE-STALE-UNJUDGED` covers the piece that has since been deleted or
|
||
whose stamp will not recompute.
|
||
|
||
### 6.4 Runtime — what is actually known, and what is guessed
|
||
|
||
Measured anchors in this codebase, all on one real model:
|
||
|
||
| measurement | figure |
|
||
| --- | --- |
|
||
| 755 assemblies collected, both flags read (`reinforceindex`) | 0.29 s |
|
||
| every assembly's cheap signature | 0.71 s |
|
||
| **full geometry build, 690 pieces** | **6.9 s — about 10 ms each** |
|
||
| slowest single piece | 0.061 s |
|
||
| full interference pipeline, one real piece, all pairs → verdict | **under 1 s** |
|
||
| the boolean pass that removes two-thirds of the box hits | ~0.1 s |
|
||
| generating a view (`read_ticket` walking a ticket sheet) | **the expensive read in this project** |
|
||
|
||
**This tool generates no view.** Like `reinforceindex`, it asks elements for
|
||
their own geometry. So the dominant cost is the interference pipeline, ~1 s per
|
||
piece, plus the opening raycasts and the per-strand distance work — call it a
|
||
small multiple of that once the synthesised strands add their own pairs.
|
||
|
||
**A first-order estimate, and it is an estimate:** on the order of a few
|
||
seconds per piece × several hundred pieces of one design ≈ **tens of minutes,
|
||
not half a day** — *provided* the sweep-and-prune stages are reused. Skip them
|
||
and solve every strand against every body as solids and half a day is exactly
|
||
what it costs; that is the difference the five-stage pipeline was built to buy.
|
||
|
||
Two honest caveats. First, that estimate is arithmetic on other checks'
|
||
measurements, not a measurement of this one; nobody has run it. Second, **the
|
||
owner expects half a day and he knows his model better than this arithmetic
|
||
does** — he may mean the whole job across every design, or a model much larger
|
||
than the one the anchors come from. **The resumability in §6.1–6.3 is not
|
||
contingent on the estimate.** It is required at ten minutes too: a run that
|
||
cannot be resumed is a run nobody starts before lunch.
|
||
|
||
**A measurement to take before building:** time `PRE-2.1` through `ENG-2.1` on
|
||
ten real pieces of one design and multiply. If it lands near half a day, the
|
||
pruning is not doing its job and that is a bug to find *before* the tool is
|
||
pointed at a whole model.
|
||
|
||
---
|
||
|
||
## 7. What it reads from Revit, and what it never touches
|
||
|
||
**Reads:**
|
||
|
||
- `AssemblyInstance` collection and each assembly's `GetMemberIds()`;
|
||
- the Structural Framing member's design-number parameter, **resolved by role**
|
||
(`panel.design_number`) through `knowledge.param_names`, **never by literal
|
||
name** — a shared-parameter name is plant knowledge, and a name written into
|
||
code is a plant that cannot be served without editing the code;
|
||
- the concrete solid's geometry: extents, faces, openings — via
|
||
`liftexport.concrete_solid` / `_openings` / `opening_penetration`;
|
||
- each member's solids for the interference pipeline (**per solid**, not one box
|
||
per element: a 9-solid embed is 3% steel by box volume);
|
||
- instance parameters read by role for thickness, width, length;
|
||
- the design spec YAML and the four knowledge layers, off disk.
|
||
|
||
**Never touches:**
|
||
|
||
- **no `Transaction`, ever.** The bridge and anything touching the production
|
||
model reads and never writes. This feature only reports.
|
||
- no parameter write, no element create, delete or move;
|
||
- no view creation, no view activation, no `Elevation Top` regeneration — which
|
||
is also why it is not slow;
|
||
- no selection change, no active-view change, nothing in the user's undo stack;
|
||
- nothing written anywhere inside the code repository — every output lands under
|
||
`designspec.root_dir(doc)`.
|
||
|
||
`tools/scrub.py --worktree` must stay clean: no model filename, no absolute user
|
||
path, no client or plant name in any output that could be pasted into a commit.
|
||
|
||
---
|
||
|
||
## 8. Open questions — only the owner can answer these
|
||
|
||
Liberal, deliberately. A strand layout rule guessed wrong is a real defect in a
|
||
real panel, and several of the rows below are numbers that exist in his head and
|
||
nowhere in the knowledge layers.
|
||
|
||
1. **The tool prefix on unit ids** (§5.1). `strands/PRE-1.1` and
|
||
`handling/PRE-2.5` from the start, or bare tags until a collision actually
|
||
bites? Recommending the prefix now: the handling ids are written down and not
|
||
yet built, so this is the cheapest moment it will ever be, and a mute keyed on
|
||
an ambiguous id is the failure mode the whole scheme exists to prevent.
|
||
2. **The preset key needs a tool axis** (§5.1). `(plant, member_type)` is not
|
||
unique across two numbered tools. `pipelines.profiles.strands.wall_panel_plant_a`,
|
||
or per-tool profile namespaces?
|
||
3. **An unreviewed pattern**: run and label loudly (recommended), or refuse the
|
||
way the weight gate refuses? `source.reviewed_by` is null on the specs today.
|
||
4. **"Too far from an opening" — what does it mean?** The other three of his
|
||
four named classes have obvious geometry; this one does not. Candidate
|
||
readings, and none is assumed: (a) an opening whose jamb has no strand within
|
||
X, so the trimmer zone is unstressed; (b) a strand that *should* have been
|
||
pulled toward an opening to reinforce it and was not; (c) the residual of
|
||
"too close" — a gap in the pattern wide enough to matter. **The number and
|
||
the meaning are both his.**
|
||
5. **Is `strand.deflection.is_last_option` a rule or a preference?** It decides
|
||
whether this tool may ever propose a deflection, or may only report that
|
||
relief is needed and name the plant's ranking (recommended). If it is a rule,
|
||
is the ranking exactly debond-then-deflect, or is there a third option
|
||
(re-route, move the obstruction, change the piece)?
|
||
6. **`min_clear_cover` — one number or three?** Cover to the concrete edge, to
|
||
an opening's jamb, and to the outer face of the wythe are three faces and may
|
||
be three numbers, and none of them is the deflected-condition cover already
|
||
in `strand.deflection.min_cover_in`. **The key does not exist. Nothing here
|
||
invents a value.**
|
||
7. **The `max_bed_width` contradiction (A6.1), still parked.** This document
|
||
proposes the new tool reads the **per-bed** table and reports NOT RUN with no
|
||
bed declared, while Ticket QC keeps reading the scalar unchanged. Is that
|
||
acceptable as an interim, or does he want to rule now — scalar goes `null`
|
||
and `beds:` is the answer, or the scalar stays and `beds:` keeps only the
|
||
grid and the capacities?
|
||
8. **Does a tool that never leaves Revit still owe a deck?** Recommending yes,
|
||
for reproducibility and mid-run staleness detection, not because a process
|
||
boundary demands it.
|
||
9. **`interference.check` defaults to `False`.** Its own comment says why: on a
|
||
model that draws bars continuously through strands it reports dozens of
|
||
by-design crossings per piece until `ignore_family_pairs` is filled in. So
|
||
this tool's headline check is off by default and its exemption list is
|
||
plant-specific and, for `plant_a`, partly filled. Does `SL-CLASH` run with
|
||
whatever exemptions exist and say what it did not exempt, or does it stand
|
||
down until the lists are complete? **Reporting hundreds of by-design
|
||
crossings across a half-day run would bury every real finding.**
|
||
10. **The subset rule** (§5.3a) — *the first question to answer.* A design's
|
||
printed strand count is a full-width maximum and a given piece carries a
|
||
subset. Is a subset always **contiguous**, as `_fit_chain` assumes? On a
|
||
narrow panel held to the fixed rail, which end of the chain is dropped? And
|
||
where a piece's strands are not modelled at all, may the tool derive the
|
||
subset (recommended, mode-labelled), or must it report unjudged?
|
||
11. **Which bed does a design cast on, and who records it?** Proposed as a spec
|
||
field; it could as easily be job knowledge keyed by design. Without it, five
|
||
units stand down.
|
||
12. **A piece that changed mid-run** (§6.3): report it stale and keep its
|
||
findings (recommended), or invalidate and re-measure it on resume?
|
||
13. **Does this tool report per design or per piece?** He said "所有的这个
|
||
design number的framing element", so at minimum a roll-up per design. On
|
||
several hundred pieces, is a per-piece page each wanted, or only the pieces
|
||
with findings — and is "no findings" a page worth writing so that its absence
|
||
is never mistaken for a piece nobody looked at?
|
||
14. **May the same run take more than one design?** The button reads one
|
||
pattern; the model has many designs. One design per press is simpler and
|
||
resumable; all designs in one press is the half-day case he may actually
|
||
mean.
|
||
15. **Is the `SL-` prefix on every finding code right**, or does he want the
|
||
collision-free ones bare? Recommending uniform: a prefix applied only where
|
||
it is needed is one nobody can predict.
|
||
16. **Does the strand pattern belong to the design summary, or to a piece?**
|
||
This document argues design summary throughout (§2.2). If any piece of a
|
||
design can legitimately carry a *different* pattern rather than a subset of
|
||
one, the whole per-design framing is wrong and it is better to know now.
|
||
|
||
---
|
||
|
||
## 9. What I am not sure of
|
||
|
||
Stated because a design that hides its soft spots gets built and then
|
||
discovered.
|
||
|
||
1. **Whether the synthesised strand bodies can go through the interference
|
||
pipeline unmodified.** The pipeline's first stage is a sweep along the
|
||
length, and a strand runs the full length — so it prunes nothing against a
|
||
strand, and every strand forms a pair with every body. The measured "under a
|
||
second per piece" was measured on modelled members, and the strand rows in
|
||
those measurements were a handful of bodies. Fourteen synthesised strands
|
||
against a few hundred bodies may not prune the way the measurement suggests.
|
||
**This is the most likely reason the run is slow, and it is testable in an
|
||
afternoon before anything else is built.**
|
||
2. **Whether a distance to a rectangular opening in the piece's local frame is
|
||
the right measurement.** `_openings` returns axis-aligned rectangles; a real
|
||
blockout may be chamfered, sleeved or non-rectangular, and the clear distance
|
||
to its actual jamb is not the distance to its bounding rectangle. The error is
|
||
in the safe direction for "too close" and the unsafe direction for "too far".
|
||
3. **Whether the second wythe is handled.** `strand.both_wythes_same_stations`
|
||
says the stations are shared at `plant_a`, `liftexport.layup_of` already
|
||
tells solid from sandwich, and `beds.<sheet>.strand_rows` is 2 — but I have
|
||
not worked through what a sandwich means for cover to a *face* when the face
|
||
in question is an insulation interface rather than air.
|
||
4. **Whether `PRE-2.5`'s three modes are really three**, or whether the
|
||
derive-it mode is unbuildable without a rule nobody has stated.
|
||
5. **Whether `POST-1.11` can classify inside-panel vs at-panel-edge reliably.**
|
||
The decision table branches on it, and an opening that breaks the edge versus
|
||
one that stops an inch short of it is a judgement the geometry may not make
|
||
cleanly. It needs its own could-not-judge and may need a tolerance the owner
|
||
sets.
|
||
6. **Whether the per-piece geometry stamp in §6.3 catches enough.** Extents plus
|
||
the opening rectangle list catches a moved or resized opening — the exact
|
||
thing `reinforcecore.signature` is documented as missing — but not a changed
|
||
embed that a strand now hits. The member-id set catches an embed added or
|
||
removed, not one moved. There may be a third stamp needed, or the honest
|
||
answer may be that a long run is re-verified rather than stamped.
|
||
7. **The unit catalogue's PRE half is derived from where the existing code is
|
||
cut**, not necessarily from where the questions are. A pass over §5.2 with
|
||
the owner is worth more than another pass by me.
|