From 1b07b3f17f6645de717f82976b1d63bbf8304a40 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 07:51:00 +0000 Subject: [PATCH 1/4] test(factsheets): census every h1 in the factsheet detail document MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hero

and the portaled print sheet's

are correct and mutually exclusive by construction: on screen `.factsheet-print-sheet { display: none }` removes the print subtree, and in print `html.factsheets-printing body > *:not(.factsheet-print-portal)` removes the shell that owns the hero. Neither state exposes two headings to the accessibility tree, and the printed PDF is a separate document whose section headings are already

, so demoting its title would leave it with no top-level heading. The real gap was that the existing assertion was scoped to the page testid, so the document-level invariant was asserted nowhere and a stray third

would not have been caught. Pin the census instead: exactly two, one per container, both carrying the title, plus a non-empty

outline in the print sheet. jsdom applies no stylesheet, so a census is the right guard rather than a visibility assertion. FactsheetPrintSheet stays in factsheet-detail-page.tsx — design-system-contract-utils.mjs scopes its raw-colour exemption to the literal factsheet-print-sheet marker and fails closed if it moves. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YEFowCVUVrybKvKvReQ924 --- tests/factsheet-detail-header.dom.test.tsx | 34 ++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tests/factsheet-detail-header.dom.test.tsx b/tests/factsheet-detail-header.dom.test.tsx index aa40115f9d..fd7724cf75 100644 --- a/tests/factsheet-detail-header.dom.test.tsx +++ b/tests/factsheet-detail-header.dom.test.tsx @@ -15,8 +15,8 @@ describe("factsheet detail header", () => { it("keeps the record title as the on-screen page's only h1", () => { // The header title is a ``; the hero owns the heading. Two `

`s // with the same text is the failure this guards. Scoped to the shell: the - // print sheet is portaled to and carries its own `

`, but it is - // `display: none` on screen and predates this header. + // print sheet is portaled to and carries its own `

`, which the + // next test pins as deliberate rather than accidental. const { factsheet } = renderFactsheet("sertraline"); const page = screen.getByTestId("factsheet-detail-page"); const headings = within(page).getAllByRole("heading", { level: 1 }); @@ -24,6 +24,36 @@ describe("factsheet detail header", () => { expect(headings[0]).toHaveTextContent(factsheet.title); }); + it("accounts for every h1 in the document: the hero and the print sheet, never both visible", () => { + // The document carries exactly two `

`s and they are mutually exclusive + // by construction, so neither state ever exposes two to the accessibility + // tree (`#295`): + // - on screen, `.factsheet-print-sheet { display: none }` removes the + // print sheet's subtree entirely; + // - in print, `html.factsheets-printing body > *:not(.factsheet-print-portal)` + // is `display: none !important`, removing the shell that owns the hero. + // The print sheet is a separate printed document with its own outline (its + // section headings are `

`), so demoting its `

` would leave the + // exported PDF with no top-level heading. jsdom applies no stylesheet, so + // the guard is this census rather than a visibility assertion: the previous + // testid-scoped check alone would not have caught a third `

` appearing + // anywhere outside the page shell. + const { factsheet } = renderFactsheet("sertraline"); + const page = screen.getByTestId("factsheet-detail-page"); + const printPortal = document.querySelector(".factsheet-print-portal"); + expect(printPortal).not.toBeNull(); + + const allHeadings = screen.getAllByRole("heading", { level: 1 }); + expect(allHeadings).toHaveLength(2); + for (const heading of allHeadings) expect(heading).toHaveTextContent(factsheet.title); + + expect(allHeadings.filter((heading) => page.contains(heading))).toHaveLength(1); + expect(allHeadings.filter((heading) => printPortal!.contains(heading))).toHaveLength(1); + // The printed document starts at h1 and descends; nothing in it may outrank + // its title. + expect(within(printPortal as HTMLElement).getAllByRole("heading", { level: 2 }).length).toBeGreaterThan(0); + }); + it("names the way back without spending the row on its label", () => { renderFactsheet("sertraline"); const back = screen.getByRole("link", { name: "Back to all factsheets" }); From 6238bacf3297bf9336a9dcf8b1df043795e987eb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 07:51:12 +0000 Subject: [PATCH 2/4] docs(testing): record the verified recipe for restoring local browser gates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The remote/Cloud drift note said to delegate browser proof to CI and left the impression that local gates were unrecoverable. They are recoverable; the blocker was two separate image faults, and the second is why the obvious fix looks impossible. The baked node_modules is stale or incomplete — containers have shipped none at all, and earlier ones reported playwright 1.62.0 against a locked 1.62.1 with tailwind-merge absent entirely, which is an incomplete install rather than a version skew, so the lockfile pin was never wrong. And npm ci cannot repair it because jsdom@30.0.1 requires node ^22.22.2 || ^24.15.0 || >=26.0.0 while images have shipped v24.13.0, so the install dies on EBADENGINE under engine-strict. Installing Node 24.19.0 clears that, npm ci then exits 0 and parity reports all seven pinned packages, and `npx playwright install` supplies Chromium 1234 (images ship only 1194). Verified end to end this session, launch included. Keeps the existing Stop intact and makes it cheap to honour: install the matching revision rather than forcing a run against 1194. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YEFowCVUVrybKvKvReQ924 --- docs/testing.md | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/docs/testing.md b/docs/testing.md index 73a75b57fe..04b28a7f7c 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -46,7 +46,28 @@ export PLAYWRIGHT_KEEP_BUILD_ROOT=true **Refuted levers (do not revive):** persistent Actions cache for the Next webpack tree (~804 MB, evicts browser cache); splitting `ui-phone-scroll*` to rebalance `--shard` (siblings still co-land); renaming specs to game alphabetical shard order; Playwright `workers > 1` or blocking retries; dropping Production UI from ordinary UI PRs; Firefox/WebKit on every PR (main/weekly matrix only). -**Remote / Cloud browser drift.** When `check:installed-lock-parity` fails on `playwright`, or `check:playwright-browser-revision` reports `/opt/pw-browsers` revision drift, do **not** point `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` at a mismatched shell. Delegate browser proof to CI Production UI (or refresh the image/install matching browsers). See `#255` and [codex-cloud.md](codex-cloud.md). +**Remote / Cloud browser drift.** When `check:installed-lock-parity` fails on `playwright`, or `check:playwright-browser-revision` reports `/opt/pw-browsers` revision drift, do **not** point `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` at a mismatched shell and do **not** set `PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD` to force a run — a browser gate against the wrong revision is not evidence. Delegating browser proof to CI Production UI is always valid. Restoring the gates locally is also possible; the recipe below was verified end to end on 2026-08-09 (`#255`). See also [codex-cloud.md](codex-cloud.md). + +Two separate image faults produce this, and the second is why the obvious fix looks impossible: + +1. **The baked `node_modules` is stale or incomplete.** Symptoms range from no `node_modules` at all to `playwright: installed 1.62.0 does not match locked 1.62.1` with `tailwind-merge` missing entirely. The lockfile is not wrong — do not re-pin it to the installed version. +2. **The image's Node is too old to run `npm ci`.** `jsdom@30.0.1` requires `^22.22.2 || ^24.15.0 || >=26.0.0`; images have shipped v24.13.0, so `npm ci --include=dev` dies on `EBADENGINE` under `engine-strict=true`. Never bypass with `--force`, `--legacy-peer-deps`, or `--engine-strict=false`. + +```bash +# 1. Node >= 24.15.0 (satisfies both the repo's 24.x engine and jsdom's floor). +curl -sSL -o /tmp/node24.tar.xz https://nodejs.org/dist/v24.19.0/node-v24.19.0-linux-x64.tar.xz +mkdir -p /root/.node24 && tar -xf /tmp/node24.tar.xz -C /root/.node24/ +export PATH=/root/.node24/node-v24.19.0-linux-x64/bin:$PATH # node v24.19.0, npm 11.17.0 + +# 2. Real install. Expect exit 0; then parity prints all seven pinned packages. +npm ci --include=dev && npm run check:installed-lock-parity + +# 3. Browsers. Playwright 1.62.1 wants Chromium 1234; images have shipped only 1194. +unset PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD +npx playwright install chromium chromium-headless-shell # installs into PLAYWRIGHT_BROWSERS_PATH +``` + +Costs roughly 5 minutes and ~330 MB (184 MB chromium + 115 MB headless shell + 32 MB node), needs a few GB free, and is paid **per session** because the container is ephemeral. The durable fix is still an image that ships Node ≥ 24.15.0, a complete `npm ci`, and the locked Chromium revision. Codex Cloud agents remain provider-free. Run authenticated Supabase tests through the manual `.github/workflows/authenticated-live-tests.yml` workflow, which requires the From f77e8db59fbcd1deb6b943fe02a1e9b4f191c2fd Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 07:51:29 +0000 Subject: [PATCH 3/4] fix(bundle-budget): split production weight from mockup scratch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One number could not honestly answer two questions. totalGzipBytes summed every built client chunk, including src/app/mockups/** design scratch that 404s in production, against a ceiling named as though it were production weight. #013 held that mockup chunks are not a production bundle; this gate charged them anyway, which is how PR #1580 blocked at +10.1% for chunks no user can load. #252 recorded the contradiction and left the metric undecided. Measured on a clean build of main at af85cbc, the blur had become the whole signal: 1546.5 KiB total was +9.96% of the 1406.4 KiB baseline — 576 bytes from failing Build — while production-only was 1279.1 KiB, 9.06% BELOW that same baseline. Every byte of the apparent regression was design scratch (267.5 KiB across 76 chunks over 66 mockup routes) and production had actually shrunk. latency-audit-2026-07-28 corroborates: 1,309,274 bytes then against 1,309,772 production-only now, flat to +0.04%, so the 2026-08-04 bump to 1,440,201 had absorbed mockup growth as production growth. Raising the ceiling again would have hidden that permanently, so this splits rather than ratchets. production (10%) covers every chunk a non-mockup route reaches plus chunks no manifest claims — framework, polyfills, runtime. mockups (25%) covers chunks reachable only from /mockups/**, as a runaway detector rather than a per-mockup gate; a ceiling tight enough to fire on the next mockup would just be --update'd reflexively. A chunk shared by both counts as production because it would be built either way. Attribution reads the per-route *_client-reference-manifest.js files under .next/server/app, since Next 16 webpack emits no app-build-manifest.json, and fails closed when that tree is missing or resolves no routes so the buckets can never silently collapse. Both fail paths proven against the real build. Also captures #296: pr-handoff-stop.test.ts fails in any root container because it chmods a fixture dir to force a write failure and root ignores permission bits — pre-existing, reproduced on clean af85cbc. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01YEFowCVUVrybKvKvReQ924 --- AGENTS.md | 31 ++- CLAUDE.md | 11 +- bundle-budget.json | 14 +- docs/outstanding-issues.md | 29 +- docs/plans/document-viewer-phase3-handover.md | 6 +- scripts/check-bundle-budget.mjs | 255 ++++++++++++++++-- tests/bundle-budget.test.ts | 147 +++++++++- 7 files changed, 442 insertions(+), 51 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 2742f9dd7b..bebe2c366c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -289,12 +289,39 @@ action must perform one; a page that ships must be reachable. allowlist (redirect targets / legacy-compat routes). Both run in `verify:cheap` and CI. Mockups (`src/app/mockups/**`, `*-mockups.tsx`) are design-scratch and exempt from both — **and from nothing else**. Mockups are compiled like any other source: they are typechecked, and their client - chunks count toward `check:bundle-budget`'s repo-wide total, so a mockup-only PR can still fail - `Build` (PR #1580, `+10.1%` against a 10% tolerance). Do not read "exempt" as "free". + chunks are still weighed by `check:bundle-budget` — against the separate `mockups` scratch budget, + not the `production` one (reconciled 2026-08-09; see "Bundle budget" below). Do not read "exempt" + as "free". - **Never** add a production page route without either an inbound link or a documented reachability allowlist entry plus an `/issues` note, and never silence the button-wiring rule with a blanket disable — wire the control or make it an explicit placeholder. +# Bundle budget + +`check:bundle-budget` enforces **two** baselines in `bundle-budget.json`, because one number could +not honestly answer both questions (`#013` vs `#252`, reconciled 2026-08-09): + +- **`production`** — every chunk a non-mockup route reaches, plus chunks no route manifest claims + (framework, polyfills, runtime). This is user-facing weight and the real regression guard. + Tolerance 10%. A failure here means find the regression; do not refresh the baseline to clear it. +- **`mockups`** — chunks reachable **only** from `/mockups/**`. Nobody downloads these, so this is a + repo-hygiene ceiling for unbounded accumulation, not a per-mockup gate. Tolerance 25%. + +A chunk shared by a mockup and a production route counts as production — it would be built either +way. Attribution comes from the per-route `*_client-reference-manifest.js` files under +`.next/server/app`; if that tree is missing or resolves no routes the check **fails closed** rather +than collapsing the two buckets. + +Why the split rather than a raised ceiling: measured on `main` at `af85cbc`, the repo-wide total was ++9.96% of the old single baseline — 576 bytes from failing `Build` — while production-only was +**9.06% below** it. Every byte of the apparent regression was design scratch; production had +actually shrunk since the baseline was captured. Raising the ceiling would have hidden that. + +**Measuring:** `npm run build` reuses a cached `.next`, and the check then reads stale output and +reports byte-identical numbers — it will tell you the budget passes when it does not. Always +`rm -rf .next` before measuring, and sanity-check `.next/BUILD_ID`'s mtime against the current +commit before trusting a number. + diff --git a/CLAUDE.md b/CLAUDE.md index 4bea7d40d7..0703ffd6d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -191,11 +191,12 @@ These fail builds, so they are worth knowing before you write code: - **Mockups are exempt from two gates, not all of them.** `src/app/mockups/**` and `*-mockups.tsx` are design scratch and 404 in production, so they sit outside the **wiring** and **reachability** gates — and nothing else. They are still compiled: they are typechecked like any source, and their - client chunks still count toward `check:bundle-budget`, which totals **every** built chunk rather - than the initial production bundle. A mockup-only PR can therefore fail `Build` on bundle budget - (PR #1580: `+10.1% vs baseline`, tolerance 10%) even though the routes never serve a user. Budget - scope vs. the "not an initial production bundle" position in `/issues` `#013` is unreconciled — - see `#252` before assuming either number governs. + client chunks are still weighed by `check:bundle-budget` — but since 2026-08-09 against a separate + `mockups` scratch baseline (tolerance 25%), not the `production` one (tolerance 10%). That split + reconciled `/issues` `#013` and `#252`: the old single total charged design scratch against a + ceiling named as though it were production weight, which is how PR #1580 blocked at `+10.1%` for + chunks no user can load. See the "Bundle budget" section in `AGENTS.md`; a mockup-only PR can still + fail `Build`, just only on genuine runaway growth. ## Repo-specific tooling diff --git a/bundle-budget.json b/bundle-budget.json index acf5fb8c05..00a17a1a36 100644 --- a/bundle-budget.json +++ b/bundle-budget.json @@ -1,7 +1,15 @@ { - "$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`.", + "$comment": "Client JS bundle-size budgets captured from a known-good production build. Two numbers, two questions (see scripts/check-bundle-budget.mjs): `production` is what users actually download and is the real regression guard; `mockups` is design scratch that 404s in production and is a repo-hygiene ceiling for unbounded accumulation, not a per-mockup gate. CI fails when either grows beyond its tolerancePct; refresh intentionally with `npm run check:bundle-budget -- --update`.", "enforce": true, + "production": { + "gzipBytes": 1309772, + "tolerancePct": 10 + }, + "mockups": { + "gzipBytes": 273873, + "tolerancePct": 25 + }, + "totalGzipBytes": 1583645, "tolerancePct": 10, - "totalGzipBytes": 1440201, - "updatedAt": "2026-08-04T09:21:11.952Z" + "updatedAt": "2026-08-09T07:31:53.000Z" } diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index e29af4210f..35625d05af 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -159,19 +159,18 @@ removed after current-main verification; it is not missing recommended work. | 104 | `#249` | A3 | High — agent process | Next issues-skill / plan touch | 1–2 hours | Extend issues/plan with an agent-safe wins classifier (optional filter; no new skill unless reused thrice). **Stop:** do not outrank A1 operator work. | | 105 | `#250` | A2 | High — multi-agent execution | After Wave 0 queue repair on main (done in this capture); run remaining Wave 0/#202 process gates next on the engineering track | multi-wave | Execute the fastest-wins multi-wave plan (Waves 0–4 + operator track) with parallel agents and per-PR gates. Waves do not outrank A1 acuity. **Stop:** provider/RAG approvals still required where flagged. | | 106 | `#251` | Optional | High — agent process | Next handoff/gates doc touch | 15–30 min | Handoff checklist pairs gates skill with verification-router; paste decisive proof line. **Stop:** do not stack broad gates by default. | -| 107 | `#252` | A2 | High — bundling/gates | Next bundle-budget decision | 1–2 hours | Decide whether check:bundle-budget should exclude mockup chunks or keep counting them as hygiene; do not raise tolerance to clear #1580. **Stop:** do not --update without deciding. | -| 108 | `#253` | A2 | High — phone results UI | Next open-PR sweep | 15–30 min | Decide #1606's fate: the `MobileResultFilterControl` it rewrites was deleted by #247, so there is nothing left to hand-merge. Verify keyboard parity of the replacement sheet on a real device, then close #1606 as superseded. **Stop:** the decision is a human's; do not close #1606 automatically. | -| 109 | `#254` | A2 | Operator — Codex Cloud | Before #1617 leaves draft | 1–2 hours | Re-run Codex Cloud acceptance at the exact current head or mark head-independent evidence. **Stop:** do not treat stale pins as coverage. | -| 110 | `#255` | A2 | High — Cloud/browser gates | Next environment image update | 2–4 hours | Align Cloud Playwright browser builds with lockfile pin; document CI delegation until then. **Stop:** do not force mismatched Chromium revisions. | -| 111 | `#256` | A2 | High — mode section nav | Next information-page / mode-nav pass | 2–4 hours | Declared information-page section sets whose target ids nothing renders — verify each set against the rendered DOM per route; render anchors or delete the set. **Stop:** do not audit by grepping for `id=` alone (sectionId props exist). | -| 112 | `#257` | Optional | High — formulation/specifiers flake | Standing until second reproduction | 15–30 min | Single unreproduced ui-formulation flake when run with ui-specifiers — record a second sighting only; do not quarantine until three on the same SHA. **Stop:** do not weaken assertions. | -| 113 | `#286` | A3 | High — in-page nav + frontend | After owner go-ahead for the information-page series | 1–2 days | Convert the six pill-rail information pages onto `InPageNavHeader`, widen Server Component–safe actions, then delete `informationPageSectionDefinitions`. **Gate:** focused DOM/contract tests + `verify:phone-chrome` for touched owners. **Stop:** do not convert DocumentViewer here; do not verify anchors by grepping `id=` alone. | -| 114 | `#287` | A3 | High — in-page nav + clinical owner | After `#286`; medications needs an owner product call | 0.5–1 day design + convert | Decide medications tab model, presentations MobileTabs vs `InPageNavHeader`, and factsheets heading→id scheme; convert or record lasting exceptions. **Stop:** do not port medications mechanically. | -| 115 | `#288` | Optional | High — document chrome | After `#286`/`#287`, or when declaring the series complete | 30–60 min | Confirm DocumentViewer non-adoption (already noted in `docs/search-chrome-behaviour.md`) as the final end state, or schedule a separate convergence PR that leaves pinned `--document-*` CSS names untouched. | -| 116 | `#289` | A2 | High — auth/identity | Next auth module touch | 1–2 hours | Export a named helper (e.g. `authorizationIdentity(headers)`) from the auth module and use it at every property-access call site; consider a lint rule or branded type so `.Authorization` stops type-checking at all. **Stop:** do not change `authorizationHeadersForAccessToken` to emit uppercase — lowercase is the correct Fetch/Headers convention and callers that pass the object wholesale to `fetch` depend on it. | +| 107 | `#253` | A2 | High — phone results UI | Next open-PR sweep | 15–30 min | Decide #1606's fate: the `MobileResultFilterControl` it rewrites was deleted by #247, so there is nothing left to hand-merge. Verify keyboard parity of the replacement sheet on a real device, then close #1606 as superseded. **Stop:** the decision is a human's; do not close #1606 automatically. | +| 108 | `#254` | A2 | Operator — Codex Cloud | Before #1617 leaves draft | 1–2 hours | Re-run Codex Cloud acceptance at the exact current head or mark head-independent evidence. **Stop:** do not treat stale pins as coverage. | +| 109 | `#255` | A2 | High — Cloud/browser gates | Next environment image update | 2–4 hours | Align Cloud Playwright browser builds with lockfile pin; document CI delegation until then. **Stop:** do not force mismatched Chromium revisions. | +| 110 | `#256` | A2 | High — mode section nav | Next information-page / mode-nav pass | 2–4 hours | Declared information-page section sets whose target ids nothing renders — verify each set against the rendered DOM per route; render anchors or delete the set. **Stop:** do not audit by grepping for `id=` alone (sectionId props exist). | +| 111 | `#257` | Optional | High — formulation/specifiers flake | Standing until second reproduction | 15–30 min | Single unreproduced ui-formulation flake when run with ui-specifiers — record a second sighting only; do not quarantine until three on the same SHA. **Stop:** do not weaken assertions. | +| 112 | `#286` | A3 | High — in-page nav + frontend | After owner go-ahead for the information-page series | 1–2 days | Convert the six pill-rail information pages onto `InPageNavHeader`, widen Server Component–safe actions, then delete `informationPageSectionDefinitions`. **Gate:** focused DOM/contract tests + `verify:phone-chrome` for touched owners. **Stop:** do not convert DocumentViewer here; do not verify anchors by grepping `id=` alone. | +| 113 | `#287` | A3 | High — in-page nav + clinical owner | After `#286`; medications needs an owner product call | 0.5–1 day design + convert | Decide medications tab model, presentations MobileTabs vs `InPageNavHeader`, and factsheets heading→id scheme; convert or record lasting exceptions. **Stop:** do not port medications mechanically. | +| 114 | `#288` | Optional | High — document chrome | After `#286`/`#287`, or when declaring the series complete | 30–60 min | Confirm DocumentViewer non-adoption (already noted in `docs/search-chrome-behaviour.md`) as the final end state, or schedule a separate convergence PR that leaves pinned `--document-*` CSS names untouched. | +| 115 | `#289` | A2 | High — auth/identity | Next auth module touch | 1–2 hours | Export a named helper (e.g. `authorizationIdentity(headers)`) from the auth module and use it at every property-access call site; consider a lint rule or branded type so `.Authorization` stops type-checking at all. **Stop:** do not change `authorizationHeadersForAccessToken` to emit uppercase — lowercase is the correct Fetch/Headers convention and callers that pass the object wholesale to `fetch` depend on it. | - + ## Open items > **Merged-main canary update (2026-07-23, run `30018289898`):** the new structured report correctly recorded evaluated tree `c24f2e8f2d30d0c59fc1eba025d3dcd63478137e`, run/attempt identity and `cross-region-runner` latency context. Golden retrieval remained 36/36 with document/content recall 1.0 and no failed cases. The 44-case answer gate had grounded-supported and unsupported-correct rates of 1.0, but failed because `neuroleptic-side-effect-escalation` again returned one citation where two are required (citation-failure rate 0.0227). `admission-discharge-comparison` again omitted the specific AKG admission document after `comparison_source_extractive_fallback`; `admission-discharge-coverage-paraphrase` was advisory-only at 24,870 ms. Answer cost was reported as `$0.234736`. Do not retry immediately: retain this as the first structured datapoint, compare it with the scheduled 2026-07-26 report, and keep retrieval/ranking unchanged. @@ -196,7 +195,7 @@ removed after current-main verification; it is not missing recommended work. | #056 | P2 | task | Reconcile the existing staging migration history | `Clinical KB Staging` already exists as a healthy, empty Supabase/Railway tier with distinct secrets and no production clinical data, but it is 24 repository migrations behind (ten earlier history holes plus fourteen after `20260719055623`). In the next approved staging schema window, apply the exact missing migration chain, then re-run indexing, health, identity and data-boundary proof. Do not recreate the environment or copy production clinical documents. | current-main staging verification; `docs/staging-setup.md`; `docs/operator-backlog.md` | 2026-07-27 | | #057 | P2 | task | Complete staging soak and rollback rehearsal | After #056, run the documented soak and rollback against an exact candidate; retain latency/error/rollback evidence. Stop on unsafe data, identity mismatch, or an unowned rollback decision. | `docs/launch-operator-runbook.md`; `docs/capacity-review.md` | 2026-07-24 | | #011 | P3 | task | Auth DB-connection allocation is operator-only | Supabase Auth (GoTrue) is capped at ~10 absolute DB connections (Supabase perf advisor). Switch to **percentage-based** allocation in the Supabase **dashboard** before the first compute scale-up — **not settable via SQL/MCP** (operator-owned). Verify via a staging soak + an approval-gated read-only advisor re-check. | `docs/auth-connection-cap-runbook.md`; `docs/process-hardening.md` (Known follow-up debts) | 2026-07-21 | -| #013 | P3 | rec | Route-chunk + mockup catalogue JSON weight | Keep this recommendation open and measurement-gated. `build:analyze` still finds route-scoped catalogue modules: `/specifiers` ships `specifiers-search-index.json` (~180 KB parsed), `/forms` ships `forms-catalog.json` (~132 KB), and `/formulation` ships `formulation-content.json` (~52 KB; client-side local search needs an index/full split or a search endpoint). The approved Lighthouse sample covered `/forms`, but `/specifiers` and `/formulation` remain unmeasured, so the precommitted `#017` rule does not permit archiving their payload work. Development-only `*-mockups.tsx` chunks are not an initial production bundle and production returns 404 for `/mockups/*`; do not restructure them without deploy-artifact or cold-start evidence. **Next:** collect route-specific LCP/CLS and CrUX INP evidence for `/specifiers` and `/formulation`, then close only the routes that meet every `#017` threshold. | session 2026-07-21 (`build:analyze`); PR #1470 review | 2026-07-21 | +| #013 | P3 | rec | Route-chunk + mockup catalogue JSON weight | Keep this recommendation open and measurement-gated. `build:analyze` still finds route-scoped catalogue modules: `/specifiers` ships `specifiers-search-index.json` (~180 KB parsed), `/forms` ships `forms-catalog.json` (~132 KB), and `/formulation` ships `formulation-content.json` (~52 KB; client-side local search needs an index/full split or a search endpoint). The approved Lighthouse sample covered `/forms`, but `/specifiers` and `/formulation` remain unmeasured, so the precommitted `#017` rule does not permit archiving their payload work. **Next:** collect route-specific LCP/CLS and CrUX INP evidence for `/specifiers` and `/formulation`, then close only the routes that meet every `#017` threshold. **Mockup half settled 2026-08-09 (#252):** this row's position that development-only `*-mockups.tsx` chunks are not an initial production bundle is now enforced rather than merely asserted — `check:bundle-budget` weighs `/mockups/**`-exclusive chunks against a separate `mockups` baseline (273,873 gzip bytes, 25% tolerance) and production against its own (1,309,772, 10%), so mockup growth no longer consumes production headroom and the mockup share is reported on every run. That also supplies the separate-reporting measurement this row asked for before any prune: 267.5 KiB across 76 chunks over 66 mockup routes, 17.29% of the repo-wide total. The 'do not restructure them without deploy-artifact or cold-start evidence' guidance stands unchanged — the split removed the false pressure to prune, it did not authorise pruning. | session 2026-07-21 (`build:analyze`); PR #1470 review | 2026-07-21 | | #016 | P3 | rec | "Big but not easy" structural + motion perf | Deferred larger levers: (a) nonce-CSP forces every product route to `╞Æ Dynamic` (zero static generation) — evaluate Partial Prerendering / static shells for the static clinical catalogues (DSM/differentials/therapy/specifiers/formulation); (b) sidebar expand/collapse animates `grid-template-columns` (biggest smoothness cost, motion-gated — needs a transform-overlay rethink); (c) Therapy Compass fetches 692 KB / 2.5 MB JSON client-side (defer until interaction + confirm brotli); (d) settings/setup/admin dialogs static-imported into the home chunk (`next/dynamic` them); (e) **DONE 2026-08-01 in PR-T (ds-v2 therapy teardown):** deleted `therapy-compass.css` and removed its route-group layout import — no longer render-blocking on `/`, `/documents`, `/forms`, `/dsm` and every mode home; (f) `shared-search-app-shell.tsx:8` statically imports the `therapy-compass` barrel, pulling `workspace.tsx` + `bindings.tsx` + `nav.tsx` into every `(search-app)` route; (g) three client waterfalls (`use-app-preferences.ts:156-182`, `ClinicalDashboard.tsx:977-1069`, `signed-image.tsx:60-84` + `use-signed-image-url.ts:39`) and the paint offenders in `globals.css` beyond the sidebar grid — three stacked `backdrop-filter` passes on an always-mounted translating element (`:709-748`), `box-shadow` inside a `transition` list (`:677-684`), and `@keyframes shimmer` animating `background-position` on the shared `Skeleton` (`:2289-2296`). **CORRECTED 2026-07-29 on (c):** the Therapy Compass filenames are unversioned and Next serves `/public` with an ETag, so only the FIRST visit pays 690.6 KB / 2,470 KB — repeat visits pay ~4 revalidation round trips. The fix is content-hashed filenames + `immutable` (touching `scripts/build-therapies-index.mjs` and `check:therapy-data-index`), NOT a bare `Cache-Control` line. See `docs/audit/latency-audit-2026-07-28.md` L3-1/L3-2/L3-3/L3-6/L3-7. | session 2026-07-21 (build route table + design audit) | 2026-07-21 | | #018 | P2 | task | Split the lithium, ADHD and metabolic residuals by mechanism | Current evidence keeps the mechanisms separate. **Lithium — closed within this item:** the row/atom-aware subject guard, foreign-parameter rejection and query-specific range promotion returned `0.5–1.0 mmol/L` with correct targeting/citation; the full retrieval canary remained 36/36 with recall 1.0 and zero per-case RR regressions, and the full answer canary passed every blocking gate. **ADHD — open corpus debt:** `CG.MHSP.ADHD.pdf` is absent from the hosted corpus and the retrieved chart exposes `accessible_table_count=0`; repair corpus/fixture or ingestion evidence rather than weakening extractive budgets. **Metabolic — open structured-evidence debt:** the standalone plural classifier worsened the live answer and was reverted; obtain auditable schedule text/table evidence before another candidate. | targeted live lithium/ADHD/metabolic evidence 2026-07-27; `docs/evidence/rag-reliability-evidence-2026-07-27.md`; refuted approaches | 2026-07-21 | | #022 | P2 | task | Source-governance metadata refresh (operator) | The selected policy is now encoded locally as auditable `third_party_reference_attested` evidence with policy version, reviewer qualification, evidence references and append-only review history. It deliberately preserves `clinical_validation_status=unverified`; malformed, stale or non-BMJ evidence remains review debt. Migration `20260727010000_bmj_third_party_source_attestation.sql` is prepared but was **not applied**. The ten most visible local-document candidates are captured in `docs/evidence/rag-top-local-review-manifest-2026-07-26.json` with `attestation_applied=false`; qualified human review, deliberate hosted apply/attestation, and warning-rate remeasurement remain operator work. | governance worklist; local policy/migration tests; top-ten evidence manifest | 2026-07-21 | @@ -297,10 +296,9 @@ removed after current-main verification; it is not missing recommended work. | #249 | P2 | rec | Extend issues/plan with an agent-safe wins classifier | **Outcome:** /issues and recommendation answers can filter agent-safe short wins (estimate ≤4h, Capability not Operator-only, no RAG/provider, not blocked on a human decision) without inventing a 35th skill. **Next:** add an optional filter/section to the issues skill (and plan/flightplan when useful) that lists those wins from the open table + queue; keep it as procedure on the existing skills unless the classifier is reused thrice — only then mint a thin wins alias. **Stop:** do not weaken acuity ordering; operator A1s still outrank engineering wins. | session 2026-08-05 fastest-wins plan | 2026-08-05 | | #250 | P2 | task | Execute the fastest-wins multi-wave plan (Wave 0–4) | **Outcome:** the 2026-08-04/05 fastest-wins plan is executed with parallel agents, correct gates, and no regression. **Acuity first:** the recommended queue remains acuity-ordered — A1 rows (#059/#053/#231/#207/#226) are not demoted by wave numbers. Operator-track items (#022/#183) retain their A2 priority and approval gates. **A1 track (parallel, not a wave):** run approved A1 work (#059/#053/#231/#207/#226) whenever capability/approvals allow — do not wait for Waves 0–3. **Engineering waves only:** 0 = ledger/process gates (#201 resolved 2026-08-06; continue with remaining #202 work); 1A = #147+#176 phone CLS; 1B = #149+#167+#204+#210 gate integrity; 1C = hygiene/docs #232–#236+#223+#157+#151/#154/#187; 2 = #117 then #118; 3 = #098 then #189 (defer #099 body); 4 = remaining non-A1 clinical/UI packaging once approvals exist (never a holding pen for A1 items). **Next:** continue Wave 0/#202 on a fresh branch off origin/main in parallel with any approved A1 work; use gates + verification-router per PR; respect #155 concurrency. **Stop:** no RAG behaviour without flag+canary; no provider gates without approval; do not mix operationalRisk with clinical/UI in one squash. | session 2026-08-05 fastest-wins plan | 2026-08-05 | | #251 | P3 | rec | Handoff checklist should pair gates skill with verification-router | **Outcome:** every PR handoff picks the smallest correct gate and pastes the decisive proof line, using verification-router when scope is unclear. **Next:** add one line to handoff/gates productivity defaults: after flightplan, run verification-router (or gates) before claiming green; never report exit 0 alone. **Stop:** do not stack verify:cheap + verify:ui + verify:release by default. | session 2026-08-05 fastest-wins plan | 2026-08-05 | -| #252 | P2 | issue | check:bundle-budget counts mockup chunks, contradicting #013's initial-bundle position | The budget's totalGzipBytes comes from measureChunkPaths(walkJsFiles(CHUNKS_DIR)) — EVERY built client chunk, including routes that 404 in production. The manifest-scoped initialDashboardChunks set is used only for the fixture-payload assertion, not the budget. So two repo positions disagree about mockups and nothing says so: #013 records that mockup chunks 'are not an initial production bundle' and must not be restructured without deploy-artifact evidence, while the gate charges them against a repo-wide ceiling. PR #1580 is the live cost — a mockups-only PR blocked on 'FAIL +10.1% vs baseline (tolerance 10%)' for chunks no user can load; it has sat red and unmerged since 2026-08-02. Docs now state the mechanism (AGENTS.md gate bullet, CLAUDE.md mockups bullet) so it stops being a surprise, but the metric decision is unmade. Next action: pick one and make the script say so — (a) exclude mockup-only chunks from totalGzipBytes so the number means production weight, which matches #013 but removes all back-pressure on mockup growth (59 routes on main today, 4 more in open PRs); or (b) keep counting them, rename the reported metric so it does not read as production bundle weight, and treat the tolerance as a deliberate hygiene ceiling. Option (b) additionally wants the mockup share reported separately, which is the measurement #013 asks for before any prune. Stop: do not raise the tolerance or run --update to clear #1580 — that discards the only back-pressure without deciding anything. Renumbered from this PR's original #249 → #252 because main claimed #249–#251 via PR #1624. **Headroom measured 2026-08-09 (document viewer Phase 3):** a clean production build reports 306 client chunks, 1538.4 KiB gzip against the 1406.4 KiB baseline captured 2026-08-04 — **+9.4% inside a 10% tolerance**, i.e. roughly 8 KiB gzip of headroom before the gate turns red. That tip measurement was not paired with a merge-base build artifact in the Phase 3 session, so do not treat it as proven pre-existing drift; it still means the next feature-sized PR of any kind can trip the gate whatever it touches, and the mockup-counting question this row is about is what decides whether that would be a real signal. **Next action unchanged, now urgent rather than theoretical:** reconcile the counting scope against #013's initial-bundle position, then either refresh the baseline deliberately (npm run check:bundle-budget -- --update) or narrow what the gate totals. Do not refresh the baseline as an incidental step inside an unrelated PR. | session 2026-08-05 open-PR review; PR #1580 Build log; scripts/check-bundle-budget.mjs; ledger #013 | 2026-08-05 | | #253 | P3 | task | #1606 needs a hand-merge against merged PR #1615, not a rebase | SUPERSEDED IN PART 2026-08-07: the component both PRs rewrite no longer exists. `MobileResultFilterControl` — the native `