Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
166 commits
Select commit Hold shift + click to select a range
6be23d3
RESIDUAL-DARK: the developer's actual question was built, routed and …
claude Sep 25, 2026
7e39f34
SCHEMA-UNGENERATED: the generated client types were never complete, a…
claude Sep 25, 2026
43cd696
RENTROLL-DARK: two engines that say whether you can believe the rent …
claude Sep 25, 2026
5c6b8e6
T12-SELFTIE: a gate that could not fail, because nobody was handing i…
claude Sep 25, 2026
f8bae56
COVENANT-DARK: two verdicts that read clean when nothing was evaluated
claude Sep 25, 2026
f661a33
VERDICT-COVERAGE: the class found five times by hand, now derived
claude Sep 25, 2026
6fc46a2
AUTHORITY-DARK: a gate you could read and could not satisfy
claude Sep 25, 2026
f779f0c
DECISION-GATE-DARK: the keystone over everything else on the tab, and…
claude Sep 25, 2026
482084b
VERDICT-COVERAGE: the gate caught the card written to demonstrate it
claude Sep 25, 2026
b2bb594
SUPPLY-DARK: an empty competitive set reported the most favourable ve…
claude Sep 25, 2026
b2cc295
SCAN-TRUNC: a model cut short turned a correct building into 500 as-b…
claude Sep 25, 2026
e22c282
SCAN-TRUNC: delete the compatibility wrapper nothing was compatible with
claude Sep 25, 2026
0601e21
TRUNC-COUNTED: three screens printed the size of a page, and one of t…
claude Sep 25, 2026
d1085e8
TRUNC-COUNTED: extract the diff view, because the ratchet said the pa…
claude Sep 25, 2026
e3905a4
SCAN-DARK: the refusal SCAN-TRUNC added pointed at a route frozen as …
claude Sep 25, 2026
850aa32
PROGRESS-UNMATCHED: an engine that promised "never silently dropped" …
claude Sep 25, 2026
5abcc1c
SCOPE-EMPTY: an empty scope register and one with nothing done were t…
claude Sep 25, 2026
7e1c0d0
DEGENERATE-SWEEP: the ledger, so the eight clean results are not re-d…
claude Sep 25, 2026
9db199b
CI-LATEST-DARK: the badge is stored so it need not be recomputed, and…
claude Sep 25, 2026
ba3bddd
PORTAL-TXN-DARK: record a transaction with neither end built, rather …
claude Sep 25, 2026
999bdd8
SSO-DOOR-DERIVED: the fourth sign-in door arrived guarded, and the ga…
claude Sep 25, 2026
55f1cca
SSO-DOOR-DERIVED: review found eight defects, and the derivation was …
claude Oct 11, 2026
27fc920
COVENANT-PCT: a percent sign made every covenant verdict wrong, in bo…
claude Oct 11, 2026
39a4ce8
AUTHORITY-REPLACE: the authority editor's "Save" was a delete button
claude Oct 11, 2026
e3a4008
REVIEW-TRIO: three findings from one review, each a declaration that …
claude Oct 11, 2026
838458c
Review round 2: four findings, and two of them are the first repair s…
claude Oct 11, 2026
4b0ad91
AUTHORITY-DOWNSTREAM: the gate that exists to block, reaching the car…
claude Oct 11, 2026
0dc544b
AUTHORITY-DOWNSTREAM review: eight findings, every one a withholding …
claude Oct 11, 2026
23a14dc
FIELD-STREAM-POISON: the reconnect loop retried a refusal for ever, a…
claude Oct 11, 2026
45f4879
FIELD-STREAM-POISON review: six findings, and the slice contradicted …
claude Oct 11, 2026
a700648
CR-REVIEW-1: four of five findings from the first independent review,…
claude Oct 11, 2026
bb9f986
WS-BTN-DEAD: measure the claim instead of relaying it
claude Oct 11, 2026
475b540
CR-REVIEW-2: the retry control was invalid HTML, and its test could n…
claude Oct 11, 2026
123f2a6
CR-REVIEW-3: a generation token that advanced on one of the two event…
claude Oct 11, 2026
39ca00a
CR-REVIEW-4: the control written to surface a reason put the reason b…
claude Oct 11, 2026
b55d4fb
OPS-REENTRY: the Operations panel re-renders into one host on every t…
claude Oct 11, 2026
679e11a
R24-FIELD-MODE slice 7: slice 6's refusal classification was inert fo…
claude Oct 11, 2026
a9decc2
CR-REVIEW-5: a pipe inside backticks is still a table cell delimiter
claude Oct 11, 2026
18f2399
CR-REVIEW-6: a notice any unrelated redraw could erase, and a gate wh…
claude Oct 11, 2026
bb8b7b5
TABLE-CELLS: the pipe gate I had just shipped was a subset, and eight…
claude Oct 11, 2026
383579f
CR-REVIEW-8: three defects in the parity check I had just shipped, tw…
claude Oct 11, 2026
f6ec1d4
WS-BTN-SELECT: the room strip marked the room you came FROM, on every…
claude Oct 11, 2026
096b6a7
PREFAB-FREEZE-RACE: the freeze write re-resolved the scope the reader…
claude Oct 11, 2026
1c22d4c
ACCENT-DOM: measured the third direction of the colour contract, and …
claude Oct 11, 2026
23badc4
CAP-IOS-CRIT: the node scan's "4 advisories" is 7, and one is a CRITI…
claude Oct 11, 2026
975d14e
CAP-IOS-CRIT: the patch is six weeks old, and the python half of that…
claude Oct 11, 2026
3546ec8
CR-REVIEW-9: the room marker mirrored the portal's expression and not…
claude Oct 11, 2026
335350f
TOTAL-EXCLUDED: two screens printed a total and not what was left out…
claude Oct 12, 2026
c114225
TRAY-UNCOMPUTED: the tray-fill screen reported a clearance over trays…
ibuilder Oct 12, 2026
26e45fe
MACRO-DARK: RECIPE-MACROS shipped complete, with a preview endpoint a…
ibuilder Oct 12, 2026
b57490e
CODE-ADOPT-DARK: which code editions govern, and which of them are ac…
ibuilder Oct 12, 2026
6e107f3
CODE-READY-DARK: the plan-review data check, and the rules its percen…
ibuilder Oct 12, 2026
59fb46b
CODE-ADOPT-DARK fixup: `/codes/seeded` leaves the frozen blind-spot a…
ibuilder Oct 12, 2026
ae9f624
docs: a web-only change can red a BACKEND gate, and the set is derive…
ibuilder Oct 12, 2026
3b705aa
VE-DARK: the three preconstruction money registers, and what their to…
claude Oct 12, 2026
575ffd1
CITED-ASK-DARK: the answer whose every claim traces to its source, an…
claude Oct 12, 2026
f4b4d31
BENCH-DARK: the portfolio space benchmark, and the exclusion its own …
claude Oct 12, 2026
1a73f6e
docs: ELEMENT-ORPHAN is NOT a gate, and the measurement is why
claude Oct 12, 2026
f19a555
PROD-DARK: the crew productivity loop, and the two things an ahead/be…
claude Oct 12, 2026
0a196a4
PERMIT-BASIS-DARK: the entitlement estimate, and the field that says …
claude Oct 12, 2026
629335b
COLOR-UNSET: colouring the model to find missing data, with the missi…
claude Oct 12, 2026
9c9ba70
EVM-COVERAGE: a front-loading accusation whose guard had a threshold …
claude Oct 12, 2026
71845b2
docs: the verdict-coverage gate saw neither EVM-COVERAGE site, measur…
claude Oct 12, 2026
dc6f74f
LAYOUT-RENUMBER: the field round-trip's join key was a POSITION, and …
claude Oct 12, 2026
4269681
fix(ci): register test_evm_coverage and test_layout_renumber in run_t…
claude Oct 12, 2026
7b03066
COORD-UNANCHORED: an issue with no element anchors was counted as one…
claude Oct 12, 2026
f7d1362
fix(ci): ruff I001 in test_coord_unanchored, and name BOTH API-gate s…
claude Oct 12, 2026
a27d9bf
EGRESS-TRAVEL-DARK: IBC 1017 travel distance had no door, and the JSO…
claude Oct 12, 2026
e3fd371
ELEMENTS-SILENT: the project browser was stale and truncated at once,…
claude Oct 12, 2026
97aa05a
FIRM-RULES-OPTIONAL: the firm's standards were evaluated by nothing, …
claude Oct 12, 2026
7741e7c
FIRM-RULES-OPTIONAL slice 2: the rule library could not be AUTHORED f…
claude Oct 12, 2026
ecc3b97
NOTICE-CLOCK: the time-bar register was computed over the OLDEST 500 …
claude Oct 12, 2026
d74f576
TURNOVER-UNGATED: the quality gate built for G704 certification was r…
claude Oct 12, 2026
cbb88dd
VERDICT-SHAPE3: the eighteenth gate's third blind spot, and the one t…
claude Oct 12, 2026
ac8a777
BATCH-DARK: three callers each paid for the same missing batch, in th…
claude Oct 12, 2026
4dfcdff
SCHEDULE-TRUNC: the door schedule printed its row count directly abov…
claude Oct 12, 2026
165c1f7
CLASSIFY-DARK: wire the classification capability, whose three routes…
claude Oct 12, 2026
f9da5b6
NEGATED-VERDICT: the guard's absence is the defect, so the gate keyed…
claude Oct 12, 2026
a69f6a7
NEGATED-VERDICT: the ratchet refused the fix, so the extraction pays …
claude Oct 12, 2026
7d558b7
NEGATED-VERDICT: correct a syntactic count that was handed over as a …
claude Oct 12, 2026
7953576
BARE-NEGATED: an unvalidated phase reported the document set COMPLETE
claude Oct 12, 2026
570c366
VERDICT-ROOT: the coverage gate counted 15 subset verdicts and ruled …
claude Oct 13, 2026
63c6f31
BARE-NEGATED sweep: a clean negative at 1 of 18, and the sharper rule…
claude Oct 13, 2026
8fdf807
WS-BTN-DEAD: the persona room allowlist that never had any effect, an…
claude Oct 13, 2026
bb7cfab
WS-BTN-DEAD follow-up: the lane cell the close made stale, and the ga…
claude Oct 13, 2026
b8ddf4d
FIN-GOV-DARK + SEC-SCENARIO-LIST: a governance workflow with neither …
claude Oct 13, 2026
d9f4387
AUTHORITY-SUPERSEDE: the explicit supersede control, and the one wron…
claude Oct 13, 2026
ddd28df
SEC-SCENARIO-COMPARE: two cross-tenant holes inside the file my own g…
claude Oct 13, 2026
0850e12
PORTAL-TXN-DARK: put the client-decision loop on the page it was buil…
claude Oct 13, 2026
f4c0d61
CLAUSE-DARK: make the clause playbook writable, and sweep the eight s…
claude Oct 13, 2026
88b9e4a
VIEWTPL-DARK: make view templates writable, and fix a select bug in t…
claude Oct 13, 2026
677718d
HOLDSELL-UNMEASURED: a third state, and the card that needed it first
claude Oct 13, 2026
b248b8e
PROVENANCE-DARK: the third reachability question, and the gap two gat…
claude Oct 13, 2026
0b92cc2
DRAWQA-UNMEASURED: the pre-issuance gate called a half-run review a pass
claude Oct 13, 2026
4bfd7a5
WORKFLOW-DARK: the workflow override, and the refusal that protects r…
claude Oct 13, 2026
1dd4ae3
RECIPE-LOG-DARK: the edit-recipe log gains a reader, and its verdict …
claude Oct 13, 2026
1d628e4
Fix the app-shell budget: the log reader's body goes lazy
claude Oct 13, 2026
331d4a8
MACRO-CAPTURE: the last unwritable stored standard, closed by a capture
claude Oct 13, 2026
e5820f6
QUANT-VACUOUS: gate the verdict shape two neighbouring gates excluded
claude Oct 13, 2026
aeec22b
VERDICT-UNOWNED: one floor over three causes, and DETAIL-REFS-DARK
claude Oct 13, 2026
2da7925
Correct ESTIMATE-DARK: the rule library is not the cost-rule source
claude Oct 13, 2026
2390ec2
DESIGN-STD-DARK: audit the model against the firm's design standards,…
claude Oct 14, 2026
eda5b84
CV-BRIDGE-DARK: the site-photo progress bridge gains its operator hal…
claude Oct 14, 2026
6749da2
OPTION-BOARD-DARK: the design-option deck can be taken to the meeting…
claude Oct 14, 2026
69ae1db
EXPIRY-UNDATED: a date field was declared and never validated, and a …
claude Oct 14, 2026
3f0d4e9
STAKEHOLDER-MEDIUM: a 2x2 grid over a three-level scale, printing "mo…
claude Oct 14, 2026
67e80e4
ROWKEY-SUPERSET: a test named for an equality asserted a containment,…
claude Oct 14, 2026
9a432f8
SCHED-STATUS-DARK: "what is late, as of a data date" gains a screen, …
claude Oct 14, 2026
0d02d40
RESPKEY-DELEGATED: follow one hop into the engine, and fix the first …
claude Oct 14, 2026
00f4baa
LOD-UNREAD: a card named "LOD coverage" showed none, and the gate's o…
claude Oct 14, 2026
0eb7923
PPC-UNASSESSED: a null PPC rendered as "0%" in critical red
claude Oct 14, 2026
9a4e8b7
VERSION-VALUES: the diff computed "1HR → 2HR" and the card printed th…
claude Oct 14, 2026
6d35c15
MODELSYNC-DARK: wire the route that ties the underwriting to the IFC …
claude Oct 14, 2026
90bdb9a
PERMIT-BLIND: the code edition was null on every call, and nothing de…
claude Oct 14, 2026
9c22a40
SCHEDHEALTH-PARITY: the parity test subtracted the keys it did not hold
claude Oct 14, 2026
c0e40c4
LIEN-PLACEHOLDER: the waiver PDF said [Claimant] and "through [date]"
claude Oct 14, 2026
a0631a6
ESTIMATE-BASIS: the midpoint-escalated total, and six local currency …
claude Oct 14, 2026
d68a05b
CAPTURE-SESSION: declare the unmatched counts; file the blocker that …
claude Oct 14, 2026
8e9a621
UNITRATE-BASIS: the engine says check the spread; the card hid it
claude Oct 14, 2026
527d300
COMPTIER-DARK, COND-REFUSED, MC-UNDEFINED: three screens that read a …
claude Oct 14, 2026
1eb8fb7
LEVEL-UNREAD, SCOPE-THIN: two cards that reported motion and coverage…
claude Oct 14, 2026
55a5afe
CO-REJECTED: the change-order total had a term the report did not nam…
claude Oct 14, 2026
a3d2da1
WF-BASIS: a deal nobody has funded produced byte-identical output to …
claude Oct 14, 2026
51f1824
FLOW-CLEAR: the flowline card's own subtitle promised the one thing i…
claude Oct 14, 2026
9e9e1f0
BASIS-UNREAD: 4/4 assemblies compliant (100%) over four of forty, and…
ibuilder Oct 14, 2026
810c221
WAIVER-FLAT: a signed conditional release and a naked exposure were t…
ibuilder Oct 14, 2026
6de4f20
DEADFIELD-INLINE: the dead-field gate's population was a third of the…
ibuilder Oct 14, 2026
724ff79
REVISE-DARK: an AIA revision could be read and never issued, and the …
ibuilder Oct 14, 2026
2614a1f
DRAFT-BASELINE: the approved scheme read as a 68-unit shortfall again…
ibuilder Oct 14, 2026
9f4eab2
LOD-TARGET-INERT: the register's only required field is not a matchin…
ibuilder Oct 14, 2026
e62dffc
CAPTABLE-PIPELINE: the Commit column summed to $16,000,000 under its …
ibuilder Oct 14, 2026
868093a
PREFIX-RANKED: a portfolio risk heading reported 0 high over a portfo…
claude Oct 14, 2026
b9e2b5a
SEQ-UNSCHED + ESTIMATE-DARK: a green "clean" over a support check tha…
claude Oct 14, 2026
1a3db36
FLOW-BUFFER: the flowline ran tight everywhere because nothing could …
claude Oct 14, 2026
ee728cc
CAPTURE-SESSION: the blocker was measured, written down, and still ha…
claude Oct 15, 2026
f8ffaa8
NAMING-UNREAD: an audit that names every offending file, printed as t…
claude Oct 15, 2026
971766d
VG-DARK: a graphics-override engine with no reader, a client type tha…
claude Oct 15, 2026
1c97d32
VG-STYLES: the per-category line-weight table, authorable — and three…
claude Oct 15, 2026
cd77951
CASCADE-DARK: a card named "Requirement flow-down" that shows no flow…
claude Oct 15, 2026
d19e06c
DOCHEALTH-VACUOUS: `naming 100%` over names the server wrote itself, …
claude Oct 15, 2026
458e100
MEP-DISCIPLINE-DARK: MEP counted in a modeller's vocabulary, paged wi…
claude Oct 15, 2026
2651bc3
DECISION-AXES: a decision log whose headline put a state count and an…
claude Oct 15, 2026
4abc583
ACTION-SUBSET: an action tracker whose only declared rollup counts fi…
claude Oct 15, 2026
0b7b09b
CX-CELL-FLAT: "not started" and "all tested, awaiting the Owner" were…
claude Oct 15, 2026
26717ea
HEALTH-WEIGHT-COVER: a weighted composite whose coverage was a count
claude Oct 15, 2026
9c3c600
PREQUAL-LAPSE: the docstring promised two registers and the body read…
claude Oct 15, 2026
65a21c5
OPTION-DISAGREE: the scheme quoted at a return its own deal does not …
claude Oct 15, 2026
522feca
docs: record the scratch-sweep hazard that deleted a tracked gate
claude Oct 15, 2026
b65cc9d
fix: qaSection.ts was 13 lines over its extraction ratchet, and I pus…
claude Oct 15, 2026
a697bc1
MODELOPT-DARK: a whole model-branching capability with no door
claude Oct 15, 2026
d0a44ae
BOARD-SHIPPED: the kanban board offered the shipped workflow, the eng…
claude Oct 15, 2026
183f7a7
SQL-CAP: the data browser printed its own row cap as a total (client …
claude Oct 15, 2026
2351786
SQL-CAP: the query route answers "was anything discarded?" instead of…
claude Oct 15, 2026
4ab9761
RESPKEY-HOP2 + CPM-SILENT: the key derivation reported "did not look"…
claude Oct 15, 2026
59b08aa
RENTROLL-BASIS: 30% occupancy read as 100% when the building area is …
claude Oct 15, 2026
80b7dcf
Fix the red build I pushed: close the rent-roll KNOWN_GAPS entry its …
claude Oct 15, 2026
b45f79d
RENTROLL-BASIS: publish the occupancy denominator, and qualify it at …
claude Oct 15, 2026
091042f
RENTROLL-BASIS: read the engine's occupancy note instead of declaring…
claude Oct 15, 2026
5050fbd
FIELDLOG-WINDOW: the field log's coverage denominator was derived fro…
claude Oct 15, 2026
6e01553
SRC-IS-PYTHON: a gate for the stray I just committed, because four ch…
claude Oct 15, 2026
c127548
TM-UNBILLED: $90k of unsigned drafts and $90k of GC-accepted T&M were…
claude Oct 15, 2026
cf51832
PRICE-MATCHED: the pricing variance subtracted two populations, and t…
claude Oct 15, 2026
a11d805
SET-REVISED: the controlled drawing register issued a twice-revised s…
claude Oct 15, 2026
257b9fd
CARBON-UNCONVERTED: a material the carbon engine recognised but could…
claude Oct 15, 2026
21f0eee
CARBON-UNCONVERTED follow-up: sweep the class, and record what predic…
claude Oct 15, 2026
19f5b69
ITB-DOUBLE: the bid-coverage screen counted bids twice, and four more…
claude Oct 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1,141 changes: 1,141 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

92 changes: 91 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,83 @@ whose regex carries the prefix by construction, and probe validity is **self-che
must match its own route), so an unhandled converter reds the build. *A list of known cases is a list
somebody stopped widening; a self-check is not.*

**A seventeenth joined them on 2026-09-25: `services/api/test_schema_types_agree.py`** — does the
committed `apps/web/src/api/schema.d.ts` describe the API this server serves? It is generated from the
FastAPI spec and checked in, so it is a claim about the server sitting beside the client, and nothing
was checking the claim: **500 of 947 paths, 541 of 1,021 operations**, 482 undeclared across 165 of 203
path groups. **Filed as "SCHEMA-STALE", and the name was the first thing wrong with it** — stale
implies it was once right, and that file and the `.gitignore` line hiding its input were written in the
SAME commit, with the app carrying *fewer* route decorators today (1016 vs 1022) than when it was
generated. *A stale-sounding name sends the next reader at the one action that cannot fix it*, which
here was re-running the generator: `gen:api-types` read `src/api/openapi.json`, **a gitignored file**,
so it regenerated from whatever dump sat on that machine, printed a green tick and wrote the same file.
*"Regenerate the types" did not mean "read the server", and nothing said so* — the generator now dumps
from `aec_api.main:app` into a temp file it deletes, so there is no persistent input left to be stale.
**The rule is method-level and that is load-bearing:** `openapi-typescript` emits all eight verbs per
path and marks the unserved ones `?: never`, so a path-level check passes a file declaring all 947 URLs
and none of their verbs — which is the shape a half-regenerated file actually has. Both arms are
mutation-proved, and the parser REFUSES a group it cannot classify rather than narrowing, because a
silent narrowing reports that path as missing everything, reading as "regeneration due" instead of "the
parser no longer understands this file".
**The reason it went unseen is worth more than the fix: SEVEN audits here exempt this file by name**
(`deadFieldScope`, `docComments`, `unfiledMap`, `deadFieldTyped`, `noRespelledShapes`,
`test_route_reachability`, `test_file_sizes`), each for a good reason of its own. *Seven exemptions and
no owner is how an artifact stops being checked by anybody.* And `test_route_reachability`'s is the
opposite error, still instructive: it once counted this file as CLIENT code, so **29 routes were
"called" by a generated file restating the server's own route table.** It is only ever a claim about
the SERVER. **The item had also been parked on a decision that was not load-bearing** — commit
`openapi.json`, or produce it in CI? — and needed neither answer, because an input that does not
persist needs no home, and the agreement check needs no node and no artifact crossing jobs. *An item
parked on a decision stays parked until somebody re-derives whether the decision was load-bearing.*
**And the gate's own first draft asked its newest precondition from the wrong place.** The
env-independence scan globbed `Path("src")`, relative to the working directory — which `run_tests.py`
sets to `services/api`, while `ci.yml` invokes `python services/api/run_tests.py` from the repo ROOT.
`rglob` over a missing directory yields nothing and raises nothing, so it would have reported a clean
tree under CI and nowhere else. *A wrong question returns a confident number* — the same failure
`test_scratch_ignored` paid for twice, in a file written days after reading its lesson. **A relative
path is a question about where somebody stood**; it is anchored on `__file__` now, the population
count is returned rather than discarded and floored beside the verdict, and the gate is run from both
directories.
**And its parity check was not a check.** A path group whose shape the group regex cannot match is
absent from the parse *silently*, and is then reported as "served but undeclared" — a parser failure
wearing the costume of a stale file — so the path keys are counted independently. The first draft
counted them with a regex that **fails in exactly the same way the group regex does**, so a
trailing-whitespace mutation broke both derivations and the counts stayed equal. *A parity check
between two derivations that share a failure mode is not a check* — the crude count catches what the
strict one cannot, and both mutations are now folded in and must be refused before the gate reports.

**An eighteenth joined them on 2026-09-25: `services/api/test_verdict_coverage.py`** — does a screen
that reads a verdict about a SUBSET also read what the engine could not evaluate? Five engines in one
session returned a boolean beside a count of the unevaluated, and the boolean was meaningless without
the count: `soft_clash.coordinated` over a page of the matrix (CLASH-TRUNC), `rent_scrub.clean` =
`bool(ran) and not failed` with 1 of 7 checks run, `t12.tie_out.reconciles` against a reference derived
from the answer, `covenants.clean`/`at_risk` on counts that are zero for want of inputs, and
`sequence_clash.clean` over the activities that happened to have a date and a location. **Every one of
those engines is careful** — each hands the caller the coverage and three attach a sentence saying why.
*The defect is always in the consumer*, and four of the five had none, so nothing was wrong until
somebody wrote one. **A subset verdict is derived structurally, not by name:** a dict key whose value
is `bool(A) and (all(…) | not B)`, the guard being the author writing down that the subset can be
empty. Matching coverage-ish WORDS instead returned 24 candidates of mostly unrelated senses — a
sprinkler's coverage area, a `dry_run` flag — and *a rule that needs an exemption list is a rule whose
population is wrong.* The structural form returns 5 with no exemptions, and **three of the five were
instances nobody had found by hand.**
**Four more drafts were wrong in ways only measurement showed.** Linking consumers by shared
VOCABULARY cross-talked — a net-effective-rent card was reported as a `sequence_clash` consumer on
`skipped`, `findings` — so the link is the call site of the owning client method, the only thing that
claims *this file reads THIS response*. Treating `not_` as a coverage prefix matched `not_covered`, a
prose note about a **different dimension**, which *would have blessed the one consumer the rule exists
to catch*. Detecting a read as `.name` missed `const { not_applicable: notRun } = …` and called a
careful card bare; widening to a bare word boundary then matched the English word "skipped" inside an
unrelated prose string in the same file. *A detector keyed on one spelling cannot see the others, and
one keyed on none sees everything.*
**And mutating it found the limit it does not cover:** wrapping the coverage render in `if (false)`
leaves the text in place and PASSES. It is a source-level check — the same bargain
`test_route_reachability` makes for URL literals — and the complement is the consumer's own test,
where `apps/web/src/proforma/rentRollQuality.test.ts` and
`apps/web/src/proforma/covenantCard.test.ts` assert the coverage is *rendered*. Written down rather
than discovered later, because a gate whose reach is assumed is the thing these eighteen entries are
about.

"Cite a gate only after `git ls-files` confirms it" is itself a rule held as prose, so it is now
`services/api/test_claude_md_gates.py`: every backticked code file named here, in
`docs/roadmap-directions.md` **and in `docs/roadmap.md`** must resolve to a tracked path — including
Expand Down Expand Up @@ -490,7 +567,7 @@ never contingent on the npm question.

**What an agent working in `apps/web/src/viewer` should know.** **Seventy-nine** commits have touched that
directory since extraction began on 2026-08-06, and `apps/web/src/viewer/app.ts` has gone from 5,064 lines to
**2,442** — largely R39-DECOMP-VIEWER, which is the same decomposition the extraction plan asks for and is being
**2,410** — largely R39-DECOMP-VIEWER, which is the same decomposition the extraction plan asks for and is being
done here first. That is good and it is also divergence: every one of those commits is a change the swap will
have to reconcile. So:

Expand All @@ -513,6 +590,19 @@ have to reconcile. So:
The `wc -l` in the re-measure command above was also bare and would have waited on stdin — an instruction to
verify that hangs is an instruction nobody runs twice.)*

*(**2,442 → 2,410 on 2026-10-12**, BATCH-DARK: the "Script this" window left for
`apps/web/src/viewer/tools/scriptThis.ts`, and this line moved in the same edit because
`services/api/test_claude_md_gates.py` refuses the build otherwise — which is the lesson above being
enforced rather than remembered. **And the commit count above was deliberately NOT re-measured, which
is the more useful half.** `git log --since=2026-08-06 -- apps/web/src/viewer | wc -l` returns **24**
in this cloud container, against the seventy-nine stated — because the checkout is **shallow** (two
grafts, 123 commits, oldest dated 2026-09-11), so `--since` cannot see past the graft and the small
number is an artefact of clone depth rather than a correction. *A re-measure that depends on the depth
of somebody's clone is not a measurement*, which is the same thing `services/api/test_gap_records.py`
had to learn when its replay used `git show` and failed only in CI. Check with
`git rev-list --count HEAD` and `test -f .git/shallow` before believing a history count here; the LINE
count is safe because `wc -l` reads the working tree.)*

- **Keep shipping.** Blocking this roadmap for the extraction would make the extraction expensive and it would
die. Landing viewer work here is the correct default.
- **Prefer changes that survive the swap** — new behaviour behind the existing seams, rather than new coupling
Expand Down
7 changes: 7 additions & 0 deletions apps/web/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,9 @@
*.local
# Vestigial, deliberately kept. `gen:api-types` no longer writes here — it dumps the spec into the OS
# temp directory and deletes it, because an input that persists is an input that can be stale, and this
# very line is what hid that: the generator's one documented invocation read a file git was told to
# ignore, so "regenerate the types" silently meant "re-read whatever dump is on this machine". See
# apps/web/scripts/gen-api-types.mjs and services/api/test_schema_types_agree.py. The pattern stays so
# that a leftover dump from before 2026-09-25 does not surface as untracked work in every lane's
# `git status` — the residue problem services/api/test_scratch_ignored.py exists for.
src/api/openapi.json
3 changes: 3 additions & 0 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@
<input id="convert-input" type="file" accept=".rvt,.dwg,.nwc" hidden />
<input id="ref-input" type="file" accept=".obj,.stl,.ply,.gltf,.glb,.pcd,.xyz,.las,.laz,.e57,.geojson,.json,.tif,.tiff,.gml,.citygml,.splat,.ksplat" hidden />
<div id="workspaces" role="tablist" aria-label="Workspace"></div>
<!-- App-level, and OUTSIDE the tablist: one live subscription, not one per room, and a
focusable control inside a role="tab" button is invalid in both HTML and ARIA. -->
<div id="live-refused" hidden></div>
<div id="next-action" hidden></div>
<span class="grow"></span>
<label id="persona-wrap" title="Gear the workspace to your role — every screen leads with what your job needs (all data stays reachable)">
Expand Down
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"cap:sync": "cap sync",
"mobile:android": "npm run build:mobile && cap sync android && cap open android",
"mobile:ios": "npm run build:mobile && cap sync ios && cap open ios",
"gen:api-types": "openapi-typescript src/api/openapi.json -o src/api/schema.d.ts"
"gen:api-types": "node scripts/gen-api-types.mjs"
},
"dependencies": {
"@mkkellogg/gaussian-splats-3d": "^0.4.7",
Expand Down
64 changes: 63 additions & 1 deletion apps/web/scripts/bundle-budget.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,69 @@ const DIST_ROOT = join(process.cwd(), "dist");
const DIST = join(DIST_ROOT, "assets");
// Budget in KB (Brotli). Set generously above the current shell so it catches regressions, not noise.
// Bump this deliberately (with a note) when the shell legitimately grows.
const BUDGET_KB = Number(process.env.BUNDLE_BUDGET_KB || 220);
//
// 220 -> 224 on 2026-10-12, and the measurement is the point. The shell hit **220.4 KB** and the
// budget had never moved since it was written, so "generously above the current shell" had quietly
// become a ceiling the shell was touching — the next change to land anywhere eager was going to pay
// for everyone's growth. CODE-READY-DARK tipped it: `Web typecheck + test + build` was green through
// b57490e7 and red on the next push, whose only bundle-affecting commit was that one.
//
// WHAT IT COST, measured per feature rather than assumed, by grepping the built chunks for a string
// only each module emits:
//
// viewer/tools/* -> app-*.js EAGER SHELL (macroTool, codeReadiness, mepSection)
// portal/panels/* -> jurisdictionPanel-*.js lazy
// reportCenter + its helpers-> reportCenter-*.js lazy
//
// So of four features shipped that day, two cost shell bytes and two cost none, and nothing told
// anybody which — a tool added to the viewer rail is downloaded by every first-time visitor before
// anything is interactive, while the same amount of code in a portal panel is not. **That asymmetry
// is the useful finding here, not the number.** The honest alternative to this bump is to lazy-load
// a tool's body behind its click, which is the right shape and is a pattern no tool in that
// directory uses today (measured: zero `await import(` under `viewer/tools/`); applying it to two
// of some forty tools would buy 0.4 KB and leave an inconsistency for the next reader to explain.
//
// **That last measurement went out of date the same day, and in the direction that flatters the
// bump.** Re-measured 2026-10-12: FOUR files under `viewer/tools/` reach a module through
// `await import(` in code — `analyseSection`, `modelReviewPanel`, `projectPanel`, `qaSection`, five
// call sites between them — every one of them added by this gate refusing a build, not chosen. The
// last one was refused TWICE: deferring only the rendering left the shell **five bytes** over, so
// the fetch, the toast, the status line and the `showResult` call went behind the click as well.
// So the alternative this paragraph declined on grounds of inconsistency is now what the newest
// tools do, and the next legitimate growth should pay for itself the same way before this number
// moves again.
//
// *A number that decays toward the conclusion it supports is the hardest kind to notice, because
// nothing it predicts ever looks wrong.* Re-derive it, never read it — and count CODE, not
// comments, because two of the six files that match the string only discuss it:
// grep -c "await import(" apps/web/src/viewer/tools/*.ts
//
// **224 -> 225 on 2026-10-12, later the same day, and the measurement is again the point.**
// FIRM-RULES-OPTIONAL slice (2) wired four routes that had no client method between them --
// `GET`/`PUT /projects/{pid}/rules` and `GET`/`PUT /firm/rules` -- so the parametric rule library
// every check in the product evaluates could finally be AUTHORED from the product. Measured: the
// shell went 223.8 -> **224.03 KB, which is 229,407 brotli bytes against a 229,376 limit: 31 over**
// -- and all of it is the four client methods. The figure is given in BYTES because the display
// rounds to one decimal and "224.0 KB" is what both a pass and a 31-byte failure look like; this
// file's own history is a chain of numbers that rotted, so the one that decides a build is written
// in the unit the decision is made in. The editor itself,
// `portal/panels/ruleStandards.ts` at 310 lines, cost ZERO shell bytes, because `portal/panels/*`
// is lazily imported by construction. That is the same asymmetry the paragraph above measures,
// seen from the other side: the UI was free and the four fetch wrappers were not.
//
// **What was tried before bumping, and why it was rejected.** The `ApiClient` mixins are composed
// eagerly, so a method on one is shell bytes by construction; the only way to defer these four
// would be to expose a generic request helper for a lazy panel to build its own URLs on -- which
// is more eager bytes, and it would put URL construction outside the one place
// `services/api/test_route_reachability.py` and `apps/web/src/api/clientCallers.test.ts` both look
// for it. *Contorting the API surface to fit a byte budget costs more than the byte budget is
// worth*, and a client method per route is the shape two gates depend on.
//
// **The bump is 1 KB, not a round-up to a comfortable number.** 220 -> 224 was four, and what that
// bought was four releases of nobody deciding anything; this leaves ~0.9 KB, which is roughly one
// more feature's worth of client methods. The next thing to land eager should pay for itself by
// deferring, the way the three tools above now do.
const BUDGET_KB = Number(process.env.BUNDLE_BUDGET_KB || 225);

// The eager shell = exactly what index.html loads up front: the entry chunk + its CSS, plus the split
// first-party `app-*` chunk the entry statically imports. Everything else is a lazy chunk that doesn't
Expand Down
Loading
Loading