Skip to content
Open
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
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1107,6 +1107,60 @@ jobs:
}
NODE

# #QCNE6N. The step above proves the manifest is not stale and `supabase
# migration up --local` proved the chain APPLIES. Neither compares the
# RESULT of the chain against supabase/schema.sql, so a migration whose
# function body or policy predicate diverges from the mirror passes every
# pre-merge gate — which is how correct_clinical_query_terms(text,real)
# reached main red on 2026-09-01, caught only by the post-merge live-drift
# alarm.
#
# The mirror side is the manifest `drift:manifest` just regenerated from
# THIS PR's schema.sql, so no second replay is built here. That does mean
# the two sides come from different images (emulator vs the pinned bare
# supabase/postgres) and some reported difference will be platform
# provenance — see the header of scripts/check-chain-mirror-parity.ts.
#
# REPORT-ONLY on purpose. The divergences that already exist cannot be
# enumerated offline (docs/database-drift-detection.md backlog item 10
# records ~13 chain-vs-mirror keys plus schema.sql-only storage buckets),
# so this lands printing them rather than blocking on a set nobody has
# measured. The follow-up commits supabase/chain-mirror-allowlist.json from
# this job's summary, adds `--strict`, and drops the `continue-on-error`
# lines below in the same change. tests/chain-mirror-parity.test.ts pins
# those two facts to each other so they cannot drift apart.
- name: Capture the migration chain's schema snapshot
id: chain-snapshot
continue-on-error: true
run: |
set -euo pipefail
# --filter rather than `| grep | head`: under `set -o pipefail` a SIGPIPE
# from head is a step failure, and grep exiting 1 on no match aborts
# before the explicit `test -n` can say what went wrong.
db_container="$(docker ps --format '{{.Names}}' --filter 'name=^supabase_db_' | head -n 1)"
test -n "${db_container}"
docker exec -i "${db_container}" \
psql -U postgres -d postgres -tA -v ON_ERROR_STOP=1 \
-c 'select public.schema_drift_snapshot()::text;' > /tmp/chain-snapshot.json
test -s /tmp/chain-snapshot.json

- name: Compare the migration chain against supabase/schema.sql
id: chain-mirror-parity
continue-on-error: true
if: steps.chain-snapshot.outcome == 'success'
run: |
npm run check:chain-mirror-parity -- \
--chain /tmp/chain-snapshot.json \
--mirror-manifest supabase/drift-manifest.json

- name: Report a chain/mirror parity step that produced no evidence
# Both steps above carry continue-on-error, so without this a crashing
# compare step is a grey mark nobody reads and is indistinguishable from
# a clean run. Cover the compare outcome too, not just the capture.
if: steps.chain-snapshot.outcome != 'success' || steps.chain-mirror-parity.outcome != 'success'
run: |
echo "::warning::chain/mirror parity produced no evidence (capture=${{ steps.chain-snapshot.outcome }}, compare=${{ steps.chain-mirror-parity.outcome }}). It is report-only today so it does not block the merge, but the comparison did not run."

- name: Upload regenerated drift manifest
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
Expand Down
4 changes: 2 additions & 2 deletions data/repo-awareness-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"version": "repo-awareness-snapshot-v2",
"captured_revision": {
"sha": "cdfb3fdda5533c126e76fecaf4e0352a6757249f",
"committed_at": "2026-09-02T11:12:52+00:00"
"sha": "824d483d4bdd94d47f26228ee8e99206b2b04bb6",
"committed_at": "2026-09-02T11:53:32+00:00"
},
"routes": {
"modes": [
Expand Down
79 changes: 78 additions & 1 deletion docs/database-drift-detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,80 @@ history is squashed and the row disappears) the entry shows as stale on the
next run — delete it. Never widen a class or drop `objects` to make an entry
pass; the finding is the point.

## Chain-vs-mirror parity (`npm run check:chain-mirror-parity`)

**Extensions are compared, not excused.** `check:drift` treats an extension present on the live
side but not in `schema.sql` as platform provenance and prints it as an info line — right for the
live gate, where Supabase provisions `pg_net` and `pgsodium` that no migration creates. On this
comparison the "live" side is the migration chain, which is our own code, so the parity script
re-promotes those to `unexpected_live` findings. There is already a real one:
`20260901033250_enable_staging_privacy_retention_schedules.sql` runs
`create extension if not exists pg_cron` and `supabase/schema.sql` never declares it, so under the
inherited rule the pair reported nothing at all. An extension the emulator image genuinely
provisions belongs in `supabase/chain-mirror-allowlist.json` as a reviewed entry, one at a time.

`check:drift` compares **live** against `supabase/schema.sql`. CI's `db-reset-verify`
proves the migration chain **applies** (`supabase migration up --local`) and that the
drift manifest is **not stale** (committed vs generated `schema_sha256`). Nothing
compared the _result_ of the chain against the mirror, so a migration whose function
body or policy predicate diverged from `schema.sql` passed every pre-merge gate.

That is `#QCNE6N`, and it is not theoretical. On 2026-09-01 migration
`20260831100000` (PR #2477) redefined `public.correct_clinical_query_terms(text,real)`
with a duplicated predicate and `schema.sql` was never updated to match. Every
pre-merge gate stayed green; the post-merge `live-drift` run went red on `main`
(`def_hash` manifest `e2356565` vs live `2ebaf978`). Behaviour impact was nil — the
duplicate predicate is a boolean no-op — but it cost a red daily alarm and a
remediation PR, and it is the second occurrence of this class after `#316`.

**How it runs.** In `db-reset-verify`, after the replay and after `drift:manifest`
regenerates the manifest from this PR's `schema.sql`:

- the **chain** side is `public.schema_drift_snapshot()` read out of the Supabase
emulator database the migrations just built;
- the **mirror** side is that regenerated manifest's `snapshot`, which is a
`schema.sql` replay — so no second replay is built;
- `scripts/check-chain-mirror-parity.ts` compares them with `compareDriftSnapshots`,
the same comparator the live gate uses. Two comparators would eventually disagree
about what "different" means, and then one of them would be wrong.

`migration_history` and `migration_history_probe` are stripped from both sides **by
construction**, never allowlisted: a `schema.sql` replay has no `supabase_migrations`
schema, so the category can only produce noise here. It stays the live gate's business.

**Known asymmetry.** The two sides come from different images — the emulator versus
the pinned bare `supabase/postgres` — so some reported difference is platform
provenance rather than real divergence. Building both sides identically was tried and
does not work: a mirror database created inside the emulator has no `auth` schema for
`schema.sql`'s `references auth.users(id)` columns, and reproducing the chain inside
the bare image means driving the whole chain by hand rather than through
`supabase migration up`.

**Report-only, with an expiry.** The gate lands printing divergences rather than
blocking, because the existing set (backlog item 10 above, plus schema.sql-only
storage buckets) has never been measured and blocking on an unmeasured set just
teaches people to ignore a red check. It emits a `::warning::` on any divergence and a
second one if either step produced no evidence, so a silently-crashing gate cannot be
mistaken for a clean one. `tests/chain-mirror-parity.test.ts` ties the mode to the
failure tolerance — `--strict` and `continue-on-error` cannot coexist — and expires
report-only mode on **2026-12-01**, after which the suite goes red until the phase
ends.

**Ending report-only** is one small PR: take the divergence list from a real run's job
summary, commit it to `supabase/chain-mirror-allowlist.json` with a reason each, add
`--strict`, and delete the `continue-on-error` lines in the same change.

**`supabase/chain-mirror-allowlist.json` is not `supabase/drift-allowlist.json`, and
they must never merge.** An entry in the live allowlist blinds the weekly live-drift
alarm. An entry here only says "the chain and the mirror are knowingly different in
this one place", and the live gate still catches the consequence post-merge. A test
asserts neither script reads the other's file.

> Note for whoever adds this to `verify:cheap` later: the matcher in
> `check-gate-manifest.mjs` that pairs a local gate with its CI step is anchored to a
> single-line `run:` invocation, and this gate's CI step uses a multi-line `run: |`.
> It would not register as CI coverage without changing that matcher first.

## Runtime index-monitoring ratchet

`search_schema_health()` monitors a curated `required_indexes` list (22 names
Expand Down Expand Up @@ -388,4 +462,7 @@ live project need explicit operator approval.
live): 13 keys where the chain diverges from schema.sql — buckets are only
created by schema.sql, `documents`/`ingestion_jobs` updated*at trigger
variants, post-legacy-drop embedding-fields index set,
`document_chunks_content_trgm_idx` shape, `rag_visual_eval*\*` shapes.
`document_chunks_content_trgm_idx` shape, `rag_visual_eval*\*` shapes. That
count is a 2026-07-07 hand measurement and has never been re-measured; the
chain/mirror parity gate above is what re-measures it, on every
database-touching PR.
2 changes: 1 addition & 1 deletion docs/scripts-index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Scripts index

Curated map of `scripts/` (285 files) and the `package.json` script surface (289 entries),
Curated map of `scripts/` (286 files) and the `package.json` script surface (290 entries),
grouped by purpose. This is orientation, not an exhaustive per-file listing — the authoritative
command list is `package.json`, and `npm run docs:check-scripts` verifies every `npm run <x>`
referenced in docs resolves to a real script. `npm run docs:update` refreshes the exact counts above.
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
"test:coverage": "node scripts/run-vitest.mjs run --coverage",
"test:coverage:node": "node scripts/run-vitest.mjs run --project=node --coverage",
"test:coverage:ui": "node scripts/run-vitest.mjs run --project=jsdom --coverage",
"test:ci-workflows": "node scripts/run-vitest.mjs run tests/ci-cache-safety.test.ts tests/authenticated-live-workflow.test.ts tests/browser-test-plan.test.ts tests/codex-autofix-workflow.test.ts tests/codex-run-pr-operator-workflow.test.ts tests/eval-canary-workflow.test.ts tests/live-drift-workflow.test.ts tests/ops-digest.test.ts tests/container-ci-contract.test.ts tests/test-runner-safety.test.ts tests/installed-lock-parity.test.ts tests/railway-config.test.ts tests/ingestion-autopilot.test.ts tests/ingestion-autopilot-workflow.test.ts tests/check-lighthouse-budget.test.ts tests/live-web-vitals-inputs.test.ts tests/offline-release-profile.test.ts tests/bundle-budget-refresh-workflow.test.ts",
"test:ci-workflows": "node scripts/run-vitest.mjs run tests/ci-cache-safety.test.ts tests/authenticated-live-workflow.test.ts tests/browser-test-plan.test.ts tests/chain-mirror-parity.test.ts tests/codex-autofix-workflow.test.ts tests/codex-run-pr-operator-workflow.test.ts tests/eval-canary-workflow.test.ts tests/live-drift-workflow.test.ts tests/ops-digest.test.ts tests/container-ci-contract.test.ts tests/test-runner-safety.test.ts tests/installed-lock-parity.test.ts tests/railway-config.test.ts tests/ingestion-autopilot.test.ts tests/ingestion-autopilot-workflow.test.ts tests/check-lighthouse-budget.test.ts tests/live-web-vitals-inputs.test.ts tests/offline-release-profile.test.ts tests/bundle-budget-refresh-workflow.test.ts",
"test:cc-guards": "node scripts/run-vitest.mjs run --reporter=dot tests/caring-contacts-plan-draft.dom.test.tsx tests/caring-contacts-plan-patient-detail.test.ts tests/caring-contacts-plan-activation.test.ts tests/caring-contacts-plan-wizard.dom.test.tsx tests/caring-contacts-schedule.test.ts tests/caring-contacts-schedule-view.test.ts tests/caring-contacts-schedule-route.test.ts tests/caring-contacts-schedule-screen.dom.test.tsx tests/caring-contacts-schedule-page.dom.test.tsx tests/caring-contacts-clock.test.ts tests/caring-contacts-new-plan-page.dom.test.tsx tests/caring-contacts-explained-automation.dom.test.tsx tests/caring-contacts-workspace-shell.dom.test.tsx tests/caring-contacts-patients-directory.dom.test.tsx tests/caring-contacts-patient-overview.dom.test.tsx tests/caring-contacts-patients-page.dom.test.tsx tests/caring-contacts-domain-isolation.test.ts tests/caring-contacts-interface-vocabulary.test.ts tests/caring-contacts-retention.test.ts tests/caring-contacts-repository.test.ts tests/caring-contacts-overlay-definitions.test.ts tests/caring-contacts-overlay-trigger-inventory.test.ts tests/caring-contacts-workspace-screens.test.ts tests/route-reachability.test.ts tests/design-system-adoption.test.ts tests/caring-contacts-contact-time-adjustment.dom.test.tsx tests/caring-contacts-contact-route.test.ts tests/caring-contacts-overlay-trigger.dom.test.tsx tests/caring-contacts-overlay-host.dom.test.tsx tests/source-control-bytes.test.ts tests/caring-contacts-demo-seed.test.ts tests/caring-contacts-pathway-versions.test.ts tests/caring-contacts-templates-library.dom.test.tsx tests/caring-contacts-templates-page.dom.test.tsx tests/caring-contacts-template-detail.dom.test.tsx tests/caring-contacts-template-detail-page.dom.test.tsx tests/caring-contacts-reporting.test.ts tests/caring-contacts-guidance-reports-pages.dom.test.tsx tests/caring-contacts-team-workload.test.ts tests/caring-contacts-team-route.test.ts tests/caring-contacts-team-roster.dom.test.tsx tests/caring-contacts-team-page.dom.test.tsx",
"test:e2e": "node scripts/run-playwright.mjs",
"test:e2e:all": "node scripts/run-playwright.mjs",
Expand Down Expand Up @@ -289,6 +289,7 @@
"arbiter:status": "node scripts/gate-arbiter.mjs status",
"arbiter:clear": "node scripts/gate-arbiter.mjs clear",
"check:drift": "node scripts/run-tsx.mjs scripts/check-drift.ts",
"check:chain-mirror-parity": "node scripts/run-tsx.mjs scripts/check-chain-mirror-parity.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",
"sync:pr-branches": "node scripts/sync-open-pr-branches.mjs",
Expand Down
Loading
Loading