Skip to content

S368 Runbook — ID-115 api-Schema Cutover (Cluster A) + ID-68 identity-guard De-ID (R vs S)

S368 Runbook — ID-115 api-Schema Cutover (Cluster A) + ID-68 identity-guard De-ID (R vs S)

Section titled “S368 Runbook — ID-115 api-Schema Cutover (Cluster A) + ID-68 identity-guard De-ID (R vs S)”

Type: Ratifiable execution runbook (design-only authoring; nothing applied). Authored: 2026-06-16/17, read-only Planner dispatch. Main checkout /Users/liamj/Documents/development/knowledge-hub, branch fix/ci-baseline-s368, tip 696608b5b (brief said 7203ecdd6 == main; branch advanced — orchestrator re-confirms base before execution). STRUCTURE (per owner reframe): Two operations were conflated in the original brief and are now SEPARATED:

  • PART 1 — ID-115 api-schema cutover (S13). Clears CI Cluster A (types-parity + 9 integration + 2 eval) by applying the already-authored api migrations to the remotes. NO squash needed. Independent of the de-ID.
  • PART 2 — ID-68 identity-guard de-ID. Clears identity-guard RED. TWO costed options for the owner: Option R (documented git filter-repo redaction — zero schema risk, ratified) vs Option S (fresh migration squash — clean public baseline). Liam ratifies every GO-gate; R-vs-S is an owner decision. KEY EMPIRICAL UPDATE (this dispatch): the owner’s correction — “ID-115 made REVOKE a non-issue for a fresh squash” — is CONFIRMED at the ACL layer but BLOCKED at the guard-regex layer. Measured against live prod: 114/114 public functions have non-default proacl → pg_dump DOES emit per-function ACLs (the S176 baseline’s 3-REVOKE problem is GONE). BUT pg_dump emits REVOKE ALL … FROM PUBLIC, and check-revoke-guard.ts only accepts REVOKE EXECUTE … FROM <list containing anon> (line 566). So Option S still needs a guard-aware normalisation pass (small, mechanical — NOT the full REVOKE re-injection my prior draft assumed). Costing revised below: S risk drops from “decisive blocker” to “one scripted normalisation step”, ~near-parity with R on schema risk — the deciding axis becomes the fleet-reclone + reference-DB gap-hunt, not REVOKEs.

Verdict
PART 1 (api cutover)Execution-ready except GAP A. ID-115 TECH §(d)/(f)/(h) + impl3 fully specify per-remote atomic apply, exposed-schema mirror, gen types --schema public,api. GAP A (greening BLOCKER): the 3 RPCs the failing CI expects in api_test_delete_broken_auth_user (integration-9), find_duplicate_pairs (eval FC-44), get_content_win_rate (eval FC-51) — are NOT in scripts/generate-api-views.ts RPC_NAMES (verified absent). Test/eval clients default to db.schema='api' (post-S9) → .rpc('find_duplicate_pairs') → absent api.find_duplicate_pairs → PGRST202. All 3 EXIST in public with correct non-default ACLs (verified) — they are wrappable; the generator just doesn’t list them. Apply-to-prod ALONE does not green these jobs; extend the generator surface + re-run + apply.
PART 2 (de-ID)A decision, not a single procedure. Documented/ratified mechanism (Option R) = git filter-repo --replace-text + --path-rename + the already-landed superseding migration, NEVER deleting an applied migration. Option S (fresh squash) is separable + undocumented-as-ID-68; with the REVOKE blocker now reduced to a normalisation step, it is viable but carries the reference-DB gap-hunt + storage/policy reconciliation precedent. Both green identity-guard.

The conflation to keep separate (mempalace prior-investigation, verbatim substance):

  • (A) S176 squash (~Apr 2026) is DONE + independent of ID-68. 43 migrations → baseline supabase/migrations/20260416102457_pre_squash_reconciliation.sql (8,600 LOC). Today’s tree = that baseline + ~137 append-only deltas (141 files at this HEAD; the drawer’s “138” predates bl-328 + the 2 id115 api migrations). Only documented “collapse history into a clean baseline” op; finished. The S176 baseline has 0 REVOKE EXECUTE because it predates OPS-43 (the functions were still default-ACL at squash time).
  • (B) ID-68 migration-history de-ID is NOT a squash. Content-redaction + file rename + append-only superseding migration, chain intact (Option R).

PART 1 — ID-115 api-Schema Cutover (clears Cluster A) — NO SQUASH

Section titled “PART 1 — ID-115 api-Schema Cutover (clears Cluster A) — NO SQUASH”

Root cause: 20260616120000_id115_api_schema_create.sql + 20260616120100_id115_api_views_and_rpcs.sql are on main but not applied to the remotes (prod half-flipped: exposed-schemas → api, api objects empty). Fix = the S13 cutover: apply api migrations atomically per remote, flip exposed-schemas, regen types, fix the CI command. No history rewrite.

P1.0 — Close GAP A first (no GO; reversible)

Section titled “P1.0 — Close GAP A first (no GO; reversible)”

Extend scripts/generate-api-views.ts so the 3 missing RPCs get api INVOKER wrappers:

  • _test_delete_broken_auth_user → a _test_*/EXTRA-DEFINER lane (it’s a test helper — confirm it should be exposed at all, or fix the integration test to stop routing it through api).
  • find_duplicate_pairs + get_content_win_rateSURFACE_RPCS or EXTRA_DEFINER_RPCS (both verified present in public with non-default ACLs; both INVOKER).
  • Per-RPC FIRST confirm the public overload signature against the local catalog — the generator introspects pg_proc per name and ABORTS on a name with no matching public fn. If any is a genuinely-dead .rpc() with a fallback, fix the test/eval side instead.
  • Re-run bun scripts/generate-api-views.tsbun scripts/check-api-view-coverage.tssupabase db reset --local (foreground) → bun run test green + the 3 RPCs present as api.*.

P1.1 — STAGING (turayklvaunphgbgscat) — full E2E + CI repurpose (no GO)

Section titled “P1.1 — STAGING (turayklvaunphgbgscat) — full E2E + CI repurpose (no GO)”
  1. cat supabase/.temp/project-refMUST be staging. It currently reads rovrymhhffssilaftdwd = PROD — relink supabase link --project-ref turayklvaunphgbgscat (CLAUDE.md drift hazard, LIVE now).
  2. Apply: bring staging current (api schema + GAP-A wrappers). supabase db push foreground only (interactive prompt — never background, it hangs). dangerouslyDisableSandbox: true for DB/CLI.
  3. config.toml: [api] schemas = ["api"] already set (:15); staging auth hook already → hook_restrict_signup_to_allowed_domain (:83-85, §PC-39(i) staging slice DONE) — confirm.
  4. Exposed-schemas → api in lockstep with the apply (INV-19): Management API PATCH /v1/projects/turayklvaunphgbgscat/postgrest body db_schema:"api", or Dashboard → Settings → API.
  5. Verify (matrix items 1/7/8 TOGETHER): PGRST106 boundary (Accept-Profile: public → 406; api default → 200, INV-1/2); exposed-schemas == api; schema-parity.yml (dispatch) staging vs prod in sync on public.
  6. Smoke: login → dashboard read → content read+write → one MCP tool → one client RPC ≠ PGRST106 (app smoke Liam Vercel-gated). Run full E2E here to classify e2e-4 (below).

P1.2 — PROD (rovrymhhffssilaftdwd) — completes the half-flip [GO-GATE P1]

Section titled “P1.2 — PROD (rovrymhhffssilaftdwd) — completes the half-flip [GO-GATE P1]”
  1. GO-GATE P1 — irreversible prod DDL. Liam GO.
  2. Relink + verify ref == prod.
  3. Dump-diff prod public vs the migration set FIRST (schema-parity-style) — reconcile migration if non-zero, else proceed.
  4. Apply api schema + views/RPCs to prod (db push foreground). Exposed-schemas already api (manual) — confirm, don’t re-flip.
  5. config.toml prod block + Vercel-prod deploy already carry api routing (on main).
  6. Verify + smoke as P1.1-5/6. Half-flip hazard: if api stays empty after apply, every prod api.* query 404s — the smoke “content read+write + client RPC → 200” is the catch; re-apply if it fails.

P1.3 — PLATFORM PROD DB + PLATFORM STAGING [GO-GATE P2]

Section titled “P1.3 — PLATFORM PROD DB + PLATFORM STAGING [GO-GATE P2]”
  1. GO-GATE P2 — irreversible Platform-prod DDL. Highest blast radius (provisioning source-of-truth behind seed-tenant-from-bundle.ts / seed-platform-from-staging.ts).
  2. Resolve the Platform-prod ref from secrets (impl3: “lives in secrets; resolve at cutover”); link + cat verify; read its current exposed-schemas state BEFORE any change (impl3).
  3. Apply api migrations; exposed-schemas → api; provisioning/config.toml + Platform deploy.
  4. Verify + smoke against the Platform surface.
  5. Platform STAGING (S14 bake target) — same apply, lower stakes; can precede P1.3 as rehearsal.
  1. Regen (both schemas, deterministic public then api): /opt/homebrew/bin/supabase gen types typescript --project-id rovrymhhffssilaftdwd --schema public,api > supabase/types/database.types.ts; then tsc + bun build (matrix item 12). database.types.ts is an agent-denied-read path — regen + commit only.
  2. Fix the CI types-parity command (the types-parity red). .github/workflows/ci.yml:1185-1194 runs --schema public (single) and diffs the committed file (which carries api) → guaranteed fail. Change :1187 --schema public--schema public,api + the ::error:: hint :1194. Commit WITH the regenerated types.

Bake --schema public,api + db.schema='api' into provisioning (seed-tenant-from-bundle.ts + preview-branch + Platform + Platform-staging + Platform-prod); confirm the api-view coverage drift check runs in provisioning CI. Per impl3 S6: catalog-verify the 5 named api wrappers → mark S6 done.


PART 2 — ID-68 identity-guard De-ID — OPTION R vs OPTION S (owner picks)

Section titled “PART 2 — ID-68 identity-guard De-ID — OPTION R vs OPTION S (owner picks)”

P2.0 — Why identity-guard is RED (precise mechanism)

Section titled “P2.0 — Why identity-guard is RED (precise mechanism)”

identity-guard.yml runs on pull_request + push, git grep-ing the full tree for the secret-held KH_CLIENT_NAME_DENYLIST tokens minus a PENDING_DEID path-exclusion list (:95). The RED is one real client-named site outside that list: supabase/migrations/20260609093000_id68_21_drop_superseded_signup_hook.sql:21 = DROP FUNCTION IF EXISTS public.hook_restrict_signup_to_phew_domain(jsonb); (its CREATE counterpart is the FILENAME 20260424202806_capture_phew_domain_hook.sql).

Do NOT “fix” by adding the path to PENDING_DEID. Guard header :40-42: “Never ADD a path here to silence a red run — that is the exact re-introduction this guard exists to reject. New entries require an ID-68 spec amendment.” Both options below remove the leak so the path is enforced forever after.

OPTION R — Documented git filter-repo redaction (zero schema risk; RATIFIED)

Section titled “OPTION R — Documented git filter-repo redaction (zero schema risk; RATIFIED)”

Authoritative docs: PRE-FLIP-DEID-PLAN.md (decisions B+E), ID-68 TECH.md §PC-35/§PC-39(i), executable runbook ${KH_PRIVATE_DOCS_DIR}/src/content/docs/runbooks/id68-filter-repo-purge.md (§4) + id68-purge-redaction-map-draft.md. Mechanism = content/message redaction + a one-file --path-rename + the already-landed generic superseding migration; applied migrations NEVER removed.

Procedure (condensed from the runbook; execute from it — this is the GO map):

  1. Mirror backup (MANDATORY FIRST — AC-E5): git clone --mirror …knowledge-hub.git → tag pre-id68-purge → store OUTSIDE every tree → journal the absolute location (retains un-redacted history → client-confidential).
  2. Path inventory: bun scripts/generate-purge-path-inventory.ts --out ~/id68-purge-paths.txt (re-derive at purge time). The auth-hook migration is NEVER in the removal set (replay-break) — emitted as comment-only advisory.
  3. Finalise the redaction map (OQ-H = Liam ratifies): phew.org.uk→client.example, Phew Design Limited→Example Client Ltd, support@phew.org.uk, kh.phew.org.uk, the old hook name, the datacentre literal, the historical ICO number.
  4. Execute on a FRESH MIRROR (two passes): pass 1 --invert-paths --paths-from-file; pass 2 --force --replace-text --replace-message + (Branch A) --path-rename 20260424202806_capture_phew_domain_hook.sql:…_capture_signup_domain_hook.sql (14-digit version prefix UNCHANGED → migration-history table stays keyed-consistent).
    • Migration handling — BOTH branches keep the file applied. Branch A redacts content + renames + replay proof: from a clone of the rewritten mirror, supabase link --project-ref turayklvaunphgbgscat (STAGING, never prod) → supabase db reset replays clean + signup gate still passes; supabase migration repair if name drift. Branch B = HEAD-scrub-only, each accepted residual journalled as an Inv 36 carve-out.
  5. Verification clone BEFORE force-push (AC-E2): --analyze + git rev-list --objects --all | grep -f paths = zero + git log --all -S'<literal>' per map entry = zero.
  6. Force-push (git push --mirror): lift main branch protection via gh api …/branches/main/protection, push, restore the exact prior payload (re-verify identity-guard still required). GitHub retains pre-rewrite objects in refs/pull/* + cached views → close superseded PRs → GitHub Support GC before the {68.9} flip.
  7. Fleet re-clone (every SHA changed): salvage .env.local/supabase/.temp/ first; delete + re-clone main; delete all .claude/worktrees/*; supabase link re-verify; re-index gitnexus + ccc; accept orphaned branches/PRs/SHA-citations (journalled, not fixed).

Option R cost/risk: HIGH irreversibility (it IS the {68.10} purge — runs LAST, after Gates 1-4/6-8), ZERO schema/data risk (no migration deleted; replay preserved; backup-first). Migration-history leak removed by content redaction, not by collapsing the chain.

Scope note: {68.10} is the FULL-tree purge. If the owner wants identity-guard green NOW (before the whole flip is ready), R can run migration-slice-only as an interim (redact just 20260424202806/20260609093000/20260419212103:48 via scoped --replace-text+--path-rename on a mirror, same backup+replay-proof+force-push+re-clone discipline) — but this STILL triggers the full fleet re-clone (every SHA changes), so it is not cheaper than doing {68.10} once. Recommend: do {68.10} once when the flip gates are ready.

OPTION S — Fresh migration squash (clean public baseline; REVOKE blocker now REDUCED)

Section titled “OPTION S — Fresh migration squash (clean public baseline; REVOKE blocker now REDUCED)”

A fresh supabase migration squash (the Docker local-replay tool — NOT db dump) collapses 141→1; the client-named CREATE+DROP vanish (baseline reflects only final state) → identity-guard green by construction + a single clean public baseline. A local Docker dry-squash was run this session: 141→1, ZERO schema deltas (tables/columns/NOT NULL/CHECK/FK/indexes/function-body md5/RLS/triggers/enums) — squash IS schema-faithful. (The dry-squash baseline artefact is NOT recoverable from /tmp/$TMPDIR/worktrees — it was ephemeral; the measurements below were taken against the LIVE prod catalog instead, which is the authoritative source for pg_dump’s ACL-emission behaviour.)

The REVOKE question — MEASURED, not assumed (owner’s correction)

Section titled “The REVOKE question — MEASURED, not assumed (owner’s correction)”

Owner’s premise: ID-115’s explicit REVOKE/GRANT discipline made the functions’ ACLs non-default, so a fresh dump now emits them (vs the S176 baseline’s 3) → revoke-guard passes.

Measured against live prod rovrymhhffssilaftdwd (this dispatch):

  • 114 public functions; 114 have non-default proacl (non-NULL); 0 default; 22 SECURITY DEFINER. → pg_dump emits an explicit ACL block for ALL 114 (pg_dump omits a function’s ACL only when proacl IS NULL). The owner is RIGHT that the REVOKEs are no longer absent. The S176 3-REVOKE problem is GONE (S176 predates OPS-43; OPS-43 20260502143049 issued 131 REVOKEs across 102 fns + tightened pg_default_acl; subsequent migrations kept ACLs non-default).
  • Sample (6 of 7): hybrid_search/claim_next_job/get_workspace_counts/merge_entities/find_duplicate_pairs/get_content_win_ratepublic_has_execute = false, grantees authenticated, service_role (anon excluded). set_configpublic_has_execute = true (the lone intended anon-exec, INV-20).

THE REMAINING OBSTACLE (guard-regex layer, not ACL layer): pg_dump renders a non-default ACL with PUBLIC-revoked as REVOKE ALL ON FUNCTION public.<fn>(…) FROM PUBLIC; + per-role GRANT EXECUTE … TO authenticated, service_role;. But scripts/check-revoke-guard.ts:

  • REVOKE_FUNCTION_RE (:115) matches only REVOKE\s+EXECUTE\s+ON\s+FUNCTION → pg_dump’s REVOKE ALL … form does not match the regex at all.
  • matchRevokeForCreate (:566): if (!r.grantees.some((g) => g.toLowerCase() === 'anon')) continue; → requires the grantee list to literally contain anon; pg_dump emits FROM PUBLIC (no anon, because once PUBLIC is revoked anon’s inherited grant is gone). Fails the anon check even if the form matched.

A fresh squash baseline WOULD still trip revoke-guard — not because REVOKEs are missing (they’re emitted for all 114), but because they’re in REVOKE ALL … FROM PUBLIC form, which the guard’s strict REVOKE EXECUTE … FROM <…anon…> matcher rejects.

Option S costing — REVISED (blocker → normalisation step)

Section titled “Option S costing — REVISED (blocker → normalisation step)”

The fix is NOT REVOKE re-injection (my prior draft was wrong — the REVOKEs exist). It is one of two mechanical steps, both small:

  • (S-fix-a) Normalise the dump’s REVOKE form in the baseline — a scripted post-process turning each REVOKE ALL ON FUNCTION public.<fn>(…) FROM PUBLIC; into the guard-accepted REVOKE EXECUTE ON FUNCTION public.<fn>(…) FROM PUBLIC, anon; (semantically identical for these fns — they have no other PUBLIC privilege; ALL≡EXECUTE for a function, and naming anon is a no-op once PUBLIC is gone). One pass over the generated baseline, deterministic.
  • (S-fix-b) OR teach the guard the dump form — extend REVOKE_FUNCTION_RE + the grantee check to accept REVOKE ALL … FROM PUBLIC as satisfying the requirement (since revoking PUBLIC strictly dominates revoking anon). A guard-side change, lands in the same PR as the baseline. This is arguably the more correct fix (the guard’s anon-literal requirement is an OPS-43-era artefact; PUBLIC-revoke is stronger), but it widens what the guard accepts — review carefully so it cannot mask a genuinely-missing REVOKE in a future hand-written migration.

Residual Option S burden (S176 precedent, verbatim — NOT eliminated by the REVOKE finding): S176 had to recover “a missing table, 9 missing columns, 4 MCP-created functions, 46 ALTER FUNCTION signature fixes, 3 storage-bucket INSERTs, 6 storage RLS policies” + ~7 reconcile migrations (20260419095200..134609). A schema-only dump omits storage buckets, storage RLS policies, and dashboard-wired auth hooks — manual re-add. S176 cross-referenced LIVE PROD to find gaps and iterated until clean replay; the same gap-hunt is required. The dry-squash’s ZERO-delta was against a reference DB; the markdown_batch CHECK drift (20260615233000) surfaces if the reference DB is wrong.

Option S No-Go checklist (ALL required): owner GO + staging creds (foreground, dangerouslyDisableSandbox: true) + verified reference DB + revoke-guard normalisation plan (S-fix-a or S-fix-b) + storage-bucket/RLS/auth-hook re-add list + git clone --mirror backup + staging supabase db reset replay proof. (KH_CLIENT_NAME_DENYLIST is unset locally → de-ID validates only in CI.)

The REVOKE finding narrows the gap but does not flip the recommendation. Take Option R unless the owner wants a fresh consolidated baseline for its own sake.

  • Schema-risk axis: R = zero (no migration deleted, replay preserved). S = low-but-nonzero (the REVOKE issue is now a one-script normalisation, BUT the storage-bucket/RLS/auth-hook omission + reference-DB gap-hunt remain real per the S176 precedent — these are the actual S risks, not REVOKEs).
  • Operational-cost axis: ~parity — both force-push and trigger a full fleet re-clone (every SHA changes).
  • Documentation axis: R is documented end-to-end (id68-filter-repo-purge.md); S’s reconciliation + normalisation steps are not yet written.
  • End-state axis: S is cleaner (single public baseline, no redaction map to maintain) — the only axis where S wins.

Decision rule for the owner: if the goal is only identity-guard green → R (ratified, zero schema risk, do {68.10} once when flip-gates ready). If the owner also wants to retire the 141-file migration history into one clean baseline for maintainability → S is now genuinely viable (the REVOKE blocker is downgraded to a scripted step), but budget the storage/RLS/auth-hook reconciliation + reference-DB gap-hunt and write S-fix-a/b first.


GO-GATE LIST (Liam GO required; orchestrator surfaces)

Section titled “GO-GATE LIST (Liam GO required; orchestrator surfaces)”
#StepPartReversibility
GO-AClose GAP A — extend generator, re-run, local reset (flagged: gates the greening, no irreversibility)P1.0Reversible
GO-P1PROD DDL — apply api schema/views/RPCs to rovrymhhffssilaftdwdP1.2Additive (api new); app routes to api once deployed
GO-P2PLATFORM PROD DB DDL + exposed-schema flips (staging/Platform)P1.3As GO-P1; provisioning-source blast radius
GO-ROption R {68.10} purge — mirror backup → filter-repo rewrite → force-push → fleet re-cloneP2/RIRREVERSIBLE; backup-first + replay-proof
GO-SOption S fresh squash — ONLY if owner picks S, after the No-Go checklist (incl. revoke-guard normalisation S-fix-a/b + storage/RLS/auth-hook re-add)P2/SIRREVERSIBLE

No GO (reversible/additive): GAP-A generator change, staging DDL apply, local db reset, CI types-parity command edit, type regen, S14 bake.


Failing jobCleared byClassification / residual
identity-guardPART 2 (Option R redaction OR Option S squash) — removes the 20260609093000:21 client-named DROP from the grepped tree.PART-2-CLEARED by either option. Non-migration phew strings (fixtures/Python tests/kh.phew.org.uk) are in PENDING_DEID today; full green needs the full {68.10} (R) or squash + those paths cleaned — confirm the only OUTSIDE-PENDING_DEID hit is the migration site (else still red on other paths).
CI: types-parityP1.4.2 ci.yml:1187--schema public,api + P1.4.1 regen.PART-1-CLEARED.
CI: integration (9)P1.2 api apply + GAP A api._test_delete_broken_auth_user.PART-1-CLEARED ONLY IF GAP A closed. Apply alone does NOT add the missing RPC.
CI: MCP eval-l4 (2 of 3)P1.2 api apply + GAP A api.find_duplicate_pairs (FC-44) + api.get_content_win_rate (FC-51).PART-1-CLEARED ONLY IF GAP A closed. FC-63 (missing seed content-item UUID) is SEPARATE — seed-data fix, not schema.
CI: e2e (4)guide-pages.spec (h1/TOC) + workspaces.spec (card/grid). get_workspace_counts IS in the surface (one of the 5 entrypoints) → workspaces.spec LIKELY green; guide-pages depends on a guide-content RPC — CONFIRM it is in RPC_NAMES (candidate GAP-A extension).PARTIALLY PART-1-CLEARED — CONFIRM via the P1.1 staging E2E run. guide-pages may need a guide RPC added (GAP-A extension).
CI: advisorsAdvisor 0010 (security_invoker) by-construction green post-api (every api view security_invoker=true; generator self-checks :563).SEPARATE unless the failure is a 0010 / api-grant finding. Pull the advisor diff to classify.
CI: pytestPython pipeline writes public.* via asyncpg search_path (INV-14, untouched).SEPARATE — unrelated (impl3: 1306 pass + 1 test_cocoindex_server async-mock fail). Diagnose independently.
CI: ci-summaryAggregates the above (ci.yml:1304+).DERIVED — greens once deps (incl. types-parity :1328/1343) green.

Net: Part 1 clears 4 (types-parity + integration-9 + eval-2, CONTINGENT ON GAP A) + partially e2e-4 (confirm guide RPC). Part 2 clears identity-guard (R or S; confirm migration is the sole outside-PENDING_DEID hit). advisors + pytest SEPARATE pending their finding; eval FC-63 + ci-summary SEPARATE/derived. The two parts are independent — Part 1 greens Cluster A without touching the de-ID; identity-guard is greened by the owner’s R-or-S choice on its own timeline.


  • Part 1 additive + reversible: api schema new, no data touched, failed apply re-applies. Half-flip hazard: never flip a remote’s exposed-schemas to api before its api objects exist (prod is the existing half-flip; the migration completes it). P1.2 smoke is the catch.
  • Part 2 Option R: the documented {68.10} — irreversible, backup-first non-negotiable, replay-proof on STAGING (never prod), force-push lifts+restores branch protection, full fleet re-clone. ZERO schema/data risk.
  • Part 2 Option S: REVOKE form mismatch trips revoke-guard (fix via S-fix-a normalisation OR S-fix-b guard widening — NOT re-injection; the ACLs are present, 114/114 non-default); storage buckets + storage RLS policies + dashboard auth hooks NOT in a schema-only dump (S176 re-added 3+6+hook) → manual reconcile; verify the reference DB (markdown_batch CHECK drift if wrong); a no-DB dry-run is impossible (lexical concat ≠ squash).
  • Live hazard (NOW): supabase/.temp/project-ref = rovrymhhffssilaftdwd (PROD). Any unverified db push from the main checkout lands on PROD silently. First action of every apply phase = relink + cat verify.
  • Worktree/sandbox (ID-115 impl3): id-115 worktree needs the server-only stub for bun run test. Local DB/REST/CLI + git commit need dangerouslyDisableSandbox: true. db push interactive-foreground-only. Clear <main>/.git/worktrees/<wt>/index.lock if a commit dies. gitnexus post-commit hook races .git/index.lock on rapid cherry-picks — git -c core.hooksPath=$(mktemp -d) for multi-commit runs.
  • KH_CLIENT_NAME_DENYLIST unset locally → de-ID (either option) validates only in CI.

LEDGER-WRITE INTENTS (orchestrator applies on MAIN — do NOT write here)

Section titled “LEDGER-WRITE INTENTS (orchestrator applies on MAIN — do NOT write here)”
WhenIntent
Pre-executionOpen a GAP-A tracking subtask under ID-115 (extend generator RPC surface for _test_delete_broken_auth_user / find_duplicate_pairs / get_content_win_rate / guide-content RPC) — prerequisite of S13, sibling to S12/S13.
After P1.1 stagingJournal ID-115 {115.13}: staging api applied + flipped + smoke GREEN + full E2E (record e2e-4 classification).
After P1.2 prodJournal {115.13}: prod half-flip completed (api populated), smoke GREEN.
After P1.3 PlatformJournal {115.13}: Platform-prod + Platform-staging applied + flipped + smoke GREEN → flip {115.13}done (Checker-gated).
After P1.4/P1.5Journal the ci.yml --schema public,api fix; {115.14} S14 bake; flip {115.6}/{115.12}done per impl3.
After Part 2 (R or S)Option R: journal ID-68 {68.10} — backup location, inventory HEAD SHA, ratified map (OQ-H), Branch A/B + replay-proof, verification outputs, protection-restore + GC. Do NOT flip {68.9} done (its own Gate 1-9 table beyond identity-guard). Option S: journal a new squash subtask — revoke-guard normalisation (S-fix-a/b), reference-DB, storage/RLS/auth-hook re-add, reconcile migrations.

Appendix — verified facts (this dispatch; empirical results bolded)

Section titled “Appendix — verified facts (this dispatch; empirical results bolded)”
  • Live prod rovrymhhffssilaftdwd: 114 public functions, 114 non-default proacl, 0 default, 22 SECURITY DEFINER → pg_dump emits ACLs for ALL 114 (owner’s “REVOKE now emitted” CONFIRMED). Sample: 6/7 public_has_execute=false (anon excluded), set_config =true (sole anon-exec, INV-20).
  • check-revoke-guard.ts: REVOKE_FUNCTION_RE (:115) matches only REVOKE EXECUTE ON FUNCTION; matchRevokeForCreate (:566) requires grantee list to contain literal anon. pg_dump’s REVOKE ALL … FROM PUBLIC form satisfies NEITHER → squash still trips the guard UNLESS normalised (S-fix-a) or the guard is widened (S-fix-b). Revoke-guard is diff-scoped (git diff --name-only <baseline> HEAD).
  • S176 baseline 20260416102457 has 0 REVOKE EXECUTE (predates OPS-43); OPS-43 20260502143049 = 131 REVOKEs / 102 fns + pg_default_acl tightening.
  • identity-guard.yml greps full tree minus PENDING_DEID (:95); header :40-42 forbids adding paths to silence reds. 20260609093000:21 DROP is the outside-PENDING_DEID hit.
  • id68-filter-repo-purge.md = the executable Option-R procedure (mirror backup → 2-pass filter-repo → --path-rename keeps the migration applied → staging replay-proof → force-push → fleet re-clone).
  • generate-api-views.ts RPC_NAMES = SURFACE_RPCS (−3 MISSING_RPCS) + 7 EXTRA_DEFINER_RPCS. ABSENT: _test_delete_broken_auth_user, find_duplicate_pairs, get_content_win_rate (GAP A; all 3 verified present in public). PRESENT: the 5 named entrypoints.
  • ci.yml:1187 = --schema public (single) — the types-parity red. config.toml:15 = schemas=["api"]; :83-85 staging hook → generic (§PC-39(i) staging DONE).
  • supabase/.temp/project-ref = rovrymhhffssilaftdwd (PROD) — drift hazard LIVE.
  • 141 migration files; worktree id-115-data-api-schema-isolation tip 71c68ad0e (+1 vs main: api-views search_path pin). The earlier dry-squash baseline artefact is NOT recoverable from disk (ephemeral) — REVOKE measurement taken against the live catalog instead.
  • mempalace precedent: S176 squash cross-referenced LIVE PROD, schema-only, needed reconcile for “a missing table, 9 missing columns, 4 MCP fns, 46 ALTER FUNCTION sig fixes, 3 storage INSERTs, 6 storage RLS policies”.