25592fa6c2
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.
427 lines
22 KiB
Markdown
427 lines
22 KiB
Markdown
---
|
|
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.
|