feat(legibility): make understandability a check that fails a diff - #381
Conversation
A product-clarity document already existed and did not change behavior: the verticals still shipped empty states with no next action, engineering vocabulary in error copy, and built screens no navigation reached. This ships the same intent as five static checks a product runs against its own source, alongside the canonical patterns and the provenance primitive the rubric depends on. Each check names file, line and column with the fix, and is suppressible only with a written reason — a reasonless suppression is itself a finding. Calibration was the work: 317 findings across two real products became 121 by removing hand-inspected false-positive classes, not by tuning thresholds. Provenance ships as a shared primitive whose basis is required, so a value cannot silently claim a document it does not have.
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 202e239d
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-08-04T09:09:25Z
tangletools
left a comment
There was a problem hiding this comment.
⚪ Value Audit — audit-incomplete
| Verdict | audit-incomplete |
| Concerns | 0 (none) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 90.0s (2 bridge agents) |
| Total | 90.0s |
💰 Value — error
value agent produced no parseable value-audit JSON.
- Model: opencode/deepseek/deepseek-v4-pro
- Bridge attempts: 3
- Bridge error: opencode/kimi-for-coding/k2p7: Bridge returned 503: {"error":{"message":"cli-bridge admission timed out after 30000ms","type":"admission_rejected","reason":"queue_timeout","admission":{"active":20,"queued":5,"maxActive":20,"maxQueue":48}}}; opencode/zai-coding-plan/glm-5.2: Bridge returned 503: {"error":{"message":"cli-bridge admission timed out after 30000ms","type":"admission_rejected","reason":
🎯 Usefulness — error
usefulness agent produced no parseable value-audit JSON.
- Model: opencode/deepseek/deepseek-v4-pro
- Bridge attempts: 3
- Bridge error: opencode/zai-coding-plan/glm-5.2: Bridge returned 503: {"error":{"message":"cli-bridge admission timed out after 30000ms","type":"admission_rejected","reason":"queue_timeout","admission":{"active":20,"queued":6,"maxActive":20,"maxQueue":48}}}; opencode/kimi-for-coding/k2p7: Bridge returned 503: {"error":{"message":"cli-bridge admission timed out after 30000ms","type":"admission_rejected","reason":
No PR concerns were produced because the value/usefulness agent pass did not complete. Treat this audit as incomplete, not as approval.
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
|
tangletools
left a comment
There was a problem hiding this comment.
✅ Auto-approved drewstone PR — 202e239d
This PR was opened by the trusted drewstone account.
The full PR reviewer audit still runs separately and will publish findings if it detects issues.
tangletools · auto-approval · reason: drewstone_author · 2026-08-04T11:27:14Z
tangletools
left a comment
There was a problem hiding this comment.
🟢 Value Audit — sound
| Verdict | sound |
| Concerns | 1 (1 weak-concern) |
| Heuristic | 0.0s |
| Duplication | 0.0s |
| Interrogation | 170.3s (2 bridge agents) |
| Total | 170.3s |
💰 Value — sound
Ships a new /legibility CI gate (5 static checks over a product's own TS/TSX source, mirroring /peer-floors//theme-contract) plus a distinct value-level ProvenanceValue affordance and three pinned docs — turns 'a well-written doc changed nothing' into 'a diff fails'. In grain, calibrated, no
- What it does: Adds a new node-only
/legibilitysubpath:checkLegibility()+agent-app-legibility-checkbin with five static checks over a consumer's own source —engineering-vocabulary(codebase words on screen),dead-end-empty-state(empty branch with no button/link/control in its subtree),unchecked-success(success signal in a handler that never reads res.ok/.status),silent-failure(catch arou - Goals it achieves: Make product understandability mechanically enforceable.
docs/product-surfaces.mdalready argued all of this (opened with a 2–4/10 clarity audit) and changed nothing: verticals then shipped 11-of-21 empty states with no next action, 'materialized' in error copy, a Settings save reporting 'Saved' on a 404, and an island of three contract screens no nav reached. The gate turns five of those defect - Assessment: Strong, in the grain of the codebase. (1) The shape claim is correct and verified:
/legibilityis a structural clone of/peer-floorsand/theme-contract— node-only, runs over the CONSUMER's own source, exit codes 0/1/2, JSON option, zero runtime deps, same bin shape, same wiring. (2) Additive-subpath rule satisfied (tsup + package.json exports + knip + bin all present). (3) Calibration is m - Better / existing approach: none — this is the right approach. Searched for existing equivalents:
git ls-files src | rg -i 'lint|vocab|banned'returns only the newsrc/legibility/checks/vocabulary.ts; no prior static checker for any of the five defect classes exists in this repo.git ls-files src/web-react | rg -i 'provenance|source-marker|lineage'returns only the new files plus the run-levelProvenanceStampin `wor - Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 2
- Bridge warning: opencode/kimi-for-coding/k2p7: opencode: opencode error
🎯 Usefulness — sound
A coherent, well-integrated understandability gate that follows the codebase's established node-only-CI-checker pattern exactly, with five checks each calibrated against measured production defects and a suppression design that keeps the gate alive.
- Integration: Fully wired and reachable:
package.jsonexports./legibility(package.json:464) + binagent-app-legibility-check(package.json:36);tsup.config.tsentries (tsup.config.ts:89-90);knip.jsonentries (knip.json:36-37); AGENTS.md module-map row added; docs generated (docs/api/legibility.md, docs/api/legibility-cli.md). It is a new additive subpath — no root barrel touched, no existing export - Fit with existing patterns: Textbook fit. It is explicitly 'the
/theme-contractshape applied to meaning instead of tokens' (src/legibility/index.ts:16-18), and the structural parallel is exact: node-onlyfsmechanism,checkX(config) -> {ok, findings}API, exit 0/1/2 bin contract, consumer-declared config file (legibility.config.mjs), per-line suppression. No competing pattern exists in the package — grep finds no ot - Real-world viability: Hardened against the one failure mode that kills this class of gate — false positives. The vocabulary check reads only lexer-PROVEN copy positions (JSX text + allowlisted copy attributes + COPY_CALLS first arg), with object-key copy opt-in because it 'was MEASURED as the false-positive tier' (src/legibility/checks/vocabulary.ts:34-41). The silent-failure check walks a fluent
.catchchain BACKWAR - Model: opencode/zai-coding-plan/glm-5.2
- Bridge attempts: 1
💰 Value Audit
🟡 One PR ships two subpaths with different peer floors [proportion] ``
The diff adds both a node-only gate (
/legibility, zero peers,src/legibility/*) and a React affordance (/web-reactprovenance, react peer,src/web-react/provenance*.ts(x)). They are tied by the rubric (Q3 needs the primitive to exist) and the commit message names the tie, but they have different consumers and different peer floors — a product may want the gate without the React surface or vice-versa. A stricter split would befeat(legibility): the gatethen `feat(web-react): Provenance
What this audit checks
It judges the change on its merits — not whether it was tasked out in an issue. Unticketed, fast-moving work is fine; the question is whether the change is good and whether a better or existing approach should be used instead.
| Pass | What it asks |
|---|---|
| Heuristic | Vague title? Whitespace-only or cruft-bearing diff? (content signals only) |
| Duplication | Do added function/class names already exist elsewhere in the repo? |
| Value Audit | What does it do? What goal does it achieve? Is it good? Better architecture or already-exists? |
| Usefulness Audit | Does it integrate and fit? Will it hold up in real use and actually get used? |
Findings are concerns, not blocks — the human reviewer decides what to do with them.
❌ Needs Work —
|
| glm | deepseek | deepseek-flash | aggregate | |
|---|---|---|---|---|
| Readiness | 0 | 0 | 0 | 0 |
| Confidence | 95 | 95 | 95 | 95 |
| Correctness | 0 | 0 | 0 | 0 |
| Security | 0 | 0 | 0 | 0 |
| Testing | 0 | 0 | 0 | 0 |
| Architecture | 0 | 0 | 0 | 0 |
Reviewer score is advisory once the run is complete and the verdict has no blockers.
Full multi-shot audit completed 8/8 planned shots over 43 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 8/8 planned shots over 43 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 8/8 planned shots over 43 changed files. Global verifier still owns final merge decision.
Blocking
🟣 CRITICAL 7 module entries missing from codemap — 174 exports undocumented — docs/codemap.json
The regeneration dropped these module entries entirely: ./documents (37 exports), ./documents/pdf-inspector (4), ./openui (35), ./openui-react (8), ./record (33), ./record/drizzle (22), ./web-react/async (35). All 7 are still active in tsup.config.ts entries, package.json exports, and have intact source files on disk. Verified: tsup.config.ts grep returns entries for all 7; package.json exports all 7; source files exist at their expected paths. The codemap is the primary machine-readable API documentation consumed by downstream tooling — missing entries mean these subpaths appear to not exist.
🟣 CRITICAL animate-pulse count assertion fails at HEAD (17 vs 16) — tests/docs/legibility-contract.test.ts
Ran
vitest runat HEAD 202e239:expected 17 to be 16. countUsages('animate-pulse').total walks all of src/ and counts 17 occurrences; statedCount('animate-pulse') parses '16 times' from docs/product-surfaces.md Pattern 4. The missing 17th is src/web-react/record-grid.tsx:512 (className="h-8 animate-pulse rounded-md bg-muted/50"), a real shipped loading-skeleton row added in #380 and omitted from Pattern 4's cited-file list. This is precisely the 'count drifts the moment someone adds a usage' failure the test's header comment names — the test caught it in its own companion doc. Impact: CI red; the new test file cannot ship green. Fix (in docs/product-surfaces.md, out of this shot's scope): bump 16→17 and add record-grid.tsx to the cited pulse-file list, or reconcile both sides.
🟣 CRITICAL animate-spin count assertion fails at HEAD (6 vs 5) — tests/docs/legibility-contract.test.ts
Ran
vitest runat HEAD 202e239:expected 6 to be 5. The test parses the doc's word 'Five' → 5 (via the {Three..Eight} map), but src/ contains six realanimate-spinusages across design-canvas-react/components/CanvasInsertPanel.tsx, web-react/mission-activity.tsx, web-react/index.tsx, web-react/chat-composer.tsx, web-react/work-product.tsx, and web-react/async/async-view.tsx. The doc's 'Fiveanimate-spinuses' undercounts by one. Impact: CI red alongside the pulse failure. Fix (in docs/product-surfaces.md): change 'Five' → 'Six'.
🔴 HIGH ATTACHMENT_ACCEPT value wrong in web-react API docs — docs/api/web-react.md
The doc shows the value as
"image/*,.pdf,.txt,.md,.csv,.json,.yaml,.yml,.html"but the actual source atsrc/chat-routes/attachment-validation.ts:54is'image/*,.pdf,.docx,.xlsx,.pptx,.txt,.md,.csv,.json,.yaml,.yml,.html'. The .docx, .xlsx, and .pptx extensions are missing from the doc. This constant is re-exported throughsrc/web-react/use-composer-attachments.ts:50. A consumer reading the docs would conclude Office files are not accepted by the upload gate, which is incorrect.
🔴 HIGH docs/api/web-react.md is stale — fails the codemap-fresh contract test — docs/api/web-react.md
Committed file says '297 exports' but
agent-docs --check(run from PR head 202e239) reports web-react.md as changed and regenerating produces '343 exports'. I verified this directly:node node_modules/@tangle-network/agent-docs/dist/cli.js --repo . --checkexits non-zero with 'changed: docs/api/web-react.md'. The merge commit 202e239 merged main (d316e9d, carrying record-grid + Office-attachment source) into the legibility branch but did not regenerate docs. Concretely missing from the committed file: (1) all 46 RecordGrid* / recordGrid* / with(out)RecordGrid* / validateRecordGrid* / sumRecordGrid* / sameRecordGrid* exports — still public via src/web-react/index.tsx:53export * from './record-grid'-> record-grid.tsx:69export * from './record-grid-model'; (2) the ATTACHMENT_ACCEPT
🔴 HIGH web-react API docs missing all RecordGrid exports — docs/api/web-react.md
The regenerated web-react.md dropped ~44 RecordGrid exports (RecordGrid, RecordGridColumn, RecordGridRow, recordGridOk, recordGridFail, etc.) despite the source still exporting them via
export * from './record-grid'at src/web-react/index.tsx:53. The old doc had 91 references to RecordGrid/recordGrid; the new doc has 0. Source files (src/web-react/record-grid.tsx,src/web-react/record-grid-model.ts) are unchanged in this PR. This makes consumers think these shared components are unavailable. Impact: any consumer reading the docs to find the shared record grid will conclude it doesn't exist.
🔴 HIGH web-react.md dropped the live RecordGrid API section; the repo's doc-freshness gate fails — docs/api/web-react.md
This PR's regeneration removed all 46 RecordGrid*/recordGrid* entries (RecordGrid, RecordGridProps, withRecordGridUpdate, parseRecordGridInput, sumRecordGridColumn, EMPTY_RECORD_GRID_OVERLAY, …) and changed the header from '315 exports.' to '297 exports.', but the source API is unchanged: src/web-react/index.tsx:53 still does
export * from './record-grid'(record-grid.tsx:69 re-exports record-grid-model.ts), and neither record-grid file is touched in this PR. So docs/api/web-react.md now under-documents a shipped, public API of@tangle-network/agent-app/web-reactand states a wrong export count. Ground-truth:node node_modules/@tangle-network/agent-docs/dist/cli.js --repo . --checkexits 1 ('docs are STALE'), and a freshagent-docsrun produces 343 exports for web-react.md, restori
🔴 HIGH 45 RecordGrid exports dropped from ./web-react codemap entry — docs/codemap.json
The head codemap has 297 web-react exports vs 315 on base. 45 RecordGrid-prefixed exports (RecordGrid, RecordGridBooleanColumn, RecordGridCellChange, recordGridOk, etc.) are gone, but src/web-react/index.tsx line 53 still has
export * from './record-grid'and src/web-react/record-grid.tsx exists. Runningtsc --noEmit src/web-react/record-grid.tsxshows 96 JSX-related errors (missing --jsx flag), suggesting the codemap generator fails on these files under its compilation environment and silently drops their exports.
🔴 HIGH Committed codemap.json is stale — fails the repo's own codemap-fresh CI gate — docs/codemap.json
Ran
agent-docs --repo <root> --check(the exact command in tests/codemap-fresh.test.ts:24): exit 1,docs are STALE, listing docs/codemap.json as changed. The committed file documents 83 entries; the package actually ships 90 (tsup.config.ts:5-94 declares 90 entry points, all with present source files and package.jsonexports). 7 real subpaths were dropped entirely: ./documents, ./documents/pdf-inspector, ./openui, ./openui-react, ./record, ./record/drizzle, ./web-react/async. Each still has its source file (src/documents/index.ts, src/record/index.ts, src/openui/index.ts, src/web-react/async/index.ts, etc.), is built by tsup.config.ts, and is exported in package.json. Regeneration (agent-docs) writes 90 entries and restores all 7 with nothing removed. Impact: tests/codemap-fresh.te
🔴 HIGH Merge dropped 7 subpaths from codemap.json; staleness gate fails — docs/codemap.json
Entry-id sets: base d316e9d 88 entries, head 202e239 83 entries. Missing at head: ./documents, ./documents/pdf-inspector, ./openui, ./openui-react, ./record, ./record/drizzle, ./web-react/async — all added to the codemap by main #380 (1b771e1) and all still present in head source (ls src/documents src/openui src/record src/web-react/async confirms). The merge commit 202e239 resolved docs/codemap.json to the legibility branch's version (git diff d76dde5 202e239 -- docs/codemap.json is empty), discarding main's additions. Ground truth: running the same generator (agent-docs 0.2.1, unchanged base→head) via
node node_modules/@tangle-network/agent-docs/dist/cli.js --repo . --checkexits non-zero and prints 'docs are STALE ... changed: docs/codemap.json'; regenerating writes back all 7 entries
🔴 HIGH Auto-generated docs missing 7 active subpath sections after merge — docs/llms-full.txt
The docs at HEAD have 83 H2 sections claiming '83 entries' but tsup.config.ts has 90 entries. Seven tsup entries present in tsup.config and package.json exports are absent from docs: documents/index, documents/pdf-inspector, openui/index, openui-react/index, record/index, record/drizzle, web-react/async/index. Source files exist at HEAD with exports (verified: src/documents/index.ts has 4 export blocks, src/openui/index.ts has 4, src/record/index.ts exports model/fold/ulid). Root cause: docs were generated at commit d76dde5 (83 entries) and not re-generated after merge 202e239 restored 7 entries. Impact: consumers reading this file for API reference find no documentation for 7 active subpaths.
🔴 HIGH Merge dropped 7 subpath sections from the generated full-API reference — docs/llms-full.txt
Head tsup.config.ts declares 90 entries including documents/index, documents/pdf-inspector, openui/index, openui-react/index, record/index, record/drizzle, web-react/async/index (all with matching head source under src/). Head docs/llms-full.txt has 83 sections and omits exactly those 7 (./documents, ./documents/pdf-inspector, ./openui, ./openui-react, ./record, ./record/drizzle, ./web-react/async); base d316e9d documented all 7 (verified: 'extractDocument', 'parseOpenUISegments', 'foldRecordEntries', 'useAsyncResource' each appear once in base docs, zero times in head docs). The header at line 5 claims '83 entries, every export with its signature' while tsup.config.ts has 90
🔴 HIGH Doc's measured counts are stale at the merged head; the PR's own enforcement test fails — docs/product-surfaces.md
Pattern 4 states
animate-pulseappears 16 times (line 447) and Fiveanimate-spinuses (line 449). Verified against the merged tree: 17 animate-pulse occurrences across 8 source files and 6 animate-spin occurrences across 6 files. The merge commit 202e239 (chore: merge main) introduced src/web-react/record-grid.tsx (animate-pulse loading skeleton at :512) and src/web-react/async/async-view.tsx (animate-spin at :110), both absent at d76dde5 where the counts were genuinely 16/5.npx vitest run tests/docs/legibility-contract.test.tsfai
🔴 HIGH Pattern 4 animate-pulse count is 16, source has 17 — PR's own contract test fails — docs/product-surfaces.md
Doc states: "
animate-pulseappears 16 times". The PR's own contract test tests/docs/legibility-contract.test.ts:242 re-counts substring occurrences across src/** (excluding tests/.d.ts) and asserts equality; it reads 17. Reproduced:vitest run tests/docs/legibility-contract.test.ts->expected 17 to be 16. The 17 occurrences live in AssistantPanel.tsx(1), VaultPane.tsx(5), mission-activity.tsx(1), result-canvas.tsx(1), session-history.tsx(3), web-react/index.tsx(3), record-grid.tsx(1), message-attachments.tsx(2 — one is a code comment that the substring counter includes). Impact: CI red on a docs-only PR; the doc's central promise ('a measured count in this document that no longer matches this package's source' fails the build) is violated by the doc itself. Fix: change '**16 tim
🔴 HIGH Pattern 4 animate-spin count is 'Five', source has 6 — contract test fails — docs/product-surfaces.md
Doc states: "Five
animate-spinuses". The PR's own contract test tests/docs/legibility-contract.test.ts:255-262 parses the word before 'animate-spinuses' into a number and asserts equality with the source count; it reads 6. Reproduced:vitest run tests/docs/legibility-contract.test.ts->expected 6 to be 5. The 6 occurrences: design-canvas-react/components/CanvasInsertPanel.tsx, web-react/mission-activity.tsx, web-react/index.tsx, web-react/chat-composer.tsx, web-react/work-product.tsx, web-react/async/async-view.tsx (none are comments, so this is a genuine miscount, not a counting-method disagreement). Impact: same as above — CI red. Fix: change 'Five' to 'Six'.
🔴 HIGH PR ships a failing test — doc counts stale vs src at head — tests/docs/legibility-contract.test.ts
npx vitest run tests/docs/legibility-contract.test.tsat head 202e239 fails 2/34. Test 242: doc statesanimate-pulseappears 16 times (docs/product-surfaces.md:447) but countUsages() measures 17 in src (extra occurrence: src/web-react/message-attachments.tsx:11, a comment). Test 261: doc states 'Fiveanimate-spinuses' (product-surfaces.md:449) but src has 6 (src/web-react/index.tsx:1167, src/web-react/work-product.tsx:248, src/design-canvas-react/components/CanvasInsertPanel.tsx:110, src/web-react/mission-activity.tsx:432, src/web-react/chat-composer.tsx:398, src/web-react/async/async-view.tsx:110). All occurrences pre-exist at base d316e9d (verified by git show), so the counts were wrong when the doc/test were written in this same PR. vitest include pattern `tests/**/*.test.{ts
🔴 HIGH Test ships with 2 failing assertions — doc counts don't match source — tests/docs/legibility-contract.test.ts
The test asserts that the animate-pulse count in pattern 4 (stated: 16) matches the source. Actual count is 17 (1 in record-grid.tsx not listed in the doc, 1 in a JSDoc comment in message-attachments.tsx:11). The document at line 447 names 7 files but misses src/web-react/record-grid.tsx which also carries animate-pulse. Either the document count and file list must be updated, or the test expectation must be set to match the current document (meaning the test won't catch future drift).
🔴 HIGH Test ships with failing animate-spin count assertion — tests/docs/legibility-contract.test.ts
Document line 449 says 'Five animate-spin uses' but src has 6 occurrences (work-product.tsx, index.tsx, async-view.tsx, chat-composer.tsx, mission-activity.tsx, and CanvasInsertPanel.tsx). The CanvasInsertPanel.tsx usage at line 110 is not accounted for in the document's count. The test correctly surfaces this drift, but the test should not land red — either the document count must be updated to 'Six' or the source usage must be removed.
Other
🟠 MEDIUM Export count changed from 315 to 297 — net loss larger than additions — docs/api/web-react.md
The doc header went from '315 exports' to '297 exports' (net -18). The PR added ~33 provenance exports but dropped ~44 RecordGrid exports and misgenerated ATTACHMENT_ACCEPT. The net effect is an undocumented API surface regression: the only intentional code change was
export * from './provenance'added at src/web-react/index.tsx:49. The RecordGrid drops and ATTACHMENT_ACCEPT value change are agent-docs tool regressions, not intended removals.
🟠 MEDIUM ./tools dependsOn dropped the openui dependency it still has — docs/codemap.json
Committed
./toolsentry (docs/codemap.json:13269-13272) lists dependsOn as ["crypto","eval"], but src/tools/openai.ts:1 still importsOPENUI_INTERACTIVE_AUTHORING_GUIDE from '../openui/authoring'(and src/tools/types.ts:58 references it), so the real dependency edge to ./openui remains. The regenerated codemap correctly reports dependsOn ["crypto","eval","openui"]. This is a second symptom of the same stale regeneration; it understates the module graph a reviewer or dependency-aware tool would read. Fixed by the samepnpm docs:genpass.
🟠 MEDIUM ATTACHMENT_ACCEPT signature truncated — docs/codemap.json
Source at src/chat-routes/attachment-validation.ts has
'image/*,.pdf,.docx,.xlsx,.pptx,.txt,.md,.csv,.json,.yaml,.yml,.html'. The head codemap shows'image/*,.pdf,.txt,.md,.csv,.json,.yaml,.yml,.html'— .docx,.xlsx,.pptx are missing. The base codemap had the full value. This is not a source change — the source line is byte-unchanged from base — it's a generator extraction bug.
🟠 MEDIUM chat-routes entry misdocuments the attachment allow-list and Office mime exports — docs/codemap.json
The committed chat-routes entry shows ATTACHMENT_ACCEPT = "image/*,.pdf,.txt,.md,.csv,.json,.yaml,.yml,.html" and omits OOXML_SNIFFED_MIMES, MACRO_ENABLED_OOXML_SNIFFED_MIMES, OOXML_WORD_MIME, OOXML_SPREADSHEET_MIME, OOXML_PRESENTATION_MIME and their macro-enabled variants. Head source still ships the post-#380 surface: src/chat-routes/attachment-validation.ts:54-55 defines ATTACHMENT_ACCEPT including '.docx,.xlsx,.pptx' and :61-77 export OOXML_SNIFFED_MIMES / MACRO_ENABLED_OOXML_SNIFFED_MIMES / the six OOXML mime consts. Regenerating at head restores the exact signatures the base codemap carried. Same root cause as the dropped modules (stale merge of a generated file), but a distinct consumer-facing symptom: any codemap/CODEMAP.md reader concludes Office packages are not admitted and that
🟠 MEDIUM Stale ATTACHMENT_ACCEPT and missing OOXML constants after merge — docs/llms-full.txt
ATTACHMENT_ACCEPT shows '"image/,.pdf,.txt,.md,.csv,.json,.yaml,.yml,.html"' but source at HEAD shows '"image/,.pdf,.docx,.xlsx,.pptx,.txt,.md,.csv,.json,.yaml,.yml,.html"'. Merge commit 202e239 restored .docx/.xlsx/.pptx via a
git merge mainthat brought back the OOXML attachment support from main. The docs also show 0 OOXML references while source has 36 in binary-sniff.ts + 23 in attachment-validation.ts = 59. Both OOXML_SNIFFED_MIMES and the 6 individual mime constants (OOXML_WORD_MIME, etc.) exported viasrc/chat-routes/index.ts(line 11:export * from './binary-sniff') are absent. Impact: developer reading docs would believe only image/pdf/text are accepted, but
🟠 MEDIUM chat-routes section stale: OOXML_* exports and ATTACHMENT_ACCEPT value reverted to a pre-fork state — docs/llms-full.txt
The ./chat-routes section at head dropped all 8 constants base documented (OOXML_WORD_MIME, OOXML_SPREADSHEET_MIME, OOXML_PRESENTATION_MIME, the three _MACRO_ENABLED_MIME, OOXML_SNIFFED_MIMES, MACRO_ENABLED_OOXML_SNIFFED_MIMES; base lines 2235-2381) and shows ATTACHMENT_ACCEPT as 'image/,.pdf,.txt,.md,.csv,.json,.yaml,.yml,.html'. Head source contradicts both: src/chat-routes/index.ts still does
export * from './binary-sniff'andexport * from './attachment-validation', src/chat-routes/binary-sniff.ts exports all six OOXML mime constants, and src/chat-routes/attachment-validation.ts exports OOXML_SNIFFED_MIMES, MACRO_ENABLED_OOXML_SNIFFED_MIMES and ATTACHMENT_AC
🟠 MEDIUM llms-full.txt is stale: drops 7 live subpaths and 8+ live constants that still exist at HEAD — docs/llms-full.txt
The regenerated doc documents a surface that does not match the HEAD source. (1) MISSING subpath sections:
./documents,./documents/pdf-inspector,./openui,./openui-react,./record,./record/drizzle,./web-react/asyncare all absent from the## \...`heading list at HEAD, yet tsup.config.ts:76-81 still declares every one of these entries (documents/index,documents/pdf-inspector,openui/index,openui-react/index,record/index,record/drizzle,web-react/async/index) and package.jsonexportsstill ships them. (2) MISSING exported constants: the OOXML block (OOXML_WORD_MIME,OOXML_SPREADSHEET_MIME,OOXML_PRESENTATION_MIME,OOXML_*_MACRO_ENABLED_MIME,OOXML_SNIFFED_MIMES,MACRO_ENABLED_OOXML_SNIFFED_MIMES`) was deleted from the doc but is still exp
🟠 MEDIUM 'agent-app-uplift' citations mislabel files that live on this branch, bypassing the contract test's existence check — docs/product-surfaces.md
The doc's own citation convention (lines 276-278) says a bare
src/…path is an in-repo citation checked for existence by tests/docs/legibility-contract.test.ts, while a cross-repo citation 'names its repo first'. Butagent-app-uplift src/web-react/record-grid.tsx(:395, :401),agent-app-uplift src/web-react/async/state.ts(:308, :540),async-view.tsx:19(:312) anduse-confirmed-mutation.ts(:541) are all cited as if on another branch, whilegit ls-tree HEADconfirms all these files exist on this branch at HEAD (they came in via base d316e9d/main). Consequences: (1) inRepoCitations() only matches spans starting withsrc/|tests/|docs/, so these citatio
🟠 MEDIUM animate-pulse count is wrong: doc says 16, source has 17 — docs/product-surfaces.md
Document states 'animate-pulse appears 16 times' but the contract test
tests/docs/legibility-contract.test.ts:242measures 17 non-test occurrences in this commit's src tree (src/assistant/AssistantPanel.tsx:1, src/vault/VaultPane.tsx:5, src/web-react/session-history.tsx:3, src/web-react/mission-activity.tsx:1, src/web-react/index.tsx:3, src/web-react/record-grid.tsx:1, src/web-react/message-attachments.tsx:2, src/studio-react/result-canvas.tsx:1). Either the count in the doc (16) or the count in the source must be updated for the contract test to pass.
🟠 MEDIUM animate-spin count is wrong: doc says 5, source has 6 — docs/product-surfaces.md
Document states 'Five
animate-spinuses' but the contract testtests/docs/legibility-contract.test.ts:261measures 6 non-test occurrences in this commit's src tree (src/design-canvas-react/components/CanvasInsertPanel.tsx:1, src/web-react/mission-activity.tsx:1, src/web-react/index.tsx:1, src/web-react/chat-composer.tsx:1, src/web-react/async/async-view.tsx:1, src/web-react/work-product.tsx:1). The count must be updated to 6 or the extra usage removed for the contract test to pass.
🟠 MEDIUM Missing or unreadable route config silently disables unreachable-capability with a green build — src/legibility/checks/reachability.ts
readScanned (line 453) swallows every error and returns null; when options.routeConfigFile is set but unreadable, or the file lexes to zero route() calls,
declaredis empty and the function returns { findings: [], routeFile: null, routeCount: 0 }. Back in index.ts:138-139, result.routeFile is null so nothing is collected and no error surfaces, while checksRun (index.ts:151) still lists 'unreachable-capability'. Verified by probe: checkReachability({ routeConfigFile: 'does-not-exist.ts' }) → 0 findings, routeFile null. A typo'd--routes src/rotues.tsin CI produces 'legibility OK — 5 check(s)' with the check never having run. This is exactly the class t
🟠 MEDIUM matchingParen duplicated 4× across check files — src/legibility/checks/success.ts
The identical
matchingParen(masked, open)function is copy-pasted in four check files:success.ts:82,reachability.ts:195,silent-failure.ts:304,empty-state.ts:191. All four are byte-identical. If a bug is found (e.g. off-by-one on the return value) or the function needs a change, it would need to be fixed in all four places. Sincesource.tsalready exports shared parser utilities (enclosingBlock,enclosingFunctionBlock), this should live there too.
🟠 MEDIUM Measurement-method mismatch: doc excludes comments, test counts them — tests/docs/legibility-contract.test.ts
countUsages() does readFileSync(file,'utf8').split(needle).length-1, i.e. counts every substring occurrence including comments and strings. The doc's '16 times' is only correct if the comment occurrence at src/web-react/message-attachments.tsx:11 ('the loading skeleton is an inline
animate-pulsespan') is excluded — the remaining 16 real JSX usages match the doc's list exactly (VaultPane 5, AssistantPanel 1, mission-activity 1, session-history 3, index 3, result-canvas 1, record-grid 1, message-attachments:182 1). So fixing only the animate-spin count leaves this assertion red unless the doc is bumped to 17 or countUsages strips comment/string occurrences. The test and doc must share one definition of 'usage'; today they disagree.
🟠 MEDIUM countUsages counts JSDoc comment references alongside actual usages — tests/docs/legibility-contract.test.ts
countUsages (line 219) uses readFileSync(file, 'utf8').split(needle).length - 1 which counts raw string occurrences including JSDoc comments. In message-attachments.tsx:11, the comment 'an inline
animate-pulsespan' is counted as a usage alongside the actual className at line 182. This makes the test count drift from a human countable-usage count permanently. The document author likely counted 1 for this file (the actual className) but the test counts 2. Recommend documenting that the count includes comment/string-lite
🟡 LOW JsonObject doc field contains wrong text — docs/codemap.json
The ./web entry's JsonObject type has doc
'Web-boundary utilities every agent app's routes hand-roll: JSON body parsing + narrowing, request-context extraction...'— this is the module-level description, not the JsonObject type's own doc. Base hadnullfor this field. The generator conflated module-level docs with export-level docs.
🟡 LOW Entry count header inaccurate — docs/llms-full.txt
Docs header says '83 entries, every export with its signature' but tsup.config.ts has 90 entries at HEAD. The count reflects the pre-merge state (commit d76dde5) and was not updated when the merge restored 7 entries.
🟡 LOW Pattern 4 spinner claim understates the fleet after the merge — docs/product-surfaces.md
Beyond the wrong count (Five vs Six), the 'spinner with nothing to say' rejected example omits src/web-react/async/async-view.tsx:110, which is now an animate-spin user on this branch — the same unlabelled-spinner defect the sentence describes. Same root cause as the high finding (merge drift); updating the count and adding the file keeps the rejected example accurate to the merged source.
🟡 LOW reachability doors include path literals that don't resolve to the product — src/legibility/checks/reachability.ts
The fallback at lines 295-298 counts ANY path-shaped literal (
/something/…) as a door for reachability, treating non-route files as always-available. While documented as the generous direction that can only REMOVE findings, a string literal like'/api/members'from a node_modules import or a serialized path in a test fixture could falsely mark a route reachable. The suffix-matching rule mitigates this (the literal's segments must be a suffix of the route's), so the blast radius is narrow.
🟡 LOW CONSOLE_CALL regex supports only one nesting level — src/legibility/checks/silent-failure.ts
The regex
/\bconsole\s*\.\s*[a-z]+\s*\([^()]*(\([^()]*\)[^()]*)*\)/ghandles at most one level of nested parentheses in console call arguments. A call likeconsole.error(formatError(new Error(msg)))would not be fully matched, leaving parts of it visible in the body. This could cause a false positive if the visible content happens to match an error surface pattern. Console calls this deeply nested are unusual in practice.
🟡 LOW carriesError not applied to .catch() handlers — src/legibility/checks/silent-failure.ts
The
carriesErrorescape hatch (checking whether the caught error binding is used in non-console code) is only applied fortry/catchblocks (line 147) but not for.catch()promise handlers (lines 166-187). A.catch((err) => sendToExternalTelemetry(err))pattern wheresendToExternalTelemetrydoes not match anyERROR_SINKpattern would be reported as a silent failure. Mitigation: the product can addsendToExternalTelemetrytoextraErrorSinks.
🟡 LOW INSPECTS_RESPONSE matches body fields and unrelated variables, letting the 'Saved on a 404' defect through — src/legibility/checks/success.ts
INSPECTS_RESPONSE = /.\s*(ok|status|statusText)\b|{[^}\n]\b(ok|status)\b[^}\n]}\s*=/ matches any
.ok/.statusanywhere in the function body. Verified by probe: (a)const body = await fetch(url).then(r => r.json()); if (body.status !== 'ready') return; toast.success('Saved')→ 0 findings, and (b)await fetch(url); if (store.ok) toast.success('Saved')→ 0 findings. The first reads a business field off a parsed body (a 404 body parses fine), the second an unrelated variable — neither inspects the HTTP response, yet both are treated as checked. This is the exact defect class the check exists for (Settings answering 'Saved' to a 404). Fix: constrain the regex to a response-shaped receiver (ares/response/destructured variable bound from the fetch, or a call within the same state
🟡 LOW checksRun reports unreachable-capability as run when no reachability config exists — src/legibility/index.ts
checksRun is LEGIBILITY_CHECKS.filter(enabled) — enabled() returns true by default — but the actual run is gated on
enabled(config, 'unreachable-capability') && config.reachability(line 134). A product that never supplies reachability config sees '5 check(s)' on the OK line and believes the capability was checked; it wasn't. Combined with the route-config finding above, two distinct paths make the report claim a check ran that never executed. Fix: only include a check in checksRun when it will actually execute, or drive checksRun from the same guard.
🟡 LOW srcDirs pointing at a file or unreadable dir yields 0 files and a green build — src/legibility/scan.ts
walkSources catches readdirSync errors and returns []; scanSources therefore reports filesScanned: 0 and checkLegibility returns ok:true (only srcDirs.length===0 throws, index.ts:97). A misconfigured srcDir (a single file, a path outside the repo, a build dir excluded by SKIP_DIRS) silently passes as 'legibility OK — 0 file(s)'. Unlike the route-config case this is visible in the report text, but a CI that greps exit code alone is green. Recommend failing (or at least warning loudly) when a configured srcDir does not exist or contributes zero files.
🟡 LOW walkSources does not guard against symlink cycles — src/legibility/scan.ts
readdirSync with withFileTypes returns Dirent objects whose isDirectory() follows symlinks to directories. A circular symlink inside a srcDir could cause infinite recursion. SKIP_DIRS covers node_modules (where pnpm symlinks live) but not arbitrary symlink cycles. Low risk for a CI tool scanning the product's own source tree. Fix: add entry.isSymbolicLink() check or a visited-set by realpath.
🟡 LOW Regex literal bodies are not masked, inconsistent with the documented contract — src/legibility/source.ts
The ScannedSource.masked field is documented as 'text with every comment, string body and template body replaced by spaces' and structural regexes (CATCH_RE, CALL_RE, IO_IN_TRY, etc.) rely on masked source so patterns inside string literals cannot match. However, regex literals are skipped by skipRegex() WITHOUT calling blank() — the regex body survives in masked. Verified: scanSource('const re = /catch\s*\{/g') leaves 'catch' in masked. Practical impact is low because each check has contextual guards: silent-failure requires tryBlockBefore (no try block before a regex literal), success dedupes by scope.start, empty-state requires .map( inside a real iteration. A theoretical false positive: a regex literal containing 'fetch(' at file top-level plus a success signal elsewhere in the same
🟡 LOW DIRECTIVE_RE lacks word boundary, can match 'legibility-ignore' inside other words — src/legibility/suppress.ts
The regex /legibility-ignore(-file)?\s+([^\n])/ has no leading \b, so a comment like '// please legibility-ignore this section' matches, treats 'this' as a check name, and generates a 'names "this", which is not a check' self-finding. The phrase is specific enough that natural occurrence is rare, and the false positive is self-explanatory. Fix: prepend \b — /\blegibility-ignore(-file)?\s+([^\n])/.
🟡 LOW entered-no-source is 'settled' while extracted-no-source is 'confirm' — unexplained trust asymmetry — src/web-react/provenance-model.ts
provenanceGapsonly emits a no-source gap forbasis === 'extracted'(line 228), andstandingFromBasisstartsenteredatsettled(line 193). So{display:'$X', basis:'entered'}with nosourcesrenders 'Traced' with no stated gap — yetcheckableAgainstsays 'the person who entered it', and with no named person nothing is actually checkable. The parallelextractedshape forcesconfirm. Either the design intends the entering person to be implicit (then say so and pin it with a test), or an un-named person should pro
🟡 LOW provenanceNextMove emits a dead instruction for a confidence-weakened computed value — src/web-react/provenance-model.ts
For a
computedrecord that HAS inputs but whose OWN standing ischeck(e.g.{basis:'computed', confidence:0.7, inputs:[...]}), none of the guards fire and it falls through toreturn generic— 'Open the source and confirm this value before you rely on it.' Verified by executing the real function: returns the generic for that shape. A computed value has no source to open, so this is exactly the dead-instruction defect the function's own docstring (lines 347-352) exists to prevent. The existing guard at line 377 only han
🟡 LOW Dead code: all-loading message condition can never be true — src/web-react/provenance.tsx
const loading = loadingProvenanceSources(record)returns a filtered subset ofrecord.sources, so wheneverloading.length > 0thensources.length > 0necessarily holds —loading.length > 0 && sources.length === 0is always false. The intended general 'Looking up where this came from…' state (a record whose only sources are still resolving, with none ready) never renders; only the per-row 'Looking up …' status does. Harmless today (each loading row carries its own text) but the condition is a logic-vs-intent mismatch that should either be removed or expressed asloading.length > 0 && loading.length === sources.length.
🟡 LOW Module-global openPanels Set is hidden cross-instance coupling — src/web-react/provenance.tsx
const openPanels = new Set() lives at module scope and is shared by every ProvenanceValue instance across every React root in the document. The 'one trail open at a time' behavior is intentional and documented, but a product that legitimately needs two independent provenance surfaces (e.g. a main return panel and a side-by-side comparison drawer) cannot opt out — opening one will close the other even when they are visually unrelated. The Set is also a test-isolation footgun in principle (though testing-library auto-cleanup clears it via the useEffect return, and the in-file tests pass). No action required for this PR; flagging because a per-root React context would be the more compositional shape if multi-surface products ever appear.
🟡 LOW Unvalidated source.href scheme in the open-source anchor — src/web-react/provenance.tsx
<a href={source.href} target="_blank" rel="noreferrer">renders the caller/model-supplied URL with no scheme allowlist. React escapes text content but not URL schemes, so ajavascript:ordata:href becomes an executable link on a review surface — precisely the surfaces (tax evidence rows, record-grid per-cell sources) that feed model-producedProvenanceSourceobjects into this component. Products usingonOpenSourcerouting are safe, but thehrefpath is a real consumer contract. Fix: gate on /^(https?:)?/// (and fall back to named-but-not-openable, which the component already supports).
🟡 LOW aria-controls references a panel that does not exist when closed — src/web-react/provenance.tsx
The trigger always sets
aria-controls={panelId}, but the element with that id is only rendered inside{open && ...}. While closed, the button references a nonexistent node — an invalid aria relationship for screen-reader tooling (and the reason the perceivable() test helper deliberately excludes id/aria-controls). Minor: set aria-controls only whenopen, or keep the panel mounted with hidden semantics.
🟡 LOW href attribute not sanitized against javascript: URLs — src/web-react/provenance.tsx
The
<a href={source.href}>tag passes the caller-suppliedhrefdirectly to the DOM. React does not sanitizehrefattributes, so ajavascript:alert(1)ordata:text/html,...value would execute on click. In this codebase, provenance source data is authored by the application's own backend and agent tool outputs, not by end users, so the practical risk is extremely low. Still, in a shared library, documenting the trust assumption on theProvenanceSource.hreffield or adding a^(https?://|/)prefix guard in a follow-up would close the surface.
🟡 LOW maxDepth default of 2 silently truncates deep provenance trees — src/web-react/provenance.tsx
maxDepth defaults to 2. A real tax return (wages → subtotals → AGI → taxable income → tax) can easily nest 4+ levels; at depth 3 the component renders the truncated branch (value + basis + origin sentence, no disclosure). The truncation is fail-safe (never a bare number) and overridable via prop, so this is a product-tuning note, not a defect. Worth mentioning in any consumer-facing example doc so products with deep composition know to bump maxDepth.
🟡 LOW openPanels global Set leaks across React roots — src/web-react/provenance.tsx
The
openPanelsSet is a module-level singleton shared across ALL React roots on the page. In a single-root SPA this is fine, but if a page mounts multiple React roots (portal, embedded widget, multi-app shell), opening a trail in root A would close one in root B viacloseTrailsOutside. The Set entries carry DOM element references that become stale if a root unmounts without triggering the useEffect cleanup (e.g., a root replaced by a client-side router outside React). Consider scoping the Set to a React context or accepting this as a documented constraint given the current consumer landscape is single-root.
🟡 LOW source.href rendered unsanitized in tag — src/web-react/provenance.tsx
When onOpenSource is absent and source.href is set, the component renders . A product that passes source.href = 'javascript:...' (or any string carrying executable content) creates a click-XSS vector. React 19 (this repo pins react 19.2.8) does not sanitize . ProvenanceSource is product-controlled rather than user input so this is defense-in-depth, not a live vuln, but the rest of /web-react's clickable links route through injected resolvers (e.g. MessageAttachments' resolveFileUrl). If a product ever surfaces a user-editable href, this becomes exploitable. Cheapest fix: add a tiny isSafeUrl(href) guard that rejects anything not starting with http:, https:, mailto:, or /. If it fails, render the source as named-but-not-openable
🟡 LOW 'blames' filter is a no-op; test checks all src/*.tsx citations, not Pattern 4's — tests/docs/legibility-contract.test.ts
const cited = inlineSpans(surfaces).filter(span => /^src\/.*\.tsx$/.test(span)); const blamed = cited.filter(path => surfaces.includes(${path})). Sincecitedis derived frominlineSpans(surfaces), everypathis already a literal substring of surfaces, so theblamedfilter is trivially true for all elements — it does not narrow to Pattern 4's blamed files as the test name states. It passes today because all src/*.tsx citations happen to still contain animate-pulse, but the assertion is weaker than advertised and would not catch a non-Pattern-4 citation losing its pulse. To match its name, scope the spans to the Pattern 4 section body before filtering.
🟡 LOW Per-file blame assertion weaker than its title and docstring claim — tests/docs/legibility-contract.test.ts
The test title promises every blamed file is re-checked, and the file header states 'a cited file is renamed and the evidence points at nothing' must break the build, but the assertion is only
expect(pulsing.length).toBeGreaterThan(0)— it passes while ANY of the src/*.tsx spans anywhere in product-surfaces.md still pulses, even if one or more cited files have stopped using animate-pulse. Theblamed = cited.filter(path => surfaces.includes(...))step is also a no-op (every span from inlineSpans is by construction a substring of surfaces). Consider asserting per-citation equality of expected-vs-present, or narrowing to Pattern 4's rejected block.
🟡 LOW statedCount regex fragile to document restructuring — tests/docs/legibility-contract.test.ts
The statedCount regex uses [^.\n]*? between the backticked phrase and the bold count. If a future doc edit inserts a period, semicolon, or line break between the phrase and its count (e.g., '
animate-pulseappears in loading skeletons. 16 times total.'), the regex silently fails to match and throws the generic error at line 237 rather than naming which part of the structure changed. Consider a more tolerant matcher or a diagnostic error that reports what regex part failed.
tangletools · 2026-08-04T12:01:23Z · trace
tangletools
left a comment
There was a problem hiding this comment.
❌ 18 Blocking Findings — 202e239d
Full multi-shot audit completed 8/8 planned shots over 43 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 8/8 planned shots over 43 changed files. Global verifier still owns final merge decision. | Full multi-shot audit completed 8/8 planned shots over 43 changed files. Global verifier still owns final merge decision.
Full immutable report for this review: trace
Summary comment for this run: full summary
tangletools · 2026-08-04T12:01:23Z · immutable trace
docs/product-surfaces.mdalready argued all of this — it opens by reporting an audit that scored product clarity 2–4/10 and concluded "Mechanism is not the gap; meaning is." Then the verticals shipped empty states with no next action, engineering vocabulary in error copy, a save button reporting success on a 404, and two deterministic engines no navigation reached.A well-written document did not change behavior. This ships the same intent as five static checks a product runs against its own source in CI — the
/theme-contractshape applied to meaning instead of tokens.The checks
engineering-vocabulary·dead-end-empty-state·unchecked-success·silent-failure·unreachable-capabilityEach names
file:line:columnplus the fix. Every finding is individually suppressible, and a suppression with no written reason is itself a finding — a gate that can only be silenced per-repo gets silenced per-repo.Calibration was the work
Hand-read every finding on two production verticals (530 files): 121 findings at 54% precision → 84 at 82%. Fewer reports and four more real defects, because two tunings were recall fixes and three were precision fixes. Nothing was threshold-tuned; every removal was an inspected false-positive class —
res.json().catch(() => ({}))is a parse fallback, not a hidden failure; a system prompt is not screen copy.The
unreachable-capabilitycheck resolves doors transitively to a fixpoint, because the real shipped defect was an island of three contract screens linking each other, which a one-hop "does a link exist" check clears entirely.What it measures on current main
Both severe classes — a screen that lies, and a built capability nobody can reach — are effectively gone. What remains is 36 vocabulary findings and 9 dead-end empty states: design backlog, not dishonesty.
Also in this PR
docs/product-surfaces.md(empty state, first-run as a declared state, provenance, motion, density, and the rule that an engine ships with its surface), each with a rejected example drawn from a real audit finding. A verification sweep caught the doc asserting two things the repos disprove, and one claim was corrected into a stronger named test./web-react): basis is required on a record, so a value cannot silently claim a document it does not have. Confidence never reaches the screen as a raw percentage. A computed value composes from its inputs, and standing rolls up to the weakest one, so a total never reads as better-traced than the worst number inside it.Verification
pnpm typecheckclean ·pnpm test4244 passed (251 files) · 18 break-restore proofs, three of which came back green on the first attempt — meaning three behaviors had no guard at all, so tests were added and re-proved red.🤖 Generated with Claude Code