# What this audit learned, sorted by where it belongs Three scales. The test for which scale a lesson belongs to is: **who would have to change for this to stop being true?** - **Plant** — a different plant would answer differently. Goes in the knowledge file, never in code. Nothing here should be defaulted, because a wrong default reads as a missing value and the check then quietly does not run. - **Tool** — the code has the wrong model of the world. Goes in the repo. - **Method** — how to go about looking at all. Goes in the skill. --- ## Plant — entries for the knowledge file Each is stated with the evidence, because a plant setting nobody can trace back is one nobody dares change. ### `legend.printed_strand_count_is_per_piece` — already false, and correctly so The design summary for WP1 says `(14) 1/2" 270K @ 28,920`. The pieces print their own counts: WP210's section A-A prints `(10)`, WP200's `(6)`, WP199's `(14)`. The 14 is the design family's figure. I first wrote this up as a setting to change. **It is already `false`, set at the job layer** — checked against the merged config. So the five `STR-QTY` entries are not a misconfiguration: with the flag false the check reports them as WARN rather than FAIL, which is the intended soft signal that a piece carries fewer strands than its design family's maximum. Reading a WARN as a defect was my error, not the tool's. ### `legend.pattern_prefixes` — the concept does not apply here No sheet in this model carries a view named `STRAND PATTERN…` or `REINFORCING PATTERN…`. 0 of 204. The reinforcing schedule is the **bill of materials table on sheet 1**, and the per-piece strand count is inside section A-A. Until the check is repointed, `LGD-MISSING` fires on every WP ticket in the job and takes `FC`, `FCI`, `PCF-NOTE` and `STR-NOTE` down with it. ### The BOM's strand row is linear feet `321 | 0010001 | M392 - 1/2" 270ksi Low Lax Strand` on a 22'-11" piece is a length. Verified on two pieces: | piece | BOM | panel | BOM / length | section prints | | --- | --- | --- | --- | --- | | WP199 | 321 | 22.9167 ft | 14.007 | (14) | | WP210 | 249 | 24.9167 ft | 9.993 | (10) | A checker that reads that row as a quantity reports a 23x error on a correct ticket. This needs to be stated somewhere the tool can read it, because nothing in the row's text says "feet". ### `interference` — P136 is exempt from everything, and a plant group went missing **Decided and applied.** `exempt_families: [P136]` is now in the plant knowledge file. The reason, in Will's words: the disk anchor is placed to a nominal position and the crew rotates it on the form to suit, so the modelled orientation is a starting point rather than the built one. Anything it overlaps in the model is a consequence of that freedom, not a defect. This is separate from `insert_bars`, which exempts the bars *belonging* to a P136 — that entry already existed and is correct, and it works by symmetry rather than distance. **But the edit does not take effect yet, and the reason matters more than the edit.** The generated project config is the last layer and it writes out *every* key at its merged-at-generation-time value, including `exempt_families: []`. So it freezes the past and overrides any later change made upstream. A plant edit is invisible until the project config is regenerated. Regenerating is Will's to do, from Revit. The same mechanism has already lost something: the plant file defines component_groups: - name: EMBED P76 families: [SAMPLE_CLIENT_EMBED_P76, SR5602, SR6999] interferes: within_group and the merged config carries only the job layer's `CONNX TOP OF PANEL TO COLUMN` group. Lists replace rather than merge, so the job layer's single entry deleted the plant's. The generated config even prints the substitution in its own header comments — the tool knows, and says so where nobody reads it. **Worth deciding separately:** whether `component_groups` should merge by `name` instead of replacing wholesale. As it stands, any layer that wants to add one group silently discards every group defined above it. ### `interference.tolerance_in` / `min_thickness_in` Currently 0.125" with `min_thickness_in` unset, so thickness defaults to the same. The recurring overlap is 0.21–0.23". Whether that is acceptance or defect is a plant answer, and it decides nine findings. --- ## Tool — the code has the wrong model ### A ticket is more than one sheet 97 of the tickets in this job are `1 OF 2` + `2 OF 2` under the same sheet name; 9 are `1 OF 1`. `collect_context` resolves a mark to a single sheet and returns whichever wins. Every ticket read this way is half a ticket, and everything on the other sheet is reported as missing. On twelve pieces this produced sixty findings from one cause; across the job it would produce roughly a thousand. **Fix:** collect every sheet whose name equals the mark and treat the union as the ticket. ### `ValueString` is not what prints The handling dimensions carry `ValueOverride = U+200E` and still print their value. Any check that decides what the ticket says by reading `ValueString`, or by reading `ValueOverride` and finding it blank, will be wrong in one direction or the other. What the override is for is not established. ### `read_ticket` field problems - `thickness` is returned in **feet** (0.667) under a name that reads as inches. - ~~`lifters` come back with `x` and `y` as `None`~~ — **retracted, this was my error.** Each lifter carries `local`, an `(X, Y, Z)` tuple in the panel's own frame; there are no `x`/`y` keys, and my probe asked for the ones that do not exist. `local[0]` is measured from the panel **centre**, so `station = length / 2 + local[0]`. Verified: WP200's four P136 sit at `local[0] = ±7.208` on a 22.917 ft piece, giving 4.25 ft from the end, and the ticket's PICKING/STRIPPING diagram prints `4'-3"`. They agree exactly. The comparison that finds real defects was available the whole time. - What is genuinely missing is a **named accessor**: `local[0]` plus an undocumented centre-origin convention is how this went wrong. A `station_ft(lifter, panel)` helper would have made the mistake impossible. ### `tools/rvt.py` crashes on hidden characters Printing a result containing `U+200E` raises `UnicodeEncodeError` on a cp1252 console. Force UTF-8 on stdout in the tool rather than making every caller set `PYTHONIOENCODING`. ### The scrub cannot read a PDF Exported tickets carry the job name and the client's name in the title block, and `scrub.py` scans text. `sample data/` and `*.pdf` are now gitignored, which is the containment, but the gate itself is blind to them. --- ## Method — how to look These are in `docs/ticket-check.skill.md` in full. In short: 1. **Render the page and look at it.** Extracting a drawing's text stream drops dimensions without saying so, and produces confident wrong findings. 2. **Both values and both sources, or it is not a finding.** 3. **Separate "they disagree" from "I could not find it"** — and do not pad the second list. Three items reported as uncheckable were printed on sheet 1. 4. **One root cause is one finding**, however many codes it fans into. 5. **A repeated family pair is one pattern**, not one defect per piece. 6. **Arithmetic before alarm.** 321 against 14 looks like a 23x error until you divide by the panel length.