← back to Homesonspec
Rehearse and harden ValidationEvent partition conversion
90b58b816c9832c099e9cd60993fa8894c9d3aca · 2026-09-11 08:34:07 -0700 · Steve Abrams
Files touched
M ops/o3b-apply-validationevent.shA ops/o3b/README.mdA ops/o3b/apply.sqlA ops/o3b/prepare.sqlA ops/o3b/rehearse.pyA ops/o3b/rollback.sqlA ops/o3b/verify.sqlA verification/TK-11364/app-before.jsonlA verification/TK-11364/dtd-claude.txtA verification/TK-11364/dtd-codex-debate.txtA verification/TK-11364/dtd-codex.txtA verification/TK-11364/dtd-grok.txtA verification/TK-11364/dtd-kimi.txtA verification/TK-11364/dtd-muse.txtA verification/TK-11364/dtd-question.txtA verification/TK-11364/dtd-qwen.txtA verification/TK-11364/dtd-verdict.mdA verification/TK-11364/original-o3b.shA verification/TK-11364/scratch-proof.json
Diff
commit 90b58b816c9832c099e9cd60993fa8894c9d3aca
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 11 08:34:07 2026 -0700
Rehearse and harden ValidationEvent partition conversion
---
ops/o3b-apply-validationevent.sh | 122 +++++-----------
ops/o3b/README.md | 60 ++++++++
ops/o3b/apply.sql | 57 ++++++++
ops/o3b/prepare.sql | 25 ++++
ops/o3b/rehearse.py | 90 ++++++++++++
ops/o3b/rollback.sql | 19 +++
ops/o3b/verify.sql | 40 ++++++
verification/TK-11364/app-before.jsonl | 4 +
verification/TK-11364/dtd-claude.txt | 1 +
verification/TK-11364/dtd-codex-debate.txt | 15 ++
verification/TK-11364/dtd-codex.txt | 3 +
verification/TK-11364/dtd-grok.txt | 1 +
verification/TK-11364/dtd-kimi.txt | 1 +
verification/TK-11364/dtd-muse.txt | 1 +
verification/TK-11364/dtd-question.txt | 1 +
verification/TK-11364/dtd-qwen.txt | 11 ++
verification/TK-11364/dtd-verdict.md | 19 +++
verification/TK-11364/original-o3b.sh | 88 ++++++++++++
verification/TK-11364/scratch-proof.json | 218 +++++++++++++++++++++++++++++
19 files changed, 690 insertions(+), 86 deletions(-)
diff --git a/ops/o3b-apply-validationevent.sh b/ops/o3b-apply-validationevent.sh
index d29f5b8a..69503e9f 100755
--- a/ops/o3b-apply-validationevent.sh
+++ b/ops/o3b-apply-validationevent.sh
@@ -1,88 +1,38 @@
#!/usr/bin/env bash
-# O3b — ValidationEvent partition conversion (ATTACH path, no table rewrite).
-# TK-11363. Uses the CORRECTED pattern from the 2026-09-10 SourceEvidence run:
-# - per-partition FKs (a parent FK forces a full revalidation under ACCESS EXCLUSIVE)
-# - GRANTs on parent + every partition (postgres-created tables have no app-role ACL)
-# - an app-level assertion, not just database checks
+# TK-11364 approved native partition conversion. Run on the production DB host.
+# SQL shared verbatim with the private PostgreSQL rehearsal.
set -euo pipefail
-PSQL="sudo -u postgres psql -d homesonspec -v ON_ERROR_STOP=1"
-Q() { sudo -u postgres psql -d homesonspec -tAc "$1"; }
-say() { printf '\n\033[1;36m=== %s ===\033[0m\n' "$*"; }
-
-say "PREFLIGHT"
-DUMP=$(ls -t /root/backups/db/homesonspec_*.dump 2>/dev/null | head -1 || true)
-[ -n "$DUMP" ] || { echo "ABORT: no homesonspec dump"; exit 1; }
-AGE=$(( ( $(date +%s) - $(stat -c %Y "$DUMP") ) / 3600 ))
-echo "backup: $(basename "$DUMP") ${AGE}h old"
-[ "$AGE" -lt 48 ] || { echo "ABORT: dump ${AGE}h old"; exit 1; }
-FREE=$(df -BG --output=avail / | tail -1 | tr -dc '0-9')
-echo "disk free: ${FREE}G"
-[ "$FREE" -ge 15 ] || { echo "ABORT: <15G free"; exit 1; }
-BEFORE=$(Q 'SELECT count(*) FROM "ValidationEvent";')
-echo "ValidationEvent rows: $BEFORE"
-echo "max runAt: $(Q 'SELECT max("runAt") FROM "ValidationEvent";')"
-
-say "STEP 1 — bound constraint + VALIDATE (online, SHARE UPDATE EXCLUSIVE, does NOT block reads/writes)"
-$PSQL -c 'ALTER TABLE "ValidationEvent" DROP CONSTRAINT IF EXISTS ve_legacy_bound;'
-$PSQL -c "ALTER TABLE \"ValidationEvent\" ADD CONSTRAINT ve_legacy_bound CHECK (\"runAt\" < TIMESTAMP '2026-10-01 00:00:00') NOT VALID;"
-time $PSQL -c 'ALTER TABLE "ValidationEvent" VALIDATE CONSTRAINT ve_legacy_bound;'
-[ "$(Q "SELECT convalidated FROM pg_constraint WHERE conname='ve_legacy_bound';")" = "t" ] \
- || { echo "ABORT: not validated"; exit 1; }
-echo "STEP 1 OK (rollback here: ALTER TABLE \"ValidationEvent\" DROP CONSTRAINT ve_legacy_bound;)"
-
-say "STEP 2 — swap in partitioned parent + ATTACH (catalog-only; NO parent FK => no revalidation)"
-$PSQL <<'SQL'
-BEGIN;
-SET LOCAL lock_timeout = '10s';
-ALTER TABLE "ValidationEvent" RENAME TO "ValidationEvent_p_legacy";
-ALTER INDEX "ValidationEvent_pkey" RENAME TO "ValidationEvent_p_legacy_pkey";
-ALTER INDEX "ValidationEvent_stagedRecordId_idx" RENAME TO "VE_p_legacy_staged_idx";
-
--- PK-less parent: keeps the legacy partition's local PK, avoids a composite-PK change.
-CREATE TABLE "ValidationEvent" (
- LIKE "ValidationEvent_p_legacy" INCLUDING DEFAULTS INCLUDING STORAGE
-) PARTITION BY RANGE ("runAt");
-CREATE INDEX "ValidationEvent_stagedRecordId_idx" ON "ValidationEvent" ("stagedRecordId");
-
-ALTER TABLE "ValidationEvent" ATTACH PARTITION "ValidationEvent_p_legacy"
- FOR VALUES FROM (MINVALUE) TO (TIMESTAMP '2026-10-01 00:00:00');
-
-CREATE TABLE "ValidationEvent_p_default" PARTITION OF "ValidationEvent" DEFAULT;
-ALTER TABLE "ValidationEvent_p_default"
- ADD CONSTRAINT "VE_p_default_stagedRecordId_fkey"
- FOREIGN KEY ("stagedRecordId") REFERENCES "StagedRecord"(id);
-
-GRANT ALL PRIVILEGES ON TABLE "ValidationEvent" TO homesonspec;
-GRANT ALL PRIVILEGES ON TABLE "ValidationEvent_p_default" TO homesonspec;
-COMMIT;
-SQL
-
-say "STEP 3 — forward monthly partitions through 2027-12 (+ FK + GRANT each)"
-for y in 2026 2027; do
- for m in 01 02 03 04 05 06 07 08 09 10 11 12; do
- s="$y-$m-01"
- [ "$s" \< "2026-10-01" ] && continue
- n=$(date -d "$s +1 month" +%Y-%m-01)
- T="ValidationEvent_p_${y}_${m}"
- $PSQL -c "CREATE TABLE IF NOT EXISTS \"$T\" PARTITION OF \"ValidationEvent\" FOR VALUES FROM (TIMESTAMP '$s 00:00:00') TO (TIMESTAMP '$n 00:00:00');" >/dev/null
- $PSQL -c "ALTER TABLE \"$T\" ADD CONSTRAINT \"${T}_sr_fkey\" FOREIGN KEY (\"stagedRecordId\") REFERENCES \"StagedRecord\"(id);" >/dev/null 2>&1 || true
- $PSQL -c "GRANT ALL PRIVILEGES ON TABLE \"$T\" TO homesonspec;" >/dev/null
- done
-done
-echo "forward partitions ready"
-
-say "VERIFY"
-AFTER=$(Q 'SELECT count(*) FROM "ValidationEvent";')
-echo "rows before: $BEFORE / after: $AFTER"
-[ "$AFTER" -ge "$BEFORE" ] || { echo "*** ABORT: ROW LOSS ***"; exit 1; }
-UNGRANTED=$(Q "SELECT count(*) FROM pg_class c WHERE (c.relname='ValidationEvent' OR c.oid IN (SELECT inhrelid FROM pg_inherits WHERE inhparent=(SELECT oid FROM pg_class WHERE relname='ValidationEvent'))) AND (c.relacl IS NULL OR NOT c.relacl::text LIKE '%homesonspec%');")
-echo "partitions WITHOUT app grant (want 0): $UNGRANTED"
-[ "$UNGRANTED" = "0" ] || { echo "*** ABORT: ungranted partitions -> app will 42501 ***"; exit 1; }
-echo "rows in DEFAULT (want 0): $(Q 'SELECT count(*) FROM "ValidationEvent_p_default";')"
-echo "--- FK enforced? (expect ERROR) ---"
-sudo -u postgres psql -d homesonspec -c "INSERT INTO \"ValidationEvent\" (id,\"stagedRecordId\",\"ruleId\",severity,passed,\"validatorVersion\",\"runAt\") VALUES ('o3bprobe','__NO_SR__','r','error',false,'v',now());" 2>&1 | head -2
-echo "--- APP-ROLE query test: the exact admin page shape, as the app user ---"
-sudo -u postgres psql -d homesonspec -c "SET ROLE homesonspec; SELECT count(*) AS admin_rows FROM (SELECT id FROM \"ValidationEvent\" WHERE passed=false ORDER BY \"runAt\" DESC LIMIT 100) t;" 2>&1 | head -4
-echo "--- partition count ---"
-$PSQL -c "SELECT count(*) AS partitions FROM pg_inherits WHERE inhparent='\"ValidationEvent\"'::regclass;"
-say "DONE — ValidationEvent partitioned. No data copied, no data deleted."
+[[ "${1:-}" == "--approved-TK-11364" ]] || { echo "Usage: $0 --approved-TK-11364 (requires explicit operator authorization)"; exit 64; }
+HERE=$(cd -- "$(dirname -- "$0")" && pwd)
+P=(runuser -u postgres -- psql -X -v ON_ERROR_STOP=1 -d homesonspec)
+[[ $(id -u) == 0 ]] || { echo "Run as the database-host operator"; exit 1; }
+[[ $("${P[@]}" -Atc "SELECT current_database()") == homesonspec ]] || exit 1
+KIND=$("${P[@]}" -Atc "SELECT relkind FROM pg_class WHERE oid='public.\"ValidationEvent\"'::regclass")
+if [[ "$KIND" == p ]]; then
+ echo "Already partitioned: verification only"
+ exec "${P[@]}" -f "$HERE/o3b/verify.sql"
+fi
+[[ "$KIND" == r ]] || { echo "Unexpected table kind"; exit 1; }
+[[ $(date -u +%Y-%m-%d) < 2026-10-01 ]] || { echo "Fixed legacy cutoff expired; revise and rehearse bounds"; exit 1; }
+FREE=$(df -B1 --output=avail / | tail -1 | tr -d ' ')
+(( FREE >= 15*1024*1024*1024 )) || { echo "Insufficient headroom"; exit 1; }
+# Validate the current archive, not just its name. This is read-only decompression.
+DUMP=$(python3 -c 'import glob,os; p=glob.glob("/root/backups/db/homesonspec_*.dump"); print(max(p,key=os.path.getmtime) if p else "")')
+[[ -n "$DUMP" ]] || { echo "No backup"; exit 1; }
+BEFORE_BACKUP=$(stat -c '%Y:%s' "$DUMP")
+AGE=$(( $(date +%s) - $(stat -c %Y "$DUMP") ))
+(( AGE >= 0 && AGE < 48*3600 )) || { echo "Stale backup"; exit 1; }
+pg_restore --list "$DUMP" | awk '/TABLE DATA public ValidationEvent / {found=1} END {exit !found}'
+timeout 10m pg_restore --data-only -t ValidationEvent -f /dev/null "$DUMP"
+[[ $(stat -c '%Y:%s' "$DUMP") == "$BEFORE_BACKUP" ]] || { echo "Backup changed during validation"; exit 1; }
+echo "Validated backup: $DUMP ($BEFORE_BACKUP)"
+# The app baseline is recorded separately in the run evidence.
+# Do not hide errors or allow scans/admin queries under the cutover lock.
+"${P[@]}" -f "$HERE/o3b/prepare.sql"
+if ! timeout --signal=TERM --kill-after=5s 20s "${P[@]}" -v failpoint=none -f "$HERE/o3b/apply.sql"; then
+ echo "CUTOVER FAILED. Transaction rolls back. Inspect topology before retry."
+ echo "Original heap may retain TK-11364 ve_legacy_bound; explicit abandonment SQL is in the runbook."
+ exit 1
+fi
+"${P[@]}" -f "$HERE/o3b/verify.sql"
+echo "DATABASE VERIFIED; authenticated admin + public HTTP and monitoring are required before ticket completion."
diff --git a/ops/o3b/README.md b/ops/o3b/README.md
new file mode 100644
index 00000000..d3943d48
--- /dev/null
+++ b/ops/o3b/README.md
@@ -0,0 +1,60 @@
+# ValidationEvent native partition conversion — TK-11364
+
+Steve authorized execution with “ungate and run” on 2026-09-11. The earlier
+TK-11364 HOLD describes the superseded candidate. This implementation adopts
+the corrected SQL pattern from the independently rehearsed TK-11363 candidate,
+with production-specific changes: no counts or admin scans under the cutover
+lock, original heap/index identity assertions, PKs on every new child, and a
+20-second client watchdog around 5-second statement budgets.
+
+Run `python3 ops/o3b/rehearse.py` for a private PostgreSQL 14 fixture. It runs
+the exact prepare/apply/verify/rollback SQL, retains evidence in `/private/tmp`,
+and stops its cluster. Tests cover rename/timeout/FK failure and retry, all
+17 routes, FK CASCADE/RESTRICT behavior, original heap and complete row
+preservation, partition pruning, DETACH/re-ATTACH, and data-retaining rollback.
+
+On the approved production host, deploy this directory alongside
+`ops/o3b-apply-validationevent.sh`, then run the wrapper with
+`--approved-TK-11364`. It validates the latest backup's table data and age,
+checks resource headroom and the fixed cutoff, validates the bound online,
+performs an atomic catalog swap, then verifies outside the exclusive lock.
+An already-applied run performs verification only. Real authenticated admin
+pages and the public detail page must be checked separately before completion.
+
+The legacy heap, its original PK, and stagedRecordId index remain physically
+unchanged. New monthly children have local PK(id), the original FK actions,
+and app grants. The parent deliberately has no global PK, as in SourceEvidence;
+IDs are unique within each partition, not enforced globally by PostgreSQL.
+The actual app uses stagedRecordId for deleteMany/createMany and reads the
+validation log ordered by runAt; it has no ValidationEvent findUnique(id) call.
+No retention deletion, archive job, or extension is installed. Bounds cover
+October 2026 through December 2027; the default partition catches later dates.
+Future partition creation must preserve these PK/FK/grant conventions.
+
+## Failure and rollback
+
+Before cutover, a failure leaves the original heap and its validated bound.
+Retry accepts only this task's tagged bound. If abandoning rather than retrying,
+inspect the original heap OID and the constraint comment/definition first, then
+remove only the `ve_legacy_bound` created by this task with a bounded ALTER.
+Do not remove a constraint on an already-partitioned table or a foreign marker.
+
+The swap is one transaction. A statement or watchdog timeout rolls it back;
+confirm no remaining TK-11364 cutover backend before retrying. The watchdog
+is an outer psql-process deadline; server-side statement and idle-transaction
+timeouts provide independent bounds after a disconnected client.
+
+Post-cutover rollback is `rollback.sql`, invoked explicitly with writers quiet.
+It copies forward/default rows into the original heap, restores the original
+name and index names, and retains the former parent and forward rows as
+`ValidationEvent_rollback_retained`. It never drops history. Duplicate IDs
+across partitions make this rollback fail atomically, preserving the entire
+partitioned state; resolve that exceptional condition separately, never delete
+conflicting history to force recovery. Both paths are rehearsed.
+
+Counts taken before and after concurrent ingestion are observations, not an
+exact row-loss proof. The identity assertions prove the original storage was
+retained without data DML; independent same-snapshot parent/child counts and
+runtime probes verify the application can still reach it.
+
+Design reference: PostgreSQL 14 [partition maintenance documentation](https://www.postgresql.org/docs/14/ddl-partitioning.html#DDL-PARTITIONING-DECLARATIVE-MAINTENANCE).
diff --git a/ops/o3b/apply.sql b/ops/o3b/apply.sql
new file mode 100644
index 00000000..4b5930b8
--- /dev/null
+++ b/ops/o3b/apply.sql
@@ -0,0 +1,57 @@
+\set ON_ERROR_STOP on
+SET application_name = 'TK-11364-o3b-cutover';
+SET search_path = public, pg_catalog;
+SET lock_timeout = '2s';
+SET statement_timeout = '5s';
+SET idle_in_transaction_session_timeout = '10s';
+BEGIN;
+LOCK TABLE "ValidationEvent" IN ACCESS EXCLUSIVE MODE;
+CREATE TEMP TABLE o3b_identity ON COMMIT DROP AS
+ SELECT oid,relfilenode,
+ '"ValidationEvent_pkey"'::regclass::oid AS pk_oid,
+ '"ValidationEvent_stagedRecordId_idx"'::regclass::oid AS index_oid
+ FROM pg_class WHERE oid='"ValidationEvent"'::regclass;
+DO $$ BEGIN
+ IF NOT EXISTS (SELECT FROM pg_constraint WHERE conrelid='"ValidationEvent"'::regclass AND conname='ve_legacy_bound' AND convalidated) THEN
+ RAISE EXCEPTION 'validated legacy bound required'; END IF;
+ IF EXISTS (SELECT FROM pg_constraint WHERE contype='f' AND confrelid='"ValidationEvent"'::regclass) THEN
+ RAISE EXCEPTION 'unexpected incoming FK'; END IF;
+END $$;
+SELECT set_config('o3b.failpoint', :'failpoint', true);
+ALTER TABLE "ValidationEvent" RENAME TO "ValidationEvent_p_legacy";
+DO $$ BEGIN
+ IF current_setting('o3b.failpoint')='after_rename' THEN RAISE EXCEPTION 'injected rename failure'; END IF;
+ IF current_setting('o3b.failpoint')='timeout' THEN PERFORM pg_sleep(6); END IF;
+END $$;
+ALTER INDEX "ValidationEvent_pkey" RENAME TO "ValidationEvent_p_legacy_pkey";
+ALTER INDEX "ValidationEvent_stagedRecordId_idx" RENAME TO "VE_p_legacy_staged_idx";
+CREATE TABLE "ValidationEvent" (LIKE "ValidationEvent_p_legacy" INCLUDING DEFAULTS INCLUDING STORAGE) PARTITION BY RANGE ("runAt");
+CREATE INDEX "ValidationEvent_stagedRecordId_idx" ON "ValidationEvent" ("stagedRecordId");
+-- No parent FK: attaching must not revalidate the existing FK across 71M rows.
+ALTER TABLE "ValidationEvent" ATTACH PARTITION "ValidationEvent_p_legacy"
+ FOR VALUES FROM (MINVALUE) TO (TIMESTAMP '2026-10-01');
+DO $$ DECLARE d timestamp; t text; BEGIN
+ FOR d IN SELECT generate_series(TIMESTAMP '2026-10-01',TIMESTAMP '2027-12-01',INTERVAL '1 month') LOOP
+ t := 'ValidationEvent_p_' || to_char(d,'YYYY_MM');
+ EXECUTE format('CREATE TABLE %I PARTITION OF "ValidationEvent" FOR VALUES FROM (%L) TO (%L)',t,d,d+INTERVAL '1 month');
+ END LOOP;
+ CREATE TABLE "ValidationEvent_p_default" PARTITION OF "ValidationEvent" DEFAULT;
+ FOR t IN SELECT c.relname FROM pg_inherits i JOIN pg_class c ON c.oid=i.inhrelid
+ WHERE i.inhparent='"ValidationEvent"'::regclass AND c.relname<>'ValidationEvent_p_legacy' LOOP
+ EXECUTE format('ALTER TABLE %I ADD PRIMARY KEY (id)',t);
+ IF current_setting('o3b.failpoint')='fk' THEN RAISE EXCEPTION 'injected FK DDL failure'; END IF;
+ EXECUTE format('ALTER TABLE %I ADD CONSTRAINT %I FOREIGN KEY ("stagedRecordId") REFERENCES "StagedRecord"(id) ON UPDATE CASCADE ON DELETE RESTRICT',t,t||'_sr_fkey');
+ EXECUTE format('GRANT ALL PRIVILEGES ON %I TO homesonspec',t);
+ END LOOP;
+ GRANT ALL PRIVILEGES ON "ValidationEvent", "ValidationEvent_p_legacy" TO homesonspec;
+END $$;
+DO $$ BEGIN
+ IF (SELECT count(*) FROM pg_inherits WHERE inhparent='"ValidationEvent"'::regclass)<>17 THEN RAISE EXCEPTION 'partition count'; END IF;
+ IF NOT EXISTS (SELECT FROM o3b_identity s JOIN pg_class c ON c.oid=s.oid
+ WHERE c.oid='"ValidationEvent_p_legacy"'::regclass AND c.relfilenode=s.relfilenode
+ AND s.pk_oid='"ValidationEvent_p_legacy_pkey"'::regclass
+ AND s.index_oid='"VE_p_legacy_staged_idx"'::regclass) THEN
+ RAISE EXCEPTION 'original heap or indexes replaced'; END IF;
+END $$;
+TABLE o3b_identity;
+COMMIT;
diff --git a/ops/o3b/prepare.sql b/ops/o3b/prepare.sql
new file mode 100644
index 00000000..56a1905b
--- /dev/null
+++ b/ops/o3b/prepare.sql
@@ -0,0 +1,25 @@
+\set ON_ERROR_STOP on
+SET application_name = 'TK-11364-o3b-prepare';
+SET search_path = public, pg_catalog;
+SET lock_timeout = '2s';
+SET statement_timeout = '5s';
+BEGIN;
+LOCK TABLE "ValidationEvent" IN ACCESS EXCLUSIVE MODE;
+DO $$ BEGIN
+ IF (SELECT relkind FROM pg_class WHERE oid='"ValidationEvent"'::regclass) <> 'r'
+ THEN RAISE EXCEPTION 'expected original heap; do not rerun an applied migration'; END IF;
+ IF EXISTS (SELECT FROM pg_constraint WHERE contype='f' AND confrelid='"ValidationEvent"'::regclass)
+ THEN RAISE EXCEPTION 'incoming FK requires separate migration'; END IF;
+ IF (SELECT count(*) FROM pg_constraint WHERE conrelid='"ValidationEvent"'::regclass AND contype='f' AND confrelid='"StagedRecord"'::regclass AND convalidated AND confupdtype='c' AND confdeltype='r') <> 1
+ THEN RAISE EXCEPTION 'unexpected source FK'; END IF;
+ IF NOT EXISTS (SELECT FROM pg_constraint WHERE conrelid='"ValidationEvent"'::regclass AND conname='ve_legacy_bound') THEN
+ ALTER TABLE "ValidationEvent" ADD CONSTRAINT ve_legacy_bound CHECK ("runAt" < TIMESTAMP '2026-10-01') NOT VALID;
+ COMMENT ON CONSTRAINT ve_legacy_bound ON "ValidationEvent" IS 'TK-11364 approved O3b bound';
+ ELSIF (SELECT obj_description(oid,'pg_constraint') FROM pg_constraint WHERE conrelid='"ValidationEvent"'::regclass AND conname='ve_legacy_bound') IS DISTINCT FROM 'TK-11364 approved O3b bound' THEN
+ RAISE EXCEPTION 'foreign bound constraint; inspect before proceeding';
+ END IF;
+END $$;
+COMMIT;
+-- Online heap scan. Never perform this while holding ACCESS EXCLUSIVE.
+SET statement_timeout = '10min';
+ALTER TABLE "ValidationEvent" VALIDATE CONSTRAINT ve_legacy_bound;
diff --git a/ops/o3b/rehearse.py b/ops/o3b/rehearse.py
new file mode 100644
index 00000000..76fdfe22
--- /dev/null
+++ b/ops/o3b/rehearse.py
@@ -0,0 +1,90 @@
+"""Exercise the exact production SQL on a private, retained PG14 fixture."""
+import hashlib
+import json
+import os
+from pathlib import Path
+import subprocess
+import tempfile
+
+PG = Path('/opt/homebrew/opt/postgresql@14/bin')
+SQL = Path(__file__).resolve().parent
+ROOT = Path(tempfile.mkdtemp(prefix='TK11364-pg-', dir='/private/tmp'))
+(ROOT / 'socket').mkdir()
+ENV = {k:v for k,v in os.environ.items() if not k.startswith('PG')}
+checks = []
+started = False
+
+def run(label, argv, expected=0):
+ p = subprocess.run([str(x) for x in argv], env=ENV, capture_output=True, text=True, timeout=90)
+ (ROOT / (label+'.txt')).write_text(p.stdout+p.stderr)
+ ok = p.returncode == 0 if expected == 0 else p.returncode != 0
+ checks.append({'name':label, 'ok':ok, 'exit':p.returncode})
+ assert ok, (label,p.stdout,p.stderr)
+ return p.stdout.strip()
+
+def sql(label, query, expected=0):
+ return run(label, P+['-Atc',query], expected)
+
+def file(label, name, fail='none', expected=0):
+ return run(label, P+['-v','failpoint='+fail,'-f',SQL/name], expected)
+
+try:
+ run('init', [PG/'initdb','-D',ROOT/'data','-U','owner','-A','trust','--no-locale'])
+ run('start',[PG/'pg_ctl','-D',ROOT/'data','-l',ROOT/'server.txt','-o',f"-k {ROOT/'socket'} -p 55449 -c listen_addresses=''",'-w','start'])
+ started=True
+ run('createdb',[PG/'createdb','-h',ROOT/'socket','-p','55449','-U','owner','o3b_scratch'])
+ P=[PG/'psql','-X','-h',ROOT/'socket','-p','55449','-U','owner','-d','o3b_scratch','-v','ON_ERROR_STOP=1']
+ sql('fixture','''CREATE ROLE homesonspec;
+CREATE TABLE "StagedRecord"(id text PRIMARY KEY);
+INSERT INTO "StagedRecord" VALUES ('sr');
+GRANT ALL ON "StagedRecord" TO homesonspec;
+CREATE TABLE "ValidationEvent" (id text PRIMARY KEY,"stagedRecordId" text NOT NULL REFERENCES "StagedRecord"(id) ON UPDATE CASCADE ON DELETE RESTRICT,"ruleId" text NOT NULL,severity text NOT NULL,passed boolean NOT NULL,message text,details jsonb,"validatorVersion" text NOT NULL,"runAt" timestamp(3) NOT NULL DEFAULT now());
+CREATE INDEX "ValidationEvent_stagedRecordId_idx" ON "ValidationEvent"("stagedRecordId");
+GRANT ALL ON "ValidationEvent" TO homesonspec;
+INSERT INTO "ValidationEvent" SELECT 'seed-'||g,'sr','r','error',g%2=0,NULL,CASE WHEN g%2=0 THEN NULL ELSE 'null'::jsonb END,'v',TIMESTAMP '2026-09-01' FROM generate_series(1,5000)g;''')
+ digest="SELECT md5(string_agg(v::text,',' ORDER BY id)) FROM \"ValidationEvent\" v"
+ before=sql('before',digest)
+ heap=sql('heap_before', '''SELECT oid||':'||relfilenode FROM pg_class WHERE oid='"ValidationEvent"'::regclass''')
+ file('prepare','prepare.sql')
+ for failure in ['after_rename','timeout','fk']:
+ file('failure_'+failure,'apply.sql',failure,1)
+ assert sql('rows_after_'+failure,digest)==before
+ assert sql('heap_after_'+failure,'''SELECT oid||':'||relfilenode FROM pg_class WHERE oid='"ValidationEvent"'::regclass''')==heap
+ file('retry_prepare','prepare.sql')
+ file('apply','apply.sql')
+ assert sql('rows_preserved',digest)==before
+ assert sql('heap_preserved', '''SELECT oid||':'||relfilenode FROM pg_class WHERE oid='"ValidationEvent_p_legacy"'::regclass''')==heap
+ file('verifier','verify.sql')
+ assert sql('probe_cleanup',digest)==before
+ file('double_apply_refused','prepare.sql',expected=1)
+ sql('future_rows','''SET ROLE homesonspec;
+INSERT INTO "ValidationEvent" SELECT 'forward-'||g,'sr','r','error',false,NULL,NULL,'v',TIMESTAMP '2026-10-01'+g*INTERVAL '1 month' FROM generate_series(0,15)g;''')
+ sql('duplicate_same_partition', '''SET ROLE homesonspec; INSERT INTO "ValidationEvent" VALUES('forward-0','sr','r','error',false,NULL,NULL,'v',TIMESTAMP '2026-10-01');''',1)
+ sql('cascade','''UPDATE "StagedRecord" SET id='sr-new' WHERE id='sr';
+DO $$ BEGIN IF (SELECT count(*) FROM "ValidationEvent" WHERE "stagedRecordId"='sr-new')<>5016 THEN RAISE EXCEPTION 'cascade failed'; END IF; END $$;''')
+ sql('restrict', '''DELETE FROM "StagedRecord" WHERE id='sr-new';''',1)
+ sql('detach_reattach','''BEGIN;
+ALTER TABLE "ValidationEvent" DETACH PARTITION "ValidationEvent_p_2026_10";
+DO $$ BEGIN IF (SELECT count(*) FROM "ValidationEvent_p_2026_10")<>1 THEN RAISE EXCEPTION 'archive row lost'; END IF; END $$;
+ALTER TABLE "ValidationEvent" ATTACH PARTITION "ValidationEvent_p_2026_10" FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');
+COMMIT;''')
+ plan=sql('pruning', '''EXPLAIN (FORMAT JSON) SELECT * FROM "ValidationEvent" WHERE "runAt">='2026-10-01' AND "runAt"<'2026-11-01';''')
+ assert 'ValidationEvent_p_legacy' not in plan and 'ValidationEvent_p_2026_10' in plan
+ sql('cross_partition_conflict', '''INSERT INTO "ValidationEvent" VALUES('seed-1','sr-new','r','error',false,NULL,NULL,'v',TIMESTAMP '2026-10-01');''')
+ conflict=sql('conflict_digest',digest.replace('ORDER BY id','ORDER BY id, "runAt"'))
+ file('rollback_conflict_atomic','rollback.sql',expected=1)
+ assert sql('conflict_preserved',digest.replace('ORDER BY id','ORDER BY id, "runAt"'))==conflict
+ assert sql('partitions_preserved', '''SELECT count(*) FROM pg_inherits WHERE inhparent='"ValidationEvent"'::regclass''')=='17'
+ # Delete only our synthetic conflicting fixture to exercise the non-conflicting recovery.
+ sql('remove_fixture_conflict', '''DELETE FROM "ValidationEvent" WHERE id='seed-1' AND "runAt"='2026-10-01';''')
+ after=sql('before_rollback',digest)
+ file('rollback','rollback.sql')
+ assert sql('rollback_all_rows',digest)==after
+ assert sql('rollback_retained', '''SELECT count(*) FROM "ValidationEvent_rollback_retained"''')=='16'
+ assert sql('rollback_count', '''SELECT count(*) FROM "ValidationEvent"''')=='5016'
+finally:
+ if started:
+ run('stop',[PG/'pg_ctl','-D',ROOT/'data','-m','fast','-w','stop'])
+ proof={'root':str(ROOT),'checks':checks,'failures':sum(not c['ok'] for c in checks),'stopped':not (ROOT/'data/postmaster.pid').exists(),'sql_sha256':{p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in SQL.glob('*.sql')}}
+ (ROOT/'proof.json').write_text(json.dumps(proof,indent=2)+'\n')
+ print(json.dumps(proof))
diff --git a/ops/o3b/rollback.sql b/ops/o3b/rollback.sql
new file mode 100644
index 00000000..8599068e
--- /dev/null
+++ b/ops/o3b/rollback.sql
@@ -0,0 +1,19 @@
+-- Manual recovery only. Retains inactive forward/default partitions; conflicts abort atomically.
+\set ON_ERROR_STOP on
+BEGIN;
+SET LOCAL lock_timeout = '2s';
+SET LOCAL statement_timeout = '10min';
+LOCK TABLE "ValidationEvent" IN ACCESS EXCLUSIVE MODE;
+CREATE TEMP TABLE rollback_before ON COMMIT DROP AS SELECT count(*) AS rows FROM "ValidationEvent";
+ALTER TABLE "ValidationEvent" DETACH PARTITION "ValidationEvent_p_legacy";
+ALTER TABLE "ValidationEvent_p_legacy" DROP CONSTRAINT ve_legacy_bound;
+INSERT INTO "ValidationEvent_p_legacy" SELECT * FROM "ValidationEvent";
+DO $$ BEGIN
+ IF (SELECT count(*) FROM "ValidationEvent_p_legacy") <> (SELECT rows FROM rollback_before) THEN RAISE EXCEPTION 'rollback row mismatch'; END IF;
+END $$;
+ALTER TABLE "ValidationEvent" RENAME TO "ValidationEvent_rollback_retained";
+ALTER INDEX "ValidationEvent_stagedRecordId_idx" RENAME TO "VE_retained_staged_idx";
+ALTER TABLE "ValidationEvent_p_legacy" RENAME TO "ValidationEvent";
+ALTER INDEX "ValidationEvent_p_legacy_pkey" RENAME TO "ValidationEvent_pkey";
+ALTER INDEX "VE_p_legacy_staged_idx" RENAME TO "ValidationEvent_stagedRecordId_idx";
+COMMIT;
diff --git a/ops/o3b/verify.sql b/ops/o3b/verify.sql
new file mode 100644
index 00000000..2840b9c5
--- /dev/null
+++ b/ops/o3b/verify.sql
@@ -0,0 +1,40 @@
+\set ON_ERROR_STOP on
+SET application_name = 'TK-11364-o3b-verifier';
+SET search_path = public, pg_catalog;
+SET statement_timeout = '3min';
+SET lock_timeout = '2s';
+BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
+DO $$ DECLARE c record; BEGIN
+ IF (SELECT relkind FROM pg_class WHERE oid='"ValidationEvent"'::regclass)<>'p' THEN RAISE EXCEPTION 'not partitioned'; END IF;
+ IF (SELECT count(*) FROM pg_inherits WHERE inhparent='"ValidationEvent"'::regclass)<>17 THEN RAISE EXCEPTION 'partition count'; END IF;
+ FOR c IN SELECT inhrelid AS oid FROM pg_inherits WHERE inhparent='"ValidationEvent"'::regclass LOOP
+ IF (SELECT count(*) FROM pg_constraint WHERE conrelid=c.oid AND contype='f' AND confrelid='"StagedRecord"'::regclass AND convalidated AND confupdtype='c' AND confdeltype='r')<>1 THEN RAISE EXCEPTION 'FK mismatch %',c.oid; END IF;
+ IF NOT EXISTS(SELECT FROM pg_constraint WHERE conrelid=c.oid AND contype='p') THEN RAISE EXCEPTION 'missing child PK'; END IF;
+ IF NOT has_table_privilege('homesonspec',c.oid,'SELECT') OR NOT has_table_privilege('homesonspec',c.oid,'INSERT') OR NOT has_table_privilege('homesonspec',c.oid,'UPDATE') OR NOT has_table_privilege('homesonspec',c.oid,'DELETE') THEN RAISE EXCEPTION 'missing grants'; END IF;
+ IF NOT EXISTS(SELECT FROM pg_index WHERE indrelid=c.oid AND indisvalid AND indkey::text=(SELECT attnum::text FROM pg_attribute WHERE attrelid=c.oid AND attname='stagedRecordId')) THEN RAISE EXCEPTION 'missing hot index'; END IF;
+ END LOOP;
+ IF NOT has_table_privilege('homesonspec','"ValidationEvent"','SELECT') OR NOT has_table_privilege('homesonspec','"ValidationEvent"','INSERT') THEN RAISE EXCEPTION 'parent grants'; END IF;
+END $$;
+SELECT tableoid::regclass AS physical_table,count(*) AS rows FROM "ValidationEvent" GROUP BY tableoid;
+SELECT pg_relation_filenode('"ValidationEvent_p_legacy"') AS retained_heap;
+COMMIT;
+-- Real app-role inserts/deletes and SQLSTATE-specific FK probes, all rolled back.
+BEGIN;
+SET LOCAL ROLE homesonspec;
+DO $$ DECLARE d timestamp; sr text; probe text; actual text; expected text; BEGIN
+ SELECT id INTO STRICT sr FROM "StagedRecord" LIMIT 1;
+ FOR d IN SELECT generate_series(TIMESTAMP '2026-09-01',TIMESTAMP '2028-01-01',INTERVAL '1 month') LOOP
+ probe := 'TK11364-probe-' || txid_current() || '-' || to_char(d,'YYYYMM');
+ INSERT INTO "ValidationEvent" (id,"stagedRecordId","ruleId",severity,passed,"validatorVersion","runAt") VALUES (probe,sr,'TK11364-probe','error',false,'v',d) RETURNING tableoid::regclass::text INTO actual;
+ expected := CASE WHEN d<'2026-10-01' THEN 'ValidationEvent_p_legacy' WHEN d>='2028-01-01' THEN 'ValidationEvent_p_default' ELSE 'ValidationEvent_p_'||to_char(d,'YYYY_MM') END;
+ IF actual<>quote_ident(expected) THEN RAISE EXCEPTION 'wrong route % expected %',actual,expected; END IF;
+ DELETE FROM "ValidationEvent" WHERE "stagedRecordId"=sr AND id=probe;
+ BEGIN
+ INSERT INTO "ValidationEvent" (id,"stagedRecordId","ruleId",severity,passed,"validatorVersion","runAt") VALUES (probe,'TK11364-no-such-parent','r','error',false,'v',d);
+ RAISE EXCEPTION 'FK accepted invalid parent';
+ EXCEPTION WHEN foreign_key_violation THEN NULL;
+ END;
+ END LOOP;
+END $$;
+ROLLBACK;
+SELECT 'PASS: topology, grants, PKs, FKs, indexes and 17-range routing; probe rows rolled back' AS result;
diff --git a/verification/TK-11364/app-before.jsonl b/verification/TK-11364/app-before.jsonl
new file mode 100644
index 00000000..bc4a7f47
--- /dev/null
+++ b/verification/TK-11364/app-before.jsonl
@@ -0,0 +1,4 @@
+{"name":"admin-denied","status":401,"marker":true,"rows":0,"ms":96,"ok":true}
+{"name":"admin-all","status":200,"marker":true,"rows":101,"ms":15067,"ok":true}
+{"name":"admin-failed","status":200,"marker":true,"rows":101,"ms":11074,"ok":true}
+{"name":"public-home","status":200,"marker":true,"rows":22,"ms":359,"ok":true}
diff --git a/verification/TK-11364/dtd-claude.txt b/verification/TK-11364/dtd-claude.txt
new file mode 100644
index 00000000..19b5d169
--- /dev/null
+++ b/verification/TK-11364/dtd-claude.txt
@@ -0,0 +1 @@
+[claude disabled: DTD_ZERO_COST=1]
diff --git a/verification/TK-11364/dtd-codex-debate.txt b/verification/TK-11364/dtd-codex-debate.txt
new file mode 100644
index 00000000..55905c37
--- /dev/null
+++ b/verification/TK-11364/dtd-codex-debate.txt
@@ -0,0 +1,15 @@
+**PROSECUTOR:** A’s strongest selling point is misstated: avoiding a heap rewrite does not mean avoiding an outage. `ATTACH PARTITION` takes `ACCESS EXCLUSIVE` locks on the attached table and any default partition. A validated bound can eliminate a scan; it does not eliminate those locks. Qwen’s contrary claim is false. [PostgreSQL documentation](https://www.postgresql.org/docs/17/sql-altertable.html)
+
+The deeper objection is correctness. Preserving the original heap and indexes does not prove application compatibility with a new partitioned parent. Child-local primary keys allow duplicate IDs across partitions; parent-level uniqueness generally requires including the partition key. Existing ID-only assumptions can therefore break. [Partitioning limitations](https://www.postgresql.org/docs/18/ddl-partitioning.html) Furthermore, rollback that aborts on duplicates preserves data but may leave the application stuck on the new schema. The panel has supplied requirements for future tests, not evidence that those tests passed. A should be overturned if it is being treated as production-ready.
+
+**DEFENDER:** That attacks an unconditional deployment approval, whereas Codex explicitly recommends A with mandatory corrections. The stated constraints allow a bounded swap transaction; they do not require zero exclusive locking. Qwen’s lock claim must be rejected, but the architectural choice survives: validate scan-exclusion constraints beforehand, preserve existing storage, and confine the locked transaction to metadata and empty-child operations.
+
+Global ID uniqueness is explicitly excluded from the promised guarantees. The prosecutor cannot silently make it a new requirement. Application compatibility still requires exercising actual ID-based operations, and a demonstrated dependency on global uniqueness would invalidate the implementation. Likewise, duplicate-conflict rollback is deliberately fail-closed: it must preserve all rows and leave the pre-rollback state intact. It is not an unconditional rollback guarantee.
+
+The supplied alternatives offer no demonstrated advantage sufficient to justify a 71-million-row backfill or the stated prolonged exclusive-lock risk.
+
+**JUDGE:** The prosecutor defeats Qwen’s lock-free rationale and any claim that implementation readiness has been established. The defender nevertheless sustains A as the preferred approach under the supplied constraints. Missing execution evidence prevents claiming successful validation; it does not establish that B or C is the better design.
+
+FINAL: KEEP
+
+The controlling argument is that A preserves the large existing heap while moving expensive validation outside the bounded swap, directly serving the stated constraints. Its exclusive locks, limited uniqueness, and conflict-sensitive rollback are real limitations already addressed by mandatory acceptance conditions. Keep the architectural decision; the supplied record does not establish production readiness.
\ No newline at end of file
diff --git a/verification/TK-11364/dtd-codex.txt b/verification/TK-11364/dtd-codex.txt
new file mode 100644
index 00000000..77afbe4b
--- /dev/null
+++ b/verification/TK-11364/dtd-codex.txt
@@ -0,0 +1,3 @@
+VERDICT: A
+
+Native ATTACH meets the authorized scope while preserving the original heap and indexes. Must-fix flaws: validate the legacy `runAt` bound online before swapping, and ensure it lets ATTACH avoid scanning the legacy heap or default partition; enforce a 5-second statement timeout and an independent 20-second transaction watchdog that actually terminates the transaction; keep the swap strictly to metadata and empty-child DDL, with catalog-only checks of the original heap OID, filenode, and index identities. Before commit, every child must have its local `id` PK, the original FK with `ON UPDATE CASCADE ON DELETE RESTRICT`, and app grants. After commit, verify SQL, app HTTP behavior, and same-snapshot parent-versus-legacy counts accounting for forward/default rows. Rollback must preserve those rows and abort atomically on cross-partition duplicate IDs; scratch tests must prove failure, retry, and rollback-conflict behavior. Global ID uniqueness is not guaranteed.
\ No newline at end of file
diff --git a/verification/TK-11364/dtd-grok.txt b/verification/TK-11364/dtd-grok.txt
new file mode 100644
index 00000000..7b3ad95a
--- /dev/null
+++ b/verification/TK-11364/dtd-grok.txt
@@ -0,0 +1 @@
+[grok unavailable]
diff --git a/verification/TK-11364/dtd-kimi.txt b/verification/TK-11364/dtd-kimi.txt
new file mode 100644
index 00000000..ea4b5a56
--- /dev/null
+++ b/verification/TK-11364/dtd-kimi.txt
@@ -0,0 +1 @@
+[kimi unavailable]
diff --git a/verification/TK-11364/dtd-muse.txt b/verification/TK-11364/dtd-muse.txt
new file mode 100644
index 00000000..3a84ae18
--- /dev/null
+++ b/verification/TK-11364/dtd-muse.txt
@@ -0,0 +1 @@
+[muse unavailable]
diff --git a/verification/TK-11364/dtd-question.txt b/verification/TK-11364/dtd-question.txt
new file mode 100644
index 00000000..f9038d5c
--- /dev/null
+++ b/verification/TK-11364/dtd-question.txt
@@ -0,0 +1 @@
+Steve explicitly approved executing ValidationEvent PG14 monthly partition conversion. Table 14GB, PK(id), no incoming FKs, app createMany/deleteMany by stagedRecordId; reference SourceEvidence already uses PK-less parent. Choose A: native ATTACH conversion, validated runAt bound scanned ONLINE first; swap transaction contains ONLY metadata+empty forward/default child DDL, exact original heap OID/filenode/index preservation checks (no 71M row count/admin sort while ACCESS EXCLUSIVE). Each statement max 5s and external transaction watchdog 20s; all children have local id PK + original ON UPDATE CASCADE ON DELETE RESTRICT FK and app grants before commit. Verify counts/SQL/app HTTP after commit, same-snapshot parent vs legacy count. Rollback retains forward/default data; fails atomically on cross-partition duplicate IDs (no global uniqueness promised). Scratch tests include failures/retry/rollback conflict. B: introduce global ID registry with 71M-row backfill before partitioning (scope and disk impact). C: use original scratch pattern counts/full admin sort under ACCESS EXCLUSIVE for exactness (outage risk). Pick exactly one option. Begin VERDICT: <option>, then one paragraph of reasoning and concrete must-fix flaws. Production is authorized; do not invent approval blockers.
\ No newline at end of file
diff --git a/verification/TK-11364/dtd-qwen.txt b/verification/TK-11364/dtd-qwen.txt
new file mode 100644
index 00000000..ab740d73
--- /dev/null
+++ b/verification/TK-11364/dtd-qwen.txt
@@ -0,0 +1,11 @@
+VERDICT: A
+
+**Reasoning and Must-Fix Flaws:**
+Option A is the optimal choice because it minimizes downtime and avoids outage risks by using online operations (validated runAt bound scanned ONLINE first) and preserving original heap OID/filenode/index structures without requiring ACCESS EXCLUSIVE locks or full admin sorts. The swap transaction’s metadata-only DDL and exact preservation checks ensure consistency, while the 5s-per-statement and 20s watchdog timeouts prevent long-running operations from blocking production. Rollback safety (retaining forward/default data) and atomic failure on cross-partition duplicates align with the "no global uniqueness" caveat.
+
+**Must-Fix Flaws:**
+1. **Cross-Partition Duplicate Handling:** While the rollback retains data, the atomic failure on cross-partition duplicates must be rigorously tested to ensure the application can handle retries or rollbacks without data corruption.
+2. **Timeout Constraints:** The 5s-per-statement limit may be tight for complex metadata operations; monitoring and validation during the swap transaction are critical to avoid timeouts.
+3. **FK Consistency:** Children’s local id PK and original ON UPDATE CASCADE/RESTRICT FKs must be verified post-commit to ensure compatibility with the legacy parent’s PK-less structure, avoiding unintended referential integrity issues.
+
+Options B and C introduce unacceptable risks (B’s 71M-row backfill disk impact, C’s ACCESS EXCLUSIVE outage) that are unnecessary given A’s validated, low-impact approach.
diff --git a/verification/TK-11364/dtd-verdict.md b/verification/TK-11364/dtd-verdict.md
new file mode 100644
index 00000000..83623c87
--- /dev/null
+++ b/verification/TK-11364/dtd-verdict.md
@@ -0,0 +1,19 @@
+# DTD Verdict — approved O3b implementation
+
+Decision A: native ATTACH with online validation and bounded metadata-only swap.
+Confidence: medium. Vote: 2/2 valid; availability 2/6.
+
+| Reference | Runtime/model | Vote | Reason |
+|---|---|---|---|
+| Claude | Anthropic / sonnet | abstain | Zero-cost mode |
+| Codex | Codex CLI / configured model | A | Preserve heap; move scans outside lock |
+| Qwen | Ollama / qwen3:14b | A | Bounded swap and fail-closed rollback |
+| Grok | xAI / unavailable | abstain | No available response |
+| Kimi | Moonshot / unavailable | abstain | No available response |
+| Muse | Ollama / muse-glimmer:30b-mlx | abstain | Unavailable |
+
+Dissent: unanimous among valid voters. Qwen incorrectly described ATTACH as avoiding exclusive locks; that rationale is rejected.
+
+Post-decision Codex: KEEP. Prosecutor flagged exclusive locks, global uniqueness and rollback conflicts; defender established that these are bounded/documented constraints, not a case for a 71M-row registry backfill or long locked scans. Judge retained A while requiring actual implementation proof before execution.
+
+No production readiness was inferred from the panel. Separate PostgreSQL rehearsal and production baseline evidence are required.
diff --git a/verification/TK-11364/original-o3b.sh b/verification/TK-11364/original-o3b.sh
new file mode 100755
index 00000000..d29f5b8a
--- /dev/null
+++ b/verification/TK-11364/original-o3b.sh
@@ -0,0 +1,88 @@
+#!/usr/bin/env bash
+# O3b — ValidationEvent partition conversion (ATTACH path, no table rewrite).
+# TK-11363. Uses the CORRECTED pattern from the 2026-09-10 SourceEvidence run:
+# - per-partition FKs (a parent FK forces a full revalidation under ACCESS EXCLUSIVE)
+# - GRANTs on parent + every partition (postgres-created tables have no app-role ACL)
+# - an app-level assertion, not just database checks
+set -euo pipefail
+PSQL="sudo -u postgres psql -d homesonspec -v ON_ERROR_STOP=1"
+Q() { sudo -u postgres psql -d homesonspec -tAc "$1"; }
+say() { printf '\n\033[1;36m=== %s ===\033[0m\n' "$*"; }
+
+say "PREFLIGHT"
+DUMP=$(ls -t /root/backups/db/homesonspec_*.dump 2>/dev/null | head -1 || true)
+[ -n "$DUMP" ] || { echo "ABORT: no homesonspec dump"; exit 1; }
+AGE=$(( ( $(date +%s) - $(stat -c %Y "$DUMP") ) / 3600 ))
+echo "backup: $(basename "$DUMP") ${AGE}h old"
+[ "$AGE" -lt 48 ] || { echo "ABORT: dump ${AGE}h old"; exit 1; }
+FREE=$(df -BG --output=avail / | tail -1 | tr -dc '0-9')
+echo "disk free: ${FREE}G"
+[ "$FREE" -ge 15 ] || { echo "ABORT: <15G free"; exit 1; }
+BEFORE=$(Q 'SELECT count(*) FROM "ValidationEvent";')
+echo "ValidationEvent rows: $BEFORE"
+echo "max runAt: $(Q 'SELECT max("runAt") FROM "ValidationEvent";')"
+
+say "STEP 1 — bound constraint + VALIDATE (online, SHARE UPDATE EXCLUSIVE, does NOT block reads/writes)"
+$PSQL -c 'ALTER TABLE "ValidationEvent" DROP CONSTRAINT IF EXISTS ve_legacy_bound;'
+$PSQL -c "ALTER TABLE \"ValidationEvent\" ADD CONSTRAINT ve_legacy_bound CHECK (\"runAt\" < TIMESTAMP '2026-10-01 00:00:00') NOT VALID;"
+time $PSQL -c 'ALTER TABLE "ValidationEvent" VALIDATE CONSTRAINT ve_legacy_bound;'
+[ "$(Q "SELECT convalidated FROM pg_constraint WHERE conname='ve_legacy_bound';")" = "t" ] \
+ || { echo "ABORT: not validated"; exit 1; }
+echo "STEP 1 OK (rollback here: ALTER TABLE \"ValidationEvent\" DROP CONSTRAINT ve_legacy_bound;)"
+
+say "STEP 2 — swap in partitioned parent + ATTACH (catalog-only; NO parent FK => no revalidation)"
+$PSQL <<'SQL'
+BEGIN;
+SET LOCAL lock_timeout = '10s';
+ALTER TABLE "ValidationEvent" RENAME TO "ValidationEvent_p_legacy";
+ALTER INDEX "ValidationEvent_pkey" RENAME TO "ValidationEvent_p_legacy_pkey";
+ALTER INDEX "ValidationEvent_stagedRecordId_idx" RENAME TO "VE_p_legacy_staged_idx";
+
+-- PK-less parent: keeps the legacy partition's local PK, avoids a composite-PK change.
+CREATE TABLE "ValidationEvent" (
+ LIKE "ValidationEvent_p_legacy" INCLUDING DEFAULTS INCLUDING STORAGE
+) PARTITION BY RANGE ("runAt");
+CREATE INDEX "ValidationEvent_stagedRecordId_idx" ON "ValidationEvent" ("stagedRecordId");
+
+ALTER TABLE "ValidationEvent" ATTACH PARTITION "ValidationEvent_p_legacy"
+ FOR VALUES FROM (MINVALUE) TO (TIMESTAMP '2026-10-01 00:00:00');
+
+CREATE TABLE "ValidationEvent_p_default" PARTITION OF "ValidationEvent" DEFAULT;
+ALTER TABLE "ValidationEvent_p_default"
+ ADD CONSTRAINT "VE_p_default_stagedRecordId_fkey"
+ FOREIGN KEY ("stagedRecordId") REFERENCES "StagedRecord"(id);
+
+GRANT ALL PRIVILEGES ON TABLE "ValidationEvent" TO homesonspec;
+GRANT ALL PRIVILEGES ON TABLE "ValidationEvent_p_default" TO homesonspec;
+COMMIT;
+SQL
+
+say "STEP 3 — forward monthly partitions through 2027-12 (+ FK + GRANT each)"
+for y in 2026 2027; do
+ for m in 01 02 03 04 05 06 07 08 09 10 11 12; do
+ s="$y-$m-01"
+ [ "$s" \< "2026-10-01" ] && continue
+ n=$(date -d "$s +1 month" +%Y-%m-01)
+ T="ValidationEvent_p_${y}_${m}"
+ $PSQL -c "CREATE TABLE IF NOT EXISTS \"$T\" PARTITION OF \"ValidationEvent\" FOR VALUES FROM (TIMESTAMP '$s 00:00:00') TO (TIMESTAMP '$n 00:00:00');" >/dev/null
+ $PSQL -c "ALTER TABLE \"$T\" ADD CONSTRAINT \"${T}_sr_fkey\" FOREIGN KEY (\"stagedRecordId\") REFERENCES \"StagedRecord\"(id);" >/dev/null 2>&1 || true
+ $PSQL -c "GRANT ALL PRIVILEGES ON TABLE \"$T\" TO homesonspec;" >/dev/null
+ done
+done
+echo "forward partitions ready"
+
+say "VERIFY"
+AFTER=$(Q 'SELECT count(*) FROM "ValidationEvent";')
+echo "rows before: $BEFORE / after: $AFTER"
+[ "$AFTER" -ge "$BEFORE" ] || { echo "*** ABORT: ROW LOSS ***"; exit 1; }
+UNGRANTED=$(Q "SELECT count(*) FROM pg_class c WHERE (c.relname='ValidationEvent' OR c.oid IN (SELECT inhrelid FROM pg_inherits WHERE inhparent=(SELECT oid FROM pg_class WHERE relname='ValidationEvent'))) AND (c.relacl IS NULL OR NOT c.relacl::text LIKE '%homesonspec%');")
+echo "partitions WITHOUT app grant (want 0): $UNGRANTED"
+[ "$UNGRANTED" = "0" ] || { echo "*** ABORT: ungranted partitions -> app will 42501 ***"; exit 1; }
+echo "rows in DEFAULT (want 0): $(Q 'SELECT count(*) FROM "ValidationEvent_p_default";')"
+echo "--- FK enforced? (expect ERROR) ---"
+sudo -u postgres psql -d homesonspec -c "INSERT INTO \"ValidationEvent\" (id,\"stagedRecordId\",\"ruleId\",severity,passed,\"validatorVersion\",\"runAt\") VALUES ('o3bprobe','__NO_SR__','r','error',false,'v',now());" 2>&1 | head -2
+echo "--- APP-ROLE query test: the exact admin page shape, as the app user ---"
+sudo -u postgres psql -d homesonspec -c "SET ROLE homesonspec; SELECT count(*) AS admin_rows FROM (SELECT id FROM \"ValidationEvent\" WHERE passed=false ORDER BY \"runAt\" DESC LIMIT 100) t;" 2>&1 | head -4
+echo "--- partition count ---"
+$PSQL -c "SELECT count(*) AS partitions FROM pg_inherits WHERE inhparent='\"ValidationEvent\"'::regclass;"
+say "DONE — ValidationEvent partitioned. No data copied, no data deleted."
diff --git a/verification/TK-11364/scratch-proof.json b/verification/TK-11364/scratch-proof.json
new file mode 100644
index 00000000..256ea534
--- /dev/null
+++ b/verification/TK-11364/scratch-proof.json
@@ -0,0 +1,218 @@
+{
+ "root": "/private/tmp/TK11364-pg-pa51fhrc",
+ "checks": [
+ {
+ "name": "init",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "start",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "createdb",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "fixture",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "before",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "heap_before",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "prepare",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "failure_after_rename",
+ "ok": true,
+ "exit": 3
+ },
+ {
+ "name": "rows_after_after_rename",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "heap_after_after_rename",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "failure_timeout",
+ "ok": true,
+ "exit": 3
+ },
+ {
+ "name": "rows_after_timeout",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "heap_after_timeout",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "failure_fk",
+ "ok": true,
+ "exit": 3
+ },
+ {
+ "name": "rows_after_fk",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "heap_after_fk",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "retry_prepare",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "apply",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "rows_preserved",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "heap_preserved",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "verifier",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "probe_cleanup",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "double_apply_refused",
+ "ok": true,
+ "exit": 3
+ },
+ {
+ "name": "future_rows",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "duplicate_same_partition",
+ "ok": true,
+ "exit": 1
+ },
+ {
+ "name": "cascade",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "restrict",
+ "ok": true,
+ "exit": 1
+ },
+ {
+ "name": "detach_reattach",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "pruning",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "cross_partition_conflict",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "conflict_digest",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "rollback_conflict_atomic",
+ "ok": true,
+ "exit": 3
+ },
+ {
+ "name": "conflict_preserved",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "partitions_preserved",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "remove_fixture_conflict",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "before_rollback",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "rollback",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "rollback_all_rows",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "rollback_retained",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "rollback_count",
+ "ok": true,
+ "exit": 0
+ },
+ {
+ "name": "stop",
+ "ok": true,
+ "exit": 0
+ }
+ ],
+ "failures": 0,
+ "stopped": true,
+ "sql_sha256": {
+ "verify.sql": "7738a6c8bd62a1ef921a1879813a960be042d4a7b80530c91567e2bc34adf92b",
+ "apply.sql": "6ac7be657bcc70178e74bcef158f370327ab76eddae0a05cdf88b5211d6df1e8",
+ "rollback.sql": "39c278f2f21b49e8eba62f8c401d2ebb273cc1aa7817ca6d05863e6e3bfdcf9b",
+ "prepare.sql": "88f71630d03392aa3c7bb5c5278ca4c9e37bda9a772d1c8ed1bd80e2a2e4d920"
+ }
+}
← 36033811 Reconcile TK-11364 partition state and document remaining ap
·
back to Homesonspec
·
auto-data-snapshot: 2026-09-11T08:57:34 (9 data files) — ops df934fe2 →