Public demo snapshot of an in-house pyRevit extension for precast concrete shop-ticket QC (37 checks / 46 checklist rows) and a standalone panel handling solver (PCI Chapter 8). - No client, job, plant or product data: knowledge layers ship as structural templates only. - Ticket QC's model-read escalation targets a LAN model server endpoint (configurable), not a cloud provider. - Built from the private development repo via tools/make_dist.py.
63 KiB
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 Nones
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.
- It is already the shape a detailer reads — the file's own comment says
columns_inholds "the same numbers a detailer would read off the dimension chain", androwsis a matrix rather than a count because "a correct strand COUNT in the wrong PLACE is still a wrong piece". - It is already consumed. Ticket QC item 4.4 (
check_strand_layout) readsreinforcing.strand_layouttoday viaspecformat.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. - It is per design, which is the granularity the owner asked for — one pattern, many pieces.
- 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: nullis 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.mdclosed 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:
- 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.
is_last_option: trueis the sharper conflict. The plant note readsDEFLECTING 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. Somax_deflectionis 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.strand.deflection.min_cover_inis 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'smin_clear_coverprobably 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-wayleft_rail_is_fixedhandling (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 differentbed_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 returnsNonefor 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.yamlcarriestitle:only. No tooltip, no author: pyRevit renders both on hover and the owner wants nothing there, andtests/test_button_tooltips.pyguards 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__inscript.pymirrorsbundle.yaml'stitle: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:
- Composition is preset by
(plant, member_type)— and that pair is not unique across tools.pipelines.profiles.wall_panel_plant_anames one ordered unit list forplant_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. - 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:
- derive it, and report every derived station as derived;
- refuse to derive it and report
SL-SUBSET-UNJUDGEDon any piece whose strands are not modelled — honest, and possibly quiet on most of the model; - 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.jsonis 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 rulesweep_in_revit.pyalready states. - Resume against different inputs is refused, not silently honoured.
manifest.jsonfreezes 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 withSL-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:
-
Wrong model.
sweep.py'scheck_source_model/model_refusal, adopted whole: compare the manifest's model stem againstdoc.Title, case-insensitively,.rvttrimmed; a missing stamp isunstampedand 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. -
Same model, changed under the run. Four hours in, someone moves an opening.
doc.Titleis unchanged andcheck_source_modelpasses. 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_allexists 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>.jsonrecords the piece's signature and a cheap geometry stamp (concrete extents plus the opening rectangle list, both already read byPRE-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 asSL-PIECE-STALEwith 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-UNJUDGEDcovers 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:
AssemblyInstancecollection and each assembly'sGetMemberIds();- the Structural Framing member's design-number parameter, resolved by role
(
panel.design_number) throughknowledge.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 Topregeneration — 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.
- The tool prefix on unit ids (§5.1).
strands/PRE-1.1andhandling/PRE-2.5from 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. - 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? - An unreviewed pattern: run and label loudly (recommended), or refuse the
way the weight gate refuses?
source.reviewed_byis null on the specs today. - "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.
- Is
strand.deflection.is_last_optiona 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)? 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 instrand.deflection.min_cover_in. The key does not exist. Nothing here invents a value.- The
max_bed_widthcontradiction (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 goesnullandbeds:is the answer, or the scalar stays andbeds:keeps only the grid and the capacities? - 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.
interference.checkdefaults toFalse. Its own comment says why: on a model that draws bars continuously through strands it reports dozens of by-design crossings per piece untilignore_family_pairsis filled in. So this tool's headline check is off by default and its exemption list is plant-specific and, forplant_a, partly filled. DoesSL-CLASHrun 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.- 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_chainassumes? 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? - 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.
- A piece that changed mid-run (§6.3): report it stale and keep its findings (recommended), or invalidate and re-measure it on resume?
- 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?
- 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.
- 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. - 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.
- 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.
- Whether a distance to a rectangular opening in the piece's local frame is
the right measurement.
_openingsreturns 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". - Whether the second wythe is handled.
strand.both_wythes_same_stationssays the stations are shared atplant_a,liftexport.layup_ofalready tells solid from sandwich, andbeds.<sheet>.strand_rowsis 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. - Whether
PRE-2.5's three modes are really three, or whether the derive-it mode is unbuildable without a rule nobody has stated. - Whether
POST-1.11can 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. - 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.signatureis 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. - 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.