diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 624df654eb..268f6e8aa7 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -1067,6 +1067,7 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-26 | PR #1241 / `cursor/imp04-prune-dead-exports-01f2` | merge `origin/main` `6fd8902b` | Main sync after GitHub CONFLICTING | SIMPLE. Staleness behind #1256/#1246/#1252/#1251. `git merge-tree` clean; only overlapping path was append-only ledger. Dropped 2 exact-duplicate #1238 rows re-appended by union driver (kept earlier copies). No product conflict markers; IMP-04 prune surface unchanged. | merge-tree clean; merge ort; `check:branch-review-ledger`; no provider calls. | | 2026-07-26 | `codex/test-concurrency-20260726` | `1b1f4817b0cf932d8b43f8715725770045528f8c` | Protected-main release-readiness review of cross-worktree test concurrency | APPROVE. Shared admission is fail-closed to explicit focused Vitest selections and isolated typechecks; full suites, lint, builds, coverage and Playwright stay exclusive with queue priority and legacy-lock compatibility. Review found and fixed one blocker before approval: junctioned worktrees would have raced the shared `node_modules/.cache` TypeScript build-info file, so shared typechecks now receive a worktree-hashed temporary `.tsbuildinfo` path. Highest residual risk is Windows cross-process filesystem timing, covered by coordinator race/recovery tests and the full local gate. | `npm run verify:pr-local` PASS: format, lint, isolated typecheck, 391 files / 3489 tests passed / 2 skipped, production build (1680 static pages), client-secret scan, and 36-case offline RAG fixtures. Focused coordinator/tooling 32/32; PDF portability 3 passed / 2 platform-or-dependency skips. No provider-backed checks. | +| 2026-07-26 | PR #1254 / `apply-audit-remediation-fixes` | `b3b1eb7e7084859cd18c05152be1b9f8968592ff` | Authorized babysit sweep | Fixed P1 locality-audit-out-of-pr-local + comparator-direction conflicts; typed locality accumulator; hardened citationTelemetry schema; clozapine mg-gated span. Merged `origin/main`. 10/10 threads replied+resolved (2 deferred). | Focused Vitest evidence + verify-pr-local 24/24 PASS. No provider-backed checks. | | 2026-07-26 | PR #1248 / `cursor/fix-mode-switch-lag-22f6` | `af4908bb9bdbf7a30fc1f8ed031ef9bd75f292ef` | Authorized babysit sweep | Fixed P1 documents-search ownership + P2 reserve-reveal transition; forms readiness null-slug + private-scope hash; merged remote Suspense standalone paths. 6/6 threads replied+resolved (1 deferred boundary scan). | Focused Vitest search-route-ownership + clinical-dashboard-merge-artifacts PASS before final push; hosted CI re-running. No provider-backed checks. | | 2026-07-26 | PR #1254 / `apply-audit-remediation-fixes` | `b3b1eb7e7084859cd18c05152be1b9f8968592ff` | Authorized babysit sweep | Fixed P1 locality-audit-out-of-pr-local + comparator-direction conflicts; typed locality accumulator; hardened citationTelemetry schema; clozapine mg-gated span. Merged `origin/main` (verify-pr-local conflict resolved to main shape). 10/10 threads replied+resolved (2 deferred: unit normalize, query-context wiring). | Focused Vitest evidence + verify-pr-local 24/24 PASS. No provider-backed checks. | | 2026-07-26 | PR #1257 / `cursor/therapy-search-trim-e63e` | `b80a3810819846760e862e1d0d4aa746ae6b0237` (merged) | Authorized babysit sweep | Already MERGED to main before code changes needed; tip had correct sidebar absence assertion; 0 unresolved threads at close. | Hosted PR required + Production UI SUCCESS on merged tip. No provider-backed checks. | @@ -1077,3 +1078,7 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-26 | PR #1241 / `cursor/imp04-prune-dead-exports-01f2` | `5de2f4cdfa707ed53145b2e39a7f283995887f85` | Authorized babysit sweep | Threads: 1 CodeRabbit ledger rewrite request dispositioned (append-only policy; hosted CI already green). Merged `origin/main` (mechanical). 0 unresolved left. | Hosted required CI previously SUCCESS on prior tip; no provider-backed checks. | | 2026-07-26 | PR #1248 / `cursor/fix-mode-switch-lag-22f6` | pending final pushed head after ledger append | PR babysit: sync main + Codex P2 submitted-param seed | Before: GitHub reported DIRTY/CONFLICTING while `git merge-tree --write-tree origin/main 7250d6d38269b734d903f9995782a4eedeaeebcb` was clean; branch was 1 behind main with 1 unresolved Codex P2. Merged `origin/main` cleanly, dropped one exact-duplicate PR #1241 ledger row reintroduced by the union driver, and fixed the P2 by deriving standalone shell chrome from `window.location.search` via `useSyncExternalStore` before the delayed `useSearchParams` bridge hydrates. Hosted CI, thread reply/resolution, and squash merge to main remain the final babysit gates. | `npm run test -- --run tests/search-route-ownership.test.ts` PASS (12/12); `npm run lint` PASS; `npm run check:branch-review-ledger` PASS; `npm run verify:cheap` PASS (393 files; 3505 passed / 5 skipped); no provider-backed checks. | | 2026-07-26 | PR #1248 / `cursor/fix-mode-switch-lag-22f6` | pending final pushed head after UI CI fix | PR babysit: Production UI failure fix | Supersedes the prior PR #1248 babysit row for hosted CI closeout. Production UI failed only on two branch-adjacent chrome tests after the P2 fix: restricted the browser query fallback to submitted searches (`run=1` + `q/query`) so route-owned query pages do not adopt submitted chrome, and aligned the phone-scroll reserve-transition assertion with the `data-reserve-transitioning` marker for no-dock/expired-marker states. | Hosted log diagnosis from run `30187649755`; local exact Chromium production rerun `npm run test:e2e:pr -- tests/ui-phone-scroll.spec.ts tests/ui-tools.spec.ts --grep "formulation/builder\\?mechanism=rumination|differentials presentation comparison"` PASS (2/2); no provider-backed checks. | +| 2026-07-26 | PR #1254 / `apply-audit-remediation-fixes` | code fix `ee6a56bd0fc08ded140c36b5de213bda40d93179` | PR babysit: main sync + P1 inclusivity/static/build fix | Supersedes the earlier #1254 sweep rows for the new head after main advanced to `dbcd4cea605ee7f5af8f2f9b5ff22d18385131ea`. Before: GitHub reported DIRTY/CONFLICTING, PR policy failed missing RAG/clinical body, Static failed Prettier, Build/Production UI failed typecheck, and Codex P1 thread `3651695530` was unresolved. `git merge-tree --write-tree origin/main 91aeb19d4d4856a3aa120265d3602414b472e19a` was clean, so merged `origin/main` without conflicts; fixed threshold inclusivity (`<`/`<=`, `>`/`>=` stay distinct), source-open telemetry types, missing `onOpenSource`, missing `SourceGovernanceWarning` export, and lint/Prettier issues. PR body/thread resolution/squash merge still require GitHub write tooling unavailable in this Cursor run, so DO NOT MERGE until those are completed and hosted required CI is green. | Focused Vitest evidence/source metadata 67/67 PASS; `npm run typecheck` PASS; `npm run format:check` PASS; `npm run lint` PASS; `npm run build` PASS; `npm run check:rag:fixtures` PASS; `npm run check:production-readiness` failed only for missing local Supabase/OpenAI env secrets. No provider-backed evals/checks run. | +| 2026-07-26 | PR #1254 / `apply-audit-remediation-fixes` | pending pushed head after CodeRabbit follow-up | PR babysit: evidence false-positive hardening | Follow-up to the prior #1254 row after CodeRabbit re-opened evidence threads on the pushed head. Fixed scoped source-governance issues: bare `clozapine` now only binds its own dose comparator when the captured threshold is mg-qualified; table/prose same-value unknown comparator is compatible with known comparator; cross-source conflicts require document-level disagreement rather than one internally inconsistent document plus another source repeating one side. Production UI hosted failure was a single `/tools` strict-locator browser flake; exact local production rerun passed. PR body metadata and reply/resolve remain blocked by missing GitHub write tooling in this run. | `npm run test -- tests/evidence.test.ts` PASS (26/26); `npm run typecheck` PASS; `npm run lint` PASS; `npm run build` PASS; `npm run check:rag:fixtures` PASS; exact local `npm run test:e2e:pr -- tests/ui-tools.spec.ts --grep "mode home search is centered at desktop width on /tools"` PASS (1/1). No provider-backed checks run. | +| 2026-07-26 | PR #1254 / `apply-audit-remediation-fixes` | pending pushed head after Production UI locator hardening | PR babysit: Production UI strict-locator rerun fix | Hosted Production UI failed twice on different `tests/ui-tools.spec.ts` strict locators because duplicated page containers under `mobile-composer-reserve-pad` made `getByTestId(...)` ambiguous in full-suite browser state. Product code unchanged; tests now scope to visible/current page containers and the metrics helper measures a visible home container. PR body metadata and review-thread reply/resolve still require GitHub write tooling unavailable in this run. | Exact local production rerun `npm run test:e2e:pr -- tests/ui-tools.spec.ts --grep "mode home search is centered at desktop width on /tools|13YARN service detail is usable at mobile"` PASS (2/2). No provider-backed checks run. | +| 2026-07-26 | PR #1254 / `apply-audit-remediation-fixes` | `5b616da1f84ffde127473e327e3ab63369244749` | PR babysit: ledger duplicate clarification | Clarifies the CodeRabbit duplicate-ledger thread without rewriting append-only history: the later `b3b1eb7e7084859cd18c05152be1b9f8968592ff` row at prior line 1072 is a superseding clarification of the earlier same-commit #1254 row, not a second independent sweep. PR body metadata and review-thread reply/resolve still require GitHub write tooling unavailable in this run, so DO NOT MERGE until those are completed and hosted required CI is green. | `npm run check:branch-review-ledger` required after this append; no provider-backed checks run. | diff --git a/package.json b/package.json index 9543690e0b..6367c667da 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "check:migration-role": "node scripts/check-hosted-migration-role.mjs", "check:function-grants": "node scripts/check-function-grants.mjs", "check:owner-scope": "node scripts/check-owner-scope-api.mjs", + "check:locality-metadata": "node scripts/run-tsx.mjs scripts/verify-locality-metadata.ts", "recover:ingestion": "node scripts/run-tsx.mjs scripts/recover-ingestion-queue.ts", "registry:seed": "node scripts/run-tsx.mjs scripts/seed-registry-records.ts", "registry:embed": "node scripts/run-tsx.mjs scripts/embed-registry-records.ts", diff --git a/scripts/verify-locality-metadata.ts b/scripts/verify-locality-metadata.ts new file mode 100644 index 0000000000..beb114a044 --- /dev/null +++ b/scripts/verify-locality-metadata.ts @@ -0,0 +1,80 @@ +import * as nextEnv from "@next/env"; +import { auditSourceAuthorityDocuments, type SourceAuthorityDocument } from "@/lib/source-authority-metadata"; + +const loadEnvConfig = + nextEnv.loadEnvConfig ?? + (nextEnv as unknown as { default?: { loadEnvConfig?: typeof nextEnv.loadEnvConfig } }).default?.loadEnvConfig; + +async function loadAdminClient() { + const { createAdminClient } = await import("@/lib/supabase/admin"); + return createAdminClient(); +} + +function asSourceAuthorityDocument(row: { + id?: string | null; + title?: string | null; + file_name?: string | null; + source_path?: string | null; + metadata?: unknown; +}): SourceAuthorityDocument { + return { + id: row.id ?? undefined, + title: row.title ?? "", + file_name: row.file_name ?? "", + source_path: row.source_path ?? null, + metadata: + row.metadata && typeof row.metadata === "object" && !Array.isArray(row.metadata) + ? (row.metadata as Record) + : null, + }; +} + +export async function main() { + if (loadEnvConfig) { + loadEnvConfig(process.cwd()); + } + + const supabase = await loadAdminClient(); + const documents: SourceAuthorityDocument[] = []; + const pageSize = 1000; + + for (let from = 0; ; from += pageSize) { + const { data, error } = await supabase + .from("documents") + .select("id,title,file_name,source_path,status,metadata") + .eq("status", "indexed") + .order("id", { ascending: true }) + .range(from, from + pageSize - 1); + + if (error) throw new Error(error.message); + documents.push(...(data ?? []).map(asSourceAuthorityDocument)); + if (!data || data.length < pageSize) break; + } + + const report = auditSourceAuthorityDocuments(documents); + + if (!report.passed) { + console.error("FAIL: Source authority metadata verification failed."); + if (report.missing_australian_locality_count > 0) { + console.error(`- Missing Australian locality metadata: ${report.missing_australian_locality_count} documents`); + } + if (report.authority_conflict_count > 0) { + console.error(`- Authority conflicts: ${report.authority_conflict_count} documents`); + } + process.exitCode = 1; + } else { + console.log("PASS: Source authority metadata verification passed."); + } +} + +if (process.argv[1] && import.meta.url === `file://${process.argv[1]}`.replace(/\\/g, "/")) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + }); +} else if (process.argv[1] && process.argv[1].endsWith("verify-locality-metadata.ts")) { + main().catch((error) => { + console.error(error instanceof Error ? error.message : error); + process.exitCode = 1; + }); +} diff --git a/scripts/verify-pr-local.mjs b/scripts/verify-pr-local.mjs index fd811bfcf0..883cc6e24b 100644 --- a/scripts/verify-pr-local.mjs +++ b/scripts/verify-pr-local.mjs @@ -3,6 +3,7 @@ import { spawnSync } from "node:child_process"; import { childProcessExitCode } from "./child-process-result.mjs"; const isWindows = process.platform === "win32"; +// Live Supabase audits (check:locality-metadata) stay out of this unconditional gate. const baseScripts = ["check:runtime", "format:changed", "lint", "typecheck", "test"]; function parseArgs(args) { diff --git a/src/app/api/search/interaction/route.ts b/src/app/api/search/interaction/route.ts index 9e6d7bc28d..5dc277b2d2 100644 --- a/src/app/api/search/interaction/route.ts +++ b/src/app/api/search/interaction/route.ts @@ -30,6 +30,24 @@ const interactionSchema = z title: z.string().trim().max(240).optional(), queryClass: z.string().trim().max(80).optional(), crossMode: crossModeTargetSchema.optional(), + citationTelemetry: z + .object({ + provenance: z + .enum([ + "model_selected", + "section_selected", + "exact_quote", + "deterministic_support", + "review_only", + "retrieval_only", + ]) + .optional(), + source_strength: z.enum(["strong", "moderate", "limited"]).optional(), + similarity: z.number().min(0).max(1).optional(), + document_status: z.string().trim().max(80).optional(), + }) + .strict() + .optional(), }) .refine((body) => Boolean(body.documentId || body.crossMode), { message: "Either documentId or a crossMode target is required.", @@ -154,6 +172,7 @@ export async function POST(request: Request) { metadata: { interaction: "source_open", ...queryPrivacyMetadata(body.query), + ...(body.citationTelemetry && { citation_telemetry: body.citationTelemetry }), }, }); if (insertError) throw new Error(insertError.message); diff --git a/src/components/clinical-dashboard/answer-content.tsx b/src/components/clinical-dashboard/answer-content.tsx index 93247322a1..e79bc6511d 100644 --- a/src/components/clinical-dashboard/answer-content.tsx +++ b/src/components/clinical-dashboard/answer-content.tsx @@ -20,7 +20,7 @@ import { subtleStatusPill, textMuted, } from "@/components/ui-primitives"; -import { sourceResultHref } from "@/components/clinical-dashboard/source-actions"; +import { sourceResultHref, logSourceOpen } from "@/components/clinical-dashboard/source-actions"; import { cleanDisplayTitle, comparableAnswerText, @@ -280,9 +280,12 @@ export function sourceStatusDotClass(metadata: ReturnType; + sourceMetadata?: SearchResult["source_metadata"]; score: number; href: string; snippet?: string; @@ -342,9 +345,12 @@ function capsulePreviewSources( sourceLinks.slice(0, 5).forEach((source) => { pushRow({ id: source.chunk_id, + documentId: source.document_id, title: source.title || source.file_name || "Source", + fileName: source.file_name, pageNumber: source.page_number, metadata: normalizeSourceMetadata(source.sourceMetadata), + sourceMetadata: source.sourceMetadata, score: source.score ?? 0, href: source.href, snippet: source.snippet, @@ -355,9 +361,12 @@ function capsulePreviewSources( if (bestSource) { pushRow({ id: bestSource.chunk_id, + documentId: bestSource.document_id, title: bestSource.title || bestSource.file_name || "Source", + fileName: bestSource.file_name, pageNumber: bestSource.page_number, metadata: normalizeSourceMetadata(bestSource.source_metadata), + sourceMetadata: bestSource.source_metadata, score: bestSource.score, href: bestSource.viewer_href, sourceStrength: bestSource.source_strength, @@ -367,9 +376,12 @@ function capsulePreviewSources( sources.slice(0, 5).forEach((source) => { pushRow({ id: source.id, + documentId: source.document_id, title: source.title || source.file_name || "Source", + fileName: source.file_name, pageNumber: source.page_number, metadata: normalizeSourceMetadata(source.source_metadata), + sourceMetadata: source.source_metadata, score: source.hybrid_score ?? source.similarity ?? source.lexical_score ?? 0, href: sourceResultHref(source), sourceStrength: source.source_strength, @@ -380,12 +392,14 @@ function capsulePreviewSources( } function SourcePreviewContent({ + query, previewSources, quoteText, copiedQuote, onCopyQuote, showHeader = true, }: { + query?: string; previewSources: CapsulePreviewSource[]; quoteText?: string | null; copiedQuote: boolean; @@ -447,6 +461,7 @@ function SourcePreviewContent({ query && logSourceOpen(query, source)} data-testid="source-capsule-preview-row" className="flex min-h-12 items-center rounded-md text-sm font-semibold leading-5 text-[color:var(--text-heading)] transition hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" aria-label={`Open source ${cleanDisplayTitle(source.title)}, page ${source.pageNumber ?? "not available"}`} @@ -471,6 +486,7 @@ function SourcePreviewContent({ query && logSourceOpen(query, source)} className={cn( index === 0 ? "inline-flex min-h-12 items-center gap-1.5 rounded-md border border-[color:var(--border)] bg-[color:var(--surface-raised)] px-2.5 text-xs font-semibold text-[color:var(--text)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--clinical-accent-border)]" @@ -495,6 +511,7 @@ function SourcePreviewContent({ {primaryPreviewSource ? ( query && logSourceOpen(query, primaryPreviewSource)} className={chatMicroAction} aria-label={`Open source page for ${primaryPreviewSource.title}`} > @@ -528,6 +545,7 @@ function SourcePreviewContent({ {primaryPreviewSource ? ( query && logSourceOpen(query, primaryPreviewSource)} className="inline-flex min-h-8 items-center gap-1.5 rounded-md px-2 text-[color:var(--clinical-accent)] transition hover:bg-[color:var(--clinical-accent-soft)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]" > Evidence details @@ -543,6 +561,7 @@ function SourcePreviewContent({ * Displays a sanitized clinical answer with source status, source previews, and copy actions. * * @param text - The raw answer text to display. + * @param query - The user's query context for logging. * @param preformatted - Whether to preserve the supplied formatting during display processing. * @param sourceCount - The number of direct sources associated with the answer. * @param sourceOnly - Whether to show a notice that the answer was assembled solely from source passages. @@ -555,6 +574,7 @@ function SourcePreviewContent({ */ export function NaturalLanguageAnswer({ text, + query, preformatted = false, sourceCount, sourceOnly, @@ -567,6 +587,7 @@ export function NaturalLanguageAnswer({ // Raw answer text (server bold intact); this component owns display // sanitization so can render the high-yield emphasis. text: string; + query?: string; preformatted?: boolean; sourceCount: number; sourceOnly: boolean; @@ -702,6 +723,7 @@ export function NaturalLanguageAnswer({ anchorRef={sourceCapsuleRef} >
query && logCitationOpen(query, finding.citation)} className="inline-flex min-h-tap min-w-0 items-center gap-1 text-xs font-semibold text-[color:var(--primary)] transition hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] lg:min-h-8" aria-label={`Open source ${formatSafetyFindingLabel(finding)}`} > @@ -1315,12 +1316,14 @@ export function QuoteCards({ onCopyQuotes, onFollowUp, onScopeDocument, + query, }: { quotes: QuoteCard[]; copiedQuotes: boolean; onCopyQuotes: () => void; onFollowUp?: (quote: QuoteCard) => void; onScopeDocument: (documentId: string) => void; + query?: string; }) { return (
@@ -1375,6 +1378,7 @@ export function QuoteCards({ documentId={quote.document_id} onScopeDocument={onScopeDocument} onFollowUp={onFollowUp ? () => onFollowUp(quote) : undefined} + onOpenSource={() => query && logCitationOpen(query, quote, quote.source_strength)} divider={false} />
diff --git a/src/components/clinical-dashboard/prior-answer-turn-surface.tsx b/src/components/clinical-dashboard/prior-answer-turn-surface.tsx index dbd330be06..f2f2c45639 100644 --- a/src/components/clinical-dashboard/prior-answer-turn-surface.tsx +++ b/src/components/clinical-dashboard/prior-answer-turn-surface.tsx @@ -85,6 +85,7 @@ export function PriorAnswerTurnSurface({ <> void; onFollowUp?: () => void; + onOpenSource?: () => void; imageCount?: number; divider?: boolean; }) { return (
- +