Files
willtools-demo/docs/ticket-check.skill.md
will 25592fa6c2
ci / test (push) Successful in 6m14s
WillTools 2026-09-09 -- clean snapshot of the development repo for review
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.
2026-09-09 08:35:15 -05:00

22 KiB

name, description, when_to_use
name description when_to_use
ticket-check 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. 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.

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.

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:

    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.