DUNIN7 · LOOMWORKS · RECORD
record.dunin7.com
Status Current
Path inspection-briefs/loomworks-clean-report-cluster-step-0-findings-v0_1.md

Loomworks — clean-report cluster Step 0 findings — v0.1

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.


1. Environment

| 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.


2. Question four, answered first — what would "clean" actually take

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.

2.1 The engine — four gates, only one in this cluster's scope

| 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.

2.2 The surface — one nuance beyond what B-34/B-42 name

| 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.

2.3 The answer

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.


3. Question one — B-33: why does that test fail

The evidence supports "test fix," not "defect report" — with a specific, well-evidenced mechanism, not a guess.

3.1 The cause

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.

3.2 When it started — a regression, not an always-broken test

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.

3.3 What it is for

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.

3.4 Whether anything else covers it — yes, and it is what actually runs

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.

3.5 Verdict

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.


4. Question two — B-34: what is the right place to fix it

4.1 The mechanism, precisely

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.

4.2 Whether it also happens in the product — the load-bearing question, answered

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.

4.3 The candidate remedies, reported without choosing

4.4 What the room-state contract implies

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.


5. Question three — B-42: is the rule right

5.1 Counted fresh this session, not carried

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).

5.2 Why the pattern exists

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.

5.3 What the rule is actually protecting against, and whether it reaches these sites

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.

5.4 The three answers, reported without choosing

5.5 The other warnings — one job or several

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.


6. Corrections preserved

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.


7. Unread and unrun

  1. 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.
  2. Whether the 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.
  3. Whether disabling or narrowing 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.