refactor(web): sharpen the src/lib vs src/utils rule and move misfiled modules#1784
Conversation
…dules (#1776) Codify the empirical rule the web client already followed loosely — utils = pure functions that compute; lib = things that instantiate, adapt, or touch the environment — in AGENTS.md and clients/web/README.md, and move the modules that sat on the wrong side of it. Moves (source + co-located test): - lib/sandbox-csp -> utils (pure CSP-string construction; type-only import) - utils/browserTabVisibility -> lib (registers visibilitychange listeners) - utils/oauthResume -> lib (reads/writes sessionStorage) - utils/clearServerOAuthState -> lib (drives live InspectorClient / OAuthStorage) Borderline audits (kept in place): pendingReauth (pure type declarations), deepLink / correlateTransportErrors / mcpNetworkHeaders / oauthUx / oauthFlow / oauthNetworkPhase (pure transforms / type-only core imports) stay in utils; downloadFile stays whole in lib (dominant side is DOM side effects). Also relocate the lone src/test/lib/downloadFile.test.ts straggler beside its source at src/lib/downloadFile.test.ts, and document the test-placement rule (side-by-side by default; src/test/ only for core/ tests, the integration project, and shared scaffolding). Pure move — no behavior change; no build/coverage config keys off the boundary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
@claude review |
|
Claude finished @cliffhall's task in 2m 45s —— View job Review:
|
| File | Line | Says | Should say |
|---|---|---|---|
specification/v2_auth_mid_session.md |
142 | utils/oauthResume.ts, utils/browserTabVisibility.ts |
lib/… (the utils/pendingReauth.ts in the same row is still correct) |
specification/v2_auth_mid_session.md |
326 | clients/web/src/utils/oauthResume.ts |
clients/web/src/lib/oauthResume.ts |
clients/web/static/sandbox_proxy.html |
21 | src/lib/sandbox-csp.ts |
src/utils/sandbox-csp.ts |
clients/web/server/sandbox-controller.ts |
56 | src/lib/sandbox-csp.ts |
src/utils/sandbox-csp.ts |
The two sandbox-csp ones matter a bit more than the spec doc: both are load-bearing security comments explaining where the CSP is built, i.e. exactly the pointer a reader follows when auditing the sandbox.
2. src/lib/types/customHeaders.ts is the one module the new rule doesn't cover
It's not in the PR's borderline-audit list, and it's the clearest counterexample to the rule as written:
// clients/web/src/lib/types/customHeaders.ts
export interface CustomHeader { name: string; value: string; enabled: boolean }
export const getEnabledHeaders = (headers: CustomHeaders): CustomHeaders => …
export const headersToRecord = (headers: CustomHeaders): Record<string, string> => …
export const migrateFromLegacyAuth = (bearerToken?: string, headerName?: string): CustomHeaders => …Types plus pure array/object transforms — zero I/O, zero subsystem ownership, testable with no mocks. By the rule this is textbook utils. It sits in lib only because it's under a types/ subdirectory that neither AGENTS.md nor the web README mentions.
I'd resolve it one of two ways, not leave it ambiguous:
- (a) Move
lib/types/customHeaders.{ts,test.ts}→utils/customHeaders.{ts,test.ts}(orutils/types/), consistent withsandbox-cspmoving for the same reason; or - (b) Keep it and add a sentence to both docs saying
src/lib/types/is a third bucket for shared domain types + their pure constructors, deliberately outside the compute/environment split.
(a) is more consistent with the rest of the PR; (b) is fine if the types/ subdir is intentional. Either way the rule should stop having a silent exception living inside the directory it governs.
3. Minor: the one utils/ → lib/ arrow
After the move, the dependency arrows between the two directories are:
lib/oauthResume.ts → utils/inspectorTabs.js, utils/oauthFlow.js (value imports, fine)
utils/pendingReauth.ts → lib/oauthResume.js (type-only — the only one)
The new rule explicitly sanctions this (a type-only import is not a subsystem dependency), so it isn't a violation. But it's the sole arrow pointing from the pure layer into the infra layer, and the type it pulls is about as pure as a type gets:
export type OAuthResumeAuthKind = "step_up" | "reauth";Declaring OAuthResumeAuthKind in utils/pendingReauth.ts and re-exporting it from lib/oauthResume.ts (which already re-exports OAUTH_PENDING_SERVER_KEY from utils/oauthFlow.js, so the pattern is established) would make the arrows strictly one-way — lib → utils, never back. Purely optional; call it if you think the one-way invariant is worth stating in the rule.
Nit on the rule's wording
AGENTS.md:248 says src/utils/ is "pure, side-effect-free functions", but three of the named anchors log: jsonUtils (console.error ×4), schemaUtils (console.warn ×3), and the newly-moved sandbox-csp (console.warn at line 70). The intent is obviously "no meaningful side effects", but as literally worded, sandbox-csp's warn is a violation of the rule the same commit uses to justify moving it. One clause — "diagnostic logging (console.warn/console.error) doesn't count as a side effect" — closes it.
Not verified
I did not run the test suite — clients/web/node_modules isn't installed in this job, so npm run validate / coverage weren't available. Everything above is static analysis (import graph, grep for string-literal mock paths, side-effect marker scan). Your npm run ci green run covers the executable side; the moves are pure renames (git detected all six as R100 except the two files with import-line edits), so the risk surface is exactly the import graph, which I did check exhaustively.
Verdict: approve-with-nits. Item 1 (stale paths) is the one I'd actually want fixed before merge; item 2 is worth a decision either way; item 3 and the wording nit are take-or-leave.
· branch v2/1776-lib-utils-rule
…one-way arrows (#1776) Responds to the @claude review on PR #1784: 1. Stale path references (fixed): update the four in-repo pointers left at pre-move paths — specification/v2_auth_mid_session.md (oauthResume, browserTabVisibility → lib/), and the two load-bearing sandbox security comments (sandbox_proxy.html, sandbox-controller.ts → src/utils/sandbox-csp). 2. lib/types/customHeaders (moved): it's pure (types + pure transforms), the rule's clearest counterexample. Moved lib/types/customHeaders.{ts,test.ts} → utils/ (no external importers; only its own test) and removed the undocumented lib/types/ subdir. Documented that utils/ is the home for pure shared domain types + their constructors — no separate types/ bucket. 3. One-way arrows (item 3): declare OAuthResumeAuthKind in utils/pendingReauth and re-export it from lib/oauthResume, so the only cross-directory arrow now points lib → utils. Codified that invariant in AGENTS.md. Wording nit: AGENTS.md + README now state diagnostic logging (console.warn/error) doesn't count as a side effect for the utils rule. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Thanks for the thorough review — addressed all four points in 86fbdfc. 1. Stale path references — fixed. All four updated: 2. 3. The one Wording nit — fixed. Both docs now state: diagnostic logging (
|
|
@claude review |
|
Claude finished @cliffhall's task in 2m 38s —— View job Review:
|
| File | Line | Says |
|---|---|---|
specification/v2_ux_interfaces_phase_0_4_inventory.md |
23 | `CustomHeader` from `clients/web/src/lib/types/customHeaders.ts` |
specification/v2_ux_interfaces_plan.md |
51 | `clients/web/src/lib/types/customHeaders.ts` — `CustomHeader` / `CustomHeaders` shape |
specification/v2_ux_interfaces_plan.md |
185 | copy `clients/web/src/lib/types/customHeaders.ts` from v1.5 verbatim |
Line 23 is the one that matters — it's prescriptive guidance ("use CustomHeader from <path> when the form edits headers specifically"), i.e. a reader is meant to follow it, and it now dead-ends. The two plan.md hits are more historical ("copy from v1.5"), so leaving them is defensible if you treat that doc as a frozen record — but then the inventory one should still be updated.
2. utils/customHeaders.ts is dead code, and the docs now anchor the rule on it
The module has no importers at all — you established this yourself while making the move. Its only consumer is its own co-located test:
src/utils/customHeaders.ts # 8 exports: CustomHeader, CustomHeaders,
# createEmptyHeader, createHeaderFromBearerToken,
# getEnabledHeaders, headersToRecord,
# recordToHeaders, migrateFromLegacyAuth
src/utils/customHeaders.test.ts # the only file that imports it
That's fine as a fact about the codebase (it predates this PR — v2_ux_interfaces_plan.md:185 shows it was copied from v1.5 ahead of the ServerSettingsForm work that would consume it). But this PR promotes it: both AGENTS.md:248 and the web README now list customHeaders as a named anchor for the utils/ rule, and AGENTS.md gives it the dedicated "shared domain types plus their pure constructors/transforms … there is no separate types/ bucket" clause. Anchoring a rule on a module nothing imports is a weak example — a reader who greps for it to see the pattern in action finds only a test.
Two ways out, either fine:
- Swap the anchor to something load-bearing (
maskSecrets,deepLink,mcpNetworkHeaders) and keepcustomHeadersonly as the illustration for the no-types/-bucket clause; or - Note the staging in a half-sentence ("
customHeaders— shape staged forServerSettingsForm, seev2_ux_interfaces_plan.md") so the zero-importer state reads as intentional rather than as rot.
Not suggesting you delete it in this PR — that's a scope change and it'd need its own issue (deleting it also drops ~8 exports out of the coverage denominator, which is a separate conversation).
3. The type-only-import clause doesn't actually describe its own examples (nit)
AGENTS.md:248 reads:
A module that only imports types from
@inspector/core(or elsewhere) is likewise pure and belongs here — a type-only import is not a subsystem dependency (e.g.pendingReauthis pure type declarations,oauthUx/oauthFlowre-export core copy/predicates).
pendingReauth fits (both its core imports are import type). The other two don't — they're value re-exports:
// utils/oauthFlow.ts:7
export { isUnauthorizedError } from "@inspector/core/auth/utils.js";
// utils/oauthUx.ts:1-12 — 7 runtime functions plus 3 types
export { authRecoveryRestoredMessage, isActionTriggeredOAuthRecovery,
oauthPreRedirectToastCopy, /* … */ } from "@inspector/core/auth/oauthUx.js";They belong in utils/ for a different reason than the one the sentence gives: the re-exported values are themselves pure functions, so re-exporting them doesn't take on a subsystem. As written, someone applying the rule literally would have to move both to lib/ (they import runtime, not just types) — which nobody wants. Splitting it into two clauses fixes it:
…a type-only import is not a subsystem dependency (
pendingReauth). Re-exporting pure functions or constants from core is likewise not subsystem ownership (oauthUx,oauthFlow) — what makes a modulelibis wrapping core's stateful runtime, not merely importing from it.
The same conflation is in the web README's condensed version ("a type-only import from @inspector/core doesn't count as a subsystem dependency"), which then lists sandbox-csp and customHeaders as examples — both of which are fine, so the README wording is only under-inclusive rather than wrong.
Cosmetic
specification/v2_auth_mid_session.md:142 — the edited table row got shorter and no longer pads its trailing | to the column width the surrounding rows use. Nothing checks it (prettier is scoped to core/** and each client's src, not specification/), so purely aesthetic in the raw file; renders identically.
Not verified
Same limitation as round 1: clients/web/node_modules isn't installed in this job, so I could not run validate / coverage / tsc. Everything above is static — import-graph traversal, a grep for vi.mock() string-literal paths against all old locations (clean; that's the failure mode a rename-only refactor most often trips on since tsc can't see mock paths), and an environment-marker scan of every file in src/utils/. Your npm run ci green run covers the executable side, and the remaining risk surface is exactly the import graph, which I checked exhaustively.
One coverage note worth stating since I couldn't run the gate: none of the moves change a file's content, and the web coverage include is directory-agnostic for clients/web/src, so per-file ≥90 is unaffected by the relocation — consistent with your green run.
Verdict: approve. Item 1 is a 3-line doc fix I'd take before merge (it's the same class of thing you just fixed, one commit later); items 2 and 3 are wording polish on the rule itself. The refactor is correct and the rule is a genuine improvement — the one-way-arrow invariant in particular is the part most likely to keep paying off, since it's the only piece of this that's mechanically checkable later.
· branch v2/1776-lib-utils-rule
…#1776) Doc-only follow-up to the round-2 @claude review on PR #1784 (no code changed): 1. Stale paths (same class as round-1 item 1): the customHeaders move left three references to the deleted lib/types/customHeaders.ts path in specification/ (v2_ux_interfaces_phase_0_4_inventory.md, v2_ux_interfaces_plan.md ×2) — updated to utils/customHeaders.ts. 2. customHeaders anchor: note in AGENTS.md that its shape is staged for ServerSettingsForm (per v2_ux_interfaces_plan.md), so its zero-importer state reads as intentional rather than rot; kept as the no-types/-bucket illustration and led the anchor list with load-bearing modules. 3. Type-only-import clause: split the conflated clause — a type-only import (pendingReauth) and re-exporting core's pure functions/constants (oauthUx/oauthFlow) are distinct reasons to stay in utils; what makes a module lib is wrapping core's stateful runtime. Fixed in both AGENTS.md and the web README. Cosmetic: re-padded the v2_auth_mid_session.md table row to column width. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Thanks — round-2 items addressed in c77df99 (doc-only, no code changed since the last green 1. Stale 2. 3. Type-only-import clause didn't describe Cosmetic: re-padded the Since this round was purely docs/spec ( |
|
@claude review |
|
Claude finished @cliffhall's task in 2m 44s —— View job Review:
|
) Doc-only follow-up to the round-3 @claude review on PR #1784 (no code changed): 1. "no separate types/ bucket" overreached: clients/web/src/types/ does exist (navigation.ts + react-syntax-highlighter .d.ts ambient stubs wired via tsconfig.app.json paths). Scoped the claim in both AGENTS.md and the web README to "no types/ sub-bucket INSIDE lib/utils", and noted src/types/ is for ambient .d.ts stubs, not domain types. 2. README example list drifted from AGENTS.md's round-2 load-bearing anchors — realigned it (inspectorTabs/mcpNetworkHeaders, dropped the zero-importer customHeaders from the headline list). 3. Clarified v2_ux_interfaces_plan.md:185 so the v2 path reads as the copy's destination, not as the v1.5 source location. Follow-up filed as #1785 (board Todo): src/types/navigation.ts InspectorTab is dead and duplicates utils/inspectorTabs.ts — kept out of this pure-move PR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Thanks — round-3 items addressed in ffcd657 (doc-only again; no 1. "no separate 2. Nits — both fixed. Realigned the README example list to AGENTS.md's round-2 load-bearing anchors ( |
|
@claude review |
|
Claude finished @cliffhall's task in 2m 6s —— View job Review:
|
…1776) Doc-only follow-up to the round-4 @claude review on PR #1784 (no code changed): The round-3 scope fix overshot: it asserted src/types/ holds "not domain types at all", but the dead navigation.ts InspectorTab is a plain-.ts domain type still living there, and specification/v2_ux_interfaces_plan.md:181-182 actively prescribes placing InspectorTab there — so AGENTS.md and that spec doc gave opposite guidance about the same directory. - AGENTS.md + web README: softened to "not the place for new domain types" and added a parenthetical that the one lingering domain type (dead navigation.ts) is tracked for removal in #1785; until then a pure domain type belongs in utils/, not src/types/. - v2_ux_interfaces_plan.md:181: marked the tab-enum placement guidance as superseded by #1785 (live type is InspectorTabId in utils/inspectorTabs.ts). Self-heals on merge of #1785. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Thanks — round-4 item addressed in 3fecb5c (doc-only; no code changed since the last green 1.
So the two docs now agree, and it fully self-heals when #1785 merges. Good call catching that this was the round-3 finding recursing one level — the fix now describes the actual current state rather than the post-#1785 one. |
|
@claude review |
|
Claude finished @cliffhall's task in 1m 50s —— View job Review:
|
…utils rule (#1776) Doc-only follow-up to the round-5 @claude review on PR #1784 (no code changed): 1. Grammar (item 1): the round-4 #1785 clause read "superseding [#1785] will remove it" — a participle with no subject. Reworded to "the dead navigation.ts InspectorTab, which #1785 will remove". 2. Readability (item 2): the src/utils/ bullet had accreted into a single ~1,400-char blob across five review rounds. Promoted the carve-outs to scannable sub-bullets (Domain types / Diagnostic logging / Importing from core) and lifted the src/types/ note out to its own line, since it's a statement about a sibling directory, not about utils. AGENTS.md only; the README's parallel sentence was already clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Thanks — both round-5 items addressed in c949218 (doc-only; no code changed since the last green 1. Garbled 2. Over-accreted bullet — restructured (took the optional polish since I was in the line). The Appreciate the five rounds of re-verification — the import-graph and one-way-arrow greps in particular. I think it's genuinely merge-ready now. |
|
@claude review |
|
Claude finished @cliffhall's task in 3m 12s —— View job Review:
|
…#1776) Responds to the round-6 @claude review on PR #1784: 1. "no build/coverage glob" was factually wrong (should-fix): the web coverage include in clients/web/vite.config.ts DOES enumerate src/lib/** and src/utils/**. The conclusion (a move between them is coverage-neutral) survives, but the reason was wrong. Reworded both AGENTS.md and the web README to say nothing *enforces* the boundary — no path alias, and the include lists both dirs so a move is coverage-neutral — plus the important whitelist caveat: a module outside components/lib/utils/server falls out of the ≥90 gate entirely. 2. Removed a pre-existing duplicate glob in vite.config.ts (src/lib/**/*.{ts,tsx} appeared twice, lines 161 and 163). Inert (vitest de-dupes) but it sat three lines from the config the doc now describes. Nits: gave the README bullet the same round-5 sub-bullet restructure as AGENTS.md, and made its #1785 reference a real markdown link (bare #N doesn't autolink in rendered repo markdown). npm run ci green (config change re-verified through coverage + smokes + Storybook). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Thanks — round-6 addressed in 2683a9d. 1. "no build/coverage glob" was factually wrong — fixed. Good catch, and it's the one sentence that mattered to get right since the doc's job is telling people where to put a new module. The web coverage 2. Duplicate glob — removed. Deleted the second Nits — both done. Gave the README bullet the same sub-bullet restructure as AGENTS.md (Diagnostic logging / Importing from core / Domain types), lifted its The exhaustive-anchor property you noted (each doc's anchor list = exactly the files in that dir) is a nice invariant — I'll try to keep it true as files come and go. |
|
@claude review |
|
Claude finished @cliffhall's task in 6m 7s —— View job Review:
|
|
Thanks — appreciate the empirical verification this round (reading the actual v8 tables from the branch's CI runs to confirm the coverage sentence, rather than inferring). Glad the wording now matches both the tree and the gate. Both findings are pre-existing and out of scope for this PR, as you noted — so rather than churn the diff, I've tracked them:
That leaves nothing outstanding in this PR's diff — the code has been stable since |
Closes #1776
Codifies the empirical rule the web client already followed loosely —
utils= functions that compute;lib= things that instantiate, adapt, or touch the environment (I/O or wraps a subsystem →lib; pure transform →utils) — inAGENTS.mdandclients/web/README.md, and moves the modules that sat on the wrong side of it. Pure, behavior-preserving move: nothing keys off the boundary (no path alias, no build/coverage glob).Moves (source + co-located test)
sandbox-csplib→utils_meta.ui.csp; only a type import +console.warn.browserTabVisibilityutils→libdocument.visibilityState, registersvisibilitychangelisteners (DOM side effects).oauthResumeutils→libwindow.sessionStorage(stateful I/O).clearServerOAuthStateutils→libInspectorClient/OAuthStoragesubsystem.Borderline audits (per the rule)
Kept in
utils— pure transforms / type-only@inspector/coreimports (a type-only import is not a subsystem dependency):pendingReauth— pure type declarations, no runtime code.deepLink,correlateTransportErrors,mcpNetworkHeaders,oauthNetworkPhase— pure URL/Map/Set/header parsing.oauthUx,oauthFlow— re-export core copy/predicates + constants; sessionStorage is mentioned only in a doc comment.Kept whole in
lib:downloadFile— mixes DOM-side-effect helpers (downloadBlob/downloadJsonFile) with a few pure ones; left whole on its dominant (side-effect) side rather than split, which the issue flags as acceptable.Straggler + docs
src/test/lib/downloadFile.test.tsbeside its source atsrc/lib/downloadFile.test.ts— it was the only web-owned test living undersrc/test/instead of side-by-side.AGENTS.md: side-by-side by default;src/test/only forcore/tests (mirroringcore/layout), theintegrationproject, and shared scaffolding.Verification
npm run cigreen (validate → coverage ≥90 gate → verify:build-gate → smoke → Storybook).🤖 Generated with Claude Code
https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5