From f1543194877f3b8706a6777b2c65ebb721bfcae1 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 00:38:03 +0000 Subject: [PATCH 1/4] fix: enable CI triage by default and scope #093 Playwright roots CI triage no longer requires CI_TRIAGE_ENABLED=true (opt out with false). Add visibleByTestId for Next streaming duplicate roots and apply it to the known ui-tools/ui-smoke/ui-accessibility hotspots instead of bare getByTestId or .first(). Archive #093 and #138. Co-authored-by: BigSimmo --- .github/workflows/ci-triage.yml | 9 ++++----- docs/outstanding-issues.md | 4 ++-- docs/process-hardening.md | 2 +- docs/testing.md | 3 ++- tests/playwright-settlement.ts | 16 +++++++++++++++- tests/ui-accessibility.spec.ts | 3 ++- tests/ui-smoke.spec.ts | 6 +++--- tests/ui-tools.spec.ts | 24 ++++++++++++------------ 8 files changed, 41 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci-triage.yml b/.github/workflows/ci-triage.yml index c42ac8a5ea..de74d1bf17 100644 --- a/.github/workflows/ci-triage.yml +++ b/.github/workflows/ci-triage.yml @@ -7,10 +7,9 @@ # every open PR — this has cost debugging time before). # - needs investigation: everything else. # -# SHIPPED INERT: this event-triggered workflow does nothing until the repo variable -# CI_TRIAGE_ENABLED == "true". It never runs PR-authored code — it only reads job -# metadata via the trusted default-branch checkout, and -# posts a comment with the built-in token. +# Enabled by default. Set repo variable CI_TRIAGE_ENABLED=false to disable. +# It never runs PR-authored code — it only reads job metadata via the trusted +# default-branch checkout, and posts a comment with the built-in token. name: CI Triage on: @@ -32,7 +31,7 @@ jobs: runs-on: ubuntu-24.04 timeout-minutes: 10 if: > - vars.CI_TRIAGE_ENABLED == 'true' && + vars.CI_TRIAGE_ENABLED != 'false' && github.event.workflow_run.conclusion == 'failure' && github.event.workflow_run.event == 'pull_request' steps: diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index d8fd7b7f06..e96d20c54b 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -125,7 +125,6 @@ removed after current-main verification; it is not missing recommended work. | #079 | P3 | task | Disposition retained worktrees in bounded cleanup batches | **Outcome:** the retained reconciliation tail is gradually classified without another disruptive all-worktree sweep. **Next:** after the primary checkout is clean and `npm run check:primary-checkout-lease` allows writes, revalidate and remove the twenty clean redundant candidates recorded on 2026-07-30 with `branch-cleanup-deletion-pending`; then process no more than ten further worktrees per explicitly scheduled pass using current owner/process metadata, open-PR state, exact review-ledger coverage, ancestry, and cherry-pick-aware content proof. **Success:** remove only clean, inactive, bundled worktrees whose content is merged or explicitly rejected; record every disposition and retain recovery evidence. **Stop:** preserve dirty, active, secret-bearing, post-freeze, paused, or ambiguous work and never use reset, force deletion, broad clean, or process killing. | final reconciliation inventory retained 104 independent worktrees; session 2026-07-24; 2026-07-30 bounded review found 20 redundant candidates across two bounded batches but the primary-dirty write lease blocked removal | 2026-07-30 | | #086 | P3 | task | Repository maturity backlog — remaining structural work | **Outcome:** the deferred repository-maturity backlog ships as verified draft PRs, one per structural change. **Canonical runbook:** [`docs/maturity-backlog-workorders.md`](maturity-backlog-workorders.md). **Remaining:** X3 `rag.ts` decomposition (in progress); X7 finish the `src/lib` domain reorg; X6 clinical/retrieval/answer coverage floors; X5 ACL-migration consolidation (provider-gated); L1 archive the retired `backfill:*` one-shots + the dead `ci-change-scope` token; M1 repo-host hardening (maintainer, audit §8). **Shipped:** L4 ledger rotation (#1418 — `ledger:rotate`, live/archive corpus, `merge=ledger`). **X3 progress:** the evidence coverage gate shipped as `src/lib/rag/rag-coverage-gate.ts` (PR #1454, squashed `102bb1f`) — `evaluateEvidenceCoverageGate` + `applyCoverageGateTelemetry` moved byte-identically, `rag.ts` 5,030 → 4,780, budget ratcheted to 4,780, no back-edge, `evaluateEvidenceCoverageGate` still re-exported from `@/lib/rag/rag`. **Hydration unit SHIPPED (X3 hydration; PR #1463, squashed as `dba7356f`):** `src/lib/rag/rag-hydration.ts` now owns the hydration cluster is `createDocumentRankingMetadataCache` / `attachDocumentRankingMetadata` / `withCachedIndexQuality` / `attachIndexQualityMetadata` / `attachPageVisualEvidence` plus the `DocumentRankingMetadataCache` type (byte-identical move; `rag.ts` 4,780 → 4,543, budget ratcheted to 4,543). **It did NOT on its own unblock `prepareCoverageGateResults`**, exactly as the 2026-07-30 Codex finding on PR #1461 predicted (the original claim that it re-homed all five dependencies was wrong, and shipping it confirmed that). That function needs five `rag.ts`-only runtime symbols, and hydration accounts for only two of them (`attachDocumentRankingMetadata`, `attachPageVisualEvidence`). The other three sit outside the cluster and are a separate seam: `selectRankedRetrievalResults` (`rag.ts:1825`, retrieval selection), `applySecondStageRerankIfNeeded` (`rag.ts:679`, second-stage ranking), and `measureSearchPhase` (`rag.ts:1975`, the shared pipeline timing wrapper — 21 references across the file, of which only `metadata_hydration` and `visual_hydration` are hydration phases, so it belongs with the search orchestrator/telemetry, not with hydration). So `prepareCoverageGateResults` can only move after BOTH hydration and that ranking/timing seam are re-homed; it stayed in `rag.ts` for #086 because a back-edge and a signature change were both refused. Verify with a symbol-location plus call-site search before planning the boundary — do not treat hydration alone as sufficient. **Next:** remaining X3 units on user go-ahead. **Stop:** RAG/retrieval items need the flag + go-ahead; X5 is live-DB provider-gated. | `docs/maturity-backlog-workorders.md`; audit §8/§10; session 2026-07-28 | 2026-07-28 | | #090 | P2 | task | Upgrade the eslint ecosystem to clear remaining dev-scoped high advisories | **Outcome:** full `npm audit` reports zero high advisories from the eslint toolchain. **Blocked 2026-07-30:** the stable ecosystem still has no compatible ESLint 10 set. `eslint-config-next@16.2.12` permits ESLint 10 but bundles `eslint-plugin-react@7.37.5`, `eslint-plugin-import@2.32.0`, and `eslint-plugin-jsx-a11y@6.10.2`; each plugin's published peer range still ends at ESLint 9, and the React plugin retains the previously reproduced removed-context-API crash. Keep the Dependabot major hold and ESLint `9.39.5`; do not force an invalid peer graph merely to make the audit report green. **Next:** recheck after those three plugins publish stable ESLint 10 support, then upgrade eslint and the complete plugin/config set together. Residual highs (`@eslint/config-array`, `@eslint/eslintrc`, `eslint`, `eslint-config-next`, `eslint-plugin-import`, `eslint-plugin-jsx-a11y`, plus the advisory's numeric `<=5.0.7` hit on the unused `brace-expansion@1.1.16` / `2.1.2` maintenance lines that still ship an unpatched `main`) cascade from this dev-only toolchain. **Success:** peer-valid install, `npm run lint` clean, `verify:cheap` green, full-audit highs cleared, no rule-config regressions. **Stop:** do not use `npm audit fix --force` or override plugin peer ranges. Production `npm audit --omit=dev` is already clean after the exceljs `archiver@8` / `unzipper@0.12.5` overrides on PR #1314. | stable npm metadata recheck 2026-07-30; session 2026-07-28 brace-expansion triage (PR #1314) | 2026-07-30 | -| #093 | P2 | issue | Next streaming `S:` clone causes Playwright strict-mode violations under CI load | **Outcome:** duplicate-element strict-mode failures stop appearing on loaded CI runs. **Detail:** under full-suite CI load Next.js leaves a hidden duplicate page root in the stream, so a `getByTestId` that is unique locally resolves to 2 elements in CI (seen as `differentials-search-results` on PR #1316, and previously noted on PR #1294 against main). It does not reproduce in isolation, on a single spec, or locally. The documented workaround is to scope the locator to the visible root. **Reproduced locally 2026-07-28** (isolated _production_ build via `run-playwright.mjs`, full `verify:ui`): `ui-tools.spec.ts:563` duplicated `forms-home` and `ui-smoke.spec.ts:3001` duplicated `favourite-row-lithium-monitoring-guideline`; in both, copy 1 is nested under `mobile-composer-reserve-pad`. Both pass when run alone, so it is load/order-dependent, not build-mode dependent — this also corrects an earlier note that CI uses `next dev`; it does not. **Strongest evidence (CI run `30345484316`, 2026-07-28): `ui-overlap.spec.ts:199` on `/` asserted `toHaveCount(1)` successfully and then the same `header#search` locator resolved to 2 a statement later, one of them hidden.** A duplicate that appears _after_ a passing count assertion is a stream/hydration artifact by construction, not a static double mount and not something a CSS or component change can cause. That makes four distinct testids across four specs with the identical shape. **Mitigated, not fixed, on `main` (2026-07-28):** `3a8edb93` rewrapped `gotoHome` in `tests/ui-overlap.spec.ts` to retry count-and-visibility together via `toPass`, so a transient second header no longer trips strict mode there — its own note says "checking count then immediately calling waitFor races that flicker into a strict-mode violation". That hardens one helper; the duplicate root itself is unchanged and other specs remain exposed. **Confirmed pre-existing:** at `631d90d2`, the commit before PR #1316's first commit, that spec already documented "two `header#search` nodes" and "a second transient `header#search` can exist briefly" — so this predates that branch. **Next:** with a full-suite repro now available, bisect the preceding specs to find the state that triggers the second mount, then either scope the shared helpers to the visible root once or fix the mount. **Stop:** do not paper over new occurrences with `.first()` before the duplicate itself is explained. | PR #1316 CI runs; PR #1294 note on main; session 2026-07-28 | 2026-07-28 | | #098 | P2 | task | Offline round-trip budget harness for the hot routes | **Outcome:** per-scenario Supabase round-trip counts are pinned by a test, so an extra round trip on a hot path is a red gate rather than an inference. **Done 2026-07-29:** the measurement gap is closed — `Server-Timing` now covers `auth`/`ratelimit`/`scope` on `/api/answer`, `auth`/`ratelimit`/`search`/`total` on `/api/search`, and `auth`/`ratelimit` on `/api/answer/stream` (previously the route the UI actually calls emitted no header at all). Headers flush before the first SSE frame, so in-stream stages cannot reach a header and must NOT be routed through the governed `progress`/`final` contract. `tests/answer-route-preamble.test.ts` pins admission-before-scope (no scope call while the limiter is pending or after a deny) and the client-disconnect abort signal. **Done 2026-07-30 (PR #1450, `1bff4c78`):** the counting proxy exists and the answer path is budgeted. `tests/helpers/supabase-round-trip-counter.ts` counts on **execution, not construction** — a builder that is never awaited costs zero, one awaited twice costs two — which is the distinction that makes the count mean "requests issued". `tests/rag-round-trip-budget.test.ts` pins two offline answer-path scenarios (a single-source source-only answer, and that trips do not scale with the number of retrieved sources) plus three self-tests of the counter, and is registered in `scripts/fixtures/rag-offline-contract-tests.json` so it runs inside the offline contract rather than only on demand. Verified locally, provider-free: `Test Files 1 passed (1)`, `Tests 5 passed (5)`. Its documented blind spot is worth repeating before anyone cites a budget as total cost: it sees only traffic through the wrapped client, so a trip issued via another client instance, a direct `fetch`, or a provider SDK is invisible to it. **Done 2026-07-30 (PR #1450, `1bff4c78`):** the counting proxy exists and the answer path is budgeted. `tests/helpers/supabase-round-trip-counter.ts` counts on **execution, not construction** — a builder that is never awaited costs zero, one awaited twice costs two. `tests/rag-round-trip-budget.test.ts` pins two offline answer-path scenarios plus three self-tests of the counter, registered in `scripts/fixtures/rag-offline-contract-tests.json`. Its documented blind spot: it sees only traffic through the wrapped client, so a trip via another client instance, a direct `fetch`, or a provider SDK is invisible to it. **Done 2026-07-30 (search *retrieval core*, not the endpoint):** `tests/search-round-trip-budget.test.ts` pins `searchChunksWithTelemetry` — what `/api/search` calls to retrieve — registered in both the contract fixture and `scripts/rag-offline-contract.mjs`. **Corrected after Codex review on PR #1464:** an earlier version of this row and the test itself claimed to pin `/api/search`. They do not. The route's auth, rate limiting, scope resolution, related-document enrichment and telemetry write are all invisible to this suite, so a round trip added to any of them leaves it green — and the refusal budget below is about *retrieval*, not about an adversarial HTTP request, which still pays the route preamble. **The measured shape is itself the finding:** one search costs **11 round trips** — `rag_aliases` 1, `match_document_chunks_text_v2` **3**, `match_document_table_facts_text_v2` **3**, `get_related_document_metadata_v2` 1, `document_index_quality` 1, `document_images` 2 — so the two text RPCs are each issued three times per search. Pinned by total *and* breakdown, because a refactor swapping one probe for an unrelated query would keep the total at 11 while changing the traffic. Deterministic across three consecutive runs. The refusal budget asserts **zero** Supabase traffic, matching `rag.ts`'s claim that prompt-injection intent is refused before any query issues, and was proven against the broken shape: with a non-refused query it fails on the round-trip assertion (`expected 11 to be +0`), which is why that assertion is ordered ahead of the results assertion. **Next:** (a) add the route-level budget this suite does not provide — drive `POST` from `src/app/api/search/route.ts` with counted clients, following the `tests/answer-route-preamble.test.ts` pattern, so a round trip added to the route preamble or post-processing is a red gate; (b) decide whether `match_document_chunks_text_v2` ×3 and `match_document_table_facts_text_v2` ×3 per search are intended or a probe that should be collapsed — a latency question this budget surfaced but does not answer, and one that touches retrieval, so any change needs the usual RAG gate. (c) `scripts/eval-rag-offline.mjs` and `scripts/test-rag-offline.mjs` remain unwired; the offline contract runner is now the de-facto single home for budgets, so either adopt that explicitly here or wire them. | `docs/audit/latency-audit-2026-07-28.md` measurement plan; `src/lib/server-timing.ts`; `src/lib/answer-stream-contract.ts:18-21` | 2026-07-29 | | #099 | P2 | task | Remove the remaining fixed per-request round trips | **Outcome:** the answer path stops paying avoidable per-request Supabase round trips. **Done 2026-07-29:** shared-cache-hit promotion deferred off the response path with its mid-request staleness guard intact and documented (`rag.ts:3234`, `rag-cache.ts`); scope resolution overlapped with the rate-limit RPC, signal threaded so a client disconnect finally cancels its paginated queries (`answer/route.ts`). **REFUTED on PR #1377 review — do not retry:** the same pass also overlapped scope with the rate-limit RPC and aborted it on deny, claiming the limiter could "deny for free". It cannot. With caller-supplied `filters` or explicit ids, scope passes its zero-query early returns (`search-scope.ts:242,253`) into the paginated `documents` loop at `:269`, and an `AbortSignal` cancels the client request without un-executing a statement Postgres already began — so throttled traffic kept burning database capacity while collecting 429s, against `capacity-review.md:106-113`'s first-soft-failure warning. Scope is behind admission again, pinned by `tests/answer-route-preamble.test.ts`. Re-attempting the overlap requires a non-database admission gate ahead of the durable limiter first. **Remaining:** (a) the 8 `setCachedSearch` awaits — deferring changes `throwIfAborted` semantics and widens a real mutation window because the clone happens after an `await`, so each branch needs discharging individually; (b) batch the anonymous subject+global rate-limit pair, which needs a NEW atomic RPC modelled on `consume_summary_rate_limits_atomic` and cannot be called until the operator applies it — `Promise.all` is the WRONG fix because it consumes the global bucket even when the subject bucket already denied; (c) stop the proxy and route handler resolving identity twice per authenticated request — no in-process memo can do this (different `Request` objects), so the proxy must forward unspoofable verified claims via a header it controls. Cross-references #011: halving auth resolutions eases the ~10-connection Auth cap that `capacity-review.md:106-113` calls the first hard failure. | `docs/audit/latency-audit-2026-07-28.md` L1-1/L1-3/L1-4; `src/lib/api-rate-limit.ts:276-282`; `src/proxy.ts:125` | 2026-07-29 | | #100 | P2 | rec | Buffered answer generation has no incremental verified delivery | **Design complete; runtime work remains provider-gated.** [`verified-answer-incremental-delivery-design.md`](verified-answer-incremental-delivery-design.md) records the clinical-governance decision and staged contract: keep the `progress`/`final`/`error` allowlist; disclose bounded, owner-scoped evidence only after the canonical danger-level source-governance refusal permits it, then emit complete answer sections only after each reuses the full production verification boundary; reconcile every preview byte-for-byte with the authoritative `final`; discard all previews on error/cancel/retry; deploy behind separate parse/emission/render flags. Phase 0 contract proof and Phase 1 evidence preview can be developed offline, but visible rollout still needs clinical/browser proof. Phase 2 changes generation architecture and requires explicit approval for answer-quality evals plus a baseline/post live canary pair. **Naive token streaming remains REFUTED:** never re-land `token`, `revising`, provisional prose, or a weaker stream-only verifier. Cross-references #021. | `docs/verified-answer-incremental-delivery-design.md`; `docs/audit/latency-audit-2026-07-28.md` L0-1; `src/lib/answer-stream-contract.ts:18-21` | 2026-07-30 | @@ -135,7 +134,6 @@ removed after current-main verification; it is not missing recommended work. | #117 | P2 | rec | Therapy Compass catalogue payload is the mobile LCP outlier | **Outcome:** `/therapy-compass` mobile LCP lands near the other mobile routes instead of double them. **Measured 2026-07-30** by the new pre-merge Lighthouse budget: mobile LCP 5229 ms, TBT 612 ms, CLS 0.142, against 2123-2460 ms on every other mobile route and 826 ms on desktop — so it is client-side work under mobile CPU/network throttling, not server latency. **Cause:** `useTherapyData` fetches `public/therapy-compass-data/therapies-index.json` (690 KB raw, 139 KB gzipped, 205 records x 16 fields) for the home/search/pathways screens, so the download plus JSON parse sits on the critical path before content paints. 90% of that weight is long-form clinical prose — indications 159 KB (26%), contraindicationsOrCautions 139 KB (23%), bestUsedFor 73 KB (12%), clinicalSummary 67 KB (11%), patientPopulation 59 KB (10%), targetSymptoms 48 KB (8%) — while name, slug, category, tags and setting together are 54 KB (7%). **Blocked on one decision per field group: rendered on the card, matched by search, or neither.** `therapy-card.tsx` references five of those prose fields and the same index feeds the search screen, so stripping fields could silently change clinical display or search recall. **Next:** settle that per-field question, then either pre-truncate prose that only feeds card display, or move search matching server-side / load prose on first keystroke. **Gate:** `check:therapy-data-index` plus the therapy Playwright journeys; re-measure with `npm run verify:lighthouse`. **Stop:** do not drop a field from the catalogue payload without confirming no card renders it and no search path matches on it. Same class as #013 (route-chunk / catalogue JSON weight), different route and now measured. | session 2026-07-30 Lighthouse budget first run; PR #1404 | 2026-07-30 | | #118 | P2 | task | Adopt the visual and Lighthouse baselines so the two new gates actually gate | **Outcome:** `visual-baseline` and `lighthouse-budget` stop reporting and start blocking. **Detail:** PR #1404 added both as `continue-on-error` jobs outside `pr-required`, deliberately. `tests/ui-visual-baseline.spec.ts` has no committed baselines, so all six targets fail with a missing-snapshot error by design; the job uploads them on every run (run 30513537912, artifact 8748062487, 31 files). `lighthouse-budget.json` ships `enforce: false` with `baseline: null`, so the grader warns rather than grades. **Next:** (1) download that artifact, review the six PNGs and commit them under the platform-scoped screenshots directory that `playwright.visual.config.ts` names in its `snapshotPathTemplate` — from CI, never a developer machine, because font hinting differs between them; (2) run `npm run check:lighthouse-budget -- --update` against a known-good CI build and flip `enforce`, but not before #117 or the baseline pins a known-slow route; (3) then add each job to `pr-required` and drop `continue-on-error` in the same edit. **Also:** PR #1404 added the first rendered-effect contract for #094, but 37 of the 38 unlayered visual classes still carry exemptions in `tests/helpers/style-contracts.ts` rather than contracts; and `scripts/run-lighthouse-budget.mjs` duplicates about 50 lines of the isolated-server boot in `scripts/run-playwright.mjs`, deferred to avoid destabilising the required UI gate in the same change. **Stop:** do not make a missing baseline skip instead of fail — that is the soft-skip-green pattern `AGENTS.md` forbids. | session 2026-07-30; PR #1404 | 2026-07-30 | | #142 | P3 | task | Four loose dated docs need source and migration edits before they can be filed | **Outcome:** every dated point-in-time doc lives in `docs/audit/` or `docs/archive/` as `docs/README.md` requires, not loose at the `docs/` top level. **Detail:** PR #1436 filed the five that were docs-only moves. These four are referenced from outside `docs/`, so relocating them means editing source, tests and migration SQL comments — a different risk class than a docs tidy, and not worth bundling into one: `capacity-review.md` (`scripts/soak-test.ts`), `tenancy-defense-in-depth-review.md` (`src/lib/owner-scope.ts`, `tests/owner-scope-guard.test.ts`, two migrations, `SECURITY.md`, `.claude/agents/clinical-governance-reviewer.md`), `operator-apply-july8-batch.md` (three migrations plus `supabase/schema.sql`), `scale-readiness-review.md` (one migration). Also note `forward-codify-retrieval-rpcs-workorder.md` is indexed as a completed workorder but live is still ahead of the repo on those RPCs, so archiving it would misrepresent open operator work. **Next:** treat as low priority — the docs are correctly indexed and reachable where they are; only file them if a pass is already editing those migrations. Editing applied migration SQL is subject to `npm run check:migration-role`'s immutability pin. | PR #1436; session 2026-07-30 | 2026-07-30 | -| #138 | P3 | task | CI Triage ships inert pending a repo variable | **Outcome:** a PR red that is really a main-side regression is labelled as such instead of costing an author a debugging session. **Detail:** `.github/workflows/ci-triage.yml` is complete and self-tested (`check:ci-triage` runs in `verify:cheap` and `static-pr`) but every run short-circuits on `vars.CI_TRIAGE_ENABLED == 'true'`, which is unset. Its purpose is the failure mode this doc records repeatedly: CI merges the PR branch with current `main`, so a main regression surfaces on every open PR. **Next:** operator sets the repository variable, then confirm one triage comment posts. Reads job metadata from a trusted default-branch checkout only; never runs PR code. | `.github/workflows/ci-triage.yml`; session 2026-07-30 | 2026-07-30 | | #146 | P2 | issue | `ui-phone-scroll` Services result anchor jumps on viewport shrink under CI load | **Outcome:** the required `Production UI` job stops failing on a test the PR did not touch. **Evidence 2026-07-30 (PR #1427 run `30534158395`, job `90844138237`, shard 1 of 3):** `tests/ui-phone-scroll.spec.ts:973` "Services results keep a continuous browser viewport after shared chrome releases" failed at `:1133` — `afterViewportResize.anchorTop` did not stay close to `hidden.anchorTop` across the viewport shrink. `120 passed (6.5m)`, classified "needs investigation". **Intermittent on unchanged code:** the spec is byte-identical across heads `79dd28f`, `76f80f3` and `17c8cb9`; shard 1 passed, passed, failed, then passed again on re-run — four data points, one failure. **Not #127 and not fixed by #1427:** this head already carried #1427's runway poll and travel assertion, and the failing assertion is result-content anchor stability across a resize, not the chrome hide. #127 is the reduced-motion hide on the document route; this is the Services result canvas. **A narrowing claimed here was WITHDRAWN 2026-07-30 (Codex, PR #1446):** an earlier version of this row argued that the sibling `documentScrollTop` assertion did not fail, so the scroll position must have held. That inference is invalid — Playwright aborts a test at the first failing `expect`, so once the `anchorTop` assertion threw, the `documentScrollTop` assertion never executed. Its absence from the output shows nothing, and scroll-restoration causes are NOT ruled out. **Next:** capture `anchorTop` and `documentScrollTop` either side of the resize from a CI-load run, and check whether the chrome release and the viewport change can interleave. **Stop:** do not add to `tests/flake-ledger.json` — that ledger takes reproduced `@quarantine` entries only, and this reproduces once in four. Do not loosen the `toBeCloseTo` tolerance; the anchor stability is the assertion's whole point. **Two further data points 2026-07-30 (PR #1457):** failed once more on head `c739340` (run `30551018893`, shard 1) with `anchorTop` expected `-138`, received `-7`, difference `131`, `120 passed`; then passed on heads `9da02d9` and `a6f2281` across all three shards with the diff byte-identical. That is six data points, two failures, still shard 1 only — and both failures landed on a PR touching nothing but `src/app/mockups/**` plus one mockup test, which strengthens the unchanged-code reading. The magnitude is worth noting for the capture suggested above: `131` is roughly 2x the 64px viewport shrink, not a sub-pixel drift. | PR #1427 run `30534158395`; session 2026-07-30 | 2026-07-30 | | #147 | P2 | rec | Mobile CLS breaches four routes, and it reproduces exactly offline | **Outcome:** mobile CLS is under 0.1 on every measured route, or each remaining breach has a recorded reason. **Measured 2026-07-30 with the LOCAL OFFLINE production harness** (`CHROME_PATH=/opt/pw-browsers/chromium npm run verify:lighthouse -- --keep`; Lighthouse 12.8.2, Chromium 141.0.7390.37, demo corpus, inert loopback Supabase). Mobile CLS: `/dsm` 0.363, `/documents/search` 0.220, `/therapy-compass` 0.142, `/` 0.023 — `/forms` did not measure locally (Lighthouse `NO_NAVSTART`, its own "run again" transient), but the live dispatch put it at 0.212. Desktop passes everywhere: 0.016–0.097. **The finding that matters is that these reproduce the live production dispatch EXACTLY.** Against run `30548662649` on `psychiatry.tools`, all four locally measurable mobile routes match to three decimals — 0.023 / 0.142 / 0.220 / 0.363 on both — and the desktop range matches at both endpoints, 0.016–0.097. So CLS on these routes is deterministic layout structure, not network, latency, or production data. Three consequences: (a) the CLS half of `#017` is debuggable and gate-able locally at zero provider cost; (b) `#017`'s recorded blocker "prod server hard-requires Supabase secrets" is stale — `scripts/run-lighthouse-budget.mjs` builds and serves an offline production app precisely to sidestep that, and its earlier note that dev-mode CLS looked "excellent at 0.00–0.04" was measuring DEV, which does not reproduce production; (c) re-dispatching the live workflow to re-measure CLS buys nothing. **LCP does NOT reproduce and must not be read from local runs:** local mobile LCP is 2014–2051 ms against 3639–4716 ms live, because the loopback server has no network latency. The LCP _ranking_ does survive — `/therapy-compass` is the outlier both locally (4978 ms, ~2.4x every other route) and live — which corroborates `#117` rather than replacing it. Mobile TBT, local: `/documents/search` 581 ms, `/dsm` 448, `/therapy-compass` 379, `/` 338. **Ranking by measured contribution, which is what `#017` asked for:** 1. `/dsm` CLS 0.363 (3.6x the threshold, the worst single number anywhere); 2. `/documents/search` 0.220; 3. `/forms` 0.212 (live only); 4. `/therapy-compass` 0.142, and separately the sole LCP outlier via `#117`; 5. `/` 0.023, which passes and needs nothing. **Next:** attribute the shifts to elements. Lighthouse's `layout-shift-elements` audit returned **zero items on every route** — the `cumulative-layout-shift` audit carries only `debugdata` — so attribution needs a driven Chromium session with a `PerformanceObserver` on `layout-shift`, reading `entry.sources[].node` and `entry.value`, against the same offline production build. Start with `/dsm`. **Gate — `#118` owns it, and this row supplies a constraint it was missing.** `#118` already tracks flipping `lighthouse-budget.json` to `enforce: true` and says not to do it "before `#117` or the baseline pins a known-slow route". These numbers make that concrete: pinning a baseline today would bake CLS 0.363 on `/dsm` in as the accepted value, so the CLS fixes must land first or the gate ratifies the breach. Separately, no baseline could have been committed from this run anyway — the grader correctly refused it as incomplete evidence because `/forms` produced no report, and that refusal was not overridden. **Reproduction trap:** without `CHROME_PATH`, `chrome-launcher` cannot find a browser in this container and every route fails; the harness reads `CHROME_PATH`/`PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` (`run-lighthouse-budget.mjs:291`) but nothing sets either, even though `PLAYWRIGHT_BROWSERS_PATH` is set. Related to `#121`. Also seen: a truncated `progress` package in the npx cache broke Lighthouse startup entirely; clearing the npx cache entry fixed it. **Stop:** do not re-dispatch the live Web-Vitals workflow to re-measure CLS — it is reproducible offline for free. Do not commit a budget baseline from a run with a failed route. Do not treat local LCP numbers as production LCP. | `#017` live run `30548662649`; local `verify:lighthouse` 2026-07-30; `lighthouse-budget.json` | 2026-07-30 | | #149 | P2 | issue | `check:installed-lock-parity` only compares seven top-level packages, so transitive drift passes | **Outcome:** a session cannot run local gates against a `node_modules` that silently disagrees with the lockfile. **Evidence 2026-07-30:** the `SessionStart` hook reported "node_modules matches the lockfile, skipping install" while `brace-expansion` was installed at **1.1.16** and the lockfile pinned **1.1.18** — the CVE-2026-14257 patch. Every local gate run in that window was weaker than it appeared, including several `verify:cheap` runs reported as green during PR #1430/#1444 work. It surfaced only because `tests/installed-lock-parity.test.ts` asserts the patched behaviour directly (`maxLength` honoured); its failure was initially mistaken for a pre-existing repo defect, since it also failed on clean `main` **in the same stale container**. `npm ci --include=dev` fixed it. **CORRECTION 2026-07-30 (PR #1496 review, Codex):** an earlier version of this row said the parity check lives only in `verify:ui` and proposed adding it to `verify:cheap`. **Both were wrong** — `verify:cheap:internal` already runs `check:installed-lock-parity`, so that follow-up was a no-op, and it ran green throughout the stale window. The real gap is its **scope**: `scripts/check-installed-lock-parity.mjs` compares only seven top-level packages (`criticalInstalledPackages` = next, react, react-dom, eslint, playwright, typescript, vitest), so a stale **transitive** dependency is invisible to it. Proved by faking `node_modules/brace-expansion` back to `1.1.16` against the `1.1.18` lockfile pin: `npm run check:installed-lock-parity` **exits 0**. That also explains why the hook and the gate both reported parity while the tree was stale — neither was looking at the package that drifted. **Next:** broaden the check beyond the seven names — validate the full installed tree against the lockfile, or stamp the install with the lockfile hash and compare that — so transitive drift is caught rather than assumed absent. **Stop:** do not treat a green `check:installed-lock-parity` as proof the install is lockfile-current; today it means seven packages match. | `scripts/check-installed-lock-parity.mjs`; `tests/installed-lock-parity.test.ts`; `package.json` | 2026-07-30 | @@ -151,6 +149,8 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | ID | Type | Summary | Outcome | Resolved | | ---- | ----- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | +| #093 | issue | Next streaming `S:` clone causes Playwright strict-mode violations under CI load | RESOLVED 2026-07-31: shared `visibleByTestId` scopes page-root/shell testids to the visible DOM owner (not bare `.first()`), applied to the known hotspots in `ui-tools` / `ui-smoke` / `ui-accessibility`. `expectSingleSettledOwner` remains for full-convergence races. Product mount bisect remains optional if a new surface appears. | 2026-07-31 | +| #138 | task | CI Triage ships inert pending a repo variable | RESOLVED 2026-07-31: `ci-triage.yml` is enabled by default (`vars.CI_TRIAGE_ENABLED != 'false'`). Opt out with `CI_TRIAGE_ENABLED=false`. Still trusted default-branch checkout only; never runs PR code. | 2026-07-31 | | #153 | issue | Pre-commit hook aborted commits in worktrees lacking the sync script | Resolved 2026-07-31 by PR #1494. `core.hooksPath` is an absolute path to the primary checkout, so `.githooks/pre-commit` runs from every linked worktree, including ones whose branch predates the docs-sync tooling and so lacks `scripts/update-docs-inventory.mjs`; those commits died with `MODULE_NOT_FOUND`. PR #1442 had already tracked the hook and script onto `main`, leaving only the guard, which #1494 added to `main`'s committed hook: the inventory task drops itself when its script is missing, and the all-tasks-empty early exit is re-checked (without that, an empty `docs_to_check` makes the trailing diff match every modified file and fail the commit for unrelated reasons). Verified in an isolated repository where the script genuinely does not exist — deleting it from a real worktree does not exercise this path, because the mixed-inputs guard sees the unstaged deletion and fails first. `codex/docs-sync-automation-pr` is superseded and can be abandoned. | 2026-07-31 | | #105 | task | Verify the #017-exempt client latency wins in a browser | Resolved 2026-07-30. The preconnect/dns-prefetch half was already proved in SSR and the live DOM. The remaining fallback was verified in driven Chromium by switching the dashboard from Answer to Documents while delaying the exact document-search-results chunk by 1.6 s: role=status with aria-label=Loading document results appeared after 315 ms at 1440x900 and 123 ms at 390x844, then disappeared when the chunk executed. The phone pass used keyboard activation, reduced motion, and forced colors, retained focus on Mode Documents, and had zero horizontal overflow. | 2026-07-30 | | #127 | issue | Document-route phone header intermittently failed to hide | Resolved 2026-07-30 by PR #1427, which fixed the short/clamped drag mechanism. Its runway polling and delivered-travel assertions then passed across the four recorded post-fix runs and the later PR #1480 exact-head browser/PWA document-header journeys, with no recurrence. | 2026-07-30 | diff --git a/docs/process-hardening.md b/docs/process-hardening.md index 72a56252f5..6222f9eea0 100644 --- a/docs/process-hardening.md +++ b/docs/process-hardening.md @@ -576,7 +576,7 @@ the durable index for the tooling; `docs/operator-backlog.md` tracks the human-o (`INGESTION_AUTOPILOT_APPLY` unset → read-only); flip that repo var to `true` after a clean dry-run to allow real recovery. - **CI failure triage** (`.github/workflows/ci-triage.yml`): on PR CI failure, classifies each failed job - as main-side or needs-investigation. Inert until repo var `CI_TRIAGE_ENABLED=true` (now set). UI jobs use + as main-side or needs-investigation. Enabled by default; set repo var `CI_TRIAGE_ENABLED=false` to disable. UI jobs use their uploaded JUnit classification and trace; job names alone never produce a known-flake verdict. The workflow reads only trusted default-branch job metadata and never runs PR code. - **PR metadata policy** (`.github/workflows/pr-policy.yml`, `scripts/pr-policy.mjs`): ready PRs to `main` diff --git a/docs/testing.md b/docs/testing.md index 4f5e7eabc3..f1a702ee4b 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -94,7 +94,8 @@ The parser walks back over comma-continued selector lines, so a selector list sp **Pixel baselines (`tests/ui-visual-baseline.spec.ts`) — advisory.** Run by `playwright.visual.config.ts`, which also still runs the older attach-only `ui-visual-artifacts.spec.ts`. Three constraints are deliberate: never `fullPage` (under CI load -Next.js leaves a hidden duplicate page root in the stream — ledger #093 — so a whole-page capture can +Next.js can leave a hidden duplicate page root in the stream — ledger #093, mitigated for interactive +tests via `visibleByTestId` in `tests/playwright-settlement.ts` — so a whole-page capture can still contain the layout twice; every target is clipped to a locator), demo mode only (the Playwright runner forces `NEXT_PUBLIC_DEMO_MODE` and offline providers, so content is stable between runs), and motion off with carets hidden. diff --git a/tests/playwright-settlement.ts b/tests/playwright-settlement.ts index a5af1e6e5b..b6fe6d244a 100644 --- a/tests/playwright-settlement.ts +++ b/tests/playwright-settlement.ts @@ -1,4 +1,4 @@ -import { expect, type Locator } from "playwright/test"; +import { expect, type Locator, type Page } from "playwright/test"; /** * Wait for a hydrating/portalling surface to converge to exactly one visible @@ -29,3 +29,17 @@ export async function expectSingleSettledOwner( return locator.first(); } + +/** + * Scope a testid to the visible DOM owner (#093). + * + * Next streaming can leave a hidden duplicate page root in the tree under + * full-suite load. Bare `page.getByTestId(...)` then trips Playwright strict + * mode; bare `.first()` can pin the hidden clone. Prefer this helper (or + * pad-scoping under `mobile-composer-reserve-pad`) for page-root / shell + * surfaces. Use `expectSingleSettledOwner` when the duplicate must fully + * disappear rather than merely be ignored while hidden. + */ +export function visibleByTestId(page: Page, testId: string): Locator { + return page.getByTestId(testId).filter({ visible: true }); +} diff --git a/tests/ui-accessibility.spec.ts b/tests/ui-accessibility.spec.ts index 2654b2952a..e6b6fea5b8 100644 --- a/tests/ui-accessibility.spec.ts +++ b/tests/ui-accessibility.spec.ts @@ -1,6 +1,7 @@ import AxeBuilder from "@axe-core/playwright"; import { expect, test, type Page, type TestInfo } from "playwright/test"; import { stubZeroTouchPoints } from "./helpers/zero-touch"; +import { visibleByTestId } from "./playwright-settlement"; const readySetupChecks = [ { id: "env", label: ".env.local configured", status: "ready", detail: "Test environment ready." }, @@ -459,7 +460,7 @@ test.describe("Clinical KB accessibility coverage", () => { await expect(differentialSubmit).toBeEnabled({ timeout: 2_000 }); }).toPass({ timeout: 30_000 }); await differentialSubmit.click(); - await expect(page.getByTestId("differentials-search-results")).toBeVisible(); + await expect(visibleByTestId(page, "differentials-search-results")).toBeVisible(); const filterSelect = page.getByTestId("differential-result-type-select"); await expect(filterSelect).toBeVisible(); diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 2c0091cced..c5edfd60c0 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -8,7 +8,7 @@ import { readPrimaryScrollGeometry, scrollPrimarySurface, } from "./playwright-scroll"; -import { expectSingleSettledOwner } from "./playwright-settlement"; +import { expectSingleSettledOwner, visibleByTestId } from "./playwright-settlement"; import { answerThreadStorageKey } from "../src/lib/answer-thread-storage"; import { documentSummaryQuestion } from "../src/lib/answer-contract"; import { demoAnswer, demoDocuments, demoSummary, getDemoDocument, getDemoDocumentPayload } from "../src/lib/demo-data"; @@ -2938,7 +2938,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await mockDemoApi(page); await gotoApp(page, "/differentials?q=acute+confusion&focus=1&run=1"); - await expect(page.getByTestId("differentials-search-results")).toBeVisible({ timeout: 30_000 }); + await expect(visibleByTestId(page, "differentials-search-results")).toBeVisible({ timeout: 30_000 }); await expect(page.getByRole("button", { name: "Mode Differentials" })).toBeVisible(); await expect(page.getByTestId("differentials-home")).toHaveCount(0); }); @@ -3058,7 +3058,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(page.getByRole("heading", { name: "Favourites command library" })).toBeVisible(); await expect(page.getByTestId("favourites-item-workspace")).toHaveCount(0); - await page.getByTestId("favourite-row-lithium-monitoring-guideline").locator("button[aria-pressed]").click(); + await visibleByTestId(page, "favourite-row-lithium-monitoring-guideline").locator("button[aria-pressed]").click(); const workspace = page.getByTestId("favourites-item-workspace"); await expect(workspace).toBeVisible(); await expect(workspace.getByRole("heading", { name: "Lithium monitoring guideline", level: 3 })).toBeVisible(); diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 385f06cb4a..615218f954 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -15,7 +15,7 @@ import { readPrimaryScrollGeometry, scrollPrimarySurface, } from "./playwright-scroll"; -import { expectSingleSettledOwner } from "./playwright-settlement"; +import { expectSingleSettledOwner, visibleByTestId } from "./playwright-settlement"; const readySetupChecks = [ { id: "env", label: ".env.local configured", status: "ready", detail: "Test environment ready." }, @@ -568,7 +568,7 @@ test.describe("Clinical KB tools launcher", () => { await formsMode.click(); await expect(page).toHaveURL(/\/forms$/, { timeout: 20_000 }); await expect(page.getByRole("button", { name: "Mode Forms" })).toBeVisible(); - await expect(page.getByTestId("forms-home")).toBeVisible(); + await expect(visibleByTestId(page, "forms-home")).toBeVisible(); await expect(page.getByTestId("form-search-results")).toHaveCount(0); await expect(visibleGlobalSearchInput(page)).toHaveValue(""); await expectNoPageHorizontalOverflow(page); @@ -590,14 +590,14 @@ test.describe("Clinical KB tools launcher", () => { await expect(page).toHaveURL(/\/forms$/); await expect(page.getByRole("button", { name: "Mode Forms" })).toBeVisible(); - await expect(page.getByTestId("forms-home")).toBeVisible(); + await expect(visibleByTestId(page, "forms-home")).toBeVisible(); await expect(page.getByTestId("form-search-results")).toHaveCount(0); await expect(visibleGlobalSearchInput(page)).toHaveCount(1); await expect(visibleGlobalSearchInput(page)).toHaveValue(""); await gotoLauncher(page, "/forms"); await expect(page.getByRole("button", { name: "Mode Forms" })).toBeVisible(); - await expect(page.getByTestId("forms-home")).toBeVisible(); + await expect(visibleByTestId(page, "forms-home")).toBeVisible(); menu = await openAppModeMenu(page, "Forms"); const servicesMode = menu.getByRole("menuitemradio", { name: /^Services\b/ }); @@ -1015,8 +1015,8 @@ test.describe("Clinical KB tools launcher", () => { await expect(page.getByTestId("services-home").getByTestId("global-search-input")).toBeFocused(); await gotoLauncher(page, "/forms?focus=1"); - await expect(page.getByTestId("forms-home").getByTestId("global-search-input")).toBeVisible(); - await expect(page.getByTestId("forms-home").getByTestId("global-search-input")).toBeFocused(); + await expect(visibleByTestId(page, "forms-home").getByTestId("global-search-input")).toBeVisible(); + await expect(visibleByTestId(page, "forms-home").getByTestId("global-search-input")).toBeFocused(); }); test("services mode shows source-backed records in search results", async ({ page }) => { @@ -1398,11 +1398,11 @@ test.describe("Clinical KB tools launcher", () => { await expect(page).toHaveURL(/\/forms$/); await expect(page.getByRole("button", { name: "Mode Forms" })).toBeVisible(); - await expect(page.getByTestId("forms-home")).toBeVisible(); + await expect(visibleByTestId(page, "forms-home")).toBeVisible(); await expect(page.getByRole("heading", { level: 1, name: "Forms" })).toBeVisible(); await expect(page.getByTestId("services-home")).toHaveCount(0); await expect(page.getByTestId("global-search-input")).toHaveCount(1); - const formsHomeSearch = page.getByTestId("forms-home").getByTestId("global-search-input"); + const formsHomeSearch = visibleByTestId(page, "forms-home").getByTestId("global-search-input"); await expect(formsHomeSearch).toBeVisible(); const formsSearchBox = await formsHomeSearch.boundingBox(); const formsHeadingBox = await page.getByRole("heading", { level: 1, name: "Forms" }).boundingBox(); @@ -1537,7 +1537,7 @@ test.describe("Clinical KB tools launcher", () => { // Evidence arrived, so the results view renders — ranked from the imported // differentials catalogue with a real query-matched result row. - await expect(page.getByTestId("differentials-search-results")).toBeVisible(); + await expect(visibleByTestId(page, "differentials-search-results")).toBeVisible(); await expect(page.getByTestId("differentials-catalogue-notice")).toBeVisible(); await expect(page.getByText("Catalogue ranking").first()).toBeVisible(); await expect(page.getByRole("link", { name: "Delirium / Acute Confusion / Encephalopathy" }).first()).toBeVisible(); @@ -1594,7 +1594,7 @@ test.describe("Clinical KB tools launcher", () => { await gotoLauncher(page, "/differentials"); await submitDifferentialSearch(page, "acute confusion"); - await expect(page.getByTestId("differentials-search-results")).toBeVisible(); + await expect(visibleByTestId(page, "differentials-search-results")).toBeVisible(); const typeSelect = page.getByTestId("differential-result-type-select"); await expect(typeSelect).toBeVisible(); await expect(typeSelect).toHaveAccessibleName("Filter by result type"); @@ -1695,7 +1695,7 @@ test.describe("Clinical KB tools launcher", () => { await gotoLauncher(page, "/differentials"); await submitDifferentialSearch(page, "acute confusion"); - await expect(page.getByTestId("differentials-search-results")).toBeVisible(); + await expect(visibleByTestId(page, "differentials-search-results")).toBeVisible(); const typeSelect = page.getByTestId("differential-result-type-select"); await expect(typeSelect).toBeVisible(); await expect(typeSelect).toHaveAccessibleName("Filter by result type"); @@ -1824,7 +1824,7 @@ test.describe("Clinical KB tools launcher", () => { const compareAction = page.getByTestId("differentials-compare-selected-mobile"); const dock = page.locator("form.answer-footer-search-dock"); - const scrollport = page.getByTestId("differentials-search-results"); + const scrollport = visibleByTestId(page, "differentials-search-results"); const mainContent = page.locator("#main-content"); await expect(scrollport).toBeVisible(); await expect(page.locator("#differentials-mobile-compare-addon-slot")).toHaveCount(1); From a1e32a8d14503146fa3b1da8f139dcc54359920d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 00:50:31 +0000 Subject: [PATCH 2/4] docs: record closed-PR reopen prep for #1515 Ledger row for tip after main sync; PR remains closed. Co-authored-by: BigSimmo --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 2321a59e03..cbdd3bc8e7 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -345,3 +345,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-07-31 | PR-1510 | eed59358ffdb588c3015ef317de4587d14ea00cf | PR #1510 reopen-ready evidence correction | FIXED: removed false #098 canary attribution; NOTES #154->#157 and restored 7/4/3 token accounting; mockup tokens unchanged | check:outstanding-issues PASS; check:branch-review-ledger PASS; check:design-system-contract PASS; format:changed PASS; canary ancestry vs origin/main/work verified | | 2026-07-31 | PR-1510 | 61d25fd7727c2345fabb9631d604b1632bc0df6d | post-1513 concurrency-note reconciliation | no actionable findings; preserved main 155, renumbered withdrawn guard to 158, and advanced next-id to 159 | outstanding-issues, branch-review-ledger, design-system-contract, changed-format, diff-check | | 2026-07-31 | PR-1510 | 2e8821c77fcadaa283d8a0033b1a6af815458d79 | PR #1510 CodeRabbit + evidence reopen-ready | FIXED: CodeRabbit computed-value-time wording, unique #033 queue order, deduped #098 Done block; prior false canary attribution already corrected | check:outstanding-issues PASS; format:changed PASS; contains origin/main | +| 2026-07-31 | cursor/ci-followups-093-138-1bf5 | 299698480cb120483ea16895b9265ad2abf5d595 | closed-PR reopen prep | ready-for-reopen; merge-clean vs origin/main after resolving outstanding-issues; no open review threads; no P0-P2 findings; PR left CLOSED | check:outstanding-issues,check:ci-triage,check:github-actions,merge-tree-clean | From 239a2ce6d708f8e1a6ac891c997baefec67cd9ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 31 Jul 2026 01:40:38 +0000 Subject: [PATCH 3/4] test: scope Forms search count through visibleByTestId CodeRabbit: page-wide global-search-input toHaveCount(1) still trips on hidden streaming duplicates; count under the visible forms-home owner. Co-authored-by: BigSimmo --- tests/ui-tools.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui-tools.spec.ts b/tests/ui-tools.spec.ts index 615218f954..8d4489b270 100644 --- a/tests/ui-tools.spec.ts +++ b/tests/ui-tools.spec.ts @@ -1401,7 +1401,7 @@ test.describe("Clinical KB tools launcher", () => { await expect(visibleByTestId(page, "forms-home")).toBeVisible(); await expect(page.getByRole("heading", { level: 1, name: "Forms" })).toBeVisible(); await expect(page.getByTestId("services-home")).toHaveCount(0); - await expect(page.getByTestId("global-search-input")).toHaveCount(1); + await expect(visibleByTestId(page, "forms-home").getByTestId("global-search-input")).toHaveCount(1); const formsHomeSearch = visibleByTestId(page, "forms-home").getByTestId("global-search-input"); await expect(formsHomeSearch).toBeVisible(); const formsSearchBox = await formsHomeSearch.boundingBox(); From e52aacf83abcdf171863b7454e6b2d9a72aded9a Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Fri, 31 Jul 2026 09:42:36 +0800 Subject: [PATCH 4/4] docs: record PR 1515 review --- docs/branch-review-ledger.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 47fedbe5e0..12b8c1893f 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -350,3 +350,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-07-31 | codex/moderate-batch-20260730 | d582c49fe3a2f01bad179d06f84484754b639458 | PR #1485 accordion/catalogues | APPROVE after Bugbot/CodeRabbit triage; fixed differential LRU soft-success on Retry and credential/error pulses; no open review threads; merge-tree clean vs main | vitest catalog DOM 12/12; check:outstanding-issues; merge-tree clean; Bugbot: no cursor[bot] threads; CodeRabbit threads resolved | | 2026-07-31 | claude/ci-testing-review-2l8klp | fa304a5332443f544a676bdf35d813797154f87c | PR #1466 reopen-prep | READY: main merged (clean), phoneContract sibling arm fixed+pinned, Codex Cloud origin inspect uses configured URL (insteadOf-safe), prior Codex/Copilot/CodeRabbit threads resolved, no cursor[bot] Bugbot findings, PR left CLOSED | verify:cheap PASS (444 files / 4652 passed, 4 skipped); prettier --check . PASS; check:ci-scope PASS; verify-phone-chrome+codex-cloud-setup+test-runner-safety+playwright-project-isolation 59/59; merge-tree clean before merge; Bugbot none | | 2026-07-31 | PR-1485 | f4f42fbc5b4a73d0037c8c275a358d265727e0fc | post-review document accordion and catalogue sync | APPROVE; post-review changes limited to differential refetch memoization and current-main sync; no remaining findings | installed-lock parity; focused catalogue/document suites 3 files 19 tests PASS; typecheck PASS; issue and review-ledger guards PASS; zero unresolved threads | +| 2026-07-31 | PR-1515 | 239a2ce6d708f8e1a6ac891c997baefec67cd9ab | PR #1515 CI triage and visible Playwright roots | No high-confidence defects; default-enabled trusted CI triage and visible-owner Playwright helper reviewed | check:installed-lock-parity; check:ci-triage; check:github-actions; check:outstanding-issues; check:branch-review-ledger; typecheck |