25592fa6c2
ci / test (push) Successful in 6m14s
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.
710 lines
47 KiB
Markdown
710 lines
47 KiB
Markdown
# WillTools
|
||
|
||
This is the document to read before opening a file here. It assumes you know precast and
|
||
Python and nothing about this codebase. Four parts, in the order they matter: **what the
|
||
tool is**, **how it is built**, **the discipline**, **where everything stands**.
|
||
|
||
Read part 3 properly. Parts 1 and 2 orient you; part 3 is what stops you doing damage.
|
||
Every rule in it was bought with a failure and carries the incident that bought it,
|
||
because a rule whose reason has been lost gets "simplified" away by the next reader.
|
||
|
||
One standing convention, and it is the project's premise: **this repository does not state
|
||
things it has not checked.** Anything unverified is marked unverified; anything measured
|
||
says when. Where a number rots — line counts, test counts, timings — you get the command
|
||
instead, because those have each been wrong more than once in a single week. Where a
|
||
document and the code disagree, **the code wins**; §4.6 lists the disagreements known on
|
||
2026-08-30.
|
||
|
||
---
|
||
|
||
## 1. What it is
|
||
|
||
### 1.1 The product
|
||
|
||
WillTools is one engineer's QC instrument, not a plant production system. The owner is
|
||
an **independent precast specialty consultant** — not the plant's employee — and on any
|
||
job his responsibility is **cladding and wall panels only**. Other engineers hold the
|
||
beams, slabs and columns, and the plant knowledge every check runs on was validated
|
||
against panels and nothing else.
|
||
|
||
`lib/vibe/scope.py` exists because of that, and insists that **out of scope is a third
|
||
state** — not a pass, not a failure, not an absence. Every report names how much work it
|
||
left alone and of what kind. The scope is *declared* in the job knowledge layer
|
||
(`scope.mark_prefixes`), never inferred. The project already paid for the alternative:
|
||
one unauthorised exclusion hid forty tickets and every check that ran came back clean.
|
||
|
||
That is the first face of the rule the whole tree is organised around — **silence is
|
||
never a pass**. Its full statement is §3.1, and it is the single most important idea
|
||
here.
|
||
|
||
### 1.2 The ribbon
|
||
|
||
One tab, four panels: **Tools**, **Setup**, **Design**, **Lab**. Folder names *are* the
|
||
UI, and `layout:` in a container's `bundle.yaml` decides what gets built at all — a child
|
||
not listed is absent, not misordered. No button carries hover text (no `tooltip:`, no
|
||
`author:`, no module docstring); `tests/test_button_tooltips.py` fails by name if one
|
||
returns.
|
||
|
||
**Reinforcing Suggestion** asks which already-reinforced panels, *between them*, account
|
||
for every feature of this one — a set-cover jigsaw, not a similarity ranking — capped at
|
||
four donors, with the shortfall named rather than relaxed to a fifth. Search in
|
||
`reinforcematch`, index in `reinforceindex`/`reinforcecore`, join in `reinforcebind`,
|
||
wording in `reinforcereport`. **It opens no Transaction, by hard rule**: a wall panel's
|
||
middle vertical band is per-design engineering, and copying a donor's cage in
|
||
automatically would be the most dangerous thing this repo could do. An ordinary press
|
||
rebuilds only pieces whose cheap signature moved; shift-click forces a full rebuild,
|
||
because the signature cannot see an opening moved or resized in place.
|
||
|
||
**Panel Lift Check** is a PCI Chapter 8 handling check over stripping, transportation and
|
||
erection. The piece is measured from the model, then **you click the inserts carrying each
|
||
stage, on the piece itself** — the rules supply highlighted defaults and no longer get the
|
||
last word. It refuses rather than guesses: fewer than two support lines, a stage naming an
|
||
insert the panel has not got, a stage with no multiplier and no `pci_case`, short/long
|
||
above 0.75 (two-way spanning, which a beam idealisation cannot represent). Multi-select
|
||
runs a background batch whose worker lives in `vibe/batchrun.py` and never in the button —
|
||
a thread reading the command module's scope after pyRevit tears it down raises on a
|
||
non-main thread and kills Revit. Batches skip the position search — 93% of a run, 12.6 s
|
||
of a 13.6 s median piece on the reference sweep.
|
||
|
||
**Ticket QC** is the flagship. It checks every shop ticket of a design against the
|
||
extracted spec plus the knowledge layers: concrete strengths, jacking force, strand
|
||
count/grade/diameter/position, W.W.F., unit weight, embeds in both directions, bar marks,
|
||
note views, size envelope, interference. **27 check functions** in `ticketchecks.CHECKS`
|
||
feed **34 numbered checklist rows** in `ticketpage.CHECKLIST_ITEMS` — both counted
|
||
2026-08-30, both countable with `len()`. Every check is arithmetic; the one exception is
|
||
quarantined (§3.1). A row's status comes from `run_all`'s disposition log, never from an
|
||
absence of findings. And a ticket is not one sheet: it is the union of every sheet named
|
||
for the mark.
|
||
|
||
**Snapshot & Close** is the daily close-out. The model is 300+ MB on disk and 10+ GB open,
|
||
so there is never a second copy in memory: phase A indexes the live document, phase B
|
||
closes Revit (the press *is* the authorisation), phase C optionally reopens it detached in
|
||
a deferred host process once the memory is free. The request is journalled before the close
|
||
and the deferred process journals its outcome, so a day it silently did not run is reported
|
||
in capitals on the next press.
|
||
|
||
Behind the **Config** pulldown on the Setup panel: *Project Config*, *Design Extract*
|
||
(scrapes a design packet into a reviewable `spec.json`, with whatever the parser could not
|
||
place landing verbatim in `unresolved` rather than dropped), *Self Check*, *Job Recon*
|
||
(reconciles the issued ticket set against the whole job — the question no per-ticket check
|
||
can answer, because a ticket that is not there cannot be checked) and *Takeoff*.
|
||
|
||
The **Design** panel is a quarantine: everything on it sits behind the `DesignHub`
|
||
pulldown, on a panel of its own, so nothing half-built ever sits beside a finished tool.
|
||
On the **Lab** panel, **GOD_MOD** opens a terminal running `claude` at the repo root with
|
||
default permissions.
|
||
|
||
### 1.3 What a run produces
|
||
|
||
**Ticket QC.** The checklist table *is* the page — first, unfolded, one per ticket that
|
||
has anything to say, in loudness order. `ticketpage.state_color`: OK green; WARN and FOR
|
||
ENGINEER yellow; **NOT RUN and FAIL both red**. An all-green ticket gets one line, not a
|
||
table.
|
||
|
||
Everything else is a `foldreport.Section`, and the folding is a contract: *the answer is
|
||
the headline and is never folded; everything else starts closed; a section carrying a
|
||
problem opens itself, and its `problem` is a **sentence** naming what is wrong, not a
|
||
boolean; folded is not gone — a closed header still says what is inside and how much.*
|
||
The reader's side of that contract is **if nothing below the tables is open, nothing needs
|
||
me.** A problem section renders open as plain HTML needing no script, because pyRevit's
|
||
output panel is a WinForms `WebBrowser` in IE7 document mode where `<details>` does not
|
||
exist and fails *silently*.
|
||
|
||
Three files land in `_ticketqc/` on every press: `last-ticketqc.txt` (the whole report,
|
||
nothing folded, long notes wrapped rather than cut, so a dead fold costs a file open and
|
||
never a sentence); `qc-run.json` (`findings` and `model_read` as two keys, never one list,
|
||
each entry carrying `source` and a boolean `computed`; muting never touches it, because a
|
||
muted row is a rendering decision and a muted check that failed still fails the ticket —
|
||
but it does not yet carry the run log, §4.4); and the `ticketqc/2` evidence file of §2.2
|
||
(`qc-run.json` itself carries `ticketvision.RUN_SCHEMA = 'qc-run/3'` — read the constants,
|
||
both have moved since this paragraph was written).
|
||
|
||
**Panel Lift Check** produces a self-contained HTML file with the `.job.json` beside it as
|
||
the complete input, re-runnable where there is no Revit: a stage table, an "Inserts picked"
|
||
table recording the one part of the run that came from a human, recommended moves in dx/dy
|
||
inches, and inline SVG free-body, shear, moment and stress plots that load **nothing
|
||
external**. Two habits in it are worth copying. A stage that was not analysed prints
|
||
*words*, never a dash, because a dash among numbers reads as a small number. And the
|
||
station tables under each plot thin ~2000 stations by stride but never thin what matters —
|
||
both ends, every opening edge, every pick or dunnage line, the peak and trough of V and M,
|
||
the worst compression, and the governing tension station, tinted and bold and saying so.
|
||
|
||
Read Δx as "about here", not as a dimension, until §4.4's solver work is done.
|
||
|
||
---
|
||
|
||
## 2. How it is built
|
||
|
||
### 2.1 Two Pythons, and the line between them
|
||
|
||
`WillTools.extension/` is **IronPython 2.7.12** on Revit's UI thread: no f-strings, no
|
||
`pathlib`, no `dataclasses`, no typing syntax. `panellift/` and `tools/` are **host
|
||
CPython** (`panellift` declares `>=3.9`).
|
||
|
||
**`panellift` is deliberately unimportable from Revit** — it uses dataclasses and typing
|
||
syntax on purpose. A calculation whose output reaches a plant must be verifiable, and
|
||
verifiable means a suite that needs no licence, no model and no Windows. That is the
|
||
whole reason the line is where it is.
|
||
|
||
`vibe/liftexport.py` is the entire Revit side of it: it measures the piece, writes the
|
||
job file, shells out with a timeout and tick callback (`find_python`, `run`), and reads
|
||
the summary back. **Mechanics live on the far side of that line; facts about *this
|
||
model* live on the near side.**
|
||
|
||
### 2.2 The contract is the file, not the function
|
||
|
||
Modules hand each other versioned JSON carrying `schema`, `written_at`, `written_by`,
|
||
`doc_key` and `knowledge_sources` — not objects. Four instances today:
|
||
`liftexport.SCHEMA = 'panellift/1'`, `reinforcecore.SCHEMA = 6`,
|
||
`specformat.SCHEMA_VERSION = 1`, and `ticketqc.SCHEMA = 'ticketqc/2'` (bumped from `/1` on
|
||
2026-08-31; read the constant, it will move again), written to
|
||
`<model>/_ticketqc/evidence/run-<stamp>.json` on every Ticket QC press. **Nothing reads
|
||
the last one yet** — that is step 2 of the migration (§4.5), and precisely why the writer
|
||
could ship alone. The four rules, from `docs/architecture-migration.md` §2:
|
||
|
||
1. **Discard whole on mismatch.** `reinforcecore.load()` and `ticketqc.load_evidence()`
|
||
throw a stale file away entirely — never partially, never migrated.
|
||
2. **No live objects.** The plan named two holdouts; the build found four (`t['doc']`,
|
||
`t['legends']`, `t['sheets']`/`t['sheet']`, and live `XYZ` in `t['strand_points']` on
|
||
the no-framing early return) — so the serialiser gates on **type**, not on a list of
|
||
known offenders.
|
||
3. **Absence is not a value.** A gather that could not read something writes an explicit
|
||
`null` *and* a reason, through one `put()`. A merely missing key reads downstream as
|
||
"nothing to check" — the same failure mode `ticketchecks._SKIP_PROBES` exists to
|
||
prevent (§3.1).
|
||
4. **Provenance travels** — which layers were in force, which spec source (`spec` or
|
||
`regex`), which build. `intent` and `spec_source` ride with each ticket. Findings do
|
||
not: `qc-run.json` owns verdicts and the evidence file owns inputs, and step 2 needs
|
||
them apart so it can compare one against the other.
|
||
|
||
### 2.3 The five knowledge layers
|
||
|
||
Each layer overrides the one before it. `knowledge.load(doc)` merges them;
|
||
`knowledge.sources(doc)` lists them, and Self Check prints them.
|
||
|
||
| layer | where it lives |
|
||
| --- | --- |
|
||
| `knowledge.DEFAULTS` | this repo — what the code assumes |
|
||
| `knowledge/catalogue.yaml` | this repo — published hardware ratings, identifies nobody |
|
||
| plant `knowledge.yaml` | `%APPDATA%\WillTools\` or `WILLTOOLS_KNOWLEDGE`, per-plant folder |
|
||
| job `knowledge.yaml` | `<model folder>/_ticketqc/` |
|
||
| the project config file | wherever the engineer put it; `_ticketqc/active-config.txt` points at it |
|
||
|
||
**Maps deep-merge, lists replace.** Two consequences bite: a list in the job file
|
||
*removes* the plant's entries rather than adding to them, and `_deep_update` simply
|
||
assigns, so **an override and a typo are indistinguishable today**.
|
||
|
||
Two refusals guard the edges. A plant named in `job.plant` whose file is missing sets
|
||
`_plant_missing` rather than silently falling back to the machine-wide file; a pointer to
|
||
a vanished config sets `_config_missing`. `knowledge.config_problems()` turns either into
|
||
a sentence the buttons refuse on.
|
||
|
||
Note the deliberate two-pass in `load()`: `job.plant` chooses the plant file but lives in
|
||
a layer that merges *after* it, so `plant_id()` reads the job and config files once,
|
||
alone, first. **Delete that pre-pass and the tool reads the wrong plant while every report
|
||
looks normal.**
|
||
|
||
Which layer a fact belongs in is not a preference; it is an argument, and it is part of
|
||
the discipline — §3.2.
|
||
|
||
### 2.4 The bridge
|
||
|
||
`tools/rvt.py` is a host client for a pyRevit routes endpoint registered by `startup.py`.
|
||
`rvt.py exec` runs a snippet *inside* the live session and returns
|
||
`{ok, stdout, result, error, traceback}`. It is how anything is prototyped against a real
|
||
model, and it is the most dangerous thing in the repo. Its one absolute rule is §3.5.
|
||
|
||
### 2.5 Two repos
|
||
|
||
This repository can be shown to anybody. The other one cannot. `knowledge/plant/`,
|
||
`knowledge/job/` and `knowledge/_archive/` sit in this working tree for convenience and
|
||
are tracked by the **other** repository, never by this one; `.gitignore` says so at the
|
||
point of exclusion. `knowledge/catalogue.yaml` is the one deliberate exception, being
|
||
published manufacturer data, and it says so in its own header. The gate that keeps the
|
||
split honest is §3.4.
|
||
|
||
---
|
||
|
||
## 3. The discipline
|
||
|
||
### 3.1 The honesty invariants
|
||
|
||
**NOT RUN is never a pass. Silence that looks like success is the cardinal sin.** NOT RUN
|
||
renders **red**, with FAIL, and `ticketpage.py` carries a comment forbidding the next
|
||
reader from greying it out: a check that did not run tells you nothing about the piece, so
|
||
it must never look calmer than one that failed. One unauthorised exclusion hid forty
|
||
tickets and every check that ran came back clean (`scope.py`, §1.1); seven `die()` calls
|
||
let refusals leave through a dialog, so a batch where every panel refused produced a blank
|
||
report. Every refusal now goes through `not_run()`.
|
||
|
||
**`None` ("could not judge") is not `[]` ("judged, found nothing").** `ticketchecks.run_all`
|
||
disambiguates an empty return with a **skip probe**: `_SKIP_PROBES[check_name]` gives
|
||
either a reason or `None` (ran, found nothing). A check with *no* probe logs `RAN-EMPTY`
|
||
and renders NOT RUN reading "no probe declared" — unexplained silence is the defect, not
|
||
agreement. A probe's sentence must name a verifiable place: row 5.4 now separates *no
|
||
spec*, *a spec predating the field*, and *a design recording no mesh*. Every registered
|
||
check has a probe now, and it is test-enforced (§4.4) rather than a count to keep
|
||
re-auditing by hand.
|
||
|
||
**A declared out-of-scope row is a third state, not a silence.** The scope is declared in
|
||
the job layer, never inferred. No declaration means judge everything, and a mark whose
|
||
prefix cannot be read is never set aside — "I could not tell" may only resolve towards
|
||
judging.
|
||
|
||
**A model-read (`AI-*`) finding is always WARN**, never OK and never FAIL. With
|
||
`vision_escalation` on, grade-D items no comparison can settle go to a small model reading
|
||
one rendered view; the answer lands in `model_read`, never in `findings`, and **can never
|
||
set a checklist row**. `ticketvision` carries that provenance four independent ways (the
|
||
`source` field, an `AI-` code owning no row, its own list, its own report section), on the
|
||
assumption that an edit will drop one and the other three still have to hold. A model that
|
||
saw nothing wrong in one view is a reason to look, not to stop looking.
|
||
|
||
**Muting never silences a FAIL**, and a muted check still *runs*: `apply_muting` repaints
|
||
it grey rather than removing it, and MUTED gets its own tally bucket. "31 OK" when four of
|
||
them were nobody's judgement is the quiet lie the table exists to stop.
|
||
|
||
**A check may not claim what the tool is in no position to claim.** Rows 5.6 and 5.7 both
|
||
printed *"no reinforcing found on this piece"*; this plant models bars outside any rebar
|
||
category, so with `drawings.reinforcing_families` empty a **301-member** piece produced the
|
||
same words as a piece with no steel in it. They now say the tool could not **recognise**
|
||
any, name the key that would turn it on, and **stay red** — *a better sentence is not
|
||
permission to soften the state*. Two siblings landed the same day: 5.1 printed OK off one
|
||
of its two questions, so a check slot now takes a tuple and **any quiet half makes the row
|
||
NOT RUN**; and 5.3, a person's job by design, printed red because a hand-kept
|
||
`FOR ENGINEER` tuple omitted it — that list is now derived from the table.
|
||
|
||
### 3.2 The knowledge layer law
|
||
|
||
The mechanism is §2.3. The law is this: **job beats plant beats what everybody in the
|
||
trade knows** — not a tie-break, but a statement about where truth lives. The trade knows
|
||
a bar mark is a recipe; the plant knows its own hardware; the job knows what was drawn.
|
||
So **placement is an argument, not a preference**. Three examples from a single day:
|
||
|
||
- **Rebar stock lengths → the plant.** One number for every size let a 30 ft `#4` pass
|
||
where the plant stocks `#4` in 20 ft — a false OK on a real defect class. It became a
|
||
**map keyed by printed size, in integer inches** (`catalogue.rebar.stock_by_size`, values
|
||
supplied by the plant layer). As a *list*, a job adding one size would silently delete
|
||
the plant's whole table.
|
||
- **A job's odd-grade bar → the job.** The manual files every stock length under a
|
||
designation the owner says is retired; if it reappears it is an exception to this job,
|
||
so it is job knowledge.
|
||
- **Which end is up → public.** The plant layer carried `top_is_higher_global_z`, and both
|
||
obvious repairs were wrong. An assembly origin is placed when the assembly is made, and
|
||
**placing it is the statement**: that is how precast is built, not a house habit, so it
|
||
belongs in the repo catalogue. A key asking a plant to declare it is not an unread key,
|
||
it is the wrong question — and it named a *mechanism* rather than an answer, which a
|
||
layer must never be able to swap.
|
||
|
||
**Dormant and loud.** A key not yet wired ships **null**, with `*_verified: false` and a
|
||
written reason — never a guess. The reinforcing rule shipped that way and proved the right
|
||
*shape* around the wrong *field*; the flag did its job, because the rule never ran and the
|
||
report named the reason, where a guess would have left every bar in the job silently
|
||
un-recognised. Turning it on was two YAML fields and no code change. §4.3 lists what is
|
||
dormant now.
|
||
|
||
### 3.3 Never guess a plant fact
|
||
|
||
Parameter names, categories, family tokens and casting conventions are not code's to know.
|
||
They are reached through **roles** declared in the knowledge layers (`PARAMETER_ROLES`,
|
||
`knowledge.param_names`). Two incidents from one day:
|
||
|
||
- **Three guesses, three wrong.** An agent needed one plant parameter name and guessed it
|
||
three ways; none of the three existed in the model. `rvt.py params <id>` answers it in
|
||
one request. The precedent: the mark parameter name left the code entirely and became
|
||
the role `member.mark`.
|
||
- **Types read where the values were on instances.** A census of the reinforcing parameter
|
||
read family *types* only, but this plant writes that value on the **instance** — so the
|
||
census saw nothing and the agent told the owner the wall panels carried no reinforcing
|
||
at all. Measured properly, one piece had 112 bars and another 84. The rule now carries
|
||
`read_from: [instance, type]`: the value sits on one or the other, mutually exclusively
|
||
per element, and a reader taking only one finds nothing on half the model **and returns
|
||
a result that looks perfectly clean**.
|
||
|
||
**Measure it, ask the owner, or stand down with a written reason — never assume.** When a
|
||
bar-size parameter could not be measured, nobody guessed; it is still an open question
|
||
(§4.3). *"Ask the owner" is the third resort, not the second:* **search the transcripts
|
||
first** — §3.8, one-liner in `CLAUDE.md` §7.
|
||
|
||
### 3.4 Secrecy and the scrub gate
|
||
|
||
**No client, job or plant information anywhere** — code, comments, docstrings, commit
|
||
messages, examples. Where you need to refer to one, write the `CLAUDE.md` §3 placeholder
|
||
rather than deleting the sentence. Product codes are not identities and stay as they are.
|
||
|
||
`tools/scrub.py` is the gate, in two modes on purpose. `--worktree` is the audit: every
|
||
file git can see, tracked *and* untracked, and a hit anywhere is a hit. `--staged` is the
|
||
pre-commit hook: only staged files can block a commit, while untracked hits print loudly
|
||
without stopping unrelated work — because a gate that blocks unrelated work is one people
|
||
learn to pass with `--no-verify`. It also runs inside `tools/package.py`. The term list
|
||
lives in gitignored `.scrub-terms`, so the list of names is never itself the leak;
|
||
structural patterns (model filenames, absolute user paths) are checked whether or not
|
||
anyone remembered to add a term.
|
||
|
||
**The cautionary tale (2026-08-30).** The gate built each term as `\b<term>\b`. In regex
|
||
`_` and digits are word characters, so `\bTerm\b` never matched `PREFIX_TERM` — and
|
||
`PREFIX_ROLE` was the dominant naming convention here before the roles refactor. **The
|
||
terms most likely to be a real leak were written in the one shape the gate was blind to.**
|
||
The fix is letter-only lookarounds, and the sanctioned placeholders are now read out of
|
||
`CLAUDE.md`'s own table so the gate cannot drift from the rule. The rescan: working tree 0
|
||
hits → **4**; history 19 hits in 3 commits → **22 in 6**, all six already pushed.
|
||
**Assume every scan before that fix under-reported.** Editing a file does not remove a
|
||
name from the commit that introduced it — §4.4 has the decision still outstanding.
|
||
|
||
### 3.5 Revit safety
|
||
|
||
- **Never reload pyRevit. Restart Revit.** `reload_pyrevit()` tears down the engine that
|
||
owns the routes handler while the HTTP server still points at it, so the next bridge
|
||
request runs against a dead engine and takes Revit down — on a non-main thread, where
|
||
Revit cannot catch it. Measured 2026-08-14: 11 clean requests, one reload, the 12th
|
||
fatal. It happened twice. Our code never gets to run, so the guard sits outside Revit:
|
||
`startup.py` appends a line to `.bridge-sessions.log` on every pyRevit load, and `rvt.py`
|
||
reads that log before *every* request and hard-refuses on two loads under one process id.
|
||
There is no `/reload` endpoint. If `rvt.py` says the bridge is unsafe, do not work around
|
||
it and do not curl the endpoint — ask for a restart, which picks up new buttons just as
|
||
well.
|
||
- **Never run an unfiltered collector; never guess an API enum.** 2026-08-18: one snippet
|
||
walked the whole document with `WhereElementIsNotElementType()`, touched `.Symbol` on
|
||
every result, and set `Options.DetailLevel` from a value reached by `__import__`. Revit
|
||
died with no exception and no journal entry, because **a bad value handed to a native
|
||
API is not a Python error**. Scope every collector; import enums by name, so a bad name
|
||
is a traceback rather than a dead process.
|
||
- **Transactions for writes; reads never need one** (`with tx('name')`). The production
|
||
model is read-only to agents: prototype read-only, show counts and a sample, and get an
|
||
explicit go-ahead before any mutation. **Nothing in the extension writes document data or
|
||
geometry, on purpose** — a wrong report can be disbelieved, but wrong geometry gets built.
|
||
The one exception is view state: Panel Lift Check opens a `tx('Show assembly')` to
|
||
isolate the piece temporarily and put an 85% transparency override on its non-insert
|
||
members, and writes both back off when it is done.
|
||
|
||
### 3.6 How the work is organised
|
||
|
||
The owner works through sub-agents. **One dedicated long-lived agent holds the bridge**,
|
||
and all live-Revit work routes through it. Everything else is split into **non-overlapping
|
||
file sets** and run in parallel, each landing committed **by explicit path** and pushed.
|
||
|
||
**`git add -A` is prohibited**, and it has cost twice. `cb1587d` swept in a
|
||
**deliberately broken** button that the tooltip agent had planted to prove its guard
|
||
fires, so HEAD briefly shipped a fake tooltip (repair and rule in `14de0d3`). And
|
||
`109d7df` — titled *"Today is the 30th, not the 31st"* — swept in the first half of
|
||
another agent's unfinished up-end work; the follow-up, `cf9e164`, opens by saying so.
|
||
|
||
A glob is the same mistake in a different coat: `git add .`, `git commit -a`,
|
||
`git add lib/` while another agent is mid-edit. **Name the files.**
|
||
|
||
### 3.7 Testing
|
||
|
||
**Host-testable by design.** The whole suite runs on plain CPython with no Revit and no
|
||
licence:
|
||
|
||
```bash
|
||
python -m pytest WillTools.extension/tests -q # the extension
|
||
cd panellift && python -m pytest -q # the solver
|
||
```
|
||
|
||
**Run them; do not quote them.** Two readings on 2026-08-30, hours apart on the same
|
||
machine: `panellift` 272 passed both times, while the extension went from 1807 passed / 1
|
||
failed to 1860 passed / 1 failed, because another agent was adding tests in between. That
|
||
is the normal condition here, and it is why every count in this document is date-stamped
|
||
or replaced by a command. The one thing that held across both readings is the single
|
||
failure — a real one, environment-coupled, described in §4.4. Several documents in this
|
||
tree still quote counts that were true when written and are not now (§4.6).
|
||
|
||
`tests/conftest.py` is what makes it possible: it registers a bare module object as
|
||
`sys.modules['vibe']` with `__path__` pointed at the real package, so `vibe/__init__.py` —
|
||
which imports the Revit API the moment it runs — never executes. That reaches any module
|
||
touching Revit only *inside* functions needing a live `doc`, and cannot reach one
|
||
importing the Revit API at **module scope**. `ticketqc.py` / `ticketchecks.py` are the
|
||
standing example: `ticketqc` imports eight `Autodesk.Revit.DB` names at the top and
|
||
`ticketchecks` imports `Finding` from it, so freeing them takes more than moving an
|
||
import. `test_ticketchecks_host.py` reaches a few pure decisions through a deliberately
|
||
**inert** stub of those names: if an edit makes one really call the API, the test raises
|
||
instead of quietly passing.
|
||
|
||
**Pin against a written-down golden, not a recomputation.** The takeoff count is pinned to
|
||
numbers typed into the test, because *a pin that recomputes both sides pins nothing* — it
|
||
agrees with any refactor, including a wrong one.
|
||
|
||
**Tombstone tests** keep a deleted thing deleted. `test_no_cap_survives_anywhere_in_the_module`
|
||
asserts that `ticketpage` has no `capped_note` and no `NOTE_CAP`: that 180-character cap
|
||
had truncated a debond run mid-number, in both the panel and the text file.
|
||
|
||
### 3.8 Search the transcripts before you ask him
|
||
|
||
**A question he has already answered is worse than a question nobody asked.** It costs him
|
||
time, it tells him nobody looked, and — the real damage — it invites him to answer a second
|
||
time. Two rulings then sit on the record with nothing to say which governs, and the next
|
||
agent gets to pick. The rule, in his words, 2026-08-31: *"开发纪律里面再加一条,有问题先搜索
|
||
聊天记录。搜不到再来问我。"*
|
||
|
||
**The incident that bought it (2026-08-31).** An overnight audit put
|
||
`handling.tension_limit_sqrt_fc` at the top of the owner's list as **A0**, under a heading
|
||
saying it was *"the only open item that changes a live engineering answer"* — reasoning
|
||
that he had made the stress limit a per-job choice, that the job layer held nothing, and
|
||
that the job was therefore running on 7.5 inherited from a layer he never said should
|
||
decide it. Every one of those facts was true. The conclusion was still wrong: he had ruled
|
||
the value twelve days earlier, **2026-08-19: *"就用7.5sqrt。"*** The plant layer holds
|
||
exactly 7.5, the tool had been doing the right thing the whole time, and A0 existed only
|
||
because nobody searched. Escalating it would have spent his morning re-deciding a decided
|
||
thing.
|
||
|
||
Note what the audit actually did wrong, because it is the subtle half: it found a **later**
|
||
quote (2026-08-30, *"应力极限写入 projectconfig … 让用户自己填写。I can see it both ways"*)
|
||
and read a ruling about **where the knob lives** as if it vacated the ruling about **what
|
||
the number is**. Those are different questions and both answers stand. A mechanism ruling
|
||
does not retract a value ruling — and a search that returns two hits is not finished until
|
||
you have worked out whether they even disagree.
|
||
|
||
**Searching is cheap. `CLAUDE.md` §7 has the one-liner; the measured cost over the whole
|
||
65 MB of transcripts is 0.2 seconds.** "It is expensive to look" is not available as a
|
||
reason, and neither is "I did not know where they were".
|
||
|
||
**But a search that finds nothing is not proof he never said it.** The transcripts are one
|
||
machine's history, they are not indexed, they hold his messages in two languages, and the
|
||
term he used is very often not the term you searched. So the honest report is
|
||
***"I searched X, Y and Z and found nothing"* — never *"he has never ruled on this"***.
|
||
State the terms; the next reader can then extend the search instead of repeating it. This
|
||
is §3.1's invariant wearing different clothes: not-found is a NOT RUN, not a pass.
|
||
|
||
When you do find it, **write it down where the value lives** — beside the number, with the
|
||
date and his words verbatim — so the next agent finds it by reading rather than by
|
||
searching. A ruling recorded only in a transcript will be asked again.
|
||
|
||
### 3.9 Three tiers: the coordinator decides, Sonnet explores, DSV4F labours
|
||
|
||
**The coordinating agent (Fable / Opus class) is the most expensive process on this
|
||
machine, and it spends its budget on four things only: planning and splitting work, judging
|
||
results, deciding anything irreversible or anything that touches live Revit, and talking to
|
||
the owner.** Below it sit two cheaper tiers, and they are not interchangeable:
|
||
|
||
- **Exploration — reading a codebase to answer "how does this work / what would it take" —
|
||
goes to a Sonnet sub-agent** (the harness's `Agent` tool with `model: sonnet`; or Opus
|
||
driving Sonnet), never to DSV4F. Exploration needs judgement about what matters, and the
|
||
owner's ruling is that DSV4F does not get to make it. Sonnet returns a note with
|
||
`file:line` citations; the coordinator reads the note, not the files, and goes to the
|
||
source only for the fact a decision hinges on.
|
||
- **Labour — extracting tables from documents, mining PDFs into notes and fixtures,
|
||
translating, classifying, reformatting, and writing code to a settled, pre-split spec —
|
||
goes to DSV4F (DeepSeek V4 Flash)**, with V4 Pro only where Flash has demonstrably failed
|
||
on the same material. The owner, 2026-09-05: *"探索你带着sub sonnet或者让opus带sonnet去做。
|
||
DSV4F别做探索。DSV4F就干点儿体力活儿"*.
|
||
|
||
**The incident that bought it (2026-09-05).** `CLAUDE.md` carried *EXPLORE (me) → WRITE
|
||
(DSV4F) → REVIEW (me)* with the sentence *"exploration never goes to DSV4F"*, and no rule
|
||
said the exploring had to be cheap. Under that, the coordinator spent an afternoon on
|
||
`docs/pa-web-spec.md`, `tools/pa_web/static/app.js`, `draw.js` and every key
|
||
`beamcalc/model.py` reads from a job file — twelve consecutive file reads on the most
|
||
expensive model, to produce a note a Sonnet sub-agent writes as well. The owner's words:
|
||
*"用DSV4F。这些东西开发纪律里面没有?"* — and until that day this section did not exist. The
|
||
first draft of the fix then over-corrected and dispatched that same reconnaissance to DSV4F;
|
||
he stopped it within minutes with the ruling quoted above. Both halves are the rule: the
|
||
coordinator does not do the reading, **and** the reading is Sonnet's, not Flash's.
|
||
|
||
**Which DSV4F route.** Two exist and they do not share a balance. `cheap` (in `PATH`) is
|
||
the DeepSeek platform direct; on 2026-09-05 that balance was **negative** (`402 Insufficient
|
||
Balance`) after the `sample_job` document mining ran V4 Pro at 65k output tokens with parallel retries, and
|
||
every `cheap` call and mining script died with it. **Command Code (`cmd -p "<brief>" --model
|
||
deepseek/deepseek-v4-flash --yolo --no-session`) is a separate account with its own quota**,
|
||
and the owner's instruction that day was to use it. Check the balance before dispatching a
|
||
batch, not after it has failed; a 402 on the first call means every call.
|
||
|
||
**Detach anything that outlives a turn.** The coordinator runs as a systemd service under a
|
||
watchdog that restarts it — three times on 2026-09-05: 13:11 and 14:11 for a mis-detected
|
||
gateway, 15:09 for a stall. A service restart kills the whole cgroup, and each time it took
|
||
the background workers with it: seven `cmd` mining jobs launched from the coordinator's
|
||
shell, four dead mid-write, output files 0 bytes or absent. A worker launched with
|
||
**`systemd-run --user --unit <name> …`** is its own transient unit and survives; one
|
||
launched with `&`, `nohup` or `setsid` from the coordinator's shell does not. Write the
|
||
worker's result to a file and its status to a log (§3.1 applies: an empty log is a NOT RUN,
|
||
not a pass), so a coordinator that has lost its memory can read what finished instead of
|
||
asking.
|
||
|
||
**Split before you send, and size to one reply.** A whole-feature brief truncates at
|
||
max-tokens and reads as "it skipped the hard parts"; one function, one section, one file's
|
||
change per task (`CLAUDE.md` § delegation, owner 2026-09-03: *"确认了边界的活儿,拆细了再让
|
||
DS 干"*). The gates — pytest, scrub, review — run on the coordinator's side, never inside the
|
||
worker.
|
||
|
||
---
|
||
|
||
## 4. Where it stands
|
||
|
||
Written 2026-08-30 against `ff10870`. The working tree was clean when this survey was
|
||
taken; it is edited concurrently by several agents, so check `git status` yourself rather
|
||
than trusting that. `docs/checklist-code-map.md` was rebuilt 08-30 17:19 and commits have
|
||
landed since.
|
||
|
||
### 4.1 The checklist, row by row
|
||
|
||
Authority is `CHECKLIST_ITEMS` in `lib/vibe/ticketpage.py`.
|
||
|
||
| # | grade | state | note |
|
||
| --- | --- | --- | --- |
|
||
| 1.1 sheet name = mark | A | running | no skip probe (§4.4) |
|
||
| 1.2 sheet design reduces to piece design | A | running | judged in `ticketqc.check_ticket`; no named check fn |
|
||
| 1.3 one ticket set per piece | A | running | no skip probe |
|
||
| 1.4 view names refer to this design | B | running; **dormant out of the box** | needs `nomenclature.design_number_re` — plant layer, not `DEFAULTS`. Re-graded A→B 08-30 |
|
||
| 1.5 call-outs do not overlap | C | running, built 08-30 | rotated annotations set aside under `CALLOUT-OVERLAP-UNREAD`; no sample, untested |
|
||
| 2.1 printed size vs modelled | B | running | |
|
||
| 2.2 weight three ways | A | running | the *measured* leg needs run option `expensive_geometry` (default ON); off, the row is NOT RUN naming it |
|
||
| 2.3 unit weight | A | running | |
|
||
| 2.4 cast extras in the weight | A | running, built 08-30 | verified end-to-end on a real ticket |
|
||
| 3.1 f'c · 3.2 f'ci | A | running | |
|
||
| 3.3 release ratio | B | **muted on this job**, still runs | owner's own dated sentence: no panel here has the thing it asks about |
|
||
| 4.1 strand count · 4.2 diameter & grade | B/A | running | |
|
||
| 4.3 jacking force | A | running | `tolerances.it_lb` wired 08-30 to **100 lb**; the job layer's `1.0` deleted, not corrected |
|
||
| 4.4 strand positions vs chain from rail | B | running | stands down with a reason where there is no strand geometry |
|
||
| 4.5 debond call-out | C | running, built 08-30 | consumes 4.7; where 4.7 could not judge, 4.5 is NOT RUN with 4.7's reason |
|
||
| 4.6 rows & spacing | A | running | same check as 4.4 |
|
||
| 4.7 strand debonded where concrete runs short | B | running | |
|
||
| 5.1 BED PLAN call-out embed exists | A | **half running by construction** | `check_bom_members` runs; `check_detail_callouts` reports NOT RUN *naming the sheets nobody read* wherever the plan cites a bubble. Only a plan citing no detail can be OK |
|
||
| 5.2 embed in model appears on ticket | A | running | |
|
||
| 5.3 face designations vs model side | C | **FOR ENGINEER by design** | printed a red NOT RUN on every ticket until 08-30 — missing from a hand-kept tuple (§3.1) |
|
||
| 5.4 mesh / WWF designation | A | **muted on this job**, still runs | no mesh on this job |
|
||
| 5.5 bar marks legible | D | **FOR ENGINEER by design** | a property of the picture |
|
||
| 5.6 bar marks unique | B | running, built 08-30 | split out of the old 5.5; runs where the reinforcing rule resolves (§4.3) |
|
||
| 5.7 no bar longer than its stock | B | running, **against one generic number** | the per-size stock table is built, populated and unreachable (§4.3) |
|
||
| 6.1 insert capacity · 6.2 edge distance | A | running | no skip probe |
|
||
| 6.3 handling result | A | running | no skip probe; three silent `return []` paths |
|
||
| 6.4 dunnage not under an opening | A | running, probe added 08-30 | open ruling on the all-blind-embed path: NOT RUN (today) or OK |
|
||
| 6.5 note views say something | B | running; **dormant out of the box** | needs `notes.instruction_view_sets`; `DEFAULTS` ships it empty |
|
||
| 7.1 interference | B | running | WARN-only by ruling, over 157 findings on 57 archived tickets. Do not re-infer that the rest are noise — that was overruled once |
|
||
| 8.1 unresolved design-summary lines | C | running | the one true FAIL of the 08-30 live run ("no probe declared"); probe added |
|
||
| 8.2 no invisible characters | A | running | no skip probe |
|
||
|
||
Nothing on the list is unbuilt. Two rows are a person's job on purpose, two are muted with
|
||
a written dated reason, the rest compute. What varies is how much of its question each row
|
||
actually reaches — **5.1 and 5.7 claim least**.
|
||
|
||
**The table above is the 34-row census this section was written against and is now stale
|
||
by count** — `checklist.master` in `knowledge/catalogue.yaml` carries **46** entries as of
|
||
2026-08-31 (verified by reading `len(ticketpage.CHECKLIST_ITEMS)` on a fresh load; run it
|
||
rather than trusting this number later). The rows above through 8.2 are still real; twelve
|
||
more (`4.8`, `5.8`–`5.15`, `6.6`–`6.8`) have landed in the master since and are not
|
||
individually described here. Extend the table from the catalogue, not from memory of it.
|
||
|
||
### 4.2 The other tools
|
||
|
||
| tool | state |
|
||
| --- | --- |
|
||
| **Panel Lift Check** | Finished, in daily use, no licence needed to test. Numerics open (§4.4). The per-stage input control aligned to the reference tool's `HandlingData` is a **mapping document only** (`docs/panellift-ewf-alignment.md`); nothing is built from it |
|
||
| **Reinforcing Suggestion** | Finished. End-to-end 08-30: cold 10 s, warm ~1 s, offline byte-identical to the bridge. Position recommendation defaults **off**; one invariant test still owed |
|
||
| **Ticket QC** | Finished, the flagship. Six in-process `vibe/ticket*.py` modules — `wc -l WillTools.extension/lib/vibe/ticket*.py`, and expect the number to have moved since you last looked; it is what §4.5 is about |
|
||
| **Snapshot & Close** | Finished. Phases A and B measured green; phase C (the deferred detached copy) is off unless the job layer enables it, and journals its own silence |
|
||
| **Takeoff** | Finished. `nomenclature.ticket_unit` wired 08-30, so the tool stops deciding for itself what one ticket covers |
|
||
| **Job Recon** | Finished. Self-loading, scope-aware, 9.3 s on the real job |
|
||
| **Design Extract** | Finished, with its caveat printed on it: every value it writes is machine-read and unverified |
|
||
| **Self Check** | Finished |
|
||
| **Gravity (WIP)** | Neither this nor Lateral writes to the Revit model — both are extractors: read the model, write a file elsewhere. Gravity is meant to extract information and write **Excel**. Today it does none of that: **a button with nothing behind it, and it says so.** ~200 lines of placeholder printing the open questions and assuming nothing. Blocked on the owner supplying old jobs and a control spreadsheet |
|
||
| **Lateral (WIP)** | Meant to extract lateral-resisting members and write **RISA** — a structural analysis package — its input format. Half-built, and it does real work: it extracts marked members and writes a JSON intermediate model, two CSVs, and a `.r3d.DRAFT.txt` (`.r3d` is RISA-3D's own file extension) — the `.DRAFT` suffix deliberate, so it cannot be loaded into RISA by accident. What the RISA format needs beyond that is not established |
|
||
|
||
### 4.3 Dormant, and what would wake it
|
||
|
||
| dormant | what wakes it | who |
|
||
| --- | --- | --- |
|
||
| `drawings.reinforcing_rule.scopes` — empty, so 5.6 and 5.7 see one population | a ruling on three edge cases: bar-formed hardware carrying the token (one is a strand loop, with neither mark nor stock length), and reinforcement carrying none | owner, A1 |
|
||
| `catalogue.rebar.stock_by_size` — populated, read by `knowledge.rebar_stock()`, **never called by `check_bar_stock_length`** | which parameter carries a bar's size | owner, A3 |
|
||
| 5.3 as a computed check | what makes an elevation line dashed — line style, subcategory, or nested detail items. The view's 17 filters were read; none carries it | owner, A5 |
|
||
| the up-end rule | woken 08-30 on evidence over 282 assemblies. Still open: the vertical-panel case, and the colour→axis mapping, which is inference not measurement | owner, A4 |
|
||
| `tolerances.psi` (inert, muted not deleted) and `details`, the 70-entry job catalogue nothing reads | one word each: wire or delete | owner, D1 / D7 |
|
||
| `drawings.rebar_must_appear_on` / `section_a` / `section_b` | a dated promise queued behind the current phase — not drift | queued |
|
||
| `check_detail_callouts` as a verdict | something that reads a detail sheet's *contents*. Not built; needs the model | build |
|
||
| vision escalation (`AI-` codes) | off by default; owns no checklist row and cannot flip one (§3.1) | config |
|
||
|
||
### 4.4 Known defects and unfinished business
|
||
|
||
- **`qc-run.json` carries findings, not the run log.** `ticketvision.qc_json` takes
|
||
`findings` + `model_read` and nothing else — checklist rows, mutes and `run_all`'s log
|
||
stay behind. A NOT RUN row's *reason* reaches the HTML page and `last-ticketqc.txt` but
|
||
never the machine-readable file, so anything reading the JSON cannot tell a row that
|
||
passed from a row nobody asked.
|
||
- **A failing test, environment-coupled.** `test_reinforceoffline.py`'s no-index case fails
|
||
here because `cli.resolve_index` finds the real, gitignored `knowledge/job/` and answers
|
||
with a job list instead of the three-ways message. Green on a clean checkout, red on the
|
||
owner's machine.
|
||
- **Closed, 2026-08-31: every registered check now has a skip probe, and it is
|
||
test-enforced rather than a hand count that can silently rot again.**
|
||
`tests/test_ticketchecks_host.py::test_every_registered_check_has_a_skip_probe` walks
|
||
`ticketchecks.CHECKS` against `_SKIP_PROBES` and fails, naming the orphan, the day a new
|
||
check ships without one. A silent `return []` from an unprobed check still logs
|
||
RAN-EMPTY and prints a red NOT RUN reading "no probe declared" — the mechanism is
|
||
unchanged — but the gap this bullet used to track (eight bare entries, then seven) is
|
||
gone; run the test rather than trusting a count copied in here.
|
||
- **Client and plant names are in pushed history** (§3.4). **Untaken decision:** redact the
|
||
working tree only, or rewrite history and force-push.
|
||
- **The lifting solver's numerics.** `docs/lift-check.md`, written up, unstarted. Not a
|
||
speed question: today's recommendations are not determined to better than several inches
|
||
and on some pieces are not the best layout available. It gates any further
|
||
insert-placement work.
|
||
- **`families.wwf_prefix` has two defaults** — `takeoffgather` falls back to empty (no mesh
|
||
detection), `ticketqc` to `'WWF'`. Reachable only when a layer sets the key null, which is
|
||
a plant saying it has no welded wire fabric. If that is what null means, `ticketqc` is
|
||
wrong.
|
||
- **Smaller, recorded:** `read_ticket` costs 37.6 s on a large piece, entirely outside the
|
||
checking wall and with no code-level cure; and `catalogue.rebar.stock_exempt_marks` is
|
||
empty pending the 60-ft continuous-bar ruling.
|
||
|
||
### 4.5 The roadmap, as it actually exists
|
||
|
||
**`docs/architecture-migration.md` — nine steps, approved 08-30.** In-process IronPython
|
||
keeps only the gatherers; judgement, matching and rendering move to host CPython, fed by
|
||
the versioned JSON of §2.2.
|
||
|
||
*Step 1 landed 08-31*: Ticket QC writes `evidence/run-<stamp>.json` on every press and
|
||
**nothing reads it**. As built it corrected its own plan — four live-object holdouts, not
|
||
two, so the serialiser gates on type. **§5.1, parity on real data, is not met and could
|
||
not be**: it needs a press, and the bridge was held.
|
||
|
||
*Step 2* buys a parity proof and nothing else: a host runner judging the 13 Revit-free
|
||
checks offline against the twelve-piece audit set. Cost M, no visible change. *Step 3* is
|
||
where it pays — the button calls that runner (reusing `liftexport.find_python`/`run`),
|
||
making it the first press where a `ticketchecks` judgement edit is visible **without a
|
||
Revit restart**. Cost S.
|
||
|
||
**§8.2, honestly:** `engine: clean` is landed on three buttons and buys nothing today. A
|
||
clean engine is already the default here, because `extension.json` declares no `url`, so
|
||
pyRevit's package lookup drops the extension and rocket mode never takes the clean engine
|
||
away — verified two ways offline. It is a **pin, not a lever**, against the day somebody
|
||
adds a `url`. **The one-minute behavioural check — press, edit a rendered string, press
|
||
again without restarting — has still never been run**, and no bridge call answers it.
|
||
|
||
**`QNA/eriksson-gap-inventory.md` — the commercial-parity list.** 46 quantities: 10 at
|
||
parity, 2 needing no build, **34 to build** (17 S · 5 S-after-dependency · 5 M · the rest
|
||
larger). **Mcr is the cheapest first win** — S, P, A and e are held at every station and
|
||
`modulus_of_rupture_psi` is written. The load-bearing finding: the reference tool is a **1D
|
||
single-member analysis, not 2D FEA** — every plot is a function of x, with no mesh
|
||
reference and no Mxy/Wood-Armer column in 18 pages. That reframes tier B entirely. The list
|
||
needs the owner's tick before anything on it is built.
|
||
|
||
Beyond those two, the only genuine plans are the reinforcing-suggestion brief's open
|
||
granularity questions and §4.3's queued coverage rule. The rest of `QNA/` is record, not
|
||
plan.
|
||
|
||
### 4.6 Where the docs and the code disagree
|
||
|
||
Several documents were accurate when written. Where they are not, the code wins.
|
||
|
||
- **Every test count quoted in this tree is stale.** `CLAUDE.md` says `panellift` has 76
|
||
tests and the extension suite is `49 passed`; `README.md` says `34 passed`. Run the
|
||
commands in §3.7.
|
||
- **Every line count quoted in this tree is stale, recent ones included.**
|
||
`architecture-migration.md` §3 warns it will drift and has drifted again; on 2026-08-30
|
||
`ticketqc.py` moved between two measurements minutes apart, because other agents were
|
||
editing. Use `wc -l`, and read any line count as a date-stamp rather than a fact. §3.3's
|
||
"26 registered checks" is now 27. **Verdicts hold; numbers do not.**
|
||
- **Design Extract calls no AI**, despite `README.md` ("hands the scrape to Claude Code")
|
||
and `CLAUDE.md`. The script says "no AI call, no subprocess", and no button routes
|
||
through `vibe/aiextract.py` any more.
|
||
- **`configdump.py` and `CLAUDE.md` say four knowledge layers**; `layer_values()` handles
|
||
five, with config behind `include_config=False`.
|
||
- **`knowledge.load()`'s docstring drops a folder** — it gives the job layer as
|
||
`<model folder>/knowledge.yaml`, where `designspec.knowledge_path()` returns
|
||
`<model folder>/_ticketqc/knowledge.yaml`.
|
||
- **`docs/handoff.md` calls Panel Lift Check "the one under development"**; it is one of
|
||
the four finished large buttons, and the same file's "Never migrate" section calls it
|
||
*already migrated*.
|
||
- **Batch speeds disagree across three files** — `panellift/cli.py` 41 min / 40 s,
|
||
`docs/lift-check.md` ~40 min / 2 min, `PanelLiftCheck/script.py` 38 min / 2.5 min. The
|
||
last is the most specific and names its path.
|
||
- **`rails.left_rail_is_fixed` is a dead flag** (`docs/knowledge-override.md` says so
|
||
correctly): one reader, `aiextract`, which puts it in a prompt. No check consumes it.
|