Version. 0.1
Date. 2026-08-04
Brief executed. inspection-briefs/loomworks-clean-report-cluster-step-0-inspection-brief-v0_1.md
Charter. standing-notes/dunin7-standing-authorization-charter-v0_1. R-5 inspection run.
Build-list items. B-33, B-34, B-42.
Status. Read-only. No fix, no change request, no recommendation.
| Repository | Branch | SHA | Tag | Tree state |
|---|---|---|---|---|
| /Users/dunin7/loomworks-engine | main | 3b10ff1 | cr-2026-157-non-member-contribution-v0_1 (exact match) | clean |
| /Users/dunin7/loomworks | main | cb8b940 | provenance-walk-v0_1 (exact match) | clean |
Both match the brief's header exactly — neither has moved. Confirmed by direct git rev-parse this session, not inherited from the brief or the build list that named them.
Databases. playground_dev and playground_test were never connected to. Every command in this session ran against one throwaway (clean_cluster_check), created for the run via the engine's own documented stand-up sequence (README ## Database section, per CR-2026-157/B-41) and dropped at the end — confirmed absent afterward by psql -l.
No. B-33, B-34 and B-42 together do not achieve it. Two further gates exist on the engine side that this cluster's own framing never named, plus one nuance on the surface side.
| Command | Result | In this cluster? |
|---|---|---|
| pytest -q | 1 failed (B-33), 3521 passed, 68 skipped | Yes — B-33 |
| ruff check . | 3606 errors across 39 distinct rules | No |
| mypy src/ (strict) | 601 errors in 171 of 374 checked files | No |
| .github/workflows/api-docs.yml (the engine's only CI job) | passes (docs freshness only) | N/A — doesn't check tests, lint, or types at all |
Neither ruff nor mypy is run by CI. The engine's repository has exactly one GitHub Actions workflow, and it checks OpenAPI-doc freshness — nothing else. Both tools are configured in pyproject.toml ([tool.ruff], [tool.mypy] with strict = true) and both ship as dev dependencies, but neither is gated anywhere, and neither was named by B-33, B-34, or B-42, or by any drafting session's framing of this cluster before now. This is the fourth item (and a fifth) the brief's own kickoff said was the most likely thing to be found.
Ruff's 39 broken rules, by volume (ruff check . --statistics): B008 function-call-in-default-argument (800), I001 unsorted-imports (671), F401 unused-import (302), UP045 (258), E501 line-too-long (227), UP037 (131), B904 (92), and 32 further rules at smaller counts down to 1. 2265 of the 3606 are --fix-mechanical.
| Command | Result | In this cluster? |
|---|---|---|
| npm run build (next build) | clean — 0 errors, 0 warnings, TypeScript passes | N/A |
| npm run lint (ESLint) | 33 errors / 23 warnings across 7 distinct rules | Partially — B-42 (errors), not the warnings explicitly |
| npm run test (vitest) | 693/693 tests pass; exits 1 on 12 unhandled rejections | Yes — B-34 |
| npx tsc --noEmit (direct, whole-project) | 4 errors, all in tests/ | No |
| npm run test:e2e (Playwright) | not run — blocked | Not named, and unrunnable under this brief's fences |
next build passing is not the same claim as "the codebase typechecks." Next's internal TypeScript check only walks files reachable from the app's build graph; a direct tsc --noEmit against the project's own tsconfig.json (which includes **/*.ts/**/*.tsx with no exclusion for tests/) finds 4 errors that next build never sees, all inside test files (RefreshFocusOnIntent.test.tsx, TagChipArea.test.tsx ×3). This is unread by any prior document in this project — nothing before this session established that next build clean does not imply tsc --noEmit clean.
Playwright e2e is entirely outside this cluster's naming and this brief's reach. The brief's own fence forbids a dev server, and test:e2e requires one — reported unrun, not assessed either way, per §7 below.
No single fourth item — three: the engine's ruff backlog, the engine's mypy backlog, and the surface's tsc --noEmit gap. All three are real, all three are currently non-zero, and none is named by B-33, B-34, or B-42. Fixing all three items this cluster does name would make pytest, npm run lint's errors, and npm run test's exit code clean — and a reader would still not be able to say either repository "reports clean" without separately addressing ruff, mypy, and the direct-tsc gap.
The evidence supports "test fix," not "defect report" — with a specific, well-evidenced mechanism, not a guess.
tests/test_stele_router_mount.py::test_stele_router_mounts_and_begin_resolves_end_to_end mounts Stele's own mountable stele.router (not the engine's production routes) and calls POST /me/security/passkeys/begin. That route depends on Depends(require_fresh_session) (stele/api.py:284) — not Depends(resolve_current_principal), the only slot the test overrides. require_fresh_session is a separate, stricter "step-up" auth slot (stele/api.py:170-201) whose own docstring states it deliberately calls resolve_session directly rather than depending on resolve_current_principal, "so overriding one slot never silently changes the other's behavior." Its default token source is extract_token (stele/api.py:95-104), which reads only an Authorization: Bearer header — never a cookie. The test sets a session cookie and never an Authorization header, and never overrides require_fresh_session or extract_token at all (its own _build_mounted_app overrides exactly five slots: provide_db_session, resolve_current_principal, provide_secret_key, provide_webauthn_config, provide_person_email — confirmed by reading the file in full).
Traced live (a standalone diagnostic script, not committed, run against the throwaway): the issued token decodes correctly under the test's own secret key when checked directly; the dependency-override for resolve_current_principal is never even reached by the begin route, because that route doesn't depend on it. require_fresh_session's default path receives no token (no Bearer header present) and raises exactly the observed 401 with exactly the observed detail text, "Authentication required (no valid session)." — confirmed by grep: this exact string exists in two places, stele/api.py:122 (inside resolve_current_principal) and stele/api.py:194 (inside require_fresh_session) — the test's failure is the second one, not the first.
The test file was last touched at engine commit 8310110 (2026-06-20). Stele's step-up work landed at 274e737 ("TS-14: add step-up freshness check slot") and a52b72e ("TS-14: require fresh session on sensitive mutations"), both dated 2026-07-07 — seventeen days after the test's last edit. git log -p on stele/api.py confirms require_fresh_session did not exist before those commits, and /passkeys/begin's dependency changed to it there. The test was passing when it was last written and broke when Stele's own dependency, seventeen days later, added a slot the test's fixture had no way to know about.
Its own docstring is explicit: "This is a VERIFICATION, not a production mount." It proves stele.router — Stele's host-agnostic, mountable HTTP surface — is consumable by a host wired with the engine's real dependency providers.
A grep across the entire engine src/ tree for stele.router, stele.api.router, or import stele (router-mounting usage) returns zero matches outside this one test file. The engine's production app never mounts Stele's mountable router at all. The real, live route the URL /me/security/passkeys/begin resolves to in production is src/loomworks/api/routers/me_security.py:157 (passkey_begin_route), a separate, native implementation that calls stele.webauthn.add_passkey_begin directly and depends on Depends(get_current_user) — the engine's own cookie-based resolver, not require_fresh_session, not resolve_current_principal, not anything from stele.router at all.
The behaviour the test's name claims to verify is real, live, and covered — by a different code path than the one this test exercises, and that different path is untouched by TS-14's step-up-slot change.
Test fix. The production passkey-enrollment flow works, is unaffected by this failure, and is not exercised by this test. The fix, confined entirely to tests/test_stele_router_mount.py's own fixture, is to also override stele.require_fresh_session (mirroring how resolve_current_principal is already overridden) — or to additionally deliver the token as a Bearer header the default extract_token slot can see. Either way, nothing outside this one test file needs to change.
fetchPlatformLevel (src/lib/api/platform-level.ts:43-51) already wraps its call in try/catch: it returns null for an ApiError, and re-throws everything else. Its sole caller, usePlatformLevel's useEffect (src/hooks/usePlatformLevel.ts:35-44), defines an inner async function resolve() that awaits fetchPlatformLevel() — and then calls resolve(); without await and without .catch(). Any re-thrown non-ApiError becomes an unhandled promise rejection at that exact call site. This is "fired in an effect with no error path," precisely the third of the brief's own named candidates at §4.1.
No — not under the conditions that produce it in the test suite, though the same code gap is latent in production too, under a different trigger.
api()'s base URL (src/lib/api.ts:31) is deliberately relative (/api), with its own comment stating this is "safe... because this value is only ever used from the browser... a relative URL is never resolved without a document origin to resolve it against." Read live in the three failing test files' captured errors this session: TypeError: Failed to parse URL from /api/me/platform-level — this is exactly what happens when a relative URL is hit through a non-browser fetch (Vitest's Node/undici environment has no document origin), not something a real browser can produce. A second observed shape, TypeError: Body is unusable: Body has already been read, is a malformed test double — a mocked Response whose body is consumed twice — not a real HTTP server behavior. A third, Cannot read properties of undefined (reading 'level'), is fetchPlatformLevel reading response.level off an incomplete mock response object. All three observed rejection shapes are test-double artifacts of three test files rendering shared layout components without a complete fetch mock for /me/platform-level — not reachable via the real fetch path in a working browser under ordinary use.
The gap is still real, under a different trigger. fetchPlatformLevel's catch only recognizes ApiError (thrown by api() after a completed HTTP round-trip). Any failure fetch() itself throws before a response exists — offline, DNS failure, a CORS block, an aborted request — is a plain TypeError/DOMException, not an ApiError, and would be re-thrown and go unhandled in the browser exactly as it does in the test today. This has not been observed in the product because ordinary product usage doesn't trigger it — not because the code path is safe.
usePlatformLevel). Widest blast radius on the surface (every screen rendering the shared layout); the one remedy that also closes the real-browser gap, if it does so honestly (see §4.4).fetchPlatformLevel itself. Narrower; already half-built (the ApiError branch exists) — would mean widening the catch to swallow any rejection into null, which changes what "the caller couldn't tell" means at the one function every other caller of this module already trusts.
Three of the four rooms carry a contract where a failed read must say the read failed, never silently render as if the record were empty (usePagedList and ManifestationRoom, read directly in §5 below, both implement this explicitly with an unloaded/loading/populated/empty/error state machine). usePlatformLevel's current shape — an uncaught rejection crashes resolve() before setResolved(true) ever runs — means resolved stays false forever on a real failure. Read literally, this already satisfies the contract's rule by accident: a permanently-false resolved is "we don't know," not a false "we checked and there is none." But it is a silent, unreported failure to a real user (a spinner or absent Tools entry with no error surfaced, and an invisible unhandled rejection in the console/error-tracker), and any remedy that instead sets resolved = true with level = null on every failure — collapsing "couldn't tell" into "confirmed none" — would break the contract this codebase has otherwise held. This is the constraint the brief said would govern the choice regardless of what's cheapest, and it does: whichever remedy is chosen, catching the rejection must not turn an unknown into a false negative.
npx eslint . -f json, parsed directly: 33 errors, 23 warnings, 7 distinct rules broken. react-hooks/set-state-in-effect accounts for 18 files — the largest single rule. The remainder: @typescript-eslint/no-unused-vars (10 files), react-hooks/refs (7), Unused eslint-disable directive (2), and three singletons (react-hooks/static-components, react-hooks/rules-of-hooks, react-hooks/globals).
Read ManifestationRoom.tsx:196-206 and usePagedList.ts:117-124 in full. Both call setState synchronously at the very top of a useEffect body, before any async operation begins — ManifestationRoom sets { status: "loading" }; usePagedList sets five flags (setAsked, setLoading, setError, setErrorValue, setReadAt) in sequence. This is not carelessness. ManifestationRoom's own comment names it explicitly: "The room's own read state, in the same contract the paged rooms use... unloaded is the state before the effect commits." usePagedList is the shared hook every paged room is built on (confirmed: it is the state-contract's own host, per this session's read). Both are implementing the identical unloaded→loading→(populated|empty|error) transition the room-state contract requires — synchronously, on purpose, so the UI can render "loading" before the fetch resolves rather than staying in a stale unloaded state.
The rule's own message (eslint-plugin-react-hooks, quoted in full from a live run): "Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended" — the named hazard is a cascade: state set in an effect that feeds back into that same effect's own dependencies, re-triggering it. At both read sites, the state being set (loading/asked/error/etc.) is not a dependency of the effect that sets it — ManifestationRoom's effect depends on projectId/refreshNonce; usePagedList's on resetKey/reloadNonce (confirmed by reading the effect's own dependency array in both files). Nothing here re-fires the effect. The pattern produces exactly one bounded extra render (unloaded→loading), not a cascade. This is the brief's own §5.3 question answered directly: a rule broken by the codebase's own model component, here, reads as a rule aimed at a pattern this codebase does not have at these call sites — not a hazard nobody has hit yet.
ManifestationRoom and usePagedList — meaning changing the pattern every later room copied from them.ManifestationRoom/usePagedList's exact shape (setState calls with no dependency on the state being set) named precisely enough to write a narrowing config that doesn't also admit a genuine cascade elsewhere.
Several, not one. The 23 warnings and 33 errors span 7 distinct ESLint rules in two unrelated families: the react-hooks/* family (errors, four rules, the set-state/refs/static-components/globals cluster) and @typescript-eslint/no-unused-vars (10 files, entirely separate — dead imports and unused bindings in test files, unrelated to hooks semantics). "Make lint pass" is at minimum two jobs — fixing (or narrowing) the hooks-rule cluster, and a routine dead-code sweep for the unused-vars cluster — and the cluster's own premise (a clean report reads as clean) fails if only the errors are addressed and the 23 warnings remain, exactly as the brief anticipated at §5.5.
No figure in the build list or any prior document is contradicted — B-34 and B-42 both correctly carry no number in the current build list (v0.20), consistent with this session's finding that both are genuinely fast-moving. What this session adds, not a correction: neither prior document (build list, status briefs, or the CR-2026-157 arc) named ruff, mypy, or a direct tsc --noEmit as part of what "clean" requires — this is new ground, not a fix to a stale claim.
npm run test:e2e (Playwright). Not run — requires a dev server, which this brief's own fence forbids. Whether e2e is part of what "clean" should mean for the surface is itself unestablished.tsc --noEmit gap (§2.2) predates or postdates any specific merge. Established that it exists and where (4 errors, all in tests/); not established when it was introduced — would need a bisection this brief's read-only, single-session scope did not attempt.react-hooks/set-state-in-effect (one of B-42's three reported options) would itself require a version-controlled config change that CI would need to gate — moot until Q3's options are chosen, which this brief does not do.DUNIN7 — Done In Seven LLC — Miami, Florida Loomworks — clean-report cluster Step 0 findings — v0.1 — 2026-08-04 Three symptoms, three causes read to ground, and two more gates neither repository's own framing had named.