# Plan — rebuild Ticket QC around the piece, and fix what it gets wrong Two things at once, because they touch the same code and the second is meaningless without the first. 1. **The button becomes selection-driven**, like Panel Lift Check: preselected assemblies run straight through; nothing preselected gives a list to tick. 2. **The checks stop producing false findings** — sixty of them on twelve pieces, roughly a thousand across the job, **all from one cause**. Order is not negotiable. **Phase 1 first** — not because it removes findings (it does not; see its corrected acceptance) but because everything phase 4 compares is split across the two sheets, so nothing after it can be verified until it lands. --- ## What is wrong today, measured An audit of twelve WP1 pieces (WP194, 195, 196, 199, 200, 210, 213, 214, 215, 219, 220, 412) produced 135 findings. **60 of them are artefacts of ONE bug:** | code | count | cause | | --- | --- | --- | | `LGD-MISSING` | 12 | looks for a placed view named `STRAND PATTERN…` / `REINFORCING PATTERN…`. **0 of the 204 WP sheets in this model has one.** The schedule is the BOM table on sheet 1; the per-piece strand count is inside section A-A. | | `FC` | 12 | **the same cause.** `read_ticket` sets `legend_text` only when a legend view is found; with none, it stays `None`, and every comparison against it reports "in the summary, not found on the ticket". | | `FCI` | 12 | " | | `PCF-NOTE` | 12 | " | | `STR-NOTE` | 12 | " | An earlier version of this plan split these between the legend premise and a separate two-sheet bug, and gave phase 1 a 48-finding drop to prove itself by. Measured after the union landed: the four legend-dependent codes did not move. **One cause, and phase 2 owns all of it.** The two-sheet fix is still required, for what phase 4 needs — see phase 1's corrected acceptance. The remaining findings include one real defect worth the whole exercise, and it is the acceptance test for phase 4 — see below. There is also a separate correction in `docs/ticket-audit-wp1-12.md` §4: the 97-tickets-are-two-sheets fact is true and still matters; the attribution of those four codes to it was not. --- ## Phase 1 — a ticket is every sheet named after the mark **Change:** `ticketqc.collect_context` currently maps a mark to one sheet. Make it map a mark to a **list**, and `read_ticket` read the union: placed views from all of them, sheet parameters from whichever carries them. **Do not** special-case `1 OF 2`. Match on the sheet *name* equalling the mark and take everything that matches, however many there are. **Acceptance — CORRECTED 2026-08-27, after the change landed.** The original text said `FC`, `FCI`, `PCF-NOTE` and `STR-NOTE` would fall from 12 to 0 here. **That was wrong, and it was my error, not the implementation's.** Those four compare against `legend_text`, which `read_ticket` sets only when a pattern-legend view is found — and this model has none. They belong to the legend premise, which is phase 2. Measured after the union landed: WP199, WP210 and WP412 each resolve to both sheets, `read_ticket` reports two, and all four still fire with `legends_found: 0`. What phase 1 must actually show: - Every one of the twelve resolves to **both** its sheets, `1 OF 2` and `2 OF 2`, and `read_ticket` reports two. - Sheet parameters — `tkt_length`, `tkt_width`, `tkt_depth`, design number, plant — come back populated, from whichever sheet carries them. - Placed views are the union of both sheets, deduped by element id. - **No regression**: nothing that passed before now fails. - Both suites green: `panellift` and `WillTools.extension`. Phase 1 is correct and necessary regardless. A ticket **is** two sheets, and everything phase 4 compares is split across them: it is a prerequisite, not a finding-reducer. --- ## Phase 2 — the schedule is the BOM table, not a legend view **Change:** repoint the legend checks. The information they want is: - **The bill of materials on sheet 1** — a QTY / MARK / DESCRIPTION table. - **The strand count for this piece** — inside section A-A on sheet 2, printed as `(n) 0010001 (E.L.)`. - **The jacking force** — a `TICKET NOTE - … STRAND JACKING FORCE` view. `legend.pattern_prefixes` stays in the knowledge file for plants that do use a legend view; this plant does not, and the check must not fail a ticket for the absence of something the plant never draws. **A trap that has already produced a "23x error" report:** the BOM's strand row is a **length in feet**, not a count. Verified on three pieces: | piece | BOM row | panel length | BOM ÷ length | section prints | | --- | --- | --- | --- | --- | | WP199 | 321 | 22.9167 ft | 14.007 | (14) | | WP210 | 249 | 24.9167 ft | 9.993 | (10) | | WP412 | 349 | 24.9167 ft | 14.006 | (14) | **Acceptance:** this phase now carries **all sixty** of the false findings, not twelve. `LGD-MISSING`, `FC`, `FCI`, `PCF-NOTE` and `STR-NOTE` all fall to 0 on the twelve pieces — all five come from the same missing legend. A new check compares the BOM strand row divided by the panel length against the section count and against the model, and passes on all twelve. --- ## Phase 3 — the button is driven by the selection Copy the shape already committed in `PanelLiftCheck.pushbutton/script.py` (`20c6bda`), with one deliberate difference. selection -> assemblies, or the assemblies that own the selected members several preselected -> check exactly those, no dialog one preselected -> check that one, no dialog nothing preselected -> a checklist of every assembly, then check the ticked ones **The difference from Panel Lift Check:** when nothing is preselected, do **not** ask "batch or single". Go straight to the list. Ticket QC is always a batch of one or more; the mode question does not exist here. The list needs to be usable at 238 pieces: show mark, design number, and whether a spec or only a regex summary backs it — the information the current design-number dialog shows, moved onto the pieces. **The engine has to change with it.** `ticketqc.run(doc, design_numbers=...)` walks whole designs. Add a path that takes **assemblies** and resolves each piece's design intent per piece. A worked read-only example of exactly this is `_scratch/claude_tkt_run.py`. Keep the design-number entry point working — `tools/sweep.py` and `ticketchecks.check_handling_result` both use it. **Acceptance:** with three assemblies selected, the report covers three pieces and names them. With nothing selected, the list appears, and ticking three gives the same report. Selecting a *member* of an assembly works like selecting the assembly. --- ## Phase 4 — the checks that were never there All of these are arithmetic between two values that already exist. None needs a drawing to be read; none needs judgement. Add them one at a time, each with its own test. ### 4.1 Handling stations against the geometry **This is the one that found a real defect, and it is the reason for the whole refactor.** WP199's ticket prints dunnage at `4'-9"` from each end. The piece is 22'-11", so the blocks go at 4.75 and **18.17 ft**. `OPENING 1` runs **15.60 to 18.96 ft**. The ticket tells the yard to set a block where **74% of the width is missing** — S of 403 in3 against 1,525 in3 for the full section. To do this in code: - Read the three handling diagrams' dimensions. They are `Dimension` elements in the `TICKET NOTE - PANEL - DUNNAGE` / `- ERECTION` / `- PICKING/STRIPPING` views. **Use the `Value`, not `ValueString`** — these dimensions carry a `ValueOverride` of `U+200E` and `ValueString` is not reliable. - **Close the chain.** Printed segments do not sum to the piece length; the difference is a segment the ticket deliberately does not dimension. That is correct and must not be reported. Recover it: `missing = length - sum(printed)`. WP412: `2'-6" 7'-6" [blank] 7'-6" 2'-6"` sums to 20'-0" on 24'-11"; the recovered 4'-11" gives stations 2.5, 10.0, 14.917, 22.417 — which are the four modelled P136 exactly. - For each **dunnage** station, ask what section is left there. An opening under a dunnage block is the finding. **Dunnage points are not inserts** — there is nothing in the model to compare a dunnage station against, only geometry. - For each **picking** station, compare against the modelled insert stations. **Insert stations:** `read_ticket` gives each lifter a `local` tuple. There is no `x` or `y` key — asking for one returns `None` and looks like missing data. `local[0]` is measured from the panel **centre**: `station = length / 2.0 + local[0]`. **Acceptance:** WP199 raises the dunnage-in-opening finding with those numbers. The other eleven do not raise a false one. WP412's four picking stations match the model to the inch. ### 4.2 Weight, three ways Not a comparison — an agreement between three numbers, and **the pattern of the disagreement is the diagnosis**: | | derived from | | --- | --- | | **printed** | the WEIGHT (lbs.) field in the title block | | **reconstructed** | l x w x t less blockouts plus cast extras, x unit weight | | **measured** | the modelled solid's volume x unit weight | - b and c agree, a differs -> the ticket's weight field is stale. - a and b agree, c differs -> something is modelled that the dimensions do not describe. - a and c agree, b differs -> **our** reading of the geometry is wrong. Suspect the tool first. `liftexport.weight_check` already computes all three as `stated`, `reconstructed`, `solid`. What is missing is reporting *which* one disagrees. **Acceptance:** WP219 printed 15,525 / model 15,525 and WP220 printed 15,371 / model 15,371 both come out clean. WP199 printed 24,345 / model 24,348 is inside tolerance. The five pieces the earlier sweep flagged (WP187, 192, 313, 409, 411) each report which of the three disagrees. ### 4.3 The rest, in descending value | check | what it compares | | --- | --- | | BOM row vs assembly members, family by family | needs `SuperComponent is not None` -> skip, or nested children double-count. `SR5602` sits inside `P76`, `RL_OVAL_PATCH_4T` inside `QLPD4T5WG`, `P106` inside `JVI PSA 6035` | | Every placed view's name refers to this design | `TICKET NOTE - WP2- STRAND JACKING FORCE` is on 100 WP1 sheets. Its **content** is WP1's own number, so report the name, never infer the content is wrong | | Invisible characters in ticket text | `U+200E` sits in the design summary and crashed a tool that printed it | | Dimension chains close | every chain, against the piece's own dimension. Report the recovered segment, not a defect | --- ## Ground truth for the tests Measured from the live model and the rendered tickets. Use these as fixtures. | piece | length ft | width ft | thick | weight (ticket / model) | strands | BOM strand ft | | --- | --- | --- | --- | --- | --- | --- | | WP199 | 22.9167 | 11.9167 | 8" | 24,345 / 24,348 | 14 | 321 | | WP200 | 22.9167 | 4.8333 | 8" | 9,389 / — | 6 | 138 | | WP210 | 24.9167 | 8.4167 | 8" | 20,950 / — | 10 | 249 | | WP213 | 21.9583 | — | 8" | — | 14 | 307 | | WP215 | 18.9167 | 11.9167 | 8" | 22,192 / 22,192 | 14 | 265 | | WP219 | 18.4583 | 8.4167 | 8" | 15,525 / 15,525 | 10 | 185 | | WP220 | 18.4583 | 8.3333 | 8" | 15,371 / 15,371 | 10 | 185 | | WP412 | 24.9167 | 11.9167 | 8" | 29,291 / 29,291 | 14 | 349 | Handling dimensions as printed: | piece | picking | erection | dunnage | | --- | --- | --- | --- | | WP199 | — | — | 4'-9" each end | | WP200 | 4'-3" each end | 9" | 4'-9" each end | | WP210 | 2'-6" 7'-6" [blank] 7'-6" 2'-6" | 1'-8" | 5'-2" each end | | WP213 | 2'-0" 6'-0" [blank] 7'-4" 2'-0" | 2'-3" | 4'-7" each end | | WP215 | 3'-10" each end | 2'-3" | 3'-11" each end | | WP219 | 3'-9" each end | 1'-8" | 3'-10" each end | | WP220 | 3'-9" each end | 1'-3" | 3'-10" each end | | WP412 | 2'-6" 7'-6" [blank] 7'-6" 2'-6" | 2'-6" | 5'-2" each end | Insert stations from the model, along the length: | piece | P136 | P76 | | --- | --- | --- | | WP213 | — | 2.75, 8.0, 12.625, 19.958 | | WP219 | 3.75, 14.708 | 2.75, 4.25, 14.208, 15.708 | | WP220 | 3.75, 14.708 | 2.75, 4.25, 14.208, 15.708 | | WP412 | 2.5, 10.0, 14.917, 22.417 | 3.5, 10.0, 14.917, 21.417 | WP199 `OPENING 1` runs 15.60 to 18.96 ft. --- ## Not in scope, and why These need an answer nobody has written down. **Do not decide them in code.** - Whether the erection dimension landing on a `P136` (4 ton) rather than a `P76` (8 ton, and the mark the BOM itself calls "Erection Anchor") is a problem. It happens on WP412. On WP219 and WP220 the printed erection station lands on **no insert at all**. Report as a question; do not fail a ticket on it. - The interference acceptance. The check flags shared material over 0.125"; `P136 x SR5602` overlaps 0.21–0.23" on nine of twelve pieces. - Whether a family pair repeating across nine pieces is one problem or nine. --- ## Working rules - **Read only.** Ticket QC touches nothing. No `Transaction` anywhere in this work. - Both suites stay green, every phase: `panellift` and `WillTools.extension`. - `WillTools.extension/` is **IronPython 2.7**: no f-strings, no pathlib, no dataclasses. - `python` is not on PATH. The interpreter is the Python 3.12 install under the current user's `AppData\Local\Programs`; `tools/rvt.py` and the test commands in `CLAUDE.md` both spell it out. - Run `python tools/scrub.py` before committing. No client or job name in the repo, ever. - **Commit each phase separately.** A phase that cannot be verified on its own is a phase that gets debugged inside the next one. - Push nothing. Will decides.