From e161fcfd028893723a00cee3acf93e47f2531448 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 04:39:12 +0000 Subject: [PATCH 1/9] issues: capture worker/edge execution gap (#098) and component state-matrix coverage (#099) Both recorded from the 2026-07-29 test-coverage analysis with measured evidence so the follow-ups survive the session context. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012YRCXgX4AWZ579bKN6sk6b --- docs/outstanding-issues.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 056df14d5d..92196b8ce5 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -76,7 +76,7 @@ removed after current-main verification; it is not missing recommended work. | 28 | `#079` | Optional | High — repository hygiene | In explicitly scheduled batches | 30–60 minutes per batch | Disposition at most ten retained worktrees per pass using owner, PR, review-ledger, ancestry, and patch evidence. Preserve every dirty, active, secret-bearing, post-freeze, or ambiguous worktree and stop rather than broad-cleaning. | | 29 | `#086` | A3 | High — repository structure + Specialist | On explicit go-ahead for X3; later packages own their gates | 1 PR per work order | Ship remaining maturity backlog (X3 rag.ts; X7 src/lib reorg; X6 coverage floors; X5 ACL consolidation; L1 one-shot archive; L4 ledger rotation; M1 host hardening) as verified draft PRs from `docs/maturity-backlog-workorders.md`. Start with X3 after go-ahead; stop before RAG edits without the flag or X5 without live-DB approval. | - + ## Open items @@ -131,6 +131,8 @@ removed after current-main verification; it is not missing recommended work. | #095 | P3 | issue | `PR required` reports failure for concurrency-cancelled jobs | **Outcome:** a red `PR required` means a real failure. **Detail:** the aggregate calls `require_success` on `coverage`/`production-ui`, so any push that supersedes an in-flight run reports `coverage result was cancelled` → exit 1, indistinguishable at a glance from a genuine failure. Eleven such reds were produced on PR #1316 in one session, and `Production UI` never once ran to completion. **Next:** in `.github/workflows/ci.yml`, either treat `cancelled` distinctly from `failure` in the aggregate, or reduce push frequency against long UI runs. **Stop:** do not relax `require_success` for genuine failures while doing so. | PR #1316 runs 30340972329 / 30341225585; session 2026-07-28 | 2026-07-28 | | #096 | P2 | task | One PR #1316 review fix is still a live gap on `main` | **Outcome:** the two remaining PR #1316 review findings are fixed on `main` with tests. **Do not chase the commits.** The seven Codex follow-up SHAs (`ff5b682`, `77cfe12`, `9840ed9`, `81ffb86`, `a5d6561`, `967e16c`, `e544d0d`) are **unreachable** — `git fetch origin ` fails for all seven, no open PR or branch carries them, and none was in the squash merge `4bcfeb90`. They were authored in a sandbox on a branch named `work` and never pushed, so the "follow-up PR metadata" each reported does not exist. **Durable source:** the [PR #1316 review threads](https://github.com/BigSimmo/Database/pull/1316/files) persist and describe every fix with file and line detail; re-derive from those, not from the hashes. **Still live on `main` (verified 2026-07-28):** the band adoption gate skips query-backed root modes — `tests/search-results-band-adoption.test.ts:101` returns null for `pathOnly === "/"`, so `/?mode=prescribing` and Documents never enter the route inventory and the root dashboard page is unchecked. **Already fixed independently, no action:** favourites hub counts (`libraryCountsTrusted`), the document-search status derivation, the 401 session-expiry path, and the record-path duplicate notice. **Corrected 2026-07-28 — the Therapy Compass retry-waiter finding is NOT a live defect.** `use-therapy-data.ts:68` `retryWaitersRef` is genuinely unscoped, so a newer request can settle an older retry's promise, but no caller observes it: `useTherapyData` lives in the long-lived `TcProvider` (`bindings.tsx:206`) and `requestKey` derives only from `screen`, so it cannot change without the screen changing; the sole awaiting caller is the band's `AsyncButton` inside `search-screen.tsx:33`, which unmounts on that transition, and `workspace.tsx:36` uses `onClick={b.retryData}` which discards the promise. An earlier note here claimed a visible "Retry stops being busy" symptom — that was wrong and is retained only as the correction. It becomes real if a future caller ever awaits `retry()` from a control that survives a `requestKey` change. **Next:** resolve root-path and href-less modes to `src/app/(search-app)/page.tsx` in the adoption gate, with a negative fixture for a disconnected root route. **Stop:** not user-facing; do not let it block a release, and do not add waiter keying without a reproducer showing a still-mounted control whose busy state clears early. | PR #1316 review sweep; session 2026-07-28 | 2026-07-28 | | #097 | P3 | issue | Gitleaks reports a false red when the PR head moves mid-run | **Outcome:** a red `Gitleaks` means a secret was found, not that someone pushed. **Detail:** on 2026-07-28 the job triggered for head `9bace1d1` checked out that merge ref, then queried the API and built its range against head `40278453` — pushed seconds later and absent from the checkout. Git rejected the range (`fatal: Invalid revision range`), so it scanned `~0 bytes`, logged `no leaks found in partial scan`, and exited 1. The scan did not run at all, which is worse than a normal failure because the natural reading is "noise, ignore it". It cleared on its own once the head stopped moving (`23 commits scanned`, `~198 KB`, `no leaks found`). Both range endpoints resolve in any complete checkout — verified locally against the branch and the PR merge ref — so this is not a `fetch-depth` problem. **Next:** pin the scan to a range the job controls (`base.sha`..the checked-out head) instead of re-querying the API mid-run, so a concurrent push cannot invalidate it. **Related:** same push-churn family as #095. **Stop:** do not weaken the gate to a soft-pass; the fix is a stable range, not a tolerated failure. | PR #1316 runs 30344938800 / 30346797225; session 2026-07-28 | 2026-07-28 | +| #098 | P2 | rec | Ingestion worker and indexing agent are verified by grepping their own source | **Outcome:** the ingestion worker and indexing agent are verified by executing code, not by asserting on their own source text. **Detail:** measured 2026-07-29 via `npm run test:coverage` — `worker/main.ts` (2,015 lines) and `supabase/functions/indexing-v3-agent/index.ts` (1,966 lines) each report **0% executed lines**; no test imports either module. Both are covered only by `readFileSync` + `toContain` assertions in `worker-safe-logging.test.ts`, `worker-visual-capture.test.ts` and `document-metadata-merge.test.ts`, which pass whenever a string is present and break on harmless refactors; `document-metadata-merge.test.ts` additionally reimplements the SQL deep-merge in TypeScript and tests the reimplementation rather than the worker. Area totals: `worker/` 18.6% lines, `supabase/functions/` 4.5%. **Next:** continue the extraction pattern that already works here — `indexing-v3-agent/behavior.ts` (167 lines, 96%) and `ingestion-worker/auth.ts` (30 lines, 90%) — pulling the highest-risk decision points out of `worker/main.ts` (job claim/retry, generation commit, failure classification) into importable modules with executing tests, retiring the matching source-text assertion as each lands. Roughly cost-neutral: each extracted test replaces a grep assertion. **Stop:** do not try to make the 2,000-line entrypoint importable in one pass; extract incrementally and keep each step green. | session 2026-07-29 test-coverage analysis | 2026-07-29 | +| #099 | P2 | rec | Component state matrices are the largest untested surface | **Outcome:** loading / empty / error / disabled states on interactive components are covered by executing tests, not only by E2E happy paths. **Detail:** measured 2026-07-29 — production components (excluding mockups) sit at **38.2% lines / 22.8% branch** across 12,602 lines, with **83 of 208 files at zero executed lines**; there are 51 `.dom.test.tsx` files against 195 components. Playwright does visit these routes, so they are smoke-covered, but branch coverage is where the state matrix lives and smoke journeys rarely reach it. Worst by uncovered lines: `global-search-shell.tsx` (7%), `mode-action-popup.tsx` (21%), `answer-content.tsx` (27%), `document-search-results.tsx` (32%), `universal-search-command-surface.tsx` (39%), `master-search-header.tsx` (43%). A concrete first target with clinical meaning: `calculator-ui.tsx` now covers all exported scoring logic, but `seedCheckboxDefaults`, `toggleCheckboxAnswer` and `selectOptionAnswer` stay uncovered because they are module-private and only reachable through React event handlers — `seedCheckboxDefaults` is what makes an all-negative CAGE / SAD PERSONS screen read as a valid 0 rather than incomplete, so a regression there is a false-negative risk. **Next:** treat as a per-PR convention rather than a backfill push — `docs/testing.md` already prescribes the state matrix, so the gap is enforcement. Start with `global-search-shell.tsx`, which `docs/search-chrome-behaviour.md` treats as a contract surface. Keep additions in the jsdom tier (measured ~0.54s per file) instead of new Playwright journeys (~231 production journeys already run serially at `workers: 1` against a 45-minute CI budget). **Stop:** do not chase the coverage percentage by backfilling low-risk components; the re-ratcheted broad floor in `vitest.config.mts` holds the line. | session 2026-07-29 test-coverage analysis | 2026-07-29 | ## Resolved / archive From 665a036291ccc30fd0d2fece9e3dfd4cfd799f1d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 04:39:12 +0000 Subject: [PATCH 2/9] test: cover clinical calculator scoring and the batch image signed-URL route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds executing coverage to the two highest-risk surfaces that had none, and re-ratchets the broad coverage floor that had drifted well below measured. Clinical calculator scoring (src/components/calculators/calculator-ui.tsx, previously 0% executed lines). The exported pure functions carry the scoring and interpretation for eight validated instruments — PHQ-9, GAD-7, K10, MDQ, CAGE, AUDIT-C, SAD PERSONS, Y-BOCS — and the only calculator test in the repo asserted mockup import boundaries. A wrong severity band renders as an ordinary result, so it needs a floor. tests/calculator-scoring.test.ts covers band-table integrity for all eight instruments (contiguous, spanning the declared range, exactly one band per reachable score, range actually reachable from the items), PHQ-9/GAD-7 boundary scores either side of every cut point, itemScore edge cases, band suppression for scales that cannot read zero (K10, CAGE), the MDQ three-criterion rule, and the result summary text. No live defect was found — the band tables are correct today; nothing was holding them there. Batch image signed URLs (src/app/api/images/signed-urls/route.ts, previously 0% executed lines, no test references). The route mints up to 100 signed URLs per call and carries its own owner-scope and committed-generation implementation, parallel to /api/images/[id]/signed-url which already had five cases. check:owner-scope-api cannot cover the real protection because document_images has no owner_id column: the only thing excluding another owner's image is the documentMap join filter in the handler. Nine cases now pin that filter, including the batch-specific case where one unowned id rides along with an owned one. Added to tests/private-access-routes.test.ts rather than a new file so it reuses that harness; createSignedUrls was added to the storage mock. Both additions were mutation-checked. Overlapping a PHQ-9 band fails three tests including "scores 5 as Mild"; loosening the MDQ impairment threshold from moderate to minor fails one; making the batch route allow a documentMap miss fails the two cross-owner cases. Before this change that last mutation passed the entire suite silently. Coverage floor: the broad group measured 63.99/55.29/57.6/66.19 against floors of 48/38/43/50 — 14-17pp of slack, so a change could delete a large amount of coverage and still pass. Each floor now sits ~2pp under measured. Kept deliberately in the cheap tiers: the unit suite is 46% test execution and 54% import/environment overhead, so a new file costs ~0.37s before it asserts anything while an assertion in an existing file costs ~0.03s. The calculator file runs 60 tests in 514ms (27ms of that is assertions). Nothing was added to the Playwright gate, which runs ~231 journeys serially against a 45-minute budget. Verified: npm run verify:cheap (exit 0; 423 files, 4340 passed | 4 skipped) and npm run test:coverage (exit 0, no threshold errors). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012YRCXgX4AWZ579bKN6sk6b --- tests/calculator-scoring.test.ts | 339 ++++++++++++++++++++++++++++ tests/private-access-routes.test.ts | 201 ++++++++++++++++- vitest.config.mts | 15 +- 3 files changed, 549 insertions(+), 6 deletions(-) create mode 100644 tests/calculator-scoring.test.ts diff --git a/tests/calculator-scoring.test.ts b/tests/calculator-scoring.test.ts new file mode 100644 index 0000000000..bc0f6c4963 --- /dev/null +++ b/tests/calculator-scoring.test.ts @@ -0,0 +1,339 @@ +import { describe, expect, it } from "vitest"; + +import { calculators, type CalculatorFixture, type CalculatorItem } from "@/components/calculators/calculator-fixtures"; +import { + deriveCalculator, + formatResultSummary, + itemScore, + progressLabel, + type AnswerMap, +} from "@/components/calculators/calculator-ui"; + +/* + * Scoring guard for the eight validated instruments in the calculators mode. + * + * `calculator-ui.tsx` held the band lookup, the MDQ three-criterion rule and the + * band-suppression rules with no executing test — the only calculator test in the + * repo asserted mockup import boundaries. A wrong band is a clinical + * misclassification that renders as a normal result, so the floor belongs here + * rather than in a Playwright journey: these are pure functions, so the whole + * file costs about as much as one browser navigation. + * + * Everything below drives the exported pure functions only. No DOM, no mocks. + */ + +function fixture(id: string): CalculatorFixture { + const calc = calculators.find((entry) => entry.id === id); + if (!calc) throw new Error(`calculator fixture "${id}" not found`); + return calc; +} + +function lowestOptionIndex(item: CalculatorItem): number { + const options = item.options ?? []; + let lowest = 0; + for (let index = 1; index < options.length; index += 1) { + if (options[index].points < options[lowest].points) lowest = index; + } + return lowest; +} + +/** Every item at its floor: the minimum score the instrument can report. */ +function baselineAnswers(calc: CalculatorFixture): AnswerMap { + const answers: AnswerMap = {}; + for (const item of calc.items) { + answers[item.id] = item.kind === "options" ? lowestOptionIndex(item) : 0; + } + return answers; +} + +/** Every item at its ceiling: the maximum score the instrument can report. */ +function maximalAnswers(calc: CalculatorFixture): AnswerMap { + const answers: AnswerMap = {}; + for (const item of calc.items) { + if (item.kind !== "options") { + answers[item.id] = 1; + continue; + } + const options = item.options ?? []; + let highest = 0; + for (let index = 1; index < options.length; index += 1) { + if (options[index].points > options[highest].points) highest = index; + } + answers[item.id] = highest; + } + return answers; +} + +/** + * Build a complete answer map scoring exactly `target`. Starts from the floor and + * raises one item at a time. Every caller asserts the resulting score, so a helper + * that cannot reach the target fails loudly instead of weakening the assertion. + */ +function answersForScore(calc: CalculatorFixture, target: number): AnswerMap { + const answers = baselineAnswers(calc); + let remaining = target - deriveCalculator(calc, answers).score; + + for (const item of calc.items) { + if (remaining <= 0) break; + if (item.kind === "options") { + const options = item.options ?? []; + const currentIndex = answers[item.id] ?? 0; + const currentPoints = options[currentIndex]?.points ?? 0; + let best = currentIndex; + for (let index = 0; index < options.length; index += 1) { + const delta = options[index].points - currentPoints; + if (delta > 0 && delta <= remaining && options[index].points > options[best].points) best = index; + } + remaining -= (options[best]?.points ?? 0) - currentPoints; + answers[item.id] = best; + } else { + const points = item.points ?? 0; + if (points > 0 && points <= remaining) { + answers[item.id] = 1; + remaining -= points; + } + } + } + + return answers; +} + +describe("calculator band tables", () => { + it.each(calculators.map((calc) => [calc.id, calc] as const))( + "%s declares contiguous bands spanning its full score range", + (_id, calc) => { + const bands = [...calc.bands].sort((left, right) => left.min - right.min); + + expect(bands.length).toBeGreaterThan(0); + expect(bands[0].min).toBe(calc.minScore); + expect(bands[bands.length - 1].max).toBe(calc.maxScore); + + for (let index = 1; index < bands.length; index += 1) { + // A gap leaves a score with no band ("—"); an overlap makes the + // reported severity depend on declaration order. + expect(bands[index].min, `band ${index} of ${calc.id} must start one above the previous band`).toBe( + bands[index - 1].max + 1, + ); + } + }, + ); + + it.each(calculators.map((calc) => [calc.id, calc] as const))( + "%s maps every reachable score to exactly one band", + (_id, calc) => { + for (let score = calc.minScore; score <= calc.maxScore; score += 1) { + const matches = calc.bands.filter((band) => score >= band.min && score <= band.max); + expect(matches, `${calc.id} score ${score} matched ${matches.length} bands`).toHaveLength(1); + } + }, + ); + + it.each(calculators.map((calc) => [calc.id, calc] as const))( + "%s items can actually reach its declared score range", + (_id, calc) => { + // A declared range the items cannot produce means either an unreachable + // band or a score that falls off the end of the table. + expect(deriveCalculator(calc, baselineAnswers(calc)).score).toBe(calc.minScore); + expect(deriveCalculator(calc, maximalAnswers(calc)).score).toBe(calc.maxScore); + }, + ); +}); + +describe("itemScore", () => { + const checkbox: CalculatorItem = { id: "x", text: "x", kind: "checkbox", points: 3 }; + const options: CalculatorItem = { + id: "y", + text: "y", + kind: "options", + options: [ + { label: "None", short: "None", points: 0 }, + { label: "Some", short: "Some", points: 2 }, + ], + }; + + it("scores an unanswered item as zero", () => { + expect(itemScore(checkbox, undefined)).toBe(0); + expect(itemScore(options, undefined)).toBe(0); + }); + + it("scores a checkbox only when explicitly ticked", () => { + expect(itemScore(checkbox, 1)).toBe(3); + expect(itemScore(checkbox, 0)).toBe(0); + }); + + it("scores an options item by the selected option's points", () => { + expect(itemScore(options, 0)).toBe(0); + expect(itemScore(options, 1)).toBe(2); + }); + + it("treats an out-of-range option index as zero rather than NaN", () => { + expect(itemScore(options, 7)).toBe(0); + }); + + it("treats a checkbox with no declared points as zero", () => { + expect(itemScore({ id: "z", text: "z", kind: "checkbox" }, 1)).toBe(0); + }); +}); + +describe("PHQ-9 severity banding", () => { + const phq9 = fixture("phq9"); + + // Boundary scores on both sides of every published PHQ-9 cut point. These map + // to stepped treatment actions, so an off-by-one here changes clinical advice. + const boundaries = [ + [0, "Minimal"], + [4, "Minimal"], + [5, "Mild"], + [9, "Mild"], + [10, "Moderate"], + [14, "Moderate"], + [15, "Moderately severe"], + [19, "Moderately severe"], + [20, "Severe"], + [27, "Severe"], + ] as const; + + it.each(boundaries)("scores %i as %s", (score, label) => { + const state = deriveCalculator(phq9, answersForScore(phq9, score)); + expect(state.score).toBe(score); + expect(state.band?.label).toBe(label); + expect(state.result.label).toBe(label); + }); + + it("raises the item-9 self-harm flag only when that item scores above zero", () => { + const flagged = deriveCalculator(phq9, { ...baselineAnswers(phq9), p9: 1 }); + expect(flagged.flags).toEqual(["Item 9 endorsed — complete a structured suicide-risk assessment now."]); + + const unflagged = deriveCalculator(phq9, { ...baselineAnswers(phq9), p9: 0 }); + expect(unflagged.flags).toEqual([]); + }); + + it("publishes a provisional band while a zero-floor scale is still filling in", () => { + const partial = deriveCalculator(phq9, { p1: 3, p2: 3 }); + expect(partial.complete).toBe(false); + expect(partial.started).toBe(true); + expect(partial.band?.label).toBe("Mild"); + }); +}); + +describe("GAD-7 severity banding", () => { + const gad7 = fixture("gad7"); + + it.each([ + [0, "Minimal"], + [4, "Minimal"], + [5, "Mild"], + [9, "Mild"], + [10, "Moderate"], + [14, "Moderate"], + [15, "Severe"], + [21, "Severe"], + ] as const)("scores %i as %s", (score, label) => { + const state = deriveCalculator(gad7, answersForScore(gad7, score)); + expect(state.score).toBe(score); + expect(state.band?.label).toBe(label); + }); +}); + +describe("band suppression for scales that cannot read zero", () => { + it("withholds a K10 band until every item is answered", () => { + const k10 = fixture("k10"); + // K10 floors at 10, so a part-filled scale would otherwise fall below the + // table and read as the lowest band on incomplete data. + const partial = deriveCalculator(k10, { k1: 4, k2: 4 }); + expect(partial.complete).toBe(false); + expect(partial.band).toBeUndefined(); + expect(partial.result.label).toBe("—"); + + const complete = deriveCalculator(k10, answersForScore(k10, 30)); + expect(complete.complete).toBe(true); + expect(complete.score).toBe(30); + expect(complete.band?.label).toBe("Very high"); + }); + + it("withholds a CAGE band until every yes/no item is recorded", () => { + const cage = fixture("cage"); + // A half-ticked checkbox screen still has undefined items; reading it as + // "low risk" would be a false negative. + const partial = deriveCalculator(cage, { c1: 0, c2: 0 }); + expect(partial.complete).toBe(false); + expect(partial.band).toBeUndefined(); + + const allNegative = deriveCalculator(cage, { c1: 0, c2: 0, c3: 0, c4: 0 }); + expect(allNegative.complete).toBe(true); + expect(allNegative.score).toBe(0); + expect(allNegative.band?.label).toBeDefined(); + }); +}); + +describe("MDQ three-criterion screen", () => { + const mdq = fixture("mdq"); + + /** `count` symptom items endorsed, plus the two criterion items. */ + function mdqAnswers(count: number, extras: AnswerMap = {}): AnswerMap { + const answers: AnswerMap = {}; + for (let index = 1; index <= 13; index += 1) { + answers[`m${index}`] = index <= count ? 1 : 0; + } + return { ...answers, ...extras }; + } + + it("returns a positive screen only when all three criteria are met", () => { + const state = deriveCalculator(mdq, mdqAnswers(7, { mco: 1, mimp: 2 })); + expect(state.score).toBe(7); + expect(state.result.label).toBe("Positive screen"); + expect(state.result.tone).toBe("danger"); + }); + + it("withholds a positive screen when co-occurrence is not confirmed", () => { + const state = deriveCalculator(mdq, mdqAnswers(7, { mco: 0, mimp: 3 })); + expect(state.result.label).toBe("Symptom threshold met"); + expect(state.result.guidance).toContain("co-occurrence"); + }); + + it("withholds a positive screen when impairment is below moderate", () => { + const state = deriveCalculator(mdq, mdqAnswers(9, { mco: 1, mimp: 1 })); + expect(state.result.label).toBe("Symptom threshold met"); + expect(state.result.guidance).toContain("moderate-or-serious impairment"); + }); + + it("names both unmet criteria when neither is confirmed", () => { + const state = deriveCalculator(mdq, mdqAnswers(13, { mco: 0, mimp: 0 })); + expect(state.result.guidance).toContain("co-occurrence and moderate-or-serious impairment"); + }); + + it("returns a negative screen one symptom below the threshold", () => { + const state = deriveCalculator(mdq, mdqAnswers(6, { mco: 1, mimp: 3 })); + expect(state.score).toBe(6); + expect(state.result.label).toBe("Negative screen"); + expect(state.result.tone).toBe("success"); + }); + + it("scores the criterion items as zero points", () => { + // `mco` and `mimp` record state but must never inflate the symptom count. + const withCriteria = deriveCalculator(mdq, mdqAnswers(7, { mco: 1, mimp: 3 })); + const withoutCriteria = deriveCalculator(mdq, mdqAnswers(7, { mco: 0, mimp: 0 })); + expect(withCriteria.score).toBe(withoutCriteria.score); + }); +}); + +describe("result summary text", () => { + const phq9 = fixture("phq9"); + + it("reports score, maximum and band once the scale is complete", () => { + const state = deriveCalculator(phq9, answersForScore(phq9, 12)); + expect(formatResultSummary(phq9, state)).toBe("PHQ-9 12/27 — Moderate"); + }); + + it("appends progress while the scale is incomplete", () => { + const state = deriveCalculator(phq9, { p1: 3, p2: 3 }); + expect(state.complete).toBe(false); + expect(formatResultSummary(phq9, state)).toBe("PHQ-9 6/27 — Mild (2 of 9 answered)"); + }); + + it("counts endorsements rather than answers for checkbox-only scales", () => { + const cage = fixture("cage"); + const state = deriveCalculator(cage, { c1: 1, c2: 1, c3: 0, c4: 0 }); + expect(progressLabel(state)).toBe("2 of 4 endorsed"); + }); +}); diff --git a/tests/private-access-routes.test.ts b/tests/private-access-routes.test.ts index 863ad2d83c..11f2e67735 100644 --- a/tests/private-access-routes.test.ts +++ b/tests/private-access-routes.test.ts @@ -221,6 +221,13 @@ function createSupabaseMock(resolve: QueryResolver = defaultQueryResolver) { data: { signedUrl: `https://signed.local/${path}` }, error: null, })); + // Batch sibling of `createSignedUrl`, used by /api/images/signed-urls. Supabase + // returns one entry per requested path, so the route matches results back to + // images by path rather than by index. + const createSignedUrls = vi.fn(async (paths: string[]) => ({ + data: paths.map((path) => ({ path, signedUrl: `https://signed.local/${path}`, error: null })), + error: null, + })); const remove = vi.fn( async ( ...args: [string[]] @@ -232,7 +239,7 @@ function createSupabaseMock(resolve: QueryResolver = defaultQueryResolver) { return { data: [], error: null }; }, ); - const storageFrom = vi.fn(() => ({ upload, createSignedUrl, remove })); + const storageFrom = vi.fn(() => ({ upload, createSignedUrl, createSignedUrls, remove })); const getUser = vi.fn(async (receivedToken?: string) => receivedToken === token ? { data: { user: { id: userId, app_metadata: { site_role: "administrator" } } }, error: null } @@ -351,7 +358,7 @@ function createSupabaseMock(resolve: QueryResolver = defaultQueryResolver) { }), rpc, storage: { from: storageFrom }, - storageMocks: { upload, createSignedUrl, remove, storageFrom }, + storageMocks: { upload, createSignedUrl, createSignedUrls, remove, storageFrom }, }; return client; @@ -1227,6 +1234,196 @@ describe("private document API access", () => { expect(client.storageMocks.createSignedUrl).not.toHaveBeenCalled(); }); + /* + * Batch sibling of the five `/api/images/[id]/signed-url` cases above. + * `/api/images/signed-urls` mints up to 100 signed URLs per call and carries its + * OWN owner-scope and committed-generation implementation. `check:owner-scope-api` + * cannot cover the real protection here: `document_images` has no `owner_id` + * column, so the only thing keeping another owner's image out of the response is + * the `documentMap` join filter in the handler. These cases pin that filter. + */ + const otherImageId = "44444444-4444-4444-8444-444444444444"; + + function signedUrlsRequest(imageIds: string[], authenticated = true) { + const init: RequestInit = { + method: "POST", + body: JSON.stringify({ imageIds }), + headers: { "content-type": "application/json" }, + }; + return authenticated + ? authenticatedRequest("/api/images/signed-urls", init) + : request("/api/images/signed-urls", init); + } + + /** Two images on two documents with different owners, plus owner-scoped document reads. */ + function createBatchImageMock(options: { imageGeneration?: string; documentGeneration?: string } = {}) { + const imageGeneration = options.imageGeneration ?? "generation-a"; + const documentGeneration = options.documentGeneration ?? "generation-a"; + return createSupabaseMock((call) => { + if (call.table === "document_images") { + const requested = call.inFilters.find((filter) => filter.column === "id")?.values ?? []; + return ok( + [ + { + id: imageId, + document_id: documentId, + storage_path: `${userId}/images/${imageId}.png`, + mime_type: "image/png", + caption: "Owned image", + metadata: { index_generation_id: imageGeneration }, + }, + { + id: otherImageId, + document_id: otherDocumentId, + storage_path: `${otherUserId}/images/${otherImageId}.png`, + mime_type: "image/png", + caption: "Other owner's image", + metadata: { index_generation_id: imageGeneration }, + }, + ].filter((image) => requested.includes(image.id)), + ); + } + if (call.table === "documents" && matchesOwnerReadScope(call, userId)) { + // Only the caller's own document comes back from an owner-scoped read. + return ok([{ id: documentId, metadata: { index_generation_id: documentGeneration } }]); + } + return ok([]); + }); + } + + it("signs a batch of images whose parent documents are owned", async () => { + const client = createBatchImageMock(); + mockRuntime(client); + const { POST } = await import("../src/app/api/images/signed-urls/route"); + + const response = await POST(signedUrlsRequest([imageId])); + const body = await payload(response); + + expect(response.status).toBe(200); + expect(body.urls).toMatchObject({ + [imageId]: { url: `https://signed.local/${userId}/images/${imageId}.png`, mimeType: "image/png" }, + }); + expect(client.storageMocks.createSignedUrls).toHaveBeenCalledWith([`${userId}/images/${imageId}.png`], 600); + }); + + it("omits images whose parent document belongs to another user", async () => { + const client = createBatchImageMock(); + mockRuntime(client); + const { POST } = await import("../src/app/api/images/signed-urls/route"); + + const response = await POST(signedUrlsRequest([otherImageId])); + const body = await payload(response); + + expect(response.status).toBe(200); + expect(body.urls).toEqual({}); + expect(client.storageMocks.createSignedUrls).not.toHaveBeenCalled(); + }); + + it("signs only the owned image when a batch mixes owners", async () => { + // The batch-specific leak: one unowned id riding along with an owned one must + // not inherit the owned document's access. + const client = createBatchImageMock(); + mockRuntime(client); + const { POST } = await import("../src/app/api/images/signed-urls/route"); + + const response = await POST(signedUrlsRequest([imageId, otherImageId])); + const body = await payload(response); + + expect(response.status).toBe(200); + expect(Object.keys(body.urls as Record)).toEqual([imageId]); + expect(client.storageMocks.createSignedUrls).toHaveBeenCalledWith([`${userId}/images/${imageId}.png`], 600); + }); + + it("scopes the batch document read to the caller", async () => { + const client = createBatchImageMock(); + mockRuntime(client); + const { POST } = await import("../src/app/api/images/signed-urls/route"); + + await POST(signedUrlsRequest([imageId])); + + const documentReads = client.calls.filter((call) => call.table === "documents"); + expect(documentReads.length).toBeGreaterThan(0); + expect(documentReads.every((call) => matchesOwnerReadScope(call, userId))).toBe(true); + }); + + it("restricts an anonymous batch to public documents", async () => { + const client = createSupabaseMock((call) => { + if (call.table === "document_images") { + return ok([ + { + id: imageId, + document_id: documentId, + storage_path: `${userId}/images/${imageId}.png`, + mime_type: "image/png", + caption: "Owned image", + metadata: { index_generation_id: "generation-a" }, + }, + ]); + } + // No public document matches, so an anonymous caller gets nothing. + if (call.table === "documents" && matchesOwnerReadScope(call)) return ok([]); + return ok([]); + }); + mockRuntime(client); + const { POST } = await import("../src/app/api/images/signed-urls/route"); + + const response = await POST(signedUrlsRequest([imageId], false)); + + expect(response.status).toBe(200); + expect(await payload(response)).toEqual({ urls: {} }); + expect(client.storageMocks.createSignedUrls).not.toHaveBeenCalled(); + }); + + it("omits batch images from an uncommitted replacement generation", async () => { + const client = createBatchImageMock({ imageGeneration: "generation-new", documentGeneration: "generation-old" }); + mockRuntime(client); + const { POST } = await import("../src/app/api/images/signed-urls/route"); + + const response = await POST(signedUrlsRequest([imageId])); + + expect(response.status).toBe(200); + expect(await payload(response)).toEqual({ urls: {} }); + expect(client.storageMocks.createSignedUrls).not.toHaveBeenCalled(); + }); + + it("returns an empty map for an empty batch without touching storage", async () => { + const client = createBatchImageMock(); + mockRuntime(client); + const { POST } = await import("../src/app/api/images/signed-urls/route"); + + const response = await POST(signedUrlsRequest([])); + + expect(response.status).toBe(200); + expect(await payload(response)).toEqual({ urls: {} }); + expect(client.storageMocks.createSignedUrls).not.toHaveBeenCalled(); + }); + + it("rejects a batch of non-uuid image ids before querying", async () => { + const client = createBatchImageMock(); + mockRuntime(client); + const { POST } = await import("../src/app/api/images/signed-urls/route"); + + const response = await POST(signedUrlsRequest(["not-a-uuid"])); + + expect(response.status).toBe(400); + expect(client.storageMocks.createSignedUrls).not.toHaveBeenCalled(); + }); + + it("rejects a batch larger than the documented maximum", async () => { + const client = createBatchImageMock(); + mockRuntime(client); + const { POST } = await import("../src/app/api/images/signed-urls/route"); + + const tooMany = Array.from( + { length: 101 }, + (_, index) => `55555555-5555-4555-8555-${String(index).padStart(12, "0")}`, + ); + const response = await POST(signedUrlsRequest(tooMany)); + + expect(response.status).toBe(400); + expect(client.storageMocks.createSignedUrls).not.toHaveBeenCalled(); + }); + it("rejects anonymous uploads without touching storage", async () => { const client = createSupabaseMock(); mockRuntime(client); diff --git a/vitest.config.mts b/vitest.config.mts index e7d5347a67..96d949689c 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -27,11 +27,18 @@ const config = { include: ["src/**/*.{ts,tsx}", "scripts/**/*.{ts,mjs,cjs}", "worker/**/*.ts", "supabase/functions/**/*.ts"], exclude: ["src/lib/supabase/database.types.ts"], thresholds: { + // Broad regression floor. Re-ratcheted 2026-07-29: the previous values + // (48/38/43/50) had drifted 14-17pp below measured coverage + // (63.99/55.29/57.6/66.19), so a change could delete a large amount of + // coverage and still pass. Each floor now sits ~2pp under measured — enough + // headroom for a PR that ships an uncovered surface, not enough to hide a + // regression. Re-measure with `npm run test:coverage` and raise these when + // the gap grows past ~5pp again; never lower them to make a red gate green. "src/{lib/**/*.ts,app/**/route.ts,components/**/*.{ts,tsx}}": { - statements: 48, - branches: 38, - functions: 43, - lines: 50, + statements: 62, + branches: 53, + functions: 55, + lines: 64, }, // Aggregate behavioral floors ratchet the full post-fixture group rather // than making individual large RAG modules brittle. Each value is the From 6f476b5f741627cb622af57d1b4665e3989789ca Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 11:29:56 +0000 Subject: [PATCH 3/9] docs: record PR #1383 babysit after main merge Ledger row for HEAD 0922d7f5: conflict cleared by renumbering coverage follow-ups to #106/#107; verify:cheap and coverage green. 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 d8f751f93b..e86ebd7ad4 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -1261,3 +1261,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-07-29 | claude/latency-fixes-2026-07-29 | 0e30215e98328513e23342a8dceafc9d1728bf9d | pr-1376-ci-bugbot-repair | fixed-p1s-plus-followups;owner-scoped-epochs;shared-cache-race;stream-signal;empty-scope-timing;threads-resolved | vitest:15-pass;docs:check-links:pass;verify:cheap:earlier-pass | | 2026-07-29 | codex/remove-source-overlays | a08a81d320c9f8e1bbbe1facc266d8257213b1ad | PR #1378 babysit | FIXED Codex P1s (restore governance notice); overlays/Preview removed; merged main; verify:cheap PASS; Bugbot no open findings | verify:cheap 4273 pass; focused DOM 4/4; eslint/tsc/build PASS; hosted CI re-running after main sync | | 2026-07-29 | claude/clinical-design-system-update-e34ca9 | 0cdae091ad92f40e0ad7335b3e2d396c44188a4f | PR #1375 conflict fix + Bugbot | FIXED second CONFLICTING after #1378: took main removal of SelectedDocumentEvidencePanel; retained tracking-eyebrow on surviving document-search-results. Prior DocumentViewerRail + form-detail settlement retained. MERGEABLE; CI re-running. | local: document-search-record-fault + design-token tests; merge-tree CLEAN; prior Production UI PASS on 6903f51f; form-detail e2e 2/2. | +| 2026-07-29 | claude/test-coverage-analysis-2vcd8a | 0922d7f56624ef84be8abcb2bbc89205027cf9a6 | PR #1383 babysit | BLOCKER CLEARED: merged origin/main; renumbered coverage follow-ups #098/#099 -> #106/#107 (main claimed #098-#105). Before: CONFLICTING/DIRTY, 4 behind; CI green on prior tip; 0 review threads; 0 Bugbot findings. After: mergeable expected; verify:cheap 424 files/4371 passed; test:coverage exit 0; format:changed + check:rag:fixtures pass. | verify:cheap PASS (424 files, 4371 passed \| 4 skipped); test:coverage PASS (no threshold errors); format:changed PASS; check:rag:fixtures PASS (36 golden); Bugbot: no findings; no provider-backed checks | From b72bc209b87cad249f4c21b7526a4472ffa71ece Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 11:30:17 +0000 Subject: [PATCH 4/9] docs: supersede PR #1383 babysit ledger at tip 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 e86ebd7ad4..136c9563a0 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -1262,3 +1262,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-07-29 | codex/remove-source-overlays | a08a81d320c9f8e1bbbe1facc266d8257213b1ad | PR #1378 babysit | FIXED Codex P1s (restore governance notice); overlays/Preview removed; merged main; verify:cheap PASS; Bugbot no open findings | verify:cheap 4273 pass; focused DOM 4/4; eslint/tsc/build PASS; hosted CI re-running after main sync | | 2026-07-29 | claude/clinical-design-system-update-e34ca9 | 0cdae091ad92f40e0ad7335b3e2d396c44188a4f | PR #1375 conflict fix + Bugbot | FIXED second CONFLICTING after #1378: took main removal of SelectedDocumentEvidencePanel; retained tracking-eyebrow on surviving document-search-results. Prior DocumentViewerRail + form-detail settlement retained. MERGEABLE; CI re-running. | local: document-search-record-fault + design-token tests; merge-tree CLEAN; prior Production UI PASS on 6903f51f; form-detail e2e 2/2. | | 2026-07-29 | claude/test-coverage-analysis-2vcd8a | 0922d7f56624ef84be8abcb2bbc89205027cf9a6 | PR #1383 babysit | BLOCKER CLEARED: merged origin/main; renumbered coverage follow-ups #098/#099 -> #106/#107 (main claimed #098-#105). Before: CONFLICTING/DIRTY, 4 behind; CI green on prior tip; 0 review threads; 0 Bugbot findings. After: mergeable expected; verify:cheap 424 files/4371 passed; test:coverage exit 0; format:changed + check:rag:fixtures pass. | verify:cheap PASS (424 files, 4371 passed \| 4 skipped); test:coverage PASS (no threshold errors); format:changed PASS; check:rag:fixtures PASS (36 golden); Bugbot: no findings; no provider-backed checks | +| 2026-07-29 | claude/test-coverage-analysis-2vcd8a | 6f476b5f741627cb622af57d1b4665e3989789ca | PR #1383 babysit | CLOSEOUT at tip after ledger bookkeeping commit. Merge conflict cleared; coverage follow-ups live as #106/#107; local gates green; awaiting hosted CI on tip. | same as prior tip 0922d7f5 plus ledger append only; no product code change | From a150eefb8b80139eec408aab4619e4f536b1b95a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 29 Jul 2026 11:37:15 +0000 Subject: [PATCH 5/9] docs: close out PR #1383 babysit after hosted CI green and merge PR required / Unit coverage / Static / Safety / Migration replay / CircleCI all passed on tip bebc6c02; squash auto-merge landed as f4a3f500. No remaining code defects or review threads. 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 c986aaa00b..82c306d33f 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -1264,3 +1264,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-07-29 | claude/clinical-design-system-update-e34ca9 | 0cdae091ad92f40e0ad7335b3e2d396c44188a4f | PR #1375 conflict fix + Bugbot | FIXED second CONFLICTING after #1378: took main removal of SelectedDocumentEvidencePanel; retained tracking-eyebrow on surviving document-search-results. Prior DocumentViewerRail + form-detail settlement retained. MERGEABLE; CI re-running. | local: document-search-record-fault + design-token tests; merge-tree CLEAN; prior Production UI PASS on 6903f51f; form-detail e2e 2/2. | | 2026-07-29 | claude/test-coverage-analysis-2vcd8a | 0922d7f56624ef84be8abcb2bbc89205027cf9a6 | PR #1383 babysit | BLOCKER CLEARED: merged origin/main; renumbered coverage follow-ups #098/#099 -> #106/#107 (main claimed #098-#105). Before: CONFLICTING/DIRTY, 4 behind; CI green on prior tip; 0 review threads; 0 Bugbot findings. After: mergeable expected; verify:cheap 424 files/4371 passed; test:coverage exit 0; format:changed + check:rag:fixtures pass. | verify:cheap PASS (424 files, 4371 passed \| 4 skipped); test:coverage PASS (no threshold errors); format:changed PASS; check:rag:fixtures PASS (36 golden); Bugbot: no findings; no provider-backed checks | | 2026-07-29 | claude/test-coverage-analysis-2vcd8a | 6f476b5f741627cb622af57d1b4665e3989789ca | PR #1383 babysit | CLOSEOUT at tip after ledger bookkeeping commit. Merge conflict cleared; coverage follow-ups live as #106/#107; local gates green; awaiting hosted CI on tip. | same as prior tip 0922d7f5 plus ledger append only; no product code change | +| 2026-07-29 | claude/test-coverage-analysis-2vcd8a | 5145dc990af47fba8b4c68f2b4537f21406535b6 | PR #1383 babysit | Hosted CI green on prior tip bebc6c02 (PR required / Unit coverage / Static / Safety / Migration replay / CircleCI all PASS). Synced one more clean main commit (ledger-only). MERGEABLE; 0 unresolved threads; 0 Bugbot findings; no code defects. | hosted: PR required PASS, Unit coverage PASS 4m41s, Static PR PASS, Safety PASS, Migration replay PASS, CircleCI verify PASS; local prior: verify:cheap + test:coverage PASS; Bugbot none | From 97a8be8bf82436965ea98afb3400055494861709 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 11:38:37 +0000 Subject: [PATCH 6/9] docs(ledger): record PR #1383 prlanded verification Confirms the squash merge landed by content: the diff between squash commit f4a3f50 and branch tip bebc6c0 is empty, so nothing was orphaned by the auto-merge race despite four concurrent pushes during the merge window. Also records that the coverage follow-ups were renumbered #098/#099 -> #106/#107 (main claimed #098-#105 concurrently) with both rows verified intact, and that the three red `PR required` results were concurrency cancellations rather than defects. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012YRCXgX4AWZ579bKN6sk6b --- 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 c3f7728b30..b1f6e820a3 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -1268,3 +1268,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-07-29 | claude/test-coverage-analysis-2vcd8a | 6f476b5f741627cb622af57d1b4665e3989789ca | PR #1383 babysit | CLOSEOUT at tip after ledger bookkeeping commit. Merge conflict cleared; coverage follow-ups live as #106/#107; local gates green; awaiting hosted CI on tip. | same as prior tip 0922d7f5 plus ledger append only; no product code change | | 2026-07-29 | claude/test-coverage-analysis-2vcd8a | 5145dc990af47fba8b4c68f2b4537f21406535b6 | PR #1383 babysit | Hosted CI green on prior tip bebc6c02 (PR required / Unit coverage / Static / Safety / Migration replay / CircleCI all PASS). Synced one more clean main commit (ledger-only). MERGEABLE; 0 unresolved threads; 0 Bugbot findings; no code defects. | hosted: PR required PASS, Unit coverage PASS 4m41s, Static PR PASS, Safety PASS, Migration replay PASS, CircleCI verify PASS; local prior: verify:cheap + test:coverage PASS; Bugbot none | | 2026-07-29 | codex/document-reader-condensed-view | 5678e878d4fe681d33bb58df5b5b3468a138a1c8 | pr-1380-ci-green-resync | hosted CI green on 7150899a (Static/Build/Unit/Advisory/Production UI/PR required/CircleCI); CodeRabbit density fallback + summary keys + search/plain compact tests landed; unresolved review threads none; resynced main after tip went BEHIND by 1 | hosted CI success on 7150899a; merge-tree clean; bugbot no P0/P1 | +| 2026-07-29 | claude/test-coverage-analysis-2vcd8a | f4a3f50048fa113d5e4e704defdafad2d37ba8d2 | prlanded | MERGED AND VERIFIED. PR #1383 squash-merged as f4a3f50. Content diff between the squash commit and branch tip bebc6c02 is EMPTY — nothing orphaned by the auto-merge race despite four concurrent pushes during the merge window. Landed on main: tests/calculator-scoring.test.ts (60 tests, 8 validated instruments), 9 batch signed-URL cases in tests/private-access-routes.test.ts, coverage floors raised 48/38/43/50 -> 62/53/55/64. Follow-ups renumbered #098/#099 -> #106/#107 during the main merge (main had claimed #098-#105); both rows verified present with content intact and next-id marker at 108. The three red 'PR required' results at 0922d7f/6f476b5/b72bc20 were concurrency cancellations (issue #095), not defects — all upstream jobs reported 'cancelled'; the clean run 30447852418 at tip bebc6c0 superseded them. | post-merge verification only: content diff squash-vs-tip EMPTY; origin/main content spot-checks PASS (test file present, floors 62/53/55/64, #106/#107 rows, 13 signed-urls refs); pre-merge local gates on the merged tree: test:coverage PASS exit 0 (424 files, 4371 passed \| 4 skipped, no threshold errors); no provider-backed checks | From 873d28df85db6e70814d8653958509b8ea809c91 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 12:48:58 +0000 Subject: [PATCH 7/9] docs: record branch-cleanup review and the shallow-clone analysis trap MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Branch-cleanup review of all 92 remote branches against main, recorded with scope `branch-cleanup-deletion-pending` because deletion could not be completed: the session git proxy rejects ref deletion with HTTP 403 and no delete-branch capability is exposed. Five branches are verified safe to delete (empty diff against main, no open PR) and are tracked as #108. The other 87 were deliberately not cleared — their touched files still differ from main, the conservative direction. #109 records the trap that made the first pass wrong: the session clone was shallow (74 of 2829 commits), which silently invalidated every merge-base, cherry-pick and ahead/behind result. In that state local main read as "ahead 52" with unrelated histories, and 90 of 91 branches read as unmerged. `git fetch --unshallow` corrected both, and the guide and sweep script should check for it rather than report silently. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012YRCXgX4AWZ579bKN6sk6b --- docs/branch-review-ledger.md | 1 + docs/outstanding-issues.md | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index b1f6e820a3..981db9c3b9 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -1269,3 +1269,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-07-29 | claude/test-coverage-analysis-2vcd8a | 5145dc990af47fba8b4c68f2b4537f21406535b6 | PR #1383 babysit | Hosted CI green on prior tip bebc6c02 (PR required / Unit coverage / Static / Safety / Migration replay / CircleCI all PASS). Synced one more clean main commit (ledger-only). MERGEABLE; 0 unresolved threads; 0 Bugbot findings; no code defects. | hosted: PR required PASS, Unit coverage PASS 4m41s, Static PR PASS, Safety PASS, Migration replay PASS, CircleCI verify PASS; local prior: verify:cheap + test:coverage PASS; Bugbot none | | 2026-07-29 | codex/document-reader-condensed-view | 5678e878d4fe681d33bb58df5b5b3468a138a1c8 | pr-1380-ci-green-resync | hosted CI green on 7150899a (Static/Build/Unit/Advisory/Production UI/PR required/CircleCI); CodeRabbit density fallback + summary keys + search/plain compact tests landed; unresolved review threads none; resynced main after tip went BEHIND by 1 | hosted CI success on 7150899a; merge-tree clean; bugbot no P0/P1 | | 2026-07-29 | claude/test-coverage-analysis-2vcd8a | f4a3f50048fa113d5e4e704defdafad2d37ba8d2 | prlanded | MERGED AND VERIFIED. PR #1383 squash-merged as f4a3f50. Content diff between the squash commit and branch tip bebc6c02 is EMPTY — nothing orphaned by the auto-merge race despite four concurrent pushes during the merge window. Landed on main: tests/calculator-scoring.test.ts (60 tests, 8 validated instruments), 9 batch signed-URL cases in tests/private-access-routes.test.ts, coverage floors raised 48/38/43/50 -> 62/53/55/64. Follow-ups renumbered #098/#099 -> #106/#107 during the main merge (main had claimed #098-#105); both rows verified present with content intact and next-id marker at 108. The three red 'PR required' results at 0922d7f/6f476b5/b72bc20 were concurrency cancellations (issue #095), not defects — all upstream jobs reported 'cancelled'; the clean run 30447852418 at tip bebc6c0 superseded them. | post-merge verification only: content diff squash-vs-tip EMPTY; origin/main content spot-checks PASS (test file present, floors 62/53/55/64, #106/#107 rows, 13 signed-urls refs); pre-merge local gates on the merged tree: test:coverage PASS exit 0 (424 files, 4371 passed \| 4 skipped, no threshold errors); no provider-backed checks | +| 2026-07-29 | main | 855aa2914fd9cf29f9ce34f67e197d7a2d0c1a86 | branch-cleanup-deletion-pending | Full-history branch-cleanup review of all 92 remote branches. IMPORTANT PRECONDITION: the session clone was SHALLOW (74 commits of origin/main); every merge-base and cherry-pick result computed before 'git fetch --unshallow' was invalid, and an initial pass wrongly showed 90/91 branches as carrying unmerged work. After unshallowing (2829 commits) the analysis is sound. Cherry-pick matching alone finds only 2 candidates because squash merges collapse N commits into 1 so per-commit patch-ids never match; a content test (files touched vs merge-base, compared between branch tip and main) finds 5. VERIFIED SAFE TO DELETE — each introduces an empty diff against main and backs no open PR: claude/clinical-kb-pwa-review-asi3wb, claude/dazzling-blackwell-f348d0, codex/document-reader-condensed-view, cursor/page-anchored-search-composer-30ee, cursor/pr-1379-babysit-ledger-9365. DELETION BLOCKED: the session git proxy rejects ref deletion with HTTP 403 and the GitHub MCP toolset exposes no delete-branch capability, so the five remain and must be removed from the GitHub UI or an interactive session. The other 87 were NOT cleared: their touched files still differ from main, which is the conservative direction (a branch whose files main later modified reads as not-landed). Local cleanup done: stale local main fast-forwarded to origin/main (0 ahead, 0 patch-unique after unshallow — its earlier 'ahead 52 / unrelated histories' was purely the shallow-clone artifact); redundant local claude/prlanded-ledger-1383 deleted after confirming its row is in the pushed branch. | npm run sweep:branch-ledger (report-only, 2 candidates); full-history recompute after git fetch --unshallow; per-branch git diff origin/main... empty for all 5; open-PR head cross-check against PRs #1374/#1377/#1384/#1385/#1386/#1387; no branch deleted (HTTP 403); no provider-backed checks | diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index d72da481b6..6c65d10ea6 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -84,7 +84,7 @@ removed after current-main verification; it is not missing recommended work. | 36 | `#101` | A3 | Specialist — retrieval/ranking | Only after `#098` sizes the win | 1–2 days plus canary | Outcome: independent retrieval stages stop running serially. Gate: live eval-canary pair, explicit approval, ~$1–2. Verification: 36/36 golden, document/content recall 1.0, zero per-case rr regressions. Stop on any regression and revert in a single commit. Resolved `#075` and `#083` are the precedents for why this is gated rather than free. | | 37 | `#104` | A3 | Standard — ingestion worker | Any ingestion-touching session | 30–60 minutes | Outcome: ingestion reads each extracted image once instead of up to three times (`worker/main.ts:997`). Gate: `verify:cheap`. Verification: targeted worker test plus one ingestion smoke run. Throughput only — no clinician-facing latency, so do not prioritise it above anything above. Stop if the read is load-bearing for OCR retry semantics. | - + ## Open items @@ -149,6 +149,8 @@ removed after current-main verification; it is not missing recommended work. | #105 | P3 | task | `#017`-exempt client latency wins | **Outcome:** zero-payload client latency fixes are not trapped behind the `#017` measurement gate. `#017` gates _payload_ decisions (#012/#013/#016 are all byte-count items); a `loading` fallback ships zero bytes and a resource hint ships ~60, so neither can be justified or refuted by a Lighthouse number. **Done 2026-07-28:** 10 of 11 `ssr:false` dashboard surfaces had NO `loading` fallback and rendered nothing between HTML arrival and chunk execution — all now use the shared `LoadingPanel` (`role="status"` + accessible label); Supabase `preconnect`/`dns-prefetch` added, since `AuthProvider` awaits a cross-origin `getUser()` on mount that every auth-gated fetch queues behind and there were no resource hints anywhere in `src/`. **Next:** verify with `verify:ui` once the heavy-run lock is free. Sidebar dialogs intentionally excluded (they mount on open). | `docs/audit/latency-audit-2026-07-28.md` L3-4/L3-5; `src/components/clinical-dashboard/clinical-dashboard-lazy.tsx` | 2026-07-28 | | #106 | P2 | rec | Ingestion worker and indexing agent are verified by grepping their own source | **Outcome:** the ingestion worker and indexing agent are verified by executing code, not by asserting on their own source text. **Detail:** measured 2026-07-29 via `npm run test:coverage` — `worker/main.ts` (2,015 lines) and `supabase/functions/indexing-v3-agent/index.ts` (1,966 lines) each report **0% executed lines**; no test imports either module. Both are covered only by `readFileSync` + `toContain` assertions in `worker-safe-logging.test.ts`, `worker-visual-capture.test.ts` and `document-metadata-merge.test.ts`, which pass whenever a string is present and break on harmless refactors; `document-metadata-merge.test.ts` additionally reimplements the SQL deep-merge in TypeScript and tests the reimplementation rather than the worker. Area totals: `worker/` 18.6% lines, `supabase/functions/` 4.5%. **Next:** continue the extraction pattern that already works here — `indexing-v3-agent/behavior.ts` (167 lines, 96%) and `ingestion-worker/auth.ts` (30 lines, 90%) — pulling the highest-risk decision points out of `worker/main.ts` (job claim/retry, generation commit, failure classification) into importable modules with executing tests, retiring the matching source-text assertion as each lands. Roughly cost-neutral: each extracted test replaces a grep assertion. **Stop:** do not try to make the 2,000-line entrypoint importable in one pass; extract incrementally and keep each step green. | session 2026-07-29 test-coverage analysis | 2026-07-29 | | #107 | P2 | rec | Component state matrices are the largest untested surface | **Outcome:** loading / empty / error / disabled states on interactive components are covered by executing tests, not only by E2E happy paths. **Detail:** measured 2026-07-29 — production components (excluding mockups) sit at **38.2% lines / 22.8% branch** across 12,602 lines, with **83 of 208 files at zero executed lines**; there are 51 `.dom.test.tsx` files against 195 components. Playwright does visit these routes, so they are smoke-covered, but branch coverage is where the state matrix lives and smoke journeys rarely reach it. Worst by uncovered lines: `global-search-shell.tsx` (7%), `mode-action-popup.tsx` (21%), `answer-content.tsx` (27%), `document-search-results.tsx` (32%), `universal-search-command-surface.tsx` (39%), `master-search-header.tsx` (43%). A concrete first target with clinical meaning: `calculator-ui.tsx` now covers all exported scoring logic, but `seedCheckboxDefaults`, `toggleCheckboxAnswer` and `selectOptionAnswer` stay uncovered because they are module-private and only reachable through React event handlers — `seedCheckboxDefaults` is what makes an all-negative CAGE / SAD PERSONS screen read as a valid 0 rather than incomplete, so a regression there is a false-negative risk. **Next:** treat as a per-PR convention rather than a backfill push — `docs/testing.md` already prescribes the state matrix, so the gap is enforcement. Start with `global-search-shell.tsx`, which `docs/search-chrome-behaviour.md` treats as a contract surface. Keep additions in the jsdom tier (measured ~0.54s per file) instead of new Playwright journeys (~231 production journeys already run serially at `workers: 1` against a 45-minute CI budget). **Stop:** do not chase the coverage percentage by backfilling low-risk components; the re-ratcheted broad floor in `vitest.config.mts` holds the line. | session 2026-07-29 test-coverage analysis | 2026-07-29 | +| #108 | P3 | task | Five verified-landed remote branches await deletion (blocked in-session) | **Outcome:** the five branches whose content is fully on `main` are gone. **Detail:** a full-history branch-cleanup review on 2026-07-29 verified these introduce an empty diff against `main` and back no open PR: `claude/clinical-kb-pwa-review-asi3wb`, `claude/dazzling-blackwell-f348d0`, `codex/document-reader-condensed-view`, `cursor/page-anchored-search-composer-30ee`, `cursor/pr-1379-babysit-ledger-9365`. Deletion could not be performed: the session git proxy rejects ref deletion with **HTTP 403**, and the GitHub MCP toolset exposes no delete-branch capability. The remaining 87 were deliberately NOT cleared — their touched files still differ from `main`, which is the conservative direction. **Next:** delete the five from the GitHub UI or an interactive session, then append a `branch-cleanup` scoped ledger row (the existing row is `branch-cleanup-deletion-pending`, which by design does not count as complete). **Stop:** do not widen to the other 87 without per-branch content proof. | session 2026-07-29 branch cleanup; ledger `branch-cleanup-deletion-pending` @ 855aa291 | 2026-07-29 | +| #109 | P2 | issue | Remote sessions clone shallow, silently invalidating all branch/merge analysis | **Outcome:** no session draws branch conclusions from a truncated history. **Detail:** on 2026-07-29 this repo's remote session had `git rev-parse --is-shallow-repository` = **true** with only **74** commits of `origin/main` (full history is 2829). Every merge-base, `--cherry-pick`, and ahead/behind number computed in that state was wrong: local `main` reported `ahead 52` and `refusing to merge unrelated histories` (it is actually 0 ahead with a shared base), and an all-branch sweep wrongly showed **90 of 91** branches as carrying unmerged work. Acting on that would have meant either deleting live branches or abandoning cleanup entirely. `git fetch --unshallow` corrected both. **Next:** make `is-shallow-repository` an explicit precondition check in `docs/branch-cleanup-guide.md` §Safety Rules and in `scripts/sweep-branch-ledger.mjs`, failing closed with the `--unshallow` remedy rather than silently reporting. **Stop:** never delete a branch, or report a branch as unmerged, from a shallow clone. | session 2026-07-29; `docs/branch-cleanup-guide.md`; `scripts/sweep-branch-ledger.mjs` | 2026-07-29 | ## Resolved / archive From 4cb33ed1480418fc7d62bd03270278e809d6c22a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 13:35:43 +0000 Subject: [PATCH 8/9] =?UTF-8?q?docs(ledger):=20address=20Codex=20review=20?= =?UTF-8?q?=E2=80=94=20per-candidate=20HEADs=20and=20provider=20classifica?= =?UTF-8?q?tion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three P2 findings from the Codex review of 873d28df, all documentation correctness on rows added in this PR. Record each deletion candidate's HEAD (finding 2, the substantive one). `hasCompletedCleanupReview` in scripts/sweep-branch-ledger.mjs matches a completed row on branch name AND HEAD together, and the aggregate row was keyed only to main@855aa291. Once the five refs are deleted their HEADs are unrecoverable, so no later operator could append the required `branch-cleanup` rows. Each candidate now has its own pending row keyed to its own HEAD, and #108 carries the same SHAs. Record the two-tree evidence explicitly (finding 1). The rows now state the merge-base and the `git diff --name-only ` result per branch rather than only the three-dot form. Two tips (df29f311, c9bec8f9) are themselves the merge-base with main, i.e. direct ancestors; the other three have tree(merge-base) == tree(tip). Correct the provider-backed classification (finding 3). The prior checks cell said "no provider-backed checks" while the outcome described GitHub open-PR cross-checks and an HTTP 403 deletion attempt. The new rows enumerate the GitHub reads and the rejected mutation, and reserve the "no provider-backed" wording for genuinely local work. The ledger is append-only, so these are superseding rows; the original aggregate row is left intact. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012YRCXgX4AWZ579bKN6sk6b --- docs/branch-review-ledger.md | 5 +++++ docs/outstanding-issues.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 5829f4f6fb..bfbe5714b0 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -1271,3 +1271,8 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie | 2026-07-29 | codex/document-reader-condensed-view | 5678e878d4fe681d33bb58df5b5b3468a138a1c8 | pr-1380-ci-green-resync | hosted CI green on 7150899a (Static/Build/Unit/Advisory/Production UI/PR required/CircleCI); CodeRabbit density fallback + summary keys + search/plain compact tests landed; unresolved review threads none; resynced main after tip went BEHIND by 1 | hosted CI success on 7150899a; merge-tree clean; bugbot no P0/P1 | | 2026-07-29 | claude/test-coverage-analysis-2vcd8a | f4a3f50048fa113d5e4e704defdafad2d37ba8d2 | prlanded | MERGED AND VERIFIED. PR #1383 squash-merged as f4a3f50. Content diff between the squash commit and branch tip bebc6c02 is EMPTY — nothing orphaned by the auto-merge race despite four concurrent pushes during the merge window. Landed on main: tests/calculator-scoring.test.ts (60 tests, 8 validated instruments), 9 batch signed-URL cases in tests/private-access-routes.test.ts, coverage floors raised 48/38/43/50 -> 62/53/55/64. Follow-ups renumbered #098/#099 -> #106/#107 during the main merge (main had claimed #098-#105); both rows verified present with content intact and next-id marker at 108. The three red 'PR required' results at 0922d7f/6f476b5/b72bc20 were concurrency cancellations (issue #095), not defects — all upstream jobs reported 'cancelled'; the clean run 30447852418 at tip bebc6c0 superseded them. | post-merge verification only: content diff squash-vs-tip EMPTY; origin/main content spot-checks PASS (test file present, floors 62/53/55/64, #106/#107 rows, 13 signed-urls refs); pre-merge local gates on the merged tree: test:coverage PASS exit 0 (424 files, 4371 passed \| 4 skipped, no threshold errors); no provider-backed checks | | 2026-07-29 | main | 855aa2914fd9cf29f9ce34f67e197d7a2d0c1a86 | branch-cleanup-deletion-pending | Full-history branch-cleanup review of all 92 remote branches. IMPORTANT PRECONDITION: the session clone was SHALLOW (74 commits of origin/main); every merge-base and cherry-pick result computed before 'git fetch --unshallow' was invalid, and an initial pass wrongly showed 90/91 branches as carrying unmerged work. After unshallowing (2829 commits) the analysis is sound. Cherry-pick matching alone finds only 2 candidates because squash merges collapse N commits into 1 so per-commit patch-ids never match; a content test (files touched vs merge-base, compared between branch tip and main) finds 5. VERIFIED SAFE TO DELETE — each introduces an empty diff against main and backs no open PR: claude/clinical-kb-pwa-review-asi3wb, claude/dazzling-blackwell-f348d0, codex/document-reader-condensed-view, cursor/page-anchored-search-composer-30ee, cursor/pr-1379-babysit-ledger-9365. DELETION BLOCKED: the session git proxy rejects ref deletion with HTTP 403 and the GitHub MCP toolset exposes no delete-branch capability, so the five remain and must be removed from the GitHub UI or an interactive session. The other 87 were NOT cleared: their touched files still differ from main, which is the conservative direction (a branch whose files main later modified reads as not-landed). Local cleanup done: stale local main fast-forwarded to origin/main (0 ahead, 0 patch-unique after unshallow — its earlier 'ahead 52 / unrelated histories' was purely the shallow-clone artifact); redundant local claude/prlanded-ledger-1383 deleted after confirming its row is in the pushed branch. | npm run sweep:branch-ledger (report-only, 2 candidates); full-history recompute after git fetch --unshallow; per-branch git diff origin/main... empty for all 5; open-PR head cross-check against PRs #1374/#1377/#1384/#1385/#1386/#1387; no branch deleted (HTTP 403); no provider-backed checks | +| 2026-07-29 | claude/clinical-kb-pwa-review-asi3wb | df29f311b60cadf8e43bf51283a9d6f496b295e3 | branch-cleanup-deletion-pending | DELETION PENDING — content proven fully on main. Branch tip IS the merge-base with main (df29f311), so the tip is a direct ancestor of main; tree comparison of merge-base vs tip shows 0 changed files. Backs no open PR. Deletion blocked by HTTP 403; HEAD recorded here so the later branch-cleanup row can be keyed to it after the ref is gone. Supersedes the aggregate main-keyed row at 855aa291, which recorded no candidate HEADs. | local git only for the content proof: git merge-base, git diff --name-only (0 files), git diff --name-only origin/main... (0 files), full history after git fetch --unshallow. PROVIDER-BACKED evidence obtained (GitHub reads, authorised repo-scope): open-PR head cross-check via GitHub API against PRs #1374/#1377/#1384/#1385/#1386/#1387, and GitHub MCP capability inspection. PROVIDER-BACKED MUTATION ATTEMPTED AND REJECTED: git push origin --delete returned HTTP 403; no branch was deleted. No OpenAI/Supabase/hosted-CI calls. | +| 2026-07-29 | claude/dazzling-blackwell-f348d0 | c9bec8f9dce38cb647de9aa64ebf08bf7823a524 | branch-cleanup-deletion-pending | DELETION PENDING — content proven fully on main. Branch tip IS the merge-base with main (c9bec8f9), so the tip is a direct ancestor of main; tree comparison of merge-base vs tip shows 0 changed files. Backs no open PR. Deletion blocked by HTTP 403; HEAD recorded here so the later branch-cleanup row can be keyed to it after the ref is gone. Supersedes the aggregate main-keyed row at 855aa291, which recorded no candidate HEADs. | local git only for the content proof: git merge-base, git diff --name-only (0 files), git diff --name-only origin/main... (0 files), full history after git fetch --unshallow. PROVIDER-BACKED evidence obtained (GitHub reads, authorised repo-scope): open-PR head cross-check via GitHub API against PRs #1374/#1377/#1384/#1385/#1386/#1387, and GitHub MCP capability inspection. PROVIDER-BACKED MUTATION ATTEMPTED AND REJECTED: git push origin --delete returned HTTP 403; no branch was deleted. No OpenAI/Supabase/hosted-CI calls. | +| 2026-07-29 | codex/document-reader-condensed-view | b5cdbf301d517239ffe9ed941b9ebe809aea0bfd | branch-cleanup-deletion-pending | DELETION PENDING — content proven fully on main. Merge-base with main is 855aa291 and tree(merge-base) equals tree(tip): git diff --name-only 855aa291 b5cdbf30 reports 0 files, so the tip introduces nothing beyond a state already in main. Its work landed as main's tip via squash. Backs no open PR. Deletion blocked by HTTP 403; HEAD recorded here so the later branch-cleanup row can be keyed to it after the ref is gone. Supersedes the aggregate main-keyed row at 855aa291, which recorded no candidate HEADs. | local git only for the content proof: git merge-base, git diff --name-only (0 files), git diff --name-only origin/main... (0 files), full history after git fetch --unshallow. PROVIDER-BACKED evidence obtained (GitHub reads, authorised repo-scope): open-PR head cross-check via GitHub API against PRs #1374/#1377/#1384/#1385/#1386/#1387, and GitHub MCP capability inspection. PROVIDER-BACKED MUTATION ATTEMPTED AND REJECTED: git push origin --delete returned HTTP 403; no branch was deleted. No OpenAI/Supabase/hosted-CI calls. | +| 2026-07-29 | cursor/page-anchored-search-composer-30ee | 7ff134ca7f614db527b8d142676640305533669d | branch-cleanup-deletion-pending | DELETION PENDING — content proven fully on main. Merge-base with main is 79d1c879 and tree(merge-base) equals tree(tip): git diff --name-only 79d1c879 7ff134ca reports 0 files, so the tip introduces nothing beyond a state already in main. Backs no open PR. Deletion blocked by HTTP 403; HEAD recorded here so the later branch-cleanup row can be keyed to it after the ref is gone. Supersedes the aggregate main-keyed row at 855aa291, which recorded no candidate HEADs. | local git only for the content proof: git merge-base, git diff --name-only (0 files), git diff --name-only origin/main... (0 files), full history after git fetch --unshallow. PROVIDER-BACKED evidence obtained (GitHub reads, authorised repo-scope): open-PR head cross-check via GitHub API against PRs #1374/#1377/#1384/#1385/#1386/#1387, and GitHub MCP capability inspection. PROVIDER-BACKED MUTATION ATTEMPTED AND REJECTED: git push origin --delete returned HTTP 403; no branch was deleted. No OpenAI/Supabase/hosted-CI calls. | +| 2026-07-29 | cursor/pr-1379-babysit-ledger-9365 | be2de03f855cb7fdfccea4bb74d05eb4c9bf6c61 | branch-cleanup-deletion-pending | DELETION PENDING — content proven fully on main. Merge-base with main is b2740480 and tree(merge-base) equals tree(tip): git diff --name-only b2740480 be2de03f reports 0 files, so the tip introduces nothing beyond a state already in main. Backs no open PR. Deletion blocked by HTTP 403; HEAD recorded here so the later branch-cleanup row can be keyed to it after the ref is gone. Supersedes the aggregate main-keyed row at 855aa291, which recorded no candidate HEADs. | local git only for the content proof: git merge-base, git diff --name-only (0 files), git diff --name-only origin/main... (0 files), full history after git fetch --unshallow. PROVIDER-BACKED evidence obtained (GitHub reads, authorised repo-scope): open-PR head cross-check via GitHub API against PRs #1374/#1377/#1384/#1385/#1386/#1387, and GitHub MCP capability inspection. PROVIDER-BACKED MUTATION ATTEMPTED AND REJECTED: git push origin --delete returned HTTP 403; no branch was deleted. No OpenAI/Supabase/hosted-CI calls. | diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 6c65d10ea6..fc5a46b27c 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -149,7 +149,7 @@ removed after current-main verification; it is not missing recommended work. | #105 | P3 | task | `#017`-exempt client latency wins | **Outcome:** zero-payload client latency fixes are not trapped behind the `#017` measurement gate. `#017` gates _payload_ decisions (#012/#013/#016 are all byte-count items); a `loading` fallback ships zero bytes and a resource hint ships ~60, so neither can be justified or refuted by a Lighthouse number. **Done 2026-07-28:** 10 of 11 `ssr:false` dashboard surfaces had NO `loading` fallback and rendered nothing between HTML arrival and chunk execution — all now use the shared `LoadingPanel` (`role="status"` + accessible label); Supabase `preconnect`/`dns-prefetch` added, since `AuthProvider` awaits a cross-origin `getUser()` on mount that every auth-gated fetch queues behind and there were no resource hints anywhere in `src/`. **Next:** verify with `verify:ui` once the heavy-run lock is free. Sidebar dialogs intentionally excluded (they mount on open). | `docs/audit/latency-audit-2026-07-28.md` L3-4/L3-5; `src/components/clinical-dashboard/clinical-dashboard-lazy.tsx` | 2026-07-28 | | #106 | P2 | rec | Ingestion worker and indexing agent are verified by grepping their own source | **Outcome:** the ingestion worker and indexing agent are verified by executing code, not by asserting on their own source text. **Detail:** measured 2026-07-29 via `npm run test:coverage` — `worker/main.ts` (2,015 lines) and `supabase/functions/indexing-v3-agent/index.ts` (1,966 lines) each report **0% executed lines**; no test imports either module. Both are covered only by `readFileSync` + `toContain` assertions in `worker-safe-logging.test.ts`, `worker-visual-capture.test.ts` and `document-metadata-merge.test.ts`, which pass whenever a string is present and break on harmless refactors; `document-metadata-merge.test.ts` additionally reimplements the SQL deep-merge in TypeScript and tests the reimplementation rather than the worker. Area totals: `worker/` 18.6% lines, `supabase/functions/` 4.5%. **Next:** continue the extraction pattern that already works here — `indexing-v3-agent/behavior.ts` (167 lines, 96%) and `ingestion-worker/auth.ts` (30 lines, 90%) — pulling the highest-risk decision points out of `worker/main.ts` (job claim/retry, generation commit, failure classification) into importable modules with executing tests, retiring the matching source-text assertion as each lands. Roughly cost-neutral: each extracted test replaces a grep assertion. **Stop:** do not try to make the 2,000-line entrypoint importable in one pass; extract incrementally and keep each step green. | session 2026-07-29 test-coverage analysis | 2026-07-29 | | #107 | P2 | rec | Component state matrices are the largest untested surface | **Outcome:** loading / empty / error / disabled states on interactive components are covered by executing tests, not only by E2E happy paths. **Detail:** measured 2026-07-29 — production components (excluding mockups) sit at **38.2% lines / 22.8% branch** across 12,602 lines, with **83 of 208 files at zero executed lines**; there are 51 `.dom.test.tsx` files against 195 components. Playwright does visit these routes, so they are smoke-covered, but branch coverage is where the state matrix lives and smoke journeys rarely reach it. Worst by uncovered lines: `global-search-shell.tsx` (7%), `mode-action-popup.tsx` (21%), `answer-content.tsx` (27%), `document-search-results.tsx` (32%), `universal-search-command-surface.tsx` (39%), `master-search-header.tsx` (43%). A concrete first target with clinical meaning: `calculator-ui.tsx` now covers all exported scoring logic, but `seedCheckboxDefaults`, `toggleCheckboxAnswer` and `selectOptionAnswer` stay uncovered because they are module-private and only reachable through React event handlers — `seedCheckboxDefaults` is what makes an all-negative CAGE / SAD PERSONS screen read as a valid 0 rather than incomplete, so a regression there is a false-negative risk. **Next:** treat as a per-PR convention rather than a backfill push — `docs/testing.md` already prescribes the state matrix, so the gap is enforcement. Start with `global-search-shell.tsx`, which `docs/search-chrome-behaviour.md` treats as a contract surface. Keep additions in the jsdom tier (measured ~0.54s per file) instead of new Playwright journeys (~231 production journeys already run serially at `workers: 1` against a 45-minute CI budget). **Stop:** do not chase the coverage percentage by backfilling low-risk components; the re-ratcheted broad floor in `vitest.config.mts` holds the line. | session 2026-07-29 test-coverage analysis | 2026-07-29 | -| #108 | P3 | task | Five verified-landed remote branches await deletion (blocked in-session) | **Outcome:** the five branches whose content is fully on `main` are gone. **Detail:** a full-history branch-cleanup review on 2026-07-29 verified these introduce an empty diff against `main` and back no open PR: `claude/clinical-kb-pwa-review-asi3wb`, `claude/dazzling-blackwell-f348d0`, `codex/document-reader-condensed-view`, `cursor/page-anchored-search-composer-30ee`, `cursor/pr-1379-babysit-ledger-9365`. Deletion could not be performed: the session git proxy rejects ref deletion with **HTTP 403**, and the GitHub MCP toolset exposes no delete-branch capability. The remaining 87 were deliberately NOT cleared — their touched files still differ from `main`, which is the conservative direction. **Next:** delete the five from the GitHub UI or an interactive session, then append a `branch-cleanup` scoped ledger row (the existing row is `branch-cleanup-deletion-pending`, which by design does not count as complete). **Stop:** do not widen to the other 87 without per-branch content proof. | session 2026-07-29 branch cleanup; ledger `branch-cleanup-deletion-pending` @ 855aa291 | 2026-07-29 | +| #108 | P3 | task | Five verified-landed remote branches await deletion (blocked in-session) | **Outcome:** the five branches whose content is fully on `main` are gone. **Detail:** a full-history branch-cleanup review on 2026-07-29 verified these introduce an empty diff against `main` and back no open PR: `claude/clinical-kb-pwa-review-asi3wb` @ `df29f311b60cadf8e43bf51283a9d6f496b295e3`, `claude/dazzling-blackwell-f348d0` @ `c9bec8f9dce38cb647de9aa64ebf08bf7823a524`, `codex/document-reader-condensed-view` @ `b5cdbf301d517239ffe9ed941b9ebe809aea0bfd`, `cursor/page-anchored-search-composer-30ee` @ `7ff134ca7f614db527b8d142676640305533669d`, `cursor/pr-1379-babysit-ledger-9365` @ `be2de03f855cb7fdfccea4bb74d05eb4c9bf6c61`. **The HEADs are recorded because they are unrecoverable once the refs are deleted:** `hasCompletedCleanupReview` (`scripts/sweep-branch-ledger.mjs:83-93`) matches a completed row on branch name AND HEAD together, so without them no later operator could ever append the required `branch-cleanup` rows. Each candidate now also has its own `branch-cleanup-deletion-pending` ledger row keyed to its own HEAD. Deletion could not be performed: the session git proxy rejects ref deletion with **HTTP 403**, and the GitHub MCP toolset exposes no delete-branch capability. The remaining 87 were deliberately NOT cleared — their touched files still differ from `main`, which is the conservative direction. **Next:** delete the five from the GitHub UI or an interactive session, then append one `branch-cleanup` scoped ledger row per branch reusing the HEAD recorded above (the existing row is `branch-cleanup-deletion-pending`, which by design does not count as complete). **Stop:** do not widen to the other 87 without per-branch content proof. | session 2026-07-29 branch cleanup; ledger `branch-cleanup-deletion-pending` @ 855aa291 | 2026-07-29 | | #109 | P2 | issue | Remote sessions clone shallow, silently invalidating all branch/merge analysis | **Outcome:** no session draws branch conclusions from a truncated history. **Detail:** on 2026-07-29 this repo's remote session had `git rev-parse --is-shallow-repository` = **true** with only **74** commits of `origin/main` (full history is 2829). Every merge-base, `--cherry-pick`, and ahead/behind number computed in that state was wrong: local `main` reported `ahead 52` and `refusing to merge unrelated histories` (it is actually 0 ahead with a shared base), and an all-branch sweep wrongly showed **90 of 91** branches as carrying unmerged work. Acting on that would have meant either deleting live branches or abandoning cleanup entirely. `git fetch --unshallow` corrected both. **Next:** make `is-shallow-repository` an explicit precondition check in `docs/branch-cleanup-guide.md` §Safety Rules and in `scripts/sweep-branch-ledger.mjs`, failing closed with the `--unshallow` remedy rather than silently reporting. **Stop:** never delete a branch, or report a branch as unmerged, from a shallow clone. | session 2026-07-29; `docs/branch-cleanup-guide.md`; `scripts/sweep-branch-ledger.mjs` | 2026-07-29 | ## Resolved / archive From 683cd083fe5babdaf2a15c5fd6ab710762974a60 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 29 Jul 2026 13:40:31 +0000 Subject: [PATCH 9/9] =?UTF-8?q?docs(issues):=20correct=20#108=20ordering?= =?UTF-8?q?=20=E2=80=94=20append=20cleanup=20rows=20before=20deleting=20re?= =?UTF-8?q?fs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex review of 4cb33ed1 found the documented order unexecutable, and it is right. #108 said to delete the five branches and then append the completed `branch-cleanup` rows, but `resolveHead` (scripts/branch-review-ledger.mjs:155-167) runs `git rev-parse --verify ^{commit}` and refuses a HEAD that is not a commit in the repository. Once the refs are gone and their objects are pruned, the append can never succeed, so recording the SHAs in the previous commit was necessary but not sufficient. Order is now append-then-delete, and the note explains why the `n/a - ` escape hatch is not an acceptable substitute: `hasCompletedCleanupReview` matches only a 7-40 char hex HEAD, so an `n/a` row would leave each branch resurfacing in every future sweep. Verified locally: all five recorded SHAs resolve in this checkout, and an absent SHA produces `refusing to append: head "..." is not a commit in this repository`. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012YRCXgX4AWZ579bKN6sk6b --- docs/outstanding-issues.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index fc5a46b27c..d07013388e 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -149,7 +149,7 @@ removed after current-main verification; it is not missing recommended work. | #105 | P3 | task | `#017`-exempt client latency wins | **Outcome:** zero-payload client latency fixes are not trapped behind the `#017` measurement gate. `#017` gates _payload_ decisions (#012/#013/#016 are all byte-count items); a `loading` fallback ships zero bytes and a resource hint ships ~60, so neither can be justified or refuted by a Lighthouse number. **Done 2026-07-28:** 10 of 11 `ssr:false` dashboard surfaces had NO `loading` fallback and rendered nothing between HTML arrival and chunk execution — all now use the shared `LoadingPanel` (`role="status"` + accessible label); Supabase `preconnect`/`dns-prefetch` added, since `AuthProvider` awaits a cross-origin `getUser()` on mount that every auth-gated fetch queues behind and there were no resource hints anywhere in `src/`. **Next:** verify with `verify:ui` once the heavy-run lock is free. Sidebar dialogs intentionally excluded (they mount on open). | `docs/audit/latency-audit-2026-07-28.md` L3-4/L3-5; `src/components/clinical-dashboard/clinical-dashboard-lazy.tsx` | 2026-07-28 | | #106 | P2 | rec | Ingestion worker and indexing agent are verified by grepping their own source | **Outcome:** the ingestion worker and indexing agent are verified by executing code, not by asserting on their own source text. **Detail:** measured 2026-07-29 via `npm run test:coverage` — `worker/main.ts` (2,015 lines) and `supabase/functions/indexing-v3-agent/index.ts` (1,966 lines) each report **0% executed lines**; no test imports either module. Both are covered only by `readFileSync` + `toContain` assertions in `worker-safe-logging.test.ts`, `worker-visual-capture.test.ts` and `document-metadata-merge.test.ts`, which pass whenever a string is present and break on harmless refactors; `document-metadata-merge.test.ts` additionally reimplements the SQL deep-merge in TypeScript and tests the reimplementation rather than the worker. Area totals: `worker/` 18.6% lines, `supabase/functions/` 4.5%. **Next:** continue the extraction pattern that already works here — `indexing-v3-agent/behavior.ts` (167 lines, 96%) and `ingestion-worker/auth.ts` (30 lines, 90%) — pulling the highest-risk decision points out of `worker/main.ts` (job claim/retry, generation commit, failure classification) into importable modules with executing tests, retiring the matching source-text assertion as each lands. Roughly cost-neutral: each extracted test replaces a grep assertion. **Stop:** do not try to make the 2,000-line entrypoint importable in one pass; extract incrementally and keep each step green. | session 2026-07-29 test-coverage analysis | 2026-07-29 | | #107 | P2 | rec | Component state matrices are the largest untested surface | **Outcome:** loading / empty / error / disabled states on interactive components are covered by executing tests, not only by E2E happy paths. **Detail:** measured 2026-07-29 — production components (excluding mockups) sit at **38.2% lines / 22.8% branch** across 12,602 lines, with **83 of 208 files at zero executed lines**; there are 51 `.dom.test.tsx` files against 195 components. Playwright does visit these routes, so they are smoke-covered, but branch coverage is where the state matrix lives and smoke journeys rarely reach it. Worst by uncovered lines: `global-search-shell.tsx` (7%), `mode-action-popup.tsx` (21%), `answer-content.tsx` (27%), `document-search-results.tsx` (32%), `universal-search-command-surface.tsx` (39%), `master-search-header.tsx` (43%). A concrete first target with clinical meaning: `calculator-ui.tsx` now covers all exported scoring logic, but `seedCheckboxDefaults`, `toggleCheckboxAnswer` and `selectOptionAnswer` stay uncovered because they are module-private and only reachable through React event handlers — `seedCheckboxDefaults` is what makes an all-negative CAGE / SAD PERSONS screen read as a valid 0 rather than incomplete, so a regression there is a false-negative risk. **Next:** treat as a per-PR convention rather than a backfill push — `docs/testing.md` already prescribes the state matrix, so the gap is enforcement. Start with `global-search-shell.tsx`, which `docs/search-chrome-behaviour.md` treats as a contract surface. Keep additions in the jsdom tier (measured ~0.54s per file) instead of new Playwright journeys (~231 production journeys already run serially at `workers: 1` against a 45-minute CI budget). **Stop:** do not chase the coverage percentage by backfilling low-risk components; the re-ratcheted broad floor in `vitest.config.mts` holds the line. | session 2026-07-29 test-coverage analysis | 2026-07-29 | -| #108 | P3 | task | Five verified-landed remote branches await deletion (blocked in-session) | **Outcome:** the five branches whose content is fully on `main` are gone. **Detail:** a full-history branch-cleanup review on 2026-07-29 verified these introduce an empty diff against `main` and back no open PR: `claude/clinical-kb-pwa-review-asi3wb` @ `df29f311b60cadf8e43bf51283a9d6f496b295e3`, `claude/dazzling-blackwell-f348d0` @ `c9bec8f9dce38cb647de9aa64ebf08bf7823a524`, `codex/document-reader-condensed-view` @ `b5cdbf301d517239ffe9ed941b9ebe809aea0bfd`, `cursor/page-anchored-search-composer-30ee` @ `7ff134ca7f614db527b8d142676640305533669d`, `cursor/pr-1379-babysit-ledger-9365` @ `be2de03f855cb7fdfccea4bb74d05eb4c9bf6c61`. **The HEADs are recorded because they are unrecoverable once the refs are deleted:** `hasCompletedCleanupReview` (`scripts/sweep-branch-ledger.mjs:83-93`) matches a completed row on branch name AND HEAD together, so without them no later operator could ever append the required `branch-cleanup` rows. Each candidate now also has its own `branch-cleanup-deletion-pending` ledger row keyed to its own HEAD. Deletion could not be performed: the session git proxy rejects ref deletion with **HTTP 403**, and the GitHub MCP toolset exposes no delete-branch capability. The remaining 87 were deliberately NOT cleared — their touched files still differ from `main`, which is the conservative direction. **Next:** delete the five from the GitHub UI or an interactive session, then append one `branch-cleanup` scoped ledger row per branch reusing the HEAD recorded above (the existing row is `branch-cleanup-deletion-pending`, which by design does not count as complete). **Stop:** do not widen to the other 87 without per-branch content proof. | session 2026-07-29 branch cleanup; ledger `branch-cleanup-deletion-pending` @ 855aa291 | 2026-07-29 | +| #108 | P3 | task | Five verified-landed remote branches await deletion (blocked in-session) | **Outcome:** the five branches whose content is fully on `main` are gone. **Detail:** a full-history branch-cleanup review on 2026-07-29 verified these introduce an empty diff against `main` and back no open PR: `claude/clinical-kb-pwa-review-asi3wb` @ `df29f311b60cadf8e43bf51283a9d6f496b295e3`, `claude/dazzling-blackwell-f348d0` @ `c9bec8f9dce38cb647de9aa64ebf08bf7823a524`, `codex/document-reader-condensed-view` @ `b5cdbf301d517239ffe9ed941b9ebe809aea0bfd`, `cursor/page-anchored-search-composer-30ee` @ `7ff134ca7f614db527b8d142676640305533669d`, `cursor/pr-1379-babysit-ledger-9365` @ `be2de03f855cb7fdfccea4bb74d05eb4c9bf6c61`. **The HEADs are recorded because they are unrecoverable once the refs are deleted:** `hasCompletedCleanupReview` (`scripts/sweep-branch-ledger.mjs:83-93`) matches a completed row on branch name AND HEAD together, so without them no later operator could ever append the required `branch-cleanup` rows. Each candidate now also has its own `branch-cleanup-deletion-pending` ledger row keyed to its own HEAD. Deletion could not be performed: the session git proxy rejects ref deletion with **HTTP 403**, and the GitHub MCP toolset exposes no delete-branch capability. The remaining 87 were deliberately NOT cleared — their touched files still differ from `main`, which is the conservative direction. **Next — ORDER MATTERS:** append the completed `branch-cleanup` row for each branch FIRST, from a checkout that still has the objects, and only then delete the refs. `resolveHead` (`scripts/branch-review-ledger.mjs:155-167`) runs `git rev-parse --verify ^{commit}` and refuses to append a HEAD that is not a commit in the repository, so the reverse order is unexecutable once the refs are gone and their objects are pruned. The `n/a - ` escape hatch does not help here: `hasCompletedCleanupReview` only matches a 7-40 char hex HEAD, so an `n/a` row would leave the branch resurfacing in every future sweep. Delete the five from the GitHub UI or an interactive session once their rows are recorded (the existing row is `branch-cleanup-deletion-pending`, which by design does not count as complete). **Stop:** do not widen to the other 87 without per-branch content proof. | session 2026-07-29 branch cleanup; ledger `branch-cleanup-deletion-pending` @ 855aa291 | 2026-07-29 | | #109 | P2 | issue | Remote sessions clone shallow, silently invalidating all branch/merge analysis | **Outcome:** no session draws branch conclusions from a truncated history. **Detail:** on 2026-07-29 this repo's remote session had `git rev-parse --is-shallow-repository` = **true** with only **74** commits of `origin/main` (full history is 2829). Every merge-base, `--cherry-pick`, and ahead/behind number computed in that state was wrong: local `main` reported `ahead 52` and `refusing to merge unrelated histories` (it is actually 0 ahead with a shared base), and an all-branch sweep wrongly showed **90 of 91** branches as carrying unmerged work. Acting on that would have meant either deleting live branches or abandoning cleanup entirely. `git fetch --unshallow` corrected both. **Next:** make `is-shallow-repository` an explicit precondition check in `docs/branch-cleanup-guide.md` §Safety Rules and in `scripts/sweep-branch-ledger.mjs`, failing closed with the `--unshallow` remedy rather than silently reporting. **Stop:** never delete a branch, or report a branch as unmerged, from a shallow clone. | session 2026-07-29; `docs/branch-cleanup-guide.md`; `scripts/sweep-branch-ledger.mjs` | 2026-07-29 | ## Resolved / archive