From c8b928d47947114557018c28949c93fcaee422d3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 17:43:00 +0000 Subject: [PATCH 01/12] Add local/dev presence checks for HMAC and probe secrets. Introduce check:local-presence (optional --fill) for safety-identifier, query-hash, and deep-probe gaps without printing values, warn from production-readiness, and record #054 local progress plus #007 blocked on the Tools canonical entry decision. Co-authored-by: BigSimmo --- docs/operator-backlog.md | 1 + docs/outstanding-issues.md | 8 +- docs/production-readiness-checklist.md | 3 + package.json | 1 + scripts/check-local-presence.mjs | 336 +++++++++++++++++++++ scripts/production-readiness.ts | 18 +- tests/local-presence.test.ts | 100 ++++++ tests/production-readiness-offline.test.ts | 7 + 8 files changed, 469 insertions(+), 5 deletions(-) create mode 100644 scripts/check-local-presence.mjs create mode 100644 tests/local-presence.test.ts diff --git a/docs/operator-backlog.md b/docs/operator-backlog.md index 31d9ec96a..ff7618411 100644 --- a/docs/operator-backlog.md +++ b/docs/operator-backlog.md @@ -44,6 +44,7 @@ dashboard/CLI action, never committed. | Secret / config | Status | Where | Notes | | ------------------------------------------ | ---------- | ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Local fillable HMAC/probe secrets | βœ… local | `.env.local` (gitignored) | Use `npm run check:local-presence` / `--fill` for distinct local `OPENAI_SAFETY_IDENTIFIER_SECRET`, `RAG_QUERY_HASH_SECRET`, and `HEALTH_DEEP_PROBE_SECRET`. Never commit values. Hosted stores are separate. | | `RAG_QUERY_HASH_SECRET` (prod) | πŸ”Ž verify | Railway runtime secret | GitHub repo secret present since 2026-07-10 (CI green); confirm the SAME value is set in Railway runtime. PIA-2 fail-closed guard requires it at boot (min 16 chars) | | `HEALTH_DEEP_PROBE_SECRET` (prod + GitHub) | ⚠️ partial | Railway runtime + GitHub repo secret | Railway production was set and the authorized deep probe returned healthy on 2026-07-19. GitHub remains pending: set the same value as a repo secret, set `PROD_HEALTH_URL`, then enable the ops-digest schedule. | | `SUPABASE_SERVICE_ROLE_KEY` (per env) | ⏳ pending | Railway runtime secret | accepts the `sb_secret_…` key | diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 5c7002f7b..61663c2bd 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -56,7 +56,7 @@ removed after current-main verification; it is not missing recommended work. | 4 | `#030`, `#075` | A2 | High β€” search correctness | Decision-ready | 2–4 hours each | Handle as separate PRs: #030 requires distinct source identities; #075 reproduces more than 1,000 labels and adds bounded pagination. Run focused contracts and `verify:cheap`; stop before alias, retrieval, or ranking changes without protected evidence. | | 5 | `#069` | A3 | Specialist β€” retrieval latency | After hosted apply of PR #1133 migrations; approval-gated live profile | 30–60 min | Operator applies `20260724120000`/`20260724130000`/`20260724130100`, then re-profiles `match_document_table_facts_text` (~70ms-class plans). Stop without mutating ranking or unpaid evals. Cloud agent blocked: no DB URL / MCP auth; live profile hit Unregistered API key. | | 6 | `#019` | A2 | Specialist β€” RAG answer pipeline | Local reproducer now; behavior change after `#051`/`#023` | 0.5–1 day reproducer | Reproduce admission-source loss in the fallback layer using PR #1096’s source shape. Any behavior change needs protected review and an approved baseline/post canary; stop if independently non-reproducible. | -| 7 | `#054` | A2 | Standard locally; Operator hosted | Local safety identifier now; hosted next approved window | 15–30 min local; 1–2 hours hosted | Presence-check and fill confirmed secret/config gaps with distinct per-environment values. Never record values. Require clean readiness/secret checks; stop on ambiguous environment or project identity. | +| 7 | `#054` | A2 | Operator hosted | After local presence tool; next approved hosted secret window | 1–2 hours hosted | Local/dev fillable gaps are handled by `npm run check:local-presence` (optionally `--fill`). Hosted remaining: presence-check Railway/GitHub for query-hash, deep-probe, service-role, OpenAI, and schedule settings with distinct per-environment values. Never record values; stop on ambiguous identity. | | 8 | `#022` | A2 | Operator β€” clinical governance + Specialist | Decision-ready | 1–2 hours policy; 0.5–1 day first ten | Decide BMJ attestation policy and review the ten highest-impact local documents. Record reviewer/evidence/time; stop after ten and remeasure warning debt. | | 9 | `#051`, `#023` | A2 | Specialist β€” RAG diagnostics | After scheduled 2026-07-26 run | 2–4 hours | With GitHub-read approval, compare structured canary/browser/irrelevant-at-10 artifacts without dispatching a rerun. Record deterministic/provider/latency deltas and disposition residuals; stop without spending. | | 10 | `#018` | A2 | Specialist β€” clinical RAG/retrieval | After `#051`/`#023`, one mechanism at a time | 1–2 days diagnosis | Give lithium, ADHD, and metabolic residuals separate current-main reproducers and candidates. Behavior canaries require approval; stop any item without a deterministic reproducer or on regression. | @@ -68,7 +68,7 @@ removed after current-main verification; it is not missing recommended work. | 16 | `#056` | A2 | Operator β€” Supabase/Railway + Specialist | After cost/ownership approval | 0.5–1 day | Provision isolated `Clinical KB Staging` with synthetic data and distinct secrets. Verify identity, schema, indexing, health, and data boundary; never copy production clinical documents. | | 17 | `#057` | A2 | High β€” release/SRE + Operator | After `#056` | 2–4 hours plus soak | Run documented staging soak and rollback against an exact candidate. Retain latency/error/rollback evidence; stop on unsafe data, identity mismatch, or unowned rollback. | | 18 | `#058` | A2 | Operator β€” production data + Specialist | Next approved production verification window | 30–60 min read-only; 1–2 hours if needed | Verify registry/differentials/medications are non-empty before writing; seed only confirmed gaps idempotently. Stop when healthy or owner/project identity is ambiguous. | -| 19 | `#007` | A3 | Operator decision + Standard frontend | When product chooses canonical Tools route | 15–30 min decision; 0.5 day | Align navigation, redirect, sitemap, and reachability around one entry point. Stop while the standalone page has an unresolved requirement. | +| 19 | `#007` | A3 | Operator decision + Standard frontend | **Blocked** until product chooses canonical Tools route | 15–30 min decision; 0.5 day | Ask product: canonical `/?mode=tools` or `/tools`? Then align navigation, redirect, sitemap, and reachability around that one entry. Stop while the decision is unresolved. | | 20 | `#011` | A3 | Operator β€” Supabase capacity | Immediately before first compute scale-up | 30–60 min plus observation | Switch Auth to percentage allocation, record before/after, and run approved advisor/health checks. Stop if no scale-up is planned. | | 21 | `#017` | A3 | High β€” performance/browser | Before `#012`/`#013`/`#016`; approved live-site window | 1–2 hours | Capture reproducible mobile/desktop Lighthouse/Web-Vitals evidence and decide whether payload work is justified. Stop if metrics are acceptable or evidence is too noisy. | | 22 | `#024` | A3 | High β€” Next.js/Playwright/WebKit | After `#051`/`#023` or real Safari reproduction | 0.5–1 day | Distinguish test interception from a Safari defect. Apply test-only correction only with a discriminating repro; keep access-control assertions meaningful. | @@ -107,13 +107,13 @@ removed after current-main verification; it is not missing recommended work. | #001 | P2 | task | Semantic reranking still gated off | `RAG_SEMANTIC_RERANK_ENABLED=false` from PR #901. Do not enable until the provider-backed 36/36 retrieval-quality gate **and** an ambiguity-focused canary are explicitly approved and recorded. | `docs/process-hardening.md` (Semantic reranking rollout debt); PR #901 | 2026-07-21 | | #069 | P3 | task | Live-profile table-facts plpgsql+EXECUTE latency | Migration `20260724120000_table_facts_plpgsql_execute.sql` plus P3 follow-ups (`20260724130000_*`, `20260724130100_*`) are in PR #1133. Hosted apply is blocked in this environment (no `SUPABASE_DB_URL`; Supabase MCP `needsAuth`). A live `profile:retrieval --rpc match_document_table_facts_text --analyze` attempt returned `Unregistered API key` against the injected service-role secret. **Next:** operator applies the three pending migrations on the live target project, then re-runs approval-gated `profile:retrieval` / `explain_retrieval_rpc` and confirms ~70ms-class plans with no ranking change. **Stop** without mutating ranking. | session 2026-07-24 Database interface audit; PR #1133 | 2026-07-24 | | #053 | P1 | task | Execute cross-border privacy/legal package | Execute OpenAI and Railway DPAs; decide ZDR and Australian data residency; obtain prompt-cache behavior in writing; review subprocessors; obtain APP 8 and APP 5/1 counsel sign-off. Do not represent the release as privacy-approved or alter final public privacy wording before sign-off. | `docs/openai-cross-border-basis.md`; `docs/privacy-impact-assessment.md` | 2026-07-24 | -| #054 | P2 | task | Reconcile local and hosted secrets/config | Presence-check safety-identifier, query-hash, deep-probe, Supabase service-role, OpenAI, project-identity, and schedule settings; set only confirmed gaps with distinct per-environment values. Never record secret values. Provider reads/writes require approval. | `.env.example`; production-readiness warning; `docs/operator-backlog.md` | 2026-07-24 | +| #054 | P2 | task | Reconcile local and hosted secrets/config | **Local (2026-07-24):** `npm run check:local-presence` (+ optional `--fill`) presence-checks safety-identifier / query-hash / deep-probe (names + length buckets only), fails closed on ambiguous project identity, and fills only those three local HMAC/probe gaps into gitignored `.env.local`. Production-readiness now warns on the same local gaps. Provider keys and hosted Railway/GitHub stores were not touched. **Next (hosted/operator):** presence-check Railway runtime + GitHub repo secrets for query-hash, deep-probe (+ `PROD_HEALTH_URL` / ops-digest), service-role, OpenAI, and schedule/Vault settings; set only confirmed gaps with distinct per-environment values. Never record secret values. Provider reads/writes require approval. | `.env.example`; `scripts/check-local-presence.mjs`; production-readiness; `docs/operator-backlog.md` | 2026-07-24 | | #055 | P2 | task | Run one exact-SHA full release and PR gate | Before the next full-confidence release/handoff, record the candidate/PR SHA and run the local/provider release gates, Firefox/WebKit, required hosted CI, and actionable GitHub review-thread closure once. Stop at the first actionable failure and rerun only the repaired smallest gate. | `docs/launch-operator-runbook.md`; `docs/codex-review-protocol.md` | 2026-07-24 | | #056 | P2 | task | Provision isolated staging environment | After explicit cost/ownership approval, provision `Clinical KB Staging` Supabase and Railway tiers with distinct secrets and synthetic/non-clinical data. Verify identity, schema, indexing, health, and the production-data boundary. | `docs/staging-setup.md`; `docs/operator-backlog.md` | 2026-07-24 | | #057 | P2 | task | Complete staging soak and rollback rehearsal | After #056, run the documented soak and rollback against an exact candidate; retain latency/error/rollback evidence. Stop on unsafe data, identity mismatch, or an unowned rollback decision. | `docs/launch-operator-runbook.md`; `docs/capacity-review.md` | 2026-07-24 | | #058 | P2 | task | Verify production content before any seed write | Against `the live target project`, verify registry, differentials, and medications surfaces are non-empty before writing. Seed only confirmed gaps idempotently with approved owner/project identity and confirmation flags. | `docs/launch-operator-runbook.md`; `docs/operator-backlog.md` | 2026-07-24 | | #005 | P3 | rec | `finalScore` saturates at clamp ceiling | Base + ~40 stacked boosts routinely exceed 1.0, so strong matches tie at 1.0 and order by an arbitrary `document_id` tiebreak. If ranking is ever revisited, break ties by the **pre-clamp** score rather than raising the `[0,1]` ceiling (downstream gates assume `[0,1]`). Ordering already sorts by the unbounded pre-clamp `rankScore` (`clinical-search.ts:1735,1927,1950-1955`), so the clamp confines only the reported confidence value, not result order. Not a defect on the current golden set; any change here is a protected RAG surface (canary required). | `docs/rag-hybrid-findings-and-todo.md` P1 item 4; `src/lib/clinical-search.ts:1735` | 2026-07-21 | -| #007 | P3 | rec | `/tools` vs `/?mode=tools` parallel Tools entry points | `/tools` (standalone `ApplicationsLauncherPage`) has no inbound in-app link; the sidebar Tools item uses `/?mode=tools`. Decide the canonical entry point and wire nav consistently, or drop the standalone `/tools` page + `/applications` redirect. Currently allowlisted in `tests/route-reachability.test.ts`. | `src/app/tools/page.tsx`; `src/app/applications/route.ts` | 2026-07-21 | +| #007 | P3 | rec | `/tools` vs `/?mode=tools` parallel Tools entry points | **Blocked 2026-07-24:** no product decision on the canonical Tools entry. Stop before nav/redirect/sitemap/reachability edits. Choose one: (A) `/?mode=tools` canonical β€” keep sidebar link, redirect `/tools` + `/applications` there (or drop standalone page), remove reachability allowlist; or (B) `/tools` canonical β€” point sidebar/nav at `/tools`, keep `/applications` β†’ `/tools`, align sitemap/reachability. Currently `/tools` (`ApplicationsLauncherPage`) has no inbound in-app link; sidebar Tools uses `/?mode=tools`; allowlisted in `tests/route-reachability.test.ts`. | `src/app/tools/page.tsx`; `src/app/applications/route.ts`; session 2026-07-24 | 2026-07-21 | | #009 | P3 | rec | Confirm `/api/jobs` is intentionally server/ops-only | No client `fetch()` reaches `/api/jobs` (only tests import it). Confirm it is a deliberate ops/manual surface; if abandoned, remove it. | `src/app/api/jobs/route.ts` | 2026-07-21 | | #010 | P3 | task | Un-built "Coming soon" controls across forms/favourites | ~10 disabled placeholders (forms refine/reset, favourites sort/add/new-set, move-to-set, remove-favourite) plus presentation Compact/Detailed density toggle (honest coming-soon placeholders; selected styling removed 2026-07-24). Correctly non-interactive β€” wire when the underlying features land. | `forms-search-results-page.tsx`; `favourites-hub.tsx`; `favourites-command-library-page.tsx`; `differential-presentation-workflow-page.tsx` | 2026-07-21 | | #011 | P3 | task | Auth DB-connection allocation is operator-only | Supabase Auth (GoTrue) is capped at ~10 absolute DB connections (Supabase perf advisor). Switch to **percentage-based** allocation in the Supabase **dashboard** before the first compute scale-up β€” **not settable via SQL/MCP** (operator-owned). Verify via a staging soak + an approval-gated read-only advisor re-check. | `docs/auth-connection-cap-runbook.md`; `docs/process-hardening.md` (Known follow-up debts) | 2026-07-21 | diff --git a/docs/production-readiness-checklist.md b/docs/production-readiness-checklist.md index c802c15bb..4dbd05c07 100644 --- a/docs/production-readiness-checklist.md +++ b/docs/production-readiness-checklist.md @@ -26,6 +26,9 @@ Last reviewed: 2026-07-10. Applies to any feature branch or release candidate. - [x] Added one-command production preflight: - `npm run check:production-readiness` - runs env validation, Supabase target checks, lockfile/env-file presence checks, and placeholder checks. +- [x] Added local/dev secret presence helper: + - `npm run check:local-presence` (optional `--fill`) + - names + length buckets only; fills only safety-identifier / query-hash / deep-probe into gitignored `.env.local`; never prints values or invents provider credentials. - [x] Added deployment startup readiness gate: - `npm run check:deployment-readiness` - verifies `next start` boot behavior and local project identity guard on a managed local port. diff --git a/package.json b/package.json index 4b74888f2..d86513d2b 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "check:branch-review-ledger": "node scripts/check-branch-review-ledger.mjs --self-test && node scripts/check-branch-review-ledger.mjs", "check:pr-policy": "node scripts/pr-policy.mjs --self-test && node scripts/check-pr-policy-workflow.mjs", "check:env-parity": "node scripts/check-env-parity.mjs", + "check:local-presence": "node scripts/check-local-presence.mjs", "sweep:branch-ledger": "node scripts/sweep-branch-ledger.mjs", "docs:check-links": "node scripts/check-docs-links.mjs", "docs:check-scripts": "node scripts/check-docs-script-refs.mjs", diff --git a/scripts/check-local-presence.mjs b/scripts/check-local-presence.mjs new file mode 100644 index 000000000..a8689c365 --- /dev/null +++ b/scripts/check-local-presence.mjs @@ -0,0 +1,336 @@ +#!/usr/bin/env node +/** + * check-local-presence β€” local/dev secret and project-identity presence checks. + * + * Names + length buckets only. Never prints secret values. + * Optional `--fill` writes distinct local-only HMAC/probe secrets into `.env.local` + * for confirmed fillable gaps. Never invents provider credentials or touches + * hosted Railway/GitHub secret stores. + * + * npm run check:local-presence + * npm run check:local-presence -- --fill + */ +import { randomBytes } from "node:crypto"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.join(path.dirname(fileURLToPath(import.meta.url)), ".."); + +/** Local-only secrets this tool may generate into `.env.local`. */ +export const FILLABLE_LOCAL_SECRETS = [ + { + name: "OPENAI_SAFETY_IDENTIFIER_SECRET", + minLength: 32, + bytes: 32, + reason: "pseudonymous OpenAI safety_identifier HMAC (local/dev distinct value)", + }, + { + name: "RAG_QUERY_HASH_SECRET", + minLength: 16, + bytes: 32, + reason: "PIA-2 query-hash HMAC (local/dev distinct value)", + }, + { + name: "HEALTH_DEEP_PROBE_SECRET", + minLength: 16, + bytes: 32, + reason: "authorized /api/health?deep=1 probe token (local/dev distinct value)", + }, +]; + +/** Report-only; never auto-filled (need real provider/project values). */ +export const REPORT_ONLY_KEYS = [ + "NEXT_PUBLIC_SUPABASE_URL", + "SUPABASE_PROJECT_REF", + "SUPABASE_PROJECT_NAME", + "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY", + "SUPABASE_SERVICE_ROLE_KEY", + "OPENAI_API_KEY", + "RAG_PROVIDER_MODE", +]; + +const EXPECTED_PROD_REF = "sjrfecxgysukkwxsowpy"; +const EXPECTED_PROD_NAME = "Clinical KB Database"; +const STALE_REF = "qjgitjyhxrwxsrydablr"; + +const ENV_FILES = [".env", ".env.local", ".env.development.local"]; + +/** Length bucket for presence reports β€” never the raw value. */ +export function lengthBucket(value) { + if (value == null || String(value).trim() === "") return "missing"; + const len = String(value).length; + if (len < 8) return "too-short(<8)"; + if (len < 16) return "short(<16)"; + if (len < 32) return "ok(16-31)"; + if (len < 64) return "ok(32-63)"; + return "ok(64+)"; +} + +export function isPresent(value) { + return Boolean(value && String(value).trim()); +} + +export function meetsMinLength(value, minLength) { + return isPresent(value) && String(value).trim().length >= minLength; +} + +/** Parse KEY=VALUE lines without shell expansion; strip simple quotes. */ +export function parseEnvFile(text) { + const out = {}; + for (const rawLine of text.split(/\r?\n/)) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) continue; + const eq = line.indexOf("="); + if (eq <= 0) continue; + const key = line.slice(0, eq).trim(); + if (!/^[A-Z][A-Z0-9_]*$/.test(key)) continue; + let value = line.slice(eq + 1); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + out[key] = value; + } + return out; +} + +export function loadLocalEnv({ cwd = root, processEnv = process.env } = {}) { + const fromFiles = {}; + const filesPresent = []; + for (const name of ENV_FILES) { + const filePath = path.join(cwd, name); + if (!existsSync(filePath)) continue; + filesPresent.push(name); + Object.assign(fromFiles, parseEnvFile(readFileSync(filePath, "utf8"))); + } + const merged = { ...fromFiles }; + for (const key of [...FILLABLE_LOCAL_SECRETS.map((s) => s.name), ...REPORT_ONLY_KEYS]) { + const fromProc = processEnv[key]; + if (fromProc != null && String(fromProc).length > 0) { + merged[key] = fromProc; + } + } + return { merged, filesPresent, fromFiles }; +} + +export function extractUrlRef(url) { + if (!isPresent(url)) return null; + try { + const hostname = new URL(String(url).trim()).hostname.toLowerCase(); + const suffix = ".supabase.co"; + if (!hostname.endsWith(suffix)) return null; + const ref = hostname.slice(0, -suffix.length); + return /^[a-z0-9]{20}$/.test(ref) ? ref : null; + } catch { + return null; + } +} + +/** + * Project-identity classification for local presence. + * - unset: no Supabase URL β†’ demo-mode local is fine + * - ready: URL + ref/name align with production or an explicit staging pair + * - ambiguous: partial/mismatched/stale identity β†’ fail closed + */ +export function classifyProjectIdentity(env) { + const url = env.NEXT_PUBLIC_SUPABASE_URL; + const ref = env.SUPABASE_PROJECT_REF?.trim() || null; + const name = env.SUPABASE_PROJECT_NAME?.trim() || null; + const urlRef = extractUrlRef(url); + + if (!isPresent(url) && !ref && !name) { + return { + status: "unset", + message: "No Supabase project identity configured (local demo mode is OK).", + }; + } + + if (!isPresent(url)) { + return { + status: "ambiguous", + message: "SUPABASE_PROJECT_REF/NAME set without NEXT_PUBLIC_SUPABASE_URL.", + }; + } + + if (!urlRef) { + return { + status: "ambiguous", + message: "NEXT_PUBLIC_SUPABASE_URL is not a recognizable *.supabase.co project URL.", + }; + } + + if (urlRef === STALE_REF || ref === STALE_REF) { + return { + status: "ambiguous", + message: `Stale Supabase project ref ${STALE_REF} detected; refuse to continue.`, + }; + } + + if (urlRef === EXPECTED_PROD_REF) { + const problems = []; + if (ref && ref !== EXPECTED_PROD_REF) { + problems.push(`SUPABASE_PROJECT_REF (${ref}) does not match URL ref.`); + } + if (name && name !== EXPECTED_PROD_NAME) { + problems.push(`SUPABASE_PROJECT_NAME does not match expected "${EXPECTED_PROD_NAME}".`); + } + if (!ref || !name) { + problems.push("Production URL present but SUPABASE_PROJECT_REF and/or SUPABASE_PROJECT_NAME missing."); + } + if (problems.length) { + return { status: "ambiguous", message: problems.join(" ") }; + } + return { + status: "ready", + message: `Project identity points at ${EXPECTED_PROD_NAME} (${EXPECTED_PROD_REF}).`, + }; + } + + // Non-production URL: require explicit staging declaration pair matching the URL ref. + const stagingRef = env.SUPABASE_STAGING_PROJECT_REF?.trim() || null; + const stagingName = env.SUPABASE_STAGING_PROJECT_NAME?.trim() || null; + if (stagingRef === urlRef && stagingName && ref === urlRef && name === stagingName) { + return { + status: "ready", + message: `Project identity points at declared staging "${stagingName}" (${urlRef}).`, + }; + } + + return { + status: "ambiguous", + message: + "Supabase URL is neither the expected production project nor a fully declared staging identity. Stop rather than guessing.", + }; +} + +export function assessLocalPresence(env) { + const fillable = FILLABLE_LOCAL_SECRETS.map((spec) => { + const value = env[spec.name]; + const present = meetsMinLength(value, spec.minLength); + return { + name: spec.name, + present, + lengthBucket: lengthBucket(value), + minLength: spec.minLength, + fillable: true, + reason: spec.reason, + status: present ? "ok" : "gap", + }; + }); + + const reportOnly = REPORT_ONLY_KEYS.map((name) => { + const value = env[name]; + const present = isPresent(value); + return { + name, + present, + lengthBucket: lengthBucket(value), + fillable: false, + status: present ? "ok" : "report-only-gap", + }; + }); + + const identity = classifyProjectIdentity(env); + return { fillable, reportOnly, identity }; +} + +export function generateLocalSecret(bytes = 32) { + return randomBytes(bytes).toString("hex"); +} + +/** + * Merge fillable gaps into `.env.local` text. Never overwrites an existing + * non-empty assignment for a key. Returns which keys were filled (names only). + */ +export function mergeFillIntoEnvLocal(existingText, gaps, { generate = generateLocalSecret } = {}) { + const existing = parseEnvFile(existingText || ""); + const filled = []; + const additions = []; + + for (const gap of gaps) { + if (meetsMinLength(existing[gap.name], gap.minLength)) continue; + const value = generate(gap.bytes); + if (!meetsMinLength(value, gap.minLength)) { + throw new Error(`Generated ${gap.name} did not meet minLength ${gap.minLength}`); + } + filled.push(gap.name); + additions.push(`# local/dev presence fill β€” ${gap.reason}`); + additions.push(`${gap.name}=${value}`); + } + + if (filled.length === 0) { + return { text: existingText || "", filled }; + } + + const base = (existingText || "").replace(/\s*$/, ""); + const next = `${base ? `${base}\n\n` : ""}# --- check:local-presence --fill (${new Date().toISOString().slice(0, 10)}) ---\n${additions.join("\n")}\n`; + return { text: next, filled }; +} + +function printPresenceReport({ filesPresent, assessment }) { + console.log("[Local presence]"); + console.log(`Env files: ${filesPresent.length ? filesPresent.join(", ") : "(none)"}`); + console.log(""); + console.log("Fillable local secrets (names + length buckets only):"); + for (const row of assessment.fillable) { + const mark = row.status === "ok" ? "ok" : "GAP"; + console.log(` - ${row.name}: ${mark} [${row.lengthBucket}] (min ${row.minLength})`); + } + console.log(""); + console.log("Report-only (never auto-filled):"); + for (const row of assessment.reportOnly) { + const mark = row.present ? "present" : "absent"; + console.log(` - ${row.name}: ${mark} [${row.lengthBucket}]`); + } + console.log(""); + console.log(`Project identity: ${assessment.identity.status} β€” ${assessment.identity.message}`); +} + +function main() { + const fill = process.argv.includes("--fill"); + const { merged, filesPresent } = loadLocalEnv(); + const assessment = assessLocalPresence(merged); + + printPresenceReport({ filesPresent, assessment }); + + if (assessment.identity.status === "ambiguous") { + console.error("\nSTOP: ambiguous project identity. Resolve SUPABASE_* identity before filling or continuing."); + process.exit(1); + } + + const gaps = FILLABLE_LOCAL_SECRETS.filter((spec) => { + const row = assessment.fillable.find((r) => r.name === spec.name); + return row && row.status === "gap"; + }); + + if (!fill) { + if (gaps.length) { + console.log( + `\n${gaps.length} fillable local gap(s). Re-run with --fill to write distinct local values into .env.local (gitignored).`, + ); + process.exitCode = 2; + } else { + console.log("\nNo fillable local gaps."); + } + return; + } + + if (gaps.length === 0) { + console.log("\n--fill: nothing to write; fillable local secrets already present."); + return; + } + + const envLocalPath = path.join(root, ".env.local"); + const existingText = existsSync(envLocalPath) ? readFileSync(envLocalPath, "utf8") : ""; + const { text, filled } = mergeFillIntoEnvLocal(existingText, gaps); + writeFileSync(envLocalPath, text, { encoding: "utf8", mode: 0o600 }); + console.log(`\n--fill: wrote ${filled.length} distinct local secret(s) into .env.local:`); + for (const name of filled) console.log(` - ${name}`); + console.log("Values were not printed. .env.local is gitignored β€” do not commit it."); +} + +const invokedDirectly = process.argv[1]?.endsWith("check-local-presence.mjs"); +if (invokedDirectly) main(); diff --git a/scripts/production-readiness.ts b/scripts/production-readiness.ts index bdd446ce4..8b99263d0 100644 --- a/scripts/production-readiness.ts +++ b/scripts/production-readiness.ts @@ -242,10 +242,14 @@ async function main() { if (envModule.env.OPENAI_API_KEY && !envModule.env.OPENAI_SAFETY_IDENTIFIER_SECRET) { result.warnings.push( - "OPENAI_SAFETY_IDENTIFIER_SECRET is not set; authenticated Responses requests omit the privacy-preserving safety identifier.", + "OPENAI_SAFETY_IDENTIFIER_SECRET is not set; authenticated Responses requests omit the privacy-preserving safety identifier. For local/dev, run npm run check:local-presence -- --fill.", ); } else if (envModule.env.OPENAI_SAFETY_IDENTIFIER_SECRET) { result.passes.push("OpenAI safety identifiers use a deployment-secret HMAC; raw owner IDs are not sent."); + } else if (!isCiMode) { + result.warnings.push( + "OPENAI_SAFETY_IDENTIFIER_SECRET is not set (optional until OpenAI is enabled). Local fill: npm run check:local-presence -- --fill.", + ); } // Exercise the real boot guard so this check tracks its behaviour instead of @@ -262,9 +266,21 @@ async function main() { const productionLike = process.env.NODE_ENV === "production" || process.env.VERCEL_ENV === "production"; if (productionLike) { result.failures.push(`Query-hash secret issue: ${message}`); + } else if (!isCiMode) { + result.warnings.push( + `RAG_QUERY_HASH_SECRET is not set for local/dev (${message}). Fill a distinct local value with npm run check:local-presence -- --fill.`, + ); } } + if (envModule.env.HEALTH_DEEP_PROBE_SECRET) { + result.passes.push("HEALTH_DEEP_PROBE_SECRET is set for authorized deep health probes."); + } else if (!isCiMode) { + result.warnings.push( + "HEALTH_DEEP_PROBE_SECRET is not set; /api/health?deep=1 stays shallow. Local fill: npm run check:local-presence -- --fill.", + ); + } + if (placeholderLooksLikeExample(envModule.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY ?? "")) { result.warnings.push("NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY looks like a placeholder."); } diff --git a/tests/local-presence.test.ts b/tests/local-presence.test.ts new file mode 100644 index 000000000..878ca55d4 --- /dev/null +++ b/tests/local-presence.test.ts @@ -0,0 +1,100 @@ +import { describe, expect, it } from "vitest"; + +import { + assessLocalPresence, + classifyProjectIdentity, + FILLABLE_LOCAL_SECRETS, + lengthBucket, + mergeFillIntoEnvLocal, + parseEnvFile, + REPORT_ONLY_KEYS, +} from "../scripts/check-local-presence.mjs"; + +describe("check-local-presence", () => { + it("parses env assignments without exposing values in helpers", () => { + const parsed = parseEnvFile( + ['# comment', 'RAG_QUERY_HASH_SECRET="abc"', "OPENAI_API_KEY=xyz", "not valid", ""].join("\n"), + ); + expect(parsed.RAG_QUERY_HASH_SECRET).toBe("abc"); + expect(parsed.OPENAI_API_KEY).toBe("xyz"); + expect(Object.keys(parsed)).not.toContain("not"); + }); + + it("buckets lengths without needing the raw secret in assertions beyond fixtures", () => { + expect(lengthBucket("")).toBe("missing"); + expect(lengthBucket("short")).toBe("too-short(<8)"); + expect(lengthBucket("0123456789abcdef")).toBe("ok(16-31)"); + expect(lengthBucket("a".repeat(40))).toBe("ok(32-63)"); + }); + + it("treats unset Supabase identity as demo-mode OK", () => { + expect(classifyProjectIdentity({}).status).toBe("unset"); + }); + + it("fails closed on stale or mismatched project identity", () => { + expect( + classifyProjectIdentity({ + NEXT_PUBLIC_SUPABASE_URL: "https://qjgitjyhxrwxsrydablr.supabase.co", + SUPABASE_PROJECT_REF: "qjgitjyhxrwxsrydablr", + SUPABASE_PROJECT_NAME: "Database", + }).status, + ).toBe("ambiguous"); + + expect( + classifyProjectIdentity({ + NEXT_PUBLIC_SUPABASE_URL: "https://sjrfecxgysukkwxsowpy.supabase.co", + SUPABASE_PROJECT_REF: "sjrfecxgysukkwxsowpy", + }).status, + ).toBe("ambiguous"); + }); + + it("accepts complete production identity", () => { + const result = classifyProjectIdentity({ + NEXT_PUBLIC_SUPABASE_URL: "https://sjrfecxgysukkwxsowpy.supabase.co", + SUPABASE_PROJECT_REF: "sjrfecxgysukkwxsowpy", + SUPABASE_PROJECT_NAME: "Clinical KB Database", + }); + expect(result.status).toBe("ready"); + }); + + it("flags fillable HMAC/probe gaps and never marks provider keys fillable", () => { + const assessment = assessLocalPresence({}); + expect(assessment.fillable.every((row) => row.status === "gap")).toBe(true); + expect(FILLABLE_LOCAL_SECRETS.map((s) => s.name)).toEqual( + expect.arrayContaining([ + "OPENAI_SAFETY_IDENTIFIER_SECRET", + "RAG_QUERY_HASH_SECRET", + "HEALTH_DEEP_PROBE_SECRET", + ]), + ); + expect(REPORT_ONLY_KEYS).toEqual( + expect.arrayContaining(["SUPABASE_SERVICE_ROLE_KEY", "OPENAI_API_KEY"]), + ); + expect(assessment.reportOnly.every((row) => row.fillable === false)).toBe(true); + }); + + it("merges only missing fillable secrets and never prints them via return shape", () => { + const existing = "RAG_PROVIDER_MODE=offline\n"; + let n = 0; + const { text, filled } = mergeFillIntoEnvLocal(existing, FILLABLE_LOCAL_SECRETS, { + generate: () => { + n += 1; + return `local-dev-secret-${n}-`.padEnd(64, "x"); + }, + }); + expect(filled).toEqual([ + "OPENAI_SAFETY_IDENTIFIER_SECRET", + "RAG_QUERY_HASH_SECRET", + "HEALTH_DEEP_PROBE_SECRET", + ]); + expect(text).toContain("RAG_PROVIDER_MODE=offline"); + expect(text).toContain("OPENAI_SAFETY_IDENTIFIER_SECRET="); + expect(text).toContain("RAG_QUERY_HASH_SECRET="); + expect(text).toContain("HEALTH_DEEP_PROBE_SECRET="); + + const again = mergeFillIntoEnvLocal(text, FILLABLE_LOCAL_SECRETS, { + generate: () => "should-not-be-used-because-already-present-pad".padEnd(64, "y"), + }); + expect(again.filled).toEqual([]); + }); +}); diff --git a/tests/production-readiness-offline.test.ts b/tests/production-readiness-offline.test.ts index adb67ef8f..fd61b23f0 100644 --- a/tests/production-readiness-offline.test.ts +++ b/tests/production-readiness-offline.test.ts @@ -19,4 +19,11 @@ describe("production readiness provider policy", () => { it("allows a missing OpenAI key only for explicit offline mode", () => { expect(openAIReadinessPolicy("offline")).toEqual({ required: false, ready: true }); }); + + it("documents local presence fill guidance for safety/query-hash/deep-probe gaps", () => { + const source = readFileSync(new URL("../scripts/production-readiness.ts", import.meta.url), "utf8"); + expect(source).toContain("check:local-presence"); + expect(source).toContain("HEALTH_DEEP_PROBE_SECRET is not set"); + expect(source).toContain("OPENAI_SAFETY_IDENTIFIER_SECRET is not set"); + }); }); From 7fe5b8cd1ab59e1fd90fe92a82103b31bfd9f0bb Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 17:44:29 +0000 Subject: [PATCH 02/12] Fix parseEnvFile typing for local-presence tests. Annotate the env map as Record so TypeScript accepts key access in the focused unit tests. Co-authored-by: BigSimmo --- scripts/check-local-presence.mjs | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check-local-presence.mjs b/scripts/check-local-presence.mjs index a8689c365..b4d26e14d 100644 --- a/scripts/check-local-presence.mjs +++ b/scripts/check-local-presence.mjs @@ -77,6 +77,7 @@ export function meetsMinLength(value, minLength) { /** Parse KEY=VALUE lines without shell expansion; strip simple quotes. */ export function parseEnvFile(text) { + /** @type {Record} */ const out = {}; for (const rawLine of text.split(/\r?\n/)) { const line = rawLine.trim(); From c1b462a0d2db74b4a9e2f7136c33bf075ab52c7e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 17:52:41 +0000 Subject: [PATCH 03/12] Fix CI gaps and canonicalize Tools at /tools (#007). Format local-presence files for Prettier, bump postcss past the <=8.5.17 advisory so dependency-scope audit can pass, and align nav, appModeHomeHref, universal-search, prefetch, sitemap, and reachability on /tools per PT-11. Archive #007; keep /?mode=tools as a mode alias. --- docs/outstanding-issues.md | 77 +++++++++---------- docs/site-map.md | 4 +- docs/wiring-conventions.md | 7 +- package-lock.json | 16 ++-- package.json | 4 +- scripts/check-local-presence.mjs | 7 +- scripts/generate-site-map.ts | 2 +- .../clinical-dashboard/ClinicalSidebar.tsx | 2 +- .../global-search-shell.tsx | 2 +- .../use-dashboard-shell-actions.ts | 2 +- src/lib/app-modes.ts | 5 +- src/lib/universal-search.ts | 2 +- tests/app-modes.test.ts | 2 +- tests/local-presence.test.ts | 18 +---- tests/route-reachability.test.ts | 4 - tests/ui-smoke.spec.ts | 4 +- 16 files changed, 73 insertions(+), 85 deletions(-) diff --git a/docs/outstanding-issues.md b/docs/outstanding-issues.md index 61663c2bd..4ccf5e8f2 100644 --- a/docs/outstanding-issues.md +++ b/docs/outstanding-issues.md @@ -48,44 +48,43 @@ removed after current-main verification; it is not missing recommended work. database/RAG/clinical/privacy expertise; Operator = named provider/product/legal authority. - **Estimate:** focused active time, excluding approval, hosted runtime, soak, and review waits. -| Order | ID(s) | Acuity | Capability | When | Estimate | Outcome, gate, verification, and stopping condition | -| ----: | ---------------------- | -------- | ------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 1 | `#059` | A1 | Operator security + independent reviewer | Immediate approved security window | 1–3 hours plus verification | Verify every reported exposed credential (GitHub, OpenAI, Supabase service role/database, E2E) is retired; rotate anything still valid and update only intended secret stores. Never record values; stop before provider action without approval. | -| 2 | `#053` | A1 | Operator β€” legal/privacy | Start now; finish before real patient use/privacy-approved release | 4–8 hours internal; 1–6 weeks elapsed | Execute DPAs; decide ZDR/residency; obtain cache behavior in writing; review subprocessors; obtain APP 8 and APP 5/1 counsel sign-off. Do not change public copy before approval. | -| 3 | `#067` | A3 | High β€” test reliability | Next flake-hardening window | 1–2 hours | Reproduce the load-sensitive reconciliation-preflight subprocess timeout, instrument its lifecycle, and make the smallest deterministic harness fix. Do not raise the global timeout or bypass the shared heavy-test lock without causal proof. | -| 4 | `#030`, `#075` | A2 | High β€” search correctness | Decision-ready | 2–4 hours each | Handle as separate PRs: #030 requires distinct source identities; #075 reproduces more than 1,000 labels and adds bounded pagination. Run focused contracts and `verify:cheap`; stop before alias, retrieval, or ranking changes without protected evidence. | -| 5 | `#069` | A3 | Specialist β€” retrieval latency | After hosted apply of PR #1133 migrations; approval-gated live profile | 30–60 min | Operator applies `20260724120000`/`20260724130000`/`20260724130100`, then re-profiles `match_document_table_facts_text` (~70ms-class plans). Stop without mutating ranking or unpaid evals. Cloud agent blocked: no DB URL / MCP auth; live profile hit Unregistered API key. | -| 6 | `#019` | A2 | Specialist β€” RAG answer pipeline | Local reproducer now; behavior change after `#051`/`#023` | 0.5–1 day reproducer | Reproduce admission-source loss in the fallback layer using PR #1096’s source shape. Any behavior change needs protected review and an approved baseline/post canary; stop if independently non-reproducible. | +| Order | ID(s) | Acuity | Capability | When | Estimate | Outcome, gate, verification, and stopping condition | +| ----: | ---------------------- | -------- | ------------------------------------------- | ---------------------------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `#059` | A1 | Operator security + independent reviewer | Immediate approved security window | 1–3 hours plus verification | Verify every reported exposed credential (GitHub, OpenAI, Supabase service role/database, E2E) is retired; rotate anything still valid and update only intended secret stores. Never record values; stop before provider action without approval. | +| 2 | `#053` | A1 | Operator β€” legal/privacy | Start now; finish before real patient use/privacy-approved release | 4–8 hours internal; 1–6 weeks elapsed | Execute DPAs; decide ZDR/residency; obtain cache behavior in writing; review subprocessors; obtain APP 8 and APP 5/1 counsel sign-off. Do not change public copy before approval. | +| 3 | `#067` | A3 | High β€” test reliability | Next flake-hardening window | 1–2 hours | Reproduce the load-sensitive reconciliation-preflight subprocess timeout, instrument its lifecycle, and make the smallest deterministic harness fix. Do not raise the global timeout or bypass the shared heavy-test lock without causal proof. | +| 4 | `#030`, `#075` | A2 | High β€” search correctness | Decision-ready | 2–4 hours each | Handle as separate PRs: #030 requires distinct source identities; #075 reproduces more than 1,000 labels and adds bounded pagination. Run focused contracts and `verify:cheap`; stop before alias, retrieval, or ranking changes without protected evidence. | +| 5 | `#069` | A3 | Specialist β€” retrieval latency | After hosted apply of PR #1133 migrations; approval-gated live profile | 30–60 min | Operator applies `20260724120000`/`20260724130000`/`20260724130100`, then re-profiles `match_document_table_facts_text` (~70ms-class plans). Stop without mutating ranking or unpaid evals. Cloud agent blocked: no DB URL / MCP auth; live profile hit Unregistered API key. | +| 6 | `#019` | A2 | Specialist β€” RAG answer pipeline | Local reproducer now; behavior change after `#051`/`#023` | 0.5–1 day reproducer | Reproduce admission-source loss in the fallback layer using PR #1096’s source shape. Any behavior change needs protected review and an approved baseline/post canary; stop if independently non-reproducible. | | 7 | `#054` | A2 | Operator hosted | After local presence tool; next approved hosted secret window | 1–2 hours hosted | Local/dev fillable gaps are handled by `npm run check:local-presence` (optionally `--fill`). Hosted remaining: presence-check Railway/GitHub for query-hash, deep-probe, service-role, OpenAI, and schedule settings with distinct per-environment values. Never record values; stop on ambiguous identity. | -| 8 | `#022` | A2 | Operator β€” clinical governance + Specialist | Decision-ready | 1–2 hours policy; 0.5–1 day first ten | Decide BMJ attestation policy and review the ten highest-impact local documents. Record reviewer/evidence/time; stop after ten and remeasure warning debt. | -| 9 | `#051`, `#023` | A2 | Specialist β€” RAG diagnostics | After scheduled 2026-07-26 run | 2–4 hours | With GitHub-read approval, compare structured canary/browser/irrelevant-at-10 artifacts without dispatching a rerun. Record deterministic/provider/latency deltas and disposition residuals; stop without spending. | -| 10 | `#018` | A2 | Specialist β€” clinical RAG/retrieval | After `#051`/`#023`, one mechanism at a time | 1–2 days diagnosis | Give lithium, ADHD, and metabolic residuals separate current-main reproducers and candidates. Behavior canaries require approval; stop any item without a deterministic reproducer or on regression. | -| 11 | `#029` | A2 | Specialist β€” answer quality/clinical safety | After `#051`/`#023` and `#018` | 0.5–1 day inventory; 1–3 days per fix | Re-enumerate current fallback stubs and fix one causal cluster at a time without weakening grounding/citation gates. Stop if a change merely makes the metric easier to pass. | -| 12 | `#001` | A2 | Specialist β€” retrieval/ranking | After `#051`/`#023` and rollout approval | 0.5–1 day plus canary | Keep semantic reranking off unless an approved ambiguity comparison preserves 36/36, recall 1.0, zero per-case regressions, and shows measured gain; otherwise record keep-off and stop. | -| 13 | `#025` | A2 | Operator β€” Railway/GitHub/chat/Supabase | Next approved observability window | 1–3 hours/channel | Choose owned deployment, CI, ingestion, and SLO alerts; mock first, then one approved controlled provider event/channel. The merged Supabase trigger remains inert until its verified inputs are configured. Stop without an accountable responder. | -| 14 | `#064` | A2 | High β€” frontend/browser | After higher-acuity local fixes; before the release UI gate | 4–8 hours | Preserve the isolated dirty formulation/contrast patch, reconcile its intent against current `main`, and run focused Playwright plus `verify:ui`. Stop rather than overwriting unrelated work or weakening access-control assertions. | -| 15 | `#055` | A2 | Specialist release owner + Operator | Before next full-confidence release/handoff | 2–4 hours plus runtime | On one exact SHA, run local/provider gates, Firefox/WebKit, required hosted CI, and close actionable GitHub threads. Stop at first failure and rerun only the repaired smallest gate. | -| 16 | `#056` | A2 | Operator β€” Supabase/Railway + Specialist | After cost/ownership approval | 0.5–1 day | Provision isolated `Clinical KB Staging` with synthetic data and distinct secrets. Verify identity, schema, indexing, health, and data boundary; never copy production clinical documents. | -| 17 | `#057` | A2 | High β€” release/SRE + Operator | After `#056` | 2–4 hours plus soak | Run documented staging soak and rollback against an exact candidate. Retain latency/error/rollback evidence; stop on unsafe data, identity mismatch, or unowned rollback. | -| 18 | `#058` | A2 | Operator β€” production data + Specialist | Next approved production verification window | 30–60 min read-only; 1–2 hours if needed | Verify registry/differentials/medications are non-empty before writing; seed only confirmed gaps idempotently. Stop when healthy or owner/project identity is ambiguous. | -| 19 | `#007` | A3 | Operator decision + Standard frontend | **Blocked** until product chooses canonical Tools route | 15–30 min decision; 0.5 day | Ask product: canonical `/?mode=tools` or `/tools`? Then align navigation, redirect, sitemap, and reachability around that one entry. Stop while the decision is unresolved. | -| 20 | `#011` | A3 | Operator β€” Supabase capacity | Immediately before first compute scale-up | 30–60 min plus observation | Switch Auth to percentage allocation, record before/after, and run approved advisor/health checks. Stop if no scale-up is planned. | -| 21 | `#017` | A3 | High β€” performance/browser | Before `#012`/`#013`/`#016`; approved live-site window | 1–2 hours | Capture reproducible mobile/desktop Lighthouse/Web-Vitals evidence and decide whether payload work is justified. Stop if metrics are acceptable or evidence is too noisy. | -| 22 | `#024` | A3 | High β€” Next.js/Playwright/WebKit | After `#051`/`#023` or real Safari reproduction | 0.5–1 day | Distinguish test interception from a Safari defect. Apply test-only correction only with a discriminating repro; keep access-control assertions meaningful. | -| 23 | `#033` | A3 | Specialist β€” prompt/source governance | After `#022` and `#051`/`#023` | 1–2 days plus approved eval | Design unknown-vs-adverse metadata wording and prompt tests. Require no supported-grounding drop and zero citation failures; stop on broad over-caveating or degradation. | -| 24 | `#037` | A3 | Operator β€” clinical/product + Standard | Next trust-policy review | 30–60 min; up to 0.5 day | Decide whether routine claims cap at medium trust. Record policy; if accepted, change only the flag/expectations and run focused tests. | -| 25 | `#012`, `#013`, `#016` | A3 | High β€” bundling/runtime performance | After `#017` or equivalent evidence | 0.5–2 days/route | Optimize only a production route with measured payload/render/motion harm. Require material gain plus focused, `verify:cheap`, and browser evidence; stop on small gain. | -| 26 | `#035` | A3 | Specialist β€” evidence rules | After a demonstrated missed conflict | 0.5–1 day design; code separate | Define a clinically reviewed conflict class with positive and negative fixtures. Stop if no bounded class can be shown; behavior change requires protected review. | -| 27 | `#027` | Optional | Operator β€” SRE/provider | When an owned external alert path is wanted | 1–2 hours | Decide vendor/cost/privacy/owner; if accepted, prove one non-PHI outage and recovery alert. Stop when no responder owns it. | -| 28 | `#028` | Optional | Specialist privacy/observability + Operator | After privacy/ownership/cost approval | 1–3 days | Define vendor/region/retention/redaction/sampling/source-map envelope before SDK work. Prove no clinical text, identifiers, or secrets leave; stop if unacceptable. | -| 29 | `#038` | Optional | High β€” product/design architecture | When a new comparison surface is approved | 0.5–1 day | Define a shared interaction contract without flattening mode-specific content. Stop when no concrete new surface exists. | -| 30 | `#040` | Optional | High β€” visual QA/accessibility | When baseline owner/update workflow exists | 1–2 days | Establish a small stable desktop/mobile/accessibility baseline set. Do not make it blocking if flake or maintenance cost outweighs detection value. | -| 31 | `#039` | Optional | High β€” frontend architecture | During a concrete catalogue-toolbar project | 0.5–1 day inventory; 1–3 days code | Converge only repeated toolbar behavior without flattening search semantics. Stop when there is no bounded implementation target. | -| 32 | `#063` | A3 | High β€” product architecture + privacy | Only when the product owner wants to evaluate the feature | 0.5–1 day | Write a product/privacy/persistence brief for β€œCurrent Clinical Work” before storage or UI implementation. Stop if demand or safe persistence cannot be established. | -| 33 | `#065` | A2 | High β€” document-viewer UI | Only when the user explicitly resumes the paused task | 0.5–1.5 days | Finish the compact source-text accordion, citation/search auto-open, print restoration, and 320/390/1280 px coverage. Keep the preserved branch untouched until explicit resume; no provider calls. | -| 34 | `#077` | A3 | High β€” workflow safety | Next workflow-hardening window | 1–2 hours | Add a cooperative primary-checkout write lease/check that prevents a second task from writing, switching, or synchronizing the canonical checkout while another owner or dirty state exists. Keep read-only work and independent feature worktrees unblocked. | -| 35 | `#078` | A3 | Standard β€” reconciliation tooling | After `#067`; before another broad reconciliation | 2–4 hours | Generate one deterministic, secret-safe evidence pack from the reconciliation lifecycle: disposition rows, operation markers, archive refs, bundle verification, hashes, worktree counts, and local/base equality. Never fetch, call providers, or delete implicitly. | -| 36 | `#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. | +| 8 | `#022` | A2 | Operator β€” clinical governance + Specialist | Decision-ready | 1–2 hours policy; 0.5–1 day first ten | Decide BMJ attestation policy and review the ten highest-impact local documents. Record reviewer/evidence/time; stop after ten and remeasure warning debt. | +| 9 | `#051`, `#023` | A2 | Specialist β€” RAG diagnostics | After scheduled 2026-07-26 run | 2–4 hours | With GitHub-read approval, compare structured canary/browser/irrelevant-at-10 artifacts without dispatching a rerun. Record deterministic/provider/latency deltas and disposition residuals; stop without spending. | +| 10 | `#018` | A2 | Specialist β€” clinical RAG/retrieval | After `#051`/`#023`, one mechanism at a time | 1–2 days diagnosis | Give lithium, ADHD, and metabolic residuals separate current-main reproducers and candidates. Behavior canaries require approval; stop any item without a deterministic reproducer or on regression. | +| 11 | `#029` | A2 | Specialist β€” answer quality/clinical safety | After `#051`/`#023` and `#018` | 0.5–1 day inventory; 1–3 days per fix | Re-enumerate current fallback stubs and fix one causal cluster at a time without weakening grounding/citation gates. Stop if a change merely makes the metric easier to pass. | +| 12 | `#001` | A2 | Specialist β€” retrieval/ranking | After `#051`/`#023` and rollout approval | 0.5–1 day plus canary | Keep semantic reranking off unless an approved ambiguity comparison preserves 36/36, recall 1.0, zero per-case regressions, and shows measured gain; otherwise record keep-off and stop. | +| 13 | `#025` | A2 | Operator β€” Railway/GitHub/chat/Supabase | Next approved observability window | 1–3 hours/channel | Choose owned deployment, CI, ingestion, and SLO alerts; mock first, then one approved controlled provider event/channel. The merged Supabase trigger remains inert until its verified inputs are configured. Stop without an accountable responder. | +| 14 | `#064` | A2 | High β€” frontend/browser | After higher-acuity local fixes; before the release UI gate | 4–8 hours | Preserve the isolated dirty formulation/contrast patch, reconcile its intent against current `main`, and run focused Playwright plus `verify:ui`. Stop rather than overwriting unrelated work or weakening access-control assertions. | +| 15 | `#055` | A2 | Specialist release owner + Operator | Before next full-confidence release/handoff | 2–4 hours plus runtime | On one exact SHA, run local/provider gates, Firefox/WebKit, required hosted CI, and close actionable GitHub threads. Stop at first failure and rerun only the repaired smallest gate. | +| 16 | `#056` | A2 | Operator β€” Supabase/Railway + Specialist | After cost/ownership approval | 0.5–1 day | Provision isolated `Clinical KB Staging` with synthetic data and distinct secrets. Verify identity, schema, indexing, health, and data boundary; never copy production clinical documents. | +| 17 | `#057` | A2 | High β€” release/SRE + Operator | After `#056` | 2–4 hours plus soak | Run documented staging soak and rollback against an exact candidate. Retain latency/error/rollback evidence; stop on unsafe data, identity mismatch, or unowned rollback. | +| 18 | `#058` | A2 | Operator β€” production data + Specialist | Next approved production verification window | 30–60 min read-only; 1–2 hours if needed | Verify registry/differentials/medications are non-empty before writing; seed only confirmed gaps idempotently. Stop when healthy or owner/project identity is ambiguous. | +| 19 | `#011` | A3 | Operator β€” Supabase capacity | Immediately before first compute scale-up | 30–60 min plus observation | Switch Auth to percentage allocation, record before/after, and run approved advisor/health checks. Stop if no scale-up is planned. | +| 20 | `#017` | A3 | High β€” performance/browser | Before `#012`/`#013`/`#016`; approved live-site window | 1–2 hours | Capture reproducible mobile/desktop Lighthouse/Web-Vitals evidence and decide whether payload work is justified. Stop if metrics are acceptable or evidence is too noisy. | +| 21 | `#024` | A3 | High β€” Next.js/Playwright/WebKit | After `#051`/`#023` or real Safari reproduction | 0.5–1 day | Distinguish test interception from a Safari defect. Apply test-only correction only with a discriminating repro; keep access-control assertions meaningful. | +| 22 | `#033` | A3 | Specialist β€” prompt/source governance | After `#022` and `#051`/`#023` | 1–2 days plus approved eval | Design unknown-vs-adverse metadata wording and prompt tests. Require no supported-grounding drop and zero citation failures; stop on broad over-caveating or degradation. | +| 23 | `#037` | A3 | Operator β€” clinical/product + Standard | Next trust-policy review | 30–60 min; up to 0.5 day | Decide whether routine claims cap at medium trust. Record policy; if accepted, change only the flag/expectations and run focused tests. | +| 24 | `#012`, `#013`, `#016` | A3 | High β€” bundling/runtime performance | After `#017` or equivalent evidence | 0.5–2 days/route | Optimize only a production route with measured payload/render/motion harm. Require material gain plus focused, `verify:cheap`, and browser evidence; stop on small gain. | +| 25 | `#035` | A3 | Specialist β€” evidence rules | After a demonstrated missed conflict | 0.5–1 day design; code separate | Define a clinically reviewed conflict class with positive and negative fixtures. Stop if no bounded class can be shown; behavior change requires protected review. | +| 26 | `#027` | Optional | Operator β€” SRE/provider | When an owned external alert path is wanted | 1–2 hours | Decide vendor/cost/privacy/owner; if accepted, prove one non-PHI outage and recovery alert. Stop when no responder owns it. | +| 27 | `#028` | Optional | Specialist privacy/observability + Operator | After privacy/ownership/cost approval | 1–3 days | Define vendor/region/retention/redaction/sampling/source-map envelope before SDK work. Prove no clinical text, identifiers, or secrets leave; stop if unacceptable. | +| 28 | `#038` | Optional | High β€” product/design architecture | When a new comparison surface is approved | 0.5–1 day | Define a shared interaction contract without flattening mode-specific content. Stop when no concrete new surface exists. | +| 29 | `#040` | Optional | High β€” visual QA/accessibility | When baseline owner/update workflow exists | 1–2 days | Establish a small stable desktop/mobile/accessibility baseline set. Do not make it blocking if flake or maintenance cost outweighs detection value. | +| 30 | `#039` | Optional | High β€” frontend architecture | During a concrete catalogue-toolbar project | 0.5–1 day inventory; 1–3 days code | Converge only repeated toolbar behavior without flattening search semantics. Stop when there is no bounded implementation target. | +| 31 | `#063` | A3 | High β€” product architecture + privacy | Only when the product owner wants to evaluate the feature | 0.5–1 day | Write a product/privacy/persistence brief for β€œCurrent Clinical Work” before storage or UI implementation. Stop if demand or safe persistence cannot be established. | +| 32 | `#065` | A2 | High β€” document-viewer UI | Only when the user explicitly resumes the paused task | 0.5–1.5 days | Finish the compact source-text accordion, citation/search auto-open, print restoration, and 320/390/1280 px coverage. Keep the preserved branch untouched until explicit resume; no provider calls. | +| 33 | `#077` | A3 | High β€” workflow safety | Next workflow-hardening window | 1–2 hours | Add a cooperative primary-checkout write lease/check that prevents a second task from writing, switching, or synchronizing the canonical checkout while another owner or dirty state exists. Keep read-only work and independent feature worktrees unblocked. | +| 34 | `#078` | A3 | Standard β€” reconciliation tooling | After `#067`; before another broad reconciliation | 2–4 hours | Generate one deterministic, secret-safe evidence pack from the reconciliation lifecycle: disposition rows, operation markers, archive refs, bundle verification, hashes, worktree counts, and local/base equality. Never fetch, call providers, or delete implicitly. | +| 35 | `#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. | @@ -107,13 +106,12 @@ removed after current-main verification; it is not missing recommended work. | #001 | P2 | task | Semantic reranking still gated off | `RAG_SEMANTIC_RERANK_ENABLED=false` from PR #901. Do not enable until the provider-backed 36/36 retrieval-quality gate **and** an ambiguity-focused canary are explicitly approved and recorded. | `docs/process-hardening.md` (Semantic reranking rollout debt); PR #901 | 2026-07-21 | | #069 | P3 | task | Live-profile table-facts plpgsql+EXECUTE latency | Migration `20260724120000_table_facts_plpgsql_execute.sql` plus P3 follow-ups (`20260724130000_*`, `20260724130100_*`) are in PR #1133. Hosted apply is blocked in this environment (no `SUPABASE_DB_URL`; Supabase MCP `needsAuth`). A live `profile:retrieval --rpc match_document_table_facts_text --analyze` attempt returned `Unregistered API key` against the injected service-role secret. **Next:** operator applies the three pending migrations on the live target project, then re-runs approval-gated `profile:retrieval` / `explain_retrieval_rpc` and confirms ~70ms-class plans with no ranking change. **Stop** without mutating ranking. | session 2026-07-24 Database interface audit; PR #1133 | 2026-07-24 | | #053 | P1 | task | Execute cross-border privacy/legal package | Execute OpenAI and Railway DPAs; decide ZDR and Australian data residency; obtain prompt-cache behavior in writing; review subprocessors; obtain APP 8 and APP 5/1 counsel sign-off. Do not represent the release as privacy-approved or alter final public privacy wording before sign-off. | `docs/openai-cross-border-basis.md`; `docs/privacy-impact-assessment.md` | 2026-07-24 | -| #054 | P2 | task | Reconcile local and hosted secrets/config | **Local (2026-07-24):** `npm run check:local-presence` (+ optional `--fill`) presence-checks safety-identifier / query-hash / deep-probe (names + length buckets only), fails closed on ambiguous project identity, and fills only those three local HMAC/probe gaps into gitignored `.env.local`. Production-readiness now warns on the same local gaps. Provider keys and hosted Railway/GitHub stores were not touched. **Next (hosted/operator):** presence-check Railway runtime + GitHub repo secrets for query-hash, deep-probe (+ `PROD_HEALTH_URL` / ops-digest), service-role, OpenAI, and schedule/Vault settings; set only confirmed gaps with distinct per-environment values. Never record secret values. Provider reads/writes require approval. | `.env.example`; `scripts/check-local-presence.mjs`; production-readiness; `docs/operator-backlog.md` | 2026-07-24 | +| #054 | P2 | task | Reconcile local and hosted secrets/config | **Local (2026-07-24):** `npm run check:local-presence` (+ optional `--fill`) presence-checks safety-identifier / query-hash / deep-probe (names + length buckets only), fails closed on ambiguous project identity, and fills only those three local HMAC/probe gaps into gitignored `.env.local`. Production-readiness now warns on the same local gaps. Provider keys and hosted Railway/GitHub stores were not touched. **Next (hosted/operator):** presence-check Railway runtime + GitHub repo secrets for query-hash, deep-probe (+ `PROD_HEALTH_URL` / ops-digest), service-role, OpenAI, and schedule/Vault settings; set only confirmed gaps with distinct per-environment values. Never record secret values. Provider reads/writes require approval. | `.env.example`; `scripts/check-local-presence.mjs`; production-readiness; `docs/operator-backlog.md` | 2026-07-24 | | #055 | P2 | task | Run one exact-SHA full release and PR gate | Before the next full-confidence release/handoff, record the candidate/PR SHA and run the local/provider release gates, Firefox/WebKit, required hosted CI, and actionable GitHub review-thread closure once. Stop at the first actionable failure and rerun only the repaired smallest gate. | `docs/launch-operator-runbook.md`; `docs/codex-review-protocol.md` | 2026-07-24 | | #056 | P2 | task | Provision isolated staging environment | After explicit cost/ownership approval, provision `Clinical KB Staging` Supabase and Railway tiers with distinct secrets and synthetic/non-clinical data. Verify identity, schema, indexing, health, and the production-data boundary. | `docs/staging-setup.md`; `docs/operator-backlog.md` | 2026-07-24 | | #057 | P2 | task | Complete staging soak and rollback rehearsal | After #056, run the documented soak and rollback against an exact candidate; retain latency/error/rollback evidence. Stop on unsafe data, identity mismatch, or an unowned rollback decision. | `docs/launch-operator-runbook.md`; `docs/capacity-review.md` | 2026-07-24 | | #058 | P2 | task | Verify production content before any seed write | Against `the live target project`, verify registry, differentials, and medications surfaces are non-empty before writing. Seed only confirmed gaps idempotently with approved owner/project identity and confirmation flags. | `docs/launch-operator-runbook.md`; `docs/operator-backlog.md` | 2026-07-24 | | #005 | P3 | rec | `finalScore` saturates at clamp ceiling | Base + ~40 stacked boosts routinely exceed 1.0, so strong matches tie at 1.0 and order by an arbitrary `document_id` tiebreak. If ranking is ever revisited, break ties by the **pre-clamp** score rather than raising the `[0,1]` ceiling (downstream gates assume `[0,1]`). Ordering already sorts by the unbounded pre-clamp `rankScore` (`clinical-search.ts:1735,1927,1950-1955`), so the clamp confines only the reported confidence value, not result order. Not a defect on the current golden set; any change here is a protected RAG surface (canary required). | `docs/rag-hybrid-findings-and-todo.md` P1 item 4; `src/lib/clinical-search.ts:1735` | 2026-07-21 | -| #007 | P3 | rec | `/tools` vs `/?mode=tools` parallel Tools entry points | **Blocked 2026-07-24:** no product decision on the canonical Tools entry. Stop before nav/redirect/sitemap/reachability edits. Choose one: (A) `/?mode=tools` canonical β€” keep sidebar link, redirect `/tools` + `/applications` there (or drop standalone page), remove reachability allowlist; or (B) `/tools` canonical β€” point sidebar/nav at `/tools`, keep `/applications` β†’ `/tools`, align sitemap/reachability. Currently `/tools` (`ApplicationsLauncherPage`) has no inbound in-app link; sidebar Tools uses `/?mode=tools`; allowlisted in `tests/route-reachability.test.ts`. | `src/app/tools/page.tsx`; `src/app/applications/route.ts`; session 2026-07-24 | 2026-07-21 | | #009 | P3 | rec | Confirm `/api/jobs` is intentionally server/ops-only | No client `fetch()` reaches `/api/jobs` (only tests import it). Confirm it is a deliberate ops/manual surface; if abandoned, remove it. | `src/app/api/jobs/route.ts` | 2026-07-21 | | #010 | P3 | task | Un-built "Coming soon" controls across forms/favourites | ~10 disabled placeholders (forms refine/reset, favourites sort/add/new-set, move-to-set, remove-favourite) plus presentation Compact/Detailed density toggle (honest coming-soon placeholders; selected styling removed 2026-07-24). Correctly non-interactive β€” wire when the underlying features land. | `forms-search-results-page.tsx`; `favourites-hub.tsx`; `favourites-command-library-page.tsx`; `differential-presentation-workflow-page.tsx` | 2026-07-21 | | #011 | P3 | task | Auth DB-connection allocation is operator-only | Supabase Auth (GoTrue) is capped at ~10 absolute DB connections (Supabase perf advisor). Switch to **percentage-based** allocation in the Supabase **dashboard** before the first compute scale-up β€” **not settable via SQL/MCP** (operator-owned). Verify via a staging soak + an approval-gated read-only advisor re-check. | `docs/auth-connection-cap-runbook.md`; `docs/process-hardening.md` (Known follow-up debts) | 2026-07-21 | @@ -153,6 +151,7 @@ Move resolved rows here with the resolution date and a one-line outcome. Keep th | ID | Type | Summary | Outcome | Resolved | | ---- | ----- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | +| #007 | rec | `/tools` vs `/?mode=tools` parallel Tools entry points | Resolved as `/tools` canonical (PT-11 already documented on `/applications` redirect). Sidebar, appModeHomeHref, universal-search, prefetch, sitemap, and reachability now use `/tools`; `/?mode=tools` remains a dashboard-mode alias. Reachability allowlist entry removed. | 2026-07-24 | | #070 | issue | Presentation mobile tabs misroute Overview/Map/Related | Fixed in PR #1135: Overview/Map/Related deep-link to diagnosis `?tab=` sections; Compare stays on the presentation page. Regression in `tests/mobile-interaction-regressions.test.ts`. (Provisional PR-branch IDs `#068`–`#072` were renumbered after `main` claimed `#068`/`#069`.) | 2026-07-24 | | #071 | issue | Evidence/Clinical Notes Add fakes success without persistence | Fixed in PR #1135: sticky Add controls use the focusable coming-soon placeholder pattern instead of optimistic `setAdded(true)`. | 2026-07-24 | | #072 | issue | Tools hub exposes false Sort/More affordances | Fixed in PR #1135: Sort is a status label, More filter targets coordination/saved without a fake menu chevron, and the favourites shortcut is labelled Saved/Favourites. | 2026-07-24 | diff --git a/docs/site-map.md b/docs/site-map.md index 3a57a8dc1..8315ce173 100644 --- a/docs/site-map.md +++ b/docs/site-map.md @@ -53,7 +53,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` - `/specifiers` - Specifiers mode. Search kind: `specifiers`. Query example: `/specifiers?q=depressed+but+racing+thoughts&focus=1&run=1`. - `/formulation` - Formulation mode. Search kind: `formulation`. Query example: `/formulation?q=I+keep+going+over+it&focus=1&run=1`. - `/?mode=prescribing` - Medication mode. Search kind: `documents`. Query example: `/?mode=prescribing&q=acamprosate+renal+dose&focus=1&run=1`. -- `/?mode=tools` - Tools mode. Search kind: `tools`. Query example: `/?mode=tools&q=medications&focus=1&run=1`. +- `/tools` - Tools mode. Search kind: `tools`. Query example: `/tools?q=medications&focus=1&run=1`. - `/therapy-compass` - Therapy mode. Search kind: `tools`. Query example: `/therapy-compass?q=behavioural+activation&focus=1&run=1`. - `/factsheets` - Factsheets mode. Search kind: `tools`. Query example: `/factsheets/search?q=sertraline&focus=1&run=1`. @@ -71,7 +71,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check` | Specifiers | `/specifiers` | `/specifiers?q=depressed+but+racing+thoughts&focus=1&run=1` | `/specifiers/[slug]`, `/specifiers/builder`, `/specifiers/compare`, and `/specifiers/map`. | | Formulation | `/formulation` | `/formulation?q=I+keep+going+over+it&focus=1&run=1` | `/formulation/[slug]`, `/formulation/builder`, `/formulation/compare`, and `/formulation/map`. | | Medication | `/?mode=prescribing` | `/?mode=prescribing&q=acamprosate+renal+dose&focus=1&run=1` | `/medications/[slug]`; `/medications` redirects to medication mode. | -| Tools | `/?mode=tools` | `/?mode=tools&q=medications&focus=1&run=1` | Tool launcher and detail panels inside dashboard tools mode (`/?mode=tools`). | +| Tools | `/tools` | `/tools?q=medications&focus=1&run=1` | Canonical Tools launcher at `/tools` (PT-11). `/?mode=tools` remains a dashboard-mode alias. | ## Documents flow index diff --git a/docs/wiring-conventions.md b/docs/wiring-conventions.md index ac278f386..b64df3fcd 100644 --- a/docs/wiring-conventions.md +++ b/docs/wiring-conventions.md @@ -95,6 +95,7 @@ Intentional exceptions are documented, not silenced: ## Known wiring debts -Tracked in `docs/outstanding-issues.md` (`/issues`): the `/tools` vs `/?mode=tools` duplicate Tools -entry point, the unused `document-flow-routes.ts` href builders, the server-only `/api/jobs` endpoint, -and the un-built "coming soon" placeholders across forms/favourites. +Tracked in `docs/outstanding-issues.md` (`/issues`): the unused `document-flow-routes.ts` href +builders, the server-only `/api/jobs` endpoint, and the un-built "coming soon" placeholders across +forms/favourites. `#007` (`/tools` vs `/?mode=tools`) is resolved: `/tools` is the canonical entry +(PT-11); `/?mode=tools` remains a dashboard-mode alias. diff --git a/package-lock.json b/package-lock.json index 0db2b6389..e0beaad4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,7 +22,7 @@ "pdf-parse": "^2.4.5", "pdfjs-dist": "^6.1.200", "pdfkit": "^0.19.1", - "postcss": "^8.5.15", + "postcss": "^8.5.18", "react": "19.2.7", "react-dom": "19.2.7", "server-only": "^0.0.1", @@ -9155,9 +9155,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -10117,9 +10117,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -10136,7 +10136,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, diff --git a/package.json b/package.json index d86513d2b..ae153369c 100644 --- a/package.json +++ b/package.json @@ -198,14 +198,14 @@ "pdf-parse": "^2.4.5", "pdfjs-dist": "^6.1.200", "pdfkit": "^0.19.1", - "postcss": "^8.5.15", + "postcss": "^8.5.18", "react": "19.2.7", "react-dom": "19.2.7", "server-only": "^0.0.1", "zod": "^4.4.3" }, "overrides": { - "postcss": "^8.5.15", + "postcss": "$postcss", "esbuild": "0.28.1", "sharp": "0.35.3", "tmp": "^0.2.7", diff --git a/scripts/check-local-presence.mjs b/scripts/check-local-presence.mjs index b4d26e14d..292b1b89b 100644 --- a/scripts/check-local-presence.mjs +++ b/scripts/check-local-presence.mjs @@ -44,6 +44,8 @@ export const REPORT_ONLY_KEYS = [ "NEXT_PUBLIC_SUPABASE_URL", "SUPABASE_PROJECT_REF", "SUPABASE_PROJECT_NAME", + "SUPABASE_STAGING_PROJECT_REF", + "SUPABASE_STAGING_PROJECT_NAME", "NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY", "SUPABASE_SERVICE_ROLE_KEY", "OPENAI_API_KEY", @@ -87,10 +89,7 @@ export function parseEnvFile(text) { const key = line.slice(0, eq).trim(); if (!/^[A-Z][A-Z0-9_]*$/.test(key)) continue; let value = line.slice(eq + 1); - if ( - (value.startsWith('"') && value.endsWith('"')) || - (value.startsWith("'") && value.endsWith("'")) - ) { + if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } out[key] = value; diff --git a/scripts/generate-site-map.ts b/scripts/generate-site-map.ts index 2d6268159..159601dda 100644 --- a/scripts/generate-site-map.ts +++ b/scripts/generate-site-map.ts @@ -372,7 +372,7 @@ function renderModePageIndex() { mode: "Tools", home: appModeHomeHref("tools"), search: appModeHomeHref("tools", { query: "medications", focus: true, run: true }), - detail: "Tool launcher and detail panels inside dashboard tools mode (`/?mode=tools`).", + detail: "Canonical Tools launcher at `/tools` (PT-11). `/?mode=tools` remains a dashboard-mode alias.", }, ]); } diff --git a/src/components/clinical-dashboard/ClinicalSidebar.tsx b/src/components/clinical-dashboard/ClinicalSidebar.tsx index 80a719964..1a8393420 100644 --- a/src/components/clinical-dashboard/ClinicalSidebar.tsx +++ b/src/components/clinical-dashboard/ClinicalSidebar.tsx @@ -80,7 +80,7 @@ const sidebarToolItems = [ { id: "specifiers", label: "Specifiers", icon: Tags, href: "/specifiers" }, { id: "formulation", label: "Formulation", icon: Network, href: "/formulation" }, { id: "prescribing", label: "Medication", icon: Pill, href: "/?mode=prescribing" }, - { id: "tools", label: "Tools", icon: Wrench, href: "/?mode=tools" }, + { id: "tools", label: "Tools", icon: Wrench, href: "/tools" }, { id: "therapy-compass", label: appModeDefinition("therapy-compass").label, diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index 4a1461679..0c787cffd 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -442,7 +442,7 @@ function GlobalStandaloneSearchShellClient({ }, [recentQueriesOwnerId]); function prefetchApplications() { - router.prefetch("/?mode=tools"); + router.prefetch("/tools"); if (favouritesAccessible) router.prefetch("/favourites"); router.prefetch("/differentials"); router.prefetch("/dsm"); diff --git a/src/components/clinical-dashboard/use-dashboard-shell-actions.ts b/src/components/clinical-dashboard/use-dashboard-shell-actions.ts index d456a6e6d..ea891ae68 100644 --- a/src/components/clinical-dashboard/use-dashboard-shell-actions.ts +++ b/src/components/clinical-dashboard/use-dashboard-shell-actions.ts @@ -92,7 +92,7 @@ export function useDashboardShellActions(options: { }, [closeTransientSurfaces, openAccountSetup, setSettingsOpen, signedIn]); const prefetchApplications = useCallback(() => { - prefetch("/?mode=tools"); + prefetch("/tools"); if (favouritesAccessible) prefetch("/favourites"); prefetch("/differentials"); }, [favouritesAccessible, prefetch]); diff --git a/src/lib/app-modes.ts b/src/lib/app-modes.ts index c28b9b68b..7140bceff 100644 --- a/src/lib/app-modes.ts +++ b/src/lib/app-modes.ts @@ -291,7 +291,9 @@ export const appModeDefinitions = [ id: "tools", label: "Tools", description: "Clinical tools and applications", - href: "/?mode=tools", + // PT-11: standalone /tools is the canonical entry; /?mode=tools remains a + // dashboard-mode alias for bookmarks and deep links. + href: "/tools", search: { kind: "tools", placeholder: "Search tools...", @@ -396,6 +398,7 @@ const namespaceIsolatedModes = new Set([ "formulation", "therapy-compass", "factsheets", + "tools", ]); export function appModeHomeHref(modeId: AppModeId, options: SearchNavigationOptions = {}) { diff --git a/src/lib/universal-search.ts b/src/lib/universal-search.ts index 4ad0af773..7b1b98bb4 100644 --- a/src/lib/universal-search.ts +++ b/src/lib/universal-search.ts @@ -739,6 +739,6 @@ export function universalSearchViewAllHref(domain: UniversalSearchDomain, query: case "therapies": return `/therapy-compass/search?q=${encodeURIComponent(query)}&run=1`; case "tools": - return `/?mode=tools&q=${encodeURIComponent(query)}&run=1`; + return `/tools?q=${encodeURIComponent(query)}&run=1`; } } diff --git a/tests/app-modes.test.ts b/tests/app-modes.test.ts index 22cacfa5a..c7acaf64f 100644 --- a/tests/app-modes.test.ts +++ b/tests/app-modes.test.ts @@ -229,7 +229,7 @@ describe("app mode search contract", () => { "/?mode=prescribing&q=acamprosate+renal+dose", ); expect(appModeHomeHref("tools", { query: " medications ", run: true, focus: true })).toBe( - "/?mode=tools&q=medications&focus=1&run=1", + "/tools?q=medications&focus=1&run=1", ); }); diff --git a/tests/local-presence.test.ts b/tests/local-presence.test.ts index 878ca55d4..5d913ca06 100644 --- a/tests/local-presence.test.ts +++ b/tests/local-presence.test.ts @@ -13,7 +13,7 @@ import { describe("check-local-presence", () => { it("parses env assignments without exposing values in helpers", () => { const parsed = parseEnvFile( - ['# comment', 'RAG_QUERY_HASH_SECRET="abc"', "OPENAI_API_KEY=xyz", "not valid", ""].join("\n"), + ["# comment", 'RAG_QUERY_HASH_SECRET="abc"', "OPENAI_API_KEY=xyz", "not valid", ""].join("\n"), ); expect(parsed.RAG_QUERY_HASH_SECRET).toBe("abc"); expect(parsed.OPENAI_API_KEY).toBe("xyz"); @@ -61,15 +61,9 @@ describe("check-local-presence", () => { const assessment = assessLocalPresence({}); expect(assessment.fillable.every((row) => row.status === "gap")).toBe(true); expect(FILLABLE_LOCAL_SECRETS.map((s) => s.name)).toEqual( - expect.arrayContaining([ - "OPENAI_SAFETY_IDENTIFIER_SECRET", - "RAG_QUERY_HASH_SECRET", - "HEALTH_DEEP_PROBE_SECRET", - ]), - ); - expect(REPORT_ONLY_KEYS).toEqual( - expect.arrayContaining(["SUPABASE_SERVICE_ROLE_KEY", "OPENAI_API_KEY"]), + expect.arrayContaining(["OPENAI_SAFETY_IDENTIFIER_SECRET", "RAG_QUERY_HASH_SECRET", "HEALTH_DEEP_PROBE_SECRET"]), ); + expect(REPORT_ONLY_KEYS).toEqual(expect.arrayContaining(["SUPABASE_SERVICE_ROLE_KEY", "OPENAI_API_KEY"])); expect(assessment.reportOnly.every((row) => row.fillable === false)).toBe(true); }); @@ -82,11 +76,7 @@ describe("check-local-presence", () => { return `local-dev-secret-${n}-`.padEnd(64, "x"); }, }); - expect(filled).toEqual([ - "OPENAI_SAFETY_IDENTIFIER_SECRET", - "RAG_QUERY_HASH_SECRET", - "HEALTH_DEEP_PROBE_SECRET", - ]); + expect(filled).toEqual(["OPENAI_SAFETY_IDENTIFIER_SECRET", "RAG_QUERY_HASH_SECRET", "HEALTH_DEEP_PROBE_SECRET"]); expect(text).toContain("RAG_PROVIDER_MODE=offline"); expect(text).toContain("OPENAI_SAFETY_IDENTIFIER_SECRET="); expect(text).toContain("RAG_QUERY_HASH_SECRET="); diff --git a/tests/route-reachability.test.ts b/tests/route-reachability.test.ts index f9ff1fea5..27ef8f372 100644 --- a/tests/route-reachability.test.ts +++ b/tests/route-reachability.test.ts @@ -29,10 +29,6 @@ const srcRoot = path.join(repoRoot, "src"); /** Intentionally-unlinked static page routes, each with the reason it is exempt. */ const REACHABILITY_ALLOWLIST = new Map([ - [ - "/tools", - "Orphan parallel to /?mode=tools; nothing links to the standalone page. Tracked as issue #007 (decide the canonical Tools entry point). Reachable via URL and the /applications redirect.", - ], [ "/documents/source", "Legacy compatibility redirect target reached by external/legacy deep links, not in-app navigation (frontend-architecture.md).", diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index 20fdd89d6..2f73fb8e3 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -1064,7 +1064,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await expandSidebar.click(); await expect(sidebar).toBeVisible(); await expect(sidebar.getByRole("link", { name: "View tools" })).toHaveCount(0); - await expect(sidebar.getByRole("link", { name: "Tools", exact: true })).toHaveAttribute("href", "/?mode=tools"); + await expect(sidebar.getByRole("link", { name: "Tools", exact: true })).toHaveAttribute("href", "/tools"); await expect(sidebar.getByTestId("sidebar-account-settings")).toHaveAccessibleName( /G Guest Not signed in\. Set up workspace/, ); @@ -1103,7 +1103,7 @@ test.describe("Clinical KB UI smoke coverage", () => { { name: "Services", href: "/services" }, // The rail speaks the catalogue-maturity badge as part of the Forms name. { name: "Forms (Early access)", href: "/forms" }, - { name: "Tools", href: "/?mode=tools" }, + { name: "Tools", href: "/tools" }, { name: "Therapy mode", href: "/therapy-compass" }, // Demo mode still exposes Favourites via the account-library rail entry. { name: "Favourites", href: "/favourites" }, From 162f64cfdb475262d5f8534313397c3e895457d4 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 17:55:18 +0000 Subject: [PATCH 04/12] Remove stale PR_POLICY_BODY leftover and record review. Stop Sync PR policy from overwriting PR descriptions with unrelated search-performance text, and append the local review ledger outcome for this branch. --- PR_POLICY_BODY.md | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 PR_POLICY_BODY.md diff --git a/PR_POLICY_BODY.md b/PR_POLICY_BODY.md deleted file mode 100644 index 80864e873..000000000 --- a/PR_POLICY_BODY.md +++ /dev/null @@ -1,33 +0,0 @@ -## Summary - -- Fix cross-mode search performance findings: prescribing catalogue debounce/abort/`fields=index`, differentials abort/debounce, universal documents typeahead soft-timeout (750ms), shared `(search-app)` shell to avoid composer remount, and Answer rate-limit in-memory fallback outside production. -- Fix Bugbot regressions: shared-shell pathname navigation (`/services` β†’ `/dsm`) syncs `searchMode` during render (no stale-mode paint) even when the query string is unchanged; extracted ClinicalDashboard lazy imports to stay under the maintainability budget. - -RAG impact: no retrieval behaviour change β€” typeahead documents domain timeout and shell URL sync only; ranking formulas and full `/api/search` retrieval path unchanged. - -## Verification - -- [x] `npm run verify:pr-local` β€” focused Vitest on touched sources (362) plus api-rate-limit / search-shell / universal / route / site-map suites green; `docs:check-index` OK -- [x] UI verification not run: full `verify:ui` not required for this pass; mode-home smoke via `npm run ensure` returned HTTP 200 for `/`, `/services`, `/dsm`, `/documents/search`, `/therapy-compass`, `/?mode=prescribing`, and `/api/answer/stream` returned 200 after the rate-limit fallback fix -- Verification not run: `eval:retrieval:latency` / soak / live OpenAI canary β€” approval-gated provider work; not needed for timeout-only typeahead change - -## Risk and rollout - -- Risk: medium β€” shared layout remount change and rate-limit fallback behaviour in non-production; production Answer/upload still fail closed when the durable limiter is unavailable -- Rollback: revert this PR; mode routes return to per-segment `GlobalSearchShell` layouts and prior timeout/fallback behaviour -- Provider or production effects: None - -## Clinical Governance Preflight - -- [x] Source-backed claims still require linked source verification before clinical use -- [x] No patient-identifiable document workflow was introduced or expanded without explicit governance approval -- [x] Supabase target remains `[REDACTED]` (`[REDACTED]`) -- [x] Service-role keys and private document access remain server-only -- [x] Demo/synthetic content remains clearly separated from real clinical sources -- [x] Source metadata, review status, and outdated/unknown-source behavior remain conservative -- [x] Deployment classification/TGA SaMD impact was checked when clinical decision-support behavior changed - -## Notes - -- Prescribing list rows keep the full catalogue payload so Safety/Monitoring filters and patient alerts still see section-derived signals; keystroke storms are controlled by debounce + abort. `fields=index` remains for identity-only consumers (cross-mode links). -- Live hybrid RPC cold tails remain a separate approval-gated follow-up. From 236d5f8a4b3b86d847335bca26256266271d9fa3 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 24 Jul 2026 17:56:03 +0000 Subject: [PATCH 05/12] docs: record PR 1178 review ledger for local-presence tip --- docs/branch-review-ledger.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index a694ec191..42a664e43 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -760,3 +760,6 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-25 | cursor/pr-babysit-bugbot-agents-6c52 (PR #1167) | ee44812aae9dad1973d8302eba5bfca5000dffb6 | Open-PR maintenance: review-thread fixes | Before: 8 unresolved Codex/CodeRabbit threads; branch current with main. After: target-head pinning, fresh-main verification, exact `cursor[bot]` identity checks, explicit mutation/provider authorization, direct reply-then-resolve semantics, and no-op ledger bookkeeping are documented. | Prettier check on both agent files pass; `git diff --check` pass; GitHub author probe confirmed `cursor[bot]` account type `Bot`; no provider-backed checks run. | | 2026-07-25 | codex/fix-merge-conflicts-and-ci-on-open-prs (PR #1170) | e979fc892f11a17b1a8f2ef1ab058ef40d629182 | Open-PR maintenance: CI fix + threads + drift | Before: Static PR checks failed because route-group moves made nine valid legacy `src/app/*` documentation references appear missing; 0 unresolved threads; branch already contained current main. After: docs-link resolution checks known App Router route groups and the focused failure is fixed. | `node scripts/check-docs-links.mjs` pass (1162 references); Prettier check pass; `git diff --check` pass; no provider-backed checks run. | | 2026-07-25 | cursor/search-performance-review-4ee9 (PR #1134) | 692834a86e612cc8b311dc6895e007f182f5c5b8 | Open-PR maintenance: superseded docs-link thread and clean main sync | Before: branch was behind current main with one outdated docs-link thread; its product tree already matched main. After: merged current main cleanly and verified the route-group-aware docs-link fix now covers legacy route references. RAG impact: no retrieval behaviour change β€” history sync and docs tooling verification only. | `node scripts/check-docs-links.mjs` pass (1154 references); clean merge-tree; no live RAG canary or provider-backed check run. | + +| 2026-07-24 | PR #1178 / `cursor/local-presence-054-7cf3` | `c1b462a0d2db74b4a9e2f7136c33bf075ab52c7e` | Local presence #054 + Tools canonical #007 + CI remediation | FINDINGS FIXED. P1 Prettier on presence files formatted. P1 Safety audit postcss<=8.5.17 cleared by bumping to ^8.5.18/$postcss (8.5.23). P1 stale PR_POLICY_BODY.md removed so Sync PR policy no longer overwrites this PR with unrelated search-performance text. #007 implemented as `/tools` canonical (PT-11); `/?mode=tools` alias retained. No P0. Residual: hosted #054 secrets remain operator-owned. | Focused Vitest 70/70; verify:cheap 374/3326; npm audit high clean; check:local-presence ok. No hosted secret/provider mutations. | +| 2026-07-24 | PR #1178 / `cursor/local-presence-054-7cf3` | `162f64cfdb475262d5f8534313397c3e895457d4` | Local presence #054 + Tools canonical #007 + CI remediation (supersedes prior tip review) | Same findings fixed on tip including PR_POLICY_BODY.md deletion commit. No new high-confidence defects. Residual: hosted #054. | verify:cheap green on prior tip; tip adds leftover PR_POLICY_BODY removal + this ledger row. | From b438cd872286c831c6d9c8db49b017745f98abcc Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 04:26:18 +0800 Subject: [PATCH 06/12] fix(ci): register local presence check --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ce7229033..91583a1d4 100644 --- a/package.json +++ b/package.json @@ -180,6 +180,7 @@ "workflow:lifecycle": "node scripts/productivity-workflow.mjs lifecycle", "skills": "node scripts/list-database-skills.mjs", "check:skills": "node scripts/list-database-skills.mjs --check", + "check:local-presence": "node scripts/check-local-presence.mjs", "check:drift": "node scripts/run-tsx.mjs scripts/check-drift.ts", "check:migration-history": "node scripts/run-tsx.mjs scripts/check-migration-history-alignment.ts", "drift:manifest": "node scripts/run-tsx.mjs scripts/generate-drift-manifest.ts", From fa75ba3194b02efa3034f9d79da81e8952d02825 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 04:26:39 +0800 Subject: [PATCH 07/12] docs: record PR 1178 maintenance --- 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 dd8b13409..b1ed44c8c 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -794,3 +794,4 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-24 | open-PR conflict sync (22 PRs) | multi-head | Conflict resolution pass | Before: all 22 open PRs behind/dirty vs main (several CONFLICTING/DIRTY). After: merged origin/main into every open head; all pushes OK; merge-tree classified 22/22 clean. | merge origin/main per branch; check:branch-review-ledger on #1172; no provider-backed checks run | | 2026-07-24 | cursor/pr-queue-hygiene-72ec | pending-push | PR queue hygiene | Add pr-branch-sync workflow + sync:pr-branches helper; bump postcss to clear npm audit high; document anti-churn guidance in AGENTS/process-hardening/pr-babysit/run-pr. | check:github-actions PASS; docs:check-scripts/index PASS; vitest sync-open-pr-branches 3/3; npm audit high clean; no provider-backed checks run | | 2026-07-24 | codex/apply-phone-layout-to-all-home-pages (PR #1124) | pending | Babysit: ledger dedupe + merge readiness | Before: Static PR failed on exact duplicate ledger rows after main sync. After: removed duplicate rows; squash auto-merge armed. | check:branch-review-ledger PASS; no provider-backed checks run | +| 2026-07-25 | cursor/local-presence-054-7cf3 (PR #1178) | b438cd872286c831c6d9c8db49b017745f98abcc | Run PR sweep: CI fix + threads + drift | Before: GitHub reported DIRTY; Static PR checks found three stale `npm run check:local-presence` references because the implemented script was not registered; Production UI had one focus-restoration failure after 266 passes; 0 unresolved threads. After: merged current `origin/main` cleanly and registered the missing local script, so all 348 docs script references resolve. | `node scripts/check-docs-script-refs.mjs` pass; Prettier and `git diff --check` pass; focused Vitest/UI rerun deferred while another worktree owns the heavyweight lock; environment-reading presence mode and provider-backed checks not run. | From 0ac4f7de4db6d0915d7b6f87ab2c223191fbd7a9 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 15:47:01 +0800 Subject: [PATCH 08/12] fix(docs): resolve codebase-index composer placement merge Co-authored-by: Cursor --- docs/codebase-index.md | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/docs/codebase-index.md b/docs/codebase-index.md index d016e669f..e6ac8cce0 100644 --- a/docs/codebase-index.md +++ b/docs/codebase-index.md @@ -303,15 +303,8 @@ sequenceDiagram One shared composer (`master-search-header.tsx`) serves every mode. Placement: -<<<<<<< HEAD - -- **Mode homes** (`/services`, `/forms`, `/favourites`, `/differentials`, `/formulation`, `/tools`, and dashboard homes): inline in the hero via the `mode-home-composer-slot` portal, on phone and tablet+ alike. (`/applications` is a redirect to `/tools`, not a composer surface.) -- **Information (detail) pages**: catalogue/record routes under each mode (`/services/[slug]`, `/forms/[slug]`, `/medications/[slug]`, `/specifiers/[slug]`, `/formulation/[slug]`, `/factsheets/[slug]`, `/dsm/diagnoses/[slug]`, …). Route detection: `src/lib/information-pages.ts` (`isInformationPage`). Shared outer chrome: `src/components/information-page-shell.tsx` (`InformationPageShell`, breadcrumbs, optional footer). Specifier/formulation mode shells re-export that primitive. Intentional opt-outs: document viewer, therapy-compass CSS workspace, differentials presentation workflow. - \======= - **Mode homes** (`/services`, `/forms`, `/favourites`, `/differentials`, `/dsm`, `/specifiers`, `/formulation`, `/factsheets`, `/therapy-compass`, `/tools`, and dashboard homes): inline in the hero via the `mode-home-composer-slot` portal, on phone and tablet+ alike. (`/applications` is a redirect to `/tools`, not a composer surface.) - -> > > > > > > origin/main - +- **Information (detail) pages**: catalogue/record routes under each mode (`/services/[slug]`, `/forms/[slug]`, `/medications/[slug]`, `/specifiers/[slug]`, `/formulation/[slug]`, `/factsheets/[slug]`, `/dsm/diagnoses/[slug]`, …). Route detection: `src/lib/information-pages.ts` (`isInformationPage`). Shared outer chrome: `src/components/information-page-shell.tsx` (`InformationPageShell`, breadcrumbs, optional footer). Specifier/formulation mode shells re-export that primitive. Intentional opt-outs: document viewer, therapy-compass CSS workspace, differentials presentation workflow. - **Result and detail views**: fixed bottom dock on phone (compact variant on submitted searches), sticky top from `sm` up. - **Results routing**: standalone routes own their submitted searches via `?q=…&run=1` (`/services` β†’ `ServicesNavigatorPage`, `/forms` β†’ `FormsSearchResultsPage`, `/differentials` β†’ `DifferentialsHome` results view, `/formulation` β†’ local mechanism results, `/favourites` filters the command library in place). Answer, Documents, and Prescribing submitted searches render inside `ClinicalDashboard` β€” intentional, since they need retrieval/answer state. `/?mode=favourites` redirects to `/favourites`; `/?mode=differentials` redirects to `/differentials`; `/?mode=dsm` redirects to `/dsm`; `/?mode=specifiers` redirects to `/specifiers`; `/?mode=formulation` redirects to `/formulation`. - **Intentionally composer-free routes**: `/differentials/presentations/*` (comparison workflow owns its chrome), `/documents/[id]` viewer (has its own in-document ask composer), `/documents/source/*` (document flow owns mobile chrome). Do not re-flag these in search-consistency audits. From 88f717465219a1f76c0999411616bb80fbc6ba96 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:07:57 +0800 Subject: [PATCH 09/12] fix(test): expect canonical /tools href in favourites rail gate Co-authored-by: Cursor --- tests/favourites-auth-gate.dom.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/favourites-auth-gate.dom.test.tsx b/tests/favourites-auth-gate.dom.test.tsx index abc7b2b88..566817b3c 100644 --- a/tests/favourites-auth-gate.dom.test.tsx +++ b/tests/favourites-auth-gate.dom.test.tsx @@ -99,7 +99,7 @@ describe("favourites auth gate DOM", () => { { name: "Services", href: "/services" }, { name: "Medications", href: "/?mode=prescribing" }, { name: "Factsheets", href: "/factsheets" }, - { name: "Tools", href: "/?mode=tools" }, + { name: "Tools", href: "/tools" }, ]); expect(screen.queryByRole("link", { name: "Favourites" })).toBeNull(); expect(screen.queryByRole("button", { name: /^Guide & help$/ })).toBeNull(); From c20b3489d5daf58a896941e25206f5a0e70f86da Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 16:22:39 +0800 Subject: [PATCH 10/12] fix(ci): do not block audit on script-only package.json edits Keep blocking npm audit for package-lock/.npmrc only. Update phone Tools mode-menu smoke to expect canonical /tools navigation (PT-11). Co-authored-by: Cursor --- scripts/ci-change-scope.mjs | 15 +++++++++------ tests/ui-smoke.spec.ts | 5 +++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/scripts/ci-change-scope.mjs b/scripts/ci-change-scope.mjs index cdd87d92b..24a20ba3c 100644 --- a/scripts/ci-change-scope.mjs +++ b/scripts/ci-change-scope.mjs @@ -180,11 +180,12 @@ const staticConfigPatterns = [ "vitest.config.mts", ]; -// Dependency-manifest changes are the only moment a PR can introduce a new -// (possibly-vulnerable) dependency, so `npm audit` blocks the merge gate only -// when one of these changes; otherwise the audit runs advisory. Scheduled/ -// full-run passes resolve to the sentinel below, which includes these paths. -const lockfilePatterns = ["package.json", "package-lock.json", ".npmrc"]; +// Dependency lockfile / npm config changes are the only moment a PR can introduce +// a new (possibly-vulnerable) dependency for `npm ci`, so `npm audit` blocks the +// merge gate only then; otherwise the audit runs advisory. Scheduled/full-run +// passes resolve to the sentinel below, which includes package-lock.json. +// Script-only `package.json` edits do not trip blocking audit (no lock churn). +const lockfilePatterns = ["package-lock.json", ".npmrc"]; function classify(files) { const normalized = [...new Set(files.map(normalizePath).filter(Boolean))].sort(); @@ -504,7 +505,9 @@ function selfTest() { container_changed: true, workflow_changed: false, build_changed: true, - lockfile_changed: true, + // Script/metadata edits to package.json alone do not introduce dependencies; + // blocking audit still keys off package-lock.json / .npmrc. + lockfile_changed: false, }); assertScope("lockfile", ["package-lock.json"], { lockfile_changed: true, diff --git a/tests/ui-smoke.spec.ts b/tests/ui-smoke.spec.ts index e1ce4677e..9e554a2cb 100644 --- a/tests/ui-smoke.spec.ts +++ b/tests/ui-smoke.spec.ts @@ -1403,6 +1403,7 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(appModeMenu.getByRole("menuitemradio", { name: /^Medication\b/ })).toBeAttached(); // Scroll the sheet body so a lower mode is interactable, then select it. + // Tools is canonical at /tools (PT-11); selecting it navigates off the dashboard. const toolsMode = appModeMenu.getByRole("menuitemradio", { name: /^Tools\b/ }); await toolsMode.scrollIntoViewIfNeeded(); await expect(toolsMode).toBeVisible(); @@ -1410,8 +1411,8 @@ test.describe("Clinical KB UI smoke coverage", () => { await expect(modeSheet).toHaveCount(0); await expect(appModeMenu).toHaveCount(0); - await expect(page.getByRole("button", { name: "Mode Tools" })).toBeVisible(); - await expect(page.getByRole("button", { name: "Mode Tools" })).toBeFocused(); + await expect(page).toHaveURL(/\/tools(?:\?|$)/); + await expect(page.getByRole("heading", { name: /tools/i }).first()).toBeVisible(); await expectNoPageHorizontalOverflow(page); }); From 5b3d98d5b5412998e42669b94894361649cc9dc4 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:24:02 +0800 Subject: [PATCH 11/12] fix(local-presence): strip stale short env assignments on fill Avoid duplicate KEY=value lines when regenerating below-minLength secrets. Co-authored-by: Cursor --- scripts/check-local-presence.mjs | 21 +++++++++++++++++---- tests/local-presence.test.ts | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/scripts/check-local-presence.mjs b/scripts/check-local-presence.mjs index 292b1b89b..9f847b37c 100644 --- a/scripts/check-local-presence.mjs +++ b/scripts/check-local-presence.mjs @@ -242,8 +242,9 @@ export function generateLocalSecret(bytes = 32) { } /** - * Merge fillable gaps into `.env.local` text. Never overwrites an existing - * non-empty assignment for a key. Returns which keys were filled (names only). + * Merge fillable gaps into `.env.local` text. Skips keys that already meet + * minLength. For present-but-too-short keys, removes stale KEY=value lines + * before appending the regenerated assignment. Returns filled key names only. */ export function mergeFillIntoEnvLocal(existingText, gaps, { generate = generateLocalSecret } = {}) { const existing = parseEnvFile(existingText || ""); @@ -265,8 +266,20 @@ export function mergeFillIntoEnvLocal(existingText, gaps, { generate = generateL return { text: existingText || "", filled }; } - const base = (existingText || "").replace(/\s*$/, ""); - const next = `${base ? `${base}\n\n` : ""}# --- check:local-presence --fill (${new Date().toISOString().slice(0, 10)}) ---\n${additions.join("\n")}\n`; + const filledSet = new Set(filled); + const stripped = (existingText || "") + .split(/\r?\n/) + .filter((rawLine) => { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) return true; + const eq = line.indexOf("="); + if (eq <= 0) return true; + const key = line.slice(0, eq).trim(); + return !filledSet.has(key); + }) + .join("\n") + .replace(/\s*$/, ""); + const next = `${stripped ? `${stripped}\n\n` : ""}# --- check:local-presence --fill (${new Date().toISOString().slice(0, 10)}) ---\n${additions.join("\n")}\n`; return { text: next, filled }; } diff --git a/tests/local-presence.test.ts b/tests/local-presence.test.ts index 5d913ca06..dc11f8fb9 100644 --- a/tests/local-presence.test.ts +++ b/tests/local-presence.test.ts @@ -87,4 +87,22 @@ describe("check-local-presence", () => { }); expect(again.filled).toEqual([]); }); + + it("replaces present-but-too-short assignments instead of duplicating keys", () => { + const existing = [ + "RAG_PROVIDER_MODE=offline", + "OPENAI_SAFETY_IDENTIFIER_SECRET=short", + "RAG_QUERY_HASH_SECRET=also-short", + ].join("\n"); + const { text, filled } = mergeFillIntoEnvLocal(existing, FILLABLE_LOCAL_SECRETS, { + generate: () => "regenerated-local-secret-value-".padEnd(64, "z"), + }); + expect(filled).toEqual(["OPENAI_SAFETY_IDENTIFIER_SECRET", "RAG_QUERY_HASH_SECRET", "HEALTH_DEEP_PROBE_SECRET"]); + expect(text).toContain("RAG_PROVIDER_MODE=offline"); + expect(text).not.toMatch(/OPENAI_SAFETY_IDENTIFIER_SECRET=short/); + expect(text).not.toMatch(/RAG_QUERY_HASH_SECRET=also-short/); + expect([...text.matchAll(/^OPENAI_SAFETY_IDENTIFIER_SECRET=/gm)]).toHaveLength(1); + expect([...text.matchAll(/^RAG_QUERY_HASH_SECRET=/gm)]).toHaveLength(1); + expect(parseEnvFile(text).OPENAI_SAFETY_IDENTIFIER_SECRET).toMatch(/^regenerated-local-secret-value-/); + }); }); From 9135891bfd194394549cb480a7ec86de12b23ee7 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sat, 25 Jul 2026 17:42:23 +0800 Subject: [PATCH 12/12] fix(ui): keep Sources sheet autofocus against focus=1 reclaim Extend Sheet open-focus retries and skip composer focus=1 hydration while a modal dialog is open. Co-authored-by: Cursor --- src/components/clinical-dashboard/global-search-shell.tsx | 4 ++++ src/components/ui/sheet.tsx | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/components/clinical-dashboard/global-search-shell.tsx b/src/components/clinical-dashboard/global-search-shell.tsx index d324676eb..991c06172 100644 --- a/src/components/clinical-dashboard/global-search-shell.tsx +++ b/src/components/clinical-dashboard/global-search-shell.tsx @@ -344,6 +344,10 @@ function GlobalStandaloneSearchShellClient({ // Guard both: open menu DOM (activeElement is often mid-transition) and // any intentional focus already moved off the composer. if (document.getElementById("app-mode-menu")) return; + // Do not reclaim composer focus while a modal Sheet is open (Sources / + // Guide / filters). The focus=1 hydration retry otherwise races sheet + // autofocus and can leave the Find field unfocused in UI smoke. + if (document.querySelector('[role="dialog"][aria-modal="true"]')) return; const active = document.activeElement; if (active instanceof HTMLElement && active !== document.body && active !== inputRef.current) { return; diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index c4a0ea333..aa8c4c6c1 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -228,13 +228,14 @@ export function Sheet({ }; focusIfNeeded(); // Retries cover sibling-sheet teardown and late-mounted autofocus inputs - // (UtilityDrawer media sync / deferred drawer children). + // (UtilityDrawer media sync / deferred drawer children). Keep retrying long + // enough to outlast focus=1 hydration (rAF + ~300ms) and composer reclaim. let attempts = 0; const retryTimer = window.setInterval(() => { attempts += 1; const focusTarget = focusIfNeeded(); if ( - attempts >= 8 || + attempts >= 40 || !isTopmostSheet(sheetId) || (focusTarget != null && document.activeElement === focusTarget) ) {