--- 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 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.