# Ticket checklist → check code mapping Rebuilt 2026-08-30 from the code (`ticketchecks.py`, `ticketpage.py`), not from the previous version of this file. The prior map (dated 08-28) had gone stale: it marked 1.1, 1.3, 5.2, 5.4 and 8.2 as missing/partial when all five now have working checks, it did not know 2.2's owner mapping had not changed, it missed that `CLASH-UNREADABLE` and `BOM-MEMBERS` had since been claimed as owners of 7.1 and 5.1 in `ticketpage.CHECKLIST_ITEMS`, and it predates item 4.7 entirely. Status below comes from reading the check functions and grepping for whether each finding code is actually emitted — never from what the old file, or `CHECKLIST_ITEMS`'s say-so alone, claimed. **Status vocabulary** (see the task brief this file answers to): `implemented` — runs and compares the thing the item describes. `partial` — implemented inside another path, or only half the item. `missing` — no code. `not needed` — deliberately not built; who decided and why is stated. ## 0 — collect the whole ticket (grade A, prose-only, not numbered) | item | code | status | |---|---|---| | Union of all sheets named the mark | `read_ticket` collects sheets by mark (ticketqc.py) | implemented | ## 1 — identity | # | item | grade | code | status | |---|---|---|---|---| | 1.1 | Sheet name equals the piece mark | A | `SHEET-NAME` | **implemented** — `check_sheet_name` / `ticketpage.sheet_name_matches`. Built since the last map; confirmed the finding is actually emitted (ticketchecks.py:2091-2102). | | 1.2 | Sheet design-number reduces to the piece's design | A | `DSN-MATCH` / `DSN-SUFFIX` / `DSN-PREFIX` | implemented (ticketqc.py:1359-1377) | | 1.3 | Exactly one ticket set per piece | A | `TICKET-SET` | **implemented** — `check_ticket_set` / `ticketpage.ticket_set_status`, plus a second finding when the plant's `TKT_SHEET_COUNT` parameter disagrees with the pages actually found (ticketchecks.py:2105-2162). Built since the last map. | | 1.4 | Every placed view name refers to this design | ~~A~~ **B** | `VIEW-DSN` | **implemented 2026-08-30** — `check_view_name_design` / `ticketpage.view_name_design_status`, with a skip probe, and it now has a `CHECKLIST_ITEMS` row (it had none, so it could not even report NOT RUN). **The contradiction this row used to flag is closed**: the item was never assessed as "not needed" — the agent that met it had no vision capability and skipped it, which was a capability gap, not a ruling (William, 2026-08-30). Re-graded A→B: recognising a design label inside a view name needs the plant's design-token pattern (derived from `nomenclature.design_number_re`, no new plant fact), and the verdict is WARN-only by nature — a stale name on a correct note is a reader's trap, never a defect in the piece, so it is not worth failing a ticket over. Only tokens sharing this design's letter prefix are compared, or `Elevation Top 2` reduces to `TOP2` and every ticket grows a false finding. | | 1.5 | Printed call-outs do not overlap each other | **C** | `CALLOUT-OVERLAP` (+ `CALLOUT-OVERLAP-UNREAD`) | **implemented 2026-08-30** — `check_callout_overlap` + `ticketpage.callout_overlap_status` / `rect_overlap`, with a skip probe. Text-bearing annotations (text notes, tags, keynotes) on the SAME placed view whose bounding boxes intersect with **positive area** → WARN naming both texts and the overlap size in printed inches. Boxes are converted from view-space feet through the view's Scale (`ft * 12 / scale`), because inches on paper is the only unit a reader can act on; a view with no readable scale is set aside, not guessed. **Text-vs-linework is deliberately out of scope** and the docstring says so — a call-out over a drawn edge is the normal condition on every elevation, so flagging it would report the whole sheet. Grade C, not B: a bounding box carries the text's leading and side bearings, so it is a little larger than the ink and an intersection is a shortlist for a person, never a defect claim. **Rotated annotations are set aside**, never flagged — an axis-aligned box circumscribes rotated glyphs and overstates them by up to the string's length — and so are annotations whose rotation cannot be read at all (unknown ≠ zero); both land under `CALLOUT-OVERLAP-UNREAD` at OK severity, which is unowned by any row so it can neither decide the verdict nor vanish. Measured before building (bridge, one WP ticket): 22/22 text-note boxes non-null, **0 overlapping pairs across 7 views** — quiet on a good sheet. | ## 2 — dimensions & mass | # | item | grade | code | status | |---|---|---|---|---| | 2.1 | Printed length/width/depth vs modelled | B | `SIZE-LENGTH` / `SIZE-WIDTH` / `SIZE-MAX-*` / `SIZE-MIN-*` | implemented (ticketqc.py:1550-1553) | | 2.2 | Weight three ways agree, pattern reported | A | `WGT-*` | **implemented 2026-08-30** — `check_weight_three_way` now gathers all three (printed = `sheet.weight`; reconstructed and measured = `liftexport.weight_volumes`, one geometry read per ticket, no erected orientation solved) and `ticketpage.weight_agreement` decides. With three, `weight_three_way` names the odd one out (`WGT-PRINTED` / `WGT-MODEL` / `WGT-READ` / `WGT-MIXED`) — it was dead code before this, which is why the item reported a disagreement without saying which of the three disagreed. With only two readable it emits `WGT-TWO-WAY`, **never OK**, naming the pair, the missing source and why it is missing; with fewer than two the skip probe reports NOT RUN with the same reason. The panel's stored weight parameter is not one of the three (it is written down, not derived) and now has its own code, `WGT-PARAM`, raised only when it contradicts a derived weight — it is what `liftexport.weight_check` reads as `stated`, so a stale one stops a healthy piece at the handling gate. The handling gate itself is unchanged: still an unclassified pass/fail. **2026-09-08**: a wythe with a non-constant thickness (a step or taper `liftexport._openings`/`_reconstructed_ft3` cannot represent in a plan-view projection — WP100) used to overstate the reconstruction and report `WGT-READ`, blaming a fine piece for a hole in the tool. `_reconstructed_ft3` now cross-checks each wythe's gross-less-openings against that same solid's own `Volume`, and when they disagree beyond `handling.weight_tolerance` with no opening to explain it, the reconstruction leg alone stands down as NOT APPLICABLE (named, with the unaccounted ft3) — the measured leg is untouched, since it never depended on the reconstruction. `check_weight_three_way` then compares printed vs measured through the new `ticketpage.weight_agreement_no_reconstruction`, which — unlike the generic two-way path — CAN return `WGT-OK`, because with the tool's own limitation named, two-way agreement really is the whole answer; disagreement in that state is `WGT-TWO-WAY-NA`, not `WGT-READ`. | | 2.3 | Unit weight on ticket vs design summary | A | `PCF-NOTE` / `PCF-MODEL` | implemented (ticketqc.py:1315-1325) | | 2.4 | Cast extras counted in weight | A | `CAST-EXTRA` | **implemented 2026-08-30** — `check_cast_extras` + `ticketpage.cast_extra_agreement`. Not a weight comparison: a SET comparison. The counted side is `liftexport.extra_cast_members` — split out of `extra_cast_ft3` so 2.4 reads exactly the members whose Volume fed 2.2's `reconstructed_lb`, and cannot drift from it. The drawn side is the ticket's words: a BOM row or a placed view's text naming a `families.cast_include` token. Three things it catches that 2.2 cannot: an extra the ticket names that the model does not carry (missing from all three weights, which then agree), an extra in the weight that the ticket never names, and a cast member whose Volume could not be read (drawn, and counted as nothing). BOM states respected: OK/PARTIAL judged, ABSENT judged from view text and says so, UNREADABLE/UNKNOWN stand the comparison down through the skip probe. | ## 3 — concrete | # | item | grade | code | status | |---|---|---|---|---| | 3.1 | f'c ticket vs summary | A | `FC` | implemented | | 3.2 | f'ci ticket vs summary | A | `FCI` | implemented | | 3.3 | f'ci/f'c release ratio in band | B | `FCI-RATIO` | **implemented, but distinguish from "not built."** `check_release_ratio` + its skip probe (`_probe_release_ratio`) exist and run; on a model whose design spec carries no f'c/f'ci, the probe reports it as **NOT RUN** with the reason stated, not silence and not a finding. That is an input gap on this model, not missing code — matches the task brief's own framing. | ## 4 — strand | # | item | grade | code | status | |---|---|---|---|---| | 4.1 | Printed strand count vs modelled | B | `STR-QTY` | implemented | | 4.2 | Diameter & grade vs design | A | `STR-DIA` / `STR-DIA-EXTRA` | implemented | | 4.3 | Jacking force on ticket vs design | A | `JACK-FORCE` | implemented (`check_jacking_force`) | | 4.4 | Strand positions vs design chain from stated rail | B | `STRAND-POS` | implemented (`check_strand_layout`) — worth flagging: the code does not read a plant-stated "which rail is fixed" setting. `_fit_chain` tries the pattern read from **both** edges ("as modelled" / "mirrored") and keeps whichever fits the design chain better. That may be the right answer to a different, real ambiguity (a piece modelled in either orientation) but it is not what the checklist item describes (a fixed rail declared as plant knowledge) — worth the engineer's eyes, not re-graded here since it still produces a correct-looking comparison. | | 4.5 | Debonded strands called out where design debonds | C | `STRAND-CALLOUT` | **implemented 2026-08-30** — `check_debond_callout` + `ticketpage.debond_callout_status`. Owner's ruling: where the debond condition is met, expect a call-out; none → WARN. It CONSUMES 4.7 rather than re-deriving geometry — both items now read one `_debond_requirement(t, know)`, cached, so they can never disagree about whether a run is short. When 4.7 could not judge, 4.5 reports NOT RUN repeating 4.7's own reason. The ticket side is `strand.debond_callout_tokens` (plant nomenclature, empty default → NOT RUN with that reason) matched against BOM rows and placed-view text. Stays grade C: the call-out is a bare note with no station and no strand number, so the OK says in words that it was not verified to cover these runs. | | 4.6 | Row count & spacing plausible | A | `STRAND-SPACING` / `STRAND-ROWS` / `STRAND-GRID` / `BED-WIDTH` / `BED-OFFSET` | implemented | | 4.7 | Strand debonded wherever concrete along it runs short | B | `STRAND-DEBOND` | **implemented — landed mid-run.** It was missing on my first pass; a re-check while writing this file found `check_strand_debond` / `ticketpage.strand_debond_runs` fully wired: registered in `CHECKS`, has a skip probe (`_probe_strand_debond`) that reports NOT RUN — not silence — when the plant threshold, strand positions, length, or opening geometry aren't available, and `CHECKLIST_ITEMS` already carries a `4.7` row owning `STRAND-DEBOND`. This is the race the task brief warned about: another agent was implementing this concurrently in the same two files. Confirmed present as of this read; re-run the grep below before trusting it further out. | ## 5 — reinforcing & embeds | # | item | grade | code | status | |---|---|---|---|---| | 5.1 | BED PLAN callout embed exists in model | A | `BOM-MEMBERS` | **partial, unchanged** — `CHECKLIST_ITEMS` now names `BOM-MEMBERS` as this item's owner, but `check_bom_members` compares **BOM schedule rows** (quantity + mark) against assembly member counts, not the "Elevation Top" BED PLAN callout text the checklist item actually describes (that text is only read elsewhere, by `check_bed_plan_reverse`, for the reverse direction). So the mapping in `CHECKLIST_ITEMS` is a real code path, but not quite the comparison 5.1 asks for — see disagreement #2. | | 5.2 | Embed in model appears on ticket | A | `BED-REVERSE` | **implemented** — `check_bed_plan_reverse` reads the "Elevation Top" view's flat text and reports model families not called out on it, excluding families declared `drawings.annotation_covered_by_parent`. Built since the last map. Note it is deliberately WARN-only and reports the difference, "not a verdict" per its own docstring — a defect list, no severity judgement baked in. | | 5.3 | Face designations consistent with model side | C | — | missing — no owner code, no narrowing logic. `CHECKLIST_ITEMS` gives it a reason string but `check_name` is `None`. | | 5.4 | Mesh / WWF designation vs design | A | `WWF` | **implemented** — `check_wwf_designation` + `ticketpage.wwf_designation_status`, with a skip probe that reports **NOT RUN** (not a silent pass) when the design's spec carries no WWF mesh token. Built since the last map. | | 5.5 | Bar marks **legible** on the drawing | D | — | **SPLIT 2026-08-30, on the owner's approval, and the row narrowed.** It used to read "legible *and unique*" and carried both. Uniqueness is a comparison between the model's marks and the sheet's printed text, so it moved to **5.6** and is computed. Legibility is a property of the *picture*, which no query answers, so it stays here: grade D, `CHECKLIST_FOR_ENGINEER = ('5.5',)`, `FOR ENGINEER` on every report so the item can never silently vanish. **The vision path is unchanged** — `ticketvision.ESCALATIONS` still carries `AI-BARMARK` against item 5.5, and its question text still names uniqueness alongside legibility. That overlap is deliberate: a model reading the rendered sheet is a second, independent look, never the computed answer, and section 8bis forbids it from claiming a verdict either way. | | 5.6 | Bar marks unique, and model and print name the same bars | **B** | `BARMARK-UNIQUE` (+ `BARMARK-UNREAD`) | **implemented 2026-08-30** — `check_bar_mark_uniqueness` + `ticketpage.bar_mark_uniqueness_status` / `mark_tokens`, with a skip probe. The computable half of the old 5.5. Three WARNs for three different defects: **(a)** two bars sharing a mark but differing in family or length — one mark naming two parts, the expensive one, because the mark is what a fabricator cuts to; **(b)** a printed mark no bar carries; **(c)** a modelled bar whose mark is never printed. A bar with no mark at all is **none of the three** — it is could-not-judge under `BARMARK-UNREAD`, and is never counted as (c), because an absent mark cannot be looked for in the printed text. **Two design decisions came from a live read and overrode the spec's assumptions.** First, the spec named tags' `TagText`; the bridge found **zero** `IndependentTag` and **zero** keynote elements on the specimen ticket — every printed bar call-out is a plain **TextNote on a Detail view**. Both paths are gathered anyway (a tag-carrying plant must not fall through to silence) but nothing depends on the tag path, and the tag-absence conclusion is one specimen on one plant and is flagged as such. Second, the printed string **contains** the mark and never equals it (`SR324 (F.F.)`, `SR310 (E.L.)`, `P76 W/ SR5602 (S.I.F.)`), so the match is whole-token containment — string equality would have called every call-out on every ticket an orphan. **Note-view pollution is real and separable by view TYPE**: Legends carry headings and the jacking-force paragraph, Detail views carry the call-outs, so Legends are excluded — a type test, needing no plant nomenclature. Marks are read instance-first then **TYPE** (`_bar_mark`), because one bar family here carries its mark only on the type; `ticketqc._member_mark` is deliberately *not* widened, since that would change what four other checks call a mark. | | 5.7 | No bar longer than the stock it is cut from | **B** | `BAR-LENGTH` (+ `BAR-LENGTH-UNREAD`) | **implemented 2026-08-30** — `check_bar_stock_length` + `ticketpage.bar_stock_length_status`, with a skip probe. Owner's ruling: bars come in 40 ft stock and a piece must be shorter unless other knowledge overrides. The number is **published data**, so it lives in `knowledge/catalogue.yaml` as `catalogue.rebar.stock_length_ft`; **null → NOT RUN naming the key, never an assumed 40**. Length comes from the `member.length` role — the plant layer names the parameter, this repository never does — and from **nothing else** — no geometry, no bounding box, ever: a bent bar consumes its *developed* length off the stick and a box only sees the folded envelope, so it understates every bent bar and would pass exactly the pieces this catches. Verified live: the parameter is on the **instance** and its internal unit is decimal feet. **Three states, not two** — over stock (WARN), under (pass), and `0.0`, which 260 elements hold and which is neither a length nor an absence, so it is reported as could-not-judge alongside the bars with no parameter at all. **Grouped by MARK**, because the model carries **2,122** elements at or over 40 ft (max 60.00) and a per-instance report would bury every other row. **The finding ships with its ruling open**: a continuous or spliced run legitimately exceeds stock (the catalogue records `x CONT. (SEE PLAN) LAP SPLICE`) and whether these 60-footers are those is with the owner. Nothing is pre-exempted; when he rules, the fix is a list in `catalogue.rebar.stock_exempt_marks` — a config edit, not a code change — matched **exactly**, never as a substring (`SR1` must not swallow `SR1101`), and exemptions are **listed in the report** rather than silently dropped. Exempt by **mark, not family**, deliberately unlike `_is_reinforcing`: one family here carries both 11 ft and 60 ft bars, so a family exemption would switch the check off. **Dormant at this plant today, correctly** — which members are bars is `_is_reinforcing` and nothing else, this plant models bars as *Specialty Equipment* so Revit's category says nothing, and `drawings.reinforcing_families` is empty, so 5.6 and 5.7 both report NOT RUN **naming that key** until it is filled in. | ## 6 — handling & lifting | # | item | grade | code | status | |---|---|---|---|---| | 6.1 | Insert capacity vs load | A | `LIFT-CAPACITY` | implemented | | 6.2 | Insert edge distances | A | `LIFT-EDGE` | implemented | | 6.3 | Current handling result exists & passes | A | `HANDLE-*` | implemented — bundles a large family of `HANDLE-` codes (reuse of a reviewed result, a sweep result, or an on-the-spot run; see the extensive module notes in ticketchecks.py above `check_handling_result`) | | 6.4 | Dunnage not under an opening | A | `DUN-OPENING` | implemented — every dunnage finding is emitted as WARN by deliberate ruling (William, 08-28), never FAIL, so `opening_clearance_fail_in` is inert as a severity knob (it only shapes when something is reported at all). **Closed 2026-08-30 — it could not say why it said nothing.** Six silent `return []` paths and **no skip probe**, so `run_all` logged `RAN-EMPTY` and the row read NOT RUN, reason *"no probe declared"* — a sentence about the tool, not the piece. Every site now sets `t['_dunnage_silence']` and `_probe_dunnage_openings` reads it (the `_bed_reverse_skip` pattern; the fallback is a reason and **never None**, since a None return would turn all six silences into a quiet OK). **Exactly one path is a pass**: the fall-through after the clearance loop, where the geometry was solved and every station cleared — it now emits an explicit OK (*"every one of the N dunnage station(s) is at least 6" clear"*), matching 4.7's flip. **"No size notes found" and "notes found but no parseable feet" are NOT passes** and keep reporting NOT RUN — they are failures to read the ticket, and an unread dunnage row is not a clear one. **One open judgement call, named not decided**: when `opening_counts_only_if_through_thickness` is set and every opening is a blind recess, the plant's own rule arguably answers the question and OK could be defended; it reports NOT RUN instead, because that under-claims (the safe direction) and because the sibling path — an empty opening list — genuinely cannot be judged, `liftexport._openings` returning `[]` both for a piece with no opening and for a solid it failed to read. 4.7 can tell those apart via `_strand_debond_openings`; this check cannot yet. Worth the owner's ruling. | | 6.5 | DUNNAGE / ERECTION / PICKING notes say something | ~~A~~ **B** | `NOTE-CONTENT` | **implemented 2026-08-30** — `check_note_views` / `ticketpage.note_view_content_status`, with a skip probe. **Contradiction closed** on the same ruling as 1.4: no census ever found nothing; the agent had no vision and skipped it. The grading question — *can the API read these notes at all, or only a picture of the sheet?* — was answered rather than assumed: a note view's contents are ordinary view-owned elements, so `FilteredElementCollector(doc, view.Id)` sees both its text notes and its graphics, and "holds nothing but its own heading" is an exact count. **No vision path was needed or built.** B not A because *which* views are the note views is plant nomenclature (`notes.instruction_views`, default empty → the row reads NOT RUN with that reason, never a pass). Graphics count as content, deliberately: a plant that draws its instruction would otherwise have every note view reported empty. **Not built:** whether a note that does say something says the *right* thing — that verdict would come from a model reading the rendered sheet, is not unit-testable to a fixed answer, and does not belong on the A/B/C/D ladder at all. Needs its own category first. | | 6.6 | Odd part count carries the BED PLAN note it requires | B | `PARITY-NOTE` | implemented 2026-08-31 — `check_parity_note` / `ticketpage.parity_note_status`. Section 6 because the trigger is a lifting insert's count and the consequence is an erection instruction. Grade B: the count rests on a plant-declared family name and the phrase match proves only that the words are on the plan, not that they are about this part. | | 6.7 | Minimum spacing between lifting anchors | A | `LIFT-SPACING` | implemented 2026-08-31 — `check_lifter_anchor_spacing`. ALP catalog p53, "minimum anchor spacing is 2x the published edge distance" — usually derived (2× the part's own published edge distance, reached through the plant's alias) rather than a printed number, so a part with no published edge distance to double is NOT judged and named rather than falling back to a plant-wide default. | | 6.8 | Printed picking stations match the modelled picking inserts | B | `PICK-STATIONS`, `PICK-UNREADABLE` | implemented 2026-08-31 — `check_picking_match`. Reads the PICKING text-note band on the ticket sheet, recovers stations from each end, and compares them to the modelled picking inserts (`handling.lifter_groups.face.families`) within `handling.picking_tolerance_ft`. Grade B and WARN-only by ruling (Will 08-27): a mismatch is for the engineer to judge, not the tool's verdict. | | 6.9 | Lifting-point count vs panel length (plant rule) | A | `LIFT-POINTS`, `LIFT-POINTS-UNREAD` | **implemented 2026-09-02** — `check_lifting_points`. An MC plant rule, not published product data: owner, verbatim, *"ticketqc在MC加一条check:lifting points: MC的规矩是,panel长度小于20ft则2p lifting,大于20则是4p lifting。实际上25以内都可以2p。你按照20-25给warn,25以上给fail写。"* Nominal rule — under 20 ft, 2-point; 20 ft and over, 4-point — is stricter than the real ceiling, where 2-point actually holds to 25 ft, so the grey zone between the two grades WARN and only past 25 ft does a 2-point piece FAIL. 4 or more points is OK at any length; the check flags only too FEW points for the length, never too many. Both thresholds (`lifters.lifting_points.two_point_max_length_ft` / `.two_point_absolute_max_ft`) are MC plant knowledge, landed in MC's own plant layer; a layer that has not set both — any non-MC plant — gets no judgement from this rule; since 2026-09-08 the row is pulled out of the coloured table entirely, into the "Not applicable at this plant" folded section naming the missing keys (`ticketchecks.PLANT_GATE_KEYS`), rather than printing as an alarming NOT RUN or holding another plant's panels to MC's numbers. A "point" is counted by clustering recognised lifters by station along the panel's long axis (`liftexport.rows_of`, within `handling.group_tolerance_ft`) so that two anchors sharing one pick point are not double-counted — the same grouping Panel Lift Check's own stage logic uses, called rather than re-derived. Zero recognised lifters is 6.1's territory and this row stands down naming it rather than repeating 6.1's own sentence. | | 6.10 | Lifting-point position vs PCI recommendation (plant rule) | A | `LIFT-POS`, `LIFT-POS-UNREAD` | **implemented 2026-09-02** — `check_lift_positions`. DISTINCT from 6.9: 6.9 grades the COUNT of lifting points against panel length, this grades WHERE each already-counted point sits against PCI's recommended station, and never re-flags a count problem — that stays 6.9's job. An MC plant rule, owner verbatim: *"follow PCI的意思是检查一下lifter的位置,如果跟PCI相差大于6" 则报WARN。很多时候没办法完全follow PCI,但是这件事儿让用户判断可行不可行。"* **WARN-ONLY, never FAIL, by that ruling** — a miss from PCI is for the engineer to judge feasible or not, same posture as 6.8's picking-station mismatch. Two-point pieces are compared against PCI's published 0.207L / 0.793L equal-moment two-point pick — a PCI PUBLIC CONSTANT (`ticketchecks.PCI_TWO_POINT_FRACTION`, mirrored by `panellift.pci.OPTIMAL_TWO_LINE_FRACTION`), never plant knowledge. Only the 6" tolerance (`lifters.lifting_points.pci_position_tolerance_in`) is MC's own number, landed beside the 6.9 thresholds; a plant that has not set it gets no judgement from this rule at all. **Any point count other than two stands the row down** (`LIFT-POS-UNREAD`, naming the count) rather than guessing a 4-point (or N-point) PCI location nobody has published to this tool yet — `_pci_recommended_stations` is keyed by point count so a future fill-in is one new branch, never a hardcoded guess. Boundary is strict: exactly 6" off passes, 6.01" warns. Reuses 6.9's own point clustering (`liftexport.rows_of`) so the two checks can never disagree about how many points there are or where one sits. | | 6.11 | Dunnage-point count vs stripping-point count (plant rule) | A | `DUN-STRIP`, `DUN-STRIP-UNREAD` | **implemented 2026-09-02** — `check_dunnage_stripping_match`. A third MC plant rule, owner verbatim: *"默认MC panels dunnage point=stripping points。4p stripping 则4p dunnage。"* — by default a panel's dunnage-point count equals its stripping-point count. And: *"stripping point就是face lifters位置"* / *"LPA开头的就是face lifter"* — a stripping point is a face (back-stripping) lifter position, and the LPA-prefixed anchors are the face lifters. **A DEFAULT, NOT AN ABSOLUTE RULE — WARN-ONLY, never FAIL**, same posture as 6.9/6.10: a mismatch is the engineer's to judge feasible or not. Which lifters are "face" ones is read STRUCTURALLY, off the plant's own `lifters.capacity_lb..usage: back_stripping` tag, reached through the exact resolver `lifter_capacity()` uses (`knowledge._longest_substring_match`, longest key wins) so this row and 6.1 can never disagree about which entry a family resolves to; the bare `LPA` family-name prefix is a FALLBACK, used only when the resolved entry (or the absence of one) carries no usage tag at all — never overriding one that says otherwise. Stripping points (N_strip) are counted exactly like 6.9/6.10 — `liftexport.rows_of` clusters recognised lifters into stations within `handling.group_tolerance_ft`, and a station counts toward N_strip when ANY lifter sharing it is a face/stripping one. Dunnage points (N_dun) are read exactly like 6.4 reads them — via `_dunnage_station_reader`, factored out of `check_dunnage_openings` 2026-09-02 so the two checks can never quietly disagree about where the dunnage sits or why a ticket's row would not read — but unlike 6.4 this needs the count whether or not the piece has an opening, so it never gates on one. Gated on `lifters.lifting_points.dunnage_matches_stripping`, co-located with the 6.9/6.10 keys; a plant that has not set it gets no judgement from this row at all. **Stand-downs**: the MC key absent (since 2026-09-08, pulled into the "Not applicable at this plant" section naming it, not a coloured-table NOT RUN); zero recognised lifters (defers to 6.1 by name, 6.9's pattern); no lifter position readable (`DUN-STRIP-UNREAD`); zero face/stripping lifters identified though other lifters exist (`DUN-STRIP-UNREAD`, named — never a quiet 0-vs-0 pass); dunnage stations unreadable (`DUN-STRIP-UNREAD`, reusing 6.4's own silence reasons). | ## 7 — interference | # | item | grade | code | status | |---|---|---|---|---| | 7.1 | Interference check | B | `CLASH`, `CLASH-UNREADABLE` | implemented — `check_member_interference`. `CLASH-UNREADABLE` is now explicitly claimed as an owner in `CHECKLIST_ITEMS` (it was in the old map's "legacy, unmapped" list — that has changed, see disagreement #3). Findings are WARN-only by deliberate ruling (Will 08-28): measured over 57 archived tickets, 157 findings, almost all reinforcing/strand/mesh crossing an anchor or embed. The engineer looked at the most frequent pair (`SR701 x P106`, 13 tickets / 39 findings) and called it a real problem worth warning about — **do not infer the rest are noise from that fact alone; that inference was made once before and overruled.** | ## 8 — text hygiene | # | item | grade | code | status | |---|---|---|---|---| | 8.1 | No unresolved design-summary lines | C | `SPEC-UNRESOLVED` | implemented (`check_spec_unresolved`) | | 8.2 | No invisible/non-printing characters | A | `INVISIBLE` | **implemented** — `check_invisible` / `ticketpage.invisible_chars`, scanning every sheet parameter string read for the ticket. Built since the last map. | --- ## Disagreements between `ticketpage.CHECKLIST_ITEMS` and `ticket-checklist.md` 1. **Two checklist items still have no row in `CHECKLIST_ITEMS`: 2.4 and 4.5.** (1.4 was on this list; it got its row on 2026-08-30 along with the check behind it.) (4.7 landed mid-run — see its row above and the race notice — and now has a `CHECKLIST_ITEMS` row.) For items with no code this means the per-ticket report has no `NOT RUN` row either — it cannot say "we didn't check this," it can only say nothing, which is exactly the failure mode `ticket-checklist.md`'s closing line warns against ("A system that cannot say what it did not do cannot be measured"). Nowhere is it explained why 2.4/4.5 are absent from the table. 2. **5.1's owner code doesn't implement 5.1 as written.** `CHECKLIST_ITEMS` names `BOM-MEMBERS` as 5.1's owner; the actual comparison it runs is BOM schedule rows vs. assembly member counts, not "the BED PLAN callouts exist in the model" (which reads a different view's text entirely, and is already coded — for the reverse direction — inside `check_bed_plan_reverse`). Not a new problem, but worth restating since the ownership claim invites reading it as more complete than it is. 3. **The previous map's "legacy codes with no checklist row" list is stale on two entries.** `BOM-MEMBERS` and `CLASH-UNREADABLE` were both listed there as unmapped; `CHECKLIST_ITEMS` now explicitly claims them as owners of 5.1 and 7.1 respectively. Removed from the unmapped list below. 4. ~~**The 1.4/6.5 "not needed" framing does not check out.**~~ **CLOSED 2026-08-30 by the owner.** There was no "not needed" decision to find, which is why no one could find it: the agent that met 1.4 and 6.5 had no vision capability and skipped both on that basis. A capability gap, not a ruling. Both were wanted, both were re-graded (A→B, reasons on their rows above) and both are now built. Worth keeping as a record of the failure shape: **a skip with no reason attached became, one map later, a decision nobody made** — the same silence-reads-as-a-verdict problem the NOT RUN row exists to prevent, one level up in the documentation. ## The three gap directions **1. Checklist items with no code (missing):** 4.5, 5.3. (4.7 was in this list at the start of this read; it landed mid-run — see the race notice on its row above — and is now implemented. 1.4 and 6.5 were built 2026-08-30.) **2. Checklist items covered only inside another path (partial):** - ~~2.2~~ **CLOSED 2026-08-30** — the ticket report now runs the three-way itself and names the odd one out; see its row above. `liftexport.weight_check` is still an unclassified pass/fail gate on the handling path, which is a different thing and deliberately left alone. - 2.4 — cast extras are folded into `weight_check`'s reconstructed number, no standalone comparison. - 5.1 — implemented as a BOM-table-vs-members check, not the BED-PLAN-text comparison the item describes. **3. Finding codes that answer no checklist item**, verified against the current `CHECKLIST_ITEMS` owners (this list has shrunk since the last map — `BOM-MEMBERS` and `CLASH-UNREADABLE` are no longer in it, see disagreement #3): - `PICK-STATIONS` — `check_picking_match` compares the ticket's PICKING diagram stations against modelled picking inserts. The checklist has no picking item (§6 covers dunnage only). Confirmed still unmapped. - `BOM-STRAND` — `check_bom_strand_count`'s BOM-linear-feet / section-print / model three-way agreement on strand count. Conceptually sits between 4.1 and 4.3 but is not named by either. Confirmed still unmapped. - `BAR-UNPAIRED` — a side-output of the interference check: bars with no counterpart in another layer and no insert nearby, i.e. unexplained by the model's own symmetry logic. Confirmed still unmapped. - `LGD-MISSING` (`ticketqc._check_legend_identity`) — **re-read 2026-08-30.** Two separate things were true of it and only one has been fixed. **The original false positive is gone.** It used to look for a placed view named `STRAND PATTERN`/`REINFORCING PATTERN` and FAIL when there was none, which fired on all 204 WP sheets because this plant draws no such view. Phase 2 repointed it: with `legend.pattern_prefixes` empty (the shipped default, and this plant's answer) it no longer asks about a legend view at all — it asks whether the ticket carries a **BOM schedule**, which is what this plant's tickets use instead. So it now reports the presence of the thing that actually exists. **The residual false positive is a "could not read" reported as a defect.** The `FAIL` branch — `'no pattern legend configured and no BOM table on the ticket'` — reads `t['bom_schedules']`, and `_read_bom_schedules` swallows every per-schedule failure with a bare `except Exception: continue` (unnamed schedule, `GetTableData` throwing, zero-row body). A ticket whose BOM could not be read and a ticket with no BOM produce the *identical* empty dict, and the check calls both a FAIL. That is the same failure this toolchain keeps meeting, inverted: not a silent pass, but an unreadable input asserted as a finding. Fixing it means giving `_read_bom_schedules` a way to say "I saw N schedules and could not read them" and splitting the FAIL into `LGD-MISSING` (genuinely absent) and an `LGD-UNREADABLE` WARN. That is Revit-side gathering and not host-testable, so it is written down here rather than changed blind. **Ownership is still open, and is an owner decision.** No checklist item claims `LGD-*`. §4 and §5 name the strand and reinforcing *values*; nothing in the checklist asks "does this ticket carry a schedule drawing at all." The two candidates: fold it into 4.1/`BOM-STRAND` (the BOM's existence is a precondition for the strand three-way, so its absence belongs there), or retire the code entirely and let the checks that need the BOM report their own NOT RUN when it is missing. The second is cleaner and loses nothing — a BOM-less ticket would then be reported by every check that wanted it, with reasons, instead of by one orphan code nobody's row displays. Until it is decided, `LGD-MISSING`'s verdict — OK **or** FAIL — never reaches the checklist table at all. - Handling-internals bundle (unchanged): `LIFT-WEIGHT`, `LIFT-UNKNOWN` (lift-family variants of 6.1/6.2, not matched by `CHECKLIST_ITEMS`' exact-code owners); `HANDLE-ROW-FLEX-*`, `HANDLE-INSERT`, `HANDLE-ASPECT`, `HANDLE-PLUMB`, `HANDLE-BUDGET`, `HANDLE-STALE`, `HANDLE-SWEPT-REFUSED`, `HANDLE-NOT-CHECKED`, `HANDLE-STAGE-CONFIG`, `HANDLE-NO-STRENGTH`, `HANDLE-NO-PYTHON`, `HANDLE-CALC-ERROR`, `HANDLE-UNAVAILABLE`, `HANDLE-WEIGHT-GATE`, `HANDLE-SWEEP-UNGATED` (all bundle into 6.3 via the `HANDLE-` prefix match in `checklist_states`, so they are not individually unmapped, just internal detail). - `DUN-UNREADABLE`, `PICK-UNREADABLE` — unavailability variants, same shape as `CLASH-UNREADABLE` was before it got claimed; these two are not claimed by any owner list and remain unmapped. - `WGT-*` variants beyond `WGT-OK`/`WGT-PRINTED` (`WGT-MODEL`, `WGT-MIXED`) — not seen live yet, so "unmapped" is moot for them so far. **Correction, 2026-09-08: `WGT-READ` is not dead code** — it fired live on WP100 (`_scratch/wp100_weight.md`), and that was the false positive the same day's fix addresses (see 2.2 above); the claim below was wrong the day it was written, not merely stale. - `CHECK-ERROR` — wrapper for any check exception, not a checklist item. ## Count by grade Counting all 31 numbered items (1.1–8.2, including 4.7; excluding the prose-only item "0"): | grade | items | implemented | partial | missing | not needed | |---|---|---|---|---|---| | A | 21 | 16 | 3 (2.2, 2.4, 5.1) | 2 (1.4, 6.5 — contested, see above) | 0 | | B | 6 | 6 (incl. 4.7, landed mid-run — see race notice) | 0 | 0 | 0 | | C | 3 | 1 (8.1) | 0 | 2 (4.5, 5.3) | 0 | | D | 1 | 0 | 0 | 0 | 1 (5.5) | | **total** | **31** | **23** | **3** | **4** | **1** | Of the 23 "implemented," two are input-dependent rather than universally green on every model — worth keeping separate from "not built": - **3.3** (f'ci/f'c release ratio) reports **NOT RUN** on a design spec that carries no f'c/f'ci — the code runs, the model just doesn't have the input. - **5.4** (mesh/WWF) reports **NOT RUN** when the design's spec has no WWF mesh token — same shape: code exists, this model's spec doesn't feed it. Both would need Extract Design re-run (or the design summary populated) to move from NOT RUN to an actual verdict on this model — I did not run that; the code alone tells you the check exists and why it currently has nothing to say. ## What I could not determine from the code alone - Whether items 1.4 and 6.5 really were the subject of a "not needed" decision, and by whom. I found no such record outside the previous version of this file, and the checklist doc's own text and tally argue the opposite for 6.5. This needs the person who made that call (if it was made) to confirm it, or `ticket-checklist.md` to be corrected to match reality — right now the two documents disagree with each other and I have no way to arbitrate that from source alone. - Whether 4.7's implementation (which landed mid-run while I was writing this file — see the race notice on its row) is correct against real strand and opening data. I read the code and it is coherent and matches the checklist's description (measures continuous-concrete runs between through-thickness openings per strand, using `strand.debond_below_continuous_ft` from plant knowledge, never defaulted), but I have not run it against a model, per the task's constraints. - Whether `check_strand_layout`'s "try both edges, keep the better fit" behavior (4.4) is an intentional handling of a real modelling ambiguity or a gap against the checklist's "which rail is fixed is plant knowledge, never inferred" requirement. This needs the engineer's judgment on intent, not something a static read can settle. - Any of this against an actual open model or ticket set — I did not run Ticket QC, touch Revit, or open the bridge, per the task's constraints.