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
1 change: 1 addition & 0 deletions .github/workflows/codex-autofix-review-comments.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
permissions:
contents: read
issues: write
models: read
pull-requests: read

jobs:
Expand Down
93 changes: 93 additions & 0 deletions docs/forward-codify-retrieval-rpcs-workorder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Work-order — forward-codify live-ahead retrieval RPC bodies (drift backlog #0)

**Status: staged, not executed.** This is plan task 1.2. It is deliberately **not** shipped as a
migration yet because the two safety preconditions were unavailable at authoring time (2026-07-12):

1. **Byte-faithful Docker-replay validation is required and Docker was down.** The established method
(see [supabase-migration-reconciliation.md](supabase-migration-reconciliation.md) and the
`20260701140631_codify_live_retrieval_rpcs` precedent) validates each codified body byte-equivalent
to live via a whitespace-stripped `pg_get_functiondef` md5 against a container replay **before** any
Comment thread
BigSimmo marked this conversation as resolved.
apply — this is what makes the apply a proven no-op on live. Docker Desktop would not start here.
2. **Live is under active concurrent multi-session editing.** `supabase/drift-allowlist.json` warns the
snapshot "needs regeneration once churn settles." A capture taken now can be stale by apply time, so
the codification must **re-capture and re-compare at apply time**.

Shipping a large multi-function migration + `schema.sql` reconciliation without (1), against a moving
target, would risk introducing the exact silent-retrieval-regression class this fixes. So this file
locks the capture fingerprints and the exact procedure; execute it in an environment with Docker and a
quiescent live DB.

## Live fingerprints captured 2026-07-12 (project `sjrfecxgysukkwxsowpy`)

Read-only capture via `pg_get_functiondef`. Re-run the capture at execution time; if any `body_md5`
below differs, live moved — codify the **new** body and note the change.

| Function (identity args) | body_md5 | len | secdef | In allowlist as |
| ---------------------------------------------------------------------------- | ---------------------------------- | ---- | ------- | ---------------------------- |
| `match_document_chunks(vector,int,float8,uuid,uuid)` | `45cc06effe9a753604eba4af5ae43c7e` | 1553 | no | live-ahead |
| `match_document_chunks_hybrid(vector,text,int,float8,uuid[],uuid)` | `90a027977c84847730a0e48481060502` | 5638 | no | live-ahead |
| `match_document_chunks_text(text,int,uuid[],uuid)` | `9d2f8aa374f01bd149a17af56e075171` | 5596 | no | live-ahead |
| `match_document_table_facts_text(text,int,uuid[],uuid)` | `904049c7635b996a6653e91c49d86ec2` | 4609 | no | live-ahead |
| `match_documents_for_query(text,int,uuid)` | `53234990b88dba5c0466f9dccb512455` | 1448 | no | live-ahead |
| `match_document_index_units_hybrid(vector,text,int,float8,uuid[],uuid)` | `7b144ff6fdd93b753bf67f7317be0cc6` | 3309 | no | (verify) |
| `match_document_memory_cards_hybrid(vector,text,int,float8,uuid[],uuid)` | `274ec5832d85a95880ec26daf5b79f23` | 1014 | no | (verify) |
| `match_document_memory_cards_hybrid_v2(vector,text,int,float8,uuid[],uuid)` | `977ba52a9a239962d3105f2e5b071f82` | 3714 | no | (verify) |
| `match_document_embedding_fields_hybrid(vector,text,int,float8,uuid[],uuid)` | `9d6f2b7bcd009d23739aabaf93234deb` | 2476 | no | not listed (already matches) |
| `get_related_document_metadata(uuid[],uuid)` | `ada68dd136a9878de2ce45e522fc0208` | 1363 | no | live-ahead (non-retrieval) |
| `get_visual_evidence_cards(uuid,int)` | `f4aac704317472dfc06c361389793cf9` | 1370 | **yes** | unexpected_live (live-only) |

Capture query:

```sql
select p.oid::regprocedure::text, pg_get_functiondef(p.oid),
md5(pg_get_functiondef(p.oid)) as body_md5
from pg_proc p join pg_namespace n on n.oid = p.pronamespace
where n.nspname='public' and p.proname = ANY (ARRAY[
'match_document_chunks','match_document_chunks_hybrid','match_document_chunks_text',
'match_document_table_facts_text','match_documents_for_query',
'match_document_index_units_hybrid','match_document_memory_cards_hybrid',
'match_document_memory_cards_hybrid_v2','get_related_document_metadata','get_visual_evidence_cards',
'run_visual_eval_case','run_all_visual_eval_cases','repair_enrichment_quality_batch'])
order by 1;
```

## Procedure (execute with Docker up + quiet live queue)

1. **Re-capture** the full `pg_get_functiondef` text for each function above (read-only). Apply
identical whitespace normalization (strip leading/trailing whitespace, normalize internal
whitespace) to both live-captured definitions and the migration text before computing md5 hashes.
Confirm normalized md5s vs this table; codify whatever live currently is.
2. **Author** `supabase/migrations/<ts>_codify_live_retrieval_rpcs_forward.sql` — one
`CREATE OR REPLACE FUNCTION` per function, body = the verbatim captured definition. Apply the
same whitespace normalization before hashing to ensure normalized comparison (not byte-identical
raw text). Preserve `security definer`/`invoker`, `set search_path`, and grants exactly.
**Capture and replay ACLs:** alongside `pg_get_functiondef`, capture each function's ACLs using
`proacl` or routine privileges query. Include the captured grants in the migration (apply after
each `CREATE OR REPLACE`) and verify the resulting ACLs match live during validation.
`get_visual_evidence_cards`, `run_visual_eval_case`, `run_all_visual_eval_cases`,
`repair_enrichment_quality_batch` are **live-only** (`unexpected_live`) — codify as `CREATE OR
REPLACE` too so schema.sql declares them. These security-definer RPCs require particular ACL
coverage.
3. **Reconcile** `supabase/schema.sql`: replace each function's old body with the captured one (same
text as the migration). Update `tests/supabase-schema.test.ts` if it pins any of these bodies.
4. **Validate (the load-bearing gate):** `npm run drift:manifest` (replays schema.sql into Docker from
scratch — proves replayability) then confirm each codified body's whitespace-normalized
`pg_get_functiondef` md5 equals live. Compare the resulting ACLs against live using the same
query from step 2. `npm run verify:cheap` includes `tests/drift-detection.test.ts`
(migration↔schema.sql parity, allowlist hygiene).
5. **Golden eval:** `npm run eval:retrieval:quality` must stay **36/36** (retrieval bodies changed —
though a faithful capture is behavior-neutral by construction).
6. **⏸ Apply** via the approved live migration workflow. Because it is a verbatim capture, apply is an
idempotent no-op on live at capture time.
7. **Remove** the now-resolved `functions` entries from `supabase/drift-allowlist.json` (the
`match_document_*` "live-ahead" set + the codified live-only functions) and regenerate the manifest.

## Not in this work-order (separate allowlist backlog)

The allowlist also carries, with the same boilerplate reason, non-retrieval drift that should be triaged
separately: the **Data-API grant** posture on ~18 tables (live revoked `authenticated` grants that
schema.sql still declares — decide codify-revokes vs restore-grants), legacy/duplicate **indexes**
(`unexpected_live` drop-candidates + `missing_live` recreate-or-remove), and the PUBLIC-execute
`security-invoker` functions (`detect_legacy_ivfflat_indexes`, `document_summary_text`,
`set_document_embedding_field_content_hash`). See
[database-drift-detection.md](database-drift-detection.md#reconciliation-backlog).
164 changes: 164 additions & 0 deletions docs/launch-operator-runbook.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Launch operator runbook

**Single sequenced runbook for the operator-gated launch steps.** It ties together the detailed docs
(linked per step) into one ordered flow with exact commands and explicit approval gates. Nothing here
runs automatically — every **⏸ PAUSE** is a provider-touching action (Supabase / Railway / OpenAI /
GitHub) that needs your explicit go-ahead, per the AGENTS.md provider boundary.

Host note: production app + worker run on **Railway** (user directive 2026-07-12), not Fly. The image is
host-agnostic. Railway has no Sydney region (closest Singapore); data at rest stays in Supabase Sydney,
so this is a latency/SLO tradeoff only — confirm answer-p95 in the staging soak (step 4).

Legend: **⏸ PAUSE** = provider action, needs your approval · **✅ verify** = check to run after.

---

## Order at a glance

```text
0. Pre-flight identity check
1. Apply pending live migrations (July-8 batch + PIA-4 + drift-codify) [Supabase]
2. Run the full release gate [live keys]
3. Provision staging + seed [Supabase + Railway]
4. Staging soak + rollback rehearsal [Railway]
5. Production deploy [Railway]
6. Post-deploy: worker, registry seed, auth conn cap, observability wiring
```

---

## 0. Pre-flight (read-only)

```bash
npm run check:supabase-project # must report Clinical KB Database / sjrfecxgysukkwxsowpy
npx supabase migration list --linked
npm run reindex:health # note jobs_pending / jobs_processing (needed for step 1 R17)
```

## 1. Apply pending live migrations 🧑 Supabase

Detailed runbook: [operator-apply-july8-batch.md](operator-apply-july8-batch.md). Apply **in this order**
when the ingestion queue is quiet. **Do not redeploy the worker until step `20260708130000` is live.**

| # | Migration | Note |
| --- | ----------------------------------------------------- | ------------------------------------------- |
| a | `20260708140000_drop_ingestion_job_stages_job_id_fk` | no-op on live |
| b | `20260708130000_ingestion_concurrency_rpc_hardening` | **worker-redeploy blocker** |
| c | `20260708150000_ensure_retrieval_owner_matches` | helper before fail-closed |
| d | `20260708160001_retrieval_owner_matches_fail_closed` | tenancy fail-closed (#409) |
| e | `20260708310000_r5_document_metadata_merge` | R5 deep-merge (#408) |
| f | `20260708170000_ingestion_jobs_one_open_per_document` | R17 — approved manual `CONCURRENTLY` path |
| g | `20260708120000_rag_query_misses_retention` | **PIA-4** purge cron |
| h | `<drift-codify-forward>` | **only after task 1.2 lands** — see step 1b |

**⏸ PAUSE:** apply via `supabase db push` (queue quiet) or the R17 manual `CREATE UNIQUE INDEX CONCURRENTLY`
path in the July-8 doc. R17 manual path is an approved exception to the live-change guardrail; record
the migration history entry and reconcile schema.sql after manual execution to prevent untracked drift.
R17 uses its own version so history/repair can't collide with `20260708160001`.

**✅ verify:**

```bash
SUPABASE_ENVIRONMENT=production npm run check:july8-live-batch
npm run check:drift
npm run check:indexing # search_schema_health() ok
npm run eval:retrieval:quality # must stay 36/36 (retrieval-affecting: step d + drift-codify)
```

### 1b. Drift-codify apply (task 1.2)

The forward-codify migration (live-diverged `match_document_chunks` `hnsw.ef_search=100` wrapper + `*_text`
multi-strategy bodies) is authored + validated with normalized fingerprint comparison vs a Docker replay
before it reaches you, so its apply is an **idempotent no-op on live**. **This step is blocked until the
migration artifact is committed and execution-time live fingerprint recapture is completed.** Before
applying, recapture live fingerprints using the exact committed capture query and compare normalized md5s
against the committed table. Abort on mismatch. Apply as step 1h only after verification, then re-run
`check:drift` + `eval:retrieval:quality` (36/36). Background:
[database-drift-detection.md](database-drift-detection.md).

## 2. Full release gate 🧑 live keys

Clears the accumulated verification debt (universal search, cross-mode links, rag.ts decomp).

**⏸ PAUSE** (bounded OpenAI spend):

```bash
npm run verify:release # full Playwright matrix + check:production-readiness
# + governance:release + eval:quality:release
npm run eval:retrieval:quality # 36/36
npm run eval:quality -- --rag-only # grounded-supported must not drop; citation-failure 0
```

Record outcomes in release notes / [process-hardening.md](process-hardening.md).

## 3. Provision staging + seed 🧑 Supabase + Railway (billable)

Detailed: [staging-setup.md](staging-setup.md). No code change — the identity guard activates on env.

1. **⏸ PAUSE** create Supabase project `Clinical KB Staging`, same org, **ap-southeast-2**, generate DB
password (Supabase MCP `create_project` after `confirm_cost`, or dashboard). Record `<staging-ref>`.
2. `supabase link --project-ref <staging-ref>` → `supabase db push` → `npm run check:indexing`.
3. Seed synthetic (~50 docs, **never** production clinical docs):
```bash
npm run samples && npm run import:docs
npm run registry:seed -- --owner-id <owner> --write --confirm
npm run differentials:seed && npm run medications:seed
```
4. Capture staging keys (`sb_publishable_…`, `sb_secret_…`).

## 4. Staging soak + rollback rehearsal 🧑 Railway

1. Build image (real staging publishable key inlines into the client bundle):
```bash
docker build --build-arg NEXT_PUBLIC_SUPABASE_URL=https://<staging-ref>.supabase.co \
--build-arg NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY=<staging sb_publishable_…> \
-t clinical-kb-app:staging .
```
(Local Docker can OOM on the 8 GiB Next heap — prefer the CI image-build workflow if it wedges.)
2. **⏸ PAUSE** deploy to Railway staging + set runtime secrets (staging values, distinct from prod):
`SUPABASE_SERVICE_ROLE_KEY`, `OPENAI_API_KEY`, `SUPABASE_PROJECT_REF=<staging-ref>`,
`SUPABASE_PROJECT_NAME=Clinical KB Staging`, `SUPABASE_STAGING_PROJECT_REF=<staging-ref>`,
`SUPABASE_STAGING_PROJECT_NAME=Clinical KB Staging`, `RAG_QUERY_HASH_SECRET` (staging),
`RAG_PROVIDER_MODE=auto`. Keep one warm instance (no scale-to-zero); health `/api/health`.
3. **✅ verify** boot + soak (soak is hard-guarded against production):
```bash
npx tsx scripts/soak-test.ts --target https://<staging-host> --confirm-staging \
--users 30 --duration-s 600 --ramp-s 120
```
Targets ([capacity-review.md](capacity-review.md) §4): search p95 ≤ 3 s, **answer p95 ≤ 25 s**
(watch this given the Railway↔Sydney hop), non-429 error rate < 1 %.
4. Rehearse rollback = redeploy the previous Railway image tag; confirm health returns.

## 5. Production deploy 🧑 Railway

Decision record: [deployment-architecture.md](deployment-architecture.md) §2. Same image contract, prod
build-args + secrets.

**⏸ PAUSE:** authorize the Railway account/service, build with the **production** publishable key, set
runtime secrets (**incl. `RAG_QUERY_HASH_SECRET`** — PIA-2 fail-closed guard requires it at boot;
`SUPABASE_SERVICE_ROLE_KEY`, `OPENAI_API_KEY`, `SUPABASE_PROJECT_REF/NAME` for prod). One warm instance,
no scale-to-zero, health `/api/health`. I'll prep the Railway service config via the `use-railway` skill.

**✅ verify:** `GET /api/health` → `{"status":"ok"}`; `npm run check:deployment-readiness`.

## 6. Post-deploy

- **Worker** 🧑 — build `Dockerfile.worker`, run **one** always-on instance in the same region with prod
secrets. **Only after migration `20260708130000` is live.** Confirms via `npm run reindex:health`.
- **Registry seed (prod)** 🧑 — `npm run registry:seed -- --owner-id <prod-owner-uuid> --write --confirm`
(+ `differentials:seed` for the slug-retitle prune). Until seeded, Services/Forms show empty.
- **Auth connection cap** 🧑 — before the first vertical scale-up, switch Supabase auth from the 10-absolute
cap to **percentage-based** allocation in the dashboard ([capacity-review.md](capacity-review.md) §3).
Not settable via SQL/MCP.
- **Observability wiring** 🧑 — once host metrics exist, wire the warn/page SLO thresholds
([observability-slos.md](observability-slos.md) §2) into a real alert channel; confirm the nightly eval
canary is green from `main` (one `workflow_dispatch` run).

---

## Standing guardrails

- Never raw-SQL against live — committed migration + `schema.sql` reconciliation only.
- Worker redeploy is blocked until `20260708130000` is live.
- Any retrieval/ranking change re-runs `eval:retrieval:quality` 36/36 before it ships.
- Each environment gets separate service-role + OpenAI keys (per-env blast radius).
1 change: 1 addition & 0 deletions docs/site-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ This file is generated by `npm run sitemap:update`. Run `npm run sitemap:check`
- `/favourites` - Saved clinical items and sets. Source: `src/app/favourites/page.tsx`.
- `/forms` - Forms home and search surface. Source: `src/app/forms/page.tsx`.
- `/medications` - Medication index redirect. Source: `src/app/medications/page.tsx`.
- `/privacy` - Route discovered from app directory Source: `src/app/privacy/page.tsx`.
- `/reference/colour-coding` - Route discovered from app directory Source: `src/app/reference/colour-coding/page.tsx`.
- `/services` - Services home and search surface. Source: `src/app/services/page.tsx`.

Expand Down
Loading
Loading