WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m14s
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,426 @@
|
||||
---
|
||||
name: ticket-check
|
||||
description: Check one precast shop ticket for errors by cross-reading three
|
||||
sources that must agree — the printed page, the Revit model, and the design.
|
||||
Encodes what went wrong when this was done by text extraction, and what a
|
||||
fresh reader got wrong when handed the same page.
|
||||
when_to_use: >
|
||||
Someone asks whether a ticket is right, or asks to audit a batch of tickets.
|
||||
Also the reference for building the coded checks: everything here that is a
|
||||
comparison between two numbers belongs in ticketchecks eventually.
|
||||
---
|
||||
|
||||
# Checking a shop ticket
|
||||
|
||||
Three sources have to agree: **the page**, **the model**, **the design**. A
|
||||
finding is a disagreement between two of them, named with both values and both
|
||||
sources. Anything else is not a finding.
|
||||
|
||||
## 0. Read the page. Do not read the page's text.
|
||||
|
||||
Render the PDF and look at it.
|
||||
|
||||
```python
|
||||
import pymupdf # pip install --target <scratch> pymupdf
|
||||
doc = pymupdf.open(path)
|
||||
for i, page in enumerate(doc):
|
||||
page.get_pixmap(dpi=200).save('p%d.png' % (i + 1))
|
||||
```
|
||||
|
||||
200 dpi on a 1224x792 pt sheet gives 3400x2200, which is legible.
|
||||
|
||||
**If something looks too small to read, that is not an answer — crop it and
|
||||
render that region larger.** A reader who reports "unreadable in the rendered
|
||||
image" has skipped a step, and on this ticket format everything it is likely to
|
||||
skip is exactly what matters: the bill of materials, the handling diagrams and
|
||||
the title block are all small type in the corners.
|
||||
|
||||
```python
|
||||
page = doc[0]
|
||||
r = page.rect
|
||||
regions = { # fractions of the sheet, this format
|
||||
'bom': (0.66, 0.02, 1.00, 0.40), # top right
|
||||
'handling': (0.63, 0.05, 0.78, 0.42), # the three diagrams
|
||||
'title': (0.80, 0.60, 1.00, 1.00), # bottom right
|
||||
}
|
||||
for name, (x0, y0, x1, y1) in regions.items():
|
||||
clip = pymupdf.Rect(r.x0 + x0 * r.width, r.y0 + y0 * r.height,
|
||||
r.x0 + x1 * r.width, r.y0 + y1 * r.height)
|
||||
page.get_pixmap(dpi=400, clip=clip).save('%s.png' % name)
|
||||
```
|
||||
|
||||
400 dpi on a crop is worth several times the whole page at 200. Re-render and
|
||||
look again before writing the words "could not read".
|
||||
|
||||
**Extracting the PDF's text stream does not work and will produce confident
|
||||
wrong answers.** On this project it silently dropped every dimension inside the
|
||||
handling diagrams, which led to a written finding that the tickets printed no
|
||||
handling positions at all. They print them clearly. The same extraction decoded
|
||||
nothing usable from four of twelve files because of their font encoding, and
|
||||
those four were nearly reported as missing their notes.
|
||||
|
||||
Text extraction is for searching a page you have already looked at. It is not
|
||||
for deciding what is on it.
|
||||
|
||||
## 1. What is on a ticket
|
||||
|
||||
Per piece, two sheets, both named after the mark and numbered `1 OF 2` and
|
||||
`2 OF 2`. **A ticket is the union of them.** Tools that resolve a mark to one
|
||||
sheet get whichever one wins and then report everything on the other as missing.
|
||||
|
||||
**Sheet 1** — BED PLAN with the embed callouts and their dimension chains; the
|
||||
**bill of materials** (QTY / MARK / DESCRIPTION); three handling diagrams
|
||||
(PICKING/STRIPPING, ERECTION, DUNNAGE) each with dimensions; production notes;
|
||||
the title block.
|
||||
|
||||
**Sheet 2** — sections. Section A-A carries the strand count for this piece and
|
||||
the transverse strand chain; section B-B carries the bar spacings.
|
||||
|
||||
The title block is the densest checkable block on the ticket: mark, length,
|
||||
width, thickness, weight, f'c, f'ci, quantity, product code, base and face mix.
|
||||
|
||||
## 2. The comparisons, in the order that finds the most
|
||||
|
||||
1. **Title block against the model.** Length, width, thickness, weight. Weight
|
||||
agreeing to a few pounds is a strong signal the geometry is right.
|
||||
2. **Handling diagram dimensions against the piece's geometry. This one is not
|
||||
optional.** Convert each printed dimension to a station along the piece and
|
||||
ask what is at that station: an opening, an insert, nothing. It is the only
|
||||
comparison that has ever found a real defect on these tickets, and both
|
||||
readers who skipped it reported the ticket as passing. See §4.
|
||||
**How to get a station.** `ticketqc.read_ticket` gives each lifter a `local`
|
||||
tuple in the panel's own frame — there is no `x` or `y` key, and asking for
|
||||
one returns None and looks like missing data. `local[0]` is measured from the
|
||||
panel **centre**, so:
|
||||
|
||||
```python
|
||||
station_ft = t['length'] / 2.0 + lift['local'][0]
|
||||
```
|
||||
|
||||
Checked: WP200's four P136 are at `local[0] = ±7.208` on a 22.917 ft piece,
|
||||
giving 4.25 ft from each end, and its PICKING/STRIPPING diagram prints
|
||||
`4'-3"`. Exact agreement. Openings come from the same read.
|
||||
|
||||
3. **Bill of materials against the assembly's members**, family by family.
|
||||
4. **Strand count in section A-A** against the model and against the design.
|
||||
5. **Anything on the page against anything else on the page.** A production note
|
||||
naming a bar mark that is not in the bill of materials is a finding and
|
||||
needs no other source.
|
||||
|
||||
## 3. Traps, each of which has bitten
|
||||
|
||||
**The BOM's strand row is a length, not a count.** `321 | 0010001 | M392 - 1/2"
|
||||
270ksi Low Lax Strand` on a 22'-11" piece is 321 linear feet. 321 / 22.917 =
|
||||
14.007, and section A-A prints `(14)`. On the next piece, 249 / 24.917 = 9.993
|
||||
against a printed `(10)`. A fresh reader compared 321 against 14 modelled
|
||||
strands and reported a 23x error as the ticket's most critical defect.
|
||||
|
||||
**Nested families double-count.** An element whose `SuperComponent` is not None
|
||||
belongs to another member. `SR5602` sits inside the `P76` embed and
|
||||
`RL_OVAL_PATCH_4T` inside `QLPD4T5WG`. Count them as their own BOM rows and the
|
||||
totals are wrong in both directions.
|
||||
|
||||
**A count printed in the design summary is not this piece's count.** The design
|
||||
covers a family. WP1's summary says 14; the pieces print 10, 6 or 14 according
|
||||
to what each carries. Comparing the summary's number to a piece's model is a
|
||||
guaranteed false finding — that is what `legend.printed_strand_count_is_per_piece`
|
||||
is for, and on this plant it is False.
|
||||
|
||||
**`ValueString` is not what prints.** Dimensions here carry a `ValueOverride` of
|
||||
`U+200E`, an invisible character, and still print their value. Read the page to
|
||||
learn what the page says.
|
||||
|
||||
**There is no legend view.** 0 of 204 sheets carry one. The information the
|
||||
`LGD-MISSING` check looks for is the BOM table plus the section counts.
|
||||
|
||||
**Read the digits carefully.** A fresh reader read 24,345 as 23,245 and reported
|
||||
a 4.7% weight error. The real difference was 3 lb.
|
||||
|
||||
**Close every dimension chain before you compare a station to anything.** Add
|
||||
the printed segments and compare the total against the piece's own length. If
|
||||
they do not match, the difference is a segment the ticket does not dimension,
|
||||
and you recover it by subtraction:
|
||||
|
||||
missing = panel_length - sum(printed segments)
|
||||
|
||||
**A blank segment in the PICKING/STRIPPING, ERECTION or DUNNAGE chain is
|
||||
correct. It is how these tickets are drawn, not a defect — do not report it.**
|
||||
The crew locates the points from the ends; the interior span is derived and is
|
||||
deliberately left off.
|
||||
|
||||
What the blank segment WILL do is produce a false finding if you assume the
|
||||
printed segments are contiguous. WP213 prints `2'-0" | 6'-0" | 7'-4" | 2'-0"`,
|
||||
summing to 17'-4" on a 21'-11" piece. Reading those as consecutive put the third
|
||||
pick at 15'-4", the model had it at 12'-7 1/2", and a 2.7 ft discrepancy was
|
||||
reported that does not exist — the missing 4'-7 1/2" closes the chain exactly
|
||||
and the model was right all along. WP412 is the same shape: printed segments sum
|
||||
to 20'-0" on a 24'-11" piece, the recovered 4'-11" closes it, and the four
|
||||
stations then land on the four modelled inserts to the inch.
|
||||
|
||||
So: close the chain, recover the blank, then compare. The recovered station is
|
||||
as good as a printed one.
|
||||
|
||||
## 4. The finding this process exists to catch
|
||||
|
||||
WP199's DUNNAGE diagram prints `4'-9"` from each end. The piece is 22'-11", so
|
||||
the ticket puts dunnage at 4.75 ft and **18.17 ft**. `OPENING 1` runs 15.60 to
|
||||
18.96 ft. The ticket instructs the yard to set a dunnage block where 74% of the
|
||||
width is missing — S of 403 in3 against 1,525 in3 for the full section.
|
||||
|
||||
Nothing about that is visible from the model alone, or from the page alone. It
|
||||
needs the printed position and the modelled geometry together, which is the
|
||||
whole argument for this being a cross-read rather than a checklist.
|
||||
|
||||
## 4b. The coverage table — fill this in and return it, always
|
||||
|
||||
Three readers have now been told that comparison 2 is mandatory. Three skipped
|
||||
it and issued a verdict anyway. Saying "not optional" in prose does not work,
|
||||
because **an omission is invisible**: a report that simply never mentions a
|
||||
comparison reads exactly like a report where it passed.
|
||||
|
||||
So the report opens with this table, every row filled, before anything else:
|
||||
|
||||
| # | comparison | done? | result |
|
||||
| --- | --- | --- | --- |
|
||||
| 1 | title block vs model — length, width, thickness, weight | | |
|
||||
| 2a | **picking** stations vs model | | |
|
||||
| 2b | **erection** station(s) vs model | | |
|
||||
| 2c | **dunnage** stations vs model | | |
|
||||
| 3 | bill of materials vs assembly members, family by family | | |
|
||||
| 4 | strand count in section A-A vs model vs design | | |
|
||||
| 5 | page against itself: notes, callouts, marks | | |
|
||||
|
||||
**Why the handling row is three rows.** It used to be one, and a reader did the
|
||||
picking comparison, marked the row **yes**, and never touched erection or
|
||||
dunnage — which is where that ticket's most interesting question was. A row that
|
||||
can be half-done will be half-done and reported as whole. **Split any row you
|
||||
are tempted to mark "yes, mostly" until it cannot be half-done**, and mark the
|
||||
pieces separately.
|
||||
|
||||
Row 1 stays one row because its four values are read from one block in one
|
||||
glance — but its `result` must name all four numbers. A `result` that says
|
||||
"matches" without the numbers is not evidence that the comparison happened.
|
||||
|
||||
`done?` is **yes** or **no**. There is no third value, and there is no partial
|
||||
**yes**. "Major items match, the rest was incomplete" is a **no** — a row is
|
||||
**yes** only when every item in it was compared. Writing **yes** and then
|
||||
describing what you did not finish is the same omission in a different place.
|
||||
|
||||
A row marked **no** carries its reason in `result`, and by §5 a single **no**
|
||||
means **no verdict of any kind** — not "pass", not "conditional pass", not "no
|
||||
defect found". Report the findings you have and stop. "Conditional pass" is a
|
||||
verdict wearing a hedge, and it will be read as a pass.
|
||||
|
||||
**There is no separate "could not check" section. The table is the only place
|
||||
it goes.** An earlier version of this skill had one, and it became the place to
|
||||
confess what the table was denying: a reader marked row 1 **yes** with
|
||||
"weight not available in the model" beside it, and listed the weight under
|
||||
"could not check" further down. Same for erection: **yes**, and "unable to
|
||||
locate which dimension corresponds" in the same cell, and again in the appendix.
|
||||
|
||||
Marking **yes** and describing a failure in the same breath is not a slip. It
|
||||
happens because the box and the prose have different audiences — the box gets
|
||||
skimmed and the prose gets written to be safe. Removing the appendix leaves the
|
||||
confession nowhere to go but the `done?` column, which is where it is read.
|
||||
|
||||
**Test before you write `yes`:** could you state the two values and the two
|
||||
sources for this row, right now, without hedging? If not, the row is **no**.
|
||||
|
||||
Filling a row **yes** means you did the arithmetic. For row 2 that means: each
|
||||
printed dimension converted to a station along the piece, and each station
|
||||
looked up against the model — what is there, an opening, an insert, nothing.
|
||||
A row is not **yes** because you looked at the diagram.
|
||||
|
||||
## 4c. What to look for at each handling station
|
||||
|
||||
Once you have the stations, ask what is there. Three things have mattered:
|
||||
|
||||
- **An opening under a dunnage point.** WP199's ticket puts dunnage at 4.75 and
|
||||
18.17 ft; `OPENING 1` runs 15.60 to 18.96 ft, so the ticket sets a block where
|
||||
74% of the width is missing. This is the one real defect this method has
|
||||
found. Check every dunnage station against the openings, every time.
|
||||
- **Which insert belongs to which stage. Read it, do not infer it.** The
|
||||
knowledge file already says:
|
||||
|
||||
lifter_groups:
|
||||
face: families: [P136] at: measured
|
||||
edge_top: families: [SAMPLE_CLIENT_EMBED_P76] at: top
|
||||
stages:
|
||||
stripping groups: [face] # P136 only
|
||||
transportation groups: [face] # P136 only
|
||||
erection groups: [face, edge_top] # P136 AND P76
|
||||
|
||||
**Stripping and transportation use P136 alone. P76 appears only at erection.**
|
||||
|
||||
**The ERECTION diagram has two points and only one carries a dimension.** The
|
||||
dimensioned one is a lower-row **P136**; the other is the **P76 along the top
|
||||
edge**, drawn as a line with a jogged leader, and it is not dimensioned along
|
||||
the length because it is an edge, not a station. That is correct drawing, not
|
||||
a missing dimension.
|
||||
|
||||
On WP412 the printed `2'-6"` lands at 22.417 ft, which is a P136 station —
|
||||
and that is exactly right. An earlier reader assumed a dimensioned erection
|
||||
point had to be a P76, compared it against the P76 stations, and reported a
|
||||
1'-0" discrepancy on three pieces that do not have one. Two of those three
|
||||
readings were also copied from someone else's report and never verified.
|
||||
|
||||
Note also `at: top` on the P76 group: its modelled box centre sits about 7 in
|
||||
inboard of the panel end because it is welded to a bar, but the anchor acts at
|
||||
the end. Never compare a printed dimension against a P76's measured centre.
|
||||
- **Whether a station has any insert at all.** A printed dimension that lands on
|
||||
bare concrete is a finding on its own — for picking and erection. **Not for
|
||||
dunnage.**
|
||||
|
||||
**Dunnage points are not inserts and nothing in the model marks them.** The
|
||||
piece rests on blocks on a trailer; there is no family, no element, no station
|
||||
to compare against. So there is no such thing as a dunnage station "not matching
|
||||
the model", and a reader who compares a printed dunnage dimension against the
|
||||
nearest lifter and reports the difference has invented a finding — that has
|
||||
happened, and the reported discrepancy was one inch between two things that are
|
||||
not the same kind of thing.
|
||||
|
||||
What a dunnage station IS checked against is the **geometry**: is there an
|
||||
opening there, and how much section is left. That is the whole of row 2c.
|
||||
|
||||
## 4d. "The tool returned nothing" is not "there is nothing"
|
||||
|
||||
This is the most common error on this task, and everyone who has attempted it
|
||||
has made it at least once, including the author of this file.
|
||||
|
||||
- A reader asked `read_ticket` for a lifter's `x` and `y`. Those keys do not
|
||||
exist; the positions live under `local`. It got `None`, wrote up a tool
|
||||
defect, and put it in two documents.
|
||||
- A reader reported the ticket's dimensions as unreadable because the render
|
||||
looked small. They were legible; nobody had cropped.
|
||||
- A reader reported "model returns empty BOM", "weight not available",
|
||||
"strand count not available" for a piece whose model carries eleven family
|
||||
rows, a weight of 15,371 lb matching the ticket to the pound, and ten
|
||||
strands. Its query had failed.
|
||||
|
||||
Empty is a claim about your query, never about the model, until you have shown
|
||||
otherwise. **Before writing that something is absent:**
|
||||
|
||||
1. Run the same query against a piece you already know has that data. The
|
||||
worked example `_scratch/claude_wp199_verify.py` returns length, width,
|
||||
weight, strand count and lifter positions for a real piece — if it works and
|
||||
yours does not, the difference is in your script.
|
||||
2. Check the response is actually yours. Result payloads can arrive from a
|
||||
previous timed-out request; confirm the JSON carries the keys your script
|
||||
returns.
|
||||
3. Say which it is. "My query returned nothing and I could not determine why"
|
||||
is a fair `no`. "The model does not have it" is a claim about the model and
|
||||
needs the evidence above.
|
||||
|
||||
The cost of getting this backwards is not a wasted row. It is a defect filed
|
||||
against a model that is correct, and an engineer sent to look for it.
|
||||
|
||||
## 4e. A criterion that is missing a dimension
|
||||
|
||||
§4d says a tool returning nothing is not the same as there being nothing. This
|
||||
is the sharper version: **a tool returning an answer is not the same as the
|
||||
answer being about what you asked.**
|
||||
|
||||
Every one of these came out of a single afternoon on a single check — whether a
|
||||
dunnage support is safe against an opening — and every one of them produced a
|
||||
confident, well-formed, wrong result:
|
||||
|
||||
| the criterion asked | what it left out | what happened |
|
||||
|---|---|---|
|
||||
| is the support *inside* an opening? | the margin | a support 5 in. from a 3'-4" hole passed |
|
||||
| is the support inside the opening's *length* range? | the width | a spandrel's 11-in. top notch failed a sound piece |
|
||||
| is the remaining concrete wider than N feet? | that pieces differ | 3.9 ft of solid on a 4.8 ft piece read as failure |
|
||||
| does the opening take the support? (asked in one branch) | the other branch | a 2.5-in. corner pocket still fails a support beside it |
|
||||
|
||||
Four faces, one shape: **the check compared the right things along too few
|
||||
axes, and every time the missing axis was invisible in the output.** Nothing
|
||||
said "I only looked lengthwise". It said FAIL, with a number.
|
||||
|
||||
What to do about it, in order:
|
||||
|
||||
1. **Say out loud what the criterion measures, and in what units.** "Distance
|
||||
from the support to the nearest opening edge, in inches, along the length."
|
||||
Written out, a missing axis is usually obvious — the sentence has no word
|
||||
for width in it.
|
||||
2. **Find the case where the criterion and the physics disagree.** Not a case
|
||||
where it gives the wrong answer — one where it gives the *right* answer for
|
||||
the wrong reason. A support beside a notch is fine, and a length-only test
|
||||
also says fine, until the notch gets longer.
|
||||
3. **Test on two objects that differ in the axis you are unsure about.** A wide
|
||||
piece and a narrow one. A big hole and a small notch. One object cannot tell
|
||||
you whether your threshold is absolute or proportional — that specific
|
||||
mistake was made here, with a threshold that worked on an 11'-11" piece and
|
||||
condemned a 4'-10" one.
|
||||
4. **When a fix adds a guard, ask which branches got it.** Three of the four
|
||||
above are the same guard arriving in one place at a time.
|
||||
|
||||
And the reason this matters more than the individual check: **a criterion
|
||||
looser than reality produces a clean report.** A missing check leaves a visible
|
||||
hole and someone eventually asks about it. A check that runs, returns, and
|
||||
quietly means something narrower than its own name is the one that gets
|
||||
trusted.
|
||||
|
||||
## 4f. An exclusion never raises
|
||||
|
||||
§4d says an empty result is not an empty world; §4e says an answer can be about
|
||||
the wrong question. This is the third and the quietest: **something that was
|
||||
excluded produces nothing at all.**
|
||||
|
||||
No finding, no exception, no red test. The tool runs, the report renders, the
|
||||
row says OK. Everything looks the way it looks when the work is fine.
|
||||
|
||||
Three of these landed in one afternoon on this project:
|
||||
|
||||
- A whole check switched off in the plant file, on a misread instruction. The
|
||||
findings did not become wrong; they stopped existing.
|
||||
- `min_remaining_width_ft` unset, so the "support sits in a hole" case could
|
||||
not fire. The support that sat 2.5 ft inside an opening came back **OK, with
|
||||
a comfortable 9.5 inch clearance** -- it measured the distance to the edge of
|
||||
the hole it was standing in.
|
||||
- One line, `for p in parents: covered.add(p)`, excluding parent parts along
|
||||
with the children they cover. A ticket that had been reporting two missing
|
||||
callouts silently became clean. Forty tickets would have followed.
|
||||
|
||||
**Every exclusion has to point at a sentence that authorises it.** Not "this is
|
||||
cleaner", not "these are always noise" -- an actual rule someone stated. The
|
||||
knowledge said `SR4996 is covered by SP29/SP30`; it did not say SP29 and SP30
|
||||
need not be drawn, and reading it that way removed the very thing the rule
|
||||
depends on.
|
||||
|
||||
Two habits that catch these:
|
||||
|
||||
1. **After adding any exclusion, re-run and count what stopped being
|
||||
reported.** If the number is larger than you expected, you excluded more
|
||||
than you meant to. This is the only symptom an exclusion has.
|
||||
2. **When a threshold is absent, do not treat that as permission to pass.**
|
||||
Absent means unknown. An unknown that renders as OK is a lie the tool tells
|
||||
with a straight face; NOT RUN with a reason is the same fact, told honestly.
|
||||
|
||||
And the reason this one is worth its own section: §4d and §4e leave evidence --
|
||||
an empty list, a wrong number. **An exclusion leaves a clean report**, which is
|
||||
indistinguishable from the outcome everybody hopes for.
|
||||
|
||||
## 5. How to report
|
||||
|
||||
- Both values and both sources, or it is not a finding.
|
||||
- **Name the source of every number correctly.** Insert stations read out of the
|
||||
model are the model's, not "the design spec". Getting the provenance wrong on
|
||||
a number that happens to be right is how a wrong number survives the next
|
||||
review — the reader checks the source you named, finds nothing, and moves on.
|
||||
- **A single `no` in the coverage table forbids a verdict.** If any comparison
|
||||
did not happen, the ticket has not been checked and no sentence beginning
|
||||
"this ticket passes" may appear. Report the findings you have and stop. A pass
|
||||
issued over an open row is worse than a false finding, because a false finding
|
||||
gets argued with and a false pass gets filed.
|
||||
- **"The sources disagree" and "I could not find it" are different things**, and
|
||||
the second belongs in a `no` row, not in a finding. Do not pad it either: a
|
||||
reader listed f'c, f'ci and the jacking force as uncheckable because the
|
||||
model's spec fields were empty. All three are printed on sheet 1 in plain
|
||||
text. Another reported the weight as unavailable from the model; it was there,
|
||||
and it matched the ticket to the pound.
|
||||
- One root cause fanning into several codes is one finding. Reading one sheet of
|
||||
a two-sheet ticket produced five codes across twelve pieces — sixty findings,
|
||||
one cause.
|
||||
- A pair of families clashing on nine pieces is one modelling pattern, not nine
|
||||
defects. Cluster by family pair before counting.
|
||||
- Say which pieces were read from the rendered page and which were not. A
|
||||
conclusion from text extraction is provisional until someone looks.
|
||||
Reference in New Issue
Block a user