From ac8434f37ae7d75a4cc64d051bccf350fbf8c257 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 16:05:08 +0000 Subject: [PATCH 1/8] perf(services): keep the services snapshot out of the client route chunk The /services client home page value-imported defaultServiceSlug from @/lib/services, compiling the ~915 KB services snapshot (~100 KB gzip) into its route chunk for one static href. The server page now computes the slug and passes it as a prop. Bundle budget swings from +3.13% over to -3.97% under the enforced baseline, which is ratcheted down to 1,309,286 bytes gzip so CI locks the win. Ledger row included. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EXsJcLrbZUXwnBeG91cVo9 --- bundle-budget.json | 4 +- docs/branch-review-ledger.md | 1 + src/app/services/page.tsx | 5 +- .../services/services-home-page.tsx | 57 ++++++++++--------- 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/bundle-budget.json b/bundle-budget.json index 9ebae9854d..034faf5186 100644 --- a/bundle-budget.json +++ b/bundle-budget.json @@ -2,6 +2,6 @@ "$comment": "Client JS bundle-size budget captured from a known-good production build. CI fails when total gzip size grows beyond tolerancePct; refresh intentionally with `npm run check:bundle-budget -- --update`.", "enforce": true, "tolerancePct": 10, - "totalGzipBytes": 1363382, - "updatedAt": "2026-07-17T12:11:55.267Z" + "totalGzipBytes": 1309274, + "updatedAt": "2026-07-18T15:49:00.100Z" } diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index db37258d12..b36d2f9de6 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -609,3 +609,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-18 | codex/rag-merge-final-20260718-655a | 20e5964bbfdcac0311b89c96931a43544be545a6 | RAG recovery, grounding, and latency merge closure | Reviewed the recovered historical RAG fix series against fresh `origin/main`. No additional source change was needed: current main already contains the stricter source-scoped numeric grounding, blocked-retrieval-only extractive recovery, generic LAI deterministic recovery, per-case answer latency diagnostics, and agitation lexical-path protections. The only residual branch diff was proven unreachable because every medication-chart trigger yields explicit search terms; it was intentionally not carried forward. | Full local Vitest: 301 files / 2,779 tests passed. Offline RAG fixture and production-contract validation: 36 golden cases, 21 suites, 294 tests passed. Prettier and `git diff --check` passed. `verify:cheap` reached its lint stage after all preceding static guards passed, but was blocked twice by active heavyweight jobs in other registered worktrees; it was not retried. No Supabase, OpenAI, live retrieval, production-readiness, deployment, or hosted CI command was run. | | 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. | | 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. | +| 2026-07-18 | claude/clinical-kb-pwa-review-asi3wb (plan Phase 4 PR; keyed by PR number on open) | Phase 4 implementation commit on this branch (single-batch push; Phase 3 landed as squash 44a4c511bc1381168474794d6f89273564659ead) | App-wide performance pass: audit-then-fix + budget ratchet (plan Phase 4) | Audit on post-#872 main: enforced gzip budget +3.13% over the 1,363,382-byte baseline; analyzer treemap showed pdfjs-dist (123 KB) and cross-mode-differentials data (121 KB) already correctly lazy, and disproved the suspected forms-catalog dashboard leak (type-only import). Confirmed one real defect: the 143-line `/services` client home page value-imported `defaultServiceSlug` from `@/lib/services`, compiling the ~915 KB services snapshot (~100 KB gzip) into its route chunk. Fixed by computing the slug in the server page (`src/app/services/page.tsx`) and passing it as a prop; the client component builds its task cards from the prop. Measured result: budget swung from +3.13% to −3.97%; baseline ratcheted down 1,363,382 → 1,309,286 bytes gzip so CI locks the win. pdfjs/differentials/#718 paths deliberately untouched per audit rules. Two transient `.next/dev` generated-type corruptions from the long-lived dev server were resolved by stopping the server and setting the generated dir aside (reversible); nothing hand-deleted. | Build + `check:bundle-budget --json` before/after (artifacts in session scratchpad); `check:bundle-budget -- --update` for the ratchet; `npm run test` 2789 passed/1 failed (known container-only `pdf-extraction-budget` artifact, hosted-CI-green through #826/#835/#872); `verify:cheap` green to the same artifact; `verify:ui` 219 passed/2 failed (the two long-established container artifacts; ui-tools spec covering the services surface passed); `verify:pr-local` unit stage identical; typecheck/lint/format clean. Lighthouse not run (dev-server churn; bundle evidence sufficed for this fix set). No provider-backed checks run. | diff --git a/src/app/services/page.tsx b/src/app/services/page.tsx index 9a2148397f..ad6cf732ea 100644 --- a/src/app/services/page.tsx +++ b/src/app/services/page.tsx @@ -2,6 +2,7 @@ import { Suspense } from "react"; import { ServicesHomePage } from "@/components/services/services-home-page"; import { ServicesNavigatorPage } from "@/components/services/services-navigator-page"; +import { defaultServiceSlug } from "@/lib/services"; type ServicesSearchParams = Promise<{ [key: string]: string | string[] | undefined }>; @@ -19,7 +20,9 @@ export default async function ServicesIndexRoute({ searchParams }: { searchParam const hasSubmittedSearch = readFirstSearchParam(resolvedSearchParams.run) === "1" && query.length > 0; if (!hasSubmittedSearch) { - return ; + // Computed server-side so the client route chunk never bundles the + // services snapshot behind @/lib/services. + return ; } return ( diff --git a/src/components/services/services-home-page.tsx b/src/components/services/services-home-page.tsx index 4fd679faff..9c4bb24ceb 100644 --- a/src/components/services/services-home-page.tsx +++ b/src/components/services/services-home-page.tsx @@ -12,33 +12,37 @@ import { } from "@/components/mode-home-template"; import { appModeHomeHref } from "@/lib/app-modes"; import { modeHomeDesktopComposerSlotId } from "@/lib/mode-home-composer"; -import { defaultServiceSlug } from "@/lib/services"; import { countVerifiedRegistryRecords, useRegistryRecords } from "@/lib/use-registry-records"; -const taskCards: ModeHomeAction[] = [ - { - title: "Search services", - description: "Need, catchment, provider, keyword.", - icon: FileSearch, - href: appModeHomeHref("services", { focus: true }), - }, - { - title: "Check catchment", - description: "Region, public/private, eligibility.", - icon: MapPinned, - href: `/services/${defaultServiceSlug() ?? ""}`, - }, - { - title: "Browse referral pathways", - description: "Crisis, youth, Aboriginal health, telehealth.", - icon: Route, - href: appModeHomeHref("services", { - query: "crisis youth Aboriginal health telehealth referral pathway", - focus: true, - run: true, - }), - }, -]; +// The default service slug is computed server-side (app/services/page.tsx) and +// passed as a prop: a direct `@/lib/services` value import here would compile +// the full services snapshot (~100 KB gzip) into this client route chunk. +function buildTaskCards(defaultServiceSlug: string | null): ModeHomeAction[] { + return [ + { + title: "Search services", + description: "Need, catchment, provider, keyword.", + icon: FileSearch, + href: appModeHomeHref("services", { focus: true }), + }, + { + title: "Check catchment", + description: "Region, public/private, eligibility.", + icon: MapPinned, + href: `/services/${defaultServiceSlug ?? ""}`, + }, + { + title: "Browse referral pathways", + description: "Crisis, youth, Aboriginal health, telehealth.", + icon: Route, + href: appModeHomeHref("services", { + query: "crisis youth Aboriginal health telehealth referral pathway", + focus: true, + run: true, + }), + }, + ]; +} const commonPathways: ModeHomePill[] = [ { @@ -78,7 +82,8 @@ const commonPathways: ModeHomePill[] = [ }, ]; -export function ServicesHomePage() { +export function ServicesHomePage({ defaultServiceSlug = null }: { defaultServiceSlug?: string | null }) { + const taskCards = buildTaskCards(defaultServiceSlug); const registry = useRegistryRecords("service"); const verifiedCount = countVerifiedRegistryRecords(registry); const registryReady = registry.status === "ready"; From de587a23a9e73b108e0611de4e8178811f736598 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 16:23:03 +0000 Subject: [PATCH 2/8] docs: record the Phase 4 reviewed HEAD SHA in its ledger row Addresses the Codex P2 on PR #890: the Reviewed HEAD cell held prose because the row predated its own single-batch commit; it now cites the reviewed implementation commit ceabc04d, keeping the exact-hash ledger lookup functional. Row remains keyed by PR number + branch ref. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EXsJcLrbZUXwnBeG91cVo9 --- docs/branch-review-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index b36d2f9de6..531aa252df 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -609,4 +609,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-18 | codex/rag-merge-final-20260718-655a | 20e5964bbfdcac0311b89c96931a43544be545a6 | RAG recovery, grounding, and latency merge closure | Reviewed the recovered historical RAG fix series against fresh `origin/main`. No additional source change was needed: current main already contains the stricter source-scoped numeric grounding, blocked-retrieval-only extractive recovery, generic LAI deterministic recovery, per-case answer latency diagnostics, and agitation lexical-path protections. The only residual branch diff was proven unreachable because every medication-chart trigger yields explicit search terms; it was intentionally not carried forward. | Full local Vitest: 301 files / 2,779 tests passed. Offline RAG fixture and production-contract validation: 36 golden cases, 21 suites, 294 tests passed. Prettier and `git diff --check` passed. `verify:cheap` reached its lint stage after all preceding static guards passed, but was blocked twice by active heavyweight jobs in other registered worktrees; it was not retried. No Supabase, OpenAI, live retrieval, production-readiness, deployment, or hosted CI command was run. | | 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. | | 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. | -| 2026-07-18 | claude/clinical-kb-pwa-review-asi3wb (plan Phase 4 PR; keyed by PR number on open) | Phase 4 implementation commit on this branch (single-batch push; Phase 3 landed as squash 44a4c511bc1381168474794d6f89273564659ead) | App-wide performance pass: audit-then-fix + budget ratchet (plan Phase 4) | Audit on post-#872 main: enforced gzip budget +3.13% over the 1,363,382-byte baseline; analyzer treemap showed pdfjs-dist (123 KB) and cross-mode-differentials data (121 KB) already correctly lazy, and disproved the suspected forms-catalog dashboard leak (type-only import). Confirmed one real defect: the 143-line `/services` client home page value-imported `defaultServiceSlug` from `@/lib/services`, compiling the ~915 KB services snapshot (~100 KB gzip) into its route chunk. Fixed by computing the slug in the server page (`src/app/services/page.tsx`) and passing it as a prop; the client component builds its task cards from the prop. Measured result: budget swung from +3.13% to −3.97%; baseline ratcheted down 1,363,382 → 1,309,286 bytes gzip so CI locks the win. pdfjs/differentials/#718 paths deliberately untouched per audit rules. Two transient `.next/dev` generated-type corruptions from the long-lived dev server were resolved by stopping the server and setting the generated dir aside (reversible); nothing hand-deleted. | Build + `check:bundle-budget --json` before/after (artifacts in session scratchpad); `check:bundle-budget -- --update` for the ratchet; `npm run test` 2789 passed/1 failed (known container-only `pdf-extraction-budget` artifact, hosted-CI-green through #826/#835/#872); `verify:cheap` green to the same artifact; `verify:ui` 219 passed/2 failed (the two long-established container artifacts; ui-tools spec covering the services surface passed); `verify:pr-local` unit stage identical; typecheck/lint/format clean. Lighthouse not run (dev-server churn; bundle evidence sufficed for this fix set). No provider-backed checks run. | +| 2026-07-18 | claude/clinical-kb-pwa-review-asi3wb (PR #890, plan Phase 4) | ceabc04d75fd0cea2a120d504418d44edd704bfb (single-batch implementation+ledger commit; Phase 3 landed as squash 44a4c511bc1381168474794d6f89273564659ead) | App-wide performance pass: audit-then-fix + budget ratchet (plan Phase 4) | Audit on post-#872 main: enforced gzip budget +3.13% over the 1,363,382-byte baseline; analyzer treemap showed pdfjs-dist (123 KB) and cross-mode-differentials data (121 KB) already correctly lazy, and disproved the suspected forms-catalog dashboard leak (type-only import). Confirmed one real defect: the 143-line `/services` client home page value-imported `defaultServiceSlug` from `@/lib/services`, compiling the ~915 KB services snapshot (~100 KB gzip) into its route chunk. Fixed by computing the slug in the server page (`src/app/services/page.tsx`) and passing it as a prop; the client component builds its task cards from the prop. Measured result: budget swung from +3.13% to −3.97%; baseline ratcheted down 1,363,382 → 1,309,286 bytes gzip so CI locks the win. pdfjs/differentials/#718 paths deliberately untouched per audit rules. Two transient `.next/dev` generated-type corruptions from the long-lived dev server were resolved by stopping the server and setting the generated dir aside (reversible); nothing hand-deleted. | Build + `check:bundle-budget --json` before/after (artifacts in session scratchpad); `check:bundle-budget -- --update` for the ratchet; `npm run test` 2789 passed/1 failed (known container-only `pdf-extraction-budget` artifact, hosted-CI-green through #826/#835/#872); `verify:cheap` green to the same artifact; `verify:ui` 219 passed/2 failed (the two long-established container artifacts; ui-tools spec covering the services surface passed); `verify:pr-local` unit stage identical; typecheck/lint/format clean. Lighthouse not run (dev-server churn; bundle evidence sufficed for this fix set). No provider-backed checks run. | From 57b28021a3b481684b37bb8751a078e718904d00 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 16:29:11 +0000 Subject: [PATCH 3/8] test: lock the services snapshot out of client modules Addresses the Codex P2 on PR #890: the ratcheted bundle budget's 10% tolerance still sits above the pre-fix total, so it cannot catch a re-introduction of the services-snapshot client import on its own. This source-level boundary test fails the unit suite (inside verify:cheap) the moment any client module value-imports @/lib/services; type-only imports remain allowed. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EXsJcLrbZUXwnBeG91cVo9 --- tests/services-client-boundary.test.ts | 46 ++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/services-client-boundary.test.ts diff --git a/tests/services-client-boundary.test.ts b/tests/services-client-boundary.test.ts new file mode 100644 index 0000000000..67d1b78d40 --- /dev/null +++ b/tests/services-client-boundary.test.ts @@ -0,0 +1,46 @@ +import { readFileSync, readdirSync, statSync } from "node:fs"; +import { join, relative } from "node:path"; +import { describe, expect, it } from "vitest"; + +// Regression guard for the PR #890 fix: the ~915 KB services snapshot reaches +// the client the moment any client module value-imports "@/lib/services" +// (~100 KB gzip in that route chunk). The enforced bundle budget alone cannot +// catch a re-introduction — its 10% tolerance sits above the pre-fix total — +// so this boundary is asserted at the source level. Type-only imports are +// erased at compile time and stay allowed. +const CLIENT_ROOTS = ["src/components", "src/app"]; +const VALUE_IMPORT_PATTERN = /^import\s+(?!type\b)[^;]*?from\s+["']@\/lib\/services["']/m; + +function collectSourceFiles(dir: string): string[] { + return readdirSync(dir).flatMap((entry) => { + const fullPath = join(dir, entry); + if (statSync(fullPath).isDirectory()) return collectSourceFiles(fullPath); + return /\.(?:ts|tsx)$/.test(entry) ? [fullPath] : []; + }); +} + +function isClientModule(source: string): boolean { + return /^\s*["']use client["']/m.test(source.slice(0, 400)); +} + +describe("services snapshot client boundary", () => { + it("keeps @/lib/services value-imports out of client modules", () => { + const offenders: string[] = []; + + for (const root of CLIENT_ROOTS) { + for (const filePath of collectSourceFiles(join(process.cwd(), root))) { + const source = readFileSync(filePath, "utf8"); + if (isClientModule(source) && VALUE_IMPORT_PATTERN.test(source)) { + offenders.push(relative(process.cwd(), filePath)); + } + } + } + + expect( + offenders, + "Client modules must not value-import @/lib/services: it compiles the full services snapshot " + + "into their chunk. Compute what you need server-side and pass it as a prop " + + "(see src/app/services/page.tsx), or use `import type` for types only.", + ).toEqual([]); + }); +}); From b1f3c90b8fbf123fec9da8b6dc64025fa990b0c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 16:34:36 +0000 Subject: [PATCH 4/8] test: cover transitive graphs and refine import classification in the services guard Addresses the Codex and CodeRabbit findings on PR #890: the boundary now walks value-import graphs from every 'use client' entry (a helper without the directive still lands in the client chunk), treats side-effect imports and value re-exports as runtime, and stops flagging named type-only import clauses that compile away. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EXsJcLrbZUXwnBeG91cVo9 --- tests/services-client-boundary.test.ts | 126 +++++++++++++++++++++---- 1 file changed, 106 insertions(+), 20 deletions(-) diff --git a/tests/services-client-boundary.test.ts b/tests/services-client-boundary.test.ts index 67d1b78d40..2870e60030 100644 --- a/tests/services-client-boundary.test.ts +++ b/tests/services-client-boundary.test.ts @@ -1,15 +1,38 @@ -import { readFileSync, readdirSync, statSync } from "node:fs"; -import { join, relative } from "node:path"; +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { dirname, join, relative, resolve } from "node:path"; import { describe, expect, it } from "vitest"; // Regression guard for the PR #890 fix: the ~915 KB services snapshot reaches -// the client the moment any client module value-imports "@/lib/services" -// (~100 KB gzip in that route chunk). The enforced bundle budget alone cannot -// catch a re-introduction — its 10% tolerance sits above the pre-fix total — -// so this boundary is asserted at the source level. Type-only imports are -// erased at compile time and stay allowed. -const CLIENT_ROOTS = ["src/components", "src/app"]; -const VALUE_IMPORT_PATTERN = /^import\s+(?!type\b)[^;]*?from\s+["']@\/lib\/services["']/m; +// the client the moment "@/lib/services" is value-imported anywhere in a +// client module graph (~100 KB gzip in that chunk). The enforced bundle +// budget alone cannot catch a re-introduction — its 10% tolerance sits above +// the pre-fix total — so the boundary is asserted at the source level, and it +// must be TRANSITIVE: a helper without its own "use client" directive still +// lands in the client bundle when a client component imports it. Type-only +// imports are erased at compile time and stay allowed. +const SRC_ROOT = join(process.cwd(), "src"); +const TARGET_SPECIFIER = "@/lib/services"; +// Side-effect imports have no `from` clause but still execute the module. +const SIDE_EFFECT_IMPORT_PATTERN = /^import\s+["']([^"']+)["']/gm; +// import/export ... from "..." — clause analysed by hasRuntimeBindings below so +// `import type`, `export type`, and named clauses whose specifiers are all +// `type X` (including multiline) stay allowed while default, namespace, mixed, +// star re-export, and value re-export forms are treated as runtime. +const FROM_STATEMENT_PATTERN = /^(import|export)\s+([\s\S]+?)\s+from\s+["']([^"']+)["']/gm; + +function hasRuntimeBindings(kind: string, clause: string): boolean { + const trimmed = clause.trim(); + if (/^type\b/.test(trimmed)) return false; + const named = trimmed.match(/^\{([\s\S]*)\}$/); + if (named) { + return named[1] + .split(",") + .map((specifier) => specifier.trim()) + .filter(Boolean) + .some((specifier) => !/^type\b/.test(specifier)); + } + return kind === "import" || kind === "export"; +} function collectSourceFiles(dir: string): string[] { return readdirSync(dir).flatMap((entry) => { @@ -19,28 +42,91 @@ function collectSourceFiles(dir: string): string[] { }); } -function isClientModule(source: string): boolean { - return /^\s*["']use client["']/m.test(source.slice(0, 400)); +function resolveImport(specifier: string, fromFile: string): string | null { + let base: string; + if (specifier.startsWith("@/")) base = join(SRC_ROOT, specifier.slice(2)); + else if (specifier.startsWith(".")) base = resolve(dirname(fromFile), specifier); + else return null; + + for (const candidate of [base, `${base}.ts`, `${base}.tsx`, join(base, "index.ts"), join(base, "index.tsx")]) { + if (existsSync(candidate) && statSync(candidate).isFile()) return candidate; + } + return null; +} + +interface ModuleInfo { + importsServices: boolean; + isClientEntry: boolean; + valueImports: string[]; +} + +function buildModuleGraph(): Map { + const graph = new Map(); + + for (const filePath of collectSourceFiles(SRC_ROOT)) { + const source = readFileSync(filePath, "utf8"); + const valueImports: string[] = []; + let importsServices = false; + + const recordRuntimeSpecifier = (specifier: string) => { + if (specifier === TARGET_SPECIFIER || specifier.startsWith(`${TARGET_SPECIFIER}/`)) importsServices = true; + const resolved = resolveImport(specifier, filePath); + if (resolved) valueImports.push(resolved); + }; + + for (const match of source.matchAll(SIDE_EFFECT_IMPORT_PATTERN)) recordRuntimeSpecifier(match[1]); + for (const match of source.matchAll(FROM_STATEMENT_PATTERN)) { + if (hasRuntimeBindings(match[1], match[2])) recordRuntimeSpecifier(match[3]); + } + + graph.set(filePath, { + importsServices, + isClientEntry: /^\s*["']use client["']/m.test(source.slice(0, 400)), + valueImports, + }); + } + + return graph; } describe("services snapshot client boundary", () => { - it("keeps @/lib/services value-imports out of client modules", () => { + it("keeps @/lib/services value-imports out of every client module graph", () => { + const graph = buildModuleGraph(); const offenders: string[] = []; - for (const root of CLIENT_ROOTS) { - for (const filePath of collectSourceFiles(join(process.cwd(), root))) { - const source = readFileSync(filePath, "utf8"); - if (isClientModule(source) && VALUE_IMPORT_PATTERN.test(source)) { - offenders.push(relative(process.cwd(), filePath)); + for (const [entryPath, entry] of graph) { + if (!entry.isClientEntry) continue; + + const cameFrom = new Map([[entryPath, ""]]); + const queue = [entryPath]; + while (queue.length > 0) { + const currentPath = queue.shift() as string; + const current = graph.get(currentPath); + if (!current) continue; + + if (current.importsServices) { + const chain: string[] = []; + for (let step: string | undefined = currentPath; step; step = cameFrom.get(step) || undefined) { + chain.unshift(relative(process.cwd(), step)); + } + offenders.push(chain.join(" -> ")); + break; + } + for (const next of current.valueImports) { + if (!cameFrom.has(next)) { + cameFrom.set(next, currentPath); + queue.push(next); + } } } } expect( offenders, - "Client modules must not value-import @/lib/services: it compiles the full services snapshot " + - "into their chunk. Compute what you need server-side and pass it as a prop " + - "(see src/app/services/page.tsx), or use `import type` for types only.", + "Client module graphs must not value-import @/lib/services: it compiles the full services " + + "snapshot into their chunk. Compute what you need server-side and pass it as a prop " + + "(see src/app/services/page.tsx), or use `import type` for types only. Chains shown as " + + "client entry -> ... -> importing module.", ).toEqual([]); }); }); From 83c9293210a107940d97852ba63c28d1d67e8b21 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 16:39:06 +0000 Subject: [PATCH 5/8] test: match resolved paths for the services boundary guard Addresses the Codex P2 on PR #890: relative specifiers like ../lib/services resolved in the graph walk but never set the services-import flag; the guard now matches the resolved module path in addition to the alias text. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EXsJcLrbZUXwnBeG91cVo9 --- tests/services-client-boundary.test.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/services-client-boundary.test.ts b/tests/services-client-boundary.test.ts index 2870e60030..7c56cf2045 100644 --- a/tests/services-client-boundary.test.ts +++ b/tests/services-client-boundary.test.ts @@ -54,6 +54,14 @@ function resolveImport(specifier: string, fromFile: string): string | null { return null; } +// Every on-disk shape the "@/lib/services" module could resolve to, so +// relative-path imports are treated identically to the alias. +const SERVICES_MODULE_PATHS = new Set( + ["lib/services.ts", "lib/services.tsx", "lib/services/index.ts", "lib/services/index.tsx"].map((candidate) => + join(SRC_ROOT, candidate), + ), +); + interface ModuleInfo { importsServices: boolean; isClientEntry: boolean; @@ -69,8 +77,16 @@ function buildModuleGraph(): Map { let importsServices = false; const recordRuntimeSpecifier = (specifier: string) => { - if (specifier === TARGET_SPECIFIER || specifier.startsWith(`${TARGET_SPECIFIER}/`)) importsServices = true; const resolved = resolveImport(specifier, filePath); + // Match the alias text AND the resolved file, so relative specifiers + // like ../lib/services are caught identically. + if ( + specifier === TARGET_SPECIFIER || + specifier.startsWith(`${TARGET_SPECIFIER}/`) || + (resolved !== null && SERVICES_MODULE_PATHS.has(resolved)) + ) { + importsServices = true; + } if (resolved) valueImports.push(resolved); }; From 838624535ef78a66756579dceeca826feb59bead Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 16:44:09 +0000 Subject: [PATCH 6/8] test: detect dynamic imports and full directive prologues in the services guard Addresses the round-4 Codex and CodeRabbit findings on PR #890: dynamic import() expressions now count as runtime edges (they still emit client JavaScript), and 'use client' detection parses the complete comment/ whitespace prologue instead of capping at 400 characters. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EXsJcLrbZUXwnBeG91cVo9 --- tests/services-client-boundary.test.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/tests/services-client-boundary.test.ts b/tests/services-client-boundary.test.ts index 7c56cf2045..53c0a5f5ea 100644 --- a/tests/services-client-boundary.test.ts +++ b/tests/services-client-boundary.test.ts @@ -14,6 +14,9 @@ const SRC_ROOT = join(process.cwd(), "src"); const TARGET_SPECIFIER = "@/lib/services"; // Side-effect imports have no `from` clause but still execute the module. const SIDE_EFFECT_IMPORT_PATTERN = /^import\s+["']([^"']+)["']/gm; +// Dynamic import() expressions defer loading but still emit client JavaScript +// for the target, so they count as runtime edges too. +const DYNAMIC_IMPORT_PATTERN = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g; // import/export ... from "..." — clause analysed by hasRuntimeBindings below so // `import type`, `export type`, and named clauses whose specifiers are all // `type X` (including multiline) stay allowed while default, namespace, mixed, @@ -62,6 +65,13 @@ const SERVICES_MODULE_PATHS = new Set( ), ); +// Strip the full directive prologue (comments and whitespace of any length) +// so a "use client" directive after a long header comment is still detected. +function isClientEntry(source: string): boolean { + const prologue = source.replace(/^(?:\s+|\/\/[^\n]*(?:\n|$)|\/\*[\s\S]*?\*\/)*/, ""); + return /^["']use client["']/.test(prologue); +} + interface ModuleInfo { importsServices: boolean; isClientEntry: boolean; @@ -91,13 +101,14 @@ function buildModuleGraph(): Map { }; for (const match of source.matchAll(SIDE_EFFECT_IMPORT_PATTERN)) recordRuntimeSpecifier(match[1]); + for (const match of source.matchAll(DYNAMIC_IMPORT_PATTERN)) recordRuntimeSpecifier(match[1]); for (const match of source.matchAll(FROM_STATEMENT_PATTERN)) { if (hasRuntimeBindings(match[1], match[2])) recordRuntimeSpecifier(match[3]); } graph.set(filePath, { importsServices, - isClientEntry: /^\s*["']use client["']/m.test(source.slice(0, 400)), + isClientEntry: isClientEntry(source), valueImports, }); } From 0e74e8480cd607452393c6beed83a2a7c283d3ad Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 16:48:01 +0000 Subject: [PATCH 7/8] test: match magic-comment dynamic imports in the services guard Addresses the round-5 Codex finding on PR #890: dynamic imports with webpack/Next magic comments before the specifier now count as runtime edges. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EXsJcLrbZUXwnBeG91cVo9 --- tests/services-client-boundary.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/services-client-boundary.test.ts b/tests/services-client-boundary.test.ts index 53c0a5f5ea..9ffb476459 100644 --- a/tests/services-client-boundary.test.ts +++ b/tests/services-client-boundary.test.ts @@ -15,8 +15,9 @@ const TARGET_SPECIFIER = "@/lib/services"; // Side-effect imports have no `from` clause but still execute the module. const SIDE_EFFECT_IMPORT_PATTERN = /^import\s+["']([^"']+)["']/gm; // Dynamic import() expressions defer loading but still emit client JavaScript -// for the target, so they count as runtime edges too. -const DYNAMIC_IMPORT_PATTERN = /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g; +// for the target, so they count as runtime edges too — including with +// webpack/Next magic comments before the specifier. +const DYNAMIC_IMPORT_PATTERN = /\bimport\s*\(\s*(?:\/\*[\s\S]*?\*\/\s*)*["']([^"']+)["'][\s\S]*?\)/g; // import/export ... from "..." — clause analysed by hasRuntimeBindings below so // `import type`, `export type`, and named clauses whose specifiers are all // `type X` (including multiline) stay allowed while default, namespace, mixed, From 649a3250db85eeab684339a1b9411a8c22b308c5 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 18 Jul 2026 17:03:04 +0000 Subject: [PATCH 8/8] docs: keep the Phase 4 reviewed-HEAD ledger cell to the bare SHA Addresses the Codex P2 on PR #890: the cell held prose after the SHA, defeating the ledger's exact rev-parse match; context moved to the branch/ref cell. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EXsJcLrbZUXwnBeG91cVo9 --- docs/branch-review-ledger.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index bb01281337..b1d8b0d1e2 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -610,4 +610,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD | 2026-07-18 | codex/rag-merge-final-20260718-655a | 20e5964bbfdcac0311b89c96931a43544be545a6 | RAG recovery, grounding, and latency merge closure | Reviewed the recovered historical RAG fix series against fresh `origin/main`. No additional source change was needed: current main already contains the stricter source-scoped numeric grounding, blocked-retrieval-only extractive recovery, generic LAI deterministic recovery, per-case answer latency diagnostics, and agitation lexical-path protections. The only residual branch diff was proven unreachable because every medication-chart trigger yields explicit search terms; it was intentionally not carried forward. | Full local Vitest: 301 files / 2,779 tests passed. Offline RAG fixture and production-contract validation: 36 golden cases, 21 suites, 294 tests passed. Prettier and `git diff --check` passed. `verify:cheap` reached its lint stage after all preceding static guards passed, but was blocked twice by active heavyweight jobs in other registered worktrees; it was not retried. No Supabase, OpenAI, live retrieval, production-readiness, deployment, or hosted CI command was run. | | 2026-07-18 | codex/main-merge-51278-final-20260718-late | 45fa3c6c7, 14a0a898c | merge integration of 51278a70d onto fresh origin/main | Replayed the requested historical design-audit commit onto fresh `origin/main`. Kept current-main versions for five conflicts, including the regenerated drift manifest and current schema assertions. Fixed the duplicate sitemap-generator declaration, corrected redirect-section coverage, and restored the current `Clinical Guide` UI contract. | `git diff --check` and `npm run sitemap:check` passed. `npm run test` was attempted twice but blocked by the repository-wide heavy-command lock held by a separate Playwright worktree; no provider-backed checks ran. | | 2026-07-18 | origin/main framework and dependency modernization snapshot | 4057677c8b92a5e1d997ec44958764fa91f5d424 | parallel build/infra, backend, and frontend modernization audit | Changes requested. No P0. Confirmed two P1 defects: Supabase SSR 0.12 auth-cookie responses discard mandatory anti-cache headers, and reindex bypasses the server-only-aware TSX runner after mutation-capable setup. Five P2 blockers cover the Webpack-to-Turbopack production cutover, incomplete Railway image-build watch ownership, missing clean `next typegen`, Node 26 types over a Node 24 runtime, and App Router retry actions that reset without re-fetching. P3 removal-readiness debt remains in Zod and Next Image APIs. Manual rewrite zones are auth response ownership, bundler/CSP/artifact consumers, JSZip resource limits, OpenAI request typing, and React Compiler adoption in the stateful dashboard/viewer roots. | Parallel read-only source/config/test audit against the exact snapshot; Node 24.18.0/npm 11.17.0; exact-version Next 16.2.10 bundled upgrade, Turbopack, error-boundary, and codemod guidance; TypeScript 6 backend no-emit analysis and Node import probes via a separately installed exact-version local dependency tree; `git diff --check`. No install, registry outdated/audit, full repo lint/typecheck/test/build/browser/Docker, Supabase/OpenAI, deployment, or hosted CI run; this worktree had no `node_modules`. | -| 2026-07-18 | claude/clinical-kb-pwa-review-asi3wb (PR #890, plan Phase 4) | ceabc04d75fd0cea2a120d504418d44edd704bfb (single-batch implementation+ledger commit; Phase 3 landed as squash 44a4c511bc1381168474794d6f89273564659ead) | App-wide performance pass: audit-then-fix + budget ratchet (plan Phase 4) | Audit on post-#872 main: enforced gzip budget +3.13% over the 1,363,382-byte baseline; analyzer treemap showed pdfjs-dist (123 KB) and cross-mode-differentials data (121 KB) already correctly lazy, and disproved the suspected forms-catalog dashboard leak (type-only import). Confirmed one real defect: the 143-line `/services` client home page value-imported `defaultServiceSlug` from `@/lib/services`, compiling the ~915 KB services snapshot (~100 KB gzip) into its route chunk. Fixed by computing the slug in the server page (`src/app/services/page.tsx`) and passing it as a prop; the client component builds its task cards from the prop. Measured result: budget swung from +3.13% to −3.97%; baseline ratcheted down 1,363,382 → 1,309,286 bytes gzip so CI locks the win. pdfjs/differentials/#718 paths deliberately untouched per audit rules. Two transient `.next/dev` generated-type corruptions from the long-lived dev server were resolved by stopping the server and setting the generated dir aside (reversible); nothing hand-deleted. | Build + `check:bundle-budget --json` before/after (artifacts in session scratchpad); `check:bundle-budget -- --update` for the ratchet; `npm run test` 2789 passed/1 failed (known container-only `pdf-extraction-budget` artifact, hosted-CI-green through #826/#835/#872); `verify:cheap` green to the same artifact; `verify:ui` 219 passed/2 failed (the two long-established container artifacts; ui-tools spec covering the services surface passed); `verify:pr-local` unit stage identical; typecheck/lint/format clean. Lighthouse not run (dev-server churn; bundle evidence sufficed for this fix set). No provider-backed checks run. | +| 2026-07-18 | claude/clinical-kb-pwa-review-asi3wb (PR #890, plan Phase 4; single-batch implementation+ledger commit — Phase 3 landed as squash 44a4c511bc1381168474794d6f89273564659ead) | ceabc04d75fd0cea2a120d504418d44edd704bfb | App-wide performance pass: audit-then-fix + budget ratchet (plan Phase 4) | Audit on post-#872 main: enforced gzip budget +3.13% over the 1,363,382-byte baseline; analyzer treemap showed pdfjs-dist (123 KB) and cross-mode-differentials data (121 KB) already correctly lazy, and disproved the suspected forms-catalog dashboard leak (type-only import). Confirmed one real defect: the 143-line `/services` client home page value-imported `defaultServiceSlug` from `@/lib/services`, compiling the ~915 KB services snapshot (~100 KB gzip) into its route chunk. Fixed by computing the slug in the server page (`src/app/services/page.tsx`) and passing it as a prop; the client component builds its task cards from the prop. Measured result: budget swung from +3.13% to −3.97%; baseline ratcheted down 1,363,382 → 1,309,286 bytes gzip so CI locks the win. pdfjs/differentials/#718 paths deliberately untouched per audit rules. Two transient `.next/dev` generated-type corruptions from the long-lived dev server were resolved by stopping the server and setting the generated dir aside (reversible); nothing hand-deleted. | Build + `check:bundle-budget --json` before/after (artifacts in session scratchpad); `check:bundle-budget -- --update` for the ratchet; `npm run test` 2789 passed/1 failed (known container-only `pdf-extraction-budget` artifact, hosted-CI-green through #826/#835/#872); `verify:cheap` green to the same artifact; `verify:ui` 219 passed/2 failed (the two long-established container artifacts; ui-tools spec covering the services surface passed); `verify:pr-local` unit stage identical; typecheck/lint/format clean. Lighthouse not run (dev-server churn; bundle evidence sufficed for this fix set). No provider-backed checks run. |