Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/sast.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ jobs:
# Runs OSS-engine only — no Semgrep account or token required.
semgrep:
name: Semgrep
continue-on-error: true
runs-on: ubuntu-24.04
timeout-minutes: 15
container:
Expand All @@ -36,6 +35,7 @@ jobs:
persist-credentials: false

- name: Semgrep scan
continue-on-error: true
# --error fails the job on any ERROR-severity finding; --oss-only keeps
# it token-free; --metrics off sends no telemetry. Named registry rules
# remain advisory because they can change independently of this repository.
Expand Down
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ Use this ledger to prevent repeated branch and PR reviews when the reviewed HEAD
| 2026-07-11 | codex/repository-review-remediation | 70ec6409a11a85e1678eb4b320519624673a94a0 | comprehensive repository report remediation | Revalidated all 17 findings from the 2026-07-09 comprehensive review. Remediated the current workflow injection, document scope, numeric faithfulness, ingestion lease/ownership, transactional enrichment replacement, request and upload budgets, browser identity isolation, PHI retention, public DTO, cache cancellation/versioning, evidence labelling, modal focus, misleading controls, telemetry, orphan-module issues, and two server/client loading-boundary failures exposed during browser QA. The runbook filename was already fixed on the reviewed head. | TypeScript, lint, focused Vitest (68/68), full offline Vitest (1,607/1,607; 1 skipped), production build, client-bundle secret scan, Docker schema replay and regenerated drift manifest, isolated full migration reset, local lease-reclaim concurrency proof, cache/enrichment SQL smoke, configured production-readiness (`READY`), Chromium document-scope/modal QA (4/4), manual disabled-control accessibility snapshots, and `git diff --check` passed. Read-only live drift found 26 unexpected differences, including the three unapplied remediation functions; no live mutation was performed. |
| 2026-07-11 | codex/responsive-accessibility-audit | 66883b7c86f606e617db4bee2bab6f85fff59bdc | responsive and accessibility audit | P2 fixed: the mobile expandable clinical table no longer wraps semantic table content in a duplicate ARIA button, and its full-screen dialog now traps keyboard focus while preserving Escape dismissal and focus return. Added responsive ARIA and focus regression coverage. No additional high-confidence responsive or accessibility defect was reproduced across audited primary app modes and 320px-1440px widths. | Multi-width DOM/geometry/contrast audit; a11y media (2/2); overlap (12/12); table Vitest (6/6); TypeScript; lint/static checks; full Vitest (1,598 passed, 1 skipped); `npm run verify:ui` (132/132); Prettier; `git diff --check`. Provider checks skipped. |
| 2026-07-13 | codex/repository-review-remediation | b72cefd2f5c0da79788cc0f8d0d40837c711ae92 | live-drift reconciliation and release review | Reconciled production migration history and live-ahead governance/retrieval definitions without mutating live; removed the migration-version collision; made captured OUT-signature changes fresh-replay-safe; preserved production ACLs; added a forward lexical-score correction; and reduced read-only live drift from 27 differences to five changes fully explained by the unapplied remediation migrations. No remaining high-confidence source defect was found in the reviewed scope. | Docker schema replay and regenerated manifest; isolated full Supabase migration reset; focused Vitest 74/74; full Vitest 1,712 passed/1 skipped; lint; typecheck; production build and client-bundle secret scan; configured production-readiness READY; targeted Chromium scope/modal/control QA 4/4; read-only live drift; Supabase security advisor clear. Live apply not run because authorization remained read-only. |
| 2026-07-13 | codex/fix-48h-review-findings-current | 49735663370735a60870d065ed0de3b9d34e077f | last-48-hours PR remediation | Revalidated the last-48-hours findings on current main after PRs #538 and #540; retained only unique fixes across auth/cache isolation, stale-response protection, upload/routing/UI behavior, RAG coalescing, telemetry, worktree tooling, and SAST enforcement. No remaining high-confidence local defect was found in the changed scope. The approved live drift check reported only the five differences already explained by unapplied migrations from #540. | Focused Vitest 107/107; `npm run verify:pr-local` (1,762 passed, 1 skipped; production build and client-bundle scan; offline RAG 60/60); critical Chromium 8/8; live `check:drift`; `git diff --check`. Full Chromium remains advisory after the earlier runner hang; the required critical subset passed on current main. |
8 changes: 4 additions & 4 deletions docs/privacy-impact-assessment.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,9 @@ Clinician browser
│ • source_chunk_ids= real chunk UUIDs ← owner's own data
│ • metadata.query_hash = HMAC/SHA-256 (query-privacy.ts:51)
└──►(E) RESPONSE CACHE (Supabase rag_response_cache, owner-scoped)
└──►(E) RESPONSE CACHE (Supabase rag_response_cache, authenticated owner-scoped)
payload = full answer, TTL ~5 min (RAG_ANSWER_CACHE_TTL_MS)
keyed by owner_id predicate (rag.ts:1667) — no cross-tenant serve
disabled for anonymous answers; authenticated rows are keyed by owner_id
Clinician browser ← answer + citations
```
Expand Down Expand Up @@ -323,8 +323,8 @@ material shortfall is **governance/contractual** — the APP 8 cross-border DPA/
collection notices are now live on `main`; see PIA-1 progress) — plus the **hardening** item still
open (conditional HMAC). Un-redacted answer retention (PIA-3) is now closed for the durable log; the
`rag_response_cache` still retains answers until a same-key overwrite (read-TTL only, no purge cron —
see §8). None of these are cross-tenant data-leak bugs — the tenancy review found **zero** confirmed
cross-tenant leaks — they are compliance-posture and PHI-minimisation gaps.
see §8). Anonymous answer caching is disabled, and the tenancy review found **zero** confirmed
cross-tenant leaks. The remaining items are compliance-posture and PHI-minimisation gaps.

---

Expand Down
6 changes: 2 additions & 4 deletions docs/staging-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,14 @@ those vars are unset.

2. **Apply the schema.** From a checkout linked to the staging project:

````bash
```bash
supabase link --project-ref <staging-ref>
supabase db push # applies supabase/migrations/* → matches schema.sql
```dotenv
```

Then confirm health: `npm run check:indexing` (runs `search_schema_health()`
over the hybrid RPCs) should report ok.

````

3. **Seed a small corpus (~50 docs).** Use synthetic/public content only — do
**not** copy clinical production documents into staging.

Expand Down
47 changes: 31 additions & 16 deletions scripts/check-github-action-pins.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { readdirSync, readFileSync } from "node:fs";
import path from "node:path";
import { yamlBlock } from "./yaml-contract.mjs";

const workflowDir = path.join(process.cwd(), ".github", "workflows");

Expand Down Expand Up @@ -35,6 +36,7 @@ const usesPattern = /^\s*uses:\s*([^@\s]+)@v(\d+)\s*(?:#.*)?$/;
const runsOnLatestPattern = /^\s*runs-on:\s*ubuntu-latest\s*(?:#.*)?$/;
const failures = [];
const expectedSupabaseCliVersion = "2.108.0";
const expectedSupabaseCliVersionPattern = expectedSupabaseCliVersion.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");

for (const fileName of readdirSync(workflowDir)
.filter((name) => /\.ya?ml$/i.test(name))
Expand Down Expand Up @@ -67,18 +69,26 @@ for (const fileName of readdirSync(workflowDir)

const ciWorkflowPath = path.join(workflowDir, "ci.yml");
const ciWorkflow = readFileSync(ciWorkflowPath, "utf8");
const requiredCiFragments = [
`SUPABASE_CLI_VERSION: ${expectedSupabaseCliVersion}`,
"version: ${{ env.SUPABASE_CLI_VERSION }}",
"id: supabase-docker-cache",
"supabase-docker-${{ runner.os }}-cli-${{ env.SUPABASE_CLI_VERSION }}-",
"if: success() && steps.supabase-docker-cache.outputs.cache-hit != 'true'",
];

for (const fragment of requiredCiFragments) {
if (!ciWorkflow.includes(fragment)) {
failures.push(`ci.yml: missing required pinned Supabase/cache contract: ${fragment}`);
}
const migrationJob = yamlBlock(ciWorkflow, "db-reset-verify:", 2);
const setupSupabaseStep = yamlBlock(migrationJob, "- name: Setup Supabase CLI", 6);
const restoreSupabaseStep = yamlBlock(migrationJob, "- name: Restore Supabase Docker image cache", 6);
const saveSupabaseStep = yamlBlock(migrationJob, "- name: Save Supabase Docker images", 6);
if (!new RegExp(`^ SUPABASE_CLI_VERSION: ${expectedSupabaseCliVersionPattern}$`, "m").test(ciWorkflow)) {
failures.push(`ci.yml: global SUPABASE_CLI_VERSION must remain pinned to ${expectedSupabaseCliVersion}.`);
}
if (!/^ version: \$\{\{ env\.SUPABASE_CLI_VERSION \}\}$/m.test(setupSupabaseStep)) {
failures.push("ci.yml: db-reset-verify Setup Supabase CLI must use the pinned env version.");
}
if (
!/^ id: supabase-docker-cache$/m.test(restoreSupabaseStep) ||
!restoreSupabaseStep.includes("supabase-docker-${{ runner.os }}-cli-${{ env.SUPABASE_CLI_VERSION }}-")
) {
failures.push("ci.yml: db-reset-verify cache step must own the pinned Supabase cache id/key.");
}
if (
!/^ if: success\(\) && steps\.supabase-docker-cache\.outputs\.cache-hit != 'true'$/m.test(saveSupabaseStep)
) {
failures.push("ci.yml: db-reset-verify save step must be gated by its own cache-hit output.");
}

if (/\bversion:\s*latest\b/.test(ciWorkflow)) {
Expand All @@ -87,11 +97,16 @@ if (/\bversion:\s*latest\b/.test(ciWorkflow)) {

const sastWorkflowPath = path.join(workflowDir, "sast.yml");
const sastWorkflow = readFileSync(sastWorkflowPath, "utf8");
if (!/^\s{4}continue-on-error:\s*true\s*$/m.test(sastWorkflow)) {
failures.push("sast.yml: Semgrep must remain advisory while it depends on mutable registry rules.");
const semgrepJob = yamlBlock(sastWorkflow, "semgrep:", 2);
const semgrepScanStep = yamlBlock(semgrepJob, "- name: Semgrep scan", 6);
if (/^ continue-on-error:\s*true\s*$/m.test(semgrepJob)) {
failures.push("sast.yml: only the Semgrep scan step may be advisory; job setup failures must block.");
}
if (!/^ continue-on-error:\s*true\s*$/m.test(semgrepScanStep)) {
failures.push("sast.yml: the Semgrep scan step must remain advisory while registry rules are mutable.");
}
if (!/src worker scripts supabase\/functions/.test(sastWorkflow)) {
failures.push("sast.yml: Semgrep must scan Supabase Edge Function source.");
if (!/^ src worker scripts supabase\/functions\s*$/m.test(semgrepScanStep)) {
failures.push("sast.yml: the Semgrep scan command must target src, worker, scripts, and supabase/functions.");
}

if (failures.length > 0) {
Expand Down
18 changes: 11 additions & 7 deletions scripts/generate-drift-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,19 +68,23 @@ async function main() {
docker(["run", "-d", "--name", CONTAINER, "-e", `POSTGRES_PASSWORD=${scratchPassword}`, "-p", `${port}:5432`, image]);

try {
// Wait for Postgres readiness (image init runs its own restarts; give it time).
let ready = false;
// The Supabase image briefly accepts connections before its init migrations
// deliberately restart Postgres. Require sustained readiness so replay does
// not race that shutdown window.
let consecutiveReadyChecks = 0;
for (let attempt = 0; attempt < 60; attempt += 1) {
try {
docker(["exec", CONTAINER, "pg_isready", "-U", "postgres", "-q"]);
ready = true;
break;
consecutiveReadyChecks += 1;
if (consecutiveReadyChecks >= 5) break;
} catch {
await new Promise((resolve) => setTimeout(resolve, 2000));
consecutiveReadyChecks = 0;
}
await new Promise((resolve) => setTimeout(resolve, 2000));
}
if (consecutiveReadyChecks < 5) {
throw new Error("scratch Postgres did not remain ready after initialization");
}
if (!ready) throw new Error("scratch Postgres did not become ready in time");
await new Promise((resolve) => setTimeout(resolve, 3000));

const psql = (user: string, sql: string) =>
docker(
Expand Down
55 changes: 55 additions & 0 deletions scripts/resolve-tsx-cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { existsSync, readFileSync, readdirSync } from "node:fs";
import { dirname, isAbsolute, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

function addAncestors(candidates, start) {
let current = resolve(start);
while (true) {
candidates.add(current);
const parent = dirname(current);
if (parent === current) return;
current = parent;
}
}

function worktreeRoots(projectRoot) {
const roots = new Set();
const dotGit = join(projectRoot, ".git");
if (!existsSync(dotGit)) return roots;
try {
const pointer = readFileSync(dotGit, "utf8")
.trim()
.replace(/^gitdir:\s*/i, "");
const gitDir = isAbsolute(pointer) ? pointer : resolve(projectRoot, pointer);
const commonGitDir = resolve(gitDir, "..", "..");
roots.add(dirname(commonGitDir));
const worktreesDir = join(commonGitDir, "worktrees");
if (existsSync(worktreesDir)) {
for (const name of readdirSync(worktreesDir)) {
const gitdirFile = join(worktreesDir, name, "gitdir");
if (!existsSync(gitdirFile)) continue;
const worktreeGitFile = readFileSync(gitdirFile, "utf8").trim();
roots.add(dirname(worktreeGitFile));
}
}
} catch {
// A normal checkout has a .git directory, not a pointer file. Ancestor
// lookup below already covers its local node_modules.
}
return roots;
}

export function resolveTsxCli(projectRoot, moduleResolve = (specifier) => import.meta.resolve(specifier)) {
try {
return fileURLToPath(moduleResolve("tsx/cli"));
} catch {
const roots = new Set();
addAncestors(roots, projectRoot);
for (const root of worktreeRoots(projectRoot)) addAncestors(roots, root);
for (const root of roots) {
const candidate = join(root, "node_modules", "tsx", "dist", "cli.mjs");
if (existsSync(candidate)) return candidate;
}
throw new Error(`Unable to resolve tsx/cli from ${projectRoot} or its Git worktrees.`);
}
}
5 changes: 4 additions & 1 deletion scripts/run-tsx.mjs
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolveTsxCli } from "./resolve-tsx-cli.mjs";

const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Usage: node scripts/run-tsx.mjs <script.ts> [args...]");
process.exit(1);
}

const tsxCli = fileURLToPath(import.meta.resolve("tsx/cli"));
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const tsxCli = resolveTsxCli(projectRoot);
const hook = new URL("./enable-server-only-stub.mjs", import.meta.url).href;
const child = spawn(process.execPath, [tsxCli, "--import", hook, ...args], {
cwd: process.cwd(),
Expand Down
18 changes: 18 additions & 0 deletions scripts/yaml-contract.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function yamlBlock(source, header, indent) {
const lines = source.split(/\r?\n/);
const prefix = `${" ".repeat(indent)}${header}`;
const start = lines.findIndex((line) => line === prefix);
if (start < 0) return "";
let end = lines.length;
for (let index = start + 1; index < lines.length; index += 1) {
const line = lines[index];
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const leading = line.length - line.trimStart().length;
if (leading <= indent) {
end = index;
break;
}
}
return lines.slice(start, end).join("\n");
}
Loading
Loading