8e6ee31596
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
2028 lines
114 KiB
Markdown
2028 lines
114 KiB
Markdown
# What to check on a shop ticket
|
||
|
||
Developed 2026-08-27 from a real audit of twelve pieces (WP194, 195, 196, 199,
|
||
200, 210, 213, 214, 215, 219, 220, 412 — all design WP1) against the model they
|
||
were drawn from. Every item below is either something the existing checks
|
||
already do, something the audit found they get wrong, or something nobody was
|
||
checking.
|
||
|
||
The organising question for each item is not "is this checkable" but **what
|
||
does it take to check it**, because that decides whether it can be code:
|
||
|
||
| grade | meaning |
|
||
| --- | --- |
|
||
| **A** | pure code. Two values exist in the model; compare them. |
|
||
| **B** | code, once a plant fact is written down. The rule is mechanical, the *constant* is local knowledge. |
|
||
| **C** | code can narrow it to a shortlist; a person decides. |
|
||
| **D** | a person reads the drawing. Code can only make sure they were asked. |
|
||
|
||
---
|
||
|
||
## 0. Before any of it: find the whole ticket
|
||
|
||
**A ticket is not one sheet.** In this model 97 tickets are `1 OF 2` + `2 OF 2`
|
||
under the same sheet name, and 9 are `1 OF 1`. The two sheets carry different
|
||
things:
|
||
|
||
| sheet | carries |
|
||
| --- | --- |
|
||
| `1 OF 2` | `Elevation Top` (the BED PLAN, with the embed callouts), the TICKET NOTE views, the strand jacking force |
|
||
| `2 OF 2` | `Elevation Front`, `Elevation Right` — and the strand count lives in Right |
|
||
|
||
Reading one sheet and calling it the ticket is how the audit produced 60
|
||
findings that were not real. **Grade A** — collect every sheet whose name equals
|
||
the mark, and treat their union as the ticket.
|
||
|
||
---
|
||
|
||
## 1. Identity — is this ticket about this piece
|
||
|
||
| # | check | grade |
|
||
| --- | --- | --- |
|
||
| 1.1 | Sheet name equals the piece mark | A |
|
||
| 1.2 | Sheet's design-number parameter reduces to the piece's design | A |
|
||
| 1.3 | Exactly one ticket set per piece — no orphan sheet, no duplicate | A |
|
||
| 1.4 | **Every placed view's name refers to this design.** | ~~A~~ **B** (re-graded 2026-08-30) |
|
||
| 1.5 | **Printed call-outs do not overlap each other.** | **C** (new 2026-08-30) |
|
||
|
||
### 1.5 — two call-outs printed on top of each other
|
||
|
||
Text-bearing annotations — text notes, tags, keynotes — on the **same placed
|
||
view**, whose view-space bounding boxes intersect with **positive area**.
|
||
WARN, naming both texts and the size of the overlap
|
||
(`CALLOUT-OVERLAP`, `ticketpage.callout_overlap_status`).
|
||
|
||
**Text against linework is deliberately out of scope**, and that is a ruling,
|
||
not an omission. A call-out printed over a drawn edge is the normal condition
|
||
on every elevation on every ticket; flagging it would report the whole sheet
|
||
and teach the reader to skip the row. Only text against text is asked.
|
||
|
||
**Two honesty limits, and the check states both in its own reporting.**
|
||
|
||
1. **A bounding box approximates the printed extent.** It carries the text's
|
||
leading and side bearings, so it is a little larger than the ink, and a
|
||
small intersection can be two notes that print clear of each other. That
|
||
is why the message states the overlap **size** in printed inches, and why
|
||
this is **grade C**: the check narrows the sheet to the pairs worth
|
||
looking at, and a person decides. It is never a defect claim.
|
||
2. **Rotated text reports an axis-aligned box**, which circumscribes the
|
||
rotated glyphs and overstates the ink by up to the length of the string.
|
||
**Those pairs are set aside, not flagged** — reported under
|
||
`CALLOUT-OVERLAP-UNREAD` at OK severity, named, so they can neither
|
||
masquerade as a finding nor silently disappear. An annotation whose
|
||
rotation cannot be read at all is set aside the same way: unknown is not
|
||
the same as zero.
|
||
|
||
Boxes are converted to **printed inches** through the view's scale, because
|
||
inches on paper is the only unit a reader can act on. Verified against the
|
||
live model: a `DUNNAGE` note on a 1:96 view boxes 5.198 x 2.156 ft, which
|
||
prints 0.65" x 0.27" — the size of the word.
|
||
|
||
**Measured before it was built** (bridge read, 2026-08-30, one WP ticket):
|
||
`get_BoundingBox(view)` returned non-null for **22 of 22** text notes, and
|
||
**zero pairs overlapped** across all seven placed views. So on a good sheet
|
||
this row is quiet, and a finding is signal rather than noise — which is the
|
||
condition a WARN-only check has to meet before it is worth adding.
|
||
|
||
1.4 is new and came out of the audit. A view named
|
||
`TICKET NOTE - WP2- STRAND JACKING FORCE` is placed on **100 sheets**, all of
|
||
them WP1 pieces. Reading the content settled it — `0.70 fpu = 28.9 kips` is
|
||
WP1's own number, so the note is right and the *name* is a leftover. That is the
|
||
point: a stale name on a correct note is a trap for the next person, and the
|
||
check is one string comparison. **Report the name mismatch; never infer the
|
||
content is wrong from it.**
|
||
|
||
**Re-graded A -> B, 2026-08-30, built the same day** (`VIEW-DSN`,
|
||
`ticketpage.view_name_design_status`). Two reasons, and the first is the
|
||
weaker one:
|
||
|
||
1. It is not one string comparison. Recognising that `WP2` inside
|
||
`TICKET NOTE - WP2- STRAND JACKING FORCE` is a design label at all needs
|
||
the plant's design-token pattern, and comparing it needs the same
|
||
suffix reduction 1.2 uses (`PNL1.0` and `PNL1` are one design). That is
|
||
"mechanical rule, local constant" — the definition of B. The pattern is
|
||
derived from `nomenclature.design_number_re` rather than added as a
|
||
second plant fact, so nothing new has to be written down.
|
||
2. **It is never worth failing a ticket over, and that is what settles it.**
|
||
This check sits in tension with a rule this project already holds: a view
|
||
name does not identify a design. The check does not break that rule, it
|
||
depends on it — the design comes from the sheet's design-number
|
||
parameter, and 1.4 only reports a view that *labels itself* and labels
|
||
itself wrongly. So a 1.4 finding is never evidence about the piece. The
|
||
audit proved exactly that: the WP2-named note was WP1's own correct
|
||
number. What it protects against is a **reader**, in both directions —
|
||
someone believing a WP2 note on a WP1 sheet, or deleting a correct note
|
||
because its name looked foreign. That is hygiene, WARN, grade B.
|
||
|
||
One deliberate blind spot: only a token whose letter prefix matches this
|
||
design's is compared, or `Elevation Top 2` reduces to `TOP2` and every
|
||
ticket grows a false finding. It can therefore miss a label from an entirely
|
||
different piece family. For a WARN, silence is the right way to be wrong.
|
||
|
||
## 2. Dimensions and mass
|
||
|
||
| # | check | grade |
|
||
| --- | --- | --- |
|
||
| 2.1 | Printed length / width / depth vs modelled | B — on stemmed pieces the printed length is nominal and runs over by design, so the tolerance is a plant fact |
|
||
| 2.2 | **Weight, three ways, all three must agree** — see below | A |
|
||
| 2.3 | Unit weight on the ticket vs the design summary | A |
|
||
| 2.4 | Cast extras (a ledge is its own family) counted in the weight | A |
|
||
|
||
### 2.2 — weight is a three-way agreement, not a comparison
|
||
|
||
Two numbers agreeing proves nothing about which one is right. Three
|
||
independently derived numbers agreeing is evidence the piece is what everybody
|
||
thinks it is, and **any one of them disagreeing tells you where to look**:
|
||
|
||
| # | number | derived from |
|
||
| --- | --- | --- |
|
||
| a | **printed** | the WEIGHT (lbs.) field in the ticket's title block |
|
||
| b | **reconstructed** | length x width x thickness, less the blockouts, plus the cast extras, x unit weight — the piece as the drawing describes it |
|
||
| c | **measured** | the actual volume of the modelled solid x unit weight — the piece as it was drawn |
|
||
|
||
What each disagreement means:
|
||
|
||
- **b and c agree, a differs** — the model is consistent with itself and the
|
||
ticket's weight field is stale or was overridden. The piece is probably fine
|
||
and the print is wrong.
|
||
- **a and b agree, c differs** — the stated dimensions and the printed weight
|
||
tell the same story but the solid does not match them. Something was modelled
|
||
that the dimensions do not describe: an uncut opening, an unrecorded recess,
|
||
a ledge, a chamfer taken too far.
|
||
- **a and c agree, b differs** — our reconstruction is wrong, which means our
|
||
reading of the geometry is wrong: a blockout missed, a cast extra not counted,
|
||
a thickness read from the wrong parameter. Suspect the tool first.
|
||
|
||
`vibe.liftexport.weight_check` already computes all three and calls them
|
||
`stated`, `reconstructed` and `solid`. What it does not yet do is report the
|
||
**pattern** of the disagreement, which is where the diagnosis is.
|
||
|
||
Both failure directions have been seen on this job. A slab-seat ledge left out
|
||
of the reconstruction understated one piece by 7.5%, and the gate refused a
|
||
model that was right — that is the third row. On four other pieces the `_CAST`
|
||
volume parameter and the real solid differ by 14–15 ft3 in both directions,
|
||
which is the second row, and it is a model-side problem.
|
||
|
||
Do not report a weight finding without saying which of the three disagrees.
|
||
|
||
### 2.4 — the extras the ticket draws vs the extras that were counted
|
||
|
||
Built 2026-08-30 (`CAST-EXTRA`, `check_cast_extras` /
|
||
`ticketpage.cast_extra_agreement`). It is a different question from 2.2 and
|
||
the difference is the whole reason it exists: **2.2 compares three weights,
|
||
and all three can be wrong the same way.** A ledge left out of the model AND
|
||
off the drawing is missing from the printed weight, from the reconstruction
|
||
and from the solid alike — they agree with each other, 2.2 says OK, and the
|
||
piece is 7.5% light. 2.4 compares the *set*, not the number.
|
||
|
||
**Telling the two sides apart.** "The extras that were counted" is not a
|
||
re-derivation: `liftexport.extra_cast_ft3` was split so its member list
|
||
(`extra_cast_members`) is the shared thing, and 2.4 reads exactly the members
|
||
whose `Volume` fed 2.2's reconstructed weight. Two implementations of "which
|
||
family is cast concrete" would drift; there is one. "The extras the ticket
|
||
draws" is read from the ticket's **words** — a BOM row, or the text on a
|
||
placed view, naming one of the same `families.cast_include` tokens.
|
||
|
||
That is a proxy and the check says so rather than implying otherwise: the
|
||
ledge is drawn as *geometry*, and no query answers "does this elevation show a
|
||
ledge". What 2.4 can honestly assert is that the two sides *name* the same
|
||
extras.
|
||
|
||
Three findings it produces, none of them reachable from 2.2:
|
||
|
||
- the ticket names an extra the assembly does not carry — **the case above**;
|
||
- an extra is in the weight that nothing on the ticket names;
|
||
- a cast member whose `Volume` could not be read: it added **nothing** to the
|
||
reconstruction, silently. Drawn, and not counted.
|
||
|
||
**The five BOM states are respected, not collapsed** (`ticketpage.bom_state`).
|
||
OK and PARTIAL are judged, PARTIAL naming what failed. ABSENT is a fact about
|
||
the ticket, so it is judged from view text and the message says that is all it
|
||
had. UNREADABLE and UNKNOWN are failures of *ours* — the row that names the
|
||
ledge may exist and we missed it — so "the ticket does not name it" is not
|
||
assertable and the comparison stands down to NOT RUN with the BOM's own
|
||
reason. The unreadable-volume finding still fires there, because it needs no
|
||
ticket at all.
|
||
|
||
**What it cannot detect, stated in the OK itself:** an extra missing from both
|
||
the model and the drawing. Nothing on either side is a genuine pass, but the
|
||
message says out loud that the blind spot was not closed.
|
||
|
||
## 3. Concrete
|
||
|
||
| # | check | grade |
|
||
| --- | --- | --- |
|
||
| 3.1 | f'c on the ticket vs the design summary | A |
|
||
| 3.2 | f'ci on the ticket vs the design summary | A |
|
||
| 3.3 | f'ci / f'c release ratio inside the plant's band | B |
|
||
|
||
**The audit's f'c and f'ci findings were false.** Both were raised as "in the
|
||
summary, not found on the ticket" because the tool was reading only sheet
|
||
`2 OF 2`. Fix item 0 first or these two produce noise on every ticket.
|
||
|
||
## 4. Strand
|
||
|
||
| # | check | grade |
|
||
| --- | --- | --- |
|
||
| 4.1 | Printed strand count vs strands modelled | B — whether the printed `(n)` is per-piece or a design maximum is a plant fact |
|
||
| 4.2 | Printed diameter and grade vs the design | A |
|
||
| 4.3 | Jacking force on the ticket vs the design, within `tolerances.it_lb` (**100 lb**, owner 2026-08-30 — the same 0.1 kips the check always used, now a knob rather than a literal) | A |
|
||
| 4.4 | Strand positions vs the design's dimension chain, from the stated rail | B — which rail is fixed is plant knowledge and is never inferable |
|
||
| 4.5 | Debonded strands called out where the design debonds | C |
|
||
| 4.6 | Row count and spacing plausible for the section | A |
|
||
| 4.7 | Strand debonded wherever the concrete along it runs short | B — the length is a plant fact |
|
||
|
||
### 4.4 — what "never inferable" does and does not forbid
|
||
|
||
The rule bars inferring **which rail the design summary dimensions from**. That
|
||
is a plant fact and guessing it silently reverses every dimension in the chain.
|
||
|
||
It does **not** bar measuring from both edges. Strands get rotated during
|
||
reinforcing, so the chain legitimately has to be tried against both sides and
|
||
the fitting one kept. `check_strand_layout` does exactly that and is correct.
|
||
Ruled by the engineer 2026-08-30, after this was raised as a possible
|
||
violation. It was not one.
|
||
|
||
### 4.5 — does the ticket say so?
|
||
|
||
Built 2026-08-30 (`STRAND-CALLOUT`, `check_debond_callout` /
|
||
`ticketpage.debond_callout_status`). Will's ruling, verbatim: *"4.5 做掉。
|
||
如果满足 debond 要求,那么就期待有 debond,没有 debond 给个 warning。"* —
|
||
where the debond condition is met, expect a call-out; where there is none,
|
||
WARN.
|
||
|
||
**It does not re-derive the geometry. It consumes 4.7.** Both items now read
|
||
one `_debond_requirement(t, know)`, cached on the ticket, so there is exactly
|
||
one place that decides whether a run is short and the two items cannot
|
||
disagree. Three consequences follow, all deliberate:
|
||
|
||
- **If 4.7 could not judge, 4.5 cannot either.** It returns nothing and its
|
||
row reads NOT RUN *repeating 4.7's own reason*. Warning that a call-out is
|
||
missing, when nothing established a requirement, would be inventing the
|
||
requirement.
|
||
- If 4.7 judged and found nothing required, 4.5 is an OK that says so — not
|
||
silence.
|
||
- A call-out where nothing is required is **not** flagged. Only one direction
|
||
warns, because that is the ruling.
|
||
|
||
The ticket side is `strand.debond_callout_tokens` — plant nomenclature, empty
|
||
by default, so a plant that has not written down its own wording gets NOT RUN
|
||
with that reason and never a pass. Measured: this plant's BED PLAN prints a
|
||
bare two-word note among the embed callouts, and the manufacturer's standards
|
||
carry a whole note sheet whose entire content is a longer form of the same
|
||
words.
|
||
|
||
**Not merged with the manufacturer's own debond standard**, which fixes a
|
||
length at each end keyed on strand *diameter*. That is a bed standard; the
|
||
threshold in 4.7 is a geometric trigger. Different rules, and the manual's
|
||
fixed length must never be substituted for the threshold.
|
||
|
||
**Still grade C, and the OK admits why.** The call-out is a bare note: no
|
||
station, no strand number. A present call-out is evidence that somebody
|
||
debonded something, not that they debonded *these* runs — so the OK says, in
|
||
those words, that it was not verified. Code narrows; a person closes it.
|
||
|
||
### 4.7 — a short run of concrete cannot develop a strand
|
||
|
||
Not the same question as 4.5. That one asks whether the ticket calls out the
|
||
debonding the design specified; this one derives from the geometry where
|
||
debonding is *required*, and can therefore catch a design that failed to
|
||
specify it.
|
||
|
||
Along the strand direction, measure the runs of continuous concrete between
|
||
openings. Where a run is shorter than `strand.debond_below_continuous_ft`
|
||
(4 ft here), the strand crossing it must be debonded.
|
||
|
||
The reason decides the shape of the check, so it is worth stating. Openings
|
||
leave little column sections. A strand crossing one has nowhere to develop —
|
||
its transfer length exceeds the concrete available, so it never reaches force
|
||
and carries essentially nothing. That alone would only be waste; the harm
|
||
comes at release, when burning the strands drives a shock transfer through a
|
||
stretch too short to take it, and a flexural crack opens.
|
||
|
||
Bursting steel answers part of the problem — the Poisson expansion as the
|
||
strand shortens — but not this part. **The instantaneous flexure from the
|
||
bursting force has no detailing remedy.** Debonding is the answer, which is
|
||
why this is a rule and not a preference.
|
||
|
||
The geometry is already in hand: `liftexport._openings` returns every void as
|
||
`(x0, x1)` along the length, and the ROW-FLEX work already cuts the piece at
|
||
every opening edge. The uncut runs between those cuts are exactly what this
|
||
measures.
|
||
|
||
|
||
On 4.1 the audit found five of the twelve print `(14)` while 10 or 6 are
|
||
modelled. That is a real discrepancy to put in front of a person; it is not
|
||
automatically a defect, because the printed count may be the design's maximum.
|
||
|
||
**Where the strand data actually is:** the count is inside the `Elevation Right`
|
||
detail view (`(14) 0010001 (E.L.)`), and the jacking force is in a TICKET NOTE
|
||
view. There is **no separate pattern-legend view on any of the 204 WP sheets in
|
||
this model.** The existing `LGD-MISSING` check looks for a placed view whose
|
||
name starts with `STRAND PATTERN` or `REINFORCING PATTERN`; it fires on every
|
||
WP ticket and always will. Either the prefixes are wrong for this plant or the
|
||
concept does not apply here — it is a knowledge-file question, not 204 defects.
|
||
|
||
**Answered, Phase 2:** the concept does not apply. `legend.pattern_prefixes` is
|
||
empty for this plant and `LGD-MISSING` now asks about the BOM schedule
|
||
instead, so the 204 false positives are gone. Two things about it are still
|
||
open — a "could not read the BOM" that reports as a defect, and the fact that
|
||
no checklist item owns `LGD-*` so neither verdict reaches the report table.
|
||
Both are written up in `docs/checklist-code-map.md`.
|
||
|
||
## 5. Reinforcing and embeds
|
||
|
||
| # | check | grade |
|
||
| --- | --- | --- |
|
||
| 5.1 | Every embed on the BED PLAN callouts exists in the model — **two halves, one of them unbuilt and saying so** (2026-08-30) | A |
|
||
| 5.2 | Every embed in the model appears on the ticket | A |
|
||
| 5.3 | Face designations (N.F. / F.F. / E.L. / S.I.F.) consistent with the model side | C |
|
||
| 5.4 | Mesh / WWF designation vs the design | A |
|
||
| 5.5 | Bar marks **legible** on the drawing | D |
|
||
| 5.6 | **Bar marks unique, and model and print name the same bars** | **B** (new 2026-08-30) |
|
||
| 5.7 | **No bar longer than the stock it is cut from** | **B** (new 2026-08-30) |
|
||
| 5.8 | Connectors present on an insulated panel | B (built 2026-08-31) |
|
||
| 5.9 | Enough connectors for what the design calls for | B (built 2026-08-31) |
|
||
| 5.10 | Connector edge distances | B (built 2026-08-31) |
|
||
| 5.11 | **Every lifter's solid zone is a member of the assembly** | **B** (built 2026-08-31) |
|
||
| 5.12 | **Solid zones appear on the ticket's insulation layout** | **B** (built 2026-08-31) |
|
||
| 5.13 | **Solid zone hatch matches the legend swatch** | **B** (built 2026-08-31) |
|
||
| 5.14 | **Insulation recess regions present where the design calls for them** | **C** (built 2026-08-31) |
|
||
| 5.15 | **Recess region hatch matches the legend swatch** | **B** (built 2026-08-31) |
|
||
|
||
### 5.1 asks two questions and only one of them was ever asked (2026-08-30)
|
||
|
||
`docs/checklist-code-map.md` has said for a while that 5.1's owner code does
|
||
not implement 5.1 as written: `check_bom_members` compares the **BOM's named
|
||
rows** against the assembly's members, which is a real comparison and not the
|
||
BED-PLAN one this row describes. The row printed OK off it anyway.
|
||
|
||
The half that was missing has a shape nobody had written down. Not every
|
||
call-out on the BED PLAN is a part. This plant collects its typical sections
|
||
onto **detail sheets of their own** and the plan cites one as a bubble — a
|
||
detail number over a sheet name — whose embeds are drawn *there* and never on
|
||
the plan. `nomenclature.detail_sheet_prefix` is the plant naming that series,
|
||
and it is the only thing that tells a bubble from a part call-out. It was
|
||
declared in the plant file, documented, and **read by nothing** (D5 in
|
||
`QNA/drift-triage.md`). The owner's ruling was one line: *"那倒是读啊。。"*
|
||
|
||
It is read now, and what it buys is exactly one thing: the ability to see
|
||
that the plan reaches past itself. **Nothing here opens a detail sheet.**
|
||
Following a bubble to that sheet and comparing the parts it draws against
|
||
this piece's members needs the sheets themselves, and that is a job of its
|
||
own — so `check_detail_callouts` stops at finding the bubbles and reports
|
||
**NOT RUN naming the sheet nobody read**. There are three outcomes and only
|
||
one of them is a pass:
|
||
|
||
| the plan | 5.1's detail half |
|
||
| --- | --- |
|
||
| no `detail_sheet_prefix` declared | NOT RUN — a bubble cannot be told from a part call-out, so the plan cannot even be read for them |
|
||
| a prefix, and the plan cites no detail | **OK, and a real verdict** — every call-out on the plan names a part directly, and nothing on this piece reaches the model through a detail sheet |
|
||
| a prefix, and the plan cites details | NOT RUN, naming the detail and its sheet — those embeds have not been asked about |
|
||
|
||
**5.1 is therefore the first row backed by two checks**, and it is answered
|
||
only when both spoke. `CHECKLIST_ITEMS` takes a tuple in its check slot and
|
||
`checklist_states` makes any quiet half NOT RUN, however green the other one
|
||
is. That direction is deliberate: a row that asks two questions and prints OK
|
||
because the half which could still have failed it was never asked is the
|
||
silent pass this whole table exists to prevent. Under-claiming is the safe
|
||
direction; a green row is a claim.
|
||
|
||
What is still not built, stated plainly so nobody reads the above as more
|
||
than it is: **the contents of a detail sheet.** Deriving
|
||
`drawings.called_out_on_detail` — today a hand-kept job-layer map of which
|
||
family is drawn on which detail — by actually reading the X-sheets is the
|
||
same missing capability seen from 5.2's side, and it needs the model.
|
||
|
||
### 5.3 reads FOR ENGINEER, not NOT RUN (fixed 2026-08-30)
|
||
|
||
The owner read a live run and reported "5.3 没跑". It had not run, and it never
|
||
will — it is one of the two rows this document has counted as *a person's job
|
||
by design* since it was written. But it printed **NOT RUN**, in red, in the
|
||
same bucket as FAIL, because `CHECKLIST_FOR_ENGINEER` was a hand-kept tuple
|
||
holding only `5.5` and nobody added 5.3 to it.
|
||
|
||
That list is now **derived from the table**: a row with no check function, no
|
||
owner finding code, and a reason spelled out is a row somebody declared a
|
||
person's. A row missing any of the three is an unbuilt row and stays red. The
|
||
reason string in the row is also what the cell prints, so 5.3 no longer
|
||
carries a stock sentence about grade D — it is grade C.
|
||
|
||
### 5.4 stands down when the design has no mesh, and says which silence it is
|
||
|
||
Also 2026-08-30, also read as "没跑". 5.4 ran; the design it was checking
|
||
records no mesh at all (`reinforcing.wwf.mesh: null` in its extracted spec —
|
||
its mild reinforcing is a bar line), so there was no design side to compare
|
||
and the row was right to stay unjudged. The probe's one sentence, however,
|
||
covered three different situations and sent the reader to Extract Design in
|
||
all of them, including the one where re-extracting returns the same nothing.
|
||
It now distinguishes: **no spec at all** (run Extract Design), **a spec with
|
||
no `wwf` section** (it predates the field — re-run it), and **a spec that
|
||
records no mesh** (nothing to re-extract; either the design uses bars, or the
|
||
summary's mesh line was not parsed, and only the summary view settles which).
|
||
All three stay NOT RUN.
|
||
|
||
### 5.6 / 5.7 say what could not be *recognised*, not what the piece has not got
|
||
|
||
Both stand down on the same gather and printed the same paragraph twice, one
|
||
row under the other, opening with *"no reinforcing found on this piece"*. That
|
||
is a claim about the piece, and with `drawings.reinforcing_families` empty the
|
||
tool is in no position to make it — this plant models bars outside any rebar
|
||
category, so a piece with three hundred members produced the same words as a
|
||
piece with no steel in it. The reason now says the tool could not **recognise**
|
||
any, says what each row lost (a mark comparison / a length comparison), and
|
||
names the key that would make it run. When the key **is** filled in and the
|
||
search still comes back empty, it says the piece appears to carry none, and
|
||
names what it looked for. Both rows stay NOT RUN and red either way — naming
|
||
the switch is not flipping it.
|
||
|
||
### 5.5 split into 5.5 and 5.6 — the two lines that matter
|
||
|
||
**What moved to code (5.6):** *uniqueness*. Whether one mark names two parts,
|
||
whether a printed mark exists in the model, and whether a modelled bar is ever
|
||
printed are all comparisons between the model's marks and the sheet's text —
|
||
and a comparison is code's job.
|
||
|
||
**What stayed with eyes (5.5):** *legibility*. Whether the printed glyphs can
|
||
actually be read is a property of the picture, which no query answers. Grade D,
|
||
still `FOR ENGINEER`, and the vision path (`ticketvision`, `AI-BARMARK`) is
|
||
unchanged and still escalates it.
|
||
|
||
### 5.6 — bar marks unique, model against print
|
||
|
||
Three questions, three different defects, each its own WARN under
|
||
`BARMARK-UNIQUE` (`ticketpage.bar_mark_uniqueness_status`):
|
||
|
||
| | |
|
||
|---|---|
|
||
| **one mark, two parts** | two bars sharing a mark but differing in family or length. The mark is what a fabricator cuts to, so this is the expensive one. |
|
||
| **printed, not modelled** | the sheet calls out a part the model does not carry. |
|
||
| **modelled, never printed** | the part exists and the drawing never names it. |
|
||
|
||
A bar carrying **no mark at all** is none of the three: it is could-not-judge,
|
||
reported under `BARMARK-UNREAD`, and is never counted as "never printed" — an
|
||
absent mark cannot be looked for in the printed text.
|
||
|
||
**The design came from a live read, not from the spec's assumptions**, and the
|
||
read changed it twice.
|
||
|
||
- **The spec said tags. There are no tags.** Measured on a bar-carrying WP
|
||
ticket: **zero** `IndependentTag` and **zero** keynote elements on either
|
||
sheet. Every printed bar call-out is a plain **TextNote on a Detail view**.
|
||
The tag path is still gathered — a plant that tags must not fall through to
|
||
silence — but nothing here depends on it. One specimen, one plant: worth
|
||
re-confirming before any tag path is deleted.
|
||
- **The printed string contains the mark; it never equals it.** Verbatim off
|
||
the sheet: `SR324 (F.F.)`, `SR310 (E.L.)`, `P76 W/ SR5602 (S.I.F.)` — a bare
|
||
mark plus a face designation. An equality comparison would have reported
|
||
every call-out on every ticket as an orphan. The match is a whole-token
|
||
containment, and the same reduction runs on both sides so an unrecognised
|
||
shape goes missing from both and produces silence, never a false finding.
|
||
- **Note-view pollution: real, and separable by view TYPE.** The note views
|
||
are Legends carrying headings and the jacking-force paragraph; the call-outs
|
||
live on Detail views. Legends are excluded — a view-type test, so no plant
|
||
nomenclature is needed and none was invented.
|
||
|
||
#### Two narrowings, both off the knowledge layers (2026-08-31)
|
||
|
||
Both landed on one live complaint — *"SR4996 是 SP29/30 的附属 rebar,我记得我增加
|
||
过这条知识了,为什么还在 WP117 报 warn?"* — and he was right twice over. The
|
||
shape, the merge law and the refusals are in `docs/knowledge-override.md`;
|
||
what a reader of the report sees is this.
|
||
|
||
**A bar covered by its parent's callout.** `SR4996` ships inside the SP29/SP30
|
||
angles and is never drawn on its own. That was recorded in three places and
|
||
**5.6 read none of them**, so the "modelled, never printed" leg reported it.
|
||
It now consults `drawings.annotation_covered_by_parent` (through
|
||
`knowledge.attached_parents`) and produces one of three answers, never a
|
||
silence:
|
||
|
||
| the parent | the row |
|
||
| --- | --- |
|
||
| **is printed** | the bar is accounted for, and the coverage is **named in the sentence** — *"SR4996 is attached to SP29/SP30"* |
|
||
| **is not printed** | **still a WARN, and now it is about the PARENT.** The bar is unaccounted for either way; what is missing is the parent's callout |
|
||
| **cannot be read** | **no claim either way** — nothing in the parent's name is shaped like a part number, so whether the ticket calls it out could not be established |
|
||
|
||
**A printed token that is not a bar mark at all.** 5.6 took every mark-shaped
|
||
token for a candidate, so WP115 reported `P78`, `P114`, `SP29`, `SP30` and
|
||
`SP31` as *"printed bar marks matching no bar in the model"*. They are
|
||
hardware. A token is now classified through the plant's part-number grammar
|
||
(`part_numbers.prefixes`, `kind: hardware` / `kind: bar_mark`), and
|
||
`P78 W/ SR5502 (S.I.F.)` splits itself — the part is set aside, **the bar
|
||
after `W/` still counts**.
|
||
|
||
Three properties hold the honesty, and each is a way this could have gone
|
||
quiet instead:
|
||
|
||
- **Only `kind: hardware` removes a token from the comparison, so only it is
|
||
gated** on a `verified: true` prefix. An unverified hardware claim is
|
||
refused and the token is reported as unplaceable. `kind: bar_mark` needs no
|
||
gate — the worst it can do is add noise.
|
||
- **A token the grammar cannot place is its own WARN**, could-not-judge, never
|
||
a quiet drop. A printed `MK12` says the grammar has a hole and names it.
|
||
- **A plant that has declared no grammar loses nothing.** Nothing is narrowed
|
||
and every token stays a candidate, exactly as before — noisy, never quiet.
|
||
|
||
**Everything set aside is stated on the row**, on a green row and on a red one
|
||
alike. A narrowing you can only see when everything else passed is a
|
||
narrowing nobody audits on the runs that matter.
|
||
|
||
### 5.7 — no bar longer than the stock it is cut from
|
||
|
||
Owner's ruling, 2026-08-30: *"rebars come in 40ft stock. unless overwritten by
|
||
other knowledge, all rebars pieces must be less than 40ft."* Stock is industry
|
||
data, not a plant habit, so the number is published in
|
||
`knowledge/catalogue.yaml` as `catalogue.rebar.stock_length_ft` and any higher
|
||
layer may override it. **With the key null the check does not run and says so —
|
||
it never assumes 40.**
|
||
|
||
**The length is READ, never measured.** Owner, same day, on a bar's length
|
||
with bent bars included: *"钢筋的长度,即使是弯筋的长度,也都不要自己测量"*
|
||
— never measure it yourself; read the parameter. He named the parameter; the
|
||
name is written down in the plant layer under the `member.length` role, which
|
||
is where a parameter name belongs. The check reads that role and nothing else. There is no geometry fallback
|
||
and there must never be one: what a bent bar consumes off the stick is its
|
||
**developed** length, and a bounding box only sees the envelope it folds into —
|
||
it understates every bent bar and would pass exactly the pieces this check
|
||
exists to catch. A bar without the parameter is **could-not-judge, named**.
|
||
|
||
**Three states, not two.** Verified on the live model: the parameter sits on
|
||
the instance and its internal unit is decimal feet (4.41666… reads as 4'-5").
|
||
A length of **0.0** is its own state — 260 elements hold one — and is neither a
|
||
length nor an absence, so it is neither passed nor flagged.
|
||
|
||
**Grouped by mark, and the reason is arithmetic.** The model carries **2,122**
|
||
elements at or over 40 ft (max exactly 60.00). As one finding per element that
|
||
is a wall of noise that buries every other row; as one finding per **mark** it
|
||
is a short list a person can rule on — and mark is also the grain the owner's
|
||
ruling will arrive in.
|
||
|
||
**What it does not claim, yet.** A bar over stock is not automatically a
|
||
defect: a continuous or spliced run is a real detail here (the catalogue
|
||
records `x CONT. (SEE PLAN) LAP SPLICE`). Whether the 60 ft marks in this model
|
||
are those **is with the owner and has not been answered**. So the WARN says the
|
||
ruling is pending, and the answer when it comes is a list in
|
||
`catalogue.rebar.stock_exempt_marks` — **a config edit, not a code change**.
|
||
Nothing is pre-exempted, and what *is* exempted is listed in the report rather
|
||
than silently dropped. The key is matched **exactly**, never as a substring, or
|
||
an entry for `SR1` would swallow `SR1101`.
|
||
|
||
#### Two fields carry a bar's size, and they cover disjoint populations
|
||
|
||
Every bar is measured against the stock length **for its own size**
|
||
(`catalogue.rebar.stock_by_size`, per-size, in integer inches). A bar whose
|
||
size cannot be resolved is **not judged at all** — not against the generic
|
||
number either, because *"40 ft"* and *"40 ft for a #4"* are different claims
|
||
and the generic one is wrong in the direction that passes a defect.
|
||
|
||
The size is reached two ways, and **neither is the other's fallback**.
|
||
Censused over 24,539 rebar members in 13 families, 2026-08-31:
|
||
|
||
| population | field | coverage |
|
||
| --- | --- | --- |
|
||
| the one **straight** family | the size parameter, `parameters.member.bar_size` | that family, and no bent one |
|
||
| all twelve **bent** families | the size at the front of the **family's** part description, `#N BENT BAR (<code>)` | 100% on the type, instance count zero; 7,405 of 7,405 parse |
|
||
|
||
**The two sets are disjoint** — no bar carries both — so the order
|
||
`knowledge.bar_size_of()` tries them in has never decided anything and must
|
||
not be read as a ranking. Bent bars are roughly **30%** of the reinforcing in
|
||
this model, and until 2026-08-31 only the straight half was wired: the size
|
||
role had landed and `ticketchecks._bar_size` still read that role and nothing
|
||
else, so every bent bar went unjudged.
|
||
|
||
**Which element the description is read off is load-bearing.** An *assembly's*
|
||
identity description is the owner's freeform notes; a *family's* is structured
|
||
plant data describing the part — one parameter name over two kinds of element.
|
||
`drawings.bar_size_rule.description_read_from` is what keeps them apart, and
|
||
`docs/knowledge-override.md` records the correction.
|
||
|
||
**The parse stays strict**, and not because the field is unreliable: the shape
|
||
`#N BENT BAR` **is** the evidence that the leading token is a bar size, so a
|
||
reworded description stops being evidence and the bar goes **unjudged rather
|
||
than mis-sized**. A leading *bare* number is refused for the same reason.
|
||
|
||
**The finding says which field answered.** `BAR-LENGTH-SIZE-BASIS` — its own
|
||
code, owned by no row, so provenance can neither turn 5.7 green nor vanish
|
||
from the report — names how many bars were sized *read from the size field
|
||
behind `parameters.member.bar_size`* and how many *parsed from the part
|
||
description*, with the sizes. A verdict that cannot say what it read cannot be
|
||
audited. The over-stock line names the size too: *"SR1101 at 60 ft vs its 40
|
||
ft stock for a #5"*.
|
||
|
||
**Exempt by mark, not by family** — a deliberate departure from how
|
||
`_is_reinforcing` identifies a bar. That function asks *is this a bar at all*,
|
||
where family is the right grain. This asks *may this bar be longer than a
|
||
stick*, which is a property of the part: one family here carries both 11 ft
|
||
bars and 60 ft ones, so exempting by family would exempt every bar at the
|
||
plant and switch the check off.
|
||
|
||
**Today it is dormant here, and that is correct.** Which members are bars is
|
||
decided by `_is_reinforcing` and by nothing else — Revit's own category, plus
|
||
`drawings.reinforcing_families`. This plant models its bars as **Specialty
|
||
Equipment**, so the category signal says nothing, and the families key is
|
||
**empty**. Until it is filled in, 5.6 and 5.7 report NOT RUN **naming that
|
||
key**. That is the conservative outcome, and it is also what keeps the length
|
||
check from opening with 2,122 findings derived from a family list nobody has
|
||
vetted.
|
||
|
||
#### `drawings.reinforcing_families` is an armed switch — what one edit does
|
||
|
||
Worth writing down before anyone throws it, because it is **not** one key and
|
||
it does **not** turn on three checks cleanly. Populating it:
|
||
|
||
- **flips 5.6 and 5.7 live.** Neither needs a second key. Expect, on the first
|
||
run over this model, **2,122** mark-grouped bar-length findings and **260**
|
||
present-but-zero lengths reported as could-not-judge — the population the
|
||
"zero is its own state" branch was written for, about 11% the size of the
|
||
≥40 ft set.
|
||
- **does not, on its own, flip 5.2's reinforcing half.** That half is gated by
|
||
a *second* key, `drawings.full_length_bar_fraction`, which is **also null by
|
||
default and is deliberately not guessed** — the standard says "full length
|
||
bar" and never says how short still counts. With bars newly visible but the
|
||
fraction unset, `bar_drawing_home` returns `None` for **every** bar, so
|
||
every bar family lands in `unclassified` and 5.2 grows a new
|
||
`BED-REVERSE-UNCLASSIFIED` line naming them all — and where bars are the
|
||
only thing left to compare, 5.2 flips from OK to **NOT RUN**.
|
||
|
||
So the honest framing is **two checks go live, and a third changes behaviour
|
||
in a way that is neither working nor broken.** Throwing both keys together is
|
||
the edit that actually lands 5.2's reinforcing half; throwing only the first
|
||
is safe but will visibly change 5.2, and a reader who was told "three checks
|
||
turn on" would read that change as a regression.
|
||
|
||
5.1 and 5.2 are worth building: `Elevation Top` on sheet 1 reads as a flat list
|
||
(`P76 W/ SR5602 (S.I.F.)`, `P106 (S.I.F.)(E.S.)`, `P136 (N.F.)` twice,
|
||
`DEBOND STRAND`, `SP126 (S.I.F.)`, `SR309 (E.L.)`) and the assembly's member
|
||
families are directly comparable to it. Nested children need the
|
||
`SuperComponent` filter or the counts double.
|
||
|
||
## 6. Handling and lifting
|
||
|
||
| # | check | grade |
|
||
| --- | --- | --- |
|
||
| 6.1 | Insert capacity vs the load each carries | A |
|
||
| 6.2 | Insert edge distances | A |
|
||
| 6.3 | A current handling result exists and passes | A |
|
||
| 6.4 | Dunnage not landing under an opening | A |
|
||
| 6.5 | The DUNNAGE / ERECTION / PICKING-STRIPPING notes actually say something | ~~A~~ **B** (re-graded 2026-08-30) |
|
||
| 6.6 | An odd count of one part carries the BED PLAN note it requires | B (built 2026-08-31) |
|
||
| 6.7 | Minimum spacing between lifting anchors | A (built 2026-08-31) |
|
||
| 6.8 | Printed picking stations match the modelled picking inserts | B (built 2026-08-27) |
|
||
| 6.9 | Lifting-point count vs panel length (plant rule) | A (built 2026-09-02) |
|
||
| 6.10 | Lifting-point position vs PCI recommendation (plant rule) | A (built 2026-09-02) |
|
||
| 6.11 | Dunnage-point count vs stripping-point count (plant rule) | A (built 2026-09-02) |
|
||
| 6.13 | Lifter accessory (cup/patch) is a member of the assembly | A (built 2026-09-08) |
|
||
|
||
Rows 6.8 - 6.11 were built in the days after this table was last edited and
|
||
sat unlisted here until 2026-09-08 (issue #26 read the section as ending at
|
||
6.7 in the doc and 6.10 in the code; the code and `checklist.master` in
|
||
`knowledge/catalogue.yaml` both run to 6.11). **The catalogue is the
|
||
authority for what exists; this table is the narrative.** 6.12 is unused: the
|
||
`FLEXURAL_CHECKS_ALREADY_ASKED` comment in `ticketchecks.py` records that a
|
||
"6.12" built to an earlier draft of the flexural table would have been 6.10
|
||
twice, which is why issue #26's lifter-accessory membership check (a nested
|
||
cup / patch that fell off its anchor's assembly) is numbered **6.13** rather
|
||
than reuse it.
|
||
|
||
**6.9 / 6.10 / 6.11 no longer print as NOT RUN on a plant without the gating
|
||
keys.** Until 2026-09-08 a job whose knowledge layer had never declared
|
||
`lifters.lifting_points.two_point_max_length_ft` / `.two_point_absolute_
|
||
max_ft` / `.pci_position_tolerance_in` / `.dunnage_matches_stripping` (any
|
||
plant that is not MC) still showed all three rows inline as red NOT RUN --
|
||
indistinguishable from a check that failed on a real piece, and read by the
|
||
owner on another plant's job as "a pile of MC checks." `ticketchecks.PLANT_GATE_
|
||
KEYS` now names, per check, the knowledge key(s) that gate it; when a job
|
||
has not set them all, `ticketqc_run.py` pulls the row out of the coloured
|
||
table entirely and lists it, with the missing key(s), in a folded "Not
|
||
applicable at this plant" section instead -- not counted in the header's
|
||
OK/WARN/FAIL/NOT RUN tally. A plant that HAS declared the keys (MC today)
|
||
still gets the rows in the table, judged exactly as before -- nothing about
|
||
the arithmetic changed, only which plants are asked to answer it. See
|
||
`ticketchecks.plant_gate_reason` / `plant_scoped_out`.
|
||
|
||
**6.13 is now built** (`check_lifter_accessory_in_assembly`,
|
||
`LIFT-ACCESSORY-NOT-IN-ASSEMBLY` / `LIFT-ACCESSORY-UNCLASSIFIED`), and it
|
||
reuses the 5.11 gather rather than adding a second reader over the same
|
||
parts: `ticketqc._nested_children_of` is the core `_read_nested_solid_zones`
|
||
(one recognised family) and the new `_read_nested_children` (every family)
|
||
both walk, so `GetSubComponentIds()` on each recognised lifter is read the
|
||
one way. Classification is against `lifters.accessories` in the plant layer
|
||
(`knowledge.lifter_accessories()`) -- a child family listed there and absent
|
||
from the assembly is a FAIL, an un-listed child also absent from the
|
||
assembly is a separate WARN, and a read failure is never a quiet pass.
|
||
Live-validated 2026-09-08 through the bridge: WPA648 (assembly 9141343)
|
||
FAILs naming its 8 DB_PATCH children nested in LPA pin anchors that never
|
||
joined the assembly (e.g. child 9521037 under anchor 9521036); WPA655
|
||
(9141344) and SMD516 (6113164) are OK.
|
||
|
||
### 6.7 — a rule that was on a page we already had
|
||
|
||
ALP catalog p53, 02/2024, in a footnote under the capacity table:
|
||
|
||
> Minimum anchor spacing is 2x the published edge distance
|
||
|
||
The owner supplied that page. Its edge-distance column became 6.2 and its
|
||
capacities became `catalogue.inserts`; **this sentence on the same sheet was
|
||
read past**, so until 2026-08-31 nothing in the tool asked how far apart two
|
||
anchors must sit. Two 8-ton anchors 20 in apart passed every row on the board
|
||
while the page asked for 28.
|
||
|
||
It is 6.2's shape one comparison further — the same insert positions, the same
|
||
per-part resolution through the plant's alias, one more measurement — and it
|
||
answers `LIFT-SPACING`.
|
||
|
||
**Where the minimum comes from, in order:**
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| `min_spacing_rule.published_in` | a sheet stating a spacing outright. Source `published`. A measured number for the part beats a rule of thumb about it. |
|
||
| `multiple_of_edge_distance` × the part's own `min_edge_distance_in` | the p53 footnote. Source `derived`, and **the finding prints the arithmetic** — *"28" is 2x QLPD8T8WG's published edge distance of 14""* — because "28 inches" and "twice this part's published 14 inches" are different claims and only the second can be checked by a reader. |
|
||
| nothing | **could not judge.** There is deliberately no plant-wide fallback: doubling a general number would produce a specific-looking claim about a part nobody measured. |
|
||
|
||
**Its states, and none of them is a quiet pass:**
|
||
|
||
* a pair too close, both ends resolved from the part's own data → **FAIL**,
|
||
naming the gap, the minimum, and where the minimum came from;
|
||
* every pair clear → **OK**, naming what they were held to;
|
||
* an insert the catalogue does not cover, or one whose entry publishes no edge
|
||
distance to double → that pair is **not judged** and is named *in the same
|
||
finding*, on the same code — a second `LIFT-SPACING` would be dropped by
|
||
`checklist_states`, which keeps the last finding per code, and the row would
|
||
print half the answer;
|
||
* nothing judged at all → **NOT RUN** with the probe's reason, in red. The
|
||
probe distinguishes three silences: fewer than two inserts with a readable
|
||
position (a fact about the *piece*), unreadable geometry (about the *read*),
|
||
and no resolvable minimum (about the *catalogue*).
|
||
|
||
**A mixed pair is held to the larger of the two minimums.** A 14 in anchor
|
||
beside a 6 in one does not get to sit at 12 in: taking the smaller passes
|
||
exactly the pair the check exists for.
|
||
|
||
**The thickness axis is excluded**, for `connector_edges`' reason and one of
|
||
its own. Anchors in a lifting wythe all sit at one depth, so the depth term
|
||
contributes nothing on a normal piece — and where it *would* contribute, a
|
||
three-axis distance is larger, which is the direction that passes a pair the
|
||
page would refuse. The axis is derived (the span closest to the piece's own
|
||
thickness), never assumed to be an index.
|
||
|
||
**Not yet run against a live model.** The host tests cover the arithmetic, the
|
||
resolution order and all four silences; what nobody has done is press Ticket QC
|
||
on a real piece and read the row.
|
||
|
||
**6.4 could not say why it said nothing, until 2026-08-30.** The check had six
|
||
silent `return []` paths and **no skip probe**, so `run_all` logged it
|
||
`RAN-EMPTY` and the row rendered NOT RUN with the reason *"no probe declared"* —
|
||
a sentence about the tool that told the reader nothing about the piece. Every
|
||
one of those sites now leaves `_dunnage_silence` behind and a probe reads it,
|
||
the same shape 5.2 already used.
|
||
|
||
**Zero openings is a pass, and it is checked FIRST — owner's correction,
|
||
2026-08-31, reading a live report: "6.4 应该是 OK, 既然没有 opening".** Until
|
||
this fix the geometry was only ever consulted *after* the printed dunnage
|
||
insets had been read off the sheet, so a piece with no opening at all still
|
||
rendered NOT RUN the moment its size notes failed to parse — the row was
|
||
complaining about a station that could never have mattered. The check now
|
||
solves the panel geometry **before it ever reads the sheet**: if the piece has
|
||
zero openings, the row is settled on the spot — *"no openings on this piece —
|
||
dunnage cannot sit over one; dunnage stations were not needed"* — and the
|
||
sheet, the notes, and the stations are never touched. The two cases that were
|
||
never a pass keep behaving exactly as before, now reached only once an
|
||
opening is actually on the piece: **"no size notes found"** and **"notes found
|
||
but none parsed into feet"** are failures to read the ticket, and a ticket
|
||
whose dunnage row could not be read is not a ticket whose blocks are clear of
|
||
the openings. Both keep reporting NOT RUN. Openings **present** and clear of
|
||
every station still reach the fall-through pass after the clearance loop —
|
||
*"every one of the N dunnage station(s) is at least 6" clear"* — the same
|
||
reasoning as 4.7's flip, unchanged.
|
||
|
||
**The zero-vs-ungathered line that made the fix safe.** `liftexport._openings`
|
||
hands back an empty list both for a solid with no opening and for a solid it
|
||
was never given (its own guard: `if solid is None: return []`), so "zero
|
||
openings" and "openings never gathered" are NOT the same signal on that
|
||
return value alone — collapsing them would have turned a geometry read
|
||
failure into a false pass. The check now checks `solid is None` itself,
|
||
**before** calling `_openings`: the panel's extents are already solved by that
|
||
point (`Q.concrete_extents`), which walks the same geometry under the same
|
||
size threshold `liftexport.concrete_solid` does, so a solid missing on the
|
||
second read is a real read failure, not a real zero. That case keeps
|
||
reporting NOT RUN with its own reason and is never presented as a clear piece.
|
||
|
||
**One open judgement call, named rather than decided.** When the plant sets
|
||
`opening_counts_only_if_through_thickness` and every opening on a piece turns
|
||
out to be a blind recess, a case can be made that the plant's own rule has
|
||
answered the question and the row should read OK. It is reported NOT RUN
|
||
instead, with a reason that says exactly what was found — NOT RUN under-claims,
|
||
which is the safe direction. This is a different case from the zero-openings
|
||
fix above: here the piece genuinely has openings, just none of them
|
||
through-thickness, so it is **worth the owner's ruling** on its own.
|
||
|
||
6.5 is new. On the pieces audited, those three note views contain their own
|
||
titles and nothing else — `DUNNAGE`, `ERECTION`, `PICKING/STRIPPING`. If the
|
||
content is graphics rather than text this check must look for graphics too, but
|
||
a heading printed over nothing is a real ticket defect and nobody was looking.
|
||
|
||
**Re-graded A -> B, 2026-08-30, built the same day** (`NOTE-CONTENT`,
|
||
`ticketpage.note_view_content_status`). The grade turned on one question that
|
||
had to be answered rather than assumed: **can the API see this at all?** It
|
||
can. A note view's contents are ordinary view-owned elements —
|
||
`FilteredElementCollector(doc, view.Id)` returns the text notes (already used
|
||
by `_flat_text` for the jacking-force and BED PLAN checks) and everything
|
||
else drawn there. "Does this view hold anything besides its own heading" is
|
||
therefore a **counting** question with an exact answer, not something that
|
||
needs a picture. No vision path is required and none was built.
|
||
|
||
B rather than A for one reason: **which views are the note views is plant
|
||
nomenclature.** `DUNNAGE` / `ERECTION` / `PICKING-STRIPPING` are this plant's
|
||
names, they go in `notes.instruction_views`, and the default is empty — so
|
||
until a plant names them the row reads **NOT RUN with that reason**, never a
|
||
pass. Mechanical rule, local constant: grade B.
|
||
|
||
Graphics count as content, and that is not defensive padding. A plant that
|
||
*draws* its stacking instruction instead of writing it would have every one
|
||
of these views reported empty by a text-only check — the tool would fail
|
||
every ticket at the plant it was built to help. So the check counts every
|
||
view-owned element, and says in its own message when the content it found was
|
||
graphics, so a reader knows what was and was not read.
|
||
|
||
**What was deliberately not built.** A note view holding *something* passes.
|
||
A note that is present but stale, wrong, or says nothing useful (`N/A`, a
|
||
leftover line, last year's instruction) is not caught, and no amount of
|
||
element counting will catch it. That residue is a genuinely different kind of
|
||
check — its verdict would come from a model reading the rendered sheet, which
|
||
cannot be unit-tested to a fixed answer and may differ run to run. **It does
|
||
not belong anywhere on the A/B/C/D ladder**, which grades what it takes to
|
||
*compute* an answer; it needs its own category before anything is built for
|
||
it. Left open on purpose.
|
||
|
||
### 6.6 — an odd count of one part demands a note on the BED PLAN
|
||
|
||
**Built 2026-08-31 on the owner's ruling** (`PARITY-NOTE`,
|
||
`ticketpage.parity_note_status` / `ticketchecks.check_parity_note`): when the
|
||
count of one particular part on a piece is **odd**, the ticket's BED PLAN must
|
||
carry a particular phrase. An odd count with no such note is a defect.
|
||
|
||
**Three plant facts, not one rule.** Which part is counted, which parity
|
||
demands the note, and what the note says are three separate things, and every
|
||
one of them is another plant's to answer differently. So none of the three is
|
||
in the code: they are `drawings.parity_note_rule.family_names`, `.parity` and
|
||
`.require_text`, and with any one of them unset the row reads **NOT RUN naming
|
||
the key that is missing**. Nothing is guessed and nothing falls back.
|
||
|
||
**The count comes from the MODEL, and the printed BOM would have been a trap.**
|
||
It is one scoped `asm.GetMemberIds()` pass, the idiom `check_bom_members` and
|
||
`_bar_members` already use. Taking it from the ticket's printed BOM looks like
|
||
the more faithful reading of *"the count on the ticket"* and is not: this
|
||
plant's lifting hardware is among the model extras that **never appear on a BOM
|
||
row at all** — `check_bom_members` says so in its own docstring, and skipping
|
||
them is why that check works. A BOM-sourced count would therefore be zero on
|
||
every piece, zero is even, and the rule would never once fire. A check that
|
||
passes everything silently is worse than no check.
|
||
|
||
What the BOM would have caught and this does not: a model and a printed bill
|
||
that disagree about how many of the part there are. That is real, and it is
|
||
**5.1's** question. The call-outs drawn on the plan — what a person counting
|
||
the ticket by eye actually counts — are **5.2's**. Three readings of one number
|
||
that can disagree with each other is how a tool gets two answers to one
|
||
question; this row asks only what the piece *is* and what the plan *says*.
|
||
|
||
**One unread member flips the parity, so an unrecognised member stands the
|
||
whole row down.** Every other check here can afford to report on what it could
|
||
read and name what it could not. A parity cannot: a census one member short is
|
||
not a slightly worse answer, it is the **opposite** answer half the time. So a
|
||
member with no readable family name makes the row NOT RUN, naming how many
|
||
there were and why.
|
||
|
||
**Zero is an answer, and it is not the same answer as silence.** A piece
|
||
carrying none of the part has a count of 0, 0 is even, the rule does not apply,
|
||
and the row is a **genuine OK** — it says so in those words, and prints how many
|
||
members it read, so a family name that matches nothing is visible instead of
|
||
silently even. A piece whose members could not be identified is could-not-judge
|
||
and is never a pass. The two arrive at the verdict function as different
|
||
arguments so that no later edit can collapse them.
|
||
|
||
**The order of the gates is the design.** Configuration first, then the census,
|
||
then the parity, and the BED PLAN **last** — because a count whose parity does
|
||
not demand the note is fully judged without ever opening the plan. Demanding a
|
||
readable plan before that would print NOT RUN for a piece the tool can answer
|
||
completely. The plan is not even read on those pieces: the check hands the
|
||
verdict function a reader rather than the text, and it is called after the
|
||
parity gate or never.
|
||
|
||
**The phrase match is case-insensitive and survives line breaks**, because a
|
||
note typed over two lines is the same note. `ticketqc._flat_text` already joins
|
||
a view's text notes with a single space and collapses runs of whitespace; the
|
||
phrase is normalised by the same function as the text it is looked for in.
|
||
The printed call-outs are plain TextNotes on the detail views — established
|
||
when 5.6 was built, and unchanged.
|
||
|
||
**Grade B rather than A, for two reasons that are worth stating.** The count
|
||
rests on a family name the **plant** declares, not on anything the model says
|
||
by itself. And a present phrase proves only that the words are on the plan, not
|
||
that they are **about this part**: the owner has used the same wording for
|
||
something else entirely (2026-08-16, on the `*.A` packets — a piece can need a
|
||
temporary precast beam that exists for the erection and is removed in the
|
||
field). The OK says exactly what it saw and claims nothing more.
|
||
|
||
#### It ships dormant, and that is the whole safety
|
||
|
||
`drawings.parity_note_rule.family_verified` is **false** until somebody has read
|
||
the family name off a live document, and while it is false the row reads NOT
|
||
RUN quoting `family_unverified_because`. This is the guard
|
||
`reinforcing_rule.parameter_verified` already carries, and here it is doing more
|
||
work than usual: **a family name that matches nothing counts zero on every
|
||
piece, and zero is even.** Get the name wrong and the row is a permanent,
|
||
invisible OK over a whole defect class — the exact failure `families.lifter_include`
|
||
had before anyone noticed the plant's inserts are not named `LIFT` or `ANCHOR`.
|
||
|
||
The match is on the **whole family name, case-insensitively**, and that is a
|
||
choice between the two shapes already in the tree rather than a third one.
|
||
`families.lifter_include` and `drawings.reinforcing_families` match substrings;
|
||
`drawings.reinforcing_family_overrides` matches the whole name, "or an entry for
|
||
`SR1` would swallow `SR1101`". A part number is exactly that shape of token, and
|
||
this job's own piece marks are `WP<number>` — a substring token would swallow
|
||
the piece named for the same number. A plant whose family name is longer than
|
||
its part number writes the longer name.
|
||
|
||
**What only a live press can settle:** whether the part is modelled under a
|
||
family name that *equals* the part number, or under a longer name containing
|
||
it. The plant layer lists it bare in `families.lifter_include`, which is
|
||
substring-matched and so cannot tell the two apart. One read of a family name
|
||
off a real assembly answers it and flips `family_verified`.
|
||
|
||
6.3's handling findings on these twelve were run on default stages with nobody
|
||
having picked the inserts. That is enough to find a problem and not enough to
|
||
sign, and the report has to keep saying so.
|
||
|
||
## 7. Interference
|
||
|
||
| # | check | grade |
|
||
| --- | --- | --- |
|
||
| 7.1 | Interference check | B — the tolerance is a plant fact |
|
||
|
||
Nine of the twelve failed. The recurring pair is `SR5602 x P136` overlapping
|
||
0.21–0.23". A pair that repeats across pieces is one modelling pattern, not nine
|
||
defects — **cluster interference findings by family pair before reporting them**,
|
||
or the count misleads.
|
||
|
||
**The pairs are a FILE, not a note (2026-08-30).** A WARN writes every pair to
|
||
`<qc root>/interference-<mark>.csv` — beside `last-ticketqc.txt`, next to the
|
||
model — with the owner's header `MARK,ID,X,MARK,ID` and then `FAMILY_1,
|
||
FAMILY_2`; the middle `X` column is the versus between
|
||
the two sides and holds a literal `x`. The row's note keeps the count, the
|
||
population, the depth band and a link to that file, because a 180-character
|
||
cell truncated the evidence and kept the diagnostics.
|
||
|
||
**`THICKNESS_IN` and `VOLUME_IN3` were dropped from the file (2026-08-30)**, on
|
||
the owner's ruling: keep them only if they could ship hidden by default in
|
||
Excel, otherwise turn them off. A `.csv` is plain text and carries no column
|
||
formatting, so a hidden column in one does not exist — both are gone. Thickness
|
||
was not decoration (it is the quantity the filter tests, so it was the only
|
||
per-pair answer to *why is this pair here*), so it survives as a band in the
|
||
WARN note — `shared material 0.13"-0.85", over the 0.125" that flags a pair,
|
||
deepest row first` — and the file's rows stay sorted deepest-first. Volume had
|
||
no such duty: depth is the question this check asks, and volume never decided
|
||
anything. Written only when there are pairs; deleted at
|
||
the start of every run, so a piece that has been fixed cannot keep the file that
|
||
says it has not. The population is the ticket's OWN assembly
|
||
(`member_boxes` → `asm.GetMemberIds()`) and never the model — the note says so
|
||
in words, because six numbers in one parenthesis, the first counting elements
|
||
and the rest counting pairs, read as a whole-model sweep to the owner on WP296
|
||
(142 members, then 940).
|
||
|
||
## 8. Text hygiene
|
||
|
||
| # | check | grade |
|
||
| --- | --- | --- |
|
||
| 8.1 | No unresolved lines in the design summary | C |
|
||
| 8.2 | No invisible or non-printing characters in ticket text | A |
|
||
|
||
8.2 is new and concrete: a `U+200E` left-to-right mark is sitting in the design
|
||
summary of these pieces. It is one of the five lines the spec reader could not
|
||
interpret, and it crashed a tool that tried to print it. Invisible characters in
|
||
text that a person is expected to read are worth refusing outright.
|
||
|
||
---
|
||
|
||
## The table itself: nothing is truncated, and a long note folds
|
||
|
||
Owner, 2026-08-30, having read a live 4.5: *"4.5被截断了。我经常在notes里面有截
|
||
断。不要有截断,item不要折叠,需要一行就一行,需要两行就两行,但是note做成默认
|
||
折叠显示跟item一样的行数。"*
|
||
|
||
**What was cutting it was the data layer, not the styling.**
|
||
`ticketpage.checklist_states` ran every non-OK message through a 180-character
|
||
`capped_note()`, so the row tuple itself carried a stump — and the row tuple is
|
||
what the HTML table, `last-ticketqc.txt` and every other consumer are built
|
||
from. 4.5 lost its second debond run mid-number (`… 0.958 ft of concrete from
|
||
25.875 t…`) in the panel *and* in the file the reader is told to fall back to.
|
||
**`NOTE_CAP` and `capped_note` are gone, and a test asserts they stay gone.**
|
||
|
||
What replaced the cap is a fold, and it is height-only:
|
||
|
||
- The **item cell is never folded.** One line if it needs one, two if it needs
|
||
two.
|
||
- The **note cell collapses to its own row's item height** — `max-height` in
|
||
`em`, `overflow: hidden`, and the whole note inside it. Nothing is removed,
|
||
so a collapsed note can still be selected and copied in full.
|
||
- **Every row folds a long note, red included.** Until 2026-08-31 a red row
|
||
(FAIL, NOT RUN) never folded — the reasoning then was that the reader must
|
||
not have to click to read a problem row. The owner overruled that:
|
||
*"notes太长的话默认折叠"* — long notes fold by default, no status named or
|
||
exempted. Since the original rule shipped, notes got long on purpose (row
|
||
probes' full multi-clause reasons, shared-silence cross-references, dated
|
||
mute reasons), overwhelmingly on NOT RUN — the very status that used to be
|
||
exempt — and he is now looking at walls of text. What still holds: the
|
||
state colour and the row's own `item_lines` worth of visible text before
|
||
any click, on every status; a fold is a clip on height, never a cut on
|
||
content.
|
||
- **A file path renders outside the fold**, so 7.1's collapsed note still hands
|
||
over its link — on any status.
|
||
- The control **says how much is behind it** (`[+] the whole note (330
|
||
characters)`), which is the one thing the old `…` did do.
|
||
- **The text file has no fold at all** — everything, wrapped, as before.
|
||
|
||
### …and the text file carries his own words, in his own script (2026-08-31)
|
||
|
||
`last-ticketqc.txt` is documented above as the complete record. A live run
|
||
wrote it with **every CJK character of the job file's mute sentences
|
||
removed** — the ASCII fragments and the gaps between them survived, and the
|
||
same sentences rendered correctly in the panel and in `qc-run.json`. A file
|
||
promised as complete that silently drops half a sentence is worse than one
|
||
that fails to write.
|
||
|
||
Two boundaries, both now closed in `ticketpage`:
|
||
|
||
- **`str()` on a note.** Replaced by `_text()`, which is `vibe.compat.as_text`
|
||
— the project's one answer to this class. Every note, detail line, mute
|
||
reason and row field goes through it, from the moment the reason is read out
|
||
of the job file (`muted_checks`) to the moment it is wrapped.
|
||
- **`textwrap`'s whitespace step.** It munges whitespace by calling
|
||
`text.translate(<a 256-entry table>)` on anything it takes for a `str`. On
|
||
CPython that table covers every byte; the engine here is IronPython 2.7,
|
||
whose `str` holds .NET UTF-16 text, so **a character above U+00FF has no
|
||
entry in the table at all** — which is exactly the set of characters that
|
||
came back missing. `_wrap_text` flattens the whitespace itself and tells
|
||
`textwrap` not to do it again.
|
||
|
||
The suite cannot reproduce this: the host interpreter it runs on has neither
|
||
boundary. So there are two tests — one that a non-ASCII note survives (true on
|
||
any engine, and the promise itself) and one **tombstone that pins the shape**,
|
||
because what protects the report here is the absence of two calls.
|
||
|
||
---
|
||
|
||
## Every row owes a reason — including the rows no check function owns
|
||
|
||
**The guarantee had a hole one level above where it was installed.** The skip
|
||
probe (`CLAUDE.md` §5) makes a check that returns nothing explain itself, and
|
||
`RAN-EMPTY` reports an unexplained silence as the defect it is. All of that is
|
||
keyed by **check function** — and seven checklist rows name no check function
|
||
at all. 1.2, 2.1, 2.3, 3.1, 3.2, 4.1 and 4.2 are answered by finding **codes**
|
||
that `ticketqc.check_ticket` emits directly, so no probe could reach them.
|
||
|
||
On a live run, 2026-08-31, six of them came back like this:
|
||
|
||
```json
|
||
{"item": "3.1", "state": "NOT RUN", "judged": false,
|
||
"checks": [], "reason": null, "note": null, "owner_codes": ["FC"]}
|
||
```
|
||
|
||
`reason` null, `note` null, and in the text report a bare red `NOT RUN` with
|
||
nothing underneath. Eleven codes waited for, not one emitted. **The tool gave
|
||
the reader no sentence to disagree with**, and he read the whole page as
|
||
unnecessary noise — correctly, because a refusal that will not say what it
|
||
lacks is indistinguishable from one.
|
||
|
||
**And the reasons existed.** The design had no extracted spec, so
|
||
`intent.source` is `regex` and every column of the intent table is a dash;
|
||
2.1's and 4.1's inputs are a printed size and a printed strand count that did
|
||
not parse — the same parse failure **row 6.4 reports in a full sentence** on
|
||
the same ticket.
|
||
|
||
What closes it:
|
||
|
||
- **A row probe** — the same contract as a skip probe, keyed by checklist row
|
||
number instead of by function name. It returns why the row *would* be
|
||
silent, or `None` when every input is there. `ticketchecks._ROW_PROBES`.
|
||
- **The answer travels in the run log**, under `row:<number>`. The log is the
|
||
one channel that already reaches every consumer — the button hands it to
|
||
`checklist_states`, `ticketvision` writes it into `qc-run.json` — so nothing
|
||
else had to be edited to carry it. (`missing.reason` in the JSON was *not*
|
||
the fix: it is a generic catch and it never reaches the text report at all.)
|
||
- **A last rung that cannot be empty.** With no probe and no declared reason,
|
||
the row prints a sentence saying exactly that, naming the codes it waited
|
||
for. Same ruling as `RAN-EMPTY`'s *"no probe declared"*: an unexplained
|
||
silence is reported, never blanked.
|
||
- **A mechanical test**, so a new code-answered row with neither a probe nor a
|
||
declared reason fails in the suite rather than on his screen —
|
||
`test_every_code_answered_row_can_explain_its_own_silence`, plus one that
|
||
walks the whole table and asserts no `NOT RUN` row carries an empty reason.
|
||
|
||
**One fact is said once and then cross-referenced.** Rows 4.4 and 4.6 stand
|
||
down on one sentence, and 3.1 / 3.2 / 2.3 / 4.2 on one missing spec; four red
|
||
rows carrying one fact read as four problems. Rows whose reason shares its
|
||
opening clause — everything before the first ` -- `, which is how these
|
||
sentences are built (the shared half, then what *this* row lost) — now name
|
||
each other: *"the same missing input also left rows 3.1, 3.2, 4.2 quiet: one
|
||
fact, not 4 separate problems"*. It **adds**; each row keeps its own words,
|
||
because a group summary that replaced them would trade one silence for
|
||
another.
|
||
|
||
**What is not claimed.** A row probe reads the same inputs the emitting code
|
||
reads, but it is not the emitter, so a future edit could make the two disagree
|
||
— a row explaining its silence with a reason that is no longer the real one.
|
||
The guard against that is that a probe must go quiet the moment its input is
|
||
present, which is pinned by test; a probe *inside* the emitter would be
|
||
better, and would mean `check_ticket` returning a disposition of its own.
|
||
|
||
**The approximation, stated.** How many lines the item cell occupies is a
|
||
*rendered* measurement — column width and font, neither of which exists when
|
||
this code runs — so `ITEM_CHARS_PER_LINE` estimates it from the item's length.
|
||
Being wrong costs a line of preview, never a character of content. The toggle
|
||
is the same inline-`onclick` convention every other fold in this tool uses
|
||
(`foldreport.wtClip`, beside `wtFold`); if the script is dead, a folded note
|
||
stays visually clipped, which is why a path and the text-file copy stay
|
||
outside the fold entirely (a red row no longer does — see above, 2026-08-31).
|
||
|
||
---
|
||
|
||
## The NOT RUN sweep (2026-08-31)
|
||
|
||
The owner's standing order, verbatim: *"qc别再给我出这么多not-run。看看能跑的,
|
||
能做的,现在都做了去。"* Every NOT RUN cause that could be eliminated **without
|
||
his data and without a ruling from him** was eliminated in one pass — and the
|
||
honesty invariant governs *how*: **a NOT RUN dies by making the check RUN, never
|
||
by softening a state.** Nothing below repaints a row; four things were built and
|
||
one plant layer was armed.
|
||
|
||
### 1. The design-summary vocabulary — which killed a row's silence *and* seven findings
|
||
|
||
Row 8.1 reported **7 uninterpreted lines out of 7** on a real insulated piece,
|
||
and row 5.9 (connector sufficiency) stood down on every press saying *"the design
|
||
states a connector requirement that the summary parser cannot read yet"* —
|
||
because the requirement it needed **was one of those seven lines**.
|
||
|
||
`ticketpage.parse_summary_line` is the grammar. It **categorises and does not
|
||
evaluate**: a line it places is a line whose *shape* is known, and nothing in it
|
||
decides whether a piece honours anything. Four productions, each drawn from a
|
||
corpus rather than from the one line that prompted it — (a) the live design
|
||
summary's 9 distinct unresolved lines, (b) 54 distinct `… @ <spacing> O.C. …`
|
||
call-outs over 75 occurrences in the plant-manual extractions, both counted
|
||
2026-08-31:
|
||
|
||
| production | shape | corpus |
|
||
| --- | --- | --- |
|
||
| `spacing` | `<product> @<spacing> O.C. <qualifiers>` | 2 live lines, all 54 manual call-outs |
|
||
| `placement` | `CL <items>[@ <zone>]` — centred, optionally in a named wythe | 3 live lines |
|
||
| `conditional` | `<requirement> NEEDED WHEN <condition>` | 1 live line |
|
||
| `label` | an all-letters heading naming the group below it | 1 live line |
|
||
|
||
**The first `@` wins and everything after it is a qualifier.** `#3S @ 18" O.C.
|
||
+(2)@3" EA. END` states one *field* spacing and then an end densification, and
|
||
the measured piece honours exactly that reading. A reader that took the last `@`
|
||
would report the end pair as the requirement and fail every correct piece.
|
||
|
||
**Three refusals are the load-bearing part.** They are what stops the row going
|
||
green by shrugging:
|
||
|
||
- **A line the grammar cannot place stays an 8.1 finding**, verbatim, in the
|
||
shape it arrived in. `REINFORCING PER DESIGN` is three capitalised words with
|
||
no digits — exactly the shape of a heading — and it is a *deferral*, so the
|
||
label production carries a word list that refuses it.
|
||
- **A line placed but not *valued* is not a line that was read.** The corpus
|
||
writes a non-numeric spacing two ways: `@ "S" O.C.` (a column of a table
|
||
printed on the sheet — 7 occurrences) and `@ __ O.C.` (a literal blank — 1).
|
||
Both are recognised as spacing requirements carrying `spacing_in: None` and a
|
||
reason, and 8.1 names them in a clause of their own.
|
||
- **A conditional is stated, never evaluated.** `CL #7 BARS NEEDED WHEN THERE
|
||
ARE OPENINGS` is recorded as a condition with the requirement it qualifies
|
||
parsed inside it, and 8.1 prints it saying the tool does not evaluate it.
|
||
|
||
**The verbatim record is untouched.** `spec['unresolved']` on disk is the
|
||
*extractor's* own account of what its rules could not place; rewriting it with a
|
||
later reader's opinion would stop it being evidence. So the grammar runs at
|
||
**report** time, over whichever list 8.1 was handed, and 8.1 re-derives the split
|
||
every press — an intent assembled by hand cannot smuggle a claim past it.
|
||
|
||
### 2. Row 5.9 now runs, and its design side is joined through the plant's own alias
|
||
|
||
The join is the whole trick, and no product name is in the code:
|
||
|
||
```
|
||
the summary says HK ST100'S @1'-4" O.C.
|
||
the plant declares connectors.marks holds R82
|
||
connectors.catalogue_alias.R82: HK Composites (ST100)
|
||
so the two join on ST100
|
||
```
|
||
|
||
`knowledge.connector_spacing_requirement` does it, **exactly, on a code token** —
|
||
one carrying both a letter and a digit — after the summary's possessive `'S` is
|
||
dropped. Matching on `HK`, or on any substring, would let `ST50` answer for
|
||
`ST100`: both are *HK Composites* and they are not the same part. That is
|
||
`connector_product`'s exact-match lesson one join along.
|
||
|
||
Three refusals again, and each has a different repair: **the summary states no
|
||
spacing**; **it states one and no declared connector is named in it** (a bar's
|
||
pitch read as a connector's is a confident wrong answer, and 5.9 FAILs on a
|
||
shortfall); **it names a connector and the spacing is not a number**.
|
||
|
||
**The tolerance, and where it came from.** The comparison was `widest > required
|
||
+ 0.5` with the `0.5` a literal inside the function. It is now
|
||
`tolerances.connector_spacing_in`, passed in, **and a run that cannot resolve one
|
||
compares nothing** rather than falling back on a constant — a borrowed tolerance
|
||
is the same class of mistake as a borrowed edge distance, and 5.10 already paid
|
||
for that lesson. The number itself is honest about its own standing: **it is a
|
||
tool default and not a plant ruling.** The one calibration that exists needed no
|
||
tolerance at all — measured 2026-08-31, the connectors sit at exactly 16.0 in
|
||
across the field against a design stating `1'-4"`, so this allowance has **never
|
||
been exercised**. 0.5 in was chosen to sit below the finest spacing the corpus
|
||
prints (½ in) and above the rounding of a printed dimension, and the finding
|
||
prints the value and the key it came from, pass as well as fail.
|
||
|
||
**End densification is normal and is never flagged**: the comparison is against
|
||
the *widest* gap, because the same measured piece carries 9.5 in and 6.5 in pairs
|
||
at its ends.
|
||
|
||
### 3. The seven checks with no skip probe
|
||
|
||
`RAN-EMPTY → "no probe declared"` is a whole *class* of NOT RUN and the worst
|
||
one: a sentence about the tool, printed in red, on a row that may well be fine.
|
||
Seven checks could still produce it — re-derived rather than copied from a
|
||
document (`CHECKS` minus the keys of `_SKIP_PROBES`, 37 − 30). All seven now have
|
||
one, and **the class is closed by test**, not by prose:
|
||
`test_every_registered_check_has_a_skip_probe` walks `CHECKS`, and two siblings
|
||
catch a probe keyed by a renamed function and a probe returning `''` (falsy, so
|
||
`run_all` would read it as *"ran and found nothing"* — a probe meant to explain a
|
||
silence turning that silence green).
|
||
|
||
| check | row | what the probe says |
|
||
| --- | --- | --- |
|
||
| `check_lifter_capacity` | 6.1 | no insert recognised — **and it names both readings**: the piece carries none, or none matches `families.lifter_include` |
|
||
| `check_handling_result` | 6.3 | three paths: no `panel_id`, `handling.lifter_groups` empty, no mark |
|
||
| `check_member_interference` | 7.1 | `interference.check` is off, or no `panel_id` — and it goes **quiet on the clean path**, or a piece with no clashes would read NOT RUN |
|
||
| `check_picking_match` | 6.8 | no sheet / no length / no picking families / **the printed band did not parse**, which is the tool's reading failure and never a defect in the piece |
|
||
| `check_sheet_name` · `check_ticket_set` · `check_invisible` | 1.1 · 1.3 · 8.2 | *these three have no silent path at all* |
|
||
|
||
The last three are a second shape of probe and it is deliberate. `sheet_name_matches`, `ticket_set_status` and `invisible_chars` answer with a severity and a sentence for **every** input, including no sheets at all — a piece with no sheet gets `FAIL "no sheet found for this piece"`, not silence. So their probes return a reason *always*, saying so: they never fire while the checks behave, and the day a refactor makes one return `[]` the row names the check and says the silence is **a defect in the tool, not a verdict on the piece**. That beats *"no probe declared"* by naming the place to look.
|
||
|
||
### 4. The two checks that ran and could reach no row
|
||
|
||
`check_bom_strand_count` (`BOM-STRAND`) and `check_picking_match`
|
||
(`PICK-STATIONS`) had been registered and running with **no master entry naming
|
||
them** — so their findings landed in `qc-run.json` under a code no row owned, and
|
||
`BOM-STRAND` can FAIL. They were recorded as a reasoned exemption
|
||
(`ROWLESS_CHECKS`) rather than wired, because giving a check a row is a decision
|
||
about what the report claims and that is the owner's. He made it. They are now
|
||
master ids **4.8** and **6.8**, on the plant board in place (46/46, contiguous,
|
||
nothing off-board).
|
||
|
||
**Why each got a row of its own rather than joining a neighbour.** 4.1 already
|
||
asks *printed strand count vs modelled* and 6.5 already asks whether the PICKING
|
||
note view says anything — so the multi-check idiom (5.1) was the obvious move and
|
||
it is the wrong one **here**, for a reason worth writing down:
|
||
|
||
> A row is answered only when **every** check it names has spoken. 5.1 pairs two
|
||
> checks because they are two halves of **one** question and half an answer is
|
||
> not an answer. 4.8 and 4.1 are two **independent** answers to a similar
|
||
> question — a BOM linear-feet row and a printed section call-out, different
|
||
> inputs, different failures, different repairs — and either alone is a real
|
||
> verdict. Hanging 4.8 off 4.1 would have made 4.1 read NOT RUN on every ticket
|
||
> whose BOM could not be read: **a row that had a verdict turned red over a
|
||
> silence about a different question.** That is the opposite of the order.
|
||
|
||
`ROWLESS_CHECKS` stays, empty, and so do its three guards — an allowlist with one
|
||
test that fails if a check reaches no row and another that fails if a listed
|
||
check gains one. An empty allowlist under a working pair of guards is what *"there
|
||
are none"* looks like; deleting it would delete the mechanism that found these two.
|
||
|
||
### 5. Rows 5.13 / 5.14 / 5.15: the gather that was a comment
|
||
|
||
These three shipped with their arithmetic built and their inputs deliberately
|
||
**un-gathered** — `read_ticket` set no `solid_zone_pattern`, `recess_patterns`,
|
||
`recess_regions` or `legend_patterns`, and a comment in `ticketqc.py` said why:
|
||
writing a Revit read nobody has run and letting the first live press find out is
|
||
worse than three rows standing down loudly. The reads have since been taken on
|
||
the live model, id by id, so `ticketqc._read_insulation_hatch` now ships the path
|
||
those measurements exercised. Every call in it is individually guarded and every
|
||
failure leaves a **sentence** in `insulation_hatch_notes` that the three rows
|
||
print behind their own — a gather that can take Ticket QC down is worse than
|
||
three rows that stand down.
|
||
|
||
Three things in it are not incidental:
|
||
|
||
- **The filters are read off the view *template*, not the view.** The override
|
||
that hatches a solid zone sits on the template; a reader that asked the view
|
||
would find no filter and report the hatch as un-overridden, on a job whose
|
||
defect **is** the override.
|
||
- **Legend views are excluded from the recess count.** A swatch is a *picture* of
|
||
a recess. The only four recess regions in the model are swatches, and they sit
|
||
across **two** legend views — counting them would make every piece look as
|
||
though it drew two recesses. The plant layer names the token (`LEGEND`) rather
|
||
than a second view name, because the next one will be called something nobody
|
||
predicted.
|
||
- **A type-level pattern read is authoritative only under a condition, and the
|
||
condition is checked every run.** A filled region is a Detail Item; a filter
|
||
selecting that category beats the type, and then the type says what the region
|
||
*would* have drawn with rather than what it drew. Measured 2026-08-31: no
|
||
filter on this template carries the category — a fact about today's template,
|
||
not a property of Revit. The day one gains it, 5.15 stands down naming the
|
||
filter instead of quietly comparing the wrong thing.
|
||
|
||
**What each row does now.** 5.13 **runs and FAILS**, id-exact: the zones draw the
|
||
pattern the template's filter sets and the legend's own swatch for a solid zone
|
||
is a different pattern element — and the one they wear resolves *by element id*
|
||
to the 2-inch-recess swatch, so the finding can say the zones render with the
|
||
recess's own pattern rather than merely *"the wrong hatch"*. 5.15 **runs clean**
|
||
for both recess types. 5.14 **stands down honestly**, and the sentence is the
|
||
sharpened one: no recess is drawn on any ticket view, the legend views are
|
||
excluded on purpose so a zero is a real zero — **and this reader has never seen a
|
||
job that does draw one**, so it cannot tell a piece that needs no recess from a
|
||
piece whose recesses were forgotten, and it claims neither.
|
||
|
||
One question came off the owner's list with a measurement rather than a ruling:
|
||
**`VOID_REVEAL_WIDE` is not a modelled recess** — 22 instances, all on one piece,
|
||
zero on any insulated piece, none an assembly member. The drafted-region path is
|
||
the whole path, `insulation.recess.modelled_path_verified` is now true with
|
||
`modelled_family` settled empty, and 5.14 has stopped printing the *"the modelled
|
||
path was NOT consulted"* caveat. A tool that keeps hedging about a closed question
|
||
is noise on a red row.
|
||
|
||
### What is left, and it is all his
|
||
|
||
After the sweep, a row can still read NOT RUN five ways, and **only the first two
|
||
are eliminable by anybody but him**:
|
||
|
||
1. a check raised (`CHECK-ERROR`) — a bug, and it names the check;
|
||
2. an input this run could not read — the probe names the ticket key or the
|
||
gather;
|
||
3. **a plant fact no layer states** — the probe names the key: `1.4`
|
||
`nomenclature.design_number_re`, `6.5` `notes.instruction_view_sets`, `5.6` /
|
||
`5.7` `drawings.reinforcing_rule.scopes`;
|
||
4. **a datasheet nobody has supplied** — `5.10` for any connector but ST100
|
||
(`catalogue.connectors.products.*.min_edge_distance_in`), `5.7` the per-size
|
||
stock table's reader;
|
||
5. **a ruling he has not given** — `5.7`'s bar-size parameter, `5.14`'s design
|
||
side, `5.15`'s legend-text-vs-type-name size discrepancy.
|
||
|
||
Three rows are not NOT RUN at all and must not be counted as such: **5.3 and 5.5
|
||
are FOR ENGINEER by design** (yellow, a person's job, with the reason that makes
|
||
them so), and **3.3, 5.4 and 1.4 are muted on this job** with his own dated
|
||
sentence — muted still runs, still shows, and a mute can never silence a FAIL.
|
||
|
||
---
|
||
|
||
## Can this be code?
|
||
|
||
**Most of it, yes — but not as one gate.**
|
||
|
||
Counting the grades: A dominates. Those are comparisons between two values that
|
||
both exist in the model, and they belong in `ticketchecks.CHECKS` alongside what
|
||
is already there.
|
||
|
||
The B items are the same shape but need a constant that is genuinely local — the
|
||
dimension tolerance on stemmed pieces, whether a printed strand count is
|
||
per-piece, which rail the dimension chain starts from, the interference
|
||
tolerance, the release band. Those belong in the knowledge file, and the audit
|
||
shows why they cannot be defaulted: a wrong default reads as a missing value and
|
||
the check quietly does not run.
|
||
|
||
The C and D items are the residue, and they are not the interesting part. What
|
||
this audit actually shows is that **the failure mode worth engineering against
|
||
is not "the check is impossible" — it is "the check ran against the wrong
|
||
input"**:
|
||
|
||
- reading one sheet of a two-sheet ticket produced 60 false findings on twelve
|
||
pieces, and would produce roughly a thousand across the model
|
||
- one missing knowledge entry (the legend prefixes) fires a FAIL on all 204 WP
|
||
tickets
|
||
- one root cause fanned out into five distinct codes, so the report's shape
|
||
suggested five problems
|
||
|
||
A checklist that a person walks through would not have caught any of those. Code
|
||
would, and did.
|
||
|
||
**So: build it as code, and let the skill be the thin part.** A skill is the
|
||
right home for the C and D items — the reading-the-drawing judgement, the
|
||
shortlist triage, deciding whether a repeated interference pair is one problem
|
||
or nine. It is the wrong home for anything a comparison can settle, because a
|
||
person walking a checklist has no way to notice that the tool handed them half a
|
||
ticket.
|
||
|
||
Before either: fix item 0, and re-run. Every number in this document was
|
||
measured against a ticket the tool had only half-read.
|
||
|
||
---
|
||
|
||
## 8bis. Checks a model has to look at a picture to make
|
||
|
||
Ruled by the engineer 2026-08-30, prompted by the residue in 6.5: a note view
|
||
that *does* say something may still say the wrong thing -- stale, `N/A`, last
|
||
year's instruction. That verdict cannot come from a query. It comes from a
|
||
model reading the rendered sheet, it cannot be pinned to a fixed answer by a
|
||
test, and it therefore does not sit anywhere on the A/B/C/D ladder, which
|
||
grades what it takes to *compute* an answer.
|
||
|
||
**The ruling: such a check reports NOT RUN, and raises a WARN alongside it.**
|
||
|
||
Not a pass, not a fail, and never silent. NOT RUN says honestly that nothing
|
||
was computed; the WARN says a human still has to look. The two together are
|
||
the only combination that does not lie: a pass would claim a verdict nobody
|
||
reached, a fail would condemn on a guess, and silence would be read as a pass
|
||
by every person who ever skims the report.
|
||
|
||
~~Nothing on this footing has been built.~~ What the report may *claim* on such
|
||
a check's behalf is settled by the above and by nothing else -- it may say what
|
||
it could not judge and that someone should look. It may not paraphrase the
|
||
content, score it, or imply it was reviewed.
|
||
|
||
### Built 2026-08-30 — `vibe.ticketvision`, `tools/vision_read.py`
|
||
|
||
The escalation path exists. **One** check uses it: **5.5**, bar marks legible
|
||
and unique — grade D, the row that has always read FOR ENGINEER because there
|
||
was nothing else it could read. Legibility is a property of the *picture*, so
|
||
a picture is the only thing that can answer it.
|
||
|
||
**How a sheet becomes an image, and why the resolution is what it is.** It
|
||
does not. **A whole sheet is never rendered**, and that is forced by a hard
|
||
ceiling at the far end: the API downscales any image whose long edge exceeds
|
||
1568 px, so the model receives about 1.1 megapixels however large a file is
|
||
sent. A 34 x 22 in sheet inside that budget is **44 dpi** — and no bigger
|
||
render helps, because the extra pixels are discarded on arrival.
|
||
|
||
So **one placed view** is exported (`Document.ExportImage`, read-only, no
|
||
transaction) at **1500 px on the long edge**, just under the ceiling. The
|
||
effective resolution then follows from how much paper that view occupies:
|
||
|
||
effective dpi = 1500 / the view's long side, in inches on the sheet
|
||
|
||
A 6-inch note view is 250 dpi. A 20-inch bed plan is 75 dpi, and is
|
||
**refused** — `render_plan` returns NOT RUN with the arithmetic in the reason
|
||
below a **150 dpi floor**. That floor is the QNA evidence, not a preference: a
|
||
text layer returned `1316"` for both `13/16"` and `1 3/16"`, a phantom strand
|
||
position survived four sheets until someone rendered at 450 dpi, and stamps
|
||
invisible to text search were found only by looking. A render nobody can read
|
||
is a NOT RUN, never a question.
|
||
|
||
**How a model-read finding is told apart from a computed one**, in four
|
||
independent places, because one of them will eventually be dropped by an edit
|
||
and the other three still have to hold:
|
||
|
||
| | |
|
||
|---|---|
|
||
| the finding | `Finding.source == 'model-read'` (everything else defaults to `'computed'`) |
|
||
| the code | `AI-*`, a prefix no computed check uses and **no `CHECKLIST_ITEMS` row owns** — so a read cannot set, flip or satisfy a computed row |
|
||
| the list | its own, `result['model_read']`, never merged into `findings` |
|
||
| the report | its own section, its own table, and a column that says *a model looked at a picture*; every message opens `[read by a model, not computed]` and ends *NOT a verdict* |
|
||
| the JSON | two keys, `findings` and `model_read`, each entry carrying `source` and a boolean `computed` |
|
||
|
||
**Every outcome is a WARN**, `clear` included. A model that looked at one view
|
||
and saw nothing wrong is a reason for a person to look, not a reason to skip
|
||
looking — which is exactly the shape 8bis requires, and there is no branch in
|
||
the code that can emit OK or FAIL.
|
||
|
||
**Cost, and why it is opt-in.** Run option `vision_escalation`, **default
|
||
OFF** — the opposite of `expensive_geometry`, deliberately: geometry spends
|
||
seconds on a machine nobody bills, a model call spends money and leaves the
|
||
building. `vision_escalation_max_calls` (default 25) is a hard per-run bound,
|
||
enforced on both sides of the file boundary, and an escalation past it reports
|
||
NOT RUN naming the bound rather than being dropped. A full run over all 1105
|
||
sheets would be roughly **1100 calls, about US$5.40, and 1.5–2 hours** — about
|
||
4,300 input tokens a call (2,200 of skill, 2,000 of image) and ~120 out, at
|
||
Haiku 4.5's $1/$5 per MTok, so **$0.005 a sheet**; the wall clock is dominated
|
||
by the per-view image export, not the model.
|
||
Which views carry bar marks is plant nomenclature (`vision.barmark_views`,
|
||
default empty), so an unconfigured plant gets NOT RUN with that reason and
|
||
never a pass.
|
||
|
||
**Credentials** are read from the environment by the SDK in the CPython child
|
||
(`ANTHROPIC_API_KEY`, or an `ant auth login` profile). No key is in the repo,
|
||
in a knowledge file, in the request, in the response, or in any log — nothing
|
||
in either half ever reads one into a variable.
|
||
|
||
**What is not test-pinnable, said plainly.** The verdict. The same sheet may
|
||
come back `clear` on one run and `cannot_tell` on the next, and no test
|
||
asserts otherwise. What *is* pinned, in `test_ticketvision.py`: a refusal
|
||
renders as NOT RUN; a failure to reach the model renders as NOT RUN and never
|
||
a pass; every outcome carries its provenance; the two kinds never share a
|
||
list, a table or a key; an `AI-` code owns no checklist row; annotation never
|
||
changes a row's status; and the resolution arithmetic refuses a render nobody
|
||
could read.
|
||
|
||
**What was proposed and left.** `NOTE-SENSE` — 6.5's residue, whether a note
|
||
that says *something* says the *right* something — is defined in the skill and
|
||
not wired: it needs `notes.instruction_views` populated to know what to
|
||
render, and no plant has populated it, so it would report NOT RUN on every
|
||
ticket today. 5.3 (face designations) and 8.1 (unresolved lines) were
|
||
considered and **not** proposed: both need a second source the picture does
|
||
not carry, so every answer would be `cannot_tell` by the skill's own rules.
|
||
The title block, the BOM and the handling dimensions were **not** proposed
|
||
either, and that is the one worth stating — every number on them is already
|
||
read exactly by code, and asking a model to re-read them off a picture would
|
||
replace an exact answer with a guess. **Escalation goes down the ladder,
|
||
never up it.**
|
||
|
||
## 9. Deliberately not checked
|
||
|
||
Recorded so nobody proposes them again as oversights. Each was considered
|
||
against real tickets and left out on purpose.
|
||
|
||
**Shear-cone overlap between adjacent erection anchors.** Two anchors close
|
||
enough that their breakout cones intersect share concrete, and the pair is
|
||
worth spreading. On two pieces in the reviewed set the engineer moved a pair
|
||
from 1'-0" to 1'-6" apart for exactly this reason. It is still not a check:
|
||
the spacing that matters depends on embedment, edge distance, what the anchor
|
||
is rated for and what the piece is actually being asked to do, and reasonable
|
||
engineers argue about it. **The verdict was: show the geometry, do not judge
|
||
it.** A tool that fails a ticket over a debatable margin trains people to
|
||
ignore its failures.
|
||
|
||
The general form of that decision is worth keeping: **a check needs a
|
||
criterion that is not in dispute.** Where the criterion is a judgement call,
|
||
the honest product is a number on the page, not a verdict.
|
||
|
||
## What the review measured
|
||
|
||
The twelve tickets were audited, then reviewed by hand, and four were
|
||
corrected. This document's items caught **one** of the four:
|
||
|
||
| piece | corrected | caught here | why not |
|
||
|---|---|---|---|
|
||
| WP199 | dunnage `4'-9"/4'-9"` → `3'-0"/5'-0"` | yes | support sat 2.5 ft inside an opening |
|
||
| WP213 | dunnage `4'-7"/4'-7"` → `4'-0"/5'-0"` | no | support sat **5 inches** from the opening edge — outside it, so a containment test passed |
|
||
| WP219 | `3'-3"/1'-0"` → `2'-9"/1'-6"` | no | anchor spacing, deliberately not checked (§9) |
|
||
| WP220 | `3'-3"/1'-0"` → `2'-9"/1'-6"` | no | same |
|
||
|
||
WP213 is the one to learn from. The check ran, returned, and said the ticket
|
||
was fine — and by its own definition it was right. **A criterion looser than
|
||
reality is more dangerous than a missing check**, because a missing check
|
||
leaves an obvious hole and a loose one produces a clean report.
|
||
|
||
### The criterion, after the fix
|
||
|
||
Dunnage against openings is now a clearance test: the distance from each
|
||
support to the nearest opening edge, with the thresholds in the plant file
|
||
(`handling.dunnage.opening_clearance_fail_in` / `_warn_in`, 3 in and 6 in
|
||
here). Inside an opening stays a fail on its own.
|
||
|
||
Re-run against the same geometry, both versions of both tickets:
|
||
|
||
| piece | version | far support | clearance | verdict |
|
||
|---|---|---|---|---|
|
||
| WP199 | before | 18.17 | inside OPENING 1 | FAIL |
|
||
| WP199 | after | 19.92 | 11.5 in | OK |
|
||
| WP213 | before | 17.38 | 5.0 in | **WARN** |
|
||
| WP213 | after | 17.96 | 8.0 in | OK |
|
||
|
||
**Two of two in-scope corrections, where it had been one of two.** The other
|
||
two the engineer made are anchor spacing, which §9 leaves alone on purpose.
|
||
|
||
The part worth noticing is that the two thresholds were given as plain numbers
|
||
by the engineer, before any of this was run, and they land exactly on the line
|
||
he had already drawn by hand: the piece he treated as a defect comes back FAIL,
|
||
the piece he merely improved comes back WARN. Nothing was tuned to make that
|
||
happen. **When a threshold reproduces a judgement nobody encoded, that is the
|
||
evidence the threshold is real** — and it is also the reason to leave it in the
|
||
plant file rather than in the code, because it is his number, not the tool's.
|
||
|
||
---
|
||
|
||
## What is left for a person, counted
|
||
|
||
The point of grading each item A to D was to find out how much of this a
|
||
program can do. With the A and B items written, the report renders 29 rows per
|
||
ticket (27 before 1.4 and 6.5 landed on 2026-08-30) and they fall out like this:
|
||
|
||
| | rows | |
|
||
|---|---|---|
|
||
| covered by code | **22** | OK / WARN / FAIL / FOR ENGINEER, all produced by a check that ran |
|
||
| code not written yet | 0 | was 1.1, 5.4, 6.5, 8.2. All four are now written; 1.4, which this tally never listed, is written too (2026-08-30) |
|
||
| missing an input | 1 | 3.3 — the spec carries no f'c/f'ci to compare against |
|
||
| a person's job by design | 2 | 5.3 narrows to a shortlist, 5.5 is reading the drawing |
|
||
|
||
**Amended 2026-08-30, second pass.** `CHECKLIST_ITEMS` now carries **31** rows,
|
||
not 29: 2.4 and 4.5 were rows-without-checks when that count was written, and
|
||
both are now built (`CAST-EXTRA`, `STRAND-CALLOUT`), so "covered by code"
|
||
is **24**. The residue is unchanged — 3.3 still lacks its input, 5.3 and 5.5
|
||
are still a person's job — and no row has moved *into* the residue. The
|
||
column arithmetic above never summed to the row count and still does not; it
|
||
was not re-derived here, because doing that honestly means re-running the
|
||
report, not editing a table.
|
||
|
||
**Not one row needs a model to look at a picture.** The four unwritten ones are
|
||
code nobody has typed; the missing input is data, not judgement; and the last
|
||
two are work that should stay with a person — a tool that took them over would
|
||
be pretending to a competence it does not have.
|
||
|
||
That answers a question that had been open on cost grounds: whether the
|
||
drawing-reading part should move to a local model on the shop's own hardware.
|
||
It should not, because after the A and B items landed there is no
|
||
drawing-reading part left to move.
|
||
|
||
**Amended 2026-08-30.** That still holds for every row as written, and 6.5 —
|
||
the item most likely to have needed a picture — turned out not to: a note
|
||
view's contents are queryable elements, so emptiness is counted, not seen.
|
||
But building it exposed a residue the ladder has no rung for: whether a note
|
||
that *does* say something says the **right** thing. A verdict from a model
|
||
reading the rendered sheet is not grade A however valuable it is, because it
|
||
cannot be pinned to a fixed answer by a test. Before any such check is built,
|
||
it needs a category of its own and a rule about what a report may claim on
|
||
its behalf. Nothing has been built on that path.
|
||
|
||
**The reason the question became answerable is worth more than the answer.**
|
||
While the report was a list of findings, "how much is left for a person" could
|
||
only be estimated, because an item nobody checked produced no line and looked
|
||
exactly like an item that passed. Once every item occupied a row — including
|
||
the ones marked NOT RUN, with the reason — the question became arithmetic.
|
||
|
||
**A system that cannot say what it did not do cannot be measured.**
|
||
|
||
|
||
---
|
||
|
||
## A row can also be *not a question about this piece* (2026-08-31)
|
||
|
||
Everything above grades a row by **who answers it**. This section is about a
|
||
different axis the ladder has no rung for: **whether the question applies at
|
||
all** — and the answer to that is plant knowledge, conditioned on what the
|
||
piece is, not a property of the row.
|
||
|
||
Owner, 2026-08-31, reading insulated panels: *"ticketqc 检查的内容需要进行分级。
|
||
根据 plant 和 ticket 的具体内容进行分级 ... 让 ticketqc 工具读取厂知识来得知啥需要
|
||
检查,啥不用检查。"* The mechanism is `checks.not_applicable` /
|
||
`checks.only_applies_to`; the shape, the merge law, the classifier and the
|
||
worked non-examples are in **`docs/knowledge-override.md`**. What belongs here
|
||
is what a **reader of the report** sees.
|
||
|
||
### The five states, and the colour each one is printed in
|
||
|
||
Colours are pinned by tests. A new state gets its own entry, never a reuse of
|
||
an existing one.
|
||
|
||
| state | colour | did the check run? | what it means |
|
||
| --- | --- | --- | --- |
|
||
| `OK` | green | yes | judged, and it is fine |
|
||
| `WARN` / `FOR ENGINEER` | yellow | yes / by design not | a person must look |
|
||
| `FAIL` | red | yes | the piece is wrong |
|
||
| `NOT RUN` | **red, exactly like FAIL** | no | **nobody checked.** An unrun check tells you nothing about the piece, so it is never quieter than a failure |
|
||
| `MUTED` | grey | **yes** | it ran, and this job asked for it quietly. The note says what it really reported, and a FAIL is never muted |
|
||
| `NOT APPLICABLE` | **blue** | **no — not at all** | a knowledge layer declares this question is not about this kind of piece |
|
||
|
||
Blue is calm on purpose, and calm is only safe because the declaration is loud
|
||
elsewhere: **every run prints, per piece, which profile ran** —
|
||
|
||
> **PNL101** classified as **insulated** — 3 row(s) declared not applicable:
|
||
> 3.3, 5.4, 6.6
|
||
|
||
— and then every switched-off row with its reason, its date, its declarer and
|
||
the layer that declared it. That section **opens itself whenever anything was
|
||
switched off**, unlike the muted section, which folds when it is tidy. A mute
|
||
leaves the check running; this stops it, and a mistaken rule here is a safety
|
||
check disabled in silence.
|
||
|
||
### Reading a blue row
|
||
|
||
* The check **did not run**. There is no verdict behind the row and there is
|
||
not meant to be — which is exactly what separates it from grey.
|
||
* `judged` is **false** in `qc-run.json`, the same as `NOT RUN`. The
|
||
difference between them is *why nobody asked*, and that is the state word
|
||
and the reason, never a truth value.
|
||
* **A row that FAILED is never blue.** The declaration is recorded as
|
||
DECLINED and the row keeps its red — the same ruling as a mute over a
|
||
failure. This is not theoretical: 4.4 and 4.6 both name
|
||
`check_strand_layout`, so a check kept alive for the row that still applies
|
||
can put a finding on the row that does not.
|
||
|
||
### Reading a red row that a *scoped* declaration produced
|
||
|
||
A row declared with `only_applies_to` exists only for one kind of piece. On a
|
||
piece nothing could classify it reads **NOT RUN, in red**, saying so — never
|
||
OK and never blue. Running it would answer an insulated question about a piece
|
||
that may not be one; calling it not applicable would claim the piece is not
|
||
that type, which is precisely what could not be established. It is cured by
|
||
making the classification work, not by a ruling.
|
||
|
||
### The boundary a plant may not cross
|
||
|
||
A plant declares applicability **over** the rows in this document. It cannot
|
||
define a new one. A new *question* is a code change, and the reason is the
|
||
ladder at the top of this file: a row is a claim the tool makes, and a claim
|
||
needs a check, a skip probe, a grade and a test — the things that let it be
|
||
*wrong in a reviewable way*. A YAML-defined row would have a number, a
|
||
sentence, and no way to fail. That is a checklist row that always looks fine.
|
||
|
||
### What is seeded today: nothing
|
||
|
||
Both tables ship **empty**, and a test pins that. The first evidence run
|
||
(2026-08-31, four live presses on insulated pieces) found **no row whose noise
|
||
was applicability** — the "nearly useless on insulated panels" wall was
|
||
already gone, and it went by running Extract Design and by the face-lifter
|
||
knowledge fix, not by switching anything off. Of the remaining non-OK rows,
|
||
four trace to known bugs (a weight sum that misses a wythe on 16" insulated
|
||
bodies, an 8.1 parser with no vocabulary for `TOP WYTHE`, a 5.6 parser reading
|
||
hardware part numbers as bar marks), and **declaring any of those inapplicable
|
||
would bury a real defect under a legitimate-looking declaration.**
|
||
|
||
The first genuine insulated-only *question* arrived the same day and from the
|
||
owner, not from the noise: every lifter on an insulated panel must have a
|
||
solid zone through the insulation. On a solid panel that question is
|
||
meaningless. It is the worked example of `only_applies_to` and it is **not
|
||
built** — the population is still being measured and what it would compare is
|
||
undetermined.
|
||
|
||
|
||
---
|
||
|
||
## The checklist is now data, and a plant numbers its own (2026-08-31)
|
||
|
||
Everything above describes the questions. Where they *live*, and what the
|
||
numbers on a report mean, changed on 2026-08-31 — the mechanism, the merge
|
||
law and the reference grammar are in **`docs/knowledge-override.md`**; what
|
||
a reader of the report needs is here.
|
||
|
||
### Three layers
|
||
|
||
| layer | owns |
|
||
| --- | --- |
|
||
| **public** (`checklist.master` in `knowledge/catalogue.yaml`) | **what the questions are** — id, wording, grade, owner codes, which check answers it |
|
||
| **plant** (`checklists.profiles`) | **which of them this plant asks of which kind of member**, in what order, under what numbers |
|
||
| **job** (`checks.muted`) | the mute buttons |
|
||
|
||
The check **functions** stay code. A row is a claim the tool makes, and a
|
||
claim needs something that can be wrong in a reviewable way — a probe, a
|
||
code, a grade, a test. So **what exists is publishable knowledge; what
|
||
answers it is code**, and a new *question* is still a code change.
|
||
|
||
### Two numbers on every row, and which is which
|
||
|
||
* the **master id** is the identity. It never changes. Every mute, every
|
||
applicability rule and every ruling names it, and a **bare number in any
|
||
document written before 2026-08-31 is a master id** and still means
|
||
exactly what it meant.
|
||
* the **display number** is what this plant's board calls it.
|
||
|
||
On the **page**, the master id prints small under the plant's number and
|
||
only when the two differ — a plant that has not renumbered sees one number,
|
||
not the same number twice. In **`last-ticketqc.txt`** it is always spelled
|
||
out (`2.1 [3.3]`), because the file is the fallback a reader is told to
|
||
trust and it has no hover text. In **`qc-run.json`** (now `qc-run/3`) every
|
||
row carries `item`/`master_id` **and** `display_number`, plus `renumbered`
|
||
so a consumer never has to work out which it is holding.
|
||
|
||
A citation that needs to be unambiguous is **qualified**:
|
||
`sample_plant-wall_panel 3.3`.
|
||
|
||
### The report header says which board ran
|
||
|
||
> **PNL101** — sample_plant checklist **Wall panel** *(profile `wall_panel`,
|
||
> chosen on member kind)* · member kind: **wall_panel**
|
||
|
||
and beside it the classification line. A renumbered table with neither has
|
||
shown the reader a checklist without telling him whose. The member kind
|
||
comes from the **mark prefix**; a piece whose prefix nobody has declared gets
|
||
**no profile and the full master checklist** — more questions, not fewer.
|
||
|
||
If the board does not name a question that exists in the code, **it is asked
|
||
anyway** and the header says so. A board is a snapshot of what existed when
|
||
somebody wrote it; the alternative would let the next check anybody builds
|
||
run nowhere.
|
||
|
||
---
|
||
|
||
## 5.8 / 5.9 / 5.10 — connectors on an insulated panel
|
||
|
||
Added 2026-08-31: *"insulated panel 加一个 check,检查 connector 是不是存在,
|
||
以及够不够"*, and *"再增加 connector edge distance 的 check"*.
|
||
|
||
A **connector** crosses the insulation of a sandwich panel and ties the two
|
||
concrete wythes together. All three rows are **insulated-only**.
|
||
|
||
| | | |
|
||
|---|---|---|
|
||
| **5.8** | connectors present | **zero is a FAIL**, not a WARN — untied wythes are a structural condition. It is only ever reached after the recognition gate has confirmed the tool can see connectors at all, so a zero here is a zero in the piece. |
|
||
| **5.9** | enough of them | compares against what the design calls for. Stands down today: the summary line `HK ST100'S @1'-4" O.C.` is one 8.1 cannot parse. |
|
||
| **5.10** | edge distances | product-first minimum; a plant-wide fallback is named as such in the finding. |
|
||
|
||
**What each says when it cannot answer**, because none of them may go quiet:
|
||
|
||
* recognition unverified → all three stand down with the plant's own reason
|
||
(one gate, three rows, never three copies of it);
|
||
* on a **solid** panel → `NOT APPLICABLE`, blue: there is no insulation for a
|
||
connector to cross, so the question is meaningless rather than quiet;
|
||
* on a piece nothing could **classify** → `NOT RUN`, **red**. Calling it not
|
||
applicable would claim the piece is not insulated, which is exactly what
|
||
could not be established;
|
||
* no published minimum for 5.10 → stands down naming the missing datum, and
|
||
**never** borrows another product's number;
|
||
* no readable requirement for 5.9 → stands down naming the count it did not
|
||
judge. A count on its own is a number, not a judgement.
|
||
|
||
**Today all three read NOT RUN on every piece**, because
|
||
`panel_types.detect_verified` is false — the connector recognition is
|
||
measured and verified, but the panel-type classifier has never been run
|
||
against the model. One press confirming it arms all three.
|
||
|
||
Two calibrations worth keeping, both measured 2026-08-31 on one insulated
|
||
piece: the field spacing is **16.0 in**, which *is* the design's 1'-4" — and
|
||
the tighter 9.5/6.5 pairs at the ends are **normal**, so the comparison is
|
||
against the widest gap. And the smallest in-plane edge distance actually
|
||
built is **6.0 in**, on work the owner has not questioned — so a published
|
||
minimum above 6 is a finding to **surface**, not a reason to set the limit
|
||
at 6.
|
||
|
||
|
||
---
|
||
|
||
## 5.11 – 5.15 — solid zones and insulation recesses (2026-08-31)
|
||
|
||
Added on the owner's **second** asking, which is the first thing worth
|
||
recording:
|
||
|
||
> "没有 solid zone 相关的检查。lifter 自带 solid zone,没检查这些 solid zone
|
||
> 是不是被加入了 assembly,是不是被显示在 ticket 上面,hatch pattern 对不对。
|
||
> 这些都没有啊"
|
||
|
||
> "不光是 solid zone,insulation recess 也得加上。你看看 page 2 里面的那些
|
||
> hatch pattern 就知道了。"
|
||
|
||
A **solid zone** is a block of concrete that replaces the insulation locally
|
||
so a lifting insert has solid material to pull against. On this job every
|
||
lifter *nests* one inside its own family. A **recess** is a partial-depth
|
||
pocket in the insulation, and on this job it is **not a model element at
|
||
all** — it is a drafted filled region on the ticket. All five rows are
|
||
**insulated-only**, through `checks.only_applies_to` in the plant layer.
|
||
|
||
### The measurements everything below rests on
|
||
|
||
All bridge-verified 2026-08-31, over the 27 insulated pieces:
|
||
|
||
| | |
|
||
| --- | --- |
|
||
| nested zones | **192 of 192** lifters nest exactly one, via `GetSubComponentIds()` |
|
||
| standalone zones | 82 more, at the openings, nested in nothing |
|
||
| assembly members | **21 of 192**. 17 of the 27 pieces have **zero** |
|
||
| rendered on a ticket | **0 of 192**. The layout view shows only the standalone ones |
|
||
| the live hatch defect | the `SOLID ZONE` view filter on the insulation view template overrides the surface foreground pattern to **`TRIANG`** — the *recess* pattern |
|
||
|
||
### Why this is five rows and not one
|
||
|
||
They are five different failures with five different repairs, and a single
|
||
row would tell a modeler something is wrong without telling him what to do.
|
||
|
||
| | | |
|
||
|---|---|---|
|
||
| **5.11** | the zone is modelled but left out of the assembly | add it to the assembly |
|
||
| **5.12** | it is out of the assembly, so it never reaches the layout view | *the same repair* — which is exactly why the row says so |
|
||
| **5.13** | it is drawn, with the wrong hatch | fix the view filter |
|
||
| **5.14** | a recess the design calls for is not drawn | draw it |
|
||
| **5.15** | a recess is drawn with the wrong hatch | fix the region type |
|
||
|
||
**5.11 and 5.12 share a repair and are still two rows**, because they do not
|
||
share a *cause*: a zone can be an assembly member and still fail to render
|
||
the day somebody changes the view. Merging them would make the second
|
||
failure invisible for as long as the first one holds.
|
||
|
||
**And 5.12 is not enough on its own.** Nested and standalone zones share
|
||
**one type**, so making the nested ones visible without fixing the filter
|
||
would put **eight more wrongly-hatched zones on every piece** rather than
|
||
none. That is why 5.13 exists as a row rather than as a footnote on 5.12.
|
||
|
||
### The reference for a hatch is the legend's own swatch
|
||
|
||
Not a table in a YAML file. The legend printed on the sheet is the sheet's
|
||
own statement of what each hatch means, and it is **the only reference that
|
||
updates itself** when the plant redraws a swatch. So `expected_hatch_pattern`
|
||
resolves the legend **first** and the plant's
|
||
`insulation.legend.expected_patterns` **second** — and every finding says
|
||
which it used, because *"the legend on this ticket says `ANSI35`"* and *"a
|
||
YAML file written on 2026-08-31 says `ANSI35`"* are different claims and only
|
||
the first can be trusted next year.
|
||
|
||
**The plant map disagrees with the manufacturer's manual on two entries**, and
|
||
that is recorded rather than resolved: the manual (p706 / 8207.2, already in
|
||
`face_designations.insulation_hatch`) says `HONEY` for a 3in recess where this
|
||
legend draws `TUCKPOINT`, and calls `STARS` a 1in cap where this legend labels
|
||
it 2in. The checks are held to **the legend printed on the ticket**, because
|
||
that is what the shop reads. Which of the two governs is the owner's ruling.
|
||
|
||
### What 5.14 does not claim, and why the row exists anyway
|
||
|
||
There is **no model counterpart** to compare a drafted region against, and
|
||
**nothing parses the design side** — no line anybody has taught a reader to
|
||
turn into "this piece should have two 2in recesses". So 5.14 stands down
|
||
today: it prints the inventory it found, states that nothing was compared,
|
||
and names **both** unblockers (a design-summary reader, or a ruling that the
|
||
recesses are modelled after all).
|
||
|
||
A presence count printed as agreement would be the alternative, and it would
|
||
be a green row over a question nobody asked. The row is still worth having
|
||
under this architecture's own reasoning: the inventory is real and written
|
||
down, the unblocker is in the sentence a reader actually sees, and the day
|
||
either half arrives the comparison is already written — `recess_presence_status`
|
||
takes the requirement argument today and simply has not been given one.
|
||
|
||
**One candidate complication, asked and unresolved.** `VOID_REVEAL_WIDE`
|
||
(`MC='VOID REVEAL'`) might be a *modelled* recess. The drafted-region path is
|
||
built; the modelled path is a **named dormant slot**
|
||
(`insulation.recess.modelled_path_verified: false`) and 5.14's own finding
|
||
says the modelled path was not consulted. Naming it rather than leaving it
|
||
implicit is what makes turning it on a YAML edit.
|
||
|
||
### The size question 5.15 refuses to answer
|
||
|
||
The legend's **text** says 1" and 2"; the region **type names** say 2in and
|
||
3in. Both are on the same sheet. 5.15 compares **patterns**, which have an
|
||
exact answer, and carries a sentence saying the size discrepancy is the
|
||
owner's question and that he has been asked. A row that quietly picked a side
|
||
would launder a question into a verdict.
|
||
|
||
### What each row does when it cannot answer
|
||
|
||
None of the five may go quiet, and every empty return leaves a sentence a
|
||
skip probe reads — the shape 6.4 had to be retrofitted with, built in from
|
||
the first commit here.
|
||
|
||
* **the solid-zone family unset, or set and unverified** → all three
|
||
solid-zone rows stand down with the plant's own reason. This is the 6.6
|
||
guard and it is doing more work than usual: a family name that matches
|
||
nothing counts **zero** zones on every piece, and zero zones reads as *"no
|
||
lifter is missing one"* — a permanent invisible pass over the whole defect
|
||
class the owner just commissioned.
|
||
* **no lifter recognised** → nothing to look inside; the existing
|
||
connector/lifter recognition is reused and **no fifth recogniser was
|
||
built**.
|
||
* **no sheet issued** → **NOT RUN, "no sheet issued"**, and it is neither a
|
||
pass nor a failure. 12 of the 27 insulated pieces have no sheet yet.
|
||
* **the layout view unnamed at this plant** → stands down naming
|
||
`drawings.insulation_layout`, which is landed beside `drawings.bed_plan`
|
||
for exactly the reason that key exists.
|
||
* **no legend read and no plant map** → the *reference itself* is missing, and
|
||
it is named rather than filled in. **No neighbouring concept's pattern is
|
||
ever borrowed** — one concept drawn with another's hatch is the whole defect.
|
||
* **the rendered pattern not gathered** → a named silence, never a pass.
|
||
|
||
**Zero and ungathered never merge.** `solid_zones` reaches the check as `[]`
|
||
when the gather ran and found none, and as `None` when it never ran, and the
|
||
two produce different sentences. That is 6.4's lesson, landed the same day and
|
||
built in here rather than retrofitted.
|
||
|
||
### The gather half, and what is dormant
|
||
|
||
**5.11 and 5.12 run today.** The gather is `GetSubComponentIds()` on each
|
||
recognised lifter plus `AssemblyInstanceId` against the assembly — the exact
|
||
reference the measurements were taken with, in `ticketqc.read_ticket`.
|
||
Membership is also how 5.12 answers: the layout view is assembly-scoped, so a
|
||
member reaches it and a non-member cannot. **That relationship is an
|
||
inference, not a rendered-geometry read, and 5.12's finding says so in its
|
||
own words** — because the repair follows from the basis, not from the verdict.
|
||
|
||
**5.13, 5.14 and 5.15 are dormant and loud.** Four keys are not gathered, and
|
||
`ticketqc.py` carries a block naming exactly what each one costs to fill:
|
||
`recess_regions` (a view-scoped `FilledRegion` collector), `recess_patterns`
|
||
(`FilledRegionType.ForegroundPatternId`, **then** any view filter, because
|
||
reading only the type would report every pattern correct on a job whose defect
|
||
is precisely a filter override), `solid_zone_pattern`
|
||
(`View.GetFilterOverrides(...).SurfaceForegroundPatternId`, resolved against
|
||
the view **template**), and `legend_patterns` (the legend view's own swatches).
|
||
None of those API paths has been run against a live document, and guessing an
|
||
enum or a property name is how Revit dies without an exception. One bridge
|
||
session lands all four.
|
||
|
||
### And the edge that 5.10 measures to changed the same day
|
||
|
||
> "pin edge distance = 3". ST100 的知识。写。检测 edge 检测的不是 concrete
|
||
> edge,而是 insulation edge,所以如果 insulation 有 solid zone 被 cut
|
||
> through 而不只是 recess 的话,那么 edge 就是被 cut 之后的 edge。如果只是
|
||
> recess,则按照原来的 edge 来算。"
|
||
|
||
Two things landed, and the second is the larger one.
|
||
|
||
**The number.** `HK Composites (ST100).min_edge_distance_in: 3.0`, source
|
||
*stated by the owner*. ST50 and the rest stay null with their reason: he gave
|
||
one product's number, and borrowing it across designations is exactly what
|
||
that reason field forbids.
|
||
|
||
**The semantics.** The edge is the **insulation** edge, not the concrete one.
|
||
A solid zone that **cuts through** the insulation redefines it — concrete has
|
||
replaced the insulation, so the zone's border *is* an insulation edge. A
|
||
**recess** does not: insulation still runs underneath, and the original
|
||
boundary governs. So 5.10's reference geometry is *the insulation extent minus
|
||
the through-cut solid zones*, which means it consumes the very population
|
||
5.11–5.13 recognise. **One recognition, several consumers** — a second reader
|
||
for the same parts is how two rows come to disagree about what is on a piece.
|
||
|
||
**Through-cut versus partial is an inference from names**, and it is labelled
|
||
one everywhere. Depth is not a field; it is text in the type's description
|
||
(`3" SOLID ZONE`, `4" SOLID ZONE`) beside an insulation type named for its own
|
||
thickness. Equal depths read as through-cut. **A zone whose depth cannot be
|
||
parsed stands the pins near it down** rather than defaulting either way —
|
||
`False` would quietly measure to the old edge, which is the permissive
|
||
direction, and `True` would invent an edge that may not exist.
|
||
|
||
**The 6.0 in calibration has been withdrawn**, and withdrawing it is the point.
|
||
It was measured pin-to-**panel**-edge, and he has just defined the measurement
|
||
as pin-to-**insulation**-edge. Those are different measurements of different
|
||
things, and carrying the number across would have set an expectation that the
|
||
first live run then looked like a regression against. The insulation-edge
|
||
distances on this job are **unmeasured**; whatever the first run prints is new
|
||
information.
|