Files
willtools-demo/docs/plan-strands-auto-layout.md
T
will 52d94303bc
ci / test (push) Has been cancelled
WillTools: precast shop-ticket QC and panel handling tools
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.
2026-09-03 16:33:47 -05:00

63 KiB
Raw Blame History

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.

  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.