-
Notifications
You must be signed in to change notification settings - Fork 0
Launch-readiness: privacy notice + /privacy page, health SLO probe, deps, runbooks #513
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
2481908
feat(privacy): add /privacy data-handling page + composer collection …
BigSimmo 6417065
feat(health): expose hybrid_rpc_errors + degraded SLO rates on the de…
BigSimmo 39cc705
test(retention): cover rag_query_misses purge in the pg_cron preview-…
BigSimmo b66b58f
chore(deps): update in-range dependencies + regenerate lockfile
BigSimmo ffa8523
docs: add launch operator runbook + retrieval-RPC forward-codify work…
BigSimmo 7a3b9e7
fix: apply CodeRabbit auto-fixes
coderabbitai[bot] 0963255
fix: grant models read permission to codex autofix workflow
Copilot 475f9a0
Merge origin/main into claude/repo-next-steps-e53523
Copilot 3f7d6d7
style(docs): prettier-format the forward-codify work-order
BigSimmo File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ on: | |
| permissions: | ||
| contents: read | ||
| issues: write | ||
| models: read | ||
| pull-requests: read | ||
|
|
||
| jobs: | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| 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). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.