From f4739a77822ce218595db67ebe0664eb10922688 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:59:35 +0900 Subject: [PATCH 01/14] Add held atomic review-media migration successor with guarded admission --- .github/review-media-sql-successor.v1.json | 344 +++++ .../review-db-successor-20261012/README.md | 43 + .../review-db-successor-20261012/SHA256SUMS | 6 + .../pg17-local-replay.json | 247 ++++ .../verify-review-media-sql-successor.mjs | 622 +++++++++ .../scripts/review-media-sql-successor.mjs | 1198 +++++++++++++++++ .../web/scripts/supabase-migration-bundle.mjs | 9 + .../review-media-migration-bundle.test.ts | 116 ++ .../review-media-sql-successor.test.ts | 392 ++++++ 9 files changed, 2977 insertions(+) create mode 100644 .github/review-media-sql-successor.v1.json create mode 100644 apps/web/performance/review-db-successor-20261012/README.md create mode 100644 apps/web/performance/review-db-successor-20261012/SHA256SUMS create mode 100644 apps/web/performance/review-db-successor-20261012/pg17-local-replay.json create mode 100644 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs create mode 100644 apps/web/scripts/review-media-sql-successor.mjs create mode 100644 apps/web/tests-unit/review-media-migration-bundle.test.ts create mode 100644 apps/web/tests-unit/review-media-sql-successor.test.ts diff --git a/.github/review-media-sql-successor.v1.json b/.github/review-media-sql-successor.v1.json new file mode 100644 index 0000000000..3dfd6f9b40 --- /dev/null +++ b/.github/review-media-sql-successor.v1.json @@ -0,0 +1,344 @@ +{ + "schemaVersion": 1, + "id": "review_media_sql_successor_v1", + "purpose": "review-media-current-state-eleven-stage", + "projectRef": "aqlcofblfxdrjhhdmarw", + "protectedSource": { + "remote": "origin", + "ref": "refs/heads/main", + "repositoryUrl": "https://github.com/twoimo/tzudong.git" + }, + "databaseName": "postgres", + "executorRole": "postgres", + "serverMajor": 17, + "advisoryLockKey": "7311754282260211", + "stateRootAlgorithm": "review-media-successor-state/v1", + "sourceRoot": "61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544", + "operatingTransition": { + "beforeEleven": 86, + "afterEleven": 97 + }, + "launchPolicy": { + "state": "held", + "code": "SUCCESSOR_LAUNCH_HELD", + "reason": "protected-release-and-fresh-admission-required" + }, + "terminalContract": { + "finishPreimageSha256": "f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1", + "finishPostimageSha256": "a6b4211b2d8486d482f0758ce249eec157249e1a5176be286b76af7e7a775ee9", + "requiredFunctionBodySha256": { + "public.admin_review_verification_cleanup(uuid,text,text[])": "468c12ee6c3463876cbccb394fcba8f870459a1f6cf3d945d97307f8551f094b", + "public.finish_review_media_cleanup()": "a6b4211b2d8486d482f0758ce249eec157249e1a5176be286b76af7e7a775ee9", + "public.mutate_review_with_media(uuid,uuid,text,timestamp with time zone,text,text[],text[])": "d5d6646a4294c0e7bd0cd75ea47a996c9c57bac6c32a39ac53fbb7a8b1e7e323", + "review_media_private.enqueue_removed()": "cbf6a87e453e8c2e14d67acd73a92e399fbba4b4f24ac27966806e13ef1fd905", + "review_media_private.guard_references()": "94113c8ae1231e8eae5a698f35a1370a38367e42c79cc87ac62db76c32e735b1" + }, + "requiredFunctions": [ + "public.mutate_review_with_media(uuid,uuid,text,timestamp with time zone,text,text[],text[])", + "public.queue_review_upload_cleanup(uuid,text[])", + "public.read_review_media_commit(uuid,uuid,text)", + "public.pending_review_media_cleanup()", + "public.finish_review_media_cleanup()", + "public.review_media_delete_allowed(text)", + "public.review_media_upload_allowed(text,text)", + "public.admin_review_verification_cleanup(uuid,text,text[])" + ], + "requiredPolicies": [ + "review_media_safe_delete", + "review_media_safe_insert", + "review_media_safe_update", + "review_verifications_owner_read", + "review_verifications_owner_insert", + "review_verifications_owner_delete", + "review_verifications_safe_delete", + "review_photos_food_insert", + "review_photos_food_update" + ] + }, + "migrations": [ + { + "id": "review_media_commit_cleanup", + "version": "20261008192455", + "name": "review_media_commit_cleanup", + "path": "backend/supabase/migrations/20261008192455_review_media_commit_cleanup.sql", + "bytes": 17278, + "sha256": "1b018884a0ba8bb25e6a9e31fa831e349bd4442118e5d4bf1699464e09e16c3c", + "statementCount": 34, + "statementVectorSha256": "b90dac7fe72cedb2338c60d85f1852ba429c8395bd6a579797178fb067a887c3", + "originalStatementVector": [ + "-- Atomic review commit receipts and durable, owner-scoped media cleanup.\n-- No scheduler is installed. The authenticated MY/review UI drains this queue.\n-- Tombstones must survive successful cleanup: a delayed write must not revive a\n-- deleted key. No retention period is introduced here.\nBEGIN", + "CREATE SCHEMA IF NOT EXISTS review_media_private", + "REVOKE ALL ON SCHEMA review_media_private FROM PUBLIC, anon, authenticated", + "CREATE TABLE review_media_private.commits (\n operation_id uuid PRIMARY KEY, owner_id uuid NOT NULL, review_id uuid NOT NULL,\n kind text NOT NULL CHECK (kind IN ('edit', 'delete')), request_hash text NOT NULL\n)", + "CREATE TABLE review_media_private.cleanup (\n path text PRIMARY KEY, owner_id uuid NOT NULL, review_id uuid NOT NULL,\n purpose text NOT NULL CHECK (purpose IN ('food', 'verification')),\n retired boolean NOT NULL DEFAULT false, complete boolean NOT NULL DEFAULT false\n)", + "ALTER TABLE review_media_private.commits ENABLE ROW LEVEL SECURITY", + "ALTER TABLE review_media_private.cleanup ENABLE ROW LEVEL SECURITY", + "REVOKE ALL ON ALL TABLES IN SCHEMA review_media_private FROM PUBLIC, anon, authenticated", + "CREATE FUNCTION review_media_private.canonical(p_path text, p_owner uuid, p_review uuid, p_purpose text)\nRETURNS boolean LANGUAGE sql IMMUTABLE SET search_path = '' AS $$\n SELECT COALESCE(p_purpose IN ('food','verification') AND\n p_path LIKE p_owner::text || '/reviews/' || p_review::text || '/' || p_purpose || '/%' AND\n length(split_part(p_path, '/', 5)) <= 240 AND p_path ~* '^[A-Za-z0-9_-]+/reviews/[A-Za-z0-9_-]+/(food|verification)/[A-Za-z0-9][A-Za-z0-9._-]{0,239}\\.(avif|jpe?g|png|webp)$', false)\n$$", + "-- Decode historical public/signed URL values for reference checks and for\n-- cleanup only after the authoritative pre-image passes owner/purpose checks.\nCREATE FUNCTION review_media_private.reference_key(p_value text)\nRETURNS text LANGUAGE plpgsql IMMUTABLE SET search_path = '' AS $$\nDECLARE v text := p_value; b bytea := ''::bytea; i integer := 1;\nBEGIN\n IF v LIKE '%/storage/v1/object/%/review-photos/%' THEN\n v := split_part(split_part(substring(v FROM '/review-photos/(.*)$'), '?', 1), '#', 1);\n WHILE i <= length(v) LOOP\n IF substring(v, i, 1) = '%' THEN\n b := b || decode(substring(v, i + 1, 2), 'hex'); i := i + 3;\n ELSE\n b := b || convert_to(substring(v, i, 1), 'UTF8'); i := i + 1;\n END IF;\n END LOOP;\n RETURN convert_from(b, 'UTF8');\n END IF;\n RETURN v;\nEXCEPTION WHEN OTHERS THEN RETURN p_value;\nEND $$", + "-- Historical composer keys are admitted only from the authoritative row\n-- pre-image. Client uploads and compensation still require review-bound keys.\nCREATE FUNCTION review_media_private.owned_legacy(p_path text, p_owner uuid, p_purpose text)\nRETURNS boolean LANGUAGE sql IMMUTABLE SET search_path = '' AS $$\n SELECT COALESCE(split_part(p_path, '/', 1) = p_owner::text AND\n CASE p_purpose\n WHEN 'food' THEN p_path ~ '^[0-9a-f-]{36}/[0-9]{10,16}_food_[0-9]{1,3}_[A-Za-z0-9][A-Za-z0-9._-]{0,200}\\.(avif|jpe?g|png|webp)$'\n WHEN 'verification' THEN p_path ~ '^[0-9a-f-]{36}/[0-9]{10,16}_verification_[A-Za-z0-9][A-Za-z0-9._-]{0,200}\\.(avif|jpe?g|png|webp)$'\n ELSE false END, false)\n$$", + "CREATE FUNCTION review_media_private.referenced(p_path text)\nRETURNS boolean LANGUAGE sql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\n SELECT EXISTS (SELECT 1 FROM public.reviews r,\n LATERAL unnest(COALESCE(r.food_photos, ARRAY[]::text[]) || ARRAY[r.verification_photo]) p\n WHERE review_media_private.reference_key(p) = p_path)\n$$", + "-- A transaction lock serializes media reference changes and retirement. It is\n-- released BEFORE any Storage HTTP call; retired keys can never be reattached.\n-- Metadata-only moderation/likes do not enter this lock.\nCREATE FUNCTION review_media_private.lock_changes()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF current_setting('transaction_isolation') <> 'read committed' THEN\n RAISE EXCEPTION USING MESSAGE = 'REVIEW_RETRY_REQUIRED', ERRCODE = '40001';\n END IF;\n PERFORM pg_advisory_xact_lock(741093, 1);\n RETURN NULL;\nEND $$", + "CREATE TRIGGER review_media_serialize BEFORE INSERT OR DELETE OR UPDATE OF food_photos, verification_photo, user_id, id\n ON public.reviews FOR EACH STATEMENT EXECUTE FUNCTION review_media_private.lock_changes()", + "CREATE FUNCTION review_media_private.guard_references()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE p text; old_food text[] := ARRAY[]::text[]; old_verification text; purpose text;\nBEGIN\n IF TG_OP = 'UPDATE' AND NEW.user_id = OLD.user_id AND NEW.id = OLD.id THEN\n old_food := COALESCE(OLD.food_photos, ARRAY[]::text[]);\n old_verification := OLD.verification_photo;\n END IF;\n FOR p, purpose IN SELECT value, 'food' FROM unnest(COALESCE(NEW.food_photos, ARRAY[]::text[])) value\n UNION ALL SELECT NEW.verification_photo, 'verification' LOOP\n IF EXISTS (SELECT 1 FROM review_media_private.cleanup c\n WHERE c.path = review_media_private.reference_key(p) AND c.retired) THEN\n RAISE EXCEPTION USING MESSAGE = 'REVIEW_MEDIA_RETIRED', ERRCODE = '23514';\n END IF;\n -- Historical keys may remain unchanged in their original purpose only.\n IF NOT COALESCE(CASE WHEN purpose = 'food' THEN p = ANY(old_food) ELSE p = old_verification END, false) THEN\n IF NOT review_media_private.canonical(p, NEW.user_id, NEW.id, purpose) THEN\n RAISE EXCEPTION USING MESSAGE = 'REVIEW_MEDIA_INVALID', ERRCODE = '23514';\n END IF;\n END IF;\n END LOOP;\n RETURN NEW;\nEND $$", + "CREATE TRIGGER review_media_reference_guard BEFORE INSERT OR UPDATE OF food_photos, verification_photo, user_id, id\n ON public.reviews FOR EACH ROW EXECUTE FUNCTION review_media_private.guard_references()", + "-- Every delete path (including legacy and guarded admin) captures OLD, not an\n-- actor-supplied path list. The statement trigger already holds the media lock.\nCREATE FUNCTION review_media_private.enqueue_removed()\nRETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE p text; purpose text; retained text[] := ARRAY[]::text[];\nBEGIN\n IF TG_OP = 'UPDATE' THEN\n retained := COALESCE(NEW.food_photos, ARRAY[]::text[]) || ARRAY[NEW.verification_photo];\n END IF;\n FOR p, purpose IN SELECT review_media_private.reference_key(value), 'food'\n FROM unnest(COALESCE(OLD.food_photos, ARRAY[]::text[])) value\n UNION ALL SELECT review_media_private.reference_key(OLD.verification_photo), 'verification' LOOP\n IF (review_media_private.canonical(p, OLD.user_id, OLD.id, purpose)\n OR review_media_private.owned_legacy(p, OLD.user_id, purpose))\n AND NOT EXISTS (SELECT 1 FROM unnest(retained) value\n WHERE review_media_private.reference_key(value) = p) THEN\n INSERT INTO review_media_private.cleanup(path, owner_id, review_id, purpose, retired)\n VALUES (p, OLD.user_id, OLD.id, purpose, NOT review_media_private.referenced(p))\n ON CONFLICT DO NOTHING;\n END IF;\n END LOOP;\n RETURN NULL;\nEND $$", + "CREATE TRIGGER review_media_enqueue_removed AFTER DELETE OR UPDATE OF food_photos, verification_photo, user_id, id\n ON public.reviews FOR EACH ROW EXECUTE FUNCTION review_media_private.enqueue_removed()", + "-- Definer is required for the private receipt/queue and global reference check,\n-- not to broaden review access. Owner authorization is checked before any row\n-- mutation. Existing review triggers, cascades and RLS policies are unchanged.\nCREATE FUNCTION public.mutate_review_with_media(\n p_operation_id uuid, p_review_id uuid, p_kind text, p_expected_updated_at timestamptz,\n p_content text DEFAULT NULL, p_categories text[] DEFAULT NULL, p_food_photos text[] DEFAULT NULL\n) RETURNS text LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE u uuid := auth.uid(); r public.reviews%ROWTYPE;\n receipt review_media_private.commits%ROWTYPE; h text; p text;\nBEGIN\n IF u IS NULL THEN RETURN 'REVIEW_UNAUTHORIZED'; END IF;\n IF p_operation_id IS NULL OR p_review_id IS NULL OR p_kind IS NULL OR p_kind NOT IN ('edit','delete') THEN\n RETURN 'REVIEW_INVALID';\n END IF;\n -- Bound client-controlled work before hashing or acquiring the shared lock.\n -- These generous transport ceilings do not trim stored or submitted text.\n IF p_kind = 'delete' AND (p_content IS NOT NULL OR p_categories IS NOT NULL OR p_food_photos IS NOT NULL) THEN\n RETURN 'REVIEW_INVALID';\n END IF;\n IF p_kind = 'edit' THEN\n IF p_content IS NULL OR octet_length(p_content) > 262144 OR length(btrim(p_content)) < 20\n OR p_categories IS NULL OR cardinality(p_categories) NOT BETWEEN 1 AND 100\n OR p_food_photos IS NULL OR cardinality(p_food_photos) NOT BETWEEN 1 AND 10 THEN\n RETURN 'REVIEW_INVALID';\n END IF;\n IF EXISTS (SELECT 1 FROM unnest(p_categories) value WHERE value IS NULL OR length(value) NOT BETWEEN 1 AND 200)\n OR EXISTS (SELECT 1 FROM unnest(p_food_photos) value WHERE value IS NULL OR octet_length(value) > 8192) THEN\n RETURN 'REVIEW_INVALID';\n END IF;\n END IF;\n IF current_setting('transaction_isolation') <> 'read committed' THEN RETURN 'REVIEW_RETRY_REQUIRED'; END IF;\n h := encode(sha256(convert_to(jsonb_build_array(p_review_id, p_kind, p_expected_updated_at,\n p_content, p_categories, p_food_photos)::text, 'UTF8')), 'hex');\n PERFORM pg_advisory_xact_lock(741093, 1);\n SELECT * INTO receipt FROM review_media_private.commits WHERE operation_id = p_operation_id;\n IF FOUND THEN\n IF receipt.owner_id = u AND receipt.review_id = p_review_id AND receipt.kind = p_kind AND receipt.request_hash = h THEN\n RETURN 'REVIEW_COMMITTED';\n END IF;\n RETURN 'REVIEW_CONFLICT';\n END IF;\n SELECT * INTO r FROM public.reviews WHERE id = p_review_id AND user_id = u FOR UPDATE;\n IF NOT FOUND THEN RETURN 'REVIEW_NOT_FOUND'; END IF;\n IF p_expected_updated_at IS NULL OR r.updated_at IS DISTINCT FROM p_expected_updated_at THEN RETURN 'REVIEW_CONFLICT'; END IF;\n IF p_kind = 'edit' THEN\n IF p_content IS NULL OR length(btrim(p_content)) < 20 OR p_categories IS NULL OR cardinality(p_categories) = 0\n OR p_food_photos IS NULL OR cardinality(p_food_photos) NOT BETWEEN 1 AND 10 THEN RETURN 'REVIEW_INVALID'; END IF;\n FOREACH p IN ARRAY p_food_photos LOOP\n -- The submitted list is the complete desired state. Historical display\n -- values can only be kept verbatim from this row or omitted, never added.\n IF p = ANY(COALESCE(r.food_photos, ARRAY[]::text[]))\n AND NOT review_media_private.canonical(p, u, r.id, 'food')\n AND NOT review_media_private.owned_legacy(p, u, 'food') THEN CONTINUE; END IF;\n IF NOT review_media_private.canonical(p, u, r.id, 'food')\n AND NOT (review_media_private.owned_legacy(p, u, 'food') AND p = ANY(COALESCE(r.food_photos, ARRAY[]::text[]))) THEN\n RETURN 'REVIEW_INVALID';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM storage.objects WHERE bucket_id = 'review-photos' AND name = p) THEN\n RETURN 'REVIEW_MEDIA_MISSING';\n END IF;\n END LOOP;\n UPDATE public.reviews SET content = btrim(p_content), categories = p_categories,\n food_photos = p_food_photos,\n is_verified = false, admin_note = NULL, updated_at = clock_timestamp()\n WHERE id = r.id AND user_id = u;\n ELSE\n DELETE FROM public.reviews WHERE id = r.id AND user_id = u;\n END IF;\n -- The AFTER trigger queues the authoritative pre-image in this transaction.\n INSERT INTO review_media_private.commits VALUES (p_operation_id, u, r.id, p_kind, h);\n RETURN 'REVIEW_COMMITTED';\nEND $$", + "-- Compensation requests are authority-checked and serialized too. A live\n-- reference is never retired, even if the caller incorrectly reports failure.\nCREATE FUNCTION public.queue_review_upload_cleanup(p_review_id uuid, p_paths text[])\nRETURNS text LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nDECLARE u uuid := auth.uid(); p text;\nBEGIN\n IF u IS NULL THEN RETURN 'REVIEW_UNAUTHORIZED'; END IF;\n IF current_setting('transaction_isolation') <> 'read committed' THEN RETURN 'REVIEW_RETRY_REQUIRED'; END IF;\n IF p_review_id IS NULL OR p_paths IS NULL OR cardinality(p_paths) > 10 THEN RETURN 'REVIEW_INVALID'; END IF;\n FOREACH p IN ARRAY p_paths LOOP\n IF NOT review_media_private.canonical(p, u, p_review_id, 'food') THEN RETURN 'REVIEW_INVALID'; END IF;\n END LOOP;\n PERFORM pg_advisory_xact_lock(741093, 1);\n FOREACH p IN ARRAY p_paths LOOP\n IF NOT review_media_private.referenced(p) THEN\n INSERT INTO review_media_private.cleanup(path, owner_id, review_id, purpose, retired)\n VALUES (p, u, p_review_id, 'food', true) ON CONFLICT DO NOTHING;\n END IF;\n END LOOP;\n RETURN 'REVIEW_CLEANUP_QUEUED';\nEND $$", + "CREATE FUNCTION public.read_review_media_commit(p_operation_id uuid, p_review_id uuid, p_kind text)\nRETURNS text LANGUAGE sql SECURITY DEFINER SET search_path = '' AS $$\n SELECT CASE WHEN EXISTS (SELECT 1 FROM review_media_private.commits\n WHERE operation_id = p_operation_id AND review_id = p_review_id AND kind = p_kind AND owner_id = auth.uid())\n THEN 'REVIEW_COMMITTED' ELSE 'REVIEW_NOT_CONFIRMED' END\n$$", + "CREATE FUNCTION public.pending_review_media_cleanup()\nRETURNS TABLE(path text, owner_id uuid, review_id uuid, purpose text)\nLANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF auth.uid() IS NULL OR current_setting('transaction_isolation') <> 'read committed' THEN RETURN; END IF;\n PERFORM pg_advisory_xact_lock(741093, 1);\n UPDATE review_media_private.cleanup c SET retired = true\n WHERE c.owner_id = auth.uid() AND NOT c.retired AND NOT review_media_private.referenced(c.path);\n RETURN QUERY SELECT c.path, c.owner_id, c.review_id, c.purpose FROM review_media_private.cleanup c\n WHERE c.owner_id = auth.uid() AND c.retired AND NOT c.complete\n AND NOT review_media_private.referenced(c.path) ORDER BY c.path LIMIT 20;\nEND $$", + "CREATE FUNCTION public.finish_review_media_cleanup()\nRETURNS bigint LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF auth.uid() IS NULL THEN RETURN NULL; END IF;\n UPDATE review_media_private.cleanup c SET complete = true\n WHERE c.owner_id = auth.uid() AND c.retired AND NOT c.complete\n AND NOT EXISTS (SELECT 1 FROM storage.objects o WHERE o.bucket_id = 'review-photos' AND o.name = c.path);\n RETURN (SELECT count(*) FROM review_media_private.cleanup c WHERE c.owner_id = auth.uid() AND NOT c.complete);\nEND $$", + "-- A restrictive policy also protects direct browser compensation by older\n-- clients: successful HTTP delete responses alone cannot bypass live references.\nCREATE FUNCTION public.review_media_delete_allowed(p_path text)\nRETURNS boolean LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE u uuid := auth.uid(); review_id uuid; purpose text;\nBEGIN\n IF u IS NULL OR current_setting('transaction_isolation') <> 'read committed' THEN RETURN false; END IF;\n IF octet_length(p_path) > 512 THEN RETURN false; END IF;\n -- Existing composer compensation also uses Storage.remove directly. Admit\n -- only canonical orphan keys, retiring them atomically before that delete;\n -- a delayed insert/update then fails rather than referring to deleted media.\n PERFORM pg_advisory_xact_lock(741093, 1);\n IF EXISTS (SELECT 1 FROM review_media_private.cleanup c WHERE c.path = p_path\n AND c.owner_id = u AND c.retired AND NOT c.complete\n AND review_media_private.owned_legacy(p_path, u, c.purpose)) THEN\n RETURN NOT review_media_private.referenced(p_path);\n END IF;\n BEGIN review_id := split_part(p_path, '/', 3)::uuid;\n EXCEPTION WHEN invalid_text_representation THEN RETURN false; END;\n purpose := split_part(p_path, '/', 4);\n IF NOT review_media_private.canonical(p_path, u, review_id, purpose) THEN RETURN false; END IF;\n IF review_media_private.referenced(p_path) THEN RETURN false; END IF;\n INSERT INTO review_media_private.cleanup(path, owner_id, review_id, purpose, retired)\n VALUES (p_path, u, review_id, purpose, true)\n ON CONFLICT (path) DO UPDATE SET retired = true;\n RETURN true;\nEND $$", + "CREATE POLICY review_media_safe_delete ON storage.objects AS RESTRICTIVE FOR DELETE TO authenticated\n USING (bucket_id <> 'review-photos' OR public.review_media_delete_allowed(name))", + "REVOKE ALL ON ALL FUNCTIONS IN SCHEMA review_media_private FROM PUBLIC, anon, authenticated", + "REVOKE ALL ON FUNCTION public.mutate_review_with_media(uuid,uuid,text,timestamptz,text,text[],text[]) FROM PUBLIC, anon", + "REVOKE ALL ON FUNCTION public.queue_review_upload_cleanup(uuid,text[]) FROM PUBLIC, anon", + "REVOKE ALL ON FUNCTION public.read_review_media_commit(uuid,uuid,text) FROM PUBLIC, anon", + "REVOKE ALL ON FUNCTION public.pending_review_media_cleanup() FROM PUBLIC, anon", + "REVOKE ALL ON FUNCTION public.finish_review_media_cleanup() FROM PUBLIC, anon", + "REVOKE ALL ON FUNCTION public.review_media_delete_allowed(text) FROM PUBLIC, anon", + "GRANT EXECUTE ON FUNCTION public.mutate_review_with_media(uuid,uuid,text,timestamptz,text,text[],text[]),\n public.queue_review_upload_cleanup(uuid,text[]), public.read_review_media_commit(uuid,uuid,text), public.pending_review_media_cleanup(),\n public.finish_review_media_cleanup(), public.review_media_delete_allowed(text) TO authenticated", + "COMMIT" + ] + }, + { + "id": "review_verification_private", + "version": "20261008200719", + "name": "review_verification_private", + "path": "backend/supabase/migrations/20261008200719_review_verification_private.sql", + "bytes": 5281, + "sha256": "92b469ab0ed1a23bf91c1ab5a3f8e92d45cf0f4e06e5fa602d6a34ad0497a51b", + "statementCount": 17, + "statementVectorSha256": "3d2e3409b2a271362fb8543b2357bcacece7a990f7960b74ae0807773854f664", + "originalStatementVector": [ + "-- Verification images are private; food photos retain their public contract.\n-- Historical verification objects must be moved with the Storage API, followed\n-- by metadata and public-denial readback. This DDL does not claim that transfer.\nBEGIN", + "INSERT INTO storage.buckets(id, name, public, file_size_limit, allowed_mime_types)\nVALUES ('review-verifications', 'review-verifications', false, 5242880,\n ARRAY['image/jpeg','image/png','image/webp','image/avif']::text[])\nON CONFLICT (id) DO UPDATE SET public = false, file_size_limit = EXCLUDED.file_size_limit,\n allowed_mime_types = EXCLUDED.allowed_mime_types", + "-- Serialize Storage writes with review references and retirement. A private\n-- object must not shadow a referenced public receipt, and a completed tombstone\n-- must continue to deny delayed uploads. Storage upsert/update uses both checks.\nCREATE FUNCTION public.review_media_upload_allowed(p_bucket text, p_path text)\nRETURNS boolean LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$\nDECLARE u uuid := auth.uid(); review_id uuid; purpose text;\nBEGIN\n IF u IS NULL OR current_setting('transaction_isolation') <> 'read committed'\n OR p_bucket IS NULL OR p_bucket NOT IN ('review-photos','review-verifications')\n OR p_path IS NULL OR octet_length(p_path) > 512 THEN RETURN false; END IF;\n BEGIN review_id := split_part(p_path, '/', 3)::uuid;\n EXCEPTION WHEN invalid_text_representation THEN RETURN false; END;\n purpose := CASE p_bucket WHEN 'review-photos' THEN 'food' ELSE 'verification' END;\n IF NOT review_media_private.canonical(p_path, u, review_id, purpose) THEN RETURN false; END IF;\n PERFORM pg_advisory_xact_lock(741093, 1);\n RETURN NOT EXISTS (SELECT 1 FROM review_media_private.cleanup c WHERE c.path = p_path AND c.retired)\n AND NOT review_media_private.referenced(p_path);\nEND $$", + "REVOKE ALL ON FUNCTION public.review_media_upload_allowed(text,text) FROM PUBLIC, anon", + "GRANT EXECUTE ON FUNCTION public.review_media_upload_allowed(text,text) TO authenticated", + "CREATE POLICY review_media_safe_insert ON storage.objects AS RESTRICTIVE FOR INSERT TO authenticated\n WITH CHECK (bucket_id NOT IN ('review-photos','review-verifications') OR public.review_media_upload_allowed(bucket_id,name))", + "CREATE POLICY review_media_safe_update ON storage.objects AS RESTRICTIVE FOR UPDATE TO authenticated\n USING (bucket_id NOT IN ('review-photos','review-verifications') OR public.review_media_upload_allowed(bucket_id,name))\n WITH CHECK (bucket_id NOT IN ('review-photos','review-verifications') OR public.review_media_upload_allowed(bucket_id,name))", + "CREATE POLICY review_verifications_owner_read ON storage.objects FOR SELECT TO authenticated\n USING (bucket_id = 'review-verifications' AND\n ((storage.foldername(name))[1] = (SELECT auth.uid()::text)\n OR public.has_role((SELECT auth.uid()), 'admin'::public.app_role)))", + "CREATE POLICY review_verifications_owner_insert ON storage.objects FOR INSERT TO authenticated\n WITH CHECK (bucket_id = 'review-verifications' AND\n (storage.foldername(name))[1] = (SELECT auth.uid()::text)\n AND split_part(name, '/', 4) = 'verification')", + "CREATE POLICY review_verifications_owner_delete ON storage.objects FOR DELETE TO authenticated\n USING (bucket_id = 'review-verifications' AND\n (storage.foldername(name))[1] = (SELECT auth.uid()::text))", + "CREATE POLICY review_verifications_safe_delete ON storage.objects AS RESTRICTIVE FOR DELETE TO authenticated\n USING (bucket_id <> 'review-verifications' OR public.review_media_delete_allowed(name))", + "-- Old browser composers must fail before publishing a verification image into\n-- the public bucket. Public food uploads keep the current policy unchanged.\nCREATE POLICY review_photos_food_insert ON storage.objects AS RESTRICTIVE FOR INSERT TO authenticated\n WITH CHECK (bucket_id <> 'review-photos' OR (split_part(name, '/', 4) = 'food'\n AND (storage.foldername(name))[1] = (SELECT auth.uid()::text)))", + "CREATE POLICY review_photos_food_update ON storage.objects AS RESTRICTIVE FOR UPDATE TO authenticated\n USING (bucket_id <> 'review-photos' OR (split_part(name, '/', 4) = 'food'\n AND (storage.foldername(name))[1] = (SELECT auth.uid()::text)))\n WITH CHECK (bucket_id <> 'review-photos' OR (split_part(name, '/', 4) = 'food'\n AND (storage.foldername(name))[1] = (SELECT auth.uid()::text)))", + "CREATE OR REPLACE FUNCTION public.finish_review_media_cleanup()\nRETURNS bigint LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$\nBEGIN\n IF auth.uid() IS NULL THEN RETURN NULL; END IF;\n UPDATE review_media_private.cleanup c SET complete = true\n WHERE c.owner_id = auth.uid() AND c.retired AND NOT c.complete\n AND NOT EXISTS (SELECT 1 FROM storage.objects o\n WHERE o.bucket_id = CASE c.purpose WHEN 'verification' THEN 'review-verifications' ELSE 'review-photos' END\n AND o.name = c.path)\n AND NOT EXISTS (SELECT 1 FROM storage.objects o WHERE o.bucket_id = 'review-photos' AND o.name = c.path);\n RETURN (SELECT count(*) FROM review_media_private.cleanup c WHERE c.owner_id = auth.uid() AND NOT c.complete);\nEND $$", + "REVOKE ALL ON FUNCTION public.finish_review_media_cleanup() FROM PUBLIC, anon", + "GRANT EXECUTE ON FUNCTION public.finish_review_media_cleanup() TO authenticated", + "COMMIT" + ] + }, + { + "id": "review_media_catalog_integration", + "version": "20261008201635", + "name": "review_media_catalog_integration", + "path": "backend/supabase/migrations/20261008201635_review_media_catalog_integration.sql", + "bytes": 47177, + "sha256": "6e820eb535bc514e78f258110e36dff00537bcaefcffd8b354356107ca9c09fe", + "statementCount": 11, + "statementVectorSha256": "e69060e11abab0564f63decd2ba601630d13724c0a71b2f6cb2c616c0bb03e9c", + "originalStatementVector": [ + "-- Integrate exactly the two preceding review-media sources into G014.\n-- CLI 2.117.0 generated this new migration. No assertion body or immutable\n-- manifest row is changed. Execute the complete transaction; do not split it.\nBEGIN", + "SET LOCAL lock_timeout = '2s'", + "SELECT pg_catalog.pg_advisory_xact_lock(pg_catalog.hashtextextended('tzudong:review-media-catalog:v1', 0))", + "CREATE TEMP TABLE review_media_expected (\n signature text PRIMARY KEY, body_sha256 text NOT NULL, final_body_sha256 text NOT NULL, definer boolean NOT NULL,\n volatility text NOT NULL, language text NOT NULL, result text NOT NULL, argument_names text[] NOT NULL\n) ON COMMIT DROP", + "INSERT INTO review_media_expected VALUES\n ('public.finish_review_media_cleanup()','f7e925a25dedb987a3166c5ca641ba7db1568a7583bdeca3cee04f27d9189f20','f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1',true,'v','plpgsql','bigint',ARRAY[]::text[]),\n ('public.mutate_review_with_media(uuid,uuid,text,timestamp with time zone,text,text[],text[])','6f1450d4afaa7d3e9dcba3abb231de62aeb7c0a687cc2a9ceafbbe6dc2c2e413','50bfad7bc4fc7ddbc9d9379e8eb13facd938a15f66553576cc69fbcf69aa98b1',true,'v','plpgsql','text',ARRAY['p_operation_id','p_review_id','p_kind','p_expected_updated_at','p_content','p_categories','p_food_photos']::text[]),\n ('public.pending_review_media_cleanup()','21fa71446354fe855508550a3df78ddbe0ad4c1f7007ea905d04bdb98f1ac766','5a92c8f282c895eabaa77f8cf3f452fb659bac33bd519ed8cf439dc1f2905c7b',true,'v','plpgsql','TABLE(path text, owner_id uuid, review_id uuid, purpose text)',ARRAY[]::text[]),\n ('public.queue_review_upload_cleanup(uuid,text[])','00784e9daa9b6b707432200e4df6592010e9f22d2bc9db831bf8329e14bdb453','89c3b573891f598cac5df6ce9a3217639092801a7439ec0404f338f2097114a8',true,'v','plpgsql','text',ARRAY['p_review_id','p_paths']::text[]),\n ('public.read_review_media_commit(uuid,uuid,text)','c7d2e4e111223150ceeda5893310df0e9656e5930823db053135812a69222081','2f14abfc04864f369467f5ac5f0603defa149b8023d7e6b04e2bb8535f8a268e',true,'v','sql','text',ARRAY['p_operation_id','p_review_id','p_kind']::text[]),\n ('public.review_media_delete_allowed(text)','45fd396f240fb1d9fc084f31bd4feea7ee097017f2fd2b8d12df9c2c59152f78','a75dbd787dd388b0761bee11c82a56d5f410688e4cfd953181bce4d613062ae7',true,'v','plpgsql','boolean',ARRAY['p_path']::text[]),\n ('public.review_media_upload_allowed(text,text)','0a3d375685c5e3fd82721bd8033345c93031704c838a0a7f1b61ff248d9b6636','298c6a127f3a44a193b78be8c1f2577ab58aa34c772b66ccd53f774d807cc90c',true,'v','plpgsql','boolean',ARRAY['p_bucket','p_path']::text[]),\n ('review_media_private.canonical(text,uuid,uuid,text)','87d8397c36cf4c7b986f75cbc2788979333a76be2cc26dc5522cd362f62414a9','87d8397c36cf4c7b986f75cbc2788979333a76be2cc26dc5522cd362f62414a9',false,'i','sql','boolean',ARRAY['p_path','p_owner','p_review','p_purpose']::text[]),\n ('review_media_private.enqueue_removed()','7edb21d70ea3ff81ab90e13617e8c1c2749b54b1506f1ff42aaa6c5dc33f9e38','7edb21d70ea3ff81ab90e13617e8c1c2749b54b1506f1ff42aaa6c5dc33f9e38',true,'v','plpgsql','trigger',ARRAY[]::text[]),\n ('review_media_private.guard_references()','846030547ffd76e17596755d7eee4279dcb9b563361435260826071d2f250d59','846030547ffd76e17596755d7eee4279dcb9b563361435260826071d2f250d59',true,'v','plpgsql','trigger',ARRAY[]::text[]),\n ('review_media_private.lock_changes()','381d89aa6bbf5a59c2ee0fef2a7392ef705147252b324527201b8b6f74376250','381d89aa6bbf5a59c2ee0fef2a7392ef705147252b324527201b8b6f74376250',true,'v','plpgsql','trigger',ARRAY[]::text[]),\n ('review_media_private.owned_legacy(text,uuid,text)','a2d55f09f03fcaeedd77461198511673da7342a6a4873d37227f9cf612090980','a2d55f09f03fcaeedd77461198511673da7342a6a4873d37227f9cf612090980',false,'i','sql','boolean',ARRAY['p_path','p_owner','p_purpose']::text[]),\n ('review_media_private.reference_key(text)','a8910cb09bfd959951045e8b0b8233b1e2a8bdc45073abeebdfc26b552b1dae7','a8910cb09bfd959951045e8b0b8233b1e2a8bdc45073abeebdfc26b552b1dae7',false,'i','plpgsql','text',ARRAY['p_value']::text[]),\n ('review_media_private.referenced(text)','cc295115d85a3a11b380e1378e2ee91b0d9f7f587c68839653f54ed826c398ce','cc295115d85a3a11b380e1378e2ee91b0d9f7f587c68839653f54ed826c398ce',true,'v','sql','boolean',ARRAY['p_path']::text[])", + "REVOKE ALL ON pg_temp.review_media_expected FROM PUBLIC, anon, authenticated, service_role", + "GRANT SELECT ON pg_temp.review_media_expected TO privacy_workflow_owner", + "DO $integration$\nDECLARE\n v_expected record; v_function record; v_oid oid;\n v_owner oid := 'privacy_workflow_owner'::regrole;\n v_runner oid := 'postgres'::regrole;\n v_members jsonb; v_assertions jsonb; v_manifest jsonb; v_functions jsonb;\n v_known oid := (SELECT p.oid FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace\n WHERE n.nspname='privacy_retention' AND p.proname='g014_catalog_protected_relations' AND p.pronargs=0);\n v_definition text;\n v_public jsonb;\n v_self jsonb; v_major integer := current_setting('server_version_num')::integer / 10000;\n v_pg15_lease boolean := false;\nBEGIN\n IF current_user <> 'postgres' OR session_user <> 'postgres' OR v_major NOT IN (15,17)\n OR current_setting('transaction_read_only') <> 'off'\n OR NOT EXISTS (SELECT 1 FROM pg_roles WHERE oid=v_owner AND NOT rolsuper\n AND NOT rolbypassrls AND NOT rolcanlogin AND NOT rolinherit) THEN\n RAISE EXCEPTION 'review_media_catalog_executor_denied';\n END IF;\n SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]')\n INTO v_members FROM pg_auth_members m;\n SELECT to_jsonb(n) INTO v_public FROM pg_namespace n WHERE nspname='public';\n IF NOT has_schema_privilege(v_owner,'public','CREATE') THEN\n RAISE EXCEPTION 'review_media_catalog_existing_public_create_missing';\n END IF;\n SELECT to_jsonb(m) INTO v_self FROM pg_auth_members m\n WHERE roleid=v_owner AND member=v_runner AND grantor=v_runner;\n IF v_self IS NOT NULL AND (v_self->>'admin_option')::boolean THEN\n RAISE EXCEPTION 'review_media_catalog_membership_denied';\n END IF;\n SELECT jsonb_agg(CASE WHEN p.oid=v_known THEN to_jsonb(p)-'prosrc' ELSE to_jsonb(p) END ORDER BY p.oid) INTO v_assertions FROM pg_proc p\n WHERE p.pronamespace='privacy_retention'::regnamespace;\n -- Pre-existing RPC/helper bodies and complete type/default metadata are pinned\n -- before any ownership or grant change. No unknown overload is admitted.\n FOR v_expected IN SELECT * FROM pg_temp.review_media_expected LOOP\n v_oid := to_regprocedure(v_expected.signature);\n SELECT * INTO v_function FROM pg_proc WHERE oid=v_oid;\n IF v_oid IS NULL OR v_function.proowner<>v_runner OR v_function.prokind<>'f'\n OR v_function.prosecdef IS DISTINCT FROM v_expected.definer OR v_function.provolatile::text<>v_expected.volatility\n OR v_function.proconfig IS DISTINCT FROM ARRAY['search_path=\"\"']::text[]\n OR (SELECT lanname FROM pg_language WHERE oid=v_function.prolang)<>v_expected.language\n OR pg_get_function_result(v_oid) IS DISTINCT FROM v_expected.result\n OR coalesce(v_function.proargnames[1:v_function.pronargs],ARRAY[]::text[]) IS DISTINCT FROM v_expected.argument_names\n OR encode(sha256(convert_to(v_function.prosrc,'UTF8')),'hex')<>v_expected.body_sha256\n OR v_function.pronargdefaults<>(CASE WHEN v_function.proname='mutate_review_with_media' THEN 3 ELSE 0 END)\n OR (v_function.pronargdefaults=3 AND pg_get_expr(v_function.proargdefaults,0)<>'NULL::text, NULL::text[], NULL::text[]')\n OR (SELECT count(*) FROM pg_proc x WHERE x.pronamespace=v_function.pronamespace AND x.proname=v_function.proname)<>1 THEN\n RAISE EXCEPTION 'review_media_catalog_source_drift';\n END IF;\n END LOOP;\n IF (SELECT count(*) FROM pg_proc WHERE pronamespace='review_media_private'::regnamespace)<>7 THEN\n RAISE EXCEPTION 'review_media_catalog_identity_conflict';\n END IF;\n -- Preserve semantic defaults. PG15 pg_node_tree contains parser location\n -- offsets that change when the identical definition is recreated. The initial\n -- exact source/default checks above remain mandatory; retain every other field.\n SELECT jsonb_agg(((to_jsonb(p)-'proowner'-'proacl'-'prosrc'-'proargdefaults') || jsonb_build_object('proargdefaults',pg_get_expr(p.proargdefaults,0))) ORDER BY p.oid) INTO v_functions\n FROM pg_proc p JOIN pg_temp.review_media_expected e ON p.oid=to_regprocedure(e.signature);\n\n -- Use only the existing G014 owner-management authority. This transaction's\n -- noninheriting SET lease is restored exactly before unchanged assertions run.\n -- Never grant a Storage/service role or alter supautils/global configuration.\n IF v_major=17 AND NOT EXISTS (SELECT 1 FROM pg_auth_members\n WHERE roleid=v_owner AND member=v_runner AND admin_option AND grantor<>v_runner) THEN\n RAISE EXCEPTION 'review_media_catalog_existing_owner_admin_missing';\n END IF;\n IF v_major=17 THEN\n EXECUTE 'GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE, INHERIT FALSE, SET TRUE GRANTED BY postgres';\n ELSE\n -- PG15 has one row per role/member pair, even when another grantor owns it.\n -- Lease only absent membership; never revoke a pre-existing grantor's row.\n v_pg15_lease := NOT pg_has_role(v_runner,v_owner,'MEMBER');\n IF v_pg15_lease THEN GRANT privacy_workflow_owner TO postgres; END IF;\n END IF;\n SET LOCAL ROLE privacy_workflow_owner;\n -- Existing G041 claim boundary: never restore auth-schema access for the\n -- workflow owner. Pin the unchanged helper before substituting auth.uid().\n IF has_schema_privilege(v_owner,'auth','USAGE,CREATE') OR NOT EXISTS (\n SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace\n WHERE n.nspname='privacy_retention' AND p.proname='g041_current_claim_user_id'\n AND p.pronargs=0 AND p.proowner=v_owner AND p.prosecdef AND p.prokind='f'\n AND NOT p.proretset AND p.prorettype='uuid'::regtype AND p.provolatile='s'\n AND p.prolang=(SELECT oid FROM pg_language WHERE lanname='plpgsql')\n AND p.proconfig=ARRAY['search_path=\"\"']::text[]\n AND encode(sha256(convert_to(p.prosrc,'UTF8')),'hex')='43f3ff2ed914944802555c4ffe3ff8ecece213e82288e2c1c47ae22efdf81585'\n AND has_function_privilege(v_owner,p.oid,'EXECUTE')\n AND NOT has_function_privilege('anon',p.oid,'EXECUTE')\n AND NOT has_function_privilege('authenticated',p.oid,'EXECUTE')\n AND NOT has_function_privilege('service_role',p.oid,'EXECUTE')\n ) THEN RAISE EXCEPTION 'review_media_catalog_claim_boundary_drift'; END IF;\n IF EXISTS (SELECT 1 FROM privacy_retention.g014_public_rpc_allowlist a\n JOIN pg_temp.review_media_expected e ON a.function_name=split_part(split_part(e.signature,'(',1),'.',2)\n WHERE a.function_schema='public') THEN\n RAISE EXCEPTION 'review_media_catalog_identity_conflict';\n END IF;\n SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) INTO v_manifest\n FROM privacy_retention.g014_catalog_contract_manifest m;\n PERFORM privacy_retention.assert_g014_catalog_manifest();\n RESET ROLE;\n GRANT USAGE, CREATE ON SCHEMA review_media_private TO privacy_workflow_owner;\n FOR v_expected IN SELECT * FROM pg_temp.review_media_expected LOOP\n EXECUTE format('ALTER FUNCTION %s OWNER TO privacy_workflow_owner',to_regprocedure(v_expected.signature));\n SET LOCAL ROLE privacy_workflow_owner;\n IF v_expected.signature LIKE 'public.%' THEN\n SELECT pg_get_functiondef(oid) INTO v_definition FROM pg_proc WHERE oid=to_regprocedure(v_expected.signature);\n EXECUTE replace(v_definition,'auth.uid()','privacy_retention.g041_current_claim_user_id()');\n END IF;\n IF (SELECT encode(sha256(convert_to(prosrc,'UTF8')),'hex') FROM pg_proc\n WHERE oid=to_regprocedure(v_expected.signature)) IS DISTINCT FROM v_expected.final_body_sha256 THEN\n RAISE EXCEPTION 'review_media_catalog_claim_transition_drift';\n END IF;\n EXECUTE format('REVOKE ALL ON FUNCTION %s FROM PUBLIC, anon, authenticated, service_role',to_regprocedure(v_expected.signature));\n IF v_expected.signature LIKE 'public.%' THEN\n EXECUTE format('GRANT EXECUTE ON FUNCTION %s TO authenticated',to_regprocedure(v_expected.signature));\n END IF;\n RESET ROLE;\n END LOOP;\n REVOKE CREATE ON SCHEMA review_media_private FROM privacy_workflow_owner;\n REVOKE ALL ON SCHEMA review_media_private FROM PUBLIC, anon, authenticated, service_role;\n REVOKE ALL ON TABLE review_media_private.commits, review_media_private.cleanup FROM PUBLIC, anon, authenticated, service_role;\n -- Private tables keep their migration owner. The implementation role receives\n -- only operations used by the reviewed functions, with explicit RLS policies.\n GRANT SELECT, INSERT ON review_media_private.commits TO privacy_workflow_owner;\n GRANT SELECT, INSERT, UPDATE ON review_media_private.cleanup TO privacy_workflow_owner;\n ALTER TABLE review_media_private.commits FORCE ROW LEVEL SECURITY;\n ALTER TABLE review_media_private.cleanup FORCE ROW LEVEL SECURITY;\n CREATE POLICY review_media_commits_select ON review_media_private.commits FOR SELECT TO privacy_workflow_owner USING (true);\n CREATE POLICY review_media_commits_insert ON review_media_private.commits FOR INSERT TO privacy_workflow_owner WITH CHECK (true);\n CREATE POLICY review_media_cleanup_select ON review_media_private.cleanup FOR SELECT TO privacy_workflow_owner USING (true);\n CREATE POLICY review_media_cleanup_insert ON review_media_private.cleanup FOR INSERT TO privacy_workflow_owner WITH CHECK (true);\n CREATE POLICY review_media_cleanup_update ON review_media_private.cleanup FOR UPDATE TO privacy_workflow_owner USING (true) WITH CHECK (true);\n -- Existing account-deletion contracts already require reviews SELECT/DELETE\n -- and storage.objects SELECT. Add only the six columns this RPC updates.\n GRANT UPDATE(content,categories,food_photos,is_verified,admin_note,updated_at)\n ON public.reviews TO privacy_workflow_owner;\n -- Existing supautils.policy_grants may authorize this DDL without table-owner\n -- USAGE/SET membership. Let the actual policy command decide; no fallback grant.\n CREATE POLICY review_media_workflow_read ON storage.objects FOR SELECT TO privacy_workflow_owner\n USING (bucket_id IN ('review-photos','review-verifications'));\n\n SET LOCAL ROLE privacy_workflow_owner;\n INSERT INTO privacy_retention.g014_public_rpc_allowlist\n (function_schema,function_name,identity_arguments,grantee,source_signature)\n SELECT 'public',p.proname,p.proargtypes::text,'authenticated',e.signature\n FROM pg_temp.review_media_expected e JOIN pg_proc p ON p.oid=to_regprocedure(e.signature)\n WHERE e.signature LIKE 'public.%';\n IF NOT FOUND THEN RAISE EXCEPTION 'review_media_catalog_allowlist_missing'; END IF;\n\n PERFORM privacy_retention.assert_g014_catalog_manifest();\n IF EXISTS (\n WITH expected(relation,ordinal,name,type,not_null,default_expr) AS (VALUES\n ('commits',1,'operation_id','uuid',true,NULL::text),\n ('commits',2,'owner_id','uuid',true,NULL::text),\n ('commits',3,'review_id','uuid',true,NULL::text),\n ('commits',4,'kind','text',true,NULL::text),\n ('commits',5,'request_hash','text',true,NULL::text),\n ('cleanup',1,'path','text',true,NULL::text),\n ('cleanup',2,'owner_id','uuid',true,NULL::text),\n ('cleanup',3,'review_id','uuid',true,NULL::text),\n ('cleanup',4,'purpose','text',true,NULL::text),\n ('cleanup',5,'retired','boolean',true,'false'),\n ('cleanup',6,'complete','boolean',true,'false')\n ), actual AS (\n SELECT c.relname::text,a.attnum::integer,a.attname::text,pg_catalog.format_type(a.atttypid,a.atttypmod),a.attnotnull,\n pg_catalog.pg_get_expr(d.adbin,d.adrelid)\n FROM pg_catalog.pg_class c JOIN pg_catalog.pg_attribute a ON a.attrelid=c.oid AND a.attnum>0 AND NOT a.attisdropped\n LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid=a.attrelid AND d.adnum=a.attnum\n WHERE c.relnamespace='review_media_private'::regnamespace AND c.relkind IN ('r','p')\n )\n (SELECT * FROM expected EXCEPT SELECT * FROM actual) UNION ALL (SELECT * FROM actual EXCEPT SELECT * FROM expected)\n ) THEN RAISE EXCEPTION 'review_media_catalog_column_shape_drift'; END IF;\n IF EXISTS (\n WITH expected(relation,name,definition) AS (VALUES\n ('commits','commits_pkey','PRIMARY KEY (operation_id)'),\n ('commits','commits_kind_check','CHECK ((kind = ANY (ARRAY[''edit''::text, ''delete''::text])))'),\n ('cleanup','cleanup_pkey','PRIMARY KEY (path)'),\n ('cleanup','cleanup_purpose_check','CHECK ((purpose = ANY (ARRAY[''food''::text, ''verification''::text])))')\n ), actual AS (\n SELECT c.relname::text,k.conname::text,pg_catalog.pg_get_constraintdef(k.oid)\n FROM pg_catalog.pg_class c JOIN pg_catalog.pg_constraint k ON k.conrelid=c.oid\n WHERE c.relnamespace='review_media_private'::regnamespace\n )\n (SELECT * FROM expected EXCEPT SELECT * FROM actual) UNION ALL (SELECT * FROM actual EXCEPT SELECT * FROM expected)\n ) OR EXISTS (SELECT 1 FROM pg_catalog.pg_trigger t JOIN pg_catalog.pg_class c ON c.oid=t.tgrelid\n WHERE c.relnamespace='review_media_private'::regnamespace AND NOT t.tgisinternal)\n OR EXISTS (SELECT 1 FROM pg_catalog.pg_constraint k JOIN pg_catalog.pg_class c ON c.oid=k.conrelid\n WHERE c.relnamespace='review_media_private'::regnamespace AND (NOT k.convalidated OR k.condeferrable OR k.condeferred))\n OR (SELECT count(*) FROM pg_catalog.pg_index i JOIN pg_catalog.pg_class c ON c.oid=i.indrelid\n WHERE c.relnamespace='review_media_private'::regnamespace)<>2\n OR EXISTS (SELECT 1 FROM pg_catalog.pg_index i JOIN pg_catalog.pg_class c ON c.oid=i.indrelid\n WHERE c.relnamespace='review_media_private'::regnamespace AND (NOT i.indisprimary OR NOT i.indisvalid OR NOT i.indisready)) THEN\n RAISE EXCEPTION 'review_media_catalog_constraint_shape_drift';\n END IF;\n -- Extend only the declarative protected-relation list, never an assertion.\n IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_proc WHERE oid=v_known AND proowner=v_owner\n AND prosecdef AND prokind='f' AND proretset AND provolatile='s'\n AND proconfig=ARRAY['search_path=\"\"']::text[]\n AND prolang=(SELECT oid FROM pg_catalog.pg_language WHERE lanname='sql')\n AND pg_catalog.pg_get_function_result(oid)='TABLE(schema_name name, relation_name name)') THEN\n RAISE EXCEPTION 'review_media_catalog_known_identity_metadata_drift';\n END IF;\n SELECT pg_catalog.pg_get_functiondef(v_known) INTO v_definition;\n IF (SELECT pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(prosrc,'UTF8')),'hex') FROM pg_catalog.pg_proc WHERE oid=v_known)<>'43bf370fe74864708e9efbc6559d899a6afa5b353e6cbc930306e31c9ecdc499'\n OR EXISTS (SELECT 1 FROM privacy_retention.g014_catalog_contract_manifest\n WHERE manifest_key->>'schema'='review_media_private') THEN\n RAISE EXCEPTION 'review_media_catalog_known_identity_source_drift';\n END IF;\n EXECUTE pg_catalog.replace(v_definition,\n $known_anchor$ ('public', 'notifications'),$known_anchor$,\n $known_replacement$ ('review_media_private', 'commits'),\n ('review_media_private', 'cleanup'),\n ('public', 'notifications'),$known_replacement$);\n IF (SELECT pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(prosrc,'UTF8')),'hex') FROM pg_catalog.pg_proc WHERE oid=v_known)<>'20acd5742d891cf8a33b265a3a1298c65dd9646402c58391cd17829951cf92f2' THEN\n RAISE EXCEPTION 'review_media_catalog_known_identity_post_drift';\n END IF;\n INSERT INTO privacy_retention.g014_catalog_contract_manifest(manifest_kind,manifest_key,manifest_value)\n SELECT manifest_kind,manifest_key,manifest_value FROM privacy_retention.g014_catalog_manifest_rows()\n WHERE manifest_key->>'schema'='review_media_private'\n AND manifest_key->>'relation' IN ('commits','cleanup');\n IF NOT FOUND THEN RAISE EXCEPTION 'review_media_catalog_manifest_append_missing'; END IF;\n PERFORM privacy_retention.assert_g014_catalog_manifest();\n CREATE FUNCTION pg_temp.review_media_g014_assert() RETURNS void\n LANGUAGE plpgsql SECURITY DEFINER SET search_path='' AS $bridge$\n BEGIN\n IF EXISTS (SELECT 1 FROM pg_temp.review_media_expected e\n JOIN pg_proc p ON p.oid=to_regprocedure(e.signature)\n WHERE e.signature LIKE 'public.%' AND (\n (SELECT count(*) FROM privacy_retention.g014_public_rpc_allowlist a\n WHERE a.function_schema='public' AND a.function_name=p.proname)<>1\n OR NOT EXISTS (SELECT 1 FROM privacy_retention.g014_public_rpc_allowlist a\n WHERE a.source_signature=e.signature AND a.function_schema='public' AND a.function_name=p.proname\n AND a.identity_arguments=p.proargtypes::text AND a.grantee='authenticated'))) THEN\n RAISE EXCEPTION 'review_media_catalog_allowlist_drift';\n END IF;\n PERFORM privacy_retention.assert_g014_public_rpc_allowlist();\n PERFORM privacy_retention.assert_g014_definer_contract();\n PERFORM privacy_retention.assert_g014_catalog_contract();\n DROP FUNCTION pg_temp.review_media_g014_assert();\n END $bridge$;\n REVOKE ALL ON FUNCTION pg_temp.review_media_g014_assert() FROM PUBLIC,anon,authenticated,service_role;\n GRANT EXECUTE ON FUNCTION pg_temp.review_media_g014_assert() TO postgres;\n IF (SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key)\n FROM privacy_retention.g014_catalog_contract_manifest m\n WHERE m.manifest_key->>'schema' IS DISTINCT FROM 'review_media_private') IS DISTINCT FROM v_manifest THEN\n RAISE EXCEPTION 'review_media_catalog_manifest_drift';\n END IF;\n RESET ROLE;\n IF v_major=15 THEN\n IF v_pg15_lease THEN REVOKE privacy_workflow_owner FROM postgres; END IF;\n ELSIF v_self IS NULL THEN\n REVOKE privacy_workflow_owner FROM postgres GRANTED BY postgres;\n ELSE\n EXECUTE format('GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE, INHERIT %s, SET %s GRANTED BY postgres',\n upper(v_self->>'inherit_option'),upper(v_self->>'set_option'));\n END IF;\n IF (SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') FROM pg_auth_members m) IS DISTINCT FROM v_members\n OR (SELECT to_jsonb(n) FROM pg_namespace n WHERE nspname='public') IS DISTINCT FROM v_public\n OR (SELECT jsonb_agg(CASE WHEN p.oid=v_known THEN to_jsonb(p)-'prosrc' ELSE to_jsonb(p) END ORDER BY p.oid) FROM pg_proc p\n WHERE p.pronamespace='privacy_retention'::regnamespace) IS DISTINCT FROM v_assertions\n OR (SELECT jsonb_agg(((to_jsonb(p)-'proowner'-'proacl'-'prosrc'-'proargdefaults') || jsonb_build_object('proargdefaults',pg_get_expr(p.proargdefaults,0))) ORDER BY p.oid) FROM pg_proc p\n JOIN pg_temp.review_media_expected e ON p.oid=to_regprocedure(e.signature)) IS DISTINCT FROM v_functions THEN\n RAISE EXCEPTION 'review_media_catalog_preservation_drift' USING DETAIL =\n jsonb_build_object(\n 'membershipPreserved',(SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') FROM pg_auth_members m) IS NOT DISTINCT FROM v_members,\n 'publicNamespacePreserved',(SELECT to_jsonb(n) FROM pg_namespace n WHERE nspname='public') IS NOT DISTINCT FROM v_public,\n 'assertionsPreserved',(SELECT jsonb_agg(CASE WHEN p.oid=v_known THEN to_jsonb(p)-'prosrc' ELSE to_jsonb(p) END ORDER BY p.oid) FROM pg_proc p\n WHERE p.pronamespace='privacy_retention'::regnamespace) IS NOT DISTINCT FROM v_assertions,\n 'functionMetadataPreserved',(SELECT jsonb_agg(((to_jsonb(p)-'proowner'-'proacl'-'prosrc'-'proargdefaults') || jsonb_build_object('proargdefaults',pg_get_expr(p.proargdefaults,0))) ORDER BY p.oid) FROM pg_proc p\n JOIN pg_temp.review_media_expected e ON p.oid=to_regprocedure(e.signature)) IS NOT DISTINCT FROM v_functions,\n 'changedMetadataFields',(SELECT jsonb_agg(DISTINCT old_field.key ORDER BY old_field.key)\n FROM jsonb_array_elements(v_functions) AS old_function(value)\n CROSS JOIN LATERAL jsonb_each(old_function.value) AS old_field(key,value)\n JOIN pg_proc p ON p.oid=(old_function.value->>'oid')::oid\n WHERE old_field.value IS DISTINCT FROM (((to_jsonb(p)-'proowner'-'proacl'-'prosrc'-'proargdefaults') || jsonb_build_object('proargdefaults',pg_get_expr(p.proargdefaults,0))))->old_field.key)\n )::text;\n END IF;\n PERFORM pg_temp.review_media_g014_assert();\n IF to_regprocedure('pg_temp.review_media_g014_assert()') IS NOT NULL THEN\n RAISE EXCEPTION 'review_media_catalog_bridge_remaining';\n END IF;\nEND $integration$", + "-- REVIEW_MEDIA_READBACK_BEGIN\n-- The same independent catalog-only checks are embedded in canonical readback.\nDO $review_media_catalog_readback$\nDECLARE\n v_expected record; v_function record; v_relation record;\n v_owner oid := 'privacy_workflow_owner'::regrole;\n v_path text := current_setting('search_path');\nBEGIN\n -- Existing G041 claim boundary: never restore auth-schema access for the\n -- workflow owner. Pin the unchanged helper before substituting auth.uid().\n IF has_schema_privilege(v_owner,'auth','USAGE,CREATE') OR NOT EXISTS (\n SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace\n WHERE n.nspname='privacy_retention' AND p.proname='g041_current_claim_user_id'\n AND p.pronargs=0 AND p.proowner=v_owner AND p.prosecdef AND p.prokind='f'\n AND NOT p.proretset AND p.prorettype='uuid'::regtype AND p.provolatile='s'\n AND p.prolang=(SELECT oid FROM pg_language WHERE lanname='plpgsql')\n AND p.proconfig=ARRAY['search_path=\"\"']::text[]\n AND encode(sha256(convert_to(p.prosrc,'UTF8')),'hex')='43f3ff2ed914944802555c4ffe3ff8ecece213e82288e2c1c47ae22efdf81585'\n AND has_function_privilege(v_owner,p.oid,'EXECUTE')\n AND NOT has_function_privilege('anon',p.oid,'EXECUTE')\n AND NOT has_function_privilege('authenticated',p.oid,'EXECUTE')\n AND NOT has_function_privilege('service_role',p.oid,'EXECUTE')\n ) THEN RAISE EXCEPTION 'review_media_catalog_claim_boundary_drift'; END IF;\n -- Fix pg_get_expr qualification before comparing the exact parsed predicates.\n PERFORM set_config('search_path','public, pg_catalog',true);\n FOR v_expected IN SELECT * FROM (VALUES\n ('public.finish_review_media_cleanup()','f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1',true,'v','plpgsql','bigint',ARRAY[]::text[]),\n ('public.mutate_review_with_media(uuid,uuid,text,timestamp with time zone,text,text[],text[])','50bfad7bc4fc7ddbc9d9379e8eb13facd938a15f66553576cc69fbcf69aa98b1',true,'v','plpgsql','text',ARRAY['p_operation_id','p_review_id','p_kind','p_expected_updated_at','p_content','p_categories','p_food_photos']::text[]),\n ('public.pending_review_media_cleanup()','5a92c8f282c895eabaa77f8cf3f452fb659bac33bd519ed8cf439dc1f2905c7b',true,'v','plpgsql','TABLE(path text, owner_id uuid, review_id uuid, purpose text)',ARRAY[]::text[]),\n ('public.queue_review_upload_cleanup(uuid,text[])','89c3b573891f598cac5df6ce9a3217639092801a7439ec0404f338f2097114a8',true,'v','plpgsql','text',ARRAY['p_review_id','p_paths']::text[]),\n ('public.read_review_media_commit(uuid,uuid,text)','2f14abfc04864f369467f5ac5f0603defa149b8023d7e6b04e2bb8535f8a268e',true,'v','sql','text',ARRAY['p_operation_id','p_review_id','p_kind']::text[]),\n ('public.review_media_delete_allowed(text)','a75dbd787dd388b0761bee11c82a56d5f410688e4cfd953181bce4d613062ae7',true,'v','plpgsql','boolean',ARRAY['p_path']::text[]),\n ('public.review_media_upload_allowed(text,text)','298c6a127f3a44a193b78be8c1f2577ab58aa34c772b66ccd53f774d807cc90c',true,'v','plpgsql','boolean',ARRAY['p_bucket','p_path']::text[]),\n ('review_media_private.canonical(text,uuid,uuid,text)','87d8397c36cf4c7b986f75cbc2788979333a76be2cc26dc5522cd362f62414a9',false,'i','sql','boolean',ARRAY['p_path','p_owner','p_review','p_purpose']::text[]),\n ('review_media_private.enqueue_removed()','7edb21d70ea3ff81ab90e13617e8c1c2749b54b1506f1ff42aaa6c5dc33f9e38',true,'v','plpgsql','trigger',ARRAY[]::text[]),\n ('review_media_private.guard_references()','846030547ffd76e17596755d7eee4279dcb9b563361435260826071d2f250d59',true,'v','plpgsql','trigger',ARRAY[]::text[]),\n ('review_media_private.lock_changes()','381d89aa6bbf5a59c2ee0fef2a7392ef705147252b324527201b8b6f74376250',true,'v','plpgsql','trigger',ARRAY[]::text[]),\n ('review_media_private.owned_legacy(text,uuid,text)','a2d55f09f03fcaeedd77461198511673da7342a6a4873d37227f9cf612090980',false,'i','sql','boolean',ARRAY['p_path','p_owner','p_purpose']::text[]),\n ('review_media_private.reference_key(text)','a8910cb09bfd959951045e8b0b8233b1e2a8bdc45073abeebdfc26b552b1dae7',false,'i','plpgsql','text',ARRAY['p_value']::text[]),\n ('review_media_private.referenced(text)','cc295115d85a3a11b380e1378e2ee91b0d9f7f587c68839653f54ed826c398ce',true,'v','sql','boolean',ARRAY['p_path']::text[])\n ) AS expected(signature,body_sha256,definer,volatility,language,result,argument_names) LOOP\n SELECT * INTO v_function FROM pg_proc WHERE oid=to_regprocedure(v_expected.signature);\n IF v_function.oid IS NULL OR v_function.proowner<>v_owner OR v_function.prokind<>'f'\n OR v_function.prosecdef IS DISTINCT FROM v_expected.definer OR v_function.provolatile::text<>v_expected.volatility\n OR v_function.proconfig IS DISTINCT FROM ARRAY['search_path=\"\"']::text[]\n OR (SELECT lanname FROM pg_language WHERE oid=v_function.prolang)<>v_expected.language\n OR pg_get_function_result(v_function.oid) IS DISTINCT FROM v_expected.result\n OR coalesce(v_function.proargnames[1:v_function.pronargs],ARRAY[]::text[]) IS DISTINCT FROM v_expected.argument_names\n OR v_function.pronargdefaults<>(CASE WHEN v_function.proname='mutate_review_with_media' THEN 3 ELSE 0 END)\n OR (v_function.pronargdefaults=3 AND pg_get_expr(v_function.proargdefaults,0)<>'NULL::text, NULL::text[], NULL::text[]')\n OR encode(sha256(convert_to(v_function.prosrc,'UTF8')),'hex')<>v_expected.body_sha256\n OR (SELECT count(*) FROM pg_proc x WHERE x.pronamespace=v_function.pronamespace AND x.proname=v_function.proname)<>1\n OR NOT has_function_privilege(v_owner,v_function.oid,'EXECUTE')\n OR has_function_privilege('anon',v_function.oid,'EXECUTE')\n OR has_function_privilege('service_role',v_function.oid,'EXECUTE')\n OR has_function_privilege('authenticated',v_function.oid,'EXECUTE') IS DISTINCT FROM (v_expected.signature LIKE 'public.%')\n OR EXISTS (SELECT 1 FROM aclexplode(coalesce(v_function.proacl,acldefault('f',v_function.proowner))) a\n WHERE a.is_grantable OR a.privilege_type<>'EXECUTE'\n OR (a.grantee<>v_owner AND NOT (v_expected.signature LIKE 'public.%' AND a.grantee='authenticated'::regrole))) THEN\n RAISE EXCEPTION 'review_media_catalog_function_drift';\n END IF;\n END LOOP;\n IF (SELECT count(*) FROM pg_proc WHERE pronamespace='review_media_private'::regnamespace)<>7\n OR (SELECT count(*) FROM pg_class WHERE relnamespace='review_media_private'::regnamespace AND relkind IN ('r','p','v','m','f','S'))<>2\n OR NOT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname='review_media_private' AND nspowner='postgres'::regrole)\n OR NOT has_schema_privilege(v_owner,'review_media_private','USAGE')\n OR has_schema_privilege(v_owner,'review_media_private','CREATE')\n OR EXISTS (SELECT 1 FROM pg_namespace n CROSS JOIN LATERAL aclexplode(coalesce(n.nspacl,acldefault('n',n.nspowner))) a\n WHERE n.nspname='review_media_private' AND a.grantee<>n.nspowner\n AND (a.grantee<>v_owner OR a.privilege_type<>'USAGE' OR a.is_grantable)) THEN\n RAISE EXCEPTION 'review_media_catalog_private_schema_drift';\n END IF;\n FOR v_expected IN SELECT * FROM (VALUES\n ('commits',ARRAY['INSERT','SELECT']::text[]),\n ('cleanup',ARRAY['INSERT','SELECT','UPDATE']::text[])\n ) AS expected(name,privileges) LOOP\n SELECT * INTO v_relation FROM pg_class WHERE oid=to_regclass('review_media_private.'||v_expected.name);\n IF v_relation.relkind<>'r' OR v_relation.relowner<>'postgres'::regrole OR NOT v_relation.relrowsecurity OR NOT v_relation.relforcerowsecurity\n OR (SELECT array_agg(a.privilege_type ORDER BY a.privilege_type) FROM aclexplode(v_relation.relacl) a\n WHERE a.grantee=v_owner) IS DISTINCT FROM v_expected.privileges\n OR EXISTS (SELECT 1 FROM aclexplode(v_relation.relacl) a WHERE a.grantee<>v_relation.relowner AND (a.grantee<>v_owner OR a.is_grantable))\n OR EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid=v_relation.oid AND attacl IS NOT NULL) THEN\n RAISE EXCEPTION 'review_media_catalog_private_table_drift';\n END IF;\n END LOOP;\n IF EXISTS (\n WITH expected(relation, name, cmd, using_expr, check_expr) AS (VALUES\n ('commits','review_media_commits_select','r','true'::text,NULL::text),\n ('commits','review_media_commits_insert','a',NULL::text,'true'),\n ('cleanup','review_media_cleanup_select','r','true',NULL::text),\n ('cleanup','review_media_cleanup_insert','a',NULL::text,'true'),\n ('cleanup','review_media_cleanup_update','w','true','true')\n ), actual AS (\n SELECT c.relname::text,p.polname::text,p.polcmd::text,pg_get_expr(p.polqual,p.polrelid),pg_get_expr(p.polwithcheck,p.polrelid),p.polpermissive,p.polroles\n FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid WHERE c.relnamespace='review_media_private'::regnamespace\n ), wanted AS (SELECT *,true,ARRAY[v_owner] FROM expected)\n (SELECT * FROM wanted EXCEPT SELECT * FROM actual) UNION ALL (SELECT * FROM actual EXCEPT SELECT * FROM wanted)\n ) THEN RAISE EXCEPTION 'review_media_catalog_private_policy_drift'; END IF;\n IF EXISTS (\n SELECT 1 FROM (VALUES ('anon'),('authenticated'),('service_role')) r(name)\n WHERE has_schema_privilege(r.name,'review_media_private','USAGE,CREATE')\n OR has_table_privilege(r.name,'review_media_private.commits','SELECT,INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER')\n OR has_table_privilege(r.name,'review_media_private.cleanup','SELECT,INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER')\n ) OR NOT has_table_privilege(v_owner,'public.reviews','SELECT')\n OR NOT has_table_privilege(v_owner,'public.reviews','DELETE')\n OR EXISTS (SELECT 1 FROM unnest(ARRAY['content','categories','food_photos','is_verified','admin_note','updated_at']) c(name)\n WHERE NOT has_column_privilege(v_owner,'public.reviews',c.name,'UPDATE'))\n OR has_table_privilege(v_owner,'public.reviews','INSERT,UPDATE,TRUNCATE,REFERENCES,TRIGGER')\n OR EXISTS (SELECT 1 FROM pg_attribute a WHERE a.attrelid='public.reviews'::regclass AND a.attnum>0 AND NOT a.attisdropped\n AND a.attname<>ALL(ARRAY['content','categories','food_photos','is_verified','admin_note','updated_at'])\n AND has_column_privilege(v_owner,a.attrelid,a.attnum,'UPDATE'))\n OR NOT has_table_privilege(v_owner,'storage.objects','SELECT')\n OR has_table_privilege(v_owner,'storage.objects','INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER') THEN\n RAISE EXCEPTION 'review_media_catalog_minimum_grant_drift';\n END IF;\n IF EXISTS (\n WITH expected(relation,ordinal,name,type,not_null,default_expr) AS (VALUES\n ('commits',1,'operation_id','uuid',true,NULL::text),\n ('commits',2,'owner_id','uuid',true,NULL::text),\n ('commits',3,'review_id','uuid',true,NULL::text),\n ('commits',4,'kind','text',true,NULL::text),\n ('commits',5,'request_hash','text',true,NULL::text),\n ('cleanup',1,'path','text',true,NULL::text),\n ('cleanup',2,'owner_id','uuid',true,NULL::text),\n ('cleanup',3,'review_id','uuid',true,NULL::text),\n ('cleanup',4,'purpose','text',true,NULL::text),\n ('cleanup',5,'retired','boolean',true,'false'),\n ('cleanup',6,'complete','boolean',true,'false')\n ), actual AS (\n SELECT c.relname::text,a.attnum::integer,a.attname::text,pg_catalog.format_type(a.atttypid,a.atttypmod),a.attnotnull,\n pg_catalog.pg_get_expr(d.adbin,d.adrelid)\n FROM pg_catalog.pg_class c JOIN pg_catalog.pg_attribute a ON a.attrelid=c.oid AND a.attnum>0 AND NOT a.attisdropped\n LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid=a.attrelid AND d.adnum=a.attnum\n WHERE c.relnamespace='review_media_private'::regnamespace AND c.relkind IN ('r','p')\n )\n (SELECT * FROM expected EXCEPT SELECT * FROM actual) UNION ALL (SELECT * FROM actual EXCEPT SELECT * FROM expected)\n ) THEN RAISE EXCEPTION 'review_media_catalog_column_shape_drift'; END IF;\n IF EXISTS (\n WITH expected(relation,name,definition) AS (VALUES\n ('commits','commits_pkey','PRIMARY KEY (operation_id)'),\n ('commits','commits_kind_check','CHECK ((kind = ANY (ARRAY[''edit''::text, ''delete''::text])))'),\n ('cleanup','cleanup_pkey','PRIMARY KEY (path)'),\n ('cleanup','cleanup_purpose_check','CHECK ((purpose = ANY (ARRAY[''food''::text, ''verification''::text])))')\n ), actual AS (\n SELECT c.relname::text,k.conname::text,pg_catalog.pg_get_constraintdef(k.oid)\n FROM pg_catalog.pg_class c JOIN pg_catalog.pg_constraint k ON k.conrelid=c.oid\n WHERE c.relnamespace='review_media_private'::regnamespace\n )\n (SELECT * FROM expected EXCEPT SELECT * FROM actual) UNION ALL (SELECT * FROM actual EXCEPT SELECT * FROM expected)\n ) OR EXISTS (SELECT 1 FROM pg_catalog.pg_trigger t JOIN pg_catalog.pg_class c ON c.oid=t.tgrelid\n WHERE c.relnamespace='review_media_private'::regnamespace AND NOT t.tgisinternal)\n OR EXISTS (SELECT 1 FROM pg_catalog.pg_constraint k JOIN pg_catalog.pg_class c ON c.oid=k.conrelid\n WHERE c.relnamespace='review_media_private'::regnamespace AND (NOT k.convalidated OR k.condeferrable OR k.condeferred))\n OR (SELECT count(*) FROM pg_catalog.pg_index i JOIN pg_catalog.pg_class c ON c.oid=i.indrelid\n WHERE c.relnamespace='review_media_private'::regnamespace)<>2\n OR EXISTS (SELECT 1 FROM pg_catalog.pg_index i JOIN pg_catalog.pg_class c ON c.oid=i.indrelid\n WHERE c.relnamespace='review_media_private'::regnamespace AND (NOT i.indisprimary OR NOT i.indisvalid OR NOT i.indisready)) THEN\n RAISE EXCEPTION 'review_media_catalog_constraint_shape_drift';\n END IF;\n IF (SELECT pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(prosrc,'UTF8')),'hex') FROM pg_catalog.pg_proc\n WHERE pronamespace=(SELECT oid FROM pg_namespace WHERE nspname='privacy_retention')\n AND proname='g014_catalog_protected_relations' AND pronargs=0) IS DISTINCT FROM '20acd5742d891cf8a33b265a3a1298c65dd9646402c58391cd17829951cf92f2' THEN\n RAISE EXCEPTION 'review_media_catalog_known_identity_post_drift';\n END IF;\n -- Exact allowlist and unchanged G014 assertions run through the owner bridge\n -- above; canonical readback also runs them before this catalog-only block.\n -- REVIEW_MEDIA_TRIGGER_READBACK_BEGIN\n IF EXISTS (\n WITH expected(name,definition) AS (VALUES\n ('review_media_enqueue_removed','CREATE TRIGGER review_media_enqueue_removed AFTER DELETE OR UPDATE OF food_photos, verification_photo, user_id, id ON public.reviews FOR EACH ROW EXECUTE FUNCTION review_media_private.enqueue_removed()'),\n ('review_media_reference_guard','CREATE TRIGGER review_media_reference_guard BEFORE INSERT OR UPDATE OF food_photos, verification_photo, user_id, id ON public.reviews FOR EACH ROW EXECUTE FUNCTION review_media_private.guard_references()'),\n ('review_media_serialize','CREATE TRIGGER review_media_serialize BEFORE INSERT OR DELETE OR UPDATE OF food_photos, verification_photo, user_id, id ON public.reviews FOR EACH STATEMENT EXECUTE FUNCTION review_media_private.lock_changes()')\n ), actual AS (\n SELECT tgname::text,pg_get_triggerdef(oid),tgenabled::text FROM pg_trigger\n WHERE tgrelid='public.reviews'::regclass AND tgname LIKE 'review_media_%' AND NOT tgisinternal\n ), wanted AS (SELECT name,definition,'O'::text FROM expected)\n (SELECT * FROM wanted EXCEPT SELECT * FROM actual) UNION ALL (SELECT * FROM actual EXCEPT SELECT * FROM wanted)\n ) THEN RAISE EXCEPTION 'review_media_catalog_trigger_drift'; END IF;\n -- REVIEW_MEDIA_TRIGGER_READBACK_END\n -- Compare command, role, permissive/restrictive bit and BOTH complete predicates.\n IF EXISTS (\n WITH expected(name,cmd,permissive,role_name,using_expr,check_expr) AS (VALUES\n ('review_media_safe_delete','d',false,'authenticated','((bucket_id <> ''review-photos''::text) OR review_media_delete_allowed(name))',NULL::text),\n ('review_media_safe_insert','a',false,'authenticated',NULL::text,'((bucket_id <> ALL (ARRAY[''review-photos''::text, ''review-verifications''::text])) OR review_media_upload_allowed(bucket_id, name))'),\n ('review_media_safe_update','w',false,'authenticated','((bucket_id <> ALL (ARRAY[''review-photos''::text, ''review-verifications''::text])) OR review_media_upload_allowed(bucket_id, name))','((bucket_id <> ALL (ARRAY[''review-photos''::text, ''review-verifications''::text])) OR review_media_upload_allowed(bucket_id, name))'),\n ('review_media_workflow_read','r',true,'privacy_workflow_owner','(bucket_id = ANY (ARRAY[''review-photos''::text, ''review-verifications''::text]))',NULL::text),\n ('review_photos_food_insert','a',false,'authenticated',NULL::text,'((bucket_id <> ''review-photos''::text) OR ((split_part(name, ''/''::text, 4) = ''food''::text) AND ((storage.foldername(name))[1] = ( SELECT (auth.uid())::text AS uid))))'),\n ('review_photos_food_update','w',false,'authenticated','((bucket_id <> ''review-photos''::text) OR ((split_part(name, ''/''::text, 4) = ''food''::text) AND ((storage.foldername(name))[1] = ( SELECT (auth.uid())::text AS uid))))','((bucket_id <> ''review-photos''::text) OR ((split_part(name, ''/''::text, 4) = ''food''::text) AND ((storage.foldername(name))[1] = ( SELECT (auth.uid())::text AS uid))))'),\n ('review_verifications_owner_delete','d',true,'authenticated','((bucket_id = ''review-verifications''::text) AND ((storage.foldername(name))[1] = ( SELECT (auth.uid())::text AS uid)))',NULL::text),\n ('review_verifications_owner_insert','a',true,'authenticated',NULL::text,'((bucket_id = ''review-verifications''::text) AND ((storage.foldername(name))[1] = ( SELECT (auth.uid())::text AS uid)) AND (split_part(name, ''/''::text, 4) = ''verification''::text))'),\n ('review_verifications_owner_read','r',true,'authenticated','((bucket_id = ''review-verifications''::text) AND (((storage.foldername(name))[1] = ( SELECT (auth.uid())::text AS uid)) OR has_role(( SELECT auth.uid() AS uid), ''admin''::app_role)))',NULL::text),\n ('review_verifications_safe_delete','d',false,'authenticated','((bucket_id <> ''review-verifications''::text) OR review_media_delete_allowed(name))',NULL::text)\n ), actual AS (\n SELECT p.polname::text,p.polcmd::text,p.polpermissive,p.polroles,\n regexp_replace(pg_get_expr(p.polqual,p.polrelid),'[[:space:]]+',' ','g'),\n regexp_replace(pg_get_expr(p.polwithcheck,p.polrelid),'[[:space:]]+',' ','g')\n FROM pg_policy p WHERE p.polrelid='storage.objects'::regclass\n AND (p.polname LIKE 'review_media_%' OR p.polname LIKE 'review_photos_%' OR p.polname LIKE 'review_verifications_%')\n ), wanted AS (SELECT name,cmd,permissive,ARRAY[to_regrole(role_name)::oid],using_expr,check_expr FROM expected)\n (SELECT * FROM wanted EXCEPT SELECT * FROM actual) UNION ALL (SELECT * FROM actual EXCEPT SELECT * FROM wanted)\n ) THEN RAISE EXCEPTION 'review_media_catalog_storage_policy_drift'; END IF;\n IF NOT EXISTS (SELECT 1 FROM storage.buckets WHERE id='review-verifications' AND name='review-verifications'\n AND public=false AND file_size_limit=5242880\n AND allowed_mime_types=ARRAY['image/jpeg','image/png','image/webp','image/avif']::text[]) THEN\n RAISE EXCEPTION 'review_media_catalog_private_bucket_drift';\n END IF;\n -- Admit the strict canonical local read policy or the three exact deployed\n -- bucket read identities. All existing API-role reads must match a known\n -- predicate; the private verification bucket is never publicly readable.\n IF EXISTS (SELECT 1 FROM pg_policy p WHERE p.polrelid='storage.objects'::regclass\n AND p.polcmd='r' AND p.polpermissive AND p.polroles && ARRAY[0::oid,'anon'::regrole::oid,'authenticated'::regrole::oid]\n AND p.polname NOT IN ('tzudong_public_media_read','Anyone can view review photos','Public read access',\n 'Public read access for profile-avatars','local_nightly_avatar_read','review_verifications_owner_read'))\n OR EXISTS (SELECT 1 FROM pg_policy p JOIN (VALUES\n ('Anyone can view review photos','(bucket_id = ''review-photos''::text)'),\n ('Public read access','(bucket_id = ''ad-banner-images''::text)'),\n ('Public read access for profile-avatars','(bucket_id = ''profile-avatars''::text)')\n ) e(name,predicate) ON p.polname=e.name\n WHERE p.polrelid='storage.objects'::regclass AND (p.polcmd<>'r' OR NOT p.polpermissive\n OR p.polroles<>ARRAY[0::oid] OR p.polwithcheck IS NOT NULL OR pg_get_expr(p.polqual,p.polrelid)<>e.predicate))\n OR EXISTS (SELECT 1 FROM pg_policy p WHERE p.polrelid='storage.objects'::regclass\n AND p.polname='tzudong_public_media_read' AND (p.polcmd<>'r' OR NOT p.polpermissive\n OR NOT p.polroles @> ARRAY['anon'::regrole::oid,'authenticated'::regrole::oid] OR cardinality(p.polroles)<>2\n OR p.polwithcheck IS NOT NULL OR pg_get_expr(p.polqual,p.polrelid)<>'(bucket_id = ANY (ARRAY[''profile-avatars''::text, ''review-photos''::text, ''ad-banner-images''::text]))'))\n OR EXISTS (SELECT 1 FROM pg_policy p WHERE p.polrelid='storage.objects'::regclass AND p.polname='local_nightly_avatar_read'\n AND (p.polcmd<>'r' OR NOT p.polpermissive OR NOT p.polroles @> ARRAY['anon'::regrole::oid,'authenticated'::regrole::oid]\n OR cardinality(p.polroles)<>2 OR p.polwithcheck IS NOT NULL OR pg_get_expr(p.polqual,p.polrelid)<>'(bucket_id = ''avatars''::text)')) THEN\n RAISE EXCEPTION 'review_media_catalog_public_read_drift';\n END IF;\n IF NOT EXISTS (SELECT 1 FROM pg_policy WHERE polrelid='storage.objects'::regclass\n AND polname='tzudong_public_media_read' AND polcmd='r' AND polpermissive\n AND polroles @> ARRAY['anon'::regrole::oid,'authenticated'::regrole::oid]\n AND cardinality(polroles)=2 AND polwithcheck IS NULL\n AND pg_get_expr(polqual,polrelid)='(bucket_id = ANY (ARRAY[''profile-avatars''::text, ''review-photos''::text, ''ad-banner-images''::text]))')\n AND (SELECT count(*) FROM pg_policy WHERE polrelid='storage.objects'::regclass\n AND polname IN ('Anyone can view review photos','Public read access','Public read access for profile-avatars'))<>3 THEN\n RAISE EXCEPTION 'review_media_catalog_public_read_drift';\n END IF;\n -- The global reference scan must see all reviews under the trusted role.\n IF NOT EXISTS (SELECT 1 FROM pg_policy WHERE polrelid='public.reviews'::regclass\n AND polname='g014_account_deletion_source_access' AND polcmd='*' AND polpermissive\n AND polroles=ARRAY[v_owner] AND pg_get_expr(polqual,polrelid)='true' AND pg_get_expr(polwithcheck,polrelid)='true') THEN\n RAISE EXCEPTION 'review_media_catalog_reference_visibility_drift';\n END IF;\n PERFORM set_config('search_path',v_path,true);\nEND $review_media_catalog_readback$", + "-- REVIEW_MEDIA_READBACK_END\nNOTIFY pgrst, 'reload schema'", + "COMMIT" + ] + }, + { + "id": "admin_review_media_completion", + "version": "20261011072824", + "name": "admin_review_media_completion", + "path": "backend/supabase/migrations/20261011072824_admin_review_media_completion.sql", + "bytes": 13429, + "sha256": "32626259ca77ea913f068ee804815ff439abbff1ce310da4525e4f0bb86d8338", + "statementCount": 7, + "statementVectorSha256": "3a47d7e182bfd764ae70401e4e8462b08eaa5576baf4d051503e80534fc8fb8e", + "originalStatementVector": [ + "-- Forward-only correction. Immutable applied SQL and G014 assertions stay intact.\n-- Relation locks precede the media global lock; global precedes object locks.\n-- Admin locks reviews before restaurants, matching review-trigger counter work.\nBEGIN", + "SET LOCAL lock_timeout='2s'", + "SELECT pg_advisory_xact_lock(hashtextextended('tzudong:review-media-catalog:v2',0))", + "DO $forward$\nDECLARE target oid; definition text; source text; patched text; metadata jsonb; defaults text;\n before_members jsonb; before_assertions jsonb; before_manifest jsonb; self_membership jsonb;\n workflow_owner oid:='privacy_workflow_owner'::regrole; runner oid:='postgres'::regrole;\n major integer:=current_setting('server_version_num')::integer/10000; pg15_lease boolean:=false;\nBEGIN\n IF current_user<>'postgres' OR session_user<>'postgres' OR major NOT IN (15,17)\n OR current_setting('transaction_read_only')<>'off' OR NOT EXISTS(SELECT 1 FROM pg_roles\n WHERE oid=workflow_owner AND NOT rolsuper AND NOT rolbypassrls AND NOT rolcanlogin AND NOT rolinherit)\n OR to_regprocedure('public.admin_review_verification_cleanup(uuid,text,text[])') IS NOT NULL\n THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_EXECUTOR_DENIED'; END IF;\n SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') INTO before_members FROM pg_auth_members m;\n SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) INTO before_assertions FROM pg_proc p WHERE pronamespace='privacy_retention'::regnamespace;\n\n target:='public.admin_record_action(uuid,text,uuid,text,uuid[],jsonb,text)'::regprocedure;\n SELECT pg_get_functiondef(p.oid),p.prosrc,to_jsonb(p)-ARRAY['prosrc','proargdefaults'],pg_get_expr(p.proargdefaults,0)\n INTO definition,source,metadata,defaults FROM pg_proc p WHERE p.oid=target;\n IF encode(sha256(convert_to(source,'UTF8')),'hex')<>'a18fad1f748d736371a9ab549a1ca483a5b2f321674fabe71b2411b7e58b3fc9' THEN RAISE EXCEPTION 'REVIEW_MEDIA_LOCK_SOURCE_DRIFT'; END IF;\n patched:=source;\n IF (length(patched)-length(replace(patched,$old$LOCK TABLE public.restaurants,public.restaurant_submissions,public.restaurant_submission_items,public.reviews,public.restaurant_requests IN SHARE ROW EXCLUSIVE MODE;$old$,'')))/length($old$LOCK TABLE public.restaurants,public.restaurant_submissions,public.restaurant_submission_items,public.reviews,public.restaurant_requests IN SHARE ROW EXCLUSIVE MODE;$old$)<>1 THEN RAISE EXCEPTION 'REVIEW_MEDIA_LOCK_ANCHOR_DRIFT'; END IF;\n patched:=replace(patched,$old$LOCK TABLE public.restaurants,public.restaurant_submissions,public.restaurant_submission_items,public.reviews,public.restaurant_requests IN SHARE ROW EXCLUSIVE MODE;$old$,$new$LOCK TABLE public.reviews,public.restaurants,public.restaurant_submissions,public.restaurant_submission_items,public.restaurant_requests IN SHARE ROW EXCLUSIVE MODE;$new$);\n IF (length(patched)-length(replace(patched,$old$IF p_phase LIKE 'cleanup_%' THEN$old$,'')))/length($old$IF p_phase LIKE 'cleanup_%' THEN$old$)<>1 THEN RAISE EXCEPTION 'REVIEW_MEDIA_LOCK_ANCHOR_DRIFT'; END IF;\n patched:=replace(patched,$old$IF p_phase LIKE 'cleanup_%' THEN$old$,$new$IF p_phase LIKE 'cleanup_%' THEN\n PERFORM pg_advisory_xact_lock(741093, 1);$new$);\n IF (length(patched)-length(replace(patched,$old$IF p_action='review.delete' THEN\n FOR media$old$,'')))/length($old$IF p_action='review.delete' THEN\n FOR media$old$)<>1 THEN RAISE EXCEPTION 'REVIEW_MEDIA_LOCK_ANCHOR_DRIFT'; END IF;\n patched:=replace(patched,$old$IF p_action='review.delete' THEN\n FOR media$old$,$new$IF p_action='review.delete' THEN\n PERFORM pg_advisory_xact_lock(741093, 1);\n FOR media$new$);\n EXECUTE replace(definition,source,patched);\n IF (SELECT to_jsonb(p)-ARRAY['prosrc','proargdefaults'] FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM metadata\n OR (SELECT pg_get_expr(p.proargdefaults,0) FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM defaults\n OR (SELECT encode(sha256(convert_to(p.prosrc,'UTF8')),'hex') FROM pg_proc p WHERE p.oid=target)<>'2353de06b4d93f45fd603380e678f4edbcc0b16a8bd9860b5967683bea5f1157'\n THEN RAISE EXCEPTION 'REVIEW_MEDIA_LOCK_POST_DRIFT'; END IF;\n\n SELECT to_jsonb(m) INTO self_membership FROM pg_auth_members m WHERE roleid=workflow_owner AND member=runner AND grantor=runner;\n IF self_membership IS NOT NULL AND (self_membership->>'admin_option')::boolean THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_MEMBERSHIP_DENIED'; END IF;\n IF major=17 THEN\n IF NOT EXISTS(SELECT 1 FROM pg_auth_members WHERE roleid=workflow_owner AND member=runner AND admin_option AND grantor<>runner)\n THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_OWNER_AUTHORITY_MISSING'; END IF;\n EXECUTE 'GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT FALSE,SET TRUE GRANTED BY postgres';\n ELSE\n pg15_lease:=NOT pg_has_role(runner,workflow_owner,'MEMBER');\n IF pg15_lease THEN GRANT privacy_workflow_owner TO postgres; END IF;\n END IF;\n SET LOCAL ROLE privacy_workflow_owner;\n SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) INTO before_manifest FROM privacy_retention.g014_catalog_contract_manifest m;\n\n target:='public.mutate_review_with_media(uuid,uuid,text,timestamp with time zone,text,text[],text[])'::regprocedure;\n SELECT pg_get_functiondef(p.oid),p.prosrc,to_jsonb(p)-ARRAY['prosrc','proargdefaults'],pg_get_expr(p.proargdefaults,0)\n INTO definition,source,metadata,defaults FROM pg_proc p WHERE p.oid=target;\n IF encode(sha256(convert_to(source,'UTF8')),'hex')<>'50bfad7bc4fc7ddbc9d9379e8eb13facd938a15f66553576cc69fbcf69aa98b1' THEN RAISE EXCEPTION 'REVIEW_MEDIA_LOCK_SOURCE_DRIFT'; END IF;\n patched:=source;\n IF (length(patched)-length(replace(patched,$old$PERFORM pg_advisory_xact_lock(741093, 1);$old$,'')))/length($old$PERFORM pg_advisory_xact_lock(741093, 1);$old$)<>1 THEN RAISE EXCEPTION 'REVIEW_MEDIA_LOCK_ANCHOR_DRIFT'; END IF;\n patched:=replace(patched,$old$PERFORM pg_advisory_xact_lock(741093, 1);$old$,$new$LOCK TABLE public.reviews IN ROW EXCLUSIVE MODE;\n PERFORM pg_advisory_xact_lock(741093, 1);$new$);\n EXECUTE replace(definition,source,patched);\n IF (SELECT to_jsonb(p)-ARRAY['prosrc','proargdefaults'] FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM metadata\n OR (SELECT pg_get_expr(p.proargdefaults,0) FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM defaults\n OR (SELECT encode(sha256(convert_to(p.prosrc,'UTF8')),'hex') FROM pg_proc p WHERE p.oid=target)<>'d5d6646a4294c0e7bd0cd75ea47a996c9c57bac6c32a39ac53fbb7a8b1e7e323'\n THEN RAISE EXCEPTION 'REVIEW_MEDIA_LOCK_POST_DRIFT'; END IF;\n\n EXECUTE $rpc$CREATE FUNCTION public.admin_review_verification_cleanup(p_review_id uuid,p_phase text,p_paths text[] DEFAULT NULL)\n RETURNS jsonb LANGUAGE plpgsql SECURITY DEFINER SET search_path='' AS $cleanup$\nDECLARE items jsonb;\nBEGIN\n IF p_review_id IS NULL OR p_phase IS NULL OR p_phase NOT IN ('prepare','finish')\n OR current_setting('transaction_isolation') <> 'read committed'\n OR (p_phase = 'prepare' AND p_paths IS NOT NULL)\n OR (p_phase = 'finish' AND (p_paths IS NULL OR cardinality(p_paths) > 20\n OR EXISTS (SELECT 1 FROM unnest(p_paths) x WHERE x IS NULL OR octet_length(x) > 8192)\n OR cardinality(p_paths) <> (SELECT count(DISTINCT x) FROM unnest(p_paths) x))) THEN\n RAISE EXCEPTION 'REVIEW_ADMIN_CLEANUP_INVALID';\n END IF;\n PERFORM pg_advisory_xact_lock(741093, 1);\n IF p_phase = 'prepare' THEN\n UPDATE review_media_private.cleanup c SET retired = true\n WHERE c.review_id = p_review_id AND c.purpose = 'verification'\n AND NOT c.retired AND NOT c.complete AND NOT review_media_private.referenced(c.path);\n SELECT coalesce(jsonb_agg(to_jsonb(x) ORDER BY x.path),'[]'::jsonb) INTO items\n FROM (SELECT c.path,c.owner_id,c.review_id,c.purpose FROM review_media_private.cleanup c\n WHERE c.review_id = p_review_id AND c.purpose = 'verification' AND c.retired AND NOT c.complete\n AND NOT review_media_private.referenced(c.path)\n AND (review_media_private.canonical(c.path,c.owner_id,c.review_id,c.purpose)\n OR review_media_private.owned_legacy(c.path,c.owner_id,c.purpose))\n ORDER BY c.path LIMIT 20) x;\n RETURN jsonb_build_object('status','READY','items',items);\n END IF;\n UPDATE review_media_private.cleanup c SET complete = true\n WHERE c.review_id = p_review_id AND c.purpose = 'verification' AND c.retired AND NOT c.complete\n AND c.path = ANY(p_paths)\n AND (review_media_private.canonical(c.path,c.owner_id,c.review_id,c.purpose)\n OR review_media_private.owned_legacy(c.path,c.owner_id,c.purpose))\n AND NOT review_media_private.referenced(c.path)\n AND NOT EXISTS (SELECT 1 FROM storage.objects o\n WHERE o.bucket_id IN ('review-photos','review-verifications') AND o.name = c.path);\n RETURN jsonb_build_object('remaining',(SELECT count(*) FROM review_media_private.cleanup c\n WHERE c.review_id = p_review_id AND c.purpose = 'verification' AND NOT c.complete));\nEND\n$cleanup$;$rpc$;\n REVOKE ALL ON FUNCTION public.admin_review_verification_cleanup(uuid,text,text[]) FROM PUBLIC,anon,authenticated,service_role;\n GRANT EXECUTE ON FUNCTION public.admin_review_verification_cleanup(uuid,text,text[]) TO service_role;\n INSERT INTO privacy_retention.g014_public_rpc_allowlist(function_schema,function_name,identity_arguments,grantee,source_signature)\n SELECT 'public',p.proname,p.proargtypes::text,'service_role','public.admin_review_verification_cleanup(uuid,text,text[])'\n FROM pg_proc p WHERE p.oid='public.admin_review_verification_cleanup(uuid,text,text[])'::regprocedure;\n IF to_regprocedure('pg_temp.admin_media_forward_assert()') IS NOT NULL THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_BRIDGE_EXISTS'; END IF;\n CREATE FUNCTION pg_temp.admin_media_forward_assert() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path='' AS $bridge$\n BEGIN\n PERFORM privacy_retention.assert_g014_public_rpc_allowlist();\n PERFORM privacy_retention.assert_g014_definer_contract();\n PERFORM privacy_retention.assert_g014_catalog_contract();\n DROP FUNCTION pg_temp.admin_media_forward_assert();\n END $bridge$;\n REVOKE ALL ON FUNCTION pg_temp.admin_media_forward_assert() FROM PUBLIC,anon,authenticated,service_role;\n GRANT EXECUTE ON FUNCTION pg_temp.admin_media_forward_assert() TO postgres;\n IF (SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) FROM privacy_retention.g014_catalog_contract_manifest m) IS DISTINCT FROM before_manifest THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_MANIFEST_DRIFT'; END IF;\n RESET ROLE;\n IF major=15 THEN\n IF pg15_lease THEN REVOKE privacy_workflow_owner FROM postgres; END IF;\n ELSIF self_membership IS NULL THEN\n REVOKE privacy_workflow_owner FROM postgres GRANTED BY postgres;\n ELSE\n EXECUTE format('GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT %s,SET %s GRANTED BY postgres',\n upper(self_membership->>'inherit_option'),upper(self_membership->>'set_option'));\n END IF;\n IF (SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') FROM pg_auth_members m) IS DISTINCT FROM before_members\n OR (SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) FROM pg_proc p WHERE pronamespace='privacy_retention'::regnamespace) IS DISTINCT FROM before_assertions\n THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_PRESERVATION_DRIFT'; END IF;\n PERFORM pg_temp.admin_media_forward_assert();\n IF to_regprocedure('pg_temp.admin_media_forward_assert()') IS NOT NULL THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_BRIDGE_REMAINING'; END IF;\nEND $forward$", + "-- ADMIN_REVIEW_MEDIA_READBACK_BEGIN\nDO $readback$\nBEGIN\n IF NOT EXISTS(SELECT 1 FROM pg_proc p WHERE p.oid='public.admin_review_verification_cleanup(uuid,text,text[])'::regprocedure\n AND p.proowner='privacy_workflow_owner'::regrole AND p.prosecdef AND p.proconfig=ARRAY['search_path=\"\"']::text[]\n AND p.pronargs=3 AND p.pronargdefaults=1 AND p.prorettype='jsonb'::regtype AND NOT p.proretset\n AND p.provolatile='v' AND p.prokind='f' AND NOT p.proisstrict\n AND p.proargnames=ARRAY['p_review_id','p_phase','p_paths']::text[]\n AND pg_get_expr(p.proargdefaults,0)='NULL::text[]'\n AND p.prolang=(SELECT oid FROM pg_language WHERE lanname='plpgsql')\n AND NOT EXISTS(SELECT 1 FROM aclexplode(coalesce(p.proacl,acldefault('f',p.proowner))) a\n WHERE a.is_grantable OR a.privilege_type<>'EXECUTE'\n OR a.grantee NOT IN ('privacy_workflow_owner'::regrole,'service_role'::regrole))\n AND encode(sha256(convert_to(p.prosrc,'UTF8')),'hex')='f4d19bc5b41fe7808635240cae80f796d12f6cad8b13802577a88afbb1f024a7'\n AND NOT has_function_privilege('anon',p.oid,'EXECUTE') AND NOT has_function_privilege('authenticated',p.oid,'EXECUTE')\n AND has_function_privilege('service_role',p.oid,'EXECUTE'))\n OR (SELECT encode(sha256(convert_to(prosrc,'UTF8')),'hex') FROM pg_proc WHERE oid='public.admin_record_action(uuid,text,uuid,text,uuid[],jsonb,text)'::regprocedure)<>'2353de06b4d93f45fd603380e678f4edbcc0b16a8bd9860b5967683bea5f1157'\n OR (SELECT encode(sha256(convert_to(prosrc,'UTF8')),'hex') FROM pg_proc WHERE oid='public.mutate_review_with_media(uuid,uuid,text,timestamp with time zone,text,text[],text[])'::regprocedure)<>'d5d6646a4294c0e7bd0cd75ea47a996c9c57bac6c32a39ac53fbb7a8b1e7e323'\n THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_READBACK_DRIFT'; END IF;\nEND $readback$", + "-- ADMIN_REVIEW_MEDIA_READBACK_END\nNOTIFY pgrst,'reload schema'", + "COMMIT" + ] + }, + { + "id": "review_legacy_shared_cleanup", + "version": "20261011085342", + "name": "review_legacy_shared_cleanup", + "path": "backend/supabase/migrations/20261011085342_review_legacy_shared_cleanup.sql", + "bytes": 7661, + "sha256": "623236ddbd87059b94c84486f9e14822dba0190d2448656259991bd1345a434f", + "statementCount": 6, + "statementVectorSha256": "b7b7cc3636ba9496aea569ec18ddeeeb18dbd97e20228e3490a41dd37f6f3956", + "originalStatementVector": [ + "-- Unreferenced shared legacy receipts follow their last authoritative review.\n-- Applied SQL, grants, actor authorization and canonical key ownership stay intact.\nBEGIN", + "SET LOCAL lock_timeout='2s'", + "SELECT pg_advisory_xact_lock(hashtextextended('tzudong:review-media-catalog:v2',0))", + "DO $forward$\nDECLARE target oid:='review_media_private.enqueue_removed()'::regprocedure;\n definition text; source text; patched text; metadata jsonb;\n before_members jsonb; before_assertions jsonb; before_manifest jsonb; self_membership jsonb;\n before_schema jsonb; schema_lease boolean;\n workflow_owner oid:='privacy_workflow_owner'::regrole; runner oid:='postgres'::regrole;\n major integer:=current_setting('server_version_num')::integer/10000; pg15_lease boolean:=false;\nBEGIN\n IF current_user<>'postgres' OR session_user<>'postgres' OR major NOT IN (15,17)\n OR current_setting('transaction_read_only')<>'off' OR NOT EXISTS(SELECT 1 FROM pg_roles\n WHERE oid=workflow_owner AND NOT rolsuper AND NOT rolbypassrls AND NOT rolcanlogin AND NOT rolinherit)\n THEN RAISE EXCEPTION 'REVIEW_SHARED_EXECUTOR_DENIED'; END IF;\n SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') INTO before_members FROM pg_auth_members m;\n SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) INTO before_assertions FROM pg_proc p WHERE pronamespace='privacy_retention'::regnamespace;\n SELECT pg_get_functiondef(p.oid),p.prosrc,to_jsonb(p)-'prosrc' INTO definition,source,metadata FROM pg_proc p WHERE p.oid=target;\n IF metadata->>'proowner'<>workflow_owner::text OR encode(sha256(convert_to(source,'UTF8')),'hex')<>'7edb21d70ea3ff81ab90e13617e8c1c2749b54b1506f1ff42aaa6c5dc33f9e38'\n THEN RAISE EXCEPTION 'REVIEW_SHARED_SOURCE_DRIFT'; END IF;\n SELECT to_jsonb(n) INTO before_schema FROM pg_namespace n WHERE n.oid='review_media_private'::regnamespace;\n IF before_schema->>'nspowner'<>runner::text THEN RAISE EXCEPTION 'REVIEW_SHARED_SCHEMA_OWNER_DRIFT'; END IF;\n schema_lease:=NOT has_schema_privilege(workflow_owner,'review_media_private','CREATE');\n IF schema_lease THEN GRANT CREATE ON SCHEMA review_media_private TO privacy_workflow_owner; END IF;\n SELECT to_jsonb(m) INTO self_membership FROM pg_auth_members m WHERE roleid=workflow_owner AND member=runner AND grantor=runner;\n IF self_membership IS NOT NULL AND (self_membership->>'admin_option')::boolean THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_MEMBERSHIP_DENIED'; END IF;\n IF major=17 THEN\n IF NOT EXISTS(SELECT 1 FROM pg_auth_members WHERE roleid=workflow_owner AND member=runner AND admin_option AND grantor<>runner)\n THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_OWNER_AUTHORITY_MISSING'; END IF;\n EXECUTE 'GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT FALSE,SET TRUE GRANTED BY postgres';\n ELSE\n pg15_lease:=NOT pg_has_role(runner,workflow_owner,'MEMBER');\n IF pg15_lease THEN GRANT privacy_workflow_owner TO postgres; END IF;\n END IF;\n SET LOCAL ROLE privacy_workflow_owner;\n SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) INTO before_manifest FROM privacy_retention.g014_catalog_contract_manifest m;\n\n IF (length(source)-length(replace(source,$old$INSERT INTO review_media_private.cleanup(path, owner_id, review_id, purpose, retired)\n VALUES (p, OLD.user_id, OLD.id, purpose, NOT review_media_private.referenced(p))\n ON CONFLICT DO NOTHING;$old$,'')))/length($old$INSERT INTO review_media_private.cleanup(path, owner_id, review_id, purpose, retired)\n VALUES (p, OLD.user_id, OLD.id, purpose, NOT review_media_private.referenced(p))\n ON CONFLICT DO NOTHING;$old$)<>1\n THEN RAISE EXCEPTION 'REVIEW_SHARED_ANCHOR_DRIFT'; END IF;\n patched:=replace(source,$old$INSERT INTO review_media_private.cleanup(path, owner_id, review_id, purpose, retired)\n VALUES (p, OLD.user_id, OLD.id, purpose, NOT review_media_private.referenced(p))\n ON CONFLICT DO NOTHING;$old$,$new$INSERT INTO review_media_private.cleanup AS queued(path, owner_id, review_id, purpose, retired)\n VALUES (p, OLD.user_id, OLD.id, purpose, NOT review_media_private.referenced(p))\n ON CONFLICT (path) DO UPDATE SET review_id = EXCLUDED.review_id,\n retired = queued.retired OR EXCLUDED.retired\n WHERE queued.owner_id = EXCLUDED.owner_id AND queued.purpose = EXCLUDED.purpose\n AND queued.purpose = 'verification' AND NOT queued.retired AND NOT queued.complete\n AND review_media_private.owned_legacy(queued.path, queued.owner_id, queued.purpose);$new$);\n EXECUTE replace(definition,source,patched);\n IF (SELECT to_jsonb(p)-'prosrc' FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM metadata\n OR (SELECT encode(sha256(convert_to(p.prosrc,'UTF8')),'hex') FROM pg_proc p WHERE p.oid=target)<>'cbf6a87e453e8c2e14d67acd73a92e399fbba4b4f24ac27966806e13ef1fd905'\n THEN RAISE EXCEPTION 'REVIEW_SHARED_POST_DRIFT'; END IF;\n CREATE FUNCTION pg_temp.review_shared_assert() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path='' AS $bridge$\n BEGIN\n PERFORM privacy_retention.assert_g014_public_rpc_allowlist();\n PERFORM privacy_retention.assert_g014_definer_contract();\n PERFORM privacy_retention.assert_g014_catalog_contract();\n DROP FUNCTION pg_temp.review_shared_assert();\n END $bridge$;\n REVOKE ALL ON FUNCTION pg_temp.review_shared_assert() FROM PUBLIC,anon,authenticated,service_role;\n GRANT EXECUTE ON FUNCTION pg_temp.review_shared_assert() TO postgres;\n IF (SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) FROM privacy_retention.g014_catalog_contract_manifest m) IS DISTINCT FROM before_manifest\n THEN RAISE EXCEPTION 'REVIEW_SHARED_MANIFEST_DRIFT'; END IF;\n RESET ROLE;\n IF schema_lease THEN REVOKE CREATE ON SCHEMA review_media_private FROM privacy_workflow_owner; END IF;\n IF major=15 THEN\n IF pg15_lease THEN REVOKE privacy_workflow_owner FROM postgres; END IF;\n ELSIF self_membership IS NULL THEN\n REVOKE privacy_workflow_owner FROM postgres GRANTED BY postgres;\n ELSE\n EXECUTE format('GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT %s,SET %s GRANTED BY postgres',\n upper(self_membership->>'inherit_option'),upper(self_membership->>'set_option'));\n END IF;\n\n IF (SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') FROM pg_auth_members m) IS DISTINCT FROM before_members\n OR (SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) FROM pg_proc p WHERE pronamespace='privacy_retention'::regnamespace) IS DISTINCT FROM before_assertions\n OR (SELECT to_jsonb(n) FROM pg_namespace n WHERE n.oid='review_media_private'::regnamespace) IS DISTINCT FROM before_schema\n THEN RAISE EXCEPTION 'REVIEW_SHARED_PRESERVATION_DRIFT'; END IF;\n PERFORM pg_temp.review_shared_assert();\nEND $forward$", + "-- REVIEW_SHARED_MEDIA_READBACK_BEGIN\nDO $readback$\nBEGIN\n IF NOT EXISTS(SELECT 1 FROM pg_proc p WHERE p.oid='review_media_private.enqueue_removed()'::regprocedure\n AND p.proowner='privacy_workflow_owner'::regrole AND p.prosecdef AND p.proconfig=ARRAY['search_path=\"\"']::text[]\n AND p.prorettype='trigger'::regtype AND p.provolatile='v' AND p.pronargs=0\n AND p.prolang=(SELECT oid FROM pg_language WHERE lanname='plpgsql')\n AND encode(sha256(convert_to(p.prosrc,'UTF8')),'hex')='cbf6a87e453e8c2e14d67acd73a92e399fbba4b4f24ac27966806e13ef1fd905'\n AND NOT has_function_privilege('anon',p.oid,'EXECUTE')\n AND NOT has_function_privilege('authenticated',p.oid,'EXECUTE')\n AND NOT has_function_privilege('service_role',p.oid,'EXECUTE'))\n THEN RAISE EXCEPTION 'REVIEW_SHARED_READBACK_DRIFT'; END IF;\nEND $readback$", + "-- REVIEW_SHARED_MEDIA_READBACK_END\nCOMMIT" + ] + }, + { + "id": "admin_evaluation_revision_safeupdate", + "version": "20261011094346", + "name": "admin_evaluation_revision_safeupdate", + "path": "backend/supabase/migrations/20261011094346_admin_evaluation_revision_safeupdate.sql", + "bytes": 3666, + "sha256": "b7061015ea07192dccfcf0774d5548a9c2a584c7daa140d77fccd7c75cb8bf88", + "statementCount": 6, + "statementVectorSha256": "8cfd8d94d5419712b767b1360361ed905b151e45c8ba3d9cca8d7fe3f1adbd69", + "originalStatementVector": [ + "-- Keep Supabase safeupdate enabled while advancing the single catalog revision.\n-- This is a forward, source-bound replacement; applied migrations are immutable.\nBEGIN", + "SET LOCAL lock_timeout='2s'", + "SELECT pg_advisory_xact_lock(hashtextextended('tzudong:admin-revision-safeupdate:v1',0))", + "DO $forward$\nDECLARE target oid:='pipeline_control.bump_admin_evaluation_revision()'::regprocedure;\n definition text; source text; patched text; metadata jsonb;\n before_members jsonb; before_manifest jsonb; before_schema jsonb;\nBEGIN\n IF current_user<>'postgres' OR session_user<>'postgres'\n OR current_setting('transaction_read_only')<>'off'\n OR current_setting('server_version_num')::integer/10000 NOT IN (15,17)\n THEN RAISE EXCEPTION 'ADMIN_REVISION_EXECUTOR_DENIED'; END IF;\n SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') INTO before_members FROM pg_auth_members m;\n SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) INTO before_manifest FROM privacy_retention.g014_catalog_contract_manifest m;\n SELECT to_jsonb(n) INTO before_schema FROM pg_namespace n WHERE n.oid='pipeline_control'::regnamespace;\n SELECT pg_get_functiondef(p.oid),p.prosrc,to_jsonb(p)-'prosrc' INTO definition,source,metadata FROM pg_proc p WHERE p.oid=target;\n IF metadata->>'proowner'<>'postgres'::regrole::oid::text\n OR encode(sha256(convert_to(source,'UTF8')),'hex')<>'ed16226885c03b05cb459381cc5ef7c8352506f7ea1f88ae526a6511e4cc1b20'\n OR (SELECT count(*) FROM pipeline_control.admin_evaluation_catalog_revision WHERE singleton)<>1\n THEN RAISE EXCEPTION 'ADMIN_REVISION_SOURCE_DRIFT'; END IF;\n IF (length(source)-length(replace(source,$old$UPDATE pipeline_control.admin_evaluation_catalog_revision SET revision = revision + 1;$old$,'')))/length($old$UPDATE pipeline_control.admin_evaluation_catalog_revision SET revision = revision + 1;$old$)<>1\n THEN RAISE EXCEPTION 'ADMIN_REVISION_ANCHOR_DRIFT'; END IF;\n patched:=replace(source,$old$UPDATE pipeline_control.admin_evaluation_catalog_revision SET revision = revision + 1;$old$,$new$UPDATE pipeline_control.admin_evaluation_catalog_revision SET revision = revision + 1 WHERE singleton;$new$);\n EXECUTE replace(definition,source,patched);\n IF (SELECT to_jsonb(p)-'prosrc' FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM metadata\n OR (SELECT encode(sha256(convert_to(p.prosrc,'UTF8')),'hex') FROM pg_proc p WHERE p.oid=target)<>'4a3bf9e7cc7d91fb1a2a5195d01f40de728539daad1a0a8132a7c10404dc7936'\n OR (SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') FROM pg_auth_members m) IS DISTINCT FROM before_members\n OR (SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) FROM privacy_retention.g014_catalog_contract_manifest m) IS DISTINCT FROM before_manifest\n OR (SELECT to_jsonb(n) FROM pg_namespace n WHERE n.oid='pipeline_control'::regnamespace) IS DISTINCT FROM before_schema\n THEN RAISE EXCEPTION 'ADMIN_REVISION_POST_DRIFT'; END IF;\nEND $forward$", + "-- ADMIN_REVISION_READBACK_BEGIN\nDO $readback$\nBEGIN\n IF NOT EXISTS(SELECT 1 FROM pg_proc WHERE oid='pipeline_control.bump_admin_evaluation_revision()'::regprocedure\n AND proowner='postgres'::regrole AND prosecdef AND proconfig=ARRAY['search_path=\"\"']::text[]\n AND proacl=ARRAY['postgres=X/postgres']::aclitem[]\n AND encode(sha256(convert_to(prosrc,'UTF8')),'hex')='4a3bf9e7cc7d91fb1a2a5195d01f40de728539daad1a0a8132a7c10404dc7936')\n OR (SELECT count(*) FROM pipeline_control.admin_evaluation_catalog_revision WHERE singleton)<>1\n THEN RAISE EXCEPTION 'ADMIN_REVISION_READBACK_DRIFT'; END IF;\nEND $readback$", + "-- ADMIN_REVISION_READBACK_END\nCOMMIT" + ] + }, + { + "id": "admin_record_expired_preview_readback", + "version": "20261011100730", + "name": "admin_record_expired_preview_readback", + "path": "backend/supabase/migrations/20261011100730_admin_record_expired_preview_readback.sql", + "bytes": 4163, + "sha256": "26c64074bb98e718ba96732ac2baeedb851afe6ff745e0ddb5ca075359892472", + "statementCount": 6, + "statementVectorSha256": "afa3246f159064fa19046cc9da75c3cc215150dc78ad8cf80f887f6263966f5a", + "originalStatementVector": [ + "-- A locked, expired preview is definitively unapplied; never infer this from a client clock.\n-- Applied receipts remain recoverable after expiry. No Apply is retried here.\nBEGIN", + "SET LOCAL lock_timeout='2s'", + "SELECT pg_advisory_xact_lock(hashtextextended('tzudong:admin-preview-expiry:v1',0))", + "DO $forward$\nDECLARE target oid:='public.admin_record_action(uuid,text,uuid,text,uuid[],jsonb,text)'::regprocedure;\n definition text; source text; patched text; metadata jsonb; defaults text;\n before_members jsonb; before_manifest jsonb; before_schema jsonb;\nBEGIN\n IF current_user<>'postgres' OR session_user<>'postgres'\n OR current_setting('transaction_read_only')<>'off'\n OR current_setting('server_version_num')::integer/10000 NOT IN (15,17)\n THEN RAISE EXCEPTION 'ADMIN_PREVIEW_EXECUTOR_DENIED'; END IF;\n SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') INTO before_members FROM pg_auth_members m;\n SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) INTO before_manifest FROM privacy_retention.g014_catalog_contract_manifest m;\n SELECT to_jsonb(n) INTO before_schema FROM pg_namespace n WHERE n.oid='public'::regnamespace;\n SELECT pg_get_functiondef(p.oid),p.prosrc,to_jsonb(p)-ARRAY['prosrc','proargdefaults'],pg_get_expr(p.proargdefaults,0)\n INTO definition,source,metadata,defaults FROM pg_proc p WHERE p.oid=target;\n IF metadata->>'proowner'<>'postgres'::regrole::oid::text\n OR encode(sha256(convert_to(source,'UTF8')),'hex')<>'2353de06b4d93f45fd603380e678f4edbcc0b16a8bd9860b5967683bea5f1157'\n THEN RAISE EXCEPTION 'ADMIN_PREVIEW_SOURCE_DRIFT'; END IF;\n IF (length(source)-length(replace(source,$old$IF op.id IS NULL THEN RAISE EXCEPTION 'RECORD_ACTION_NOT_FOUND'; END IF;\n RETURN op.receipt||$old$,'')))/length($old$IF op.id IS NULL THEN RAISE EXCEPTION 'RECORD_ACTION_NOT_FOUND'; END IF;\n RETURN op.receipt||$old$)<>1\n THEN RAISE EXCEPTION 'ADMIN_PREVIEW_ANCHOR_DRIFT'; END IF;\n patched:=replace(source,$old$IF op.id IS NULL THEN RAISE EXCEPTION 'RECORD_ACTION_NOT_FOUND'; END IF;\n RETURN op.receipt||$old$,$new$IF op.id IS NULL THEN RAISE EXCEPTION 'RECORD_ACTION_NOT_FOUND'; END IF;\n IF op.state='preview' AND op.expires_at<=clock_timestamp() THEN RAISE EXCEPTION 'RECORD_ACTION_PREVIEW_EXPIRED'; END IF;\n RETURN op.receipt||$new$);\n EXECUTE replace(definition,source,patched);\n IF (SELECT to_jsonb(p)-ARRAY['prosrc','proargdefaults'] FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM metadata\n OR (SELECT pg_get_expr(p.proargdefaults,0) FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM defaults\n OR (SELECT encode(sha256(convert_to(p.prosrc,'UTF8')),'hex') FROM pg_proc p WHERE p.oid=target)<>'50b5900a7b6ca43062d6373dbba75378d714312c9ef02c6939997389f6833ba0'\n OR (SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') FROM pg_auth_members m) IS DISTINCT FROM before_members\n OR (SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) FROM privacy_retention.g014_catalog_contract_manifest m) IS DISTINCT FROM before_manifest\n OR (SELECT to_jsonb(n) FROM pg_namespace n WHERE n.oid='public'::regnamespace) IS DISTINCT FROM before_schema\n THEN RAISE EXCEPTION 'ADMIN_PREVIEW_POST_DRIFT'; END IF;\nEND $forward$", + "-- ADMIN_PREVIEW_EXPIRY_READBACK_BEGIN\nDO $readback$\nBEGIN\n IF NOT EXISTS(SELECT 1 FROM pg_proc WHERE oid='public.admin_record_action(uuid,text,uuid,text,uuid[],jsonb,text)'::regprocedure\n AND proowner='postgres'::regrole AND NOT prosecdef AND proconfig=ARRAY['search_path=\"\"']::text[]\n AND encode(sha256(convert_to(prosrc,'UTF8')),'hex')='50b5900a7b6ca43062d6373dbba75378d714312c9ef02c6939997389f6833ba0')\n OR has_function_privilege('anon','public.admin_record_action(uuid,text,uuid,text,uuid[],jsonb,text)','EXECUTE')\n OR has_function_privilege('authenticated','public.admin_record_action(uuid,text,uuid,text,uuid[],jsonb,text)','EXECUTE')\n OR NOT has_function_privilege('service_role','public.admin_record_action(uuid,text,uuid,text,uuid[],jsonb,text)','EXECUTE')\n THEN RAISE EXCEPTION 'ADMIN_PREVIEW_READBACK_DRIFT'; END IF;\nEND $readback$", + "-- ADMIN_PREVIEW_EXPIRY_READBACK_END\nCOMMIT" + ] + }, + { + "id": "admin_verification_durable_staging", + "version": "20261011103501", + "name": "admin_verification_durable_staging", + "path": "backend/supabase/migrations/20261011103501_admin_verification_durable_staging.sql", + "bytes": 12008, + "sha256": "081c48e91172e4e98079b03aa210c0f7481d95a21945e5a288187263089e57db", + "statementCount": 6, + "statementVectorSha256": "9c2f696f41d5e001d574809b911ba6dc2451bbd6cc9f23684a98fe543d56b512", + "originalStatementVector": [ + "-- Persist an owner-bound private verification key before Storage transport.\n-- Only retired unreferenced cleanup counts as outstanding work; shared references remain protected.\n-- Applied SQL, grants, actor authorization and canonical key ownership stay intact.\nBEGIN", + "SET LOCAL lock_timeout='2s'", + "SELECT pg_advisory_xact_lock(hashtextextended('tzudong:review-media-catalog:v2',0))", + "DO $forward$\nDECLARE target oid:='public.admin_review_verification_cleanup(uuid,text,text[])'::regprocedure;\n definition text; source text; patched text; metadata jsonb; defaults text;\n before_members jsonb; before_assertions jsonb; before_manifest jsonb; self_membership jsonb;\n before_schema jsonb; schema_lease boolean;\n workflow_owner oid:='privacy_workflow_owner'::regrole; runner oid:='postgres'::regrole;\n major integer:=current_setting('server_version_num')::integer/10000; pg15_lease boolean:=false;\nBEGIN\n IF current_user<>'postgres' OR session_user<>'postgres' OR major NOT IN (15,17)\n OR current_setting('transaction_read_only')<>'off' OR NOT EXISTS(SELECT 1 FROM pg_roles\n WHERE oid=workflow_owner AND NOT rolsuper AND NOT rolbypassrls AND NOT rolcanlogin AND NOT rolinherit)\n THEN RAISE EXCEPTION 'REVIEW_SHARED_EXECUTOR_DENIED'; END IF;\n SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') INTO before_members FROM pg_auth_members m;\n SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) INTO before_assertions FROM pg_proc p WHERE pronamespace='privacy_retention'::regnamespace;\n SELECT pg_get_functiondef(p.oid),p.prosrc,to_jsonb(p)-ARRAY['prosrc','proargdefaults'],pg_get_expr(p.proargdefaults,0) INTO definition,source,metadata,defaults FROM pg_proc p WHERE p.oid=target;\n IF metadata->>'proowner'<>workflow_owner::text OR encode(sha256(convert_to(source,'UTF8')),'hex')<>'f4d19bc5b41fe7808635240cae80f796d12f6cad8b13802577a88afbb1f024a7'\n THEN RAISE EXCEPTION 'REVIEW_SHARED_SOURCE_DRIFT'; END IF;\n SELECT to_jsonb(n) INTO before_schema FROM pg_namespace n WHERE n.oid='public'::regnamespace;\n IF before_schema->>'nspowner' NOT IN (runner::text,'pg_database_owner'::regrole::oid::text)\n THEN RAISE EXCEPTION 'REVIEW_SHARED_SCHEMA_OWNER_DRIFT'; END IF;\n schema_lease:=NOT has_schema_privilege(workflow_owner,'public','CREATE');\n IF schema_lease THEN GRANT CREATE ON SCHEMA public TO privacy_workflow_owner; END IF;\n SELECT to_jsonb(m) INTO self_membership FROM pg_auth_members m WHERE roleid=workflow_owner AND member=runner AND grantor=runner;\n IF self_membership IS NOT NULL AND (self_membership->>'admin_option')::boolean THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_MEMBERSHIP_DENIED'; END IF;\n IF major=17 THEN\n IF NOT EXISTS(SELECT 1 FROM pg_auth_members WHERE roleid=workflow_owner AND member=runner AND admin_option AND grantor<>runner)\n THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_OWNER_AUTHORITY_MISSING'; END IF;\n EXECUTE 'GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT FALSE,SET TRUE GRANTED BY postgres';\n ELSE\n pg15_lease:=NOT pg_has_role(runner,workflow_owner,'MEMBER');\n IF pg15_lease THEN GRANT privacy_workflow_owner TO postgres; END IF;\n END IF;\n SET LOCAL ROLE privacy_workflow_owner;\n SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) INTO before_manifest FROM privacy_retention.g014_catalog_contract_manifest m;\n\n IF source IS DISTINCT FROM $old$\nDECLARE items jsonb;\nBEGIN\n IF p_review_id IS NULL OR p_phase IS NULL OR p_phase NOT IN ('prepare','finish')\n OR current_setting('transaction_isolation') <> 'read committed'\n OR (p_phase = 'prepare' AND p_paths IS NOT NULL)\n OR (p_phase = 'finish' AND (p_paths IS NULL OR cardinality(p_paths) > 20\n OR EXISTS (SELECT 1 FROM unnest(p_paths) x WHERE x IS NULL OR octet_length(x) > 8192)\n OR cardinality(p_paths) <> (SELECT count(DISTINCT x) FROM unnest(p_paths) x))) THEN\n RAISE EXCEPTION 'REVIEW_ADMIN_CLEANUP_INVALID';\n END IF;\n PERFORM pg_advisory_xact_lock(741093, 1);\n IF p_phase = 'prepare' THEN\n UPDATE review_media_private.cleanup c SET retired = true\n WHERE c.review_id = p_review_id AND c.purpose = 'verification'\n AND NOT c.retired AND NOT c.complete AND NOT review_media_private.referenced(c.path);\n SELECT coalesce(jsonb_agg(to_jsonb(x) ORDER BY x.path),'[]'::jsonb) INTO items\n FROM (SELECT c.path,c.owner_id,c.review_id,c.purpose FROM review_media_private.cleanup c\n WHERE c.review_id = p_review_id AND c.purpose = 'verification' AND c.retired AND NOT c.complete\n AND NOT review_media_private.referenced(c.path)\n AND (review_media_private.canonical(c.path,c.owner_id,c.review_id,c.purpose)\n OR review_media_private.owned_legacy(c.path,c.owner_id,c.purpose))\n ORDER BY c.path LIMIT 20) x;\n RETURN jsonb_build_object('status','READY','items',items);\n END IF;\n UPDATE review_media_private.cleanup c SET complete = true\n WHERE c.review_id = p_review_id AND c.purpose = 'verification' AND c.retired AND NOT c.complete\n AND c.path = ANY(p_paths)\n AND (review_media_private.canonical(c.path,c.owner_id,c.review_id,c.purpose)\n OR review_media_private.owned_legacy(c.path,c.owner_id,c.purpose))\n AND NOT review_media_private.referenced(c.path)\n AND NOT EXISTS (SELECT 1 FROM storage.objects o\n WHERE o.bucket_id IN ('review-photos','review-verifications') AND o.name = c.path);\n RETURN jsonb_build_object('remaining',(SELECT count(*) FROM review_media_private.cleanup c\n WHERE c.review_id = p_review_id AND c.purpose = 'verification' AND NOT c.complete));\nEND\n$old$ THEN RAISE EXCEPTION 'REVIEW_SHARED_ANCHOR_DRIFT'; END IF;\n patched:=$new$\nDECLARE items jsonb; owner uuid;\nBEGIN\n IF p_review_id IS NULL OR p_phase IS NULL OR p_phase NOT IN ('prepare','finish','stage')\n OR current_setting('transaction_isolation') <> 'read committed'\n OR (p_phase = 'prepare' AND p_paths IS NOT NULL)\n OR (p_phase IN ('finish','stage') AND (p_paths IS NULL OR cardinality(p_paths) > 20\n OR EXISTS (SELECT 1 FROM unnest(p_paths) x WHERE x IS NULL OR octet_length(x) > 8192)\n OR cardinality(p_paths) <> (SELECT count(DISTINCT x) FROM unnest(p_paths) x))) THEN\n RAISE EXCEPTION 'REVIEW_ADMIN_CLEANUP_INVALID';\n END IF;\n PERFORM pg_advisory_xact_lock(741093, 1);\n IF p_phase = 'stage' THEN\n SELECT user_id INTO owner FROM public.reviews WHERE id=p_review_id;\n IF owner IS NULL OR cardinality(p_paths)<>1\n OR NOT review_media_private.canonical(p_paths[1],owner,p_review_id,'verification')\n THEN RAISE EXCEPTION 'REVIEW_ADMIN_CLEANUP_INVALID'; END IF;\n INSERT INTO review_media_private.cleanup(path,owner_id,review_id,purpose,retired)\n VALUES(p_paths[1],owner,p_review_id,'verification',false) ON CONFLICT DO NOTHING;\n IF NOT EXISTS(SELECT 1 FROM review_media_private.cleanup WHERE path=p_paths[1]\n AND owner_id=owner AND review_id=p_review_id AND purpose='verification'\n AND NOT retired AND NOT complete) THEN RAISE EXCEPTION 'REVIEW_ADMIN_UPLOAD_RETIRED'; END IF;\n RETURN jsonb_build_object('status','STAGED');\n END IF;\n IF p_phase = 'prepare' THEN\n UPDATE review_media_private.cleanup c SET retired = true\n WHERE c.review_id = p_review_id AND c.purpose = 'verification'\n AND NOT c.retired AND NOT c.complete AND NOT review_media_private.referenced(c.path);\n SELECT coalesce(jsonb_agg(to_jsonb(x) ORDER BY x.path),'[]'::jsonb) INTO items\n FROM (SELECT c.path,c.owner_id,c.review_id,c.purpose FROM review_media_private.cleanup c\n WHERE c.review_id = p_review_id AND c.purpose = 'verification' AND c.retired AND NOT c.complete\n AND NOT review_media_private.referenced(c.path)\n AND (review_media_private.canonical(c.path,c.owner_id,c.review_id,c.purpose)\n OR review_media_private.owned_legacy(c.path,c.owner_id,c.purpose))\n ORDER BY c.path LIMIT 20) x;\n RETURN jsonb_build_object('status','READY','items',items);\n END IF;\n UPDATE review_media_private.cleanup c SET complete = true\n WHERE c.review_id = p_review_id AND c.purpose = 'verification' AND c.retired AND NOT c.complete\n AND c.path = ANY(p_paths)\n AND (review_media_private.canonical(c.path,c.owner_id,c.review_id,c.purpose)\n OR review_media_private.owned_legacy(c.path,c.owner_id,c.purpose))\n AND NOT review_media_private.referenced(c.path)\n AND NOT EXISTS (SELECT 1 FROM storage.objects o\n WHERE o.bucket_id IN ('review-photos','review-verifications') AND o.name = c.path);\n RETURN jsonb_build_object('remaining',(SELECT count(*) FROM review_media_private.cleanup c\n WHERE c.review_id = p_review_id AND c.purpose = 'verification' AND c.retired AND NOT c.complete\n AND NOT review_media_private.referenced(c.path)));\nEND\n$new$;\n EXECUTE replace(definition,source,patched);\n IF (SELECT to_jsonb(p)-ARRAY['prosrc','proargdefaults'] FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM metadata\n OR (SELECT pg_get_expr(p.proargdefaults,0) FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM defaults\n OR (SELECT encode(sha256(convert_to(p.prosrc,'UTF8')),'hex') FROM pg_proc p WHERE p.oid=target)<>'468c12ee6c3463876cbccb394fcba8f870459a1f6cf3d945d97307f8551f094b'\n THEN RAISE EXCEPTION 'REVIEW_SHARED_POST_DRIFT'; END IF;\n CREATE FUNCTION pg_temp.review_shared_assert() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path='' AS $bridge$\n BEGIN\n PERFORM privacy_retention.assert_g014_public_rpc_allowlist();\n PERFORM privacy_retention.assert_g014_definer_contract();\n PERFORM privacy_retention.assert_g014_catalog_contract();\n DROP FUNCTION pg_temp.review_shared_assert();\n END $bridge$;\n REVOKE ALL ON FUNCTION pg_temp.review_shared_assert() FROM PUBLIC,anon,authenticated,service_role;\n GRANT EXECUTE ON FUNCTION pg_temp.review_shared_assert() TO postgres;\n IF (SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) FROM privacy_retention.g014_catalog_contract_manifest m) IS DISTINCT FROM before_manifest\n THEN RAISE EXCEPTION 'REVIEW_SHARED_MANIFEST_DRIFT'; END IF;\n RESET ROLE;\n IF schema_lease THEN REVOKE CREATE ON SCHEMA public FROM privacy_workflow_owner; END IF;\n IF major=15 THEN\n IF pg15_lease THEN REVOKE privacy_workflow_owner FROM postgres; END IF;\n ELSIF self_membership IS NULL THEN\n REVOKE privacy_workflow_owner FROM postgres GRANTED BY postgres;\n ELSE\n EXECUTE format('GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT %s,SET %s GRANTED BY postgres',\n upper(self_membership->>'inherit_option'),upper(self_membership->>'set_option'));\n END IF;\n\n IF (SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') FROM pg_auth_members m) IS DISTINCT FROM before_members\n OR (SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) FROM pg_proc p WHERE pronamespace='privacy_retention'::regnamespace) IS DISTINCT FROM before_assertions\n OR (SELECT to_jsonb(n) FROM pg_namespace n WHERE n.oid='public'::regnamespace) IS DISTINCT FROM before_schema\n THEN RAISE EXCEPTION 'REVIEW_SHARED_PRESERVATION_DRIFT'; END IF;\n PERFORM pg_temp.review_shared_assert();\nEND $forward$", + "-- ADMIN_STAGING_READBACK_BEGIN\nDO $readback$\nBEGIN\n IF NOT EXISTS(SELECT 1 FROM pg_proc WHERE oid='public.admin_review_verification_cleanup(uuid,text,text[])'::regprocedure\n AND proowner='privacy_workflow_owner'::regrole AND prosecdef AND proconfig=ARRAY['search_path=\"\"']::text[]\n AND encode(sha256(convert_to(prosrc,'UTF8')),'hex')='468c12ee6c3463876cbccb394fcba8f870459a1f6cf3d945d97307f8551f094b')\n OR has_function_privilege('anon','public.admin_review_verification_cleanup(uuid,text,text[])','EXECUTE')\n OR has_function_privilege('authenticated','public.admin_review_verification_cleanup(uuid,text,text[])','EXECUTE')\n OR NOT has_function_privilege('service_role','public.admin_review_verification_cleanup(uuid,text,text[])','EXECUTE')\n THEN RAISE EXCEPTION 'ADMIN_STAGING_READBACK_DRIFT'; END IF;\nEND $readback$", + "-- ADMIN_STAGING_READBACK_END\nCOMMIT" + ] + }, + { + "id": "review_media_storage_retirement_fence", + "version": "20261011105114", + "name": "review_media_storage_retirement_fence", + "path": "backend/supabase/migrations/20261011105114_review_media_storage_retirement_fence.sql", + "bytes": 11418, + "sha256": "79375138d7e82a3be2b85b219ff52147db13db9c93070569be40a61ab97c3d53", + "statementCount": 6, + "statementVectorSha256": "6be5e2a7387d2e6d71e8ee830cb9a48df74b592ffd18bb51eba43869fc005c89", + "originalStatementVector": [ + "-- Retirement must also fence late privileged Storage metadata commits.\n-- A BEFORE STATEMENT serializer takes the global lock before row or object locks.\n-- Applied SQL, grants, actor authorization and canonical key ownership stay intact.\nBEGIN", + "SET LOCAL lock_timeout='2s'", + "SELECT pg_advisory_xact_lock(hashtextextended('tzudong:review-media-catalog:v2',0))", + "DO $forward$\nDECLARE target oid:='review_media_private.guard_references()'::regprocedure;\n definition text; source text; patched text; metadata jsonb;\n before_members jsonb; before_assertions jsonb; before_manifest jsonb; self_membership jsonb;\n before_schema jsonb; schema_lease boolean;\n workflow_owner oid:='privacy_workflow_owner'::regrole; runner oid:='postgres'::regrole;\n major integer:=current_setting('server_version_num')::integer/10000; pg15_lease boolean:=false;\nBEGIN\n IF current_user<>'postgres' OR session_user<>'postgres' OR major NOT IN (15,17)\n OR current_setting('transaction_read_only')<>'off' OR NOT EXISTS(SELECT 1 FROM pg_roles\n WHERE oid=workflow_owner AND NOT rolsuper AND NOT rolbypassrls AND NOT rolcanlogin AND NOT rolinherit)\n THEN RAISE EXCEPTION 'REVIEW_SHARED_EXECUTOR_DENIED'; END IF;\n SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') INTO before_members FROM pg_auth_members m;\n SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) INTO before_assertions FROM pg_proc p WHERE pronamespace='privacy_retention'::regnamespace;\n SELECT pg_get_functiondef(p.oid),p.prosrc,to_jsonb(p)-'prosrc' INTO definition,source,metadata FROM pg_proc p WHERE p.oid=target;\n IF metadata->>'proowner'<>workflow_owner::text OR encode(sha256(convert_to(source,'UTF8')),'hex')<>'846030547ffd76e17596755d7eee4279dcb9b563361435260826071d2f250d59'\n THEN RAISE EXCEPTION 'REVIEW_SHARED_SOURCE_DRIFT'; END IF;\n SELECT to_jsonb(n) INTO before_schema FROM pg_namespace n WHERE n.oid='review_media_private'::regnamespace;\n IF before_schema->>'nspowner'<>runner::text THEN RAISE EXCEPTION 'REVIEW_SHARED_SCHEMA_OWNER_DRIFT'; END IF;\n schema_lease:=NOT has_schema_privilege(workflow_owner,'review_media_private','CREATE');\n IF schema_lease THEN GRANT CREATE ON SCHEMA review_media_private TO privacy_workflow_owner; END IF;\n SELECT to_jsonb(m) INTO self_membership FROM pg_auth_members m WHERE roleid=workflow_owner AND member=runner AND grantor=runner;\n IF self_membership IS NOT NULL AND (self_membership->>'admin_option')::boolean THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_MEMBERSHIP_DENIED'; END IF;\n IF major=17 THEN\n IF NOT EXISTS(SELECT 1 FROM pg_auth_members WHERE roleid=workflow_owner AND member=runner AND admin_option AND grantor<>runner)\n THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_OWNER_AUTHORITY_MISSING'; END IF;\n EXECUTE 'GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT FALSE,SET TRUE GRANTED BY postgres';\n ELSE\n pg15_lease:=NOT pg_has_role(runner,workflow_owner,'MEMBER');\n IF pg15_lease THEN GRANT privacy_workflow_owner TO postgres; END IF;\n END IF;\n SET LOCAL ROLE privacy_workflow_owner;\n SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) INTO before_manifest FROM privacy_retention.g014_catalog_contract_manifest m;\n\n IF source IS DISTINCT FROM $old$\nDECLARE p text; old_food text[] := ARRAY[]::text[]; old_verification text; purpose text;\nBEGIN\n IF TG_OP = 'UPDATE' AND NEW.user_id = OLD.user_id AND NEW.id = OLD.id THEN\n old_food := COALESCE(OLD.food_photos, ARRAY[]::text[]);\n old_verification := OLD.verification_photo;\n END IF;\n FOR p, purpose IN SELECT value, 'food' FROM unnest(COALESCE(NEW.food_photos, ARRAY[]::text[])) value\n UNION ALL SELECT NEW.verification_photo, 'verification' LOOP\n IF EXISTS (SELECT 1 FROM review_media_private.cleanup c\n WHERE c.path = review_media_private.reference_key(p) AND c.retired) THEN\n RAISE EXCEPTION USING MESSAGE = 'REVIEW_MEDIA_RETIRED', ERRCODE = '23514';\n END IF;\n -- Historical keys may remain unchanged in their original purpose only.\n IF NOT COALESCE(CASE WHEN purpose = 'food' THEN p = ANY(old_food) ELSE p = old_verification END, false) THEN\n IF NOT review_media_private.canonical(p, NEW.user_id, NEW.id, purpose) THEN\n RAISE EXCEPTION USING MESSAGE = 'REVIEW_MEDIA_INVALID', ERRCODE = '23514';\n END IF;\n END IF;\n END LOOP;\n RETURN NEW;\nEND $old$ THEN RAISE EXCEPTION 'REVIEW_SHARED_ANCHOR_DRIFT'; END IF;\n patched:=$new$\nDECLARE p text; old_food text[] := ARRAY[]::text[]; old_verification text; purpose text;\nBEGIN\n IF TG_TABLE_SCHEMA='storage' AND TG_TABLE_NAME='objects' THEN\n IF NEW.bucket_id IN ('review-photos','review-verifications') AND EXISTS(\n SELECT 1 FROM review_media_private.cleanup WHERE path=NEW.name AND retired)\n THEN RAISE EXCEPTION USING MESSAGE='REVIEW_MEDIA_RETIRED',ERRCODE='23514'; END IF;\n RETURN NEW;\n END IF;\n IF TG_OP = 'UPDATE' AND NEW.user_id = OLD.user_id AND NEW.id = OLD.id THEN\n old_food := COALESCE(OLD.food_photos, ARRAY[]::text[]);\n old_verification := OLD.verification_photo;\n END IF;\n FOR p, purpose IN SELECT value, 'food' FROM unnest(COALESCE(NEW.food_photos, ARRAY[]::text[])) value\n UNION ALL SELECT NEW.verification_photo, 'verification' LOOP\n IF EXISTS (SELECT 1 FROM review_media_private.cleanup c\n WHERE c.path = review_media_private.reference_key(p) AND c.retired) THEN\n RAISE EXCEPTION USING MESSAGE = 'REVIEW_MEDIA_RETIRED', ERRCODE = '23514';\n END IF;\n -- Historical keys may remain unchanged in their original purpose only.\n IF NOT COALESCE(CASE WHEN purpose = 'food' THEN p = ANY(old_food) ELSE p = old_verification END, false) THEN\n IF NOT review_media_private.canonical(p, NEW.user_id, NEW.id, purpose) THEN\n RAISE EXCEPTION USING MESSAGE = 'REVIEW_MEDIA_INVALID', ERRCODE = '23514';\n END IF;\n END IF;\n END LOOP;\n RETURN NEW;\nEND $new$;\n EXECUTE replace(definition,source,patched);\n IF (SELECT to_jsonb(p)-'prosrc' FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM metadata\n OR (SELECT encode(sha256(convert_to(p.prosrc,'UTF8')),'hex') FROM pg_proc p WHERE p.oid=target)<>'94113c8ae1231e8eae5a698f35a1370a38367e42c79cc87ac62db76c32e735b1'\n THEN RAISE EXCEPTION 'REVIEW_SHARED_POST_DRIFT'; END IF;\n CREATE FUNCTION pg_temp.review_shared_assert() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path='' AS $bridge$\n BEGIN\n PERFORM privacy_retention.assert_g014_public_rpc_allowlist();\n PERFORM privacy_retention.assert_g014_definer_contract();\n PERFORM privacy_retention.assert_g014_catalog_contract();\n DROP FUNCTION pg_temp.review_shared_assert();\n END $bridge$;\n REVOKE ALL ON FUNCTION pg_temp.review_shared_assert() FROM PUBLIC,anon,authenticated,service_role;\n GRANT EXECUTE ON FUNCTION pg_temp.review_shared_assert() TO postgres;\n IF (SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) FROM privacy_retention.g014_catalog_contract_manifest m) IS DISTINCT FROM before_manifest\n THEN RAISE EXCEPTION 'REVIEW_SHARED_MANIFEST_DRIFT'; END IF;\n IF has_function_privilege('postgres','review_media_private.guard_references()','EXECUTE')\n OR has_function_privilege('postgres','review_media_private.lock_changes()','EXECUTE')\n THEN RAISE EXCEPTION 'STORAGE_FENCE_LEASE_BASELINE_DRIFT'; END IF;\n GRANT EXECUTE ON FUNCTION review_media_private.guard_references(),review_media_private.lock_changes() TO postgres;\n RESET ROLE;\n CREATE TRIGGER a00_review_media_storage_serialize BEFORE INSERT OR UPDATE OF bucket_id,name ON storage.objects\n FOR EACH STATEMENT EXECUTE FUNCTION review_media_private.lock_changes();\n CREATE TRIGGER a00_review_media_storage_guard BEFORE INSERT OR UPDATE OF bucket_id,name ON storage.objects\n FOR EACH ROW EXECUTE FUNCTION review_media_private.guard_references();\n SET LOCAL ROLE privacy_workflow_owner;\n REVOKE EXECUTE ON FUNCTION review_media_private.guard_references(),review_media_private.lock_changes() FROM postgres;\n RESET ROLE;\n IF schema_lease THEN REVOKE CREATE ON SCHEMA review_media_private FROM privacy_workflow_owner; END IF;\n IF major=15 THEN\n IF pg15_lease THEN REVOKE privacy_workflow_owner FROM postgres; END IF;\n ELSIF self_membership IS NULL THEN\n REVOKE privacy_workflow_owner FROM postgres GRANTED BY postgres;\n ELSE\n EXECUTE format('GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT %s,SET %s GRANTED BY postgres',\n upper(self_membership->>'inherit_option'),upper(self_membership->>'set_option'));\n END IF;\n\n IF (SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') FROM pg_auth_members m) IS DISTINCT FROM before_members\n OR (SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) FROM pg_proc p WHERE pronamespace='privacy_retention'::regnamespace) IS DISTINCT FROM before_assertions\n OR (SELECT to_jsonb(n) FROM pg_namespace n WHERE n.oid='review_media_private'::regnamespace) IS DISTINCT FROM before_schema\n THEN RAISE EXCEPTION 'REVIEW_SHARED_PRESERVATION_DRIFT'; END IF;\n PERFORM pg_temp.review_shared_assert();\nEND $forward$", + "-- STORAGE_RETIREMENT_FENCE_READBACK_BEGIN\nDO $readback$\nBEGIN\n IF NOT EXISTS(SELECT 1 FROM pg_proc WHERE oid='review_media_private.guard_references()'::regprocedure\n AND proowner='privacy_workflow_owner'::regrole AND prosecdef AND proconfig=ARRAY['search_path=\"\"']::text[]\n AND encode(sha256(convert_to(prosrc,'UTF8')),'hex')='94113c8ae1231e8eae5a698f35a1370a38367e42c79cc87ac62db76c32e735b1')\n OR has_function_privilege('anon','review_media_private.guard_references()','EXECUTE')\n OR has_function_privilege('authenticated','review_media_private.guard_references()','EXECUTE')\n OR has_function_privilege('service_role','review_media_private.guard_references()','EXECUTE')\n OR has_function_privilege('postgres','review_media_private.guard_references()','EXECUTE')\n OR NOT EXISTS(SELECT 1 FROM pg_proc WHERE oid='review_media_private.lock_changes()'::regprocedure\n AND proowner='privacy_workflow_owner'::regrole AND prosecdef AND proconfig=ARRAY['search_path=\"\"']::text[]\n AND encode(sha256(convert_to(prosrc,'UTF8')),'hex')='381d89aa6bbf5a59c2ee0fef2a7392ef705147252b324527201b8b6f74376250')\n OR EXISTS(SELECT 1 FROM unnest(ARRAY['anon','authenticated','service_role','postgres']) r\n WHERE has_function_privilege(r,'review_media_private.lock_changes()','EXECUTE'))\n OR NOT EXISTS(SELECT 1 FROM pg_trigger WHERE tgrelid='storage.objects'::regclass AND tgname='a00_review_media_storage_serialize'\n AND tgenabled='O' AND NOT tgisinternal AND tgfoid='review_media_private.lock_changes()'::regprocedure AND tgtype=22 AND pg_get_triggerdef(oid)='CREATE TRIGGER a00_review_media_storage_serialize BEFORE INSERT OR UPDATE OF bucket_id, name ON storage.objects FOR EACH STATEMENT EXECUTE FUNCTION review_media_private.lock_changes()')\n OR NOT EXISTS(SELECT 1 FROM pg_trigger WHERE tgrelid='storage.objects'::regclass AND tgname='a00_review_media_storage_guard'\n AND tgenabled='O' AND NOT tgisinternal AND tgfoid='review_media_private.guard_references()'::regprocedure AND tgtype=23 AND pg_get_triggerdef(oid)='CREATE TRIGGER a00_review_media_storage_guard BEFORE INSERT OR UPDATE OF bucket_id, name ON storage.objects FOR EACH ROW EXECUTE FUNCTION review_media_private.guard_references()')\n THEN RAISE EXCEPTION 'STORAGE_RETIREMENT_FENCE_READBACK_DRIFT'; END IF;\nEND $readback$", + "-- STORAGE_RETIREMENT_FENCE_READBACK_END\nCOMMIT" + ] + }, + { + "id": "review_storage_caller_bound_admin_policy", + "version": "20261011110108", + "name": "review_storage_caller_bound_admin_policy", + "path": "backend/supabase/migrations/20261011110108_review_storage_caller_bound_admin_policy.sql", + "bytes": 8463, + "sha256": "45b2a0db333a1e24ac1f68b7693c7cfb98e10cd0ab366b98089c3679a6b5dfbc", + "statementCount": 7, + "statementVectorSha256": "02a88a7bb3c01228d2534cd0147c59f5791e42c790b53dad009ac0d442a4f0cd", + "originalStatementVector": [ + "-- Retired legacy administrator EXEC must not poison unrelated Storage writes.\n-- Admit only the two exact historical banner policies, or their absence in a\n-- fresh canonical replay. Private receipt reads remain owner-only; admin reads use the guarded server route.\n-- No public function, table grant or RPC is added.\nBEGIN", + "SET LOCAL lock_timeout='2s'", + "SET LOCAL search_path=public,extensions", + "SELECT pg_advisory_xact_lock(hashtextextended('tzudong:review-media-catalog:v2',0))", + "DO $forward$\nDECLARE item record; actual record; before_other jsonb; before_functions jsonb;\n before_members jsonb; before_manifest jsonb; before_schema jsonb;\n expression text := $predicate$(bucket_id = 'ad-banner-images' AND EXISTS (\n SELECT 1 FROM public.user_roles AS role_row\n JOIN public.user_account_status AS status_row ON status_row.user_id=role_row.user_id\n WHERE role_row.user_id=(SELECT auth.uid()) AND role_row.role::text='admin'\n AND status_row.account_status='active' AND status_row.disabled_at IS NULL))$predicate$;\nBEGIN\n IF current_user<>'postgres' OR session_user<>'postgres' OR current_setting('transaction_read_only')<>'off'\n OR current_setting('server_version_num')::integer/10000 NOT IN (15,17)\n THEN RAISE EXCEPTION 'STORAGE_ADMIN_POLICY_EXECUTOR_DENIED'; END IF;\n SELECT coalesce(jsonb_agg(to_jsonb(p) ORDER BY oid),'[]') INTO before_other FROM pg_policy p\n WHERE polrelid<>'storage.objects'::regclass OR polname NOT IN ('Admin upload access','Admin delete access','review_verifications_owner_read');\n SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) INTO before_functions FROM pg_proc p\n WHERE pronamespace IN ('public'::regnamespace,'privacy_retention'::regnamespace,'review_media_private'::regnamespace);\n SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') INTO before_members FROM pg_auth_members m;\n SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) INTO before_manifest FROM privacy_retention.g014_catalog_contract_manifest m;\n SELECT jsonb_agg(to_jsonb(n) ORDER BY oid) INTO before_schema FROM pg_namespace n;\n IF (SELECT count(*) FROM pg_policy WHERE polrelid='storage.objects'::regclass\n AND polname IN ('Admin upload access','Admin delete access')) NOT IN (0,2)\n THEN RAISE EXCEPTION 'STORAGE_ADMIN_POLICY_INVENTORY_DRIFT'; END IF;\n FOR item IN SELECT * FROM (VALUES ('Admin upload access','a'::\"char\"),('Admin delete access','d'::\"char\")) AS expected(name,cmd) LOOP\n SELECT p.*,pg_get_expr(polqual,polrelid) AS using_expr,pg_get_expr(polwithcheck,polrelid) AS check_expr INTO actual\n FROM pg_policy p WHERE polrelid='storage.objects'::regclass AND polname=item.name;\n IF NOT FOUND THEN CONTINUE; END IF;\n IF NOT actual.polpermissive OR actual.polcmd<>item.cmd OR actual.polroles<>ARRAY[0::oid]\n OR (CASE WHEN item.cmd='a' THEN actual.using_expr IS NOT NULL ELSE actual.check_expr IS NOT NULL END)\n OR coalesce(CASE WHEN item.cmd='a' THEN actual.check_expr ELSE actual.using_expr END,'') NOT IN (\n '((bucket_id = ''ad-banner-images''::text) AND is_user_admin(uid()))',\n '((bucket_id = ''ad-banner-images''::text) AND is_user_admin(auth.uid()))')\n OR (SELECT count(*) FROM pg_depend WHERE classid='pg_policy'::regclass AND objid=actual.oid\n AND refclassid='pg_proc'::regclass AND refobjid='public.is_user_admin(uuid)'::regprocedure)<>1\n OR (SELECT count(*) FROM pg_depend WHERE classid='pg_policy'::regclass AND objid=actual.oid\n AND refclassid='pg_proc'::regclass AND refobjid='auth.uid()'::regprocedure)<>1\n THEN RAISE EXCEPTION 'STORAGE_ADMIN_POLICY_SOURCE_DRIFT'; END IF;\n EXECUTE format('ALTER POLICY %I ON storage.objects TO authenticated %s (%s)',item.name,\n CASE WHEN item.cmd='a' THEN 'WITH CHECK' ELSE 'USING' END,expression);\n END LOOP;\n SELECT p.*,pg_get_expr(polqual,polrelid) AS using_expr,pg_get_expr(polwithcheck,polrelid) AS check_expr INTO actual\n FROM pg_policy p WHERE polrelid='storage.objects'::regclass AND polname='review_verifications_owner_read';\n IF NOT FOUND OR actual.polcmd<>'r' OR NOT actual.polpermissive\n OR actual.polroles<>ARRAY['authenticated'::regrole::oid] OR actual.check_expr IS NOT NULL\n OR replace(actual.using_expr,'auth.uid()','uid()') IS DISTINCT FROM\n '((bucket_id = ''review-verifications''::text) AND (((storage.foldername(name))[1] = ( SELECT (uid())::text AS uid)) OR has_role(( SELECT uid() AS uid), ''admin''::app_role)))'\n OR (SELECT count(*) FROM pg_depend WHERE classid='pg_policy'::regclass AND objid=actual.oid\n AND refclassid='pg_proc'::regclass AND refobjid='public.has_role(uuid,public.app_role)'::regprocedure)<>1\n THEN RAISE EXCEPTION 'STORAGE_OWNER_READ_SOURCE_DRIFT'; END IF;\n ALTER POLICY review_verifications_owner_read ON storage.objects TO authenticated\n USING (bucket_id='review-verifications' AND (storage.foldername(name))[1]=(SELECT auth.uid()::text));\n IF (SELECT coalesce(jsonb_agg(to_jsonb(p) ORDER BY oid),'[]') FROM pg_policy p\n WHERE polrelid<>'storage.objects'::regclass OR polname NOT IN ('Admin upload access','Admin delete access','review_verifications_owner_read')) IS DISTINCT FROM before_other\n OR (SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) FROM pg_proc p\n WHERE pronamespace IN ('public'::regnamespace,'privacy_retention'::regnamespace,'review_media_private'::regnamespace)) IS DISTINCT FROM before_functions\n OR (SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') FROM pg_auth_members m) IS DISTINCT FROM before_members\n OR (SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) FROM privacy_retention.g014_catalog_contract_manifest m) IS DISTINCT FROM before_manifest\n OR (SELECT jsonb_agg(to_jsonb(n) ORDER BY oid) FROM pg_namespace n) IS DISTINCT FROM before_schema\n THEN RAISE EXCEPTION 'STORAGE_ADMIN_POLICY_PRESERVATION_DRIFT'; END IF;\nEND $forward$", + "-- STORAGE_CALLER_BOUND_ADMIN_READBACK_BEGIN\nDO $readback$\nDECLARE item record; policy_row record; expression text;\nBEGIN\n IF EXISTS(SELECT 1 FROM pg_depend d JOIN pg_policy p ON d.classid='pg_policy'::regclass AND d.objid=p.oid\n WHERE p.polrelid='storage.objects'::regclass AND d.refclassid='pg_proc'::regclass\n AND d.refobjid IN ('public.is_user_admin(uuid)'::regprocedure,'public.has_role(uuid,public.app_role)'::regprocedure))\n OR has_function_privilege('anon','public.is_user_admin(uuid)','EXECUTE')\n OR has_function_privilege('authenticated','public.is_user_admin(uuid)','EXECUTE')\n OR has_function_privilege('service_role','public.is_user_admin(uuid)','EXECUTE')\n THEN RAISE EXCEPTION 'STORAGE_ADMIN_POLICY_READBACK_DRIFT'; END IF;\n IF NOT EXISTS(SELECT 1 FROM pg_policy WHERE polrelid='storage.objects'::regclass AND polname='review_verifications_owner_read'\n AND polcmd='r' AND polpermissive AND polroles=ARRAY['authenticated'::regrole::oid] AND polwithcheck IS NULL\n AND replace(pg_get_expr(polqual,polrelid),'auth.uid()','uid()')='((bucket_id = ''review-verifications''::text) AND ((storage.foldername(name))[1] = ( SELECT (uid())::text AS uid)))')\n THEN RAISE EXCEPTION 'STORAGE_OWNER_READ_READBACK_DRIFT'; END IF;\n FOR item IN SELECT * FROM (VALUES ('Admin upload access','a'::\"char\"),('Admin delete access','d'::\"char\")) AS expected(name,cmd) LOOP\n SELECT p.*,pg_get_expr(polqual,polrelid) AS using_expr,pg_get_expr(polwithcheck,polrelid) AS check_expr INTO policy_row\n FROM pg_policy p WHERE polrelid='storage.objects'::regclass AND polname=item.name;\n IF NOT FOUND THEN CONTINUE; END IF;\n expression:=coalesce(CASE WHEN item.cmd='a' THEN policy_row.check_expr ELSE policy_row.using_expr END,'');\n IF NOT policy_row.polpermissive OR policy_row.polcmd<>item.cmd\n OR policy_row.polroles<>ARRAY['authenticated'::regrole::oid]\n OR (CASE WHEN item.cmd='a' THEN policy_row.using_expr IS NOT NULL ELSE policy_row.check_expr IS NOT NULL END)\n OR encode(sha256(convert_to(regexp_replace(replace(expression,'auth.uid()','uid()'),'[[:space:]]+',' ','g'),'UTF8')),'hex')<>'049d579041975ddfaa0c560f56e4c57ee06099b2290f742636ba5bd9345463df'\n OR (SELECT count(*) FROM pg_depend WHERE classid='pg_policy'::regclass AND objid=policy_row.oid\n AND refclassid='pg_proc'::regclass AND refobjid='auth.uid()'::regprocedure)<>1\n THEN RAISE EXCEPTION 'STORAGE_ADMIN_POLICY_READBACK_DRIFT'; END IF;\n END LOOP;\nEND $readback$", + "-- STORAGE_CALLER_BOUND_ADMIN_READBACK_END\nCOMMIT" + ] + }, + { + "id": "review_owner_cleanup_completion", + "version": "20261011175912", + "name": "review_owner_cleanup_completion", + "path": "backend/supabase/migrations/20261011175912_review_owner_cleanup_completion.sql", + "bytes": 6694, + "sha256": "9164be35fc17e67c3f1aaf98aad13139a8fd469130bd1315879bff5dac63ba26", + "statementCount": 6, + "statementVectorSha256": "1343501bbfeac4c075fcb09f07de1a11e3b02c27d12d5091b65d72c99555269a", + "originalStatementVector": [ + "-- Count only retired, unreferenced owner cleanup, excluding live OCR staging.\n-- Forward correction preserves applied SQL, RPC metadata, ACLs and G014 rows.\nBEGIN", + "SET LOCAL lock_timeout='2s'", + "SELECT pg_advisory_xact_lock(hashtextextended('tzudong:review-media-catalog:v2',0))", + "DO $forward$\nDECLARE target oid:='public.finish_review_media_cleanup()'::regprocedure;\n definition text; source text; patched text; metadata jsonb; defaults text;\n before_members jsonb; before_assertions jsonb; before_manifest jsonb; self_membership jsonb;\n before_schema jsonb; schema_lease boolean;\n workflow_owner oid:='privacy_workflow_owner'::regrole; runner oid:='postgres'::regrole;\n major integer:=current_setting('server_version_num')::integer/10000; pg15_lease boolean:=false;\nBEGIN\n IF current_user<>'postgres' OR session_user<>'postgres' OR major NOT IN (15,17)\n OR current_setting('transaction_read_only')<>'off' OR NOT EXISTS(SELECT 1 FROM pg_roles\n WHERE oid=workflow_owner AND NOT rolsuper AND NOT rolbypassrls AND NOT rolcanlogin AND NOT rolinherit)\n THEN RAISE EXCEPTION 'REVIEW_SHARED_EXECUTOR_DENIED'; END IF;\n SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') INTO before_members FROM pg_auth_members m;\n SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) INTO before_assertions FROM pg_proc p WHERE pronamespace='privacy_retention'::regnamespace;\n SELECT pg_get_functiondef(p.oid),p.prosrc,to_jsonb(p)-ARRAY['prosrc','proargdefaults'],pg_get_expr(p.proargdefaults,0) INTO definition,source,metadata,defaults FROM pg_proc p WHERE p.oid=target;\n IF metadata->>'proowner'<>workflow_owner::text OR encode(sha256(convert_to(source,'UTF8')),'hex')<>'f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1'\n THEN RAISE EXCEPTION 'REVIEW_SHARED_SOURCE_DRIFT'; END IF;\n SELECT to_jsonb(n) INTO before_schema FROM pg_namespace n WHERE n.oid='public'::regnamespace;\n IF before_schema->>'nspowner' NOT IN (runner::text,'pg_database_owner'::regrole::oid::text)\n THEN RAISE EXCEPTION 'REVIEW_SHARED_SCHEMA_OWNER_DRIFT'; END IF;\n schema_lease:=NOT has_schema_privilege(workflow_owner,'public','CREATE');\n IF schema_lease THEN GRANT CREATE ON SCHEMA public TO privacy_workflow_owner; END IF;\n SELECT to_jsonb(m) INTO self_membership FROM pg_auth_members m WHERE roleid=workflow_owner AND member=runner AND grantor=runner;\n IF self_membership IS NOT NULL AND (self_membership->>'admin_option')::boolean THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_MEMBERSHIP_DENIED'; END IF;\n IF major=17 THEN\n IF NOT EXISTS(SELECT 1 FROM pg_auth_members WHERE roleid=workflow_owner AND member=runner AND admin_option AND grantor<>runner)\n THEN RAISE EXCEPTION 'REVIEW_MEDIA_FORWARD_OWNER_AUTHORITY_MISSING'; END IF;\n EXECUTE 'GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT FALSE,SET TRUE GRANTED BY postgres';\n ELSE\n pg15_lease:=NOT pg_has_role(runner,workflow_owner,'MEMBER');\n IF pg15_lease THEN GRANT privacy_workflow_owner TO postgres; END IF;\n END IF;\n SET LOCAL ROLE privacy_workflow_owner;\n SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) INTO before_manifest FROM privacy_retention.g014_catalog_contract_manifest m;\n\n IF (length(source)-length(replace(source,$old$AND NOT c.complete);$old$,'')))/length($old$AND NOT c.complete);$old$)<>1\n THEN RAISE EXCEPTION 'OWNER_CLEANUP_ANCHOR_DRIFT'; END IF;\n patched:=replace(source,$old$AND NOT c.complete);$old$,$new$AND c.retired AND NOT c.complete AND NOT review_media_private.referenced(c.path));$new$);\n EXECUTE replace(definition,source,patched);\n IF (SELECT to_jsonb(p)-ARRAY['prosrc','proargdefaults'] FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM metadata\n OR (SELECT pg_get_expr(p.proargdefaults,0) FROM pg_proc p WHERE p.oid=target) IS DISTINCT FROM defaults\n OR (SELECT encode(sha256(convert_to(p.prosrc,'UTF8')),'hex') FROM pg_proc p WHERE p.oid=target)<>'a6b4211b2d8486d482f0758ce249eec157249e1a5176be286b76af7e7a775ee9'\n THEN RAISE EXCEPTION 'OWNER_CLEANUP_POST_DRIFT'; END IF;\n CREATE FUNCTION pg_temp.review_shared_assert() RETURNS void LANGUAGE plpgsql SECURITY DEFINER SET search_path='' AS $bridge$\n BEGIN\n PERFORM privacy_retention.assert_g014_public_rpc_allowlist();\n PERFORM privacy_retention.assert_g014_definer_contract();\n PERFORM privacy_retention.assert_g014_catalog_contract();\n DROP FUNCTION pg_temp.review_shared_assert();\n END $bridge$;\n REVOKE ALL ON FUNCTION pg_temp.review_shared_assert() FROM PUBLIC,anon,authenticated,service_role;\n GRANT EXECUTE ON FUNCTION pg_temp.review_shared_assert() TO postgres;\n IF (SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) FROM privacy_retention.g014_catalog_contract_manifest m) IS DISTINCT FROM before_manifest\n THEN RAISE EXCEPTION 'REVIEW_SHARED_MANIFEST_DRIFT'; END IF;\n RESET ROLE;\n IF schema_lease THEN REVOKE CREATE ON SCHEMA public FROM privacy_workflow_owner; END IF;\n IF major=15 THEN\n IF pg15_lease THEN REVOKE privacy_workflow_owner FROM postgres; END IF;\n ELSIF self_membership IS NULL THEN\n REVOKE privacy_workflow_owner FROM postgres GRANTED BY postgres;\n ELSE\n EXECUTE format('GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT %s,SET %s GRANTED BY postgres',\n upper(self_membership->>'inherit_option'),upper(self_membership->>'set_option'));\n END IF;\n\n IF (SELECT coalesce(jsonb_agg(to_jsonb(m) ORDER BY roleid,member,grantor),'[]') FROM pg_auth_members m) IS DISTINCT FROM before_members\n OR (SELECT jsonb_agg(to_jsonb(p) ORDER BY oid) FROM pg_proc p WHERE pronamespace='privacy_retention'::regnamespace) IS DISTINCT FROM before_assertions\n OR (SELECT to_jsonb(n) FROM pg_namespace n WHERE n.oid='public'::regnamespace) IS DISTINCT FROM before_schema\n THEN RAISE EXCEPTION 'REVIEW_SHARED_PRESERVATION_DRIFT'; END IF;\n PERFORM pg_temp.review_shared_assert();\nEND $forward$", + "-- OWNER_CLEANUP_READBACK_BEGIN\nDO $readback$\nBEGIN\n IF NOT EXISTS(SELECT 1 FROM pg_proc WHERE oid='public.finish_review_media_cleanup()'::regprocedure\n AND proowner='privacy_workflow_owner'::regrole AND prosecdef AND proconfig=ARRAY['search_path=\"\"']::text[]\n AND encode(sha256(convert_to(prosrc,'UTF8')),'hex')='a6b4211b2d8486d482f0758ce249eec157249e1a5176be286b76af7e7a775ee9')\n OR has_function_privilege('anon','public.finish_review_media_cleanup()','EXECUTE')\n OR has_function_privilege('service_role','public.finish_review_media_cleanup()','EXECUTE')\n OR NOT has_function_privilege('authenticated','public.finish_review_media_cleanup()','EXECUTE')\n THEN RAISE EXCEPTION 'OWNER_CLEANUP_READBACK_DRIFT'; END IF;\nEND $readback$", + "-- OWNER_CLEANUP_READBACK_END\nCOMMIT" + ] + } + ], + "legacyReleaseManifest": { + "path": ".github/supabase-migration-release-manifest.v1.json", + "entries": 3, + "sha256": "515743d094b4b431a29df772a363837bdad8f7541aa3acf4a923efb79f460c0d" + }, + "toolchain": [ + { + "path": "apps/web/scripts/review-media-sql-successor.mjs", + "sha256": "928d7b8bd601ab585870d4e04ee468e952f6053df81e403de925e032924f40d1" + }, + { + "path": "apps/web/scripts/supabase-migration-bundle.mjs", + "sha256": "a374211be376e3a9af8215751accb52eb43943fc93b8651ba4e186e412588ba7" + }, + { + "path": "apps/web/scripts/supabase-migration-transaction.mjs", + "sha256": "c67bdff4c63b1bf1b4a157863b3eb5253b5973a8fb54b7fd49351270787bed26" + }, + { + "path": "apps/web/scripts/apply-supabase-migration.mjs", + "sha256": "02c404e0be3e81b7c8d4d7766ff29e750b1797f5ad49b5f496168170e21192d4" + }, + { + "path": "apps/web/scripts/vercel-rollback-readback.mjs", + "sha256": "fa3876153f0e949d23115612cb34bc545c6c29bccb6ba2fd6042a38e70555e04" + }, + { + "path": "backend/supabase/scripts/g037_supabase_statement_vector.mjs", + "sha256": "398e3945c0d0fb656daef0d0a42409dbdeb45a9bb1f6f8c03445e4436d4db0bd" + } + ], + "requiredWriterFences": { + "adminRecordMutationsHold": "active", + "g037WriteFreeze": "active", + "automaticReviewer": "stopped", + "browserRpc": "stopped", + "directSql": "stopped", + "dashboard": "stopped", + "providerIngress": "stopped" + } +} diff --git a/apps/web/performance/review-db-successor-20261012/README.md b/apps/web/performance/review-db-successor-20261012/README.md new file mode 100644 index 0000000000..fb7c8bdf65 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/README.md @@ -0,0 +1,43 @@ +# Review media SQL successor: local PG17 evidence + +This directory contains local-only evidence for the held 11-stage review-media +successor. It does not authorize or prove a hosted database write. + +The harness clones the owned, schema-only `ranking_clone` template into the +fixed database `review_successor_local_20261012_v1`, refuses an existing target, +and drops that database before writing the proof. The template contains zero +profiles and zero reviews. Six already-applied predecessor sources reconstruct +the relevant pre-state; 86 synthetic ledger rows model the observed predecessor +cardinality without claiming hosted parity. This harness does not read or prove +the hosted ledger. A hosted attempt must freshly confirm that the actual ledger +is still exactly 86 and capture all 12 hosted stage states inside the admitted +window; this local proof cannot be reused as that rehearsal receipt. + +Run with Node 24: + +```sh +/opt/homebrew/opt/node@24/bin/node \ + apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +``` + +The checked proof records 11 migrations, 112 source statements, exact stage +roots, ledger transition 86 to 97, atomic success, forced-failure rollback, +wrong `finish_review_media_cleanup` preimage rejection, post-lock admission +expiry rejection under real advisory-lock contention, and independent catalog +drift rejection for view `security_invoker`, column ACLs, and PostgreSQL 17 role +membership options. Every rejection preserves the pre-apply database state, and +the temporary database and local fixture roles are removed before proof output. +The compiled transaction is 6,492,122 bytes. Two complete runs produced the +same proof SHA-256, `6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f`. + +The Storage root deliberately excludes bucket creation and update timestamps so +the same security and behavior configuration has a stable digest across an +independent replay. It still binds the bucket identity, visibility, ownership, +limits, MIME types, type, versioning, and lifecycle fields. Therefore this proof +does **not** establish preservation of timestamp-only changes or distinguish a +bucket that was deleted and recreated with identical included settings. + +The production manifest remains `held`. Hosted execution still requires a +protected-main source receipt, fresh 12-state rehearsal receipt, current Vercel +rollback readback, all seven writer-fence receipts, a private admission file, +and a new journal path. diff --git a/apps/web/performance/review-db-successor-20261012/SHA256SUMS b/apps/web/performance/review-db-successor-20261012/SHA256SUMS new file mode 100644 index 0000000000..1e6628b406 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/SHA256SUMS @@ -0,0 +1,6 @@ +9fac81d6e3ec81207b9c8985ccfdaf9e300bb7d44a7617d95579d4e8f96af19b .github/review-media-sql-successor.v1.json +928d7b8bd601ab585870d4e04ee468e952f6053df81e403de925e032924f40d1 apps/web/scripts/review-media-sql-successor.mjs +a374211be376e3a9af8215751accb52eb43943fc93b8651ba4e186e412588ba7 apps/web/scripts/supabase-migration-bundle.mjs +03e999caeda7b57925126ba8d6bcec4ac5a671fedb2c69e07e988924ccb9f895 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f apps/web/performance/review-db-successor-20261012/pg17-local-replay.json +05176617c49dbf5343b0b9d0587dbf41ffa32e6f9497321517474fbb347c373e apps/web/performance/review-db-successor-20261012/README.md diff --git a/apps/web/performance/review-db-successor-20261012/pg17-local-replay.json b/apps/web/performance/review-db-successor-20261012/pg17-local-replay.json new file mode 100644 index 0000000000..66fadc8fda --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/pg17-local-replay.json @@ -0,0 +1,247 @@ +{ + "schemaVersion": 1, + "status": "passed", + "environment": { + "dockerContext": "colima-tzudong-catalog-20261007", + "dockerContainer": "tzudong-ranking-clone-20261008", + "engine": "17.6", + "templateDatabase": "ranking_clone", + "temporaryDatabase": "review_successor_local_20261012_v1" + }, + "input": { + "engine": "17.6", + "ledger": 0, + "manifest": 1963, + "profiles": 0, + "reviews": 0 + }, + "manifestSha256": "9fac81d6e3ec81207b9c8985ccfdaf9e300bb7d44a7617d95579d4e8f96af19b", + "rehearsalHarnessSha256": "03e999caeda7b57925126ba8d6bcec4ac5a671fedb2c69e07e988924ccb9f895", + "sourceRoot": "61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544", + "predecessorSources": [ + { + "path": "backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql", + "sha256": "b373b7ea472c0352a33a4d4043cf8d6aa8474c04cc1ca5778805edfa77ac9c95" + }, + { + "path": "backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql", + "sha256": "66eace1d6fb0dd55c1d780776bab855cc37690335ad40b0fdc1294c610e07d3a" + }, + { + "path": "backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql", + "sha256": "e1c105df82c4f814d3e6adad807ff9d072b8cd42ae770b4b78f75b89a9387020" + }, + { + "path": "backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql", + "sha256": "8acf6d1428764260ed57dac5fe09711868a82896f0a6d631d502128004c168a3" + }, + { + "path": "backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql", + "sha256": "b596b200e52c6813a4cfa1b0a2818625f067864549e8854f3497afdcdab706da" + }, + { + "path": "backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql", + "sha256": "2067538f89c9f90d28e784672c7a1288306ba22d5ae92b087c8692503da9b1ae" + } + ], + "migrationCount": 11, + "statementCount": 112, + "stageStatesSha256": "9b6ab856ce82bfba9b8eb52fdf40943eac645b5d52af6ea9c4c25bcd4b82ad23", + "stages": [ + { + "index": 0, + "ledgerCount": 86, + "targetCount": 0, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": null, + "schemaRoot": "70ef24069d2f6ceeec28db7f715b2bc1e1830e51007df5f97476a64f7583c2be", + "ledgerRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "g014Root": "4f3886411c6329ec0a6a61743e7e89de21cf519848bc145be2f3e96f1e685149", + "storageRoot": "2e4b000d1012f1df1e6e6fd3e19cc66e8718a5be1d8cdc3695677bd2cc3bbda8", + "finishCleanupBodySha256": null, + "ownerAuthorityReady": true, + "terminalContractReady": false + }, + { + "index": 1, + "ledgerCount": 87, + "targetCount": 1, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": null, + "schemaRoot": "6660c658b956479a86b85dc5f6c257bdfde7b94d1e6e7e675b989303bc776de4", + "ledgerRoot": "9dff2943b2fb957d82b7ac325a999fef829c077e5738f178e5bc19da67609701", + "g014Root": "4f3886411c6329ec0a6a61743e7e89de21cf519848bc145be2f3e96f1e685149", + "storageRoot": "145e14b4467ccabbbd0d1009df236b3cc734174a9b287811a55a29058257111f", + "finishCleanupBodySha256": "1a0eb803e2f2ee4098960ad2fbe2b99b363405536c03a09c0465b8a8ffddecc7", + "ownerAuthorityReady": true, + "terminalContractReady": false + }, + { + "index": 2, + "ledgerCount": 88, + "targetCount": 2, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": null, + "schemaRoot": "7574cc0bd42e93dd1112e9068b6538fc2401b5de8d7de3e7ce40762405abbe35", + "ledgerRoot": "5f3dcac3e6820c2571b2f04221cbdf34ee18085363c434605c445a76ea59061a", + "g014Root": "4f3886411c6329ec0a6a61743e7e89de21cf519848bc145be2f3e96f1e685149", + "storageRoot": "98ad8115175e671fce1f853cb48b2e1bb7e430892107fd333bc74df24799cdea", + "finishCleanupBodySha256": "f7e925a25dedb987a3166c5ca641ba7db1568a7583bdeca3cee04f27d9189f20", + "ownerAuthorityReady": true, + "terminalContractReady": false + }, + { + "index": 3, + "ledgerCount": 89, + "targetCount": 3, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": null, + "schemaRoot": "fe8ca8917b834397b49253154bed7074372bc403ffc2a0e06037a650c7cadfb2", + "ledgerRoot": "928e46ee03f4f2ed492be89617cf8b455f2931f79fdf3c218adbc92c5fa3d21e", + "g014Root": "810c5888b257c16ea07881e99914a718eede2e2339cbd805381fe7bf346aef34", + "storageRoot": "4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f", + "finishCleanupBodySha256": "f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1", + "ownerAuthorityReady": true, + "terminalContractReady": false + }, + { + "index": 4, + "ledgerCount": 90, + "targetCount": 4, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": null, + "schemaRoot": "2085388afbef4445f615ee40e5e00837c526e2f29a2e1c3e0786afe8a569fb22", + "ledgerRoot": "73b30ad98e4cbace156815cd214091312364302e9403065b24a2d1e86227a2c0", + "g014Root": "68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00", + "storageRoot": "4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f", + "finishCleanupBodySha256": "f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1", + "ownerAuthorityReady": true, + "terminalContractReady": false + }, + { + "index": 5, + "ledgerCount": 91, + "targetCount": 5, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": null, + "schemaRoot": "0f3f4ed4b7547cc154e5beaeb2621db1c05ff0d4dc7325c47246dcfd4be6fbc5", + "ledgerRoot": "150bb9f1c5a4b447dd50a86097923fb1c78e522fb5f2bf8ca16b9f08ca6d2b58", + "g014Root": "68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00", + "storageRoot": "4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f", + "finishCleanupBodySha256": "f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1", + "ownerAuthorityReady": true, + "terminalContractReady": false + }, + { + "index": 6, + "ledgerCount": 92, + "targetCount": 6, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": null, + "schemaRoot": "8aa69a0aa43e60ab1b6680ebb7b3fb4e4f53b07aa8231847883a181375554690", + "ledgerRoot": "f57304a9d3122d643535464cf72565aa04c450dbc1d8fe0a3a64add17b5c7c97", + "g014Root": "68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00", + "storageRoot": "4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f", + "finishCleanupBodySha256": "f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1", + "ownerAuthorityReady": true, + "terminalContractReady": false + }, + { + "index": 7, + "ledgerCount": 93, + "targetCount": 7, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": null, + "schemaRoot": "cee1ac952c3bd3aa8fff26a625a4140c5ce2028a08a8510eecdc1e760aaf5166", + "ledgerRoot": "3da3b76460e0ad7249d50a1f4ddf0cb433edd0ca7b3b6b7a8f6e334974888065", + "g014Root": "68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00", + "storageRoot": "4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f", + "finishCleanupBodySha256": "f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1", + "ownerAuthorityReady": true, + "terminalContractReady": false + }, + { + "index": 8, + "ledgerCount": 94, + "targetCount": 8, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": null, + "schemaRoot": "ab7f4f4d84c2d759d570669641131f149db2a1a023765fb468e04d7f162348be", + "ledgerRoot": "c8920bba0db232a705ddf012ee5da809df71d72e3ddfb3f2fbc91d156b87cca2", + "g014Root": "68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00", + "storageRoot": "4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f", + "finishCleanupBodySha256": "f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1", + "ownerAuthorityReady": true, + "terminalContractReady": false + }, + { + "index": 9, + "ledgerCount": 95, + "targetCount": 9, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": null, + "schemaRoot": "7409744a943050706edaf080c59b069c10bb3b471296728991c589b2ce3d18bb", + "ledgerRoot": "b6f0aa150d0a5177828767ca5ad31796f6ae4cc034fcd5c943e15cea3d876a9b", + "g014Root": "68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00", + "storageRoot": "4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f", + "finishCleanupBodySha256": "f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1", + "ownerAuthorityReady": true, + "terminalContractReady": false + }, + { + "index": 10, + "ledgerCount": 96, + "targetCount": 10, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": null, + "schemaRoot": "284d8a28560707a02167e6f9131c2ca4e0658808ace9061b5251d8ff12a236cd", + "ledgerRoot": "e56731bbe2c7bbdfa826e594d468082c9f16354350f66325e7c58cfdc549fe1a", + "g014Root": "68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00", + "storageRoot": "6bf06a15d9ef5cc3abbe210657b6a21b3b50f18e0dde58bea9af5ba958b46a60", + "finishCleanupBodySha256": "f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1", + "ownerAuthorityReady": true, + "terminalContractReady": false + }, + { + "index": 11, + "ledgerCount": 97, + "targetCount": 11, + "prefixRoot": "c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2", + "suffixRoot": "1ecd299cb2525a1d66738d2c0adf01a482e47c44f881f6d09ddc2141d469d32b", + "schemaRoot": "a255fb9f415627e139762649c4e4230c4f6c56e541be3a09d9f986120287ea7e", + "ledgerRoot": "fa142da48d834923b4702a302d44ea0f9818d7713da76a316d5959b8852065f2", + "g014Root": "68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00", + "storageRoot": "6bf06a15d9ef5cc3abbe210657b6a21b3b50f18e0dde58bea9af5ba958b46a60", + "finishCleanupBodySha256": "a6b4211b2d8486d482f0758ce249eec157249e1a5176be286b76af7e7a775ee9", + "ownerAuthorityReady": true, + "terminalContractReady": true + } + ], + "results": { + "advisoryContentionObserved": true, + "atomicCommit": true, + "atomicRollbackExact": true, + "catalogDriftStatePreserved": true, + "columnAclDriftRejected": true, + "compiledSqlBytes": 6492122, + "compiledSqlSha256": "706084abff22426a012f11ccbc08f7b1bb7d7c16b2f961cf2d04ac2b7914e142", + "contendedAdmissionExpiryRejected": true, + "contendedAdmissionExpiryStatePreserved": true, + "membershipOptionsBaseline": { + "inherit": true, + "set": true + }, + "membershipInheritOptionDriftRejected": true, + "membershipSetOptionDriftRejected": true, + "securityInvokerBaseline": true, + "securityInvokerDriftRejected": true, + "wrongFinishPreimageRejected": true, + "wrongFinishPreimageRollbackExact": true, + "terminalLedgerCount": 97, + "terminalContractReady": true + }, + "sourceMutation": false, + "hostedWrites": false, + "operatingWrites": false, + "temporaryDatabaseDropped": true +} diff --git a/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs new file mode 100644 index 0000000000..f8cd1ff120 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs @@ -0,0 +1,622 @@ +#!/usr/bin/env node + +/** + * Local-only PostgreSQL 17.6 rehearsal for the held eleven-stage successor. + * + * The harness clones the existing schema-only ranking_clone template into one + * fixed, owned database name. It refuses a reused target, never connects to a + * hosted endpoint, and drops the owned database before writing its proof. + */ +import { createHash } from 'node:crypto'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawn, spawnSync } from 'node:child_process'; + +import { + buildSuccessorStateQuery, + compileSuccessorPlan, + loadSuccessorManifest, +} from '../../scripts/review-media-sql-successor.mjs'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPOSITORY_ROOT = resolve(HERE, '../../../..'); +const OUTPUT_PATH = resolve(HERE, 'pg17-local-replay.json'); +const DOCKER_CONTEXT = process.env.TZUDONG_REHEARSAL_DOCKER_CONTEXT || 'colima-tzudong-catalog-20261007'; +const DOCKER_CONTAINER = process.env.TZUDONG_REHEARSAL_DOCKER_CONTAINER || 'tzudong-ranking-clone-20261008'; +const TEMPLATE_DATABASE = 'ranking_clone'; +const DATABASE = 'review_successor_local_20261012_v1'; +const FIXTURE_PASSWORD = 'fixture-only'; +const FIXTURE_PARENT_ROLE = 'review_successor_fixture_parent_20261012'; +const FIXTURE_MEMBER_ROLE = 'review_successor_fixture_member_20261012'; +const EXPIRY_APPLICATION_NAME = 'tzudong-review-successor-expiry-fixture'; +const PREDECESSOR_PATHS = Object.freeze([ + 'backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql', + 'backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql', + 'backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql', + 'backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql', + 'backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql', + 'backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql', +]); +const sha256 = value => createHash('sha256').update(value).digest('hex'); +const quote = value => `'${String(value).replaceAll("'", "''")}'`; +const canonical = value => Array.isArray(value) + ? value.map(canonical) + : value && typeof value === 'object' + ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) + : value; +const equal = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right)); + +function fail(code, detail = '') { + throw new Error(detail ? `${code}:${detail}` : code); +} + +function docker(args, { input, allowFailure = false } = {}) { + const result = spawnSync('docker', ['--context', DOCKER_CONTEXT, ...args], { + encoding: 'utf8', + input, + maxBuffer: 64 * 1024 * 1024, + }); + if (!allowFailure && (result.error || result.status !== 0)) { + const sqlstates = [...(result.stderr || '').matchAll(/(?:ERROR|FATAL):\s+([0-9A-Z]{5}):/g)] + .map(match => match[1]); + const diagnostic = (result.stderr || '').split(/\r?\n/) + .find(line => /(?:ERROR|FATAL):/.test(line)) + ?.replace(/\s+/g, ' ').slice(0, 240) || ''; + fail('LOCAL_REHEARSAL_COMMAND_FAILED', [sqlstates.join(',') || String(result.status), diagnostic] + .filter(Boolean).join(':')); + } + return result; +} + +function psql(database, sql, { actor = 'postgres', singleTransaction = false, allowFailure = false } = {}) { + const options = [ + 'exec', '-i', '-e', `PGPASSWORD=${FIXTURE_PASSWORD}`, DOCKER_CONTAINER, + 'psql', '-XAtq', '-v', 'ON_ERROR_STOP=1', '-v', 'VERBOSITY=terse', + ]; + if (singleTransaction) options.push('--single-transaction'); + options.push('-h', '127.0.0.1', '-U', actor, '-d', database, '--file=-'); + return docker(options, { input: sql, allowFailure }); +} + +function scalar(database, sql, actor = 'postgres') { + return psql(database, `${sql}\n`, { actor }).stdout.trim(); +} + +function readState(database, stateQuery) { + const output = scalar(database, `${stateQuery};`); + const lines = output.split(/\r?\n/).filter(Boolean); + if (lines.length !== 1) fail('LOCAL_REHEARSAL_STATE_SHAPE_INVALID', String(lines.length)); + try { + return JSON.parse(lines[0]); + } catch { + fail('LOCAL_REHEARSAL_STATE_JSON_INVALID'); + } +} + +function ledgerInsert(migration) { + const vector = JSON.stringify(migration.originalStatementVector); + const tag = `$vector_${sha256(vector).slice(0, 20)}$`; + return `INSERT INTO supabase_migrations.schema_migrations(version,statements,name) +SELECT ${quote(migration.version)},ARRAY(SELECT jsonb_array_elements_text(${tag}${vector}${tag}::jsonb)),${quote(migration.name)};`; +} + +function fixtureSql() { + return ` +DO $fixture$ +BEGIN + IF (SELECT count(*) FROM supabase_migrations.schema_migrations)<>0 THEN + RAISE EXCEPTION 'LOCAL_REHEARSAL_LEDGER_NOT_EMPTY'; + END IF; + IF (SELECT count(*) FROM public.profiles)<>0 OR (SELECT count(*) FROM public.reviews)<>0 THEN + RAISE EXCEPTION 'LOCAL_REHEARSAL_PERSONAL_ROWS_PRESENT'; + END IF; +END $fixture$; +INSERT INTO supabase_migrations.schema_migrations(version,statements,name) +SELECT lpad(value::text,14,'0'),ARRAY['local schema-only predecessor fixture'],format('local_predecessor_%s',value) +FROM generate_series(1,86) value; +INSERT INTO pipeline_control.restaurant_review_policy(singleton,enabled) +VALUES(true,false) +ON CONFLICT(singleton) DO UPDATE SET enabled=false,operator_id=NULL,last_run_at=NULL; +INSERT INTO pipeline_control.admin_evaluation_catalog_revision(singleton,revision) +VALUES(true,1) +ON CONFLICT(singleton) DO NOTHING; +`; +} + +function createOwnedDatabase() { + const exists = scalar('postgres', `SELECT count(*) FROM pg_database WHERE datname=${quote(DATABASE)};`, 'supabase_admin'); + if (exists !== '0') fail('LOCAL_REHEARSAL_TARGET_ALREADY_EXISTS'); + psql('postgres', `CREATE DATABASE ${DATABASE} TEMPLATE ${TEMPLATE_DATABASE} OWNER postgres;`, { actor: 'supabase_admin' }); + PREDECESSOR_PATHS.forEach((path, index) => { + try { + psql(DATABASE, readFileSync(resolve(REPOSITORY_ROOT, path))); + } catch (error) { + fail('LOCAL_REHEARSAL_PREDECESSOR_FAILED', `${index + 1}:${error.message}`); + } + }); + psql(DATABASE, fixtureSql()); +} + +function dropOwnedDatabase() { + const exists = scalar('postgres', `SELECT count(*) FROM pg_database WHERE datname=${quote(DATABASE)};`, 'supabase_admin'); + if (exists === '1') { + psql('postgres', `DROP DATABASE ${DATABASE} WITH (FORCE);`, { actor: 'supabase_admin' }); + } else if (exists !== '0') { + fail('LOCAL_REHEARSAL_TARGET_CARDINALITY_INVALID'); + } +} + +function resetOwnedDatabase() { + dropOwnedDatabase(); + createOwnedDatabase(); +} + +function createFixtureMembership() { + const existing = scalar('postgres', `SELECT count(*) FROM pg_roles WHERE rolname IN(${quote(FIXTURE_PARENT_ROLE)},${quote(FIXTURE_MEMBER_ROLE)});`, 'supabase_admin'); + if (existing !== '0') fail('LOCAL_REHEARSAL_FIXTURE_ROLE_EXISTS'); + psql('postgres', ` +BEGIN; +CREATE ROLE ${FIXTURE_PARENT_ROLE} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS; +CREATE ROLE ${FIXTURE_MEMBER_ROLE} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOREPLICATION NOBYPASSRLS; +GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} + WITH ADMIN FALSE, INHERIT TRUE, SET TRUE GRANTED BY supabase_admin; +COMMIT; +`, { actor: 'supabase_admin' }); +} + +function dropFixtureMembership() { + const existing = scalar('postgres', `SELECT count(*) FROM pg_roles WHERE rolname IN(${quote(FIXTURE_PARENT_ROLE)},${quote(FIXTURE_MEMBER_ROLE)});`, 'supabase_admin'); + if (existing === '0') return; + if (existing !== '2') fail('LOCAL_REHEARSAL_FIXTURE_ROLE_CARDINALITY_INVALID'); + psql('postgres', ` +BEGIN; +REVOKE ${FIXTURE_PARENT_ROLE} FROM ${FIXTURE_MEMBER_ROLE} GRANTED BY supabase_admin; +DROP ROLE ${FIXTURE_MEMBER_ROLE}; +DROP ROLE ${FIXTURE_PARENT_ROLE}; +COMMIT; +`, { actor: 'supabase_admin' }); +} + +function applyRawMigration(record, index) { + const migration = record.manifest.migrations[index]; + const material = record.materials[index]; + try { + psql(DATABASE, material.bytes); + psql(DATABASE, `${ledgerInsert(migration)}\n`); + } catch (error) { + fail('LOCAL_REHEARSAL_MIGRATION_FAILED', `${index + 1}:${error.message}`); + } +} + +function collectStageStates(record, stateQuery) { + const states = [readState(DATABASE, stateQuery)]; + for (let index = 0; index < record.manifest.migrations.length; index += 1) { + applyRawMigration(record, index); + states.push(readState(DATABASE, stateQuery)); + } + return states; +} + +function summarizeStage(stage, index) { + return { + index, + ledgerCount: stage.bundle.ledgerCount, + targetCount: stage.bundle.targetCount, + prefixRoot: stage.bundle.prefixRoot, + suffixRoot: stage.bundle.suffixRoot, + schemaRoot: stage.state.schemaRoot, + ledgerRoot: stage.state.ledgerRoot, + g014Root: stage.state.g014Root, + storageRoot: stage.state.storageRoot, + finishCleanupBodySha256: stage.state.finishCleanupBodySha256, + ownerAuthorityReady: stage.state.ownerAuthorityReady, + terminalContractReady: stage.state.terminalContractReady, + }; +} + +function assertRehearsalStages(record, stages) { + const { manifest } = record; + if (stages.length !== 12) fail('LOCAL_REHEARSAL_STAGE_COUNT_INVALID'); + const prefixRoot = stages[0].bundle.prefixRoot; + stages.forEach((stage, index) => { + if (stage.bundle.ledgerCount !== 86 + index + || stage.bundle.priorCount !== 86 + || stage.bundle.prefixRoot !== prefixRoot + || stage.bundle.targetCount !== index + || stage.state.activeReviewItems !== 0 + || stage.state.reviewItems !== 0 + || stage.state.reviewRuns !== 0 + || stage.state.reviewPolicyEnabled !== false + || stage.state.currentUser !== 'postgres' + || stage.state.sessionUser !== 'postgres' + || stage.state.databaseName !== DATABASE + || stage.state.serverMajor !== 17 + || stage.state.ownerAuthorityReady !== true + || stage.state.terminalContractReady !== (index === 11)) { + fail('LOCAL_REHEARSAL_STAGE_INVALID', String(index)); + } + }); + if (stages[10].state.finishCleanupBodySha256 !== manifest.terminalContract.finishPreimageSha256 + || stages[11].state.finishCleanupBodySha256 !== manifest.terminalContract.finishPostimageSha256) { + fail('LOCAL_REHEARSAL_FINISH_PREIMAGE_INVALID'); + } + if (!equal(stages.slice(0, 3).map(stage => stage.state.g014Root), Array(3).fill(stages[0].state.g014Root)) + || !equal(stages.slice(4).map(stage => stage.state.g014Root), Array(8).fill(stages[4].state.g014Root))) { + fail('LOCAL_REHEARSAL_G014_TRANSITION_INVALID'); + } +} + +function tamperFinishCleanupPreimage() { + psql(DATABASE, ` +BEGIN; +DO $guard$ BEGIN + IF EXISTS(SELECT 1 FROM pg_auth_members + WHERE roleid='privacy_workflow_owner'::regrole AND member='postgres'::regrole AND grantor='postgres'::regrole) + THEN RAISE EXCEPTION 'LOCAL_REHEARSAL_SELF_GRANT_EXISTS'; END IF; +END $guard$; +GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT FALSE,SET TRUE GRANTED BY postgres; +SET LOCAL ROLE privacy_workflow_owner; +DO $tamper$ +DECLARE definition text; source text; +BEGIN + SELECT pg_get_functiondef(oid),prosrc INTO definition,source + FROM pg_proc WHERE oid='public.finish_review_media_cleanup()'::regprocedure; + EXECUTE replace(definition,source,source || E'\\n-- local-only preimage drift'); +END $tamper$; +RESET ROLE; +REVOKE privacy_workflow_owner FROM postgres GRANTED BY postgres; +COMMIT; +`); +} + +function assertCatalogDriftRejected({ label, plan, stages, stateQuery, tamperSql, restoreSql = null }) { + resetOwnedDatabase(); + const baseline = readState(DATABASE, stateQuery); + if (!equal(baseline, stages[0])) fail('LOCAL_REHEARSAL_DRIFT_BASELINE_INVALID', label); + psql(DATABASE, tamperSql, { actor: 'supabase_admin' }); + const driftedBefore = readState(DATABASE, stateQuery); + if (driftedBefore.state.schemaRoot === baseline.state.schemaRoot + || driftedBefore.state.g014Root !== baseline.state.g014Root + || driftedBefore.state.storageRoot !== baseline.state.storageRoot + || driftedBefore.state.ledgerRoot !== baseline.state.ledgerRoot) { + fail('LOCAL_REHEARSAL_DRIFT_PROJECTION_INVALID', label); + } + const rejected = psql(DATABASE, plan.sql, { singleTransaction: true, allowFailure: true }); + if (rejected.status === 0 || !/MIGRATION_PRIOR_STATE_MISMATCH/.test(rejected.stderr)) { + fail('LOCAL_REHEARSAL_CATALOG_DRIFT_NOT_REJECTED', label); + } + const driftedAfter = readState(DATABASE, stateQuery); + if (!equal(driftedBefore, driftedAfter) + || driftedAfter.bundle.targetCount !== 0 + || driftedAfter.bundle.ledgerCount !== 86) { + fail('LOCAL_REHEARSAL_CATALOG_DRIFT_STATE_CHANGED', label); + } + if (restoreSql) { + psql(DATABASE, restoreSql, { actor: 'supabase_admin' }); + if (!equal(readState(DATABASE, stateQuery), baseline)) { + fail('LOCAL_REHEARSAL_CATALOG_DRIFT_RESTORE_FAILED', label); + } + } + return true; +} + +function startAdvisoryLockHolder() { + const args = [ + '--context', DOCKER_CONTEXT, + 'exec', '-i', '-e', `PGPASSWORD=${FIXTURE_PASSWORD}`, DOCKER_CONTAINER, + 'psql', '-XAtq', '-v', 'ON_ERROR_STOP=1', '-h', '127.0.0.1', '-U', 'postgres', '-d', DATABASE, + ]; + const child = spawn('docker', args, { stdio: ['pipe', 'pipe', 'pipe'] }); + let stdout = ''; + let stderr = ''; + let settled = false; + const ready = new Promise((resolveReady, rejectReady) => { + const reject = detail => { + if (!settled) { + settled = true; + rejectReady(new Error(`LOCAL_REHEARSAL_LOCK_HOLDER_FAILED:${detail}`)); + } + }; + child.on('error', error => reject(error.code || 'SPAWN')); + child.stdout.on('data', chunk => { + stdout += chunk.toString(); + if (!settled && stdout.includes('LOCK_HELD')) { + settled = true; + resolveReady(); + } + }); + child.stderr.on('data', chunk => { stderr += chunk.toString(); }); + child.on('exit', code => { + if (!settled) reject(`${code}:${stderr.replace(/\s+/g, ' ').slice(0, 120)}`); + }); + }); + const completion = new Promise((resolveCompletion, rejectCompletion) => { + child.on('error', rejectCompletion); + child.on('close', code => { + if (code === 0) resolveCompletion(); + else rejectCompletion(new Error(`LOCAL_REHEARSAL_LOCK_HOLDER_FAILED:${code}:${stderr.replace(/\s+/g, ' ').slice(0, 120)}`)); + }); + }); + child.stdin.end(`BEGIN;\nSELECT pg_advisory_xact_lock(7311754282260211::bigint);\nSELECT 'LOCK_HELD';\nSELECT pg_sleep(1.55);\nCOMMIT;\n`); + return { child, completion, ready }; +} + +function startPsqlProcess(database, sql, { applicationName, singleTransaction = false } = {}) { + const args = [ + '--context', DOCKER_CONTEXT, + 'exec', '-i', '-e', `PGPASSWORD=${FIXTURE_PASSWORD}`, + ]; + if (applicationName) args.push('-e', `PGAPPNAME=${applicationName}`); + args.push( + DOCKER_CONTAINER, + 'psql', '-XAtq', '-v', 'ON_ERROR_STOP=1', '-v', 'VERBOSITY=terse', + ); + if (singleTransaction) args.push('--single-transaction'); + args.push('-h', '127.0.0.1', '-U', 'postgres', '-d', database, '--file=-'); + const child = spawn('docker', args, { stdio: ['pipe', 'pipe', 'pipe'] }); + let stdout = ''; + let stderr = ''; + let inputError = null; + child.stdout.on('data', chunk => { stdout += chunk.toString(); }); + child.stderr.on('data', chunk => { stderr += chunk.toString(); }); + child.stdin.on('error', error => { + if (error.code !== 'EPIPE') inputError = error; + }); + const completion = new Promise((resolveCompletion, rejectCompletion) => { + child.on('error', rejectCompletion); + child.on('close', status => { + if (inputError) rejectCompletion(inputError); + else resolveCompletion({ status, stderr, stdout }); + }); + }); + child.stdin.end(sql); + return { child, completion }; +} + +async function waitForAdvisoryContention(expiresAt) { + const deadline = Math.min(expiresAt - 25, Date.now() + 700); + while (Date.now() < deadline) { + const waiting = scalar(DATABASE, `SELECT count(*) FROM pg_stat_activity a + JOIN pg_locks l ON l.pid=a.pid + WHERE a.application_name=${quote(EXPIRY_APPLICATION_NAME)} + AND l.locktype='advisory' AND NOT l.granted;`, 'supabase_admin'); + if (waiting === '1') return Date.now(); + if (waiting !== '0') fail('LOCAL_REHEARSAL_CONTENTION_CARDINALITY_INVALID'); + await new Promise(resolveDelay => setTimeout(resolveDelay, 20)); + } + fail('LOCAL_REHEARSAL_ADVISORY_CONTENTION_NOT_OBSERVED'); +} + +async function assertContendedExpiryRejected(record, stages, stateQuery) { + resetOwnedDatabase(); + const baseline = readState(DATABASE, stateQuery); + if (!equal(baseline, stages[0])) fail('LOCAL_REHEARSAL_EXPIRY_BASELINE_INVALID'); + const expiresAt = Date.now() + 1500; + const expiry = new Date(expiresAt).toISOString(); + const expiringPlan = compileSuccessorPlan(record, { expiresAt: expiry, stageStates: stages }); + const holder = startAdvisoryLockHolder(); + let apply = null; + try { + await holder.ready; + if (Date.now() >= expiresAt) fail('LOCAL_REHEARSAL_EXPIRY_WINDOW_TOO_SHORT'); + apply = startPsqlProcess(DATABASE, expiringPlan.sql, { + applicationName: EXPIRY_APPLICATION_NAME, + singleTransaction: true, + }); + const contentionObservedAt = await waitForAdvisoryContention(expiresAt); + if (contentionObservedAt >= expiresAt) fail('LOCAL_REHEARSAL_CONTENTION_OBSERVED_AFTER_EXPIRY'); + const [rejected] = await Promise.all([apply.completion, holder.completion]); + if (rejected.status === 0 + || !/SUCCESSOR_ADMISSION_EXPIRED/.test(rejected.stderr) + || /lock timeout/.test(rejected.stderr)) { + fail('LOCAL_REHEARSAL_CONTENDED_EXPIRY_NOT_REJECTED'); + } + } finally { + if (apply?.child.exitCode === null) apply.child.kill('SIGTERM'); + if (holder.child.exitCode === null) holder.child.kill('SIGTERM'); + } + const after = readState(DATABASE, stateQuery); + if (!equal(after, baseline) || after.bundle.targetCount !== 0 || after.bundle.ledgerCount !== 86) { + fail('LOCAL_REHEARSAL_CONTENDED_EXPIRY_STATE_CHANGED'); + } + return true; +} + +async function run() { + if (existsSync(OUTPUT_PATH)) fail('LOCAL_REHEARSAL_PROOF_EXISTS'); + const manifestBytes = readFileSync(resolve(REPOSITORY_ROOT, '.github/review-media-sql-successor.v1.json')); + const record = loadSuccessorManifest(); + const stateQuery = buildSuccessorStateQuery(record.manifest); + const template = JSON.parse(scalar(TEMPLATE_DATABASE, `SELECT json_build_object( + 'engine',current_setting('server_version'), + 'ledger',(SELECT count(*) FROM supabase_migrations.schema_migrations), + 'manifest',(SELECT count(*) FROM privacy_retention.g014_catalog_contract_manifest), + 'profiles',(SELECT count(*) FROM public.profiles), + 'reviews',(SELECT count(*) FROM public.reviews) + )::text;`, 'supabase_admin')); + if (!String(template.engine).startsWith('17.6') + || template.ledger !== 0 + || template.profiles !== 0 + || template.reviews !== 0) fail('LOCAL_REHEARSAL_TEMPLATE_INVALID'); + + let created = false; + let fixtureMembershipCreated = false; + let proof; + try { + createFixtureMembership(); + fixtureMembershipCreated = true; + createOwnedDatabase(); + created = true; + if (scalar(DATABASE, `SELECT coalesce('security_invoker=true'=ANY(c.reloptions),false) + FROM pg_class c WHERE c.oid='public.admin_evaluation_related_rows'::regclass;`) !== 't') { + fail('LOCAL_REHEARSAL_SECURITY_INVOKER_BASELINE_INVALID'); + } + if (scalar(DATABASE, `SELECT m.inherit_option::text || ':' || m.set_option::text + FROM pg_auth_members m + WHERE m.roleid=${quote(FIXTURE_PARENT_ROLE)}::regrole + AND m.member=${quote(FIXTURE_MEMBER_ROLE)}::regrole + AND m.grantor='supabase_admin'::regrole;`, 'supabase_admin') !== 'true:true') { + fail('LOCAL_REHEARSAL_MEMBERSHIP_OPTIONS_BASELINE_INVALID'); + } + const stages = collectStageStates(record, stateQuery); + assertRehearsalStages(record, stages); + const plan = compileSuccessorPlan(record, { + expiresAt: '9999-12-31T23:59:59.999Z', + stageStates: stages, + }); + + resetOwnedDatabase(); + let success; + try { + success = psql(DATABASE, plan.sql, { singleTransaction: true }); + } catch (error) { + let stage = 0; + const diagnosticSql = plan.sql.replaceAll('MIGRATION_TERMINAL_READBACK_FAILED', () => { + stage += 1; + return `MIGRATION_TERMINAL_READBACK_FAILED_STAGE_${stage}`; + }); + try { + psql(DATABASE, diagnosticSql, { singleTransaction: true }); + } catch (diagnostic) { + fail('LOCAL_REHEARSAL_ATOMIC_STAGE_FAILED', diagnostic.message); + } + throw error; + } + const successLines = success.stdout.trim().split(/\r?\n/).filter(Boolean); + const successReadback = JSON.parse(successLines.at(-1)); + const committed = readState(DATABASE, stateQuery); + if (!equal(successReadback, stages[11]) || !equal(committed, stages[11])) { + fail('LOCAL_REHEARSAL_COMMIT_READBACK_INVALID'); + } + + resetOwnedDatabase(); + const rollback = psql(DATABASE, `${plan.sql}\nDO $forced$ BEGIN RAISE EXCEPTION 'LOCAL_FORCED_ROLLBACK'; END $forced$;\n`, { + singleTransaction: true, + allowFailure: true, + }); + if (rollback.status === 0 || !/LOCAL_FORCED_ROLLBACK/.test(rollback.stderr)) { + fail('LOCAL_REHEARSAL_FORCED_ROLLBACK_MISSING'); + } + const rolledBack = readState(DATABASE, stateQuery); + if (!equal(rolledBack, stages[0])) fail('LOCAL_REHEARSAL_ROLLBACK_DRIFT'); + + resetOwnedDatabase(); + for (let index = 0; index < 10; index += 1) applyRawMigration(record, index); + const cleanPreimage = readState(DATABASE, stateQuery); + if (!equal(cleanPreimage, stages[10])) fail('LOCAL_REHEARSAL_STAGE10_REPLAY_DRIFT'); + tamperFinishCleanupPreimage(); + const tamperedBefore = readState(DATABASE, stateQuery); + const wrongPreimage = psql(DATABASE, record.materials[10].bytes, { allowFailure: true }); + if (wrongPreimage.status === 0 || !/REVIEW_SHARED_SOURCE_DRIFT/.test(wrongPreimage.stderr)) { + fail('LOCAL_REHEARSAL_WRONG_PREIMAGE_NOT_REJECTED'); + } + const tamperedAfter = readState(DATABASE, stateQuery); + if (!equal(tamperedBefore, tamperedAfter) + || tamperedAfter.bundle.targetCount !== 10 + || tamperedAfter.bundle.ledgerCount !== 96) { + fail('LOCAL_REHEARSAL_WRONG_PREIMAGE_ROLLBACK_DRIFT'); + } + + const securityInvokerDriftRejected = assertCatalogDriftRejected({ + label: 'security-invoker', + plan, + stages, + stateQuery, + tamperSql: `ALTER VIEW public.admin_evaluation_related_rows SET (security_invoker=false);`, + }); + const columnAclDriftRejected = assertCatalogDriftRejected({ + label: 'column-acl', + plan, + stages, + stateQuery, + tamperSql: `GRANT UPDATE(approved_name) ON TABLE public.restaurants TO anon;`, + }); + const membershipInheritOptionDriftRejected = assertCatalogDriftRejected({ + label: 'membership-inherit-option', + plan, + stages, + stateQuery, + tamperSql: `GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} WITH ADMIN FALSE, INHERIT FALSE, SET TRUE GRANTED BY supabase_admin;`, + restoreSql: `GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} WITH ADMIN FALSE, INHERIT TRUE, SET TRUE GRANTED BY supabase_admin;`, + }); + const membershipSetOptionDriftRejected = assertCatalogDriftRejected({ + label: 'membership-set-option', + plan, + stages, + stateQuery, + tamperSql: `GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} WITH ADMIN FALSE, INHERIT TRUE, SET FALSE GRANTED BY supabase_admin;`, + restoreSql: `GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} WITH ADMIN FALSE, INHERIT TRUE, SET TRUE GRANTED BY supabase_admin;`, + }); + const contendedAdmissionExpiryRejected = await assertContendedExpiryRejected(record, stages, stateQuery); + + proof = { + schemaVersion: 1, + status: 'passed', + environment: { + dockerContext: DOCKER_CONTEXT, + dockerContainer: DOCKER_CONTAINER, + engine: template.engine, + templateDatabase: TEMPLATE_DATABASE, + temporaryDatabase: DATABASE, + }, + input: template, + manifestSha256: sha256(manifestBytes), + rehearsalHarnessSha256: sha256(readFileSync(fileURLToPath(import.meta.url))), + sourceRoot: record.manifest.sourceRoot, + predecessorSources: PREDECESSOR_PATHS.map(path => ({ + path, + sha256: sha256(readFileSync(resolve(REPOSITORY_ROOT, path))), + })), + migrationCount: record.manifest.migrations.length, + statementCount: record.manifest.migrations.reduce((sum, migration) => sum + migration.statementCount, 0), + stageStatesSha256: sha256(JSON.stringify(canonical(stages))), + stages: stages.map(summarizeStage), + results: { + advisoryContentionObserved: true, + atomicCommit: true, + atomicRollbackExact: true, + catalogDriftStatePreserved: true, + columnAclDriftRejected, + compiledSqlBytes: Buffer.byteLength(plan.sql), + compiledSqlSha256: sha256(plan.sql), + contendedAdmissionExpiryRejected, + contendedAdmissionExpiryStatePreserved: true, + membershipOptionsBaseline: { inherit: true, set: true }, + membershipInheritOptionDriftRejected, + membershipSetOptionDriftRejected, + securityInvokerBaseline: true, + securityInvokerDriftRejected, + wrongFinishPreimageRejected: true, + wrongFinishPreimageRollbackExact: true, + terminalLedgerCount: committed.bundle.ledgerCount, + terminalContractReady: committed.state.terminalContractReady, + }, + sourceMutation: false, + hostedWrites: false, + operatingWrites: false, + }; + } finally { + if (created) dropOwnedDatabase(); + if (fixtureMembershipCreated) dropFixtureMembership(); + } + proof.temporaryDatabaseDropped = true; + writeFileSync(OUTPUT_PATH, `${JSON.stringify(proof, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); + process.stdout.write(`${JSON.stringify({ + status: proof.status, + migrationCount: proof.migrationCount, + statementCount: proof.statementCount, + atomicCommit: proof.results.atomicCommit, + atomicRollbackExact: proof.results.atomicRollbackExact, + catalogDriftRejected: proof.results.securityInvokerDriftRejected + && proof.results.columnAclDriftRejected + && proof.results.membershipInheritOptionDriftRejected + && proof.results.membershipSetOptionDriftRejected, + contendedAdmissionExpiryRejected: proof.results.contendedAdmissionExpiryRejected, + wrongFinishPreimageRejected: proof.results.wrongFinishPreimageRejected, + terminalLedgerCount: proof.results.terminalLedgerCount, + temporaryDatabaseDropped: proof.temporaryDatabaseDropped, + })}\n`); +} + +await run(); diff --git a/apps/web/scripts/review-media-sql-successor.mjs b/apps/web/scripts/review-media-sql-successor.mjs new file mode 100644 index 0000000000..1b3e7ef70a --- /dev/null +++ b/apps/web/scripts/review-media-sql-successor.mjs @@ -0,0 +1,1198 @@ +/** Local-only controller for the atomic eleven-stage review-media successor. */ +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { + closeSync, + existsSync, + fsyncSync, + lstatSync, + openSync, + readFileSync, + realpathSync, + writeSync, +} from 'node:fs'; +import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { + boundedMigrationError, + psqlConnectionEnvironment, + readOriginalStatementVector, + selectDirectDatabaseTransport, +} from './apply-supabase-migration.mjs'; +import { + compileElevenMigrationBundle, + executeMigrationBundle, + elevenMigrationBundleSuffixRoot, +} from './supabase-migration-bundle.mjs'; +import { + TZUDONG_GITHUB_REPOSITORY, + TZUDONG_PRODUCTION_ALIASES, + TZUDONG_VERCEL_PROJECT_ID, + TZUDONG_VERCEL_TEAM_ID, + captureVercelRollbackReadbackSync, + normalizeVercelRollbackExpected, +} from './vercel-rollback-readback.mjs'; + +const SCRIPT_PATH = fileURLToPath(import.meta.url); +export const REPOSITORY_ROOT = resolve(dirname(SCRIPT_PATH), '../../..'); +export const SUCCESSOR_MANIFEST_PATH = resolve(REPOSITORY_ROOT, '.github/review-media-sql-successor.v1.json'); + +const HEX_SHA256 = /^[0-9a-f]{64}$/; +const REVISION = /^[0-9a-f]{40}$/; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const PROJECT_REF = 'aqlcofblfxdrjhhdmarw'; +const PURPOSE = 'review-media-current-state-eleven-stage'; +const ADMISSION_MAX_AGE_MS = 15 * 60 * 1000; +const GIT_REMOTE_READ_TIMEOUT_MS = 15_000; +const PRIVATE_RECEIPT_FILES = Object.freeze({ + protectedMain: 'protected-main-receipt.json', + rehearsal: 'rehearsal-receipt.json', + rollback: 'rollback-readback-receipt.json', + source: 'source-receipt.json', +}); +const FIXED_CONTROLLER_CODES = new Set([ + 'MIGRATION_BUNDLE_OUTCOME_UNCONFIRMED', + 'MIGRATION_BUNDLE_READBACK_INVALID', + 'MIGRATION_BUNDLE_RECONCILIATION_CONFLICT', + 'MIGRATION_BUNDLE_RECONCILIATION_UNKNOWN', + 'MIGRATION_BUNDLE_TERMINAL_READBACK_FAILED', + 'SUCCESSOR_ADMISSION_EXPIRED', + 'SUCCESSOR_ADMISSION_INVALID', + 'SUCCESSOR_ARGUMENT_INVALID', + 'SUCCESSOR_CHECKOUT_INVALID', + 'SUCCESSOR_DATABASE_TARGET_INVALID', + 'SUCCESSOR_JOURNAL_APPEND_FAILED', + 'SUCCESSOR_JOURNAL_BINDING_MISMATCH', + 'SUCCESSOR_JOURNAL_EXISTS', + 'SUCCESSOR_JOURNAL_INVALID', + 'SUCCESSOR_LAUNCH_HELD', + 'SUCCESSOR_MANIFEST_INVALID', + 'SUCCESSOR_PREFLIGHT_MISMATCH', + 'SUCCESSOR_READBACK_INVALID', + 'SUCCESSOR_ROLLBACK_READBACK_INVALID', + 'SUCCESSOR_SOURCE_DRIFT', +]); + +const operationError = code => { + const error = new Error(code); + error.code = code; + return error; +}; +const fail = code => { throw operationError(code); }; +const sha256 = value => createHash('sha256').update(value).digest('hex'); +const quote = value => `'${String(value).replaceAll("'", "''")}'`; + +function canonical(value) { + if (Array.isArray(value)) return value.map(canonical); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])); + } + return value; +} + +function equal(left, right) { + return JSON.stringify(canonical(left)) === JSON.stringify(canonical(right)); +} + +function exactKeys(value, keys, code) { + if (!value || typeof value !== 'object' || Array.isArray(value)) fail(code); + const actual = Object.keys(value).sort(); + const expected = [...keys].sort(); + if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) fail(code); +} + +function safeCode(error) { + try { return typeof error?.code === 'string' ? error.code : ''; } catch { return ''; } +} + +export function boundedSuccessorError(error) { + const code = safeCode(error); + if (FIXED_CONTROLLER_CODES.has(code)) return operationError(code); + return boundedMigrationError(error); +} + +function parseJsonObject(bytes, code) { + let value; + try { value = JSON.parse(Buffer.isBuffer(bytes) ? bytes.toString('utf8') : bytes); } catch { fail(code); } + if (!value || typeof value !== 'object' || Array.isArray(value)) fail(code); + return value; +} + +function assertHex(value, code) { + if (typeof value !== 'string' || !HEX_SHA256.test(value)) fail(code); +} + +function sourceProjection(migration) { + return { + id: migration.id, + version: migration.version, + name: migration.name, + path: migration.path, + bytes: migration.bytes, + sha256: migration.sha256, + statementCount: migration.statementCount, + statementVectorSha256: migration.statementVectorSha256, + originalStatementVector: migration.originalStatementVector, + }; +} + +function assertToolchain(manifest) { + const expected = [ + 'apps/web/scripts/review-media-sql-successor.mjs', + 'apps/web/scripts/supabase-migration-bundle.mjs', + 'apps/web/scripts/supabase-migration-transaction.mjs', + 'apps/web/scripts/apply-supabase-migration.mjs', + 'apps/web/scripts/vercel-rollback-readback.mjs', + 'backend/supabase/scripts/g037_supabase_statement_vector.mjs', + ]; + if (!Array.isArray(manifest.toolchain) || manifest.toolchain.length !== expected.length) fail('SUCCESSOR_MANIFEST_INVALID'); + manifest.toolchain.forEach((entry, index) => { + exactKeys(entry, ['path', 'sha256'], 'SUCCESSOR_MANIFEST_INVALID'); + if (entry.path !== expected[index]) fail('SUCCESSOR_MANIFEST_INVALID'); + assertHex(entry.sha256, 'SUCCESSOR_MANIFEST_INVALID'); + const bytes = readFileSync(resolve(REPOSITORY_ROOT, entry.path)); + if (sha256(bytes) !== entry.sha256) fail('SUCCESSOR_SOURCE_DRIFT'); + }); + exactKeys(manifest.legacyReleaseManifest, ['entries', 'path', 'sha256'], 'SUCCESSOR_MANIFEST_INVALID'); + if (manifest.legacyReleaseManifest.path !== '.github/supabase-migration-release-manifest.v1.json' + || manifest.legacyReleaseManifest.entries !== 3) fail('SUCCESSOR_MANIFEST_INVALID'); + const legacyBytes = readFileSync(resolve(REPOSITORY_ROOT, manifest.legacyReleaseManifest.path)); + if (sha256(legacyBytes) !== manifest.legacyReleaseManifest.sha256) fail('SUCCESSOR_SOURCE_DRIFT'); + const legacy = parseJsonObject(legacyBytes, 'SUCCESSOR_SOURCE_DRIFT'); + if (!Array.isArray(legacy.migrations) || legacy.migrations.length !== 3) fail('SUCCESSOR_SOURCE_DRIFT'); +} + +export function loadSuccessorManifest({ + manifestPath = SUCCESSOR_MANIFEST_PATH, + readVectorImpl = readOriginalStatementVector, +} = {}) { + if (resolve(manifestPath) !== SUCCESSOR_MANIFEST_PATH) fail('SUCCESSOR_MANIFEST_INVALID'); + const bytes = readFileSync(manifestPath); + const manifest = parseJsonObject(bytes, 'SUCCESSOR_MANIFEST_INVALID'); + exactKeys(manifest, [ + 'advisoryLockKey', + 'databaseName', + 'executorRole', + 'id', + 'launchPolicy', + 'legacyReleaseManifest', + 'migrations', + 'operatingTransition', + 'projectRef', + 'protectedSource', + 'purpose', + 'requiredWriterFences', + 'schemaVersion', + 'serverMajor', + 'sourceRoot', + 'stateRootAlgorithm', + 'terminalContract', + 'toolchain', + ], 'SUCCESSOR_MANIFEST_INVALID'); + if (manifest.schemaVersion !== 1 + || manifest.id !== 'review_media_sql_successor_v1' + || manifest.purpose !== PURPOSE + || manifest.projectRef !== PROJECT_REF + || manifest.databaseName !== 'postgres' + || manifest.executorRole !== 'postgres' + || manifest.serverMajor !== 17 + || manifest.advisoryLockKey !== '7311754282260211' + || manifest.stateRootAlgorithm !== 'review-media-successor-state/v1' + || !Array.isArray(manifest.migrations) + || manifest.migrations.length !== 11) fail('SUCCESSOR_MANIFEST_INVALID'); + exactKeys(manifest.operatingTransition, ['afterEleven', 'beforeEleven'], 'SUCCESSOR_MANIFEST_INVALID'); + if (!equal(manifest.operatingTransition, { beforeEleven: 86, afterEleven: 97 })) { + fail('SUCCESSOR_MANIFEST_INVALID'); + } + exactKeys(manifest.protectedSource, ['ref', 'remote', 'repositoryUrl'], 'SUCCESSOR_MANIFEST_INVALID'); + if (manifest.protectedSource.remote !== 'origin' + || manifest.protectedSource.ref !== 'refs/heads/main' + || manifest.protectedSource.repositoryUrl !== 'https://github.com/twoimo/tzudong.git') { + fail('SUCCESSOR_MANIFEST_INVALID'); + } + assertHex(manifest.sourceRoot, 'SUCCESSOR_MANIFEST_INVALID'); + exactKeys(manifest.launchPolicy, ['code', 'reason', 'state'], 'SUCCESSOR_MANIFEST_INVALID'); + if (!['held', 'ready'].includes(manifest.launchPolicy.state) + || manifest.launchPolicy.code !== 'SUCCESSOR_LAUNCH_HELD' + || manifest.launchPolicy.reason !== 'protected-release-and-fresh-admission-required') { + fail('SUCCESSOR_MANIFEST_INVALID'); + } + exactKeys(manifest.requiredWriterFences, [ + 'adminRecordMutationsHold', + 'automaticReviewer', + 'browserRpc', + 'dashboard', + 'directSql', + 'g037WriteFreeze', + 'providerIngress', + ], 'SUCCESSOR_MANIFEST_INVALID'); + if (!equal(manifest.requiredWriterFences, { + adminRecordMutationsHold: 'active', + automaticReviewer: 'stopped', + browserRpc: 'stopped', + dashboard: 'stopped', + directSql: 'stopped', + g037WriteFreeze: 'active', + providerIngress: 'stopped', + })) fail('SUCCESSOR_MANIFEST_INVALID'); + exactKeys(manifest.terminalContract, [ + 'finishPostimageSha256', + 'finishPreimageSha256', + 'requiredFunctionBodySha256', + 'requiredFunctions', + 'requiredPolicies', + ], 'SUCCESSOR_MANIFEST_INVALID'); + const terminalContract = { + finishPreimageSha256: 'f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1', + finishPostimageSha256: 'a6b4211b2d8486d482f0758ce249eec157249e1a5176be286b76af7e7a775ee9', + requiredFunctionBodySha256: { + 'public.admin_review_verification_cleanup(uuid,text,text[])': '468c12ee6c3463876cbccb394fcba8f870459a1f6cf3d945d97307f8551f094b', + 'public.finish_review_media_cleanup()': 'a6b4211b2d8486d482f0758ce249eec157249e1a5176be286b76af7e7a775ee9', + 'public.mutate_review_with_media(uuid,uuid,text,timestamp with time zone,text,text[],text[])': 'd5d6646a4294c0e7bd0cd75ea47a996c9c57bac6c32a39ac53fbb7a8b1e7e323', + 'review_media_private.enqueue_removed()': 'cbf6a87e453e8c2e14d67acd73a92e399fbba4b4f24ac27966806e13ef1fd905', + 'review_media_private.guard_references()': '94113c8ae1231e8eae5a698f35a1370a38367e42c79cc87ac62db76c32e735b1', + }, + requiredFunctions: [ + 'public.mutate_review_with_media(uuid,uuid,text,timestamp with time zone,text,text[],text[])', + 'public.queue_review_upload_cleanup(uuid,text[])', + 'public.read_review_media_commit(uuid,uuid,text)', + 'public.pending_review_media_cleanup()', + 'public.finish_review_media_cleanup()', + 'public.review_media_delete_allowed(text)', + 'public.review_media_upload_allowed(text,text)', + 'public.admin_review_verification_cleanup(uuid,text,text[])', + ], + requiredPolicies: [ + 'review_media_safe_delete', + 'review_media_safe_insert', + 'review_media_safe_update', + 'review_verifications_owner_read', + 'review_verifications_owner_insert', + 'review_verifications_owner_delete', + 'review_verifications_safe_delete', + 'review_photos_food_insert', + 'review_photos_food_update', + ], + }; + if (!equal(manifest.terminalContract, terminalContract)) fail('SUCCESSOR_MANIFEST_INVALID'); + assertToolchain(manifest); + + const expectedIds = [ + 'review_media_commit_cleanup', + 'review_verification_private', + 'review_media_catalog_integration', + 'admin_review_media_completion', + 'review_legacy_shared_cleanup', + 'admin_evaluation_revision_safeupdate', + 'admin_record_expired_preview_readback', + 'admin_verification_durable_staging', + 'review_media_storage_retirement_fence', + 'review_storage_caller_bound_admin_policy', + 'review_owner_cleanup_completion', + ]; + const materials = manifest.migrations.map((migration, index) => { + exactKeys(migration, [ + 'bytes', + 'id', + 'name', + 'originalStatementVector', + 'path', + 'sha256', + 'statementCount', + 'statementVectorSha256', + 'version', + ], 'SUCCESSOR_MANIFEST_INVALID'); + const pathMatch = /^backend\/supabase\/migrations\/(\d{14})_([a-z0-9_]+)\.sql$/.exec(migration.path); + if (migration.id !== expectedIds[index] + || !pathMatch + || migration.version !== pathMatch[1] + || migration.name !== pathMatch[2] + || migration.id !== migration.name + || !Number.isSafeInteger(migration.bytes) + || migration.bytes <= 0 + || !Number.isSafeInteger(migration.statementCount) + || migration.statementCount <= 0 + || !Array.isArray(migration.originalStatementVector) + || migration.originalStatementVector.length !== migration.statementCount) fail('SUCCESSOR_MANIFEST_INVALID'); + assertHex(migration.sha256, 'SUCCESSOR_MANIFEST_INVALID'); + assertHex(migration.statementVectorSha256, 'SUCCESSOR_MANIFEST_INVALID'); + if (sha256(JSON.stringify(migration.originalStatementVector)) !== migration.statementVectorSha256) { + fail('SUCCESSOR_MANIFEST_INVALID'); + } + const sourceBytes = readFileSync(resolve(REPOSITORY_ROOT, migration.path)); + if (sourceBytes.length !== migration.bytes || sha256(sourceBytes) !== migration.sha256) fail('SUCCESSOR_SOURCE_DRIFT'); + const observedVector = readVectorImpl(migration, sourceBytes); + if (!equal(observedVector, migration.originalStatementVector)) fail('SUCCESSOR_SOURCE_DRIFT'); + return { path: migration.path, bytes: sourceBytes, originalVector: observedVector }; + }); + if (sha256(JSON.stringify(manifest.migrations.map(sourceProjection))) !== manifest.sourceRoot) { + fail('SUCCESSOR_MANIFEST_INVALID'); + } + return Object.freeze({ + bytes, + manifest, + manifestSha256: sha256(bytes), + materials: Object.freeze(materials), + }); +} + +function sortedAclSql(expression) { + return `ARRAY(SELECT value::text FROM unnest(coalesce(${expression},'{}')) value ORDER BY value::text)`; +} + +export function buildSuccessorStateQuery(manifest, suffixRoot = elevenMigrationBundleSuffixRoot(manifest.migrations)) { + const versions = manifest.migrations.map(migration => migration.version); + const targetCount = manifest.migrations.length; + const targetValues = versions.map(version => `(${quote(version)})`).join(','); + const targets = versions.map(version => quote(version)).join(','); + const exactRows = manifest.migrations.map(migration => `EXISTS(SELECT 1 FROM supabase_migrations.schema_migrations WHERE version=${quote(migration.version)} AND name=${quote(migration.name)} AND statements=ARRAY(SELECT jsonb_array_elements_text(${quote(JSON.stringify(migration.originalStatementVector))}::jsonb)))`).join(' AND '); + const schemas = "'public','pipeline_control','privacy_retention','review_media_private','storage'"; + const acl = expression => sortedAclSql(expression); + return `SELECT * FROM (WITH + target_versions(version) AS (VALUES ${targetValues}), + ledger_prefix AS ( + SELECT count(*)::int AS row_count,coalesce(jsonb_agg(jsonb_build_object('version',version,'name',name,'statements',statements) ORDER BY version),'[]'::jsonb)::text AS material + FROM supabase_migrations.schema_migrations WHERE version<>ALL(ARRAY[${targets}]) + ), + ledger_all AS ( + SELECT count(*)::int AS row_count,coalesce(jsonb_agg(jsonb_build_object('version',version,'name',name,'statements',statements) ORDER BY version),'[]'::jsonb)::text AS material + FROM supabase_migrations.schema_migrations + ), + target_state AS ( + SELECT count(*)::int AS target_count FROM supabase_migrations.schema_migrations WHERE version=ANY(ARRAY[${targets}]) + ), + catalog_material AS ( + SELECT jsonb_build_object( + 'schemas',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.schema_name) FROM (SELECT n.nspname AS schema_name,pg_get_userbyid(n.nspowner) AS owner,${acl('n.nspacl')} AS acl FROM pg_namespace n WHERE n.nspname IN(${schemas})) x),'[]'::jsonb), + 'relations',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.schema_name,x.relation_name) FROM (SELECT n.nspname AS schema_name,c.relname AS relation_name,c.relkind,c.relpersistence,pg_get_userbyid(c.relowner) AS owner,c.relrowsecurity,c.relforcerowsecurity,c.relreplident,${acl('c.reloptions')} AS reloptions,CASE WHEN c.relkind IN('v','m') THEN pg_get_viewdef(c.oid,true) ELSE '' END AS view_definition,${acl('c.relacl')} AS acl FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname IN(${schemas})) x),'[]'::jsonb), + 'columns',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.schema_name,x.relation_name,x.attnum) FROM (SELECT n.nspname AS schema_name,c.relname AS relation_name,a.attnum,a.attname,format_type(a.atttypid,a.atttypmod) AS data_type,a.attnotnull,a.attidentity,a.attgenerated,coalesce(pg_get_expr(d.adbin,d.adrelid),'') AS default_expression,${acl('a.attacl')} AS acl FROM pg_attribute a JOIN pg_class c ON c.oid=a.attrelid JOIN pg_namespace n ON n.oid=c.relnamespace LEFT JOIN pg_attrdef d ON d.adrelid=a.attrelid AND d.adnum=a.attnum WHERE n.nspname IN(${schemas}) AND a.attnum>0 AND NOT a.attisdropped) x),'[]'::jsonb), + 'constraints',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.schema_name,x.relation_name,x.constraint_name) FROM (SELECT n.nspname AS schema_name,c.relname AS relation_name,k.conname AS constraint_name,k.contype,k.condeferrable,k.condeferred,k.convalidated,pg_get_constraintdef(k.oid,true) AS definition FROM pg_constraint k JOIN pg_class c ON c.oid=k.conrelid JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname IN(${schemas})) x),'[]'::jsonb), + 'indexes',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.schema_name,x.index_name) FROM (SELECT n.nspname AS schema_name,c.relname AS index_name,pg_get_indexdef(i.indexrelid) AS definition FROM pg_index i JOIN pg_class c ON c.oid=i.indexrelid JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname IN(${schemas})) x),'[]'::jsonb), + 'triggers',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.schema_name,x.relation_name,x.trigger_name) FROM (SELECT n.nspname AS schema_name,c.relname AS relation_name,t.tgname AS trigger_name,t.tgenabled,pg_get_triggerdef(t.oid,true) AS definition FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname IN(${schemas}) AND NOT t.tgisinternal) x),'[]'::jsonb), + 'policies',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.schema_name,x.relation_name,x.policy_name) FROM (SELECT n.nspname AS schema_name,c.relname AS relation_name,p.polname AS policy_name,p.polcmd,p.polpermissive,ARRAY(SELECT coalesce(r.rolname,'PUBLIC') FROM unnest(p.polroles) role_oid LEFT JOIN pg_roles r ON r.oid=role_oid ORDER BY coalesce(r.rolname,'PUBLIC')) AS roles,coalesce(pg_get_expr(p.polqual,p.polrelid),'') AS using_expression,coalesce(pg_get_expr(p.polwithcheck,p.polrelid),'') AS check_expression FROM pg_policy p JOIN pg_class c ON c.oid=p.polrelid JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname IN(${schemas})) x),'[]'::jsonb), + 'functions',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.signature) FROM (SELECT p.oid::regprocedure::text AS signature,pg_get_userbyid(p.proowner) AS owner,l.lanname AS language,format_type(p.prorettype,NULL) AS result_type,p.prokind,p.prosecdef,p.proleakproof,p.proisstrict,p.proretset,p.provolatile,p.proparallel,coalesce(p.proconfig,'{}'::text[]) AS config,${acl('p.proacl')} AS acl,encode(sha256(convert_to(p.prosrc,'UTF8')),'hex') AS body_sha256 FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace JOIN pg_language l ON l.oid=p.prolang WHERE n.nspname IN(${schemas})) x),'[]'::jsonb), + 'roles',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.role_name) FROM (SELECT r.rolname AS role_name,r.rolsuper,r.rolinherit,r.rolcreaterole,r.rolcreatedb,r.rolcanlogin,r.rolreplication,r.rolbypassrls FROM pg_roles r) x),'[]'::jsonb), + 'memberships',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.role_name,x.member_name,x.grantor_name) FROM (SELECT role.rolname AS role_name,member.rolname AS member_name,grantor.rolname AS grantor_name,m.admin_option,m.inherit_option,m.set_option FROM pg_auth_members m JOIN pg_roles role ON role.oid=m.roleid JOIN pg_roles member ON member.oid=m.member JOIN pg_roles grantor ON grantor.oid=m.grantor) x),'[]'::jsonb), + 'defaultAcls',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.owner,x.schema_name,x.object_type) FROM (SELECT pg_get_userbyid(d.defaclrole) AS owner,coalesce(n.nspname,'') AS schema_name,d.defaclobjtype AS object_type,${acl('d.defaclacl')} AS acl FROM pg_default_acl d LEFT JOIN pg_namespace n ON n.oid=d.defaclnamespace) x),'[]'::jsonb) + )::text AS material + ), + g014_material AS ( + SELECT jsonb_build_object( + 'manifest',coalesce((SELECT jsonb_agg(to_jsonb(m) ORDER BY manifest_kind,manifest_key) FROM privacy_retention.g014_catalog_contract_manifest m),'[]'::jsonb), + 'allowlist',coalesce((SELECT jsonb_agg(to_jsonb(a) ORDER BY function_schema,function_name,identity_arguments,grantee) FROM privacy_retention.g014_public_rpc_allowlist a),'[]'::jsonb) + )::text AS material + ), + storage_material AS ( + SELECT jsonb_build_object( + 'buckets',coalesce((SELECT jsonb_agg(to_jsonb(b) ORDER BY id) FROM ( + SELECT id,name,owner,public,avif_autodetection,file_size_limit,allowed_mime_types,owner_id,type, + versioning_status,lifecycle_configuration,lifecycle_configuration_generation + FROM storage.buckets WHERE id IN('review-photos','review-verifications')) b),'[]'::jsonb), + 'policies',coalesce((SELECT jsonb_agg(to_jsonb(x) ORDER BY x.policy_name) FROM (SELECT p.polname AS policy_name,p.polcmd,p.polpermissive,ARRAY(SELECT coalesce(r.rolname,'PUBLIC') FROM unnest(p.polroles) role_oid LEFT JOIN pg_roles r ON r.oid=role_oid ORDER BY coalesce(r.rolname,'PUBLIC')) AS roles,coalesce(pg_get_expr(p.polqual,p.polrelid),'') AS using_expression,coalesce(pg_get_expr(p.polwithcheck,p.polrelid),'') AS check_expression FROM pg_policy p WHERE p.polrelid='storage.objects'::regclass) x),'[]'::jsonb) + )::text AS material + ), + contract AS ( + SELECT + (SELECT encode(sha256(convert_to(p.prosrc,'UTF8')),'hex') FROM pg_proc p WHERE p.oid=to_regprocedure('public.finish_review_media_cleanup()')) AS finish_hash, + EXISTS(SELECT 1 FROM pg_roles WHERE rolname='privacy_workflow_owner' AND NOT rolsuper AND NOT rolinherit AND NOT rolcreaterole AND NOT rolcreatedb AND NOT rolcanlogin AND NOT rolreplication AND NOT rolbypassrls) + AND EXISTS(SELECT 1 FROM pg_auth_members m WHERE m.roleid='privacy_workflow_owner'::regrole AND m.member='postgres'::regrole AND m.admin_option AND m.grantor<>'postgres'::regrole) + AND NOT EXISTS(SELECT 1 FROM pg_auth_members m WHERE m.roleid='privacy_workflow_owner'::regrole AND m.member='postgres'::regrole AND m.admin_option AND m.grantor='postgres'::regrole) AS owner_authority_ready, + to_regclass('review_media_private.commits') IS NOT NULL + AND to_regclass('review_media_private.cleanup') IS NOT NULL + AND NOT EXISTS(SELECT 1 FROM unnest(ARRAY[ + 'public.mutate_review_with_media(uuid,uuid,text,timestamp with time zone,text,text[],text[])', + 'public.queue_review_upload_cleanup(uuid,text[])','public.read_review_media_commit(uuid,uuid,text)', + 'public.pending_review_media_cleanup()','public.finish_review_media_cleanup()', + 'public.review_media_delete_allowed(text)','public.review_media_upload_allowed(text,text)', + 'public.admin_review_verification_cleanup(uuid,text,text[])']) signature WHERE to_regprocedure(signature) IS NULL) + AND (SELECT encode(sha256(convert_to(prosrc,'UTF8')),'hex') FROM pg_proc WHERE oid=to_regprocedure('public.mutate_review_with_media(uuid,uuid,text,timestamp with time zone,text,text[],text[])'))='d5d6646a4294c0e7bd0cd75ea47a996c9c57bac6c32a39ac53fbb7a8b1e7e323' + AND (SELECT encode(sha256(convert_to(prosrc,'UTF8')),'hex') FROM pg_proc WHERE oid=to_regprocedure('review_media_private.enqueue_removed()'))='cbf6a87e453e8c2e14d67acd73a92e399fbba4b4f24ac27966806e13ef1fd905' + AND (SELECT encode(sha256(convert_to(prosrc,'UTF8')),'hex') FROM pg_proc WHERE oid=to_regprocedure('review_media_private.guard_references()'))='94113c8ae1231e8eae5a698f35a1370a38367e42c79cc87ac62db76c32e735b1' + AND (SELECT encode(sha256(convert_to(prosrc,'UTF8')),'hex') FROM pg_proc WHERE oid=to_regprocedure('public.admin_review_verification_cleanup(uuid,text,text[])'))='468c12ee6c3463876cbccb394fcba8f870459a1f6cf3d945d97307f8551f094b' + AND (SELECT encode(sha256(convert_to(prosrc,'UTF8')),'hex') FROM pg_proc WHERE oid=to_regprocedure('public.finish_review_media_cleanup()'))='a6b4211b2d8486d482f0758ce249eec157249e1a5176be286b76af7e7a775ee9' + AND NOT EXISTS(SELECT 1 FROM unnest(ARRAY[ + 'public.mutate_review_with_media(uuid,uuid,text,timestamp with time zone,text,text[],text[])', + 'public.queue_review_upload_cleanup(uuid,text[])','public.read_review_media_commit(uuid,uuid,text)', + 'public.pending_review_media_cleanup()','public.finish_review_media_cleanup()', + 'public.review_media_delete_allowed(text)','public.review_media_upload_allowed(text,text)']) signature + WHERE NOT coalesce(has_function_privilege('authenticated',to_regprocedure(signature),'EXECUTE'),false) + OR coalesce(has_function_privilege('anon',to_regprocedure(signature),'EXECUTE'),false) + OR coalesce(has_function_privilege('service_role',to_regprocedure(signature),'EXECUTE'),false)) + AND coalesce(has_function_privilege('service_role',to_regprocedure('public.admin_review_verification_cleanup(uuid,text,text[])'),'EXECUTE'),false) + AND NOT coalesce(has_function_privilege('anon',to_regprocedure('public.admin_review_verification_cleanup(uuid,text,text[])'),'EXECUTE'),false) + AND NOT coalesce(has_function_privilege('authenticated',to_regprocedure('public.admin_review_verification_cleanup(uuid,text,text[])'),'EXECUTE'),false) + AND NOT coalesce(has_function_privilege('anon',to_regprocedure('public.is_user_admin(uuid)'),'EXECUTE'),false) + AND NOT coalesce(has_function_privilege('authenticated',to_regprocedure('public.is_user_admin(uuid)'),'EXECUTE'),false) + AND NOT coalesce(has_function_privilege('service_role',to_regprocedure('public.is_user_admin(uuid)'),'EXECUTE'),false) + AND (SELECT count(*)=9 FROM pg_policy p WHERE p.polrelid='storage.objects'::regclass AND p.polname=ANY(ARRAY[ + 'review_media_safe_delete','review_media_safe_insert','review_media_safe_update', + 'review_verifications_owner_read','review_verifications_owner_insert','review_verifications_owner_delete', + 'review_verifications_safe_delete','review_photos_food_insert','review_photos_food_update'])) + AND EXISTS(SELECT 1 FROM storage.buckets WHERE id='review-verifications' AND NOT public) AS terminal_ready + ) + SELECT json_build_object( + 'bundle',json_build_object( + 'ledgerCount',ledger_all.row_count, + 'priorCount',ledger_prefix.row_count, + 'prefixRoot',encode(sha256(convert_to(ledger_prefix.material,'UTF8')),'hex'), + 'targetCount',target_state.target_count, + 'suffixRoot',CASE WHEN target_state.target_count=${targetCount} AND ${exactRows} THEN ${quote(suffixRoot)} ELSE NULL END + ), + 'state',json_build_object( + 'activeReviewItems',(SELECT count(*)::int FROM pipeline_control.restaurant_review_items WHERE state IN('queued','running')), + 'currentUser',current_user, + 'databaseName',current_database(), + 'finishCleanupBodySha256',contract.finish_hash, + 'g014Root',encode(sha256(convert_to(g014_material.material,'UTF8')),'hex'), + 'ledgerRoot',encode(sha256(convert_to(ledger_all.material,'UTF8')),'hex'), + 'ownerAuthorityReady',contract.owner_authority_ready, + 'reviewItems',(SELECT count(*)::int FROM pipeline_control.restaurant_review_items), + 'reviewPolicyEnabled',coalesce((SELECT enabled FROM pipeline_control.restaurant_review_policy WHERE singleton),true), + 'reviewRuns',(SELECT count(*)::int FROM pipeline_control.restaurant_review_runs), + 'schemaRoot',encode(sha256(convert_to(catalog_material.material,'UTF8')),'hex'), + 'serverMajor',(current_setting('server_version_num')::int/10000), + 'sessionUser',session_user, + 'storageRoot',encode(sha256(convert_to(storage_material.material,'UTF8')),'hex'), + 'terminalContractReady',contract.terminal_ready + ) + )::text AS state + FROM ledger_prefix,ledger_all,target_state,catalog_material,g014_material,storage_material,contract) AS successor_state`; +} + +function validateStageState(stage, index, manifest, suffixRoot, priorCount, prefixRoot) { + exactKeys(stage, ['bundle', 'state'], 'SUCCESSOR_ADMISSION_INVALID'); + exactKeys(stage.bundle, ['ledgerCount', 'prefixRoot', 'priorCount', 'suffixRoot', 'targetCount'], 'SUCCESSOR_ADMISSION_INVALID'); + exactKeys(stage.state, [ + 'activeReviewItems', + 'currentUser', + 'databaseName', + 'finishCleanupBodySha256', + 'g014Root', + 'ledgerRoot', + 'ownerAuthorityReady', + 'reviewItems', + 'reviewPolicyEnabled', + 'reviewRuns', + 'schemaRoot', + 'serverMajor', + 'sessionUser', + 'storageRoot', + 'terminalContractReady', + ], 'SUCCESSOR_ADMISSION_INVALID'); + if (stage.bundle.ledgerCount !== priorCount + index + || stage.bundle.priorCount !== priorCount + || stage.bundle.prefixRoot !== prefixRoot + || stage.bundle.targetCount !== index + || stage.bundle.suffixRoot !== (index === manifest.migrations.length ? suffixRoot : null) + || stage.state.activeReviewItems !== 0 + || stage.state.currentUser !== manifest.executorRole + || stage.state.databaseName !== manifest.databaseName + || stage.state.ownerAuthorityReady !== true + || stage.state.reviewItems !== 0 + || stage.state.reviewPolicyEnabled !== false + || stage.state.reviewRuns !== 0 + || stage.state.serverMajor !== manifest.serverMajor + || stage.state.sessionUser !== manifest.executorRole + || stage.state.terminalContractReady !== (index === manifest.migrations.length)) { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + assertHex(stage.state.ledgerRoot, 'SUCCESSOR_ADMISSION_INVALID'); + assertHex(stage.state.schemaRoot, 'SUCCESSOR_ADMISSION_INVALID'); + assertHex(stage.state.g014Root, 'SUCCESSOR_ADMISSION_INVALID'); + assertHex(stage.state.storageRoot, 'SUCCESSOR_ADMISSION_INVALID'); + if (stage.state.finishCleanupBodySha256 !== null) { + assertHex(stage.state.finishCleanupBodySha256, 'SUCCESSOR_ADMISSION_INVALID'); + } + if (index === manifest.migrations.length - 1 + && stage.state.finishCleanupBodySha256 !== manifest.terminalContract.finishPreimageSha256) { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + if (index === manifest.migrations.length + && stage.state.finishCleanupBodySha256 !== manifest.terminalContract.finishPostimageSha256) { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + if (index === 0 && stage.state.ledgerRoot !== prefixRoot) fail('SUCCESSOR_ADMISSION_INVALID'); +} + +function canonicalReceiptBytes(value) { + return Buffer.from(`${JSON.stringify(canonical(value))}\n`); +} + +function readPrivateCanonicalReceipt(directory, fileName, expectedSha256) { + if (typeof directory !== 'string' || !isAbsolute(directory) || typeof fileName !== 'string') { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + let bytes; + let receiptPath; + let receiptDirectory; + try { + const directoryStatus = lstatSync(directory); + if (!directoryStatus.isDirectory() + || directoryStatus.isSymbolicLink() + || (process.platform !== 'win32' && (directoryStatus.mode & 0o077) !== 0)) { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + receiptDirectory = realpathSync(directory); + receiptPath = assertPrivateAdmissionPath(join(directory, fileName)); + bytes = readFileSync(receiptPath); + } catch { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + if (dirname(realpathSync(receiptPath)) !== receiptDirectory) fail('SUCCESSOR_ADMISSION_INVALID'); + if (sha256(bytes) !== expectedSha256) fail('SUCCESSOR_ADMISSION_INVALID'); + const receipt = parseJsonObject(bytes, 'SUCCESSOR_ADMISSION_INVALID'); + if (!bytes.equals(canonicalReceiptBytes(receipt))) fail('SUCCESSOR_ADMISSION_INVALID'); + return receipt; +} + +function assertReceiptWindow(receipt, admission, createdAt, expiresAt, nowMs) { + if (receipt.expiresAt !== admission.expiresAt) fail('SUCCESSOR_ADMISSION_INVALID'); + const observedAt = Date.parse(receipt.observedAt); + if (!Number.isFinite(observedAt) + || observedAt < createdAt + || observedAt >= expiresAt + || observedAt > nowMs + 30_000 + || Date.parse(receipt.expiresAt) !== expiresAt) fail('SUCCESSOR_ADMISSION_INVALID'); +} + +function validatePrivateReceipts(admission, manifestRecord, { + createdAt, + expiresAt, + nowMs, + receiptDirectory, +}) { + const { manifest, manifestSha256 } = manifestRecord; + const common = receipt => receipt.schemaVersion === 1 + && receipt.id === manifest.id + && receipt.projectRef === manifest.projectRef + && receipt.purpose === manifest.purpose + && receipt.manifestSha256 === manifestSha256 + && receipt.sourceRevision === admission.sourceRevision; + + const source = readPrivateCanonicalReceipt( + receiptDirectory, + PRIVATE_RECEIPT_FILES.source, + admission.sourceReceiptSha256, + ); + exactKeys(source, [ + 'expiresAt', 'id', 'kind', 'manifestSha256', 'observedAt', 'projectRef', 'purpose', + 'schemaVersion', 'sourceRevision', 'sourceRoot', + ], 'SUCCESSOR_ADMISSION_INVALID'); + if (!common(source) + || source.kind !== 'successor-source-receipt' + || source.sourceRoot !== manifest.sourceRoot) fail('SUCCESSOR_ADMISSION_INVALID'); + assertReceiptWindow(source, admission, createdAt, expiresAt, nowMs); + + const protectedMain = readPrivateCanonicalReceipt( + receiptDirectory, + PRIVATE_RECEIPT_FILES.protectedMain, + admission.protectedMainReceiptSha256, + ); + exactKeys(protectedMain, [ + 'expiresAt', 'id', 'kind', 'manifestSha256', 'observedAt', 'projectRef', 'purpose', + 'ref', 'remote', 'repositoryUrl', 'revision', 'schemaVersion', 'sourceRevision', + ], 'SUCCESSOR_ADMISSION_INVALID'); + if (!common(protectedMain) + || protectedMain.kind !== 'protected-main-readback-receipt' + || protectedMain.remote !== manifest.protectedSource.remote + || protectedMain.ref !== manifest.protectedSource.ref + || protectedMain.repositoryUrl !== manifest.protectedSource.repositoryUrl + || protectedMain.revision !== admission.sourceRevision) fail('SUCCESSOR_ADMISSION_INVALID'); + assertReceiptWindow(protectedMain, admission, createdAt, expiresAt, nowMs); + + const rehearsal = readPrivateCanonicalReceipt( + receiptDirectory, + PRIVATE_RECEIPT_FILES.rehearsal, + admission.rehearsalReceiptSha256, + ); + exactKeys(rehearsal, [ + 'expiresAt', 'id', 'kind', 'manifestSha256', 'observedAt', 'passed', 'projectRef', + 'purpose', 'schemaVersion', 'serverVersionNum', 'sourceRevision', 'sourceRoot', + 'stageStatesSha256', 'suffixRoot', + ], 'SUCCESSOR_ADMISSION_INVALID'); + if (!common(rehearsal) + || rehearsal.kind !== 'successor-rehearsal-receipt' + || rehearsal.passed !== true + || !Number.isSafeInteger(rehearsal.serverVersionNum) + || Math.floor(rehearsal.serverVersionNum / 10_000) !== manifest.serverMajor + || rehearsal.sourceRoot !== manifest.sourceRoot + || rehearsal.suffixRoot !== elevenMigrationBundleSuffixRoot(manifest.migrations) + || rehearsal.stageStatesSha256 !== sha256(JSON.stringify(canonical(admission.stageStates)))) { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + assertReceiptWindow(rehearsal, admission, createdAt, expiresAt, nowMs); + + const rollback = readPrivateCanonicalReceipt( + receiptDirectory, + PRIVATE_RECEIPT_FILES.rollback, + admission.rollback.readbackSha256, + ); + exactKeys(rollback, [ + 'deploymentId', 'deploymentSha', 'deploymentUrl', 'expiresAt', 'gitRef', 'id', 'kind', + 'manifestSha256', 'observedAt', 'productionAliases', 'projectId', 'projectRef', + 'purpose', 'readyState', 'repository', 'schemaVersion', 'sourceRevision', 'state', + 'target', 'teamId', + ], 'SUCCESSOR_ADMISSION_INVALID'); + if (!common(rollback) + || rollback.kind !== 'rollback-readback-receipt' + || rollback.deploymentId !== admission.rollback.deploymentId + || rollback.deploymentSha !== admission.rollback.deploymentSha + || rollback.deploymentUrl !== admission.rollback.deploymentUrl + || rollback.gitRef !== admission.rollback.gitRef + || !equal(rollback.productionAliases, admission.rollback.productionAliases) + || rollback.projectId !== admission.rollback.projectId + || rollback.readyState !== admission.rollback.readyState + || rollback.repository !== admission.rollback.repository + || rollback.state !== admission.rollback.state + || rollback.target !== admission.rollback.target + || rollback.teamId !== admission.rollback.teamId) fail('SUCCESSOR_ADMISSION_INVALID'); + assertReceiptWindow(rollback, admission, createdAt, expiresAt, nowMs); + + for (const key of Object.keys(manifest.requiredWriterFences).sort()) { + const fence = readPrivateCanonicalReceipt( + receiptDirectory, + `writer-fence-${key}.json`, + admission.writerFences[key].evidenceSha256, + ); + exactKeys(fence, [ + 'expiresAt', 'fence', 'id', 'kind', 'manifestSha256', 'observedAt', 'projectRef', + 'purpose', 'schemaVersion', 'sourceRevision', 'state', + ], 'SUCCESSOR_ADMISSION_INVALID'); + if (!common(fence) + || fence.kind !== 'writer-fence-readback-receipt' + || fence.fence !== key + || fence.state !== manifest.requiredWriterFences[key]) fail('SUCCESSOR_ADMISSION_INVALID'); + assertReceiptWindow(fence, admission, createdAt, expiresAt, nowMs); + } +} + +export function validateSuccessorAdmission(admission, manifestRecord, { + now = new Date(), + receiptDirectory, +} = {}) { + const { manifest, manifestSha256 } = manifestRecord; + exactKeys(admission, [ + 'attemptId', + 'createdAt', + 'expiresAt', + 'id', + 'journalPathSha256', + 'manifestSha256', + 'projectRef', + 'protectedMainReceiptSha256', + 'purpose', + 'rehearsalReceiptSha256', + 'rollback', + 'schemaVersion', + 'sourceReceiptSha256', + 'sourceRevision', + 'sourceRoot', + 'stageStates', + 'writerFences', + ], 'SUCCESSOR_ADMISSION_INVALID'); + if (admission.schemaVersion !== 1 + || admission.id !== manifest.id + || admission.purpose !== manifest.purpose + || admission.projectRef !== manifest.projectRef + || admission.manifestSha256 !== manifestSha256 + || admission.sourceRoot !== manifest.sourceRoot + || !UUID.test(admission.attemptId) + || !REVISION.test(admission.sourceRevision)) fail('SUCCESSOR_ADMISSION_INVALID'); + for (const field of ['protectedMainReceiptSha256', 'rehearsalReceiptSha256', 'sourceReceiptSha256']) { + assertHex(admission[field], 'SUCCESSOR_ADMISSION_INVALID'); + } + assertHex(admission.journalPathSha256, 'SUCCESSOR_ADMISSION_INVALID'); + exactKeys(admission.rollback, [ + 'deploymentId', 'deploymentSha', 'deploymentUrl', 'gitRef', 'productionAliases', + 'projectId', 'readbackSha256', 'readyState', 'repository', 'state', 'target', 'teamId', + ], 'SUCCESSOR_ADMISSION_INVALID'); + if (!REVISION.test(admission.rollback.deploymentSha)) fail('SUCCESSOR_ADMISSION_INVALID'); + assertHex(admission.rollback.readbackSha256, 'SUCCESSOR_ADMISSION_INVALID'); + let normalizedRollback; + try { + normalizedRollback = normalizeVercelRollbackExpected({ + deploymentId: admission.rollback.deploymentId, + deploymentUrl: admission.rollback.deploymentUrl, + gitRef: admission.rollback.gitRef, + gitSha: admission.rollback.deploymentSha, + }); + } catch { fail('SUCCESSOR_ADMISSION_INVALID'); } + if (normalizedRollback.deploymentUrl !== admission.rollback.deploymentUrl + || admission.rollback.projectId !== TZUDONG_VERCEL_PROJECT_ID + || admission.rollback.teamId !== TZUDONG_VERCEL_TEAM_ID + || admission.rollback.repository !== TZUDONG_GITHUB_REPOSITORY + || !equal(admission.rollback.productionAliases, TZUDONG_PRODUCTION_ALIASES) + || admission.rollback.readyState !== 'READY' + || admission.rollback.target !== 'production' + || admission.rollback.state !== 'ready') fail('SUCCESSOR_ADMISSION_INVALID'); + + const createdAt = Date.parse(admission.createdAt); + const expiresAt = Date.parse(admission.expiresAt); + const nowMs = now.getTime(); + if (!Number.isFinite(createdAt) || !Number.isFinite(expiresAt) + || expiresAt <= createdAt + || expiresAt - createdAt > ADMISSION_MAX_AGE_MS + || createdAt > nowMs + 30_000 + || nowMs >= expiresAt) fail('SUCCESSOR_ADMISSION_EXPIRED'); + + const fenceKeys = Object.keys(manifest.requiredWriterFences).sort(); + exactKeys(admission.writerFences, fenceKeys, 'SUCCESSOR_ADMISSION_INVALID'); + for (const key of fenceKeys) { + const fence = admission.writerFences[key]; + exactKeys(fence, ['evidenceSha256', 'state'], 'SUCCESSOR_ADMISSION_INVALID'); + if (fence.state !== manifest.requiredWriterFences[key]) fail('SUCCESSOR_ADMISSION_INVALID'); + assertHex(fence.evidenceSha256, 'SUCCESSOR_ADMISSION_INVALID'); + } + + if (!Array.isArray(admission.stageStates) + || admission.stageStates.length !== manifest.migrations.length + 1) fail('SUCCESSOR_ADMISSION_INVALID'); + const suffixRoot = elevenMigrationBundleSuffixRoot(manifest.migrations); + const priorCount = admission.stageStates[0]?.bundle?.priorCount; + const prefixRoot = admission.stageStates[0]?.bundle?.prefixRoot; + if (priorCount !== manifest.operatingTransition.beforeEleven + || priorCount + manifest.migrations.length !== manifest.operatingTransition.afterEleven) { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + assertHex(prefixRoot, 'SUCCESSOR_ADMISSION_INVALID'); + admission.stageStates.forEach((stage, index) => validateStageState( + stage, + index, + manifest, + suffixRoot, + priorCount, + prefixRoot, + )); + const g014Roots = admission.stageStates.map(stage => stage.state.g014Root); + if (g014Roots[1] !== g014Roots[0] + || g014Roots[2] !== g014Roots[0] + || new Set(g014Roots.slice(4)).size !== 1) { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + const ledgerRoots = admission.stageStates.map(stage => stage.state.ledgerRoot); + if (new Set(ledgerRoots).size !== ledgerRoots.length) fail('SUCCESSOR_ADMISSION_INVALID'); + validatePrivateReceipts(admission, manifestRecord, { + createdAt, + expiresAt, + nowMs, + receiptDirectory, + }); + return Object.freeze({ createdAt, expiresAt, prefixRoot, priorCount, suffixRoot }); +} + +function assertAdmissionFresh(expiresAt, value) { + const nowMs = value instanceof Date ? value.getTime() : NaN; + if (!Number.isFinite(nowMs) || nowMs >= expiresAt) fail('SUCCESSOR_ADMISSION_EXPIRED'); +} + +function assertLiveRollbackReadback(admission, observed, { checkedAt, createdAt, expiresAt }) { + exactKeys(observed, [ + 'deploymentId', 'deploymentUrl', 'gitRef', 'gitSha', 'kind', 'observedAt', + 'productionAliases', 'projectId', 'readyState', 'repository', 'schemaVersion', + 'target', 'teamId', + ], 'SUCCESSOR_ROLLBACK_READBACK_INVALID'); + const liveObservedAt = Date.parse(observed.observedAt); + if (observed.schemaVersion !== 1 + || observed.kind !== 'vercel-rollback-readback' + || observed.deploymentId !== admission.rollback.deploymentId + || observed.deploymentUrl !== admission.rollback.deploymentUrl + || observed.gitRef !== admission.rollback.gitRef + || observed.gitSha !== admission.rollback.deploymentSha + || !equal(observed.productionAliases, admission.rollback.productionAliases) + || observed.projectId !== admission.rollback.projectId + || observed.readyState !== admission.rollback.readyState + || observed.repository !== admission.rollback.repository + || observed.target !== admission.rollback.target + || observed.teamId !== admission.rollback.teamId + || !Number.isFinite(liveObservedAt) + || liveObservedAt < createdAt + || liveObservedAt >= expiresAt + || liveObservedAt > checkedAt.getTime()) fail('SUCCESSOR_ROLLBACK_READBACK_INVALID'); +} + +export function assertLaunchReady(manifest) { + if (manifest.launchPolicy.state !== 'ready') fail('SUCCESSOR_LAUNCH_HELD'); +} + +export function compileSuccessorPlan(manifestRecord, admission) { + const { manifest, materials } = manifestRecord; + const suffixRoot = elevenMigrationBundleSuffixRoot(manifest.migrations); + const stateQuery = buildSuccessorStateQuery(manifest, suffixRoot); + const migrations = manifest.migrations.map((migration, index) => ({ + id: migration.id, + path: migration.path, + sha256: migration.sha256, + statementVectorSha256: migration.statementVectorSha256, + expectedPriorState: { query: stateQuery, expected: admission.stageStates[index] }, + terminalReadback: { query: stateQuery, expected: admission.stageStates[index + 1] }, + })); + const prior = admission.stageStates[0]; + const terminal = admission.stageStates.at(-1); + const compiled = compileElevenMigrationBundle({ + schemaVersion: 1, + id: manifest.id, + priorCount: prior.bundle.priorCount, + prefixRoot: prior.bundle.prefixRoot, + suffixRoot, + migrations, + priorReadback: { query: stateQuery, expected: prior }, + finalReadback: { query: stateQuery, expected: terminal }, + }, materials); + const lockTag = `$successor_${sha256(manifest.advisoryLockKey).slice(0, 20)}$`; + const expiryTag = `$successor_expiry_${sha256(admission.expiresAt).slice(0, 20)}$`; + const guardedSql = `SET LOCAL lock_timeout='2s';\nSET LOCAL statement_timeout='15s';\nSET LOCAL idle_in_transaction_session_timeout='30s';\nDO ${lockTag} BEGIN PERFORM pg_advisory_xact_lock(${manifest.advisoryLockKey}::bigint); END ${lockTag};\nDO ${expiryTag} BEGIN IF clock_timestamp() >= ${quote(admission.expiresAt)}::timestamptz THEN RAISE EXCEPTION 'SUCCESSOR_ADMISSION_EXPIRED'; END IF; END ${expiryTag};\n${compiled.sql}`; + return Object.freeze({ + ...compiled, + sql: guardedSql, + stateQuery, + }); +} + +function checkoutPath(path, code, { mustExist = true } = {}) { + if (typeof path !== 'string' || !isAbsolute(path)) fail(code); + const absolute = resolve(path); + const comparison = mustExist ? realpathSync(absolute) : realpathSync(dirname(absolute)); + const rel = relative(realpathSync(REPOSITORY_ROOT), comparison); + if (!rel || (rel !== '..' && !rel.startsWith(`..${sep}`))) fail(code); + return absolute; +} + +function assertPrivateAdmissionPath(path) { + const absolute = checkoutPath(path, 'SUCCESSOR_ADMISSION_INVALID'); + const status = lstatSync(absolute); + if (!status.isFile() || status.isSymbolicLink()) fail('SUCCESSOR_ADMISSION_INVALID'); + if (process.platform !== 'win32' && (status.mode & 0o077) !== 0) fail('SUCCESSOR_ADMISSION_INVALID'); + return absolute; +} + +function assertJournalPath(path) { + const absolute = checkoutPath(path, 'SUCCESSOR_JOURNAL_INVALID', { mustExist: false }); + if (existsSync(absolute)) fail('SUCCESSOR_JOURNAL_EXISTS'); + const parent = lstatSync(dirname(absolute)); + if (!parent.isDirectory() || parent.isSymbolicLink()) fail('SUCCESSOR_JOURNAL_INVALID'); + if (process.platform !== 'win32' && (parent.mode & 0o077) !== 0) fail('SUCCESSOR_JOURNAL_INVALID'); + return absolute; +} + +function writeJournalStart(path, entry) { + let descriptor; + try { + descriptor = openSync(path, 'wx', 0o600); + writeSync(descriptor, `${JSON.stringify(canonical(entry))}\n`); + fsyncSync(descriptor); + return descriptor; + } catch (error) { + if (descriptor !== undefined) closeSync(descriptor); + if (safeCode(error) === 'EEXIST') fail('SUCCESSOR_JOURNAL_EXISTS'); + fail('SUCCESSOR_JOURNAL_INVALID'); + } +} + +function appendJournal(descriptor, entry) { + try { + writeSync(descriptor, `${JSON.stringify(canonical(entry))}\n`); + fsyncSync(descriptor); + closeSync(descriptor); + } catch { + try { closeSync(descriptor); } catch { /* fixed outcome below */ } + fail('SUCCESSOR_JOURNAL_APPEND_FAILED'); + } +} + +function gitCommand(args, repositoryRoot = REPOSITORY_ROOT) { + const result = spawnSync('/usr/bin/git', args, { cwd: repositoryRoot, encoding: 'utf8' }); + if (result.error || result.status !== 0) fail('SUCCESSOR_CHECKOUT_INVALID'); + return result.stdout.trim(); +} + +export function currentSuccessorGitFacts({ repositoryRoot = REPOSITORY_ROOT } = {}) { + const revision = gitCommand(['rev-parse', 'HEAD'], repositoryRoot); + const dirty = gitCommand(['status', '--porcelain=v1'], repositoryRoot); + const symbolic = spawnSync('/usr/bin/git', ['symbolic-ref', '-q', '--short', 'HEAD'], { + cwd: repositoryRoot, + encoding: 'utf8', + }); + if (symbolic.error || ![0, 1].includes(symbolic.status)) fail('SUCCESSOR_CHECKOUT_INVALID'); + return { revision, clean: dirty === '', detached: symbolic.status === 1 }; +} + +export function protectedSourceReadbackBinding(manifest, revision) { + if (!manifest?.protectedSource || !REVISION.test(revision ?? '')) fail('SUCCESSOR_ADMISSION_INVALID'); + exactKeys(manifest.protectedSource, ['ref', 'remote', 'repositoryUrl'], 'SUCCESSOR_ADMISSION_INVALID'); + return Object.freeze({ + projectRef: manifest.projectRef, + ref: manifest.protectedSource.ref, + remote: manifest.protectedSource.remote, + repositoryUrl: manifest.protectedSource.repositoryUrl, + revision, + schemaVersion: 1, + }); +} + +export function currentProtectedMainReadback(manifest, { + repositoryRoot = REPOSITORY_ROOT, + spawnImpl = spawnSync, +} = {}) { + if (!manifest?.protectedSource) fail('SUCCESSOR_CHECKOUT_INVALID'); + const run = args => { + const result = spawnImpl('/usr/bin/git', args, { + cwd: repositoryRoot, + encoding: 'utf8', + killSignal: 'SIGKILL', + maxBuffer: 1024 * 1024, + timeout: GIT_REMOTE_READ_TIMEOUT_MS, + }); + if (result.error || result.status !== 0) fail('SUCCESSOR_CHECKOUT_INVALID'); + return result.stdout.trim(); + }; + const repositoryUrl = run(['remote', 'get-url', manifest.protectedSource.remote]); + if (repositoryUrl !== manifest.protectedSource.repositoryUrl) fail('SUCCESSOR_CHECKOUT_INVALID'); + const output = run([ + 'ls-remote', + '--exit-code', + '--refs', + manifest.protectedSource.remote, + manifest.protectedSource.ref, + ]); + const rows = output.split(/\r?\n/).filter(Boolean); + const match = rows.length === 1 ? /^([0-9a-f]{40})\t(.+)$/.exec(rows[0]) : null; + if (!match || match[2] !== manifest.protectedSource.ref) fail('SUCCESSOR_CHECKOUT_INVALID'); + return protectedSourceReadbackBinding(manifest, match[1]); +} + +function assertCheckout(admission, facts) { + if (!facts || facts.revision !== admission.sourceRevision || facts.clean !== true || facts.detached !== true) { + fail('SUCCESSOR_CHECKOUT_INVALID'); + } +} + +function assertProtectedSourceReadback(admission, manifest, observed) { + const expected = protectedSourceReadbackBinding(manifest, admission.sourceRevision); + if (!equal(observed, expected)) fail('SUCCESSOR_CHECKOUT_INVALID'); +} + +export function assertSuccessorDatabaseTarget(databaseUrl, manifest) { + let target; + try { target = new URL(databaseUrl); } catch { fail('SUCCESSOR_DATABASE_TARGET_INVALID'); } + const malformedPercentEncoding = value => /%(?![0-9a-f]{2})/i.test(value); + const queryKeys = [...target.searchParams.keys()]; + const sslModes = target.searchParams.getAll('sslmode'); + const safeQuery = queryKeys.length === 0 + || (queryKeys.length === 1 + && queryKeys[0] === 'sslmode' + && sslModes.length === 1 + && ['require', 'verify-ca', 'verify-full'].includes(sslModes[0])); + const directHost = target.hostname === `db.${manifest.projectRef}.supabase.co`; + const sharedPoolerHost = /^aws-[0-9]+-[a-z0-9]+(?:-[a-z0-9]+)*\.pooler\.supabase\.com$/.test(target.hostname); + const directTarget = directHost + && target.username === 'postgres' + && ['5432', '6543'].includes(target.port); + const sharedPoolerTarget = sharedPoolerHost + && target.username === `postgres.${manifest.projectRef}` + && ['5432', '6543'].includes(target.port); + if (!['postgres:', 'postgresql:'].includes(target.protocol) + || target.hash + || malformedPercentEncoding(target.username) + || malformedPercentEncoding(target.password) + || malformedPercentEncoding(target.pathname) + || malformedPercentEncoding(target.search) + || target.pathname !== `/${manifest.databaseName}` + || !safeQuery + || (!directTarget && !sharedPoolerTarget)) fail('SUCCESSOR_DATABASE_TARGET_INVALID'); +} + +function transportError(result) { + if (result.error) return operationError('MIGRATION_PSQL_EXECUTION_FAILED'); + const stderr = typeof result.stderr === 'string' ? result.stderr : ''; + const sqlstate = /ERROR:\s+([0-9A-Z]{5}):/m.exec(stderr)?.[1]; + const error = operationError(sqlstate ? `MIGRATION_PSQL_FAILED_${sqlstate}` : 'MIGRATION_PSQL_FAILED'); + error.fixedSqlCode = /ERROR:\s+(?:[0-9A-Z]{5}:\s*)?(MIGRATION_[A-Z0-9_]{1,96})\b/m.exec(stderr)?.[1]; + return error; +} + +export function createSuccessorPsqlRunner(databaseUrl, { + environment = process.env, + psql = environment.TZUDONG_SUCCESSOR_PSQL || 'psql', + spawnImpl = spawnSync, +} = {}) { + return (_databaseUrl, sql, singleTransaction) => { + const args = ['--no-psqlrc', '--set=ON_ERROR_STOP=1', '--quiet', '--tuples-only', '--no-align']; + if (singleTransaction) args.splice(1, 0, '--single-transaction'); + args.push('--file=-'); + const result = spawnImpl(psql, args, { + encoding: 'utf8', + env: psqlConnectionEnvironment(databaseUrl, environment), + input: `\\set VERBOSITY verbose\n${sql}`, + maxBuffer: 16 * 1024 * 1024, + }); + if (result.error || result.status !== 0) throw transportError(result); + return result.stdout || ''; + }; +} + +function parseReadback(output) { + if (typeof output !== 'string' || !output.trim()) fail('SUCCESSOR_READBACK_INVALID'); + return parseJsonObject(output.trim(), 'SUCCESSOR_READBACK_INVALID'); +} + +export function runReviewMediaSuccessor({ + admissionPath, + journalPath, + environment = process.env, +}, { + now = () => new Date(), + gitFactsImpl = currentSuccessorGitFacts, + loadManifestImpl = loadSuccessorManifest, + protectedMainReadbackImpl = currentProtectedMainReadback, + rollbackReadbackImpl = captureVercelRollbackReadbackSync, + runPsqlImpl, +} = {}) { + const manifestRecord = loadManifestImpl(); + assertLaunchReady(manifestRecord.manifest); + const admissionFile = assertPrivateAdmissionPath(admissionPath); + const admissionBytes = readFileSync(admissionFile); + const admission = parseJsonObject(admissionBytes, 'SUCCESSOR_ADMISSION_INVALID'); + const admissionWindow = validateSuccessorAdmission(admission, manifestRecord, { + now: now(), + receiptDirectory: dirname(admissionFile), + }); + assertCheckout(admission, gitFactsImpl()); + assertProtectedSourceReadback( + admission, + manifestRecord.manifest, + protectedMainReadbackImpl(manifestRecord.manifest), + ); + let rollbackReadback; + try { + rollbackReadback = rollbackReadbackImpl({ + expected: { + deploymentId: admission.rollback.deploymentId, + deploymentUrl: admission.rollback.deploymentUrl, + gitRef: admission.rollback.gitRef, + gitSha: admission.rollback.deploymentSha, + }, + now, + }); + } catch { + fail('SUCCESSOR_ROLLBACK_READBACK_INVALID'); + } + const rollbackCheckedAt = now(); + assertAdmissionFresh(admissionWindow.expiresAt, rollbackCheckedAt); + try { + assertLiveRollbackReadback(admission, rollbackReadback, { + ...admissionWindow, + checkedAt: rollbackCheckedAt, + }); + } catch { + fail('SUCCESSOR_ROLLBACK_READBACK_INVALID'); + } + const journalFile = assertJournalPath(journalPath); + if (sha256(Buffer.from(journalFile)) !== admission.journalPathSha256) fail('SUCCESSOR_JOURNAL_BINDING_MISMATCH'); + const plan = compileSuccessorPlan(manifestRecord, admission); + const { databaseUrl } = selectDirectDatabaseTransport(environment); + assertSuccessorDatabaseTarget(databaseUrl, manifestRecord.manifest); + const transport = runPsqlImpl ?? createSuccessorPsqlRunner(databaseUrl, { environment }); + + let observed; + try { + observed = parseReadback(transport( + databaseUrl, + `SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;\n${plan.stateQuery};`, + true, + )); + } catch (error) { + throw boundedSuccessorError(error); + } + if (!equal(observed, plan.priorReadback.expected)) fail('SUCCESSOR_PREFLIGHT_MISMATCH'); + + let finalRollbackReadback; + try { + finalRollbackReadback = rollbackReadbackImpl({ + expected: { + deploymentId: admission.rollback.deploymentId, + deploymentUrl: admission.rollback.deploymentUrl, + gitRef: admission.rollback.gitRef, + gitSha: admission.rollback.deploymentSha, + }, + now, + }); + } catch { + fail('SUCCESSOR_ROLLBACK_READBACK_INVALID'); + } + const finalRollbackCheckedAt = now(); + assertAdmissionFresh(admissionWindow.expiresAt, finalRollbackCheckedAt); + assertLiveRollbackReadback(admission, finalRollbackReadback, { + ...admissionWindow, + checkedAt: finalRollbackCheckedAt, + }); + + const admissionSha256 = sha256(admissionBytes); + const descriptor = writeJournalStart(journalFile, { + admissionSha256, + attemptId: admission.attemptId, + event: 'started', + manifestSha256: manifestRecord.manifestSha256, + sourceRevision: admission.sourceRevision, + sourceRoot: manifestRecord.manifest.sourceRoot, + startedAt: now().toISOString(), + }); + let result; + try { + assertAdmissionFresh(admissionWindow.expiresAt, now()); + result = executeMigrationBundle(databaseUrl, plan, { runPsqlImpl: transport }); + } catch (error) { + const bounded = boundedSuccessorError(error); + appendJournal(descriptor, { + attemptId: admission.attemptId, + code: bounded.code, + completedAt: now().toISOString(), + event: 'terminal', + }); + throw bounded; + } + appendJournal(descriptor, { + attemptId: admission.attemptId, + code: 'SUCCESSOR_COMMITTED', + completedAt: now().toISOString(), + event: 'terminal', + }); + return Object.freeze({ + attemptId: admission.attemptId, + code: 'SUCCESSOR_COMMITTED', + sourceRevision: admission.sourceRevision, + status: 'committed', + terminal: result, + }); +} + +function parseArguments(argv) { + const result = {}; + for (let index = 0; index < argv.length; index += 2) { + const option = argv[index]; + const value = argv[index + 1]; + if (!['--admission', '--journal'].includes(option) || !value || value.startsWith('--') || option.slice(2) in result) { + fail('SUCCESSOR_ARGUMENT_INVALID'); + } + result[option.slice(2)] = value; + } + if (argv.length !== 4 || !result.admission || !result.journal) fail('SUCCESSOR_ARGUMENT_INVALID'); + return result; +} + +export function main(argv = process.argv.slice(2), dependencies) { + const args = parseArguments(argv); + return runReviewMediaSuccessor({ + admissionPath: args.admission, + journalPath: args.journal, + }, dependencies); +} + +if (process.argv[1] && resolve(process.argv[1]) === SCRIPT_PATH) { + try { + console.log(JSON.stringify(main())); + } catch (error) { + const bounded = boundedSuccessorError(error); + console.error(JSON.stringify({ code: bounded.code, status: 'failed' })); + process.exitCode = 1; + } +} diff --git a/apps/web/scripts/supabase-migration-bundle.mjs b/apps/web/scripts/supabase-migration-bundle.mjs index b41250d338..9c8826ce95 100644 --- a/apps/web/scripts/supabase-migration-bundle.mjs +++ b/apps/web/scripts/supabase-migration-bundle.mjs @@ -10,6 +10,7 @@ import { const BUNDLE_SIZE = 4; const SUCCESSOR_BUNDLE_SIZE = 5; +const REVIEW_MEDIA_SUCCESSOR_BUNDLE_SIZE = 11; const HEX_SHA256 = /^[0-9a-f]{64}$/; const MIGRATION_ID = /^[a-z0-9_]{1,80}$/; @@ -152,6 +153,10 @@ export function fiveMigrationBundleSuffixRoot(migrations) { return bundleSuffixRoot(migrations, SUCCESSOR_BUNDLE_SIZE); } +export function elevenMigrationBundleSuffixRoot(migrations) { + return bundleSuffixRoot(migrations, REVIEW_MEDIA_SUCCESSOR_BUNDLE_SIZE); +} + function validateBundle(bundle, materials, expectedSize) { exactKeys(bundle, [ 'finalReadback', @@ -277,6 +282,10 @@ export function compileFiveMigrationBundle(bundle, materials) { return compileMigrationSequence(bundle, materials, SUCCESSOR_BUNDLE_SIZE); } +export function compileElevenMigrationBundle(bundle, materials) { + return compileMigrationSequence(bundle, materials, REVIEW_MEDIA_SUCCESSOR_BUNDLE_SIZE); +} + export function bundleReconciliationSql(compiled) { const { query } = validatedReadbackParts(compiled.finalReadback); return `SET TRANSACTION READ ONLY; SET LOCAL standard_conforming_strings=on; SET LOCAL statement_timeout='15s';\n${query};\n`; diff --git a/apps/web/tests-unit/review-media-migration-bundle.test.ts b/apps/web/tests-unit/review-media-migration-bundle.test.ts new file mode 100644 index 0000000000..25c779f42b --- /dev/null +++ b/apps/web/tests-unit/review-media-migration-bundle.test.ts @@ -0,0 +1,116 @@ +import { expect, test } from 'bun:test'; +import { readFileSync } from 'node:fs'; + +import { + bundleReconciliationOutcome, + compileElevenMigrationBundle, + elevenMigrationBundleSuffixRoot, + executeMigrationBundle, +} from '../scripts/supabase-migration-bundle.mjs'; +import { statementSpans } from '../scripts/supabase-migration-transaction.mjs'; + +const manifest = JSON.parse(readFileSync( + new URL('../../../.github/review-media-sql-successor.v1.json', import.meta.url), + 'utf8', +)); + +function fixture() { + const prefixRoot = 'a'.repeat(64); + const priorCount = 86; + const materials = manifest.migrations.map((migration: any) => ({ + path: migration.path, + bytes: readFileSync(new URL(`../../../${migration.path}`, import.meta.url)), + originalVector: migration.originalStatementVector, + })); + const migrations = manifest.migrations.map((migration: any, index: number) => ({ + id: migration.id, + path: migration.path, + sha256: migration.sha256, + statementVectorSha256: migration.statementVectorSha256, + expectedPriorState: { + query: "SELECT '{\"fixture\":true}'::text AS state", + expected: { fixture: true, stage: index }, + }, + terminalReadback: { + query: "SELECT '{\"fixture\":true}'::text AS state", + expected: { fixture: true, stage: index + 1 }, + }, + })); + const suffixRoot = elevenMigrationBundleSuffixRoot(migrations); + const query = "SELECT '{\"fixture\":true}'::text AS state"; + const priorReadback = { + query, + expected: { bundle: { ledgerCount: 86, prefixRoot, priorCount, suffixRoot: null, targetCount: 0 } }, + }; + const finalReadback = { + query, + expected: { bundle: { ledgerCount: 97, prefixRoot, priorCount, suffixRoot, targetCount: 11 } }, + }; + migrations[0].expectedPriorState = priorReadback; + return { + bundle: { + schemaVersion: 1, + id: 'review_media_sql_successor_v1', + priorCount, + prefixRoot, + suffixRoot, + migrations, + priorReadback, + finalReadback, + }, + materials, + }; +} + +test('all eleven exact sources compile into one outer transaction', () => { + const input = fixture(); + const compiled = compileElevenMigrationBundle(input.bundle, input.materials); + const spans = statementSpans(compiled.sql); + expect(compiled.transactionMode).toBe('single-outer-transaction'); + expect(compiled.plans).toHaveLength(11); + expect(compiled.sql.match(/INSERT INTO supabase_migrations\.schema_migrations/g)).toHaveLength(11); + expect(spans.filter(span => /^SELECT\b/i.test(span.token))).toHaveLength(1); + expect(spans.some(span => /^(?:BEGIN|COMMIT|ROLLBACK|ABORT|START\s+TRANSACTION)\b/i.test(span.token))).toBe(false); + expect(spans.at(-1)?.token).toStartWith("SELECT '{\"fixture\":true}'"); +}); + +test('eleven-stage compiler rejects source, vector, order and cardinality drift before transport', () => { + { + const input = fixture(); + input.materials[0].bytes = Buffer.concat([input.materials[0].bytes, Buffer.from(' ')]); + expect(() => compileElevenMigrationBundle(input.bundle, input.materials)).toThrow('MIGRATION_FILE_DIGEST_MISMATCH'); + } + { + const input = fixture(); + input.materials[0].originalVector = [...input.materials[0].originalVector, 'SELECT 1']; + expect(() => compileElevenMigrationBundle(input.bundle, input.materials)).toThrow('MIGRATION_VECTOR_MISMATCH'); + } + { + const input = fixture(); + [input.bundle.migrations[0], input.bundle.migrations[1]] = [input.bundle.migrations[1], input.bundle.migrations[0]]; + [input.materials[0], input.materials[1]] = [input.materials[1], input.materials[0]]; + expect(() => compileElevenMigrationBundle(input.bundle, input.materials)).toThrow('ORDER_OR_IDENTITY_INVALID'); + } + { + const input = fixture(); + input.bundle.migrations.pop(); + input.materials.pop(); + expect(() => compileElevenMigrationBundle(input.bundle, input.materials)).toThrow('MIGRATION_BUNDLE_INVALID'); + } +}); + +test('lost acknowledgement reconciles exactly once and never resends the eleven mutations', () => { + const input = fixture(); + const compiled = compileElevenMigrationBundle(input.bundle, input.materials); + const calls: string[] = []; + expect(() => executeMigrationBundle('fixture-db', compiled, { runPsqlImpl: (_url, sql) => { + calls.push(sql); + if (calls.length === 1) throw new Error('lost acknowledgement'); + return JSON.stringify(compiled.finalReadback.expected); + } })).toThrow('MIGRATION_BUNDLE_OUTCOME_UNCONFIRMED'); + expect(calls).toHaveLength(2); + expect(calls[0].match(/INSERT INTO supabase_migrations\.schema_migrations/g)).toHaveLength(11); + expect(calls[1]).not.toContain('INSERT INTO'); + expect(bundleReconciliationOutcome(compiled.finalReadback.expected, compiled)).toBe('committed'); + expect(bundleReconciliationOutcome(compiled.priorReadback.expected, compiled)).toBe('not_applied'); +}); diff --git a/apps/web/tests-unit/review-media-sql-successor.test.ts b/apps/web/tests-unit/review-media-sql-successor.test.ts new file mode 100644 index 0000000000..df0ab216b5 --- /dev/null +++ b/apps/web/tests-unit/review-media-sql-successor.test.ts @@ -0,0 +1,392 @@ +import { afterAll, expect, test } from 'bun:test'; +import { createHash } from 'node:crypto'; +import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { + compileSuccessorPlan, + loadSuccessorManifest, + protectedSourceReadbackBinding, + runReviewMediaSuccessor, + validateSuccessorAdmission, +} from '../scripts/review-media-sql-successor.mjs'; +import { elevenMigrationBundleSuffixRoot } from '../scripts/supabase-migration-bundle.mjs'; +import { statementSpans } from '../scripts/supabase-migration-transaction.mjs'; + +const manifestBytes = readFileSync(new URL('../../../.github/review-media-sql-successor.v1.json', import.meta.url)); +const manifest = JSON.parse(manifestBytes.toString('utf8')); +const record = Object.freeze({ + bytes: manifestBytes, + manifest, + manifestSha256: createHash('sha256').update(manifestBytes).digest('hex'), + materials: Object.freeze(manifest.migrations.map((migration: any) => ({ + path: migration.path, + bytes: readFileSync(new URL(`../../../${migration.path}`, import.meta.url)), + originalVector: migration.originalStatementVector, + }))), +}); +const executableRecord = Object.freeze({ + ...record, + manifest: Object.freeze({ + ...record.manifest, + launchPolicy: Object.freeze({ ...record.manifest.launchPolicy, state: 'ready' }), + }), +}); +const fixedNow = new Date('2026-10-12T08:00:00.000Z'); +const revision = '056a86600f412eb42df4304e3206841679012f3e'; +const databaseUrlFixture = new URL('postgresql://localhost/postgres'); +databaseUrlFixture.hostname = 'aws-0-ap-southeast-1.pooler.supabase.com'; +databaseUrlFixture.username = 'postgres.aqlcofblfxdrjhhdmarw'; +databaseUrlFixture.port = '6543'; +databaseUrlFixture.password = 'fixture-only'; +const databaseUrl = databaseUrlFixture.toString(); +const rollback = Object.freeze({ + deploymentId: 'dpl_EUxJnWmDDozCSMWb2sqNSPgKJLM5', + deploymentSha: '1884f87e53c4567ead7c9a0fb394b09b91a70396', + deploymentUrl: 'https://tzudong-fixture-twoimos-projects.vercel.app/', + gitRef: 'main', + productionAliases: ['tzudong.app', 'www.tzudong.app'], + projectId: 'prj_sau35J5uUtShIQ9OKofRtOVVnTSl', + readyState: 'READY', + repository: 'twoimo/tzudong', + state: 'ready', + target: 'production', + teamId: 'team_OUj64KeLxJI3PkEbOaFZnorA', +}); +const directories: string[] = []; +const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex'); +const canonical = (value: any): any => Array.isArray(value) + ? value.map(canonical) + : value && typeof value === 'object' + ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) + : value; +const canonicalBytes = (value: any) => Buffer.from(`${JSON.stringify(canonical(value))}\n`); + +afterAll(() => { + for (const directory of directories) rmSync(directory, { recursive: true, force: true }); +}); + +function stageStates() { + const prefixRoot = '1'.repeat(64); + return Array.from({ length: 12 }, (_, index) => ({ + bundle: { + ledgerCount: 86 + index, + prefixRoot, + priorCount: 86, + suffixRoot: index === 11 ? elevenMigrationBundleSuffixRoot(manifest.migrations) : null, + targetCount: index, + }, + state: { + activeReviewItems: 0, + currentUser: 'postgres', + databaseName: 'postgres', + finishCleanupBodySha256: index === 10 + ? manifest.terminalContract.finishPreimageSha256 + : index === 11 + ? manifest.terminalContract.finishPostimageSha256 + : index === 0 ? null : (index + 2).toString(16).repeat(64), + g014Root: (index < 3 ? 'a' : index === 3 ? 'b' : 'c').repeat(64), + ledgerRoot: (index + 1).toString(16).repeat(64), + ownerAuthorityReady: true, + reviewItems: 0, + reviewPolicyEnabled: false, + reviewRuns: 0, + schemaRoot: (index + 3).toString(16).repeat(64), + serverMajor: 17, + sessionUser: 'postgres', + storageRoot: (index + 4).toString(16).repeat(64), + terminalContractReady: index === 11, + }, + })); +} + +function admission() { + const stages = stageStates(); + const writerFences = Object.fromEntries(Object.entries(manifest.requiredWriterFences).map( + ([key, state], index) => [key, { evidenceSha256: (index + 8).toString(16).repeat(64), state }], + )); + return { + attemptId: '123e4567-e89b-42d3-a456-426614174000', + createdAt: '2026-10-12T07:59:00.000Z', + expiresAt: '2026-10-12T08:10:00.000Z', + id: manifest.id, + journalPathSha256: 'f'.repeat(64), + manifestSha256: record.manifestSha256, + projectRef: manifest.projectRef, + protectedMainReceiptSha256: '1'.repeat(64), + purpose: manifest.purpose, + rehearsalReceiptSha256: '2'.repeat(64), + rollback: { ...structuredClone(rollback), readbackSha256: '3'.repeat(64) }, + schemaVersion: 1, + sourceReceiptSha256: '4'.repeat(64), + sourceRevision: revision, + sourceRoot: manifest.sourceRoot, + stageStates: stages, + writerFences, + }; +} + +function custodyFiles(document = admission()) { + const directory = mkdtempSync(join(tmpdir(), 'tzudong-review-successor-')); + directories.push(directory); + chmodSync(directory, 0o700); + const admissionPath = join(directory, 'admission.json'); + const journalPath = join(directory, 'attempt.jsonl'); + document.journalPathSha256 = sha256(journalPath); + const common = { + schemaVersion: 1, + id: document.id, + projectRef: document.projectRef, + purpose: document.purpose, + manifestSha256: document.manifestSha256, + sourceRevision: document.sourceRevision, + observedAt: document.createdAt, + expiresAt: document.expiresAt, + }; + const receipts: Record = { + 'source-receipt.json': { ...common, kind: 'successor-source-receipt', sourceRoot: document.sourceRoot }, + 'protected-main-receipt.json': { + ...common, + kind: 'protected-main-readback-receipt', + ref: manifest.protectedSource.ref, + remote: manifest.protectedSource.remote, + repositoryUrl: manifest.protectedSource.repositoryUrl, + revision: document.sourceRevision, + }, + 'rehearsal-receipt.json': { + ...common, + kind: 'successor-rehearsal-receipt', + passed: true, + serverVersionNum: 170006, + sourceRoot: document.sourceRoot, + stageStatesSha256: sha256(JSON.stringify(canonical(document.stageStates))), + suffixRoot: elevenMigrationBundleSuffixRoot(manifest.migrations), + }, + 'rollback-readback-receipt.json': { + ...common, + kind: 'rollback-readback-receipt', + deploymentId: document.rollback.deploymentId, + deploymentSha: document.rollback.deploymentSha, + deploymentUrl: document.rollback.deploymentUrl, + gitRef: document.rollback.gitRef, + productionAliases: document.rollback.productionAliases, + projectId: document.rollback.projectId, + readyState: document.rollback.readyState, + repository: document.rollback.repository, + state: document.rollback.state, + target: document.rollback.target, + teamId: document.rollback.teamId, + }, + }; + document.sourceReceiptSha256 = sha256(canonicalBytes(receipts['source-receipt.json'])); + document.protectedMainReceiptSha256 = sha256(canonicalBytes(receipts['protected-main-receipt.json'])); + document.rehearsalReceiptSha256 = sha256(canonicalBytes(receipts['rehearsal-receipt.json'])); + document.rollback.readbackSha256 = sha256(canonicalBytes(receipts['rollback-readback-receipt.json'])); + for (const [key, state] of Object.entries(manifest.requiredWriterFences)) { + const name = `writer-fence-${key}.json`; + receipts[name] = { ...common, fence: key, kind: 'writer-fence-readback-receipt', state }; + document.writerFences[key].evidenceSha256 = sha256(canonicalBytes(receipts[name])); + } + for (const [name, receipt] of Object.entries(receipts)) { + const path = join(directory, name); + writeFileSync(path, canonicalBytes(receipt), { mode: 0o600 }); + chmodSync(path, 0o600); + } + writeFileSync(admissionPath, `${JSON.stringify(document)}\n`, { mode: 0o600 }); + chmodSync(admissionPath, 0o600); + return { admissionPath, directory, document, journalPath }; +} + +function rollbackReadback(now = fixedNow) { + return { + schemaVersion: 1, + kind: 'vercel-rollback-readback', + projectId: rollback.projectId, + teamId: rollback.teamId, + deploymentId: rollback.deploymentId, + deploymentUrl: rollback.deploymentUrl, + readyState: rollback.readyState, + target: rollback.target, + repository: rollback.repository, + gitSha: rollback.deploymentSha, + gitRef: rollback.gitRef, + productionAliases: [...rollback.productionAliases], + observedAt: now.toISOString(), + }; +} + +test('manifest binds the exact eleven sources and remains held without fresh admission', () => { + const loaded = loadSuccessorManifest(); + expect(loaded.manifest.migrations).toHaveLength(11); + expect(loaded.manifest.toolchain[0].path).toBe('apps/web/scripts/review-media-sql-successor.mjs'); + expect(loaded.manifest.operatingTransition).toEqual({ beforeEleven: 86, afterEleven: 97 }); + expect(loaded.manifest.launchPolicy.state).toBe('held'); + expect(loaded.manifest.sourceRoot).toBe('61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544'); + expect(() => loadSuccessorManifest({ readVectorImpl: () => ['drift'] })).toThrow('SUCCESSOR_SOURCE_DRIFT'); +}); + +test('compiled plan has one advisory lock, one outer transaction and eleven ledger writes', () => { + const admitted = admission(); + const plan = compileSuccessorPlan(executableRecord, admitted); + expect(plan.transactionMode).toBe('single-outer-transaction'); + expect(plan.sql.match(/pg_advisory_xact_lock\(7311754282260211::bigint\)/g)).toHaveLength(1); + expect(plan.sql.match(/INSERT INTO supabase_migrations\.schema_migrations/g)).toHaveLength(11); + expect(statementSpans(plan.sql).some(span => /^(?:BEGIN|COMMIT|ROLLBACK)\b/i.test(span.token))).toBe(false); + const lockTimeout = plan.sql.indexOf("SET LOCAL lock_timeout='2s'"); + const statementTimeout = plan.sql.indexOf("SET LOCAL statement_timeout='15s'"); + const advisoryLock = plan.sql.indexOf('pg_advisory_xact_lock(7311754282260211::bigint)'); + const expiryCheck = plan.sql.indexOf('SUCCESSOR_ADMISSION_EXPIRED'); + const firstMigration = plan.sql.indexOf('INSERT INTO supabase_migrations.schema_migrations'); + expect(lockTimeout).toBeGreaterThanOrEqual(0); + expect(statementTimeout).toBeGreaterThan(lockTimeout); + expect(advisoryLock).toBeGreaterThan(statementTimeout); + expect(expiryCheck).toBeGreaterThan(advisoryLock); + expect(firstMigration).toBeGreaterThan(expiryCheck); + expect(plan.sql).toContain(`clock_timestamp() >= '${admitted.expiresAt}'::timestamptz`); + expect(plan.stateQuery).toContain('terminalContractReady'); + expect(plan.stateQuery).toContain(manifest.terminalContract.finishPostimageSha256); + expect(plan.stateQuery).toContain("c.relkind IN('v','m') THEN pg_get_viewdef(c.oid,true)"); + expect(plan.stateQuery).toContain("unnest(coalesce(c.reloptions,'{}'))"); + expect(plan.stateQuery).toContain("unnest(coalesce(a.attacl,'{}'))"); + expect(plan.stateQuery).toContain('m.inherit_option,m.set_option'); + const storageState = plan.stateQuery.slice( + plan.stateQuery.indexOf('storage_material AS'), + plan.stateQuery.indexOf('contract AS'), + ); + expect(storageState).toContain('lifecycle_configuration_generation'); + expect(storageState).not.toContain('created_at'); + expect(storageState).not.toContain('updated_at'); +}); + +test('admission rejects wrong preimage and every wrong writer fence', () => { + { + const files = custodyFiles(); + files.document.stageStates[10].state.finishCleanupBodySha256 = '0'.repeat(64); + expect(() => validateSuccessorAdmission(files.document, executableRecord, { + now: fixedNow, + receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); + } + { + const files = custodyFiles(); + files.document.stageStates[0].state.ownerAuthorityReady = false; + expect(() => validateSuccessorAdmission(files.document, executableRecord, { + now: fixedNow, + receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); + } + for (const key of Object.keys(manifest.requiredWriterFences)) { + const files = custodyFiles(); + files.document.writerFences[key].state = 'wrong'; + expect(() => validateSuccessorAdmission(files.document, executableRecord, { + now: fixedNow, + receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); + } +}); + +test('success path performs preflight, rechecks rollback and commits all eleven once', () => { + const files = custodyFiles(); + const plan = compileSuccessorPlan(executableRecord, files.document); + const calls: string[] = []; + let rollbackReads = 0; + const result = runReviewMediaSuccessor({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => executableRecord, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => { rollbackReads += 1; return rollbackReadback(); }, + runPsqlImpl: (_url, sql) => { + calls.push(sql); + return JSON.stringify(calls.length === 1 ? plan.priorReadback.expected : plan.finalReadback.expected); + }, + }); + expect(result).toMatchObject({ code: 'SUCCESSOR_COMMITTED', status: 'committed' }); + expect(rollbackReads).toBe(2); + expect(calls).toHaveLength(2); + expect(calls[0]).toStartWith('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;'); + expect(calls[0]).not.toMatch(/^INSERT INTO supabase_migrations\.schema_migrations/gm); + expect(calls[1].match(/INSERT INTO supabase_migrations\.schema_migrations/g)).toHaveLength(11); + expect(readFileSync(files.journalPath, 'utf8')).toContain('SUCCESSOR_COMMITTED'); +}); + +test('concurrent rollback deployment change rejects before journal or mutation', () => { + const files = custodyFiles(); + const plan = compileSuccessorPlan(executableRecord, files.document); + let rollbackReads = 0; + const calls: string[] = []; + expect(() => runReviewMediaSuccessor({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => executableRecord, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => { + rollbackReads += 1; + return rollbackReads === 1 ? rollbackReadback() : { ...rollbackReadback(), gitSha: '0'.repeat(40) }; + }, + runPsqlImpl: (_url, sql) => { calls.push(sql); return JSON.stringify(plan.priorReadback.expected); }, + })).toThrow('SUCCESSOR_ROLLBACK_READBACK_INVALID'); + expect(rollbackReads).toBe(2); + expect(calls).toHaveLength(1); + expect(calls[0]).toStartWith('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;'); + expect(calls[0]).not.toMatch(/^INSERT INTO supabase_migrations\.schema_migrations/gm); +}); + +test('prior readback mismatch fails before journal and mutating transport', () => { + const files = custodyFiles(); + const plan = compileSuccessorPlan(executableRecord, files.document); + const mismatch = structuredClone(plan.priorReadback.expected); + mismatch.bundle.ledgerCount += 1; + const calls: string[] = []; + expect(() => runReviewMediaSuccessor({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => executableRecord, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: (_url, sql) => { calls.push(sql); return JSON.stringify(mismatch); }, + })).toThrow('SUCCESSOR_PREFLIGHT_MISMATCH'); + expect(calls).toHaveLength(1); + expect(calls[0]).not.toMatch(/^INSERT INTO supabase_migrations\.schema_migrations/gm); + expect(() => readFileSync(files.journalPath)).toThrow(); +}); + +test('lost apply acknowledgement reconciles once and never resends eleven mutations', () => { + const files = custodyFiles(); + const plan = compileSuccessorPlan(executableRecord, files.document); + const calls: string[] = []; + expect(() => runReviewMediaSuccessor({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => executableRecord, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: (_url, sql) => { + calls.push(sql); + if (calls.length === 1) return JSON.stringify(plan.priorReadback.expected); + if (calls.length === 2) throw new Error('lost acknowledgement'); + return JSON.stringify(plan.finalReadback.expected); + }, + })).toThrow('MIGRATION_BUNDLE_OUTCOME_UNCONFIRMED'); + expect(calls).toHaveLength(3); + expect(calls[1].match(/INSERT INTO supabase_migrations\.schema_migrations/g)).toHaveLength(11); + expect(calls[2]).not.toMatch(/^INSERT INTO supabase_migrations\.schema_migrations/gm); + expect(readFileSync(files.journalPath, 'utf8')).toContain('MIGRATION_BUNDLE_OUTCOME_UNCONFIRMED'); +}); From 4d4ecf608a7922df5a68beaf3e1ea2dcc250a3b7 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:07:34 +0900 Subject: [PATCH 02/14] Require durable rehearsal receipts before atomic review-media apply --- .github/review-media-sql-successor.v1.json | 8 +- .../review-db-rehearsal-20261012/README.md | 91 ++ .../review-db-rehearsal-20261012/SHA256SUMS | 16 + .../SHA256SUMS-pre-hardening-historical | 7 + ...7-rollback-rehearsal-current-20261013.json | 65 ++ ...g17-rollback-rehearsal-final-20261013.json | 65 ++ .../pg17-rollback-rehearsal.json | 62 ++ .../scanner-readback.json | 33 + .../verify-review-media-sql-rehearsal.mjs | 495 +++++++++ .../verify-review-media-sql-successor.mjs | 181 +++- .../scripts/review-media-sql-rehearsal.mjs | 54 + .../scripts/review-media-sql-successor.mjs | 989 ++++++++++++++++-- .../web/scripts/supabase-migration-bundle.mjs | 48 + .../review-media-migration-bundle.test.ts | 46 + ...eview-media-sql-rehearsal-verifier.test.ts | 305 ++++++ ...review-media-sql-successor-harness.test.ts | 146 +++ .../review-media-sql-successor.test.ts | 952 ++++++++++++++++- 17 files changed, 3433 insertions(+), 130 deletions(-) create mode 100644 apps/web/performance/review-db-rehearsal-20261012/README.md create mode 100644 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS create mode 100644 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical create mode 100644 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json create mode 100644 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json create mode 100644 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json create mode 100644 apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json create mode 100644 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs create mode 100644 apps/web/scripts/review-media-sql-rehearsal.mjs create mode 100644 apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts create mode 100644 apps/web/tests-unit/review-media-sql-successor-harness.test.ts diff --git a/.github/review-media-sql-successor.v1.json b/.github/review-media-sql-successor.v1.json index 3dfd6f9b40..1bc26aa656 100644 --- a/.github/review-media-sql-successor.v1.json +++ b/.github/review-media-sql-successor.v1.json @@ -309,11 +309,15 @@ "toolchain": [ { "path": "apps/web/scripts/review-media-sql-successor.mjs", - "sha256": "928d7b8bd601ab585870d4e04ee468e952f6053df81e403de925e032924f40d1" + "sha256": "2080947cbc3c92934c98cee259d4f41e9c1eebc80e437f20862849fd36bd4f40" + }, + { + "path": "apps/web/scripts/review-media-sql-rehearsal.mjs", + "sha256": "c893c294719776bf5b064c3355866ab97e0fa9412082d9b47d7de5266094c65f" }, { "path": "apps/web/scripts/supabase-migration-bundle.mjs", - "sha256": "a374211be376e3a9af8215751accb52eb43943fc93b8651ba4e186e412588ba7" + "sha256": "fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9" }, { "path": "apps/web/scripts/supabase-migration-transaction.mjs", diff --git a/apps/web/performance/review-db-rehearsal-20261012/README.md b/apps/web/performance/review-db-rehearsal-20261012/README.md new file mode 100644 index 0000000000..917f4e8e4a --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/README.md @@ -0,0 +1,91 @@ +# Review-media successor rollback rehearsal + +The held eleven-migration controller now has a separate rollback-only rehearsal +and durable publication protocol. Operating application remains unperformed. + +## Behavior and failure boundaries + +- The rehearsal admission binds the clean detached protected-main source, + rollback identity, seven writer fences, private paths and exact statement vectors. +- The eleven vectors run once in one explicit transaction. State 0 through 11 + is captured before ROLLBACK, and a separate read-only transaction must match + state 0. Transport failure triggers readback only, never a repeated mutation. +- The pending receipt is exclusively created and fsynced; the verification + journal entry is fsynced before the canonical hard link is created. The custody + directory is fsynced before publication confirmation is appended. +- Publication audit fsync failure cannot reconcile from visible bytes. Only a + close acknowledgement failure after successful fsync permits exact-byte + reconciliation. Apply separately validates and fsyncs the exact admitted + receipt and three-entry journal plus their custody directory. Thus an earlier + unconfirmed fsync cannot be silently treated as durable by a later process. +- Apply writes and fsyncs its own start journal and parent directory before any + mutating transport. Five injected failures across receipt, rehearsal journal, + custody directory, start journal and start directory produced zero mutations. +- A failed terminal audit preserves its bounded operation code alongside the + journal failure. Arbitrary diagnostics and throwing getters cannot escape the + bounded failure JSON. +- Both SQL paths check expiry after acquiring the advisory lock. Rehearsal also + checks before each migration; apply checks once after the lock. These are not + claims that all eleven migrations finish before the admission expiry. + +## Current verification + +Pinned Node 24 and Bun 1.4.0 ran four affected suites: **53 passed, 0 failed, +284 assertions**. The checks include publication fsync/close failures, five +pre-mutation durability failures, expiry plus audit failure, tampered/orphan +journals, retained evidence, unique container ownership and lost acknowledgements. +Targeted ESLint and Node syntax checks passed. + +The final source was replayed once in an isolated Supabase PostgreSQL 17.6 +container. It compiled and executed eleven immutable migrations (112 source +statements), captured twelve states, and verified exact restoration after normal +rollback, forced SQL failure, a discarded response and contended expiry. The +lost-response case attempted the mutation once. The owned container was removed. +This database replay exercises the SQL compiler and SQL behavior; filesystem +controller failures are covered by the separate fault-injection tests above. + +The source fixture is local `ranking_clone`, copied with its contract seed rows. +The verifier explicitly checks `public.profiles` and `public.reviews` are empty; +it does not claim a universal personal-data census. Its synthetic ledger has 86 +entries, not an operating ledger readback. + +- Current proof: `pg17-rollback-rehearsal-final-20261013.json` +- Proof SHA-256: `7f74c175be3eee72b5e93eb75dac07bfefc0ef88d7d9957c26d877ee125ff590` +- Manifest SHA-256: `8297b18793513956118c720e3aa5bd481d0769645c263f680a5272825b70569e` +- Toolchain root: `91a7166b76b0f867b4e74dd0e2258d1f22457c75a61e55b161148378f08ac739` +- Source root: `61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544` +- Verifier SHA-256: `8088aa57f39045e2bf93cba084711119395098d053318b70aaa676b5c13fea26` +- Compiled SQL SHA-256: `285d0c859117006646389dbd2a22de9aef1c0b97bc796a9e4d4448a8a232b093` + +## Reproduce without overwriting evidence + +From the repository root: + +```sh +/opt/homebrew/opt/node@24/bin/node \ + apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs +``` + +The existing proof is checksum-verified and reported as historical when its +bindings differ. Each execution prints a current proof to stdout. Optional +`--output /absolute/new-proof.json` creates a separate private file exclusively; +it refuses any existing output. The verifier does not delete the source +container or a fixed-name container: creation and cleanup require a unique name, +per-run owner label and exact inspected container ID. Lost create/delete +acknowledgements are reconciled by ownership/absence readback. + +`pg17-rollback-rehearsal.json` and `SHA256SUMS-pre-hardening-historical` preserve +the earlier pre-hardening evidence. `pg17-rollback-rehearsal-current-20261013.json` +is the intermediate replay before the safe-getter fix. Neither is labelled as +final-source proof. The original `performance/review-db-successor-20261012/` +README, retained proof and SHA256SUMS remain unchanged. + +## Limits and next operating step + +Launch policy remains held. No hosted DDL, operating row read, Storage mutation, +deployment, cost reduction or performance improvement is established here. +Operating execution needs current protected main, fresh actual-ledger rehearsal, +source/rollback and writer-fence receipts, private admission and a new journal. +Storage projections exclude timestamps and cannot distinguish recreation of an +otherwise identical bucket. The retained evidence and source hashes are in +`SHA256SUMS`; raw SQL, credentials and operating rows are absent from the proof. diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS new file mode 100644 index 0000000000..2fbd3f4e24 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS @@ -0,0 +1,16 @@ +8297b18793513956118c720e3aa5bd481d0769645c263f680a5272825b70569e .github/review-media-sql-successor.v1.json +2080947cbc3c92934c98cee259d4f41e9c1eebc80e437f20862849fd36bd4f40 apps/web/scripts/review-media-sql-successor.mjs +c893c294719776bf5b064c3355866ab97e0fa9412082d9b47d7de5266094c65f apps/web/scripts/review-media-sql-rehearsal.mjs +fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs +7ab17835c727c22bad4138b86f4d3695d55bd1b4511141d1d374b2ca4694d080 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +0489bf7de7252e652c9c4967be77664cdeacb8afc07dcf846d73f0cf7c25dbe9 apps/web/performance/review-db-rehearsal-20261012/README.md +6c474d8e78c0eba3f15c077dbdd8634f073524c3b5f842213372bf0f1fed84fc apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical +b279a6b900dd3be10b9084a6362d169e87435bae955b2a1419858709645a6883 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json +7f74c175be3eee72b5e93eb75dac07bfefc0ef88d7d9957c26d877ee125ff590 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json +cb766e279baad0315e9d7055bc326b41b78a7d50a4972dac3214bc071aee0e49 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json +c53a467eaf9b929647bc1d66248308ba1ff73eba89fc9221dbf7db4711eb8562 apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json +8088aa57f39045e2bf93cba084711119395098d053318b70aaa676b5c13fea26 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs +099e0b728cbfde7f1b292ec5c90e1ab14af54bdf9e87d07ef44ead9bb885883e apps/web/tests-unit/review-media-sql-successor.test.ts +dc1f2594e629d5af16541db93e2e51f9b98d8282dd7fbeb802dd240fa6f522c0 apps/web/tests-unit/review-media-migration-bundle.test.ts +c2c833a6fb2529477e02bbdc353a1e840d0c079d045a81dbc8a0e905ef16a601 apps/web/tests-unit/review-media-sql-successor-harness.test.ts +b25b5dcc6516435a1c74a038c20856cc5dd719b303d9c3f3e600dc314197c7cd apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical new file mode 100644 index 0000000000..b53872fe14 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical @@ -0,0 +1,7 @@ +f6cf33c2f5f5fdc8f5a5361f1ffc54a2c2cb70aa752da4815e4f614dffba3410 .github/review-media-sql-successor.v1.json +dfef9d60199b8656cf6bb50ce32ba19825a8caf1a1dbca28f08e91e4d6c7e38c apps/web/scripts/review-media-sql-successor.mjs +0857216fc85b1a59a29bf70a0550a285952cde97fb9553b0860da04046908a49 apps/web/scripts/review-media-sql-rehearsal.mjs +fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs +29622de79b7ebc63080ef3edb4c7f9f8ba61e14efa5600fcb589791a53323356 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs +cb766e279baad0315e9d7055bc326b41b78a7d50a4972dac3214bc071aee0e49 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json +159d19163e920882ce0ae98c16eed8120d53bebac4e3c173615d930ebddcfc31 apps/web/performance/review-db-rehearsal-20261012/README.md diff --git a/apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json b/apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json new file mode 100644 index 0000000000..2eeda57552 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json @@ -0,0 +1,65 @@ +{ + "schemaVersion": 1, + "status": "passed", + "environment": { + "engine": "17.6", + "image": "supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab", + "emptyRowChecks": [ + "public.profiles", + "public.reviews" + ] + }, + "manifestSha256": "4c4d8bd20dfee1b20706670a63acf66eacb8286999119b72690489a1a2202c8c", + "manifestToolchainRoot": "a95a588b05726a1c57739ea570f93bf3602346c817f73f0c648d628455d5d0db", + "rehearsalHarnessSha256": "8088aa57f39045e2bf93cba084711119395098d053318b70aaa676b5c13fea26", + "sourceRoot": "61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544", + "fixtureDumpSha256": "b7777306a43cc84408434e9835973604b2eb520cf9e9e8d0ee4bba6dcb41257f", + "predecessorSources": [ + { + "path": "backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql", + "sha256": "b373b7ea472c0352a33a4d4043cf8d6aa8474c04cc1ca5778805edfa77ac9c95" + }, + { + "path": "backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql", + "sha256": "66eace1d6fb0dd55c1d780776bab855cc37690335ad40b0fdc1294c610e07d3a" + }, + { + "path": "backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql", + "sha256": "e1c105df82c4f814d3e6adad807ff9d072b8cd42ae770b4b78f75b89a9387020" + }, + { + "path": "backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql", + "sha256": "8acf6d1428764260ed57dac5fe09711868a82896f0a6d631d502128004c168a3" + }, + { + "path": "backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql", + "sha256": "b596b200e52c6813a4cfa1b0a2818625f067864549e8854f3497afdcdab706da" + }, + { + "path": "backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql", + "sha256": "2067538f89c9f90d28e784672c7a1288306ba22d5ae92b087c8692503da9b1ae" + } + ], + "migrationCount": 11, + "statementCount": 112, + "baselineStateSha256": "3802888324e1651ad7b2f71e7cf15fac78ab77aa57637d0c60431f8da67e6298", + "stageStatesSha256": "bd25ecbd82759ca75e994ca0f1da38b49acc848d5dd556e702e4e43024edaa65", + "terminalStateSha256": "9cf59a5d983e78f6f7d9a594267c6feda3276611bf3539289b8a3de3295f700a", + "compiledSqlBytes": 2510560, + "compiledSqlSha256": "285d0c859117006646389dbd2a22de9aef1c0b97bc796a9e4d4448a8a232b093", + "results": { + "rollbackStageCount": 12, + "rollbackExact": true, + "forcedErrorRejected": true, + "forcedErrorRollbackExact": true, + "lostResponseMutationAttempts": 1, + "lostResponseRestorationExact": true, + "contendedExpiryRejected": true, + "contendedExpiryRollbackExact": true + }, + "hostedWrites": false, + "operatingRowsRead": false, + "rawSqlInProof": false, + "sourceMutation": false, + "temporaryContainerRemoved": true +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json b/apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json new file mode 100644 index 0000000000..7fc1ac6b3b --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json @@ -0,0 +1,65 @@ +{ + "schemaVersion": 1, + "status": "passed", + "environment": { + "engine": "17.6", + "image": "supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab", + "emptyRowChecks": [ + "public.profiles", + "public.reviews" + ] + }, + "manifestSha256": "8297b18793513956118c720e3aa5bd481d0769645c263f680a5272825b70569e", + "manifestToolchainRoot": "91a7166b76b0f867b4e74dd0e2258d1f22457c75a61e55b161148378f08ac739", + "rehearsalHarnessSha256": "8088aa57f39045e2bf93cba084711119395098d053318b70aaa676b5c13fea26", + "sourceRoot": "61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544", + "fixtureDumpSha256": "b7777306a43cc84408434e9835973604b2eb520cf9e9e8d0ee4bba6dcb41257f", + "predecessorSources": [ + { + "path": "backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql", + "sha256": "b373b7ea472c0352a33a4d4043cf8d6aa8474c04cc1ca5778805edfa77ac9c95" + }, + { + "path": "backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql", + "sha256": "66eace1d6fb0dd55c1d780776bab855cc37690335ad40b0fdc1294c610e07d3a" + }, + { + "path": "backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql", + "sha256": "e1c105df82c4f814d3e6adad807ff9d072b8cd42ae770b4b78f75b89a9387020" + }, + { + "path": "backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql", + "sha256": "8acf6d1428764260ed57dac5fe09711868a82896f0a6d631d502128004c168a3" + }, + { + "path": "backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql", + "sha256": "b596b200e52c6813a4cfa1b0a2818625f067864549e8854f3497afdcdab706da" + }, + { + "path": "backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql", + "sha256": "2067538f89c9f90d28e784672c7a1288306ba22d5ae92b087c8692503da9b1ae" + } + ], + "migrationCount": 11, + "statementCount": 112, + "baselineStateSha256": "3802888324e1651ad7b2f71e7cf15fac78ab77aa57637d0c60431f8da67e6298", + "stageStatesSha256": "bd25ecbd82759ca75e994ca0f1da38b49acc848d5dd556e702e4e43024edaa65", + "terminalStateSha256": "9cf59a5d983e78f6f7d9a594267c6feda3276611bf3539289b8a3de3295f700a", + "compiledSqlBytes": 2510560, + "compiledSqlSha256": "285d0c859117006646389dbd2a22de9aef1c0b97bc796a9e4d4448a8a232b093", + "results": { + "rollbackStageCount": 12, + "rollbackExact": true, + "forcedErrorRejected": true, + "forcedErrorRollbackExact": true, + "lostResponseMutationAttempts": 1, + "lostResponseRestorationExact": true, + "contendedExpiryRejected": true, + "contendedExpiryRollbackExact": true + }, + "hostedWrites": false, + "operatingRowsRead": false, + "rawSqlInProof": false, + "sourceMutation": false, + "temporaryContainerRemoved": true +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json b/apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json new file mode 100644 index 0000000000..d14f6d473a --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json @@ -0,0 +1,62 @@ +{ + "schemaVersion": 1, + "status": "passed", + "environment": { + "engine": "17.6", + "image": "supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab", + "personalRowsEmpty": true + }, + "manifestSha256": "f6cf33c2f5f5fdc8f5a5361f1ffc54a2c2cb70aa752da4815e4f614dffba3410", + "manifestToolchainRoot": "a173ad52d4612b5f08e0b0dce6acaa1dadd3a15996e6854dca196bda00c5c65b", + "rehearsalHarnessSha256": "29622de79b7ebc63080ef3edb4c7f9f8ba61e14efa5600fcb589791a53323356", + "sourceRoot": "61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544", + "fixtureDumpSha256": "b7777306a43cc84408434e9835973604b2eb520cf9e9e8d0ee4bba6dcb41257f", + "predecessorSources": [ + { + "path": "backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql", + "sha256": "b373b7ea472c0352a33a4d4043cf8d6aa8474c04cc1ca5778805edfa77ac9c95" + }, + { + "path": "backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql", + "sha256": "66eace1d6fb0dd55c1d780776bab855cc37690335ad40b0fdc1294c610e07d3a" + }, + { + "path": "backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql", + "sha256": "e1c105df82c4f814d3e6adad807ff9d072b8cd42ae770b4b78f75b89a9387020" + }, + { + "path": "backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql", + "sha256": "8acf6d1428764260ed57dac5fe09711868a82896f0a6d631d502128004c168a3" + }, + { + "path": "backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql", + "sha256": "b596b200e52c6813a4cfa1b0a2818625f067864549e8854f3497afdcdab706da" + }, + { + "path": "backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql", + "sha256": "2067538f89c9f90d28e784672c7a1288306ba22d5ae92b087c8692503da9b1ae" + } + ], + "migrationCount": 11, + "statementCount": 112, + "baselineStateSha256": "3802888324e1651ad7b2f71e7cf15fac78ab77aa57637d0c60431f8da67e6298", + "stageStatesSha256": "bd25ecbd82759ca75e994ca0f1da38b49acc848d5dd556e702e4e43024edaa65", + "terminalStateSha256": "9cf59a5d983e78f6f7d9a594267c6feda3276611bf3539289b8a3de3295f700a", + "compiledSqlBytes": 2510560, + "compiledSqlSha256": "285d0c859117006646389dbd2a22de9aef1c0b97bc796a9e4d4448a8a232b093", + "results": { + "rollbackStageCount": 12, + "rollbackExact": true, + "forcedErrorRejected": true, + "forcedErrorRollbackExact": true, + "lostResponseMutationAttempts": 1, + "lostResponseRestorationExact": true, + "contendedExpiryRejected": true, + "contendedExpiryRollbackExact": true + }, + "hostedWrites": false, + "operatingRowsRead": false, + "rawSqlInProof": false, + "sourceMutation": false, + "temporaryContainerRemoved": true +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json b/apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json new file mode 100644 index 0000000000..bf73e67768 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json @@ -0,0 +1,33 @@ +{ + "schema": "tzudong-gitguardian-single-incident-readback/v1", + "recordedAtUtc": "2026-10-12T13:42:52.816277+00:00", + "repository": "twoimo/tzudong", + "pullRequest": 3196, + "sourceRevision": "f4739a77822ce218595db67ebe0664eb10922688", + "incidentId": 38128430, + "occurrenceId": 303564026, + "incidentCount": 1, + "classification": "test credential", + "action": "Skip: test cred", + "actionRequests": 2, + "reason": "The same single incident was present in the original check and an already-existing rerun. No new workflow or check rerun was requested by this task.", + "apiReadback": [ + { + "id": 114780500942, + "status": "completed", + "conclusion": "skipped", + "title": "You tagged this GitGuardian alert as a test credential" + }, + { + "id": 114833294224, + "status": "completed", + "conclusion": "skipped", + "title": "You tagged this GitGuardian alert as a test credential" + } + ], + "incidentStatusShown": "Triggered", + "incidentResolvedClaimed": false, + "scannerSettingsChanged": false, + "otherIncidentsChanged": false, + "currentSourcePasswordFixtureRemoved": true +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs b/apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs new file mode 100644 index 0000000000..b6266528ec --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs @@ -0,0 +1,495 @@ +#!/usr/bin/env node + +/** + * Isolated PostgreSQL 17.6 replay for the rollback-only successor controller. + * + * A disposable container is populated from the empty-row ranking_clone + * schema fixture and its non-personal contract seed rows. No hosted endpoint + * is contacted. Every scenario verifies the same restored state 0 in a + * database named `postgres`, matching the operating database contract. + */ +import { createHash, randomBytes } from 'node:crypto'; +import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { dirname, relative, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawn, spawnSync } from 'node:child_process'; + +import { + buildSuccessorStateQuery, + compileSuccessorRehearsalPlan, + loadSuccessorManifest, + validateSuccessorStageStates, +} from '../../scripts/review-media-sql-successor.mjs'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPOSITORY_ROOT = resolve(HERE, '../../../..'); +const OUTPUT_PATH = resolve(HERE, 'pg17-rollback-rehearsal.json'); +const CHECKSUMS_PATH = resolve(HERE, 'SHA256SUMS'); +const DOCKER_CONTEXT = process.env.TZUDONG_REHEARSAL_DOCKER_CONTEXT || 'colima-tzudong-catalog-20261007'; +const SOURCE_CONTAINER = process.env.TZUDONG_REHEARSAL_SOURCE_CONTAINER || 'tzudong-ranking-clone-20261008'; +const CONTAINER_PREFIX = 'tzudong-review-rollback-rehearsal'; +const OWNER_LABEL = 'tzudong.review-rehearsal.owner'; +const IMAGE = 'supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab'; +const BOOTSTRAP_PASSWORD = randomBytes(24).toString('hex'); +const ADVISORY_LOCK_KEY = '7311754282260211'; +const PREDECESSOR_PATHS = Object.freeze([ + 'backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql', + 'backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql', + 'backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql', + 'backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql', + 'backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql', + 'backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql', +]); +const REQUIRED_ROLES = Object.freeze([ + 'cli_login_postgres', + 'privacy_auth_bridge', + 'privacy_retention_activation_operator', + 'privacy_retention_legal_approver', + 'privacy_retention_operator_approver', + 'privacy_workflow_owner', + 'supabase_privileged_role', + 'supabase_realtime_admin', + 'tzudong_g037_readonly', +]); +const sha256 = value => createHash('sha256').update(value).digest('hex'); +const quote = value => `'${String(value).replaceAll("'", "''")}'`; +const canonical = value => Array.isArray(value) + ? value.map(canonical) + : value && typeof value === 'object' + ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) + : value; +const equal = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right)); + +function fail(code) { + const error = new Error(code); + error.code = code; + throw error; +} + +function currentBindings(record, manifestBytes, harnessBytes) { + return Object.freeze({ + manifestSha256: sha256(manifestBytes), + rehearsalHarnessSha256: sha256(harnessBytes), + sourceRoot: record.manifest.sourceRoot, + }); +} + +function checksumEntry(checksumsBytes, repositoryPath) { + const entries = checksumsBytes.toString('utf8').split(/\r?\n/).filter(Boolean).map(line => { + const match = /^([0-9a-f]{64}) (.+)$/.exec(line); + if (!match) fail('LOCAL_REHEARSAL_CHECKSUMS_INVALID'); + return { sha256: match[1], path: match[2] }; + }); + const matches = entries.filter(entry => entry.path === repositoryPath); + if (matches.length !== 1) fail('LOCAL_REHEARSAL_RETAINED_CHECKSUM_MISSING'); + return matches[0]; +} + +export function inspectRetainedProof({ + outputPath = OUTPUT_PATH, + checksumsPath = CHECKSUMS_PATH, + bindings, + repositoryRoot = REPOSITORY_ROOT, +} = {}) { + if (!existsSync(outputPath)) return Object.freeze({ present: false, classification: 'missing' }); + if (!existsSync(checksumsPath)) fail('LOCAL_REHEARSAL_CHECKSUMS_MISSING'); + const proofBytes = readFileSync(outputPath); + const repositoryPath = relative(repositoryRoot, outputPath).replaceAll('\\', '/'); + if (!repositoryPath || repositoryPath.startsWith('../')) fail('LOCAL_REHEARSAL_RETAINED_PATH_INVALID'); + const expected = checksumEntry(readFileSync(checksumsPath), repositoryPath); + const actualSha256 = sha256(proofBytes); + if (actualSha256 !== expected.sha256) fail('LOCAL_REHEARSAL_RETAINED_SHA256_MISMATCH'); + let proof; + try { proof = JSON.parse(proofBytes.toString('utf8')); } catch { fail('LOCAL_REHEARSAL_RETAINED_JSON_INVALID'); } + const fields = ['manifestSha256', 'rehearsalHarnessSha256', 'sourceRoot']; + const bindingDifferences = bindings + ? fields.filter(field => proof[field] !== bindings[field]).map(field => ({ + field, + current: bindings[field], + retained: proof[field] ?? null, + })) + : []; + return Object.freeze({ + present: true, + classification: bindingDifferences.length === 0 + ? 'retained-current-source-evidence' + : 'retained-historical-evidence', + repositoryPath, + expectedSha256: expected.sha256, + actualSha256, + currentSourceMatch: bindingDifferences.length === 0, + usedAsCurrentProof: false, + bindingDifferences, + }); +} + +function docker(args, { allowFailure = false, input, maxBuffer = 64 * 1024 * 1024 } = {}) { + const result = spawnSync('docker', ['--context', DOCKER_CONTEXT, ...args], { + encoding: 'utf8', + input, + maxBuffer, + }); + if (!allowFailure && (result.error || result.status !== 0)) { + const marker = /ERROR:\s+([A-Z][A-Z0-9_]{3,96})\b/.exec(result.stderr)?.[1] + || /FATAL:\s+([A-Z][A-Z0-9_]{3,96})\b/.exec(result.stderr)?.[1] + || 'UNKNOWN'; + fail(`LOCAL_REHEARSAL_COMMAND_FAILED_${marker}`); + } + return result; +} + +function psql(container, database, sql, { actor = 'postgres', allowFailure = false } = {}) { + return docker([ + 'exec', '-i', '-e', `PGPASSWORD=${BOOTSTRAP_PASSWORD}`, container, + 'psql', '-XAtq', '-v', 'ON_ERROR_STOP=1', '-v', 'VERBOSITY=terse', + '-h', '127.0.0.1', '-U', actor, '-d', database, '--file=-', + ], { allowFailure, input: sql }); +} + +function scalar(container, database, sql, actor = 'postgres') { + return psql(container, database, `${sql}\n`, { actor }).stdout.trim(); +} + +function state(container, database, stateQuery) { + const lines = scalar(container, database, `${stateQuery};`).split(/\r?\n/).filter(Boolean); + if (lines.length !== 1) fail('LOCAL_REHEARSAL_STATE_SHAPE_INVALID'); + try { return JSON.parse(lines[0]); } catch { fail('LOCAL_REHEARSAL_STATE_JSON_INVALID'); } +} + +function waitForReady(container) { + for (let attempt = 0; attempt < 60; attempt += 1) { + const ready = docker([ + 'exec', '-e', `PGPASSWORD=${BOOTSTRAP_PASSWORD}`, container, + 'pg_isready', '-h', '127.0.0.1', '-U', 'postgres', '-d', 'postgres', + ], { allowFailure: true }); + if (ready.status === 0) return; + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250); + } + fail('LOCAL_REHEARSAL_CONTAINER_NOT_READY'); +} + +function createRequiredRoles(container) { + const roleArray = `ARRAY[${REQUIRED_ROLES.map(quote).join(',')}]`; + psql(container, 'postgres', `DO $roles$ +DECLARE role_name text; +BEGIN + FOREACH role_name IN ARRAY ${roleArray} LOOP + IF to_regrole(role_name) IS NULL THEN + EXECUTE format('CREATE ROLE %I NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS',role_name); + END IF; + END LOOP; + ALTER ROLE privacy_auth_bridge INHERIT; + ALTER ROLE privacy_retention_activation_operator NOINHERIT; + ALTER ROLE privacy_retention_legal_approver NOINHERIT; + ALTER ROLE privacy_retention_operator_approver NOINHERIT; + ALTER ROLE privacy_workflow_owner NOINHERIT; +END $roles$;`); + psql(container, 'postgres', `GRANT privacy_auth_bridge TO postgres WITH ADMIN TRUE,INHERIT FALSE,SET FALSE GRANTED BY supabase_admin; +GRANT privacy_retention_activation_operator TO postgres WITH ADMIN TRUE,INHERIT FALSE,SET FALSE GRANTED BY supabase_admin; +GRANT privacy_retention_legal_approver TO postgres WITH ADMIN TRUE,INHERIT FALSE,SET FALSE GRANTED BY supabase_admin; +GRANT privacy_retention_operator_approver TO postgres WITH ADMIN TRUE,INHERIT FALSE,SET FALSE GRANTED BY supabase_admin; +GRANT privacy_workflow_owner TO postgres WITH ADMIN TRUE,INHERIT FALSE,SET FALSE GRANTED BY supabase_admin;`, { actor: 'supabase_admin' }); + psql(container, 'postgres', 'GRANT privacy_workflow_owner TO privacy_auth_bridge WITH ADMIN FALSE,INHERIT TRUE,SET TRUE GRANTED BY postgres;'); +} + +function fixtureSql() { + return `DO $fixture$ +BEGIN + IF (SELECT count(*) FROM supabase_migrations.schema_migrations)<>0 + OR (SELECT count(*) FROM public.profiles)<>0 + OR (SELECT count(*) FROM public.reviews)<>0 + THEN RAISE EXCEPTION 'LOCAL_REHEARSAL_FIXTURE_INVALID'; END IF; +END $fixture$; +INSERT INTO supabase_migrations.schema_migrations(version,statements,name) +SELECT lpad(value::text,14,'0'),ARRAY['local schema-only predecessor fixture'],format('local_predecessor_%s',value) +FROM generate_series(1,86) value; +INSERT INTO pipeline_control.restaurant_review_policy(singleton,enabled) +VALUES(true,false) +ON CONFLICT(singleton) DO UPDATE SET enabled=false,operator_id=NULL,last_run_at=NULL; +INSERT INTO pipeline_control.admin_evaluation_catalog_revision(singleton,revision) +VALUES(true,1) +ON CONFLICT(singleton) DO NOTHING;`; +} + +function inspectContainer(target, expected, dockerImpl) { + const result = dockerImpl(['container', 'inspect', target], { allowFailure: true }); + if (result.error || result.status !== 0) { + if (/No such (?:object|container)/i.test(result.stderr || '')) return null; + fail('LOCAL_REHEARSAL_CONTAINER_INSPECTION_FAILED'); + } + let values; + try { values = JSON.parse(result.stdout); } catch { fail('LOCAL_REHEARSAL_CONTAINER_INSPECTION_INVALID'); } + if (!Array.isArray(values) || values.length !== 1) fail('LOCAL_REHEARSAL_CONTAINER_INSPECTION_INVALID'); + const value = values[0]; + const id = typeof value?.Id === 'string' ? value.Id : ''; + const name = typeof value?.Name === 'string' ? value.Name.replace(/^\//, '') : ''; + const owner = value?.Config?.Labels?.[OWNER_LABEL]; + if (!/^[0-9a-f]{12,64}$/.test(id) + || (expected.id && id !== expected.id) + || name !== expected.name + || owner !== expected.token) { + fail('LOCAL_REHEARSAL_CONTAINER_OWNERSHIP_UNPROVEN'); + } + return Object.freeze({ id, name, token: owner }); +} + +export function createOwnedContainer(ownership, { + dockerImpl = docker, + generateToken = () => randomBytes(16).toString('hex'), + processId = process.pid, +} = {}) { + if (ownership.created || ownership.id || ownership.name || ownership.token) { + fail('LOCAL_REHEARSAL_CONTAINER_OWNERSHIP_STATE_INVALID'); + } + const token = generateToken(); + if (!/^[0-9a-f]{32}$/.test(token)) fail('LOCAL_REHEARSAL_CONTAINER_TOKEN_INVALID'); + const name = `${CONTAINER_PREFIX}-${processId}-${token.slice(0, 12)}`; + ownership.name = name; + ownership.token = token; + const creation = dockerImpl([ + 'run', '-d', '--rm', '--name', name, '--label', `${OWNER_LABEL}=${token}`, + '-e', `POSTGRES_PASSWORD=${BOOTSTRAP_PASSWORD}`, IMAGE, + ], { allowFailure: true }); + const observed = inspectContainer(name, ownership, dockerImpl); + if (!observed) fail('LOCAL_REHEARSAL_CONTAINER_CREATION_UNCONFIRMED'); + // The name and per-run label are the ownership authority. Record the exact + // inspected ID before reconciling the run acknowledgement so any later + // mismatch is still cleaned up by the caller's finally block. + ownership.id = observed.id; + ownership.created = true; + const acknowledgedId = creation.status === 0 ? creation.stdout.trim() : ''; + if (acknowledgedId && acknowledgedId !== observed.id) fail('LOCAL_REHEARSAL_CONTAINER_ACK_MISMATCH'); + ownership.creationAcknowledged = !creation.error && creation.status === 0 && Boolean(acknowledgedId); + ownership.creationReconciled = !ownership.creationAcknowledged; + return observed.id; +} + +export function removeOwnedContainer(ownership, { dockerImpl = docker } = {}) { + if (!ownership.created || !ownership.id || !ownership.name || !ownership.token) return false; + const observed = inspectContainer(ownership.id, ownership, dockerImpl); + if (!observed) { + ownership.created = false; + ownership.removed = true; + return true; + } + const removal = dockerImpl(['container', 'rm', '--force', ownership.id], { allowFailure: true }); + const after = inspectContainer(ownership.id, ownership, dockerImpl); + if (after) fail('LOCAL_REHEARSAL_CONTAINER_REMOVAL_UNCONFIRMED'); + ownership.created = false; + ownership.removed = true; + ownership.removalAcknowledged = removal.status === 0; + return true; +} + +function initializeFixture(ownership) { + const container = createOwnedContainer(ownership); + waitForReady(container); + psql(container, 'template1', 'ALTER ROLE postgres SUPERUSER;\nDROP DATABASE postgres WITH (FORCE);\nCREATE DATABASE postgres OWNER postgres TEMPLATE template0;', { actor: 'supabase_admin' }); + createRequiredRoles(container); + const dump = docker([ + 'exec', SOURCE_CONTAINER, + 'pg_dump', '-U', 'postgres', '-d', 'ranking_clone', + ]).stdout; + const deterministicDump = dump.replace(/^\\(?:un)?restrict [^\n]*\n/gm, ''); + psql(container, 'postgres', dump); + // pg_dump recreates enum/type OIDs. Rebind only the fixture's derived OID + // projection to each unchanged source signature before replaying migrations. + psql(container, 'postgres', `UPDATE privacy_retention.g014_public_rpc_allowlist allowed +SET identity_arguments=procedure.proargtypes::text +FROM pg_proc procedure +WHERE procedure.oid=to_regprocedure(allowed.source_signature) + AND allowed.identity_arguments IS DISTINCT FROM procedure.proargtypes::text;`); + psql(container, 'postgres', 'ALTER ROLE postgres NOSUPERUSER;', { actor: 'supabase_admin' }); + PREDECESSOR_PATHS.forEach((path, index) => { + const result = psql(container, 'postgres', readFileSync(resolve(REPOSITORY_ROOT, path)), { allowFailure: true }); + if (result.status !== 0) { + const marker = /ERROR:\s+([A-Z][A-Z0-9_]{3,96})\b/.exec(result.stderr)?.[1] || 'UNKNOWN'; + fail(`LOCAL_REHEARSAL_PREDECESSOR_FAILED_${index + 1}_${marker}`); + } + }); + psql(container, 'postgres', fixtureSql()); + return Object.freeze({ + container, + dumpSha256: sha256(deterministicDump), + engine: scalar(container, 'postgres', 'SHOW server_version;'), + }); +} + +function parseStages(output, manifest) { + const lines = output.split(/\r?\n/).map(line => line.trim()).filter(Boolean); + if (lines.length !== 12) fail('LOCAL_REHEARSAL_STAGE_COUNT_INVALID'); + let stages; + try { + stages = lines.map((line, index) => { + const value = JSON.parse(line); + if (value.kind !== 'successor-rehearsal-stage' + || value.index !== index + || value.serverVersionNum !== 170006) fail('LOCAL_REHEARSAL_STAGE_SHAPE_INVALID'); + return value.state; + }); + } catch { fail('LOCAL_REHEARSAL_STAGE_JSON_INVALID'); } + validateSuccessorStageStates(stages, manifest); + return stages; +} + +function startLockHolder(container) { + const args = [ + '--context', DOCKER_CONTEXT, + 'exec', '-i', '-e', `PGPASSWORD=${BOOTSTRAP_PASSWORD}`, container, + 'psql', '-XAtq', '-v', 'ON_ERROR_STOP=1', '-h', '127.0.0.1', '-U', 'postgres', '-d', 'postgres', + ]; + const child = spawn('docker', args, { stdio: ['pipe', 'pipe', 'pipe'] }); + let stdout = ''; + let stderr = ''; + let readySettled = false; + const ready = new Promise((resolveReady, rejectReady) => { + child.on('error', rejectReady); + child.stdout.on('data', chunk => { + stdout += chunk.toString(); + if (!readySettled && stdout.includes('LOCK_HELD')) { + readySettled = true; + resolveReady(); + } + }); + child.stderr.on('data', chunk => { stderr += chunk.toString(); }); + child.on('exit', code => { + if (!readySettled) rejectReady(new Error(code === 0 ? 'LOCAL_LOCK_OUTPUT_MISSING' : 'LOCAL_LOCK_FAILED')); + }); + }); + const completion = new Promise((resolveCompletion, rejectCompletion) => { + child.on('error', rejectCompletion); + child.on('close', code => code === 0 ? resolveCompletion() : rejectCompletion(new Error('LOCAL_LOCK_FAILED'))); + }); + child.stdin.end(`BEGIN;\nSELECT pg_advisory_xact_lock(${ADVISORY_LOCK_KEY}::bigint);\nSELECT 'LOCK_HELD';\nSELECT pg_sleep(1.6);\nCOMMIT;\n`); + return { child, completion, ready, stderr: () => stderr }; +} + +export function parseArguments(argv) { + if (argv.length === 0) return Object.freeze({ outputPath: null }); + if (argv.length !== 2 || argv[0] !== '--output' || !argv[1]) fail('LOCAL_REHEARSAL_ARGUMENTS_INVALID'); + return Object.freeze({ outputPath: resolve(argv[1]) }); +} + +export function publishCurrentProof({ retainedProof, proof, outputPath }, { + writeFileImpl = writeFileSync, + writeStdoutImpl = value => process.stdout.write(value), +} = {}) { + if (outputPath) { + writeFileImpl(outputPath, `${JSON.stringify(proof, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); + } + const envelope = Object.freeze({ + kind: 'review-media-sql-local-rehearsal', + retainedProof, + currentProof: proof, + currentOutputPath: outputPath, + }); + writeStdoutImpl(`${JSON.stringify(envelope)}\n`); + return envelope; +} + +export async function run(argv = process.argv.slice(2)) { + const options = parseArguments(argv); + if (options.outputPath && existsSync(options.outputPath)) fail('LOCAL_REHEARSAL_CURRENT_OUTPUT_EXISTS'); + const record = loadSuccessorManifest(); + const manifestBytes = readFileSync(resolve(REPOSITORY_ROOT, '.github/review-media-sql-successor.v1.json')); + const harnessBytes = readFileSync(fileURLToPath(import.meta.url)); + const bindings = currentBindings(record, manifestBytes, harnessBytes); + const retainedProof = inspectRetainedProof({ bindings }); + const ownership = { created: false, id: null, name: null, token: null }; + let proof; + try { + const environment = initializeFixture(ownership); + const container = environment.container; + if (!environment.engine.startsWith('17.6')) fail('LOCAL_REHEARSAL_ENGINE_INVALID'); + const stateQuery = buildSuccessorStateQuery(record.manifest); + + const baseline = state(container, 'postgres', stateQuery); + const plan = compileSuccessorRehearsalPlan(record, { expiresAt: '9999-12-31T23:59:59.999Z' }, baseline); + const replay = psql(container, 'postgres', plan.sql); + const stages = parseStages(replay.stdout, record.manifest); + const restored = state(container, 'postgres', stateQuery); + if (!equal(restored, baseline) || !equal(stages[0], baseline)) fail('LOCAL_REHEARSAL_ROLLBACK_DRIFT'); + + const errorBaseline = state(container, 'postgres', stateQuery); + const errorPlan = compileSuccessorRehearsalPlan(record, { expiresAt: '9999-12-31T23:59:59.999Z' }, errorBaseline); + const forcedErrorSql = errorPlan.sql.replace( + /\nROLLBACK;\s*$/, + "\nDO $forced$ BEGIN RAISE EXCEPTION 'LOCAL_FORCED_REHEARSAL_ERROR'; END $forced$;\nROLLBACK;", + ); + const forcedError = psql(container, 'postgres', forcedErrorSql, { allowFailure: true }); + if (forcedError.status === 0 || !/LOCAL_FORCED_REHEARSAL_ERROR/.test(forcedError.stderr)) { + fail('LOCAL_REHEARSAL_FORCED_ERROR_MISSING'); + } + if (!equal(state(container, 'postgres', stateQuery), errorBaseline)) fail('LOCAL_REHEARSAL_ERROR_ROLLBACK_DRIFT'); + + const lostBaseline = state(container, 'postgres', stateQuery); + const lostPlan = compileSuccessorRehearsalPlan(record, { expiresAt: '9999-12-31T23:59:59.999Z' }, lostBaseline); + psql(container, 'postgres', lostPlan.sql); // Deliberately discard the complete response once. + if (!equal(state(container, 'postgres', stateQuery), lostBaseline)) fail('LOCAL_REHEARSAL_LOST_RESPONSE_DRIFT'); + + const expiryBaseline = state(container, 'postgres', stateQuery); + const holder = startLockHolder(container); + await holder.ready; + const expiresAt = new Date(Date.now() + 1_000).toISOString(); + const expiryPlan = compileSuccessorRehearsalPlan(record, { expiresAt }, expiryBaseline); + const expiryResult = psql(container, 'postgres', expiryPlan.sql, { allowFailure: true }); + await holder.completion; + if (expiryResult.status === 0 + || !/SUCCESSOR_ADMISSION_EXPIRED/.test(expiryResult.stderr) + || /lock timeout/i.test(expiryResult.stderr)) { + fail('LOCAL_REHEARSAL_CONTENDED_EXPIRY_NOT_REJECTED'); + } + if (!equal(state(container, 'postgres', stateQuery), expiryBaseline)) fail('LOCAL_REHEARSAL_EXPIRY_DRIFT'); + + const toolchain = record.manifest.toolchain.map(entry => ({ ...entry })); + proof = { + schemaVersion: 1, + status: 'passed', + environment: { + engine: environment.engine, + image: IMAGE, + emptyRowChecks: ['public.profiles', 'public.reviews'], + }, + manifestSha256: sha256(manifestBytes), + manifestToolchainRoot: sha256(JSON.stringify(canonical(toolchain))), + rehearsalHarnessSha256: sha256(harnessBytes), + sourceRoot: record.manifest.sourceRoot, + fixtureDumpSha256: environment.dumpSha256, + predecessorSources: PREDECESSOR_PATHS.map(path => ({ + path, + sha256: sha256(readFileSync(resolve(REPOSITORY_ROOT, path))), + })), + migrationCount: record.manifest.migrations.length, + statementCount: record.manifest.migrations.reduce((sum, migration) => sum + migration.statementCount, 0), + baselineStateSha256: sha256(JSON.stringify(canonical(baseline))), + stageStatesSha256: sha256(JSON.stringify(canonical(stages))), + terminalStateSha256: sha256(JSON.stringify(canonical(stages.at(-1)))), + compiledSqlBytes: Buffer.byteLength(plan.sql), + compiledSqlSha256: sha256(plan.sql), + results: { + rollbackStageCount: stages.length, + rollbackExact: true, + forcedErrorRejected: true, + forcedErrorRollbackExact: true, + lostResponseMutationAttempts: 1, + lostResponseRestorationExact: true, + contendedExpiryRejected: true, + contendedExpiryRollbackExact: true, + }, + hostedWrites: false, + operatingRowsRead: false, + rawSqlInProof: false, + sourceMutation: false, + }; + } finally { + removeOwnedContainer(ownership); + } + proof.temporaryContainerRemoved = ownership.removed === true; + if (!proof.temporaryContainerRemoved) fail('LOCAL_REHEARSAL_CONTAINER_REMOVAL_UNCONFIRMED'); + publishCurrentProof({ retainedProof, proof, outputPath: options.outputPath }); + return Object.freeze({ retainedProof, currentProof: proof, currentOutputPath: options.outputPath }); +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + await run(); +} diff --git a/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs index f8cd1ff120..e1dafc077f 100644 --- a/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +++ b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs @@ -5,10 +5,10 @@ * * The harness clones the existing schema-only ranking_clone template into one * fixed, owned database name. It refuses a reused target, never connects to a - * hosted endpoint, and drops the owned database before writing its proof. + * hosted endpoint, and drops the owned database before emitting its current-source result. */ import { createHash } from 'node:crypto'; -import { existsSync, readFileSync, writeFileSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawn, spawnSync } from 'node:child_process'; @@ -22,6 +22,8 @@ import { const HERE = dirname(fileURLToPath(import.meta.url)); const REPOSITORY_ROOT = resolve(HERE, '../../../..'); const OUTPUT_PATH = resolve(HERE, 'pg17-local-replay.json'); +const CHECKSUMS_PATH = resolve(HERE, 'SHA256SUMS'); +const RETAINED_PROOF_REPOSITORY_PATH = 'apps/web/performance/review-db-successor-20261012/pg17-local-replay.json'; const DOCKER_CONTEXT = process.env.TZUDONG_REHEARSAL_DOCKER_CONTEXT || 'colima-tzudong-catalog-20261007'; const DOCKER_CONTAINER = process.env.TZUDONG_REHEARSAL_DOCKER_CONTAINER || 'tzudong-ranking-clone-20261008'; const TEMPLATE_DATABASE = 'ranking_clone'; @@ -124,32 +126,41 @@ ON CONFLICT(singleton) DO NOTHING; `; } -function createOwnedDatabase() { - const exists = scalar('postgres', `SELECT count(*) FROM pg_database WHERE datname=${quote(DATABASE)};`, 'supabase_admin'); +export function createOwnedDatabase(ownership, { + scalarImpl = scalar, + psqlImpl = psql, + readFileImpl = readFileSync, +} = {}) { + if (!ownership || ownership.database !== false) fail('LOCAL_REHEARSAL_OWNERSHIP_STATE_INVALID'); + const exists = scalarImpl('postgres', `SELECT count(*) FROM pg_database WHERE datname=${quote(DATABASE)};`, 'supabase_admin'); if (exists !== '0') fail('LOCAL_REHEARSAL_TARGET_ALREADY_EXISTS'); - psql('postgres', `CREATE DATABASE ${DATABASE} TEMPLATE ${TEMPLATE_DATABASE} OWNER postgres;`, { actor: 'supabase_admin' }); + psqlImpl('postgres', `CREATE DATABASE ${DATABASE} TEMPLATE ${TEMPLATE_DATABASE} OWNER postgres;`, { actor: 'supabase_admin' }); + ownership.database = true; PREDECESSOR_PATHS.forEach((path, index) => { try { - psql(DATABASE, readFileSync(resolve(REPOSITORY_ROOT, path))); + psqlImpl(DATABASE, readFileImpl(resolve(REPOSITORY_ROOT, path))); } catch (error) { fail('LOCAL_REHEARSAL_PREDECESSOR_FAILED', `${index + 1}:${error.message}`); } }); - psql(DATABASE, fixtureSql()); + psqlImpl(DATABASE, fixtureSql()); } -function dropOwnedDatabase() { - const exists = scalar('postgres', `SELECT count(*) FROM pg_database WHERE datname=${quote(DATABASE)};`, 'supabase_admin'); +export function dropOwnedDatabase(ownership, { scalarImpl = scalar, psqlImpl = psql } = {}) { + if (!ownership?.database) return false; + const exists = scalarImpl('postgres', `SELECT count(*) FROM pg_database WHERE datname=${quote(DATABASE)};`, 'supabase_admin'); if (exists === '1') { - psql('postgres', `DROP DATABASE ${DATABASE} WITH (FORCE);`, { actor: 'supabase_admin' }); + psqlImpl('postgres', `DROP DATABASE ${DATABASE} WITH (FORCE);`, { actor: 'supabase_admin' }); } else if (exists !== '0') { fail('LOCAL_REHEARSAL_TARGET_CARDINALITY_INVALID'); } + ownership.database = false; + return exists === '1'; } -function resetOwnedDatabase() { - dropOwnedDatabase(); - createOwnedDatabase(); +function resetOwnedDatabase(ownership) { + dropOwnedDatabase(ownership); + createOwnedDatabase(ownership); } function createFixtureMembership() { @@ -178,6 +189,81 @@ COMMIT; `, { actor: 'supabase_admin' }); } +function cleanupOwnedFixtures(ownership) { + const failures = []; + try { + dropOwnedDatabase(ownership); + } catch (error) { + failures.push(`database:${error.message}`); + } + try { + if (ownership.fixtureMembership) dropFixtureMembership(); + ownership.fixtureMembership = false; + } catch (error) { + failures.push(`roles:${error.message}`); + } + if (failures.length > 0) fail('LOCAL_REHEARSAL_CLEANUP_FAILED', failures.join(',')); +} + +export function inspectRetainedProof({ + outputPath = OUTPUT_PATH, + checksumsPath = CHECKSUMS_PATH, + currentBindings, + existsImpl = existsSync, + readFileImpl = readFileSync, +} = {}) { + if (!existsImpl(outputPath)) return Object.freeze({ present: false }); + if (!currentBindings || typeof currentBindings !== 'object') { + fail('LOCAL_REHEARSAL_CURRENT_BINDINGS_INVALID'); + } + const checksumLines = readFileImpl(checksumsPath, 'utf8').split(/\r?\n/).filter(Boolean); + const matchingLines = checksumLines.filter(line => line.endsWith(` ${RETAINED_PROOF_REPOSITORY_PATH}`)); + if (matchingLines.length !== 1) fail('LOCAL_REHEARSAL_RETAINED_CHECKSUM_MISSING'); + const checksumMatch = matchingLines[0].match(/^([0-9a-f]{64}) (.+)$/); + if (!checksumMatch || checksumMatch[2] !== RETAINED_PROOF_REPOSITORY_PATH) { + fail('LOCAL_REHEARSAL_RETAINED_CHECKSUM_INVALID'); + } + const bytes = readFileImpl(outputPath); + const actualSha256 = sha256(bytes); + if (actualSha256 !== checksumMatch[1]) fail('LOCAL_REHEARSAL_RETAINED_SHA256_MISMATCH'); + let retained; + try { + retained = JSON.parse(bytes.toString('utf8')); + } catch { + fail('LOCAL_REHEARSAL_RETAINED_JSON_INVALID'); + } + const retainedBindings = { + manifestSha256: retained.manifestSha256, + rehearsalHarnessSha256: retained.rehearsalHarnessSha256, + sourceRoot: retained.sourceRoot, + }; + if (retained.status !== 'passed' + || retained.temporaryDatabaseDropped !== true + || retained.hostedWrites !== false + || retained.operatingWrites !== false + || Object.values(retainedBindings).some(value => !/^[0-9a-f]{64}$/.test(value))) { + fail('LOCAL_REHEARSAL_RETAINED_SHAPE_INVALID'); + } + const bindingDifferences = Object.keys(retainedBindings) + .filter(key => retainedBindings[key] !== currentBindings[key]) + .map(key => ({ + field: key, + retained: retainedBindings[key], + current: currentBindings[key], + })); + return Object.freeze({ + present: true, + classification: 'retained-historical-evidence', + repositoryPath: RETAINED_PROOF_REPOSITORY_PATH, + expectedSha256: checksumMatch[1], + actualSha256, + bindings: Object.freeze(retainedBindings), + currentSourceMatch: bindingDifferences.length === 0, + bindingDifferences: Object.freeze(bindingDifferences), + usedAsCurrentProof: false, + }); +} + function applyRawMigration(record, index) { const migration = record.manifest.migrations[index]; const material = record.materials[index]; @@ -270,8 +356,8 @@ COMMIT; `); } -function assertCatalogDriftRejected({ label, plan, stages, stateQuery, tamperSql, restoreSql = null }) { - resetOwnedDatabase(); +function assertCatalogDriftRejected({ label, ownership, plan, stages, stateQuery, tamperSql, restoreSql = null }) { + resetOwnedDatabase(ownership); const baseline = readState(DATABASE, stateQuery); if (!equal(baseline, stages[0])) fail('LOCAL_REHEARSAL_DRIFT_BASELINE_INVALID', label); psql(DATABASE, tamperSql, { actor: 'supabase_admin' }); @@ -388,8 +474,8 @@ async function waitForAdvisoryContention(expiresAt) { fail('LOCAL_REHEARSAL_ADVISORY_CONTENTION_NOT_OBSERVED'); } -async function assertContendedExpiryRejected(record, stages, stateQuery) { - resetOwnedDatabase(); +async function assertContendedExpiryRejected(record, stages, stateQuery, ownership) { + resetOwnedDatabase(ownership); const baseline = readState(DATABASE, stateQuery); if (!equal(baseline, stages[0])) fail('LOCAL_REHEARSAL_EXPIRY_BASELINE_INVALID'); const expiresAt = Date.now() + 1500; @@ -423,10 +509,15 @@ async function assertContendedExpiryRejected(record, stages, stateQuery) { return true; } -async function run() { - if (existsSync(OUTPUT_PATH)) fail('LOCAL_REHEARSAL_PROOF_EXISTS'); +export async function run() { const manifestBytes = readFileSync(resolve(REPOSITORY_ROOT, '.github/review-media-sql-successor.v1.json')); const record = loadSuccessorManifest(); + const currentBindings = Object.freeze({ + manifestSha256: sha256(manifestBytes), + rehearsalHarnessSha256: sha256(readFileSync(fileURLToPath(import.meta.url))), + sourceRoot: record.manifest.sourceRoot, + }); + const retainedProof = inspectRetainedProof({ currentBindings }); const stateQuery = buildSuccessorStateQuery(record.manifest); const template = JSON.parse(scalar(TEMPLATE_DATABASE, `SELECT json_build_object( 'engine',current_setting('server_version'), @@ -440,14 +531,12 @@ async function run() { || template.profiles !== 0 || template.reviews !== 0) fail('LOCAL_REHEARSAL_TEMPLATE_INVALID'); - let created = false; - let fixtureMembershipCreated = false; + const ownership = { database: false, fixtureMembership: false }; let proof; try { createFixtureMembership(); - fixtureMembershipCreated = true; - createOwnedDatabase(); - created = true; + ownership.fixtureMembership = true; + createOwnedDatabase(ownership); if (scalar(DATABASE, `SELECT coalesce('security_invoker=true'=ANY(c.reloptions),false) FROM pg_class c WHERE c.oid='public.admin_evaluation_related_rows'::regclass;`) !== 't') { fail('LOCAL_REHEARSAL_SECURITY_INVOKER_BASELINE_INVALID'); @@ -466,7 +555,7 @@ async function run() { stageStates: stages, }); - resetOwnedDatabase(); + resetOwnedDatabase(ownership); let success; try { success = psql(DATABASE, plan.sql, { singleTransaction: true }); @@ -490,7 +579,7 @@ async function run() { fail('LOCAL_REHEARSAL_COMMIT_READBACK_INVALID'); } - resetOwnedDatabase(); + resetOwnedDatabase(ownership); const rollback = psql(DATABASE, `${plan.sql}\nDO $forced$ BEGIN RAISE EXCEPTION 'LOCAL_FORCED_ROLLBACK'; END $forced$;\n`, { singleTransaction: true, allowFailure: true, @@ -501,7 +590,7 @@ async function run() { const rolledBack = readState(DATABASE, stateQuery); if (!equal(rolledBack, stages[0])) fail('LOCAL_REHEARSAL_ROLLBACK_DRIFT'); - resetOwnedDatabase(); + resetOwnedDatabase(ownership); for (let index = 0; index < 10; index += 1) applyRawMigration(record, index); const cleanPreimage = readState(DATABASE, stateQuery); if (!equal(cleanPreimage, stages[10])) fail('LOCAL_REHEARSAL_STAGE10_REPLAY_DRIFT'); @@ -520,6 +609,7 @@ async function run() { const securityInvokerDriftRejected = assertCatalogDriftRejected({ label: 'security-invoker', + ownership, plan, stages, stateQuery, @@ -527,6 +617,7 @@ async function run() { }); const columnAclDriftRejected = assertCatalogDriftRejected({ label: 'column-acl', + ownership, plan, stages, stateQuery, @@ -534,6 +625,7 @@ async function run() { }); const membershipInheritOptionDriftRejected = assertCatalogDriftRejected({ label: 'membership-inherit-option', + ownership, plan, stages, stateQuery, @@ -542,13 +634,14 @@ async function run() { }); const membershipSetOptionDriftRejected = assertCatalogDriftRejected({ label: 'membership-set-option', + ownership, plan, stages, stateQuery, tamperSql: `GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} WITH ADMIN FALSE, INHERIT TRUE, SET FALSE GRANTED BY supabase_admin;`, restoreSql: `GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} WITH ADMIN FALSE, INHERIT TRUE, SET TRUE GRANTED BY supabase_admin;`, }); - const contendedAdmissionExpiryRejected = await assertContendedExpiryRejected(record, stages, stateQuery); + const contendedAdmissionExpiryRejected = await assertContendedExpiryRejected(record, stages, stateQuery, ownership); proof = { schemaVersion: 1, @@ -561,9 +654,7 @@ async function run() { temporaryDatabase: DATABASE, }, input: template, - manifestSha256: sha256(manifestBytes), - rehearsalHarnessSha256: sha256(readFileSync(fileURLToPath(import.meta.url))), - sourceRoot: record.manifest.sourceRoot, + ...currentBindings, predecessorSources: PREDECESSOR_PATHS.map(path => ({ path, sha256: sha256(readFileSync(resolve(REPOSITORY_ROOT, path))), @@ -597,26 +688,20 @@ async function run() { operatingWrites: false, }; } finally { - if (created) dropOwnedDatabase(); - if (fixtureMembershipCreated) dropFixtureMembership(); + cleanupOwnedFixtures(ownership); } proof.temporaryDatabaseDropped = true; - writeFileSync(OUTPUT_PATH, `${JSON.stringify(proof, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); + proof.fixtureRolesDropped = true; + const currentProofBytes = Buffer.from(`${JSON.stringify(canonical(proof))}\n`); process.stdout.write(`${JSON.stringify({ - status: proof.status, - migrationCount: proof.migrationCount, - statementCount: proof.statementCount, - atomicCommit: proof.results.atomicCommit, - atomicRollbackExact: proof.results.atomicRollbackExact, - catalogDriftRejected: proof.results.securityInvokerDriftRejected - && proof.results.columnAclDriftRejected - && proof.results.membershipInheritOptionDriftRejected - && proof.results.membershipSetOptionDriftRejected, - contendedAdmissionExpiryRejected: proof.results.contendedAdmissionExpiryRejected, - wrongFinishPreimageRejected: proof.results.wrongFinishPreimageRejected, - terminalLedgerCount: proof.results.terminalLedgerCount, - temporaryDatabaseDropped: proof.temporaryDatabaseDropped, + current: { + classification: 'current-source-verification', + proofSha256: sha256(currentProofBytes), + bindings: currentBindings, + proof, + }, + retained: retainedProof, })}\n`); } -await run(); +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await run(); diff --git a/apps/web/scripts/review-media-sql-rehearsal.mjs b/apps/web/scripts/review-media-sql-rehearsal.mjs new file mode 100644 index 0000000000..69f2113830 --- /dev/null +++ b/apps/web/scripts/review-media-sql-rehearsal.mjs @@ -0,0 +1,54 @@ +#!/usr/bin/env node +/** Rollback-only operating rehearsal for the eleven-stage review-media successor. */ +import { resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { + successorFailureResult, + runReviewMediaSuccessorRehearsal, +} from './review-media-sql-successor.mjs'; + +const SCRIPT_PATH = fileURLToPath(import.meta.url); + +function fail(code) { + const error = new Error(code); + error.code = code; + throw error; +} + +export function parseRehearsalArguments(argv) { + const result = {}; + for (let index = 0; index < argv.length; index += 2) { + const option = argv[index]; + const value = argv[index + 1]; + if (!['--admission', '--journal', '--receipt'].includes(option) + || !value + || value.startsWith('--') + || option.slice(2) in result) { + fail('SUCCESSOR_REHEARSAL_ARGUMENT_INVALID'); + } + result[option.slice(2)] = value; + } + if (argv.length !== 6 || !result.admission || !result.journal || !result.receipt) { + fail('SUCCESSOR_REHEARSAL_ARGUMENT_INVALID'); + } + return result; +} + +export function main(argv = process.argv.slice(2), dependencies) { + const args = parseRehearsalArguments(argv); + return runReviewMediaSuccessorRehearsal({ + admissionPath: args.admission, + journalPath: args.journal, + receiptPath: args.receipt, + }, dependencies); +} + +if (process.argv[1] && resolve(process.argv[1]) === SCRIPT_PATH) { + try { + console.log(JSON.stringify(main())); + } catch (error) { + console.error(JSON.stringify(successorFailureResult(error))); + process.exitCode = 1; + } +} diff --git a/apps/web/scripts/review-media-sql-successor.mjs b/apps/web/scripts/review-media-sql-successor.mjs index 1b3e7ef70a..2db595b9b3 100644 --- a/apps/web/scripts/review-media-sql-successor.mjs +++ b/apps/web/scripts/review-media-sql-successor.mjs @@ -3,8 +3,11 @@ import { spawnSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { closeSync, + constants, existsSync, + fstatSync, fsyncSync, + linkSync, lstatSync, openSync, readFileSync, @@ -21,6 +24,7 @@ import { selectDirectDatabaseTransport, } from './apply-supabase-migration.mjs'; import { + compileElevenMigrationRehearsalSources, compileElevenMigrationBundle, executeMigrationBundle, elevenMigrationBundleSuffixRoot, @@ -48,6 +52,7 @@ const GIT_REMOTE_READ_TIMEOUT_MS = 15_000; const PRIVATE_RECEIPT_FILES = Object.freeze({ protectedMain: 'protected-main-receipt.json', rehearsal: 'rehearsal-receipt.json', + rehearsalJournal: 'rehearsal-journal.jsonl', rollback: 'rollback-readback-receipt.json', source: 'source-receipt.json', }); @@ -70,6 +75,14 @@ const FIXED_CONTROLLER_CODES = new Set([ 'SUCCESSOR_MANIFEST_INVALID', 'SUCCESSOR_PREFLIGHT_MISMATCH', 'SUCCESSOR_READBACK_INVALID', + 'SUCCESSOR_REHEARSAL_ARGUMENT_INVALID', + 'SUCCESSOR_REHEARSAL_BASELINE_INVALID', + 'SUCCESSOR_REHEARSAL_OUTCOME_UNCONFIRMED', + 'SUCCESSOR_REHEARSAL_OUTPUT_INVALID', + 'SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED', + 'SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED', + 'SUCCESSOR_REHEARSAL_RESTORE_CONFLICT', + 'SUCCESSOR_REHEARSAL_RESTORE_UNKNOWN', 'SUCCESSOR_ROLLBACK_READBACK_INVALID', 'SUCCESSOR_SOURCE_DRIFT', ]); @@ -102,16 +115,41 @@ function exactKeys(value, keys, code) { if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) fail(code); } -function safeCode(error) { - try { return typeof error?.code === 'string' ? error.code : ''; } catch { return ''; } +function safeCode(error, key = 'code') { + try { + const value = error?.[key]; + return typeof value === 'string' ? value : ''; + } catch { return ''; } } export function boundedSuccessorError(error) { const code = safeCode(error); - if (FIXED_CONTROLLER_CODES.has(code)) return operationError(code); + if (FIXED_CONTROLLER_CODES.has(code)) { + const bounded = operationError(code); + if (code === 'SUCCESSOR_JOURNAL_APPEND_FAILED') { + const operationCode = safeCode(error, 'operationCode'); + if (FIXED_CONTROLLER_CODES.has(operationCode)) bounded.operationCode = operationCode; + } + return bounded; + } return boundedMigrationError(error); } +export function successorFailureResult(error) { + const bounded = boundedSuccessorError(error); + return { + code: bounded.code, + ...(bounded.operationCode ? { operationCode: bounded.operationCode } : {}), + status: 'failed', + }; +} + +function journalFailure(operationCode) { + const error = operationError('SUCCESSOR_JOURNAL_APPEND_FAILED'); + if (FIXED_CONTROLLER_CODES.has(operationCode)) error.operationCode = operationCode; + return error; +} + function parseJsonObject(bytes, code) { let value; try { value = JSON.parse(Buffer.isBuffer(bytes) ? bytes.toString('utf8') : bytes); } catch { fail(code); } @@ -140,6 +178,7 @@ function sourceProjection(migration) { function assertToolchain(manifest) { const expected = [ 'apps/web/scripts/review-media-sql-successor.mjs', + 'apps/web/scripts/review-media-sql-rehearsal.mjs', 'apps/web/scripts/supabase-migration-bundle.mjs', 'apps/web/scripts/supabase-migration-transaction.mjs', 'apps/web/scripts/apply-supabase-migration.mjs', @@ -546,6 +585,39 @@ function readPrivateCanonicalReceipt(directory, fileName, expectedSha256) { return receipt; } +function readPrivateCanonicalJournal(directory, fileName, expectedSha256) { + if (typeof directory !== 'string' || !isAbsolute(directory) || typeof fileName !== 'string') { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + let bytes; + let journalPath; + let receiptDirectory; + try { + const directoryStatus = lstatSync(directory); + if (!directoryStatus.isDirectory() + || directoryStatus.isSymbolicLink() + || (process.platform !== 'win32' && (directoryStatus.mode & 0o077) !== 0)) { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + receiptDirectory = realpathSync(directory); + journalPath = assertPrivateAdmissionPath(join(directory, fileName)); + bytes = readFileSync(journalPath); + } catch { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + if (dirname(realpathSync(journalPath)) !== receiptDirectory + || sha256(bytes) !== expectedSha256) fail('SUCCESSOR_ADMISSION_INVALID'); + const text = bytes.toString('utf8'); + if (!text.endsWith('\n')) fail('SUCCESSOR_ADMISSION_INVALID'); + const lines = text.slice(0, -1).split('\n'); + if (lines.length !== 3 || lines.some(line => !line)) fail('SUCCESSOR_ADMISSION_INVALID'); + return Object.freeze(lines.map(line => { + const entry = parseJsonObject(line, 'SUCCESSOR_ADMISSION_INVALID'); + if (`${JSON.stringify(canonical(entry))}\n` !== `${line}\n`) fail('SUCCESSOR_ADMISSION_INVALID'); + return entry; + })); +} + function assertReceiptWindow(receipt, admission, createdAt, expiresAt, nowMs) { if (receipt.expiresAt !== admission.expiresAt) fail('SUCCESSOR_ADMISSION_INVALID'); const observedAt = Date.parse(receipt.observedAt); @@ -556,9 +628,24 @@ function assertReceiptWindow(receipt, admission, createdAt, expiresAt, nowMs) { || Date.parse(receipt.expiresAt) !== expiresAt) fail('SUCCESSOR_ADMISSION_INVALID'); } +function assertPriorReceiptWindow(receipt, createdAt, expiresAt, nowMs) { + const observedAt = Date.parse(receipt.observedAt); + const receiptExpiresAt = Date.parse(receipt.expiresAt); + if (!Number.isFinite(observedAt) + || !Number.isFinite(receiptExpiresAt) + || receiptExpiresAt <= observedAt + || receiptExpiresAt - observedAt > ADMISSION_MAX_AGE_MS + || observedAt < createdAt - ADMISSION_MAX_AGE_MS + || observedAt > createdAt + 30_000 + || observedAt > nowMs + 30_000 + || receiptExpiresAt < expiresAt + || nowMs >= receiptExpiresAt) fail('SUCCESSOR_ADMISSION_INVALID'); +} + function validatePrivateReceipts(admission, manifestRecord, { createdAt, expiresAt, + includeRehearsal = true, nowMs, receiptDirectory, }) { @@ -601,27 +688,99 @@ function validatePrivateReceipts(admission, manifestRecord, { || protectedMain.revision !== admission.sourceRevision) fail('SUCCESSOR_ADMISSION_INVALID'); assertReceiptWindow(protectedMain, admission, createdAt, expiresAt, nowMs); - const rehearsal = readPrivateCanonicalReceipt( - receiptDirectory, - PRIVATE_RECEIPT_FILES.rehearsal, - admission.rehearsalReceiptSha256, - ); - exactKeys(rehearsal, [ - 'expiresAt', 'id', 'kind', 'manifestSha256', 'observedAt', 'passed', 'projectRef', - 'purpose', 'schemaVersion', 'serverVersionNum', 'sourceRevision', 'sourceRoot', - 'stageStatesSha256', 'suffixRoot', - ], 'SUCCESSOR_ADMISSION_INVALID'); - if (!common(rehearsal) - || rehearsal.kind !== 'successor-rehearsal-receipt' - || rehearsal.passed !== true - || !Number.isSafeInteger(rehearsal.serverVersionNum) - || Math.floor(rehearsal.serverVersionNum / 10_000) !== manifest.serverMajor - || rehearsal.sourceRoot !== manifest.sourceRoot - || rehearsal.suffixRoot !== elevenMigrationBundleSuffixRoot(manifest.migrations) - || rehearsal.stageStatesSha256 !== sha256(JSON.stringify(canonical(admission.stageStates)))) { - fail('SUCCESSOR_ADMISSION_INVALID'); + if (includeRehearsal) { + const rehearsal = readPrivateCanonicalReceipt( + receiptDirectory, + PRIVATE_RECEIPT_FILES.rehearsal, + admission.rehearsalReceiptSha256, + ); + exactKeys(rehearsal, [ + 'admissionSha256', 'attemptId', 'expiresAt', 'id', 'journalPathSha256', 'kind', + 'manifestSha256', 'observedAt', 'passed', + 'preflightStateSha256', 'projectRef', 'purpose', 'restoredStateSha256', + 'schemaVersion', 'serverVersionNum', 'sourceRevision', 'sourceRoot', 'stageStates', + 'stageStatesSha256', 'suffixRoot', + ], 'SUCCESSOR_ADMISSION_INVALID'); + const stageStatesSha256 = sha256(JSON.stringify(canonical(admission.stageStates))); + const priorStateSha256 = sha256(JSON.stringify(canonical(admission.stageStates[0]))); + if (!common(rehearsal) + || rehearsal.kind !== 'successor-rehearsal-receipt' + || rehearsal.passed !== true + || !UUID.test(rehearsal.attemptId) + || !HEX_SHA256.test(rehearsal.admissionSha256) + || rehearsal.journalPathSha256 !== sha256(Buffer.from(resolve( + receiptDirectory, + PRIVATE_RECEIPT_FILES.rehearsalJournal, + ))) + || !Number.isSafeInteger(rehearsal.serverVersionNum) + || Math.floor(rehearsal.serverVersionNum / 10_000) !== manifest.serverMajor + || rehearsal.sourceRoot !== manifest.sourceRoot + || rehearsal.suffixRoot !== elevenMigrationBundleSuffixRoot(manifest.migrations) + || rehearsal.stageStatesSha256 !== stageStatesSha256 + || rehearsal.preflightStateSha256 !== priorStateSha256 + || rehearsal.restoredStateSha256 !== priorStateSha256 + || !equal(rehearsal.stageStates, admission.stageStates)) { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + assertPriorReceiptWindow(rehearsal, createdAt, expiresAt, nowMs); + + const journal = readPrivateCanonicalJournal( + receiptDirectory, + PRIVATE_RECEIPT_FILES.rehearsalJournal, + admission.rehearsalJournalSha256, + ); + const [started, verification, publication] = journal; + exactKeys(started, [ + 'admissionSha256', 'attemptId', 'event', 'manifestSha256', 'sourceRevision', + 'sourceRoot', 'startedAt', + ], 'SUCCESSOR_ADMISSION_INVALID'); + exactKeys(verification, [ + 'admissionSha256', 'attemptId', 'code', 'event', 'manifestSha256', + 'receiptSha256', 'sourceRevision', 'sourceRoot', 'verifiedAt', + ], 'SUCCESSOR_ADMISSION_INVALID'); + exactKeys(publication, [ + 'admissionSha256', 'attemptId', 'code', 'event', 'manifestSha256', + 'publishedAt', 'receiptSha256', 'sourceRevision', 'sourceRoot', + ], 'SUCCESSOR_ADMISSION_INVALID'); + const startedAt = Date.parse(started.startedAt); + const verifiedAt = Date.parse(verification.verifiedAt); + const publishedAt = Date.parse(publication.publishedAt); + const observedAt = Date.parse(rehearsal.observedAt); + if (started.event !== 'started' + || verification.event !== 'verification' + || verification.code !== 'SUCCESSOR_REHEARSAL_VERIFIED' + || publication.event !== 'publication' + || publication.code !== 'SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED' + || started.attemptId !== rehearsal.attemptId + || verification.attemptId !== rehearsal.attemptId + || publication.attemptId !== rehearsal.attemptId + || started.admissionSha256 !== rehearsal.admissionSha256 + || verification.admissionSha256 !== rehearsal.admissionSha256 + || publication.admissionSha256 !== rehearsal.admissionSha256 + || started.manifestSha256 !== manifestSha256 + || verification.manifestSha256 !== manifestSha256 + || publication.manifestSha256 !== manifestSha256 + || started.sourceRevision !== admission.sourceRevision + || verification.sourceRevision !== admission.sourceRevision + || publication.sourceRevision !== admission.sourceRevision + || started.sourceRoot !== manifest.sourceRoot + || verification.sourceRoot !== manifest.sourceRoot + || publication.sourceRoot !== manifest.sourceRoot + || verification.receiptSha256 !== admission.rehearsalReceiptSha256 + || publication.receiptSha256 !== admission.rehearsalReceiptSha256 + || !Number.isFinite(startedAt) + || !Number.isFinite(verifiedAt) + || !Number.isFinite(publishedAt) + || startedAt > observedAt + || verifiedAt < observedAt + || publishedAt < verifiedAt + || verifiedAt >= Date.parse(rehearsal.expiresAt) + || publishedAt >= Date.parse(rehearsal.expiresAt) + || verifiedAt > createdAt + 30_000 + || publishedAt > createdAt + 30_000 + || verifiedAt > nowMs + 30_000 + || publishedAt > nowMs + 30_000) fail('SUCCESSOR_ADMISSION_INVALID'); } - assertReceiptWindow(rehearsal, admission, createdAt, expiresAt, nowMs); const rollback = readPrivateCanonicalReceipt( receiptDirectory, @@ -667,39 +826,24 @@ function validatePrivateReceipts(admission, manifestRecord, { } } -export function validateSuccessorAdmission(admission, manifestRecord, { - now = new Date(), +function validateAdmissionEnvelope(admission, manifestRecord, { + expectedKind, + includeRehearsal, + now, receiptDirectory, -} = {}) { + requiredReceiptHashes, +}) { const { manifest, manifestSha256 } = manifestRecord; - exactKeys(admission, [ - 'attemptId', - 'createdAt', - 'expiresAt', - 'id', - 'journalPathSha256', - 'manifestSha256', - 'projectRef', - 'protectedMainReceiptSha256', - 'purpose', - 'rehearsalReceiptSha256', - 'rollback', - 'schemaVersion', - 'sourceReceiptSha256', - 'sourceRevision', - 'sourceRoot', - 'stageStates', - 'writerFences', - ], 'SUCCESSOR_ADMISSION_INVALID'); if (admission.schemaVersion !== 1 || admission.id !== manifest.id || admission.purpose !== manifest.purpose || admission.projectRef !== manifest.projectRef || admission.manifestSha256 !== manifestSha256 || admission.sourceRoot !== manifest.sourceRoot + || (expectedKind !== undefined && admission.kind !== expectedKind) || !UUID.test(admission.attemptId) || !REVISION.test(admission.sourceRevision)) fail('SUCCESSOR_ADMISSION_INVALID'); - for (const field of ['protectedMainReceiptSha256', 'rehearsalReceiptSha256', 'sourceReceiptSha256']) { + for (const field of requiredReceiptHashes) { assertHex(admission[field], 'SUCCESSOR_ADMISSION_INVALID'); } assertHex(admission.journalPathSha256, 'SUCCESSOR_ADMISSION_INVALID'); @@ -744,18 +888,69 @@ export function validateSuccessorAdmission(admission, manifestRecord, { if (fence.state !== manifest.requiredWriterFences[key]) fail('SUCCESSOR_ADMISSION_INVALID'); assertHex(fence.evidenceSha256, 'SUCCESSOR_ADMISSION_INVALID'); } + validatePrivateReceipts(admission, manifestRecord, { + createdAt, + expiresAt, + includeRehearsal, + nowMs, + receiptDirectory, + }); + return Object.freeze({ createdAt, expiresAt }); +} + +export function validateSuccessorAdmission(admission, manifestRecord, { + now = new Date(), + receiptDirectory, +} = {}) { + const { manifest } = manifestRecord; + exactKeys(admission, [ + 'attemptId', + 'createdAt', + 'expiresAt', + 'id', + 'journalPathSha256', + 'manifestSha256', + 'projectRef', + 'protectedMainReceiptSha256', + 'purpose', + 'rehearsalJournalSha256', + 'rehearsalReceiptSha256', + 'rollback', + 'schemaVersion', + 'sourceReceiptSha256', + 'sourceRevision', + 'sourceRoot', + 'stageStates', + 'writerFences', + ], 'SUCCESSOR_ADMISSION_INVALID'); + const envelope = validateAdmissionEnvelope(admission, manifestRecord, { + includeRehearsal: true, + now, + receiptDirectory, + requiredReceiptHashes: [ + 'protectedMainReceiptSha256', + 'rehearsalJournalSha256', + 'rehearsalReceiptSha256', + 'sourceReceiptSha256', + ], + }); + + const stageBinding = validateSuccessorStageStates(admission.stageStates, manifest); + return Object.freeze({ ...envelope, ...stageBinding }); +} - if (!Array.isArray(admission.stageStates) - || admission.stageStates.length !== manifest.migrations.length + 1) fail('SUCCESSOR_ADMISSION_INVALID'); +export function validateSuccessorStageStates(stageStates, manifest) { + if (!Array.isArray(stageStates) + || stageStates.length !== manifest.migrations.length + 1) fail('SUCCESSOR_ADMISSION_INVALID'); const suffixRoot = elevenMigrationBundleSuffixRoot(manifest.migrations); - const priorCount = admission.stageStates[0]?.bundle?.priorCount; - const prefixRoot = admission.stageStates[0]?.bundle?.prefixRoot; + const priorCount = stageStates[0]?.bundle?.priorCount; + const prefixRoot = stageStates[0]?.bundle?.prefixRoot; if (priorCount !== manifest.operatingTransition.beforeEleven || priorCount + manifest.migrations.length !== manifest.operatingTransition.afterEleven) { fail('SUCCESSOR_ADMISSION_INVALID'); } assertHex(prefixRoot, 'SUCCESSOR_ADMISSION_INVALID'); - admission.stageStates.forEach((stage, index) => validateStageState( + stageStates.forEach((stage, index) => validateStageState( stage, index, manifest, @@ -763,21 +958,48 @@ export function validateSuccessorAdmission(admission, manifestRecord, { priorCount, prefixRoot, )); - const g014Roots = admission.stageStates.map(stage => stage.state.g014Root); + const g014Roots = stageStates.map(stage => stage.state.g014Root); if (g014Roots[1] !== g014Roots[0] || g014Roots[2] !== g014Roots[0] || new Set(g014Roots.slice(4)).size !== 1) { fail('SUCCESSOR_ADMISSION_INVALID'); } - const ledgerRoots = admission.stageStates.map(stage => stage.state.ledgerRoot); + const ledgerRoots = stageStates.map(stage => stage.state.ledgerRoot); if (new Set(ledgerRoots).size !== ledgerRoots.length) fail('SUCCESSOR_ADMISSION_INVALID'); - validatePrivateReceipts(admission, manifestRecord, { - createdAt, - expiresAt, - nowMs, + return Object.freeze({ prefixRoot, priorCount, suffixRoot }); +} + +export function validateSuccessorRehearsalAdmission(admission, manifestRecord, { + now = new Date(), + receiptDirectory, +} = {}) { + exactKeys(admission, [ + 'attemptId', + 'createdAt', + 'expiresAt', + 'id', + 'journalPathSha256', + 'kind', + 'manifestSha256', + 'projectRef', + 'protectedMainReceiptSha256', + 'purpose', + 'receiptPathSha256', + 'rollback', + 'schemaVersion', + 'sourceReceiptSha256', + 'sourceRevision', + 'sourceRoot', + 'writerFences', + ], 'SUCCESSOR_ADMISSION_INVALID'); + assertHex(admission.receiptPathSha256, 'SUCCESSOR_ADMISSION_INVALID'); + return validateAdmissionEnvelope(admission, manifestRecord, { + expectedKind: 'successor-rehearsal-admission', + includeRehearsal: false, + now, receiptDirectory, + requiredReceiptHashes: ['protectedMainReceiptSha256', 'sourceReceiptSha256'], }); - return Object.freeze({ createdAt, expiresAt, prefixRoot, priorCount, suffixRoot }); } function assertAdmissionFresh(expiresAt, value) { @@ -848,6 +1070,89 @@ export function compileSuccessorPlan(manifestRecord, admission) { }); } +export function validateSuccessorBaseline(stage, manifest) { + const suffixRoot = elevenMigrationBundleSuffixRoot(manifest.migrations); + const priorCount = stage?.bundle?.priorCount; + const prefixRoot = stage?.bundle?.prefixRoot; + if (priorCount !== manifest.operatingTransition.beforeEleven) { + fail('SUCCESSOR_REHEARSAL_BASELINE_INVALID'); + } + try { + assertHex(prefixRoot, 'SUCCESSOR_ADMISSION_INVALID'); + validateStageState(stage, 0, manifest, suffixRoot, priorCount, prefixRoot); + } catch { + fail('SUCCESSOR_REHEARSAL_BASELINE_INVALID'); + } + return Object.freeze({ prefixRoot, priorCount, suffixRoot }); +} + +function rehearsalExpirySql(admission, suffix) { + const tag = `$successor_rehearsal_expiry_${sha256(`${admission.expiresAt}:${suffix}`).slice(0, 18)}$`; + return `DO ${tag} BEGIN IF clock_timestamp() >= ${quote(admission.expiresAt)}::timestamptz THEN RAISE EXCEPTION 'SUCCESSOR_ADMISSION_EXPIRED'; END IF; END ${tag};`; +} + +function rehearsalStageSql(stateQuery, index) { + return `SELECT json_build_object('index',${index},'kind','successor-rehearsal-stage','serverVersionNum',current_setting('server_version_num')::int,'state',(${stateQuery})::jsonb)::text;`; +} + +export function compileSuccessorStateReadQuery(manifestRecord) { + const stateQuery = buildSuccessorStateQuery(manifestRecord.manifest); + return Object.freeze({ + sql: `SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;\nSET LOCAL statement_timeout='15s';\nSELECT json_build_object('kind','successor-rehearsal-readback','serverVersionNum',current_setting('server_version_num')::int,'state',(${stateQuery})::jsonb)::text;`, + stateQuery, + }); +} + +export function compileSuccessorRehearsalPlan(manifestRecord, admission, expectedBaseline) { + const { manifest, materials } = manifestRecord; + validateSuccessorBaseline(expectedBaseline, manifest); + const sources = compileElevenMigrationRehearsalSources(manifest.migrations, materials); + const suffixRoot = elevenMigrationBundleSuffixRoot(manifest.migrations); + const stateQuery = buildSuccessorStateQuery(manifest, suffixRoot); + const lockTag = `$successor_rehearsal_${sha256(manifest.advisoryLockKey).slice(0, 18)}$`; + const baselineTag = `$successor_rehearsal_baseline_${sha256(JSON.stringify(canonical(expectedBaseline))).slice(0, 18)}$`; + const parts = [ + 'BEGIN;', + "SET LOCAL lock_timeout='2s';", + "SET LOCAL statement_timeout='15s';", + "SET LOCAL idle_in_transaction_session_timeout='30s';", + `DO ${lockTag} BEGIN PERFORM pg_advisory_xact_lock(${manifest.advisoryLockKey}::bigint); END ${lockTag};`, + rehearsalExpirySql(admission, 'after-lock'), + 'LOCK TABLE supabase_migrations.schema_migrations IN EXCLUSIVE MODE;', + `DO ${baselineTag} BEGIN + IF NOT has_schema_privilege(current_user,'supabase_migrations','USAGE') + OR NOT has_table_privilege(current_user,'supabase_migrations.schema_migrations','SELECT') + OR NOT has_table_privilege(current_user,'supabase_migrations.schema_migrations','INSERT') + OR (${stateQuery})::jsonb IS DISTINCT FROM ${quote(JSON.stringify(canonical(expectedBaseline)))}::jsonb + THEN RAISE EXCEPTION 'SUCCESSOR_REHEARSAL_BASELINE_INVALID'; END IF; +END ${baselineTag};`, + rehearsalStageSql(stateQuery, 0), + ]; + sources.forEach((source, index) => { + const migration = manifest.migrations[index]; + const ledgerTag = `$successor_rehearsal_ledger_${sha256(migration.id).slice(0, 18)}$`; + parts.push( + rehearsalExpirySql(admission, `before-${index + 1}`), + '\\o /dev/null', + source.execution, + `INSERT INTO supabase_migrations.schema_migrations(version,name,statements) VALUES(${quote(source.version)},${quote(source.name)},ARRAY(SELECT jsonb_array_elements_text(${quote(JSON.stringify(source.originalVector))}::jsonb)));`, + `DO ${ledgerTag} BEGIN + IF NOT EXISTS(SELECT 1 FROM supabase_migrations.schema_migrations WHERE version=${quote(source.version)} AND name IS NOT DISTINCT FROM ${quote(source.name)} AND statements IS NOT DISTINCT FROM ARRAY(SELECT jsonb_array_elements_text(${quote(JSON.stringify(source.originalVector))}::jsonb))) + THEN RAISE EXCEPTION 'MIGRATION_LEDGER_READBACK_FAILED'; END IF; +END ${ledgerTag};`, + '\\o', + rehearsalStageSql(stateQuery, index + 1), + ); + }); + parts.push(rehearsalExpirySql(admission, 'before-rollback'), 'ROLLBACK;'); + return Object.freeze({ + sql: parts.join('\n'), + stateQuery, + suffixRoot, + transactionMode: 'explicit-rollback-only', + }); +} + function checkoutPath(path, code, { mustExist = true } = {}) { if (typeof path !== 'string' || !isAbsolute(path)) fail(code); const absolute = resolve(path); @@ -874,31 +1179,85 @@ function assertJournalPath(path) { return absolute; } -function writeJournalStart(path, entry) { +function syncDirectory(path, fsyncImpl = fsyncSync) { + const descriptor = openSync(path, 'r'); + try { fsyncImpl(descriptor); } finally { closeSync(descriptor); } +} + +function writeJournalStart(path, entry, { fsyncImpl = fsyncSync } = {}) { let descriptor; try { descriptor = openSync(path, 'wx', 0o600); - writeSync(descriptor, `${JSON.stringify(canonical(entry))}\n`); - fsyncSync(descriptor); + writeAllSync(descriptor, Buffer.from(`${JSON.stringify(canonical(entry))}\n`)); + fsyncImpl(descriptor); + syncDirectory(dirname(path), fsyncImpl); return descriptor; } catch (error) { - if (descriptor !== undefined) closeSync(descriptor); + if (descriptor !== undefined) { + try { closeSync(descriptor); } catch { /* preserve the bounded original failure */ } + } if (safeCode(error) === 'EEXIST') fail('SUCCESSOR_JOURNAL_EXISTS'); fail('SUCCESSOR_JOURNAL_INVALID'); } } -function appendJournal(descriptor, entry) { +function writeAllSync(descriptor, bytes, code = 'SUCCESSOR_JOURNAL_APPEND_FAILED') { + let offset = 0; + while (offset < bytes.length) { + const written = writeSync(descriptor, bytes, offset, bytes.length - offset); + if (!Number.isSafeInteger(written) || written <= 0) fail(code); + offset += written; + } +} + +export function appendJournal(descriptor, entry, { + close = true, + fsyncImpl = fsyncSync, + closeImpl = closeSync, +} = {}) { + let durable = false; try { - writeSync(descriptor, `${JSON.stringify(canonical(entry))}\n`); - fsyncSync(descriptor); - closeSync(descriptor); + writeAllSync(descriptor, Buffer.from(`${JSON.stringify(canonical(entry))}\n`)); + fsyncImpl(descriptor); + durable = true; + if (close) closeImpl(descriptor); } catch { try { closeSync(descriptor); } catch { /* fixed outcome below */ } - fail('SUCCESSOR_JOURNAL_APPEND_FAILED'); + const error = operationError('SUCCESSOR_JOURNAL_APPEND_FAILED'); + error.publicationAuditReadbackEligible = durable; + throw error; } } +function assertDurableRehearsalArtifacts(directory, admission, fsyncImpl) { + // Visibility after a failed publication fsync is not evidence of durability. + // Reconfirm the exact admitted files before any new database mutation. + try { + const directoryStatus = lstatSync(directory); + if (!directoryStatus.isDirectory() || directoryStatus.isSymbolicLink() + || (process.platform !== 'win32' && (directoryStatus.mode & 0o077) !== 0)) { + fail('SUCCESSOR_ADMISSION_INVALID'); + } + for (const [fileName, expected] of [ + [PRIVATE_RECEIPT_FILES.rehearsal, admission.rehearsalReceiptSha256], + [PRIVATE_RECEIPT_FILES.rehearsalJournal, admission.rehearsalJournalSha256], + ]) { + const path = join(directory, fileName); + const descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW); + try { + const status = fstatSync(descriptor); + const named = lstatSync(path); + if (!status.isFile() || named.isSymbolicLink() + || status.dev !== named.dev || status.ino !== named.ino + || (process.platform !== 'win32' && (status.mode & 0o077) !== 0) + || sha256(readFileSync(descriptor)) !== expected) fail('SUCCESSOR_ADMISSION_INVALID'); + fsyncImpl(descriptor); + } finally { closeSync(descriptor); } + } + syncDirectory(directory, fsyncImpl); + } catch { fail('SUCCESSOR_ADMISSION_INVALID'); } +} + function gitCommand(args, repositoryRoot = REPOSITORY_ROOT) { const result = spawnSync('/usr/bin/git', args, { cwd: repositoryRoot, encoding: 'utf8' }); if (result.error || result.status !== 0) fail('SUCCESSOR_CHECKOUT_INVALID'); @@ -1005,6 +1364,9 @@ function transportError(result) { if (result.error) return operationError('MIGRATION_PSQL_EXECUTION_FAILED'); const stderr = typeof result.stderr === 'string' ? result.stderr : ''; const sqlstate = /ERROR:\s+([0-9A-Z]{5}):/m.exec(stderr)?.[1]; + if (/ERROR:\s+(?:[0-9A-Z]{5}:\s*)?SUCCESSOR_ADMISSION_EXPIRED\b/m.test(stderr)) { + return operationError('SUCCESSOR_ADMISSION_EXPIRED'); + } const error = operationError(sqlstate ? `MIGRATION_PSQL_FAILED_${sqlstate}` : 'MIGRATION_PSQL_FAILED'); error.fixedSqlCode = /ERROR:\s+(?:[0-9A-Z]{5}:\s*)?(MIGRATION_[A-Z0-9_]{1,96})\b/m.exec(stderr)?.[1]; return error; @@ -1035,6 +1397,472 @@ function parseReadback(output) { return parseJsonObject(output.trim(), 'SUCCESSOR_READBACK_INVALID'); } +function parseRehearsalReadback(output, manifest) { + let value; + try { + value = parseJsonObject(output.trim(), 'SUCCESSOR_REHEARSAL_BASELINE_INVALID'); + exactKeys(value, ['kind', 'serverVersionNum', 'state'], 'SUCCESSOR_REHEARSAL_BASELINE_INVALID'); + if (value.kind !== 'successor-rehearsal-readback' + || !Number.isSafeInteger(value.serverVersionNum) + || Math.floor(value.serverVersionNum / 10_000) !== manifest.serverMajor) { + fail('SUCCESSOR_REHEARSAL_BASELINE_INVALID'); + } + validateSuccessorBaseline(value.state, manifest); + } catch { + fail('SUCCESSOR_REHEARSAL_BASELINE_INVALID'); + } + return value; +} + +function parseRehearsalStages(output, manifest, expectedBaseline, expectedServerVersionNum) { + if (typeof output !== 'string') fail('SUCCESSOR_REHEARSAL_OUTPUT_INVALID'); + const lines = output.split(/\r?\n/).map(line => line.trim()).filter(Boolean); + if (lines.length !== manifest.migrations.length + 1) fail('SUCCESSOR_REHEARSAL_OUTPUT_INVALID'); + let stages; + try { + stages = lines.map((line, index) => { + const value = parseJsonObject(line, 'SUCCESSOR_REHEARSAL_OUTPUT_INVALID'); + exactKeys(value, ['index', 'kind', 'serverVersionNum', 'state'], 'SUCCESSOR_REHEARSAL_OUTPUT_INVALID'); + if (value.kind !== 'successor-rehearsal-stage' + || value.index !== index + || value.serverVersionNum !== expectedServerVersionNum) { + fail('SUCCESSOR_REHEARSAL_OUTPUT_INVALID'); + } + return value.state; + }); + validateSuccessorStageStates(stages, manifest); + } catch { + fail('SUCCESSOR_REHEARSAL_OUTPUT_INVALID'); + } + if (!equal(stages[0], expectedBaseline)) fail('SUCCESSOR_REHEARSAL_OUTPUT_INVALID'); + return Object.freeze(stages); +} + +function assertRehearsalReceiptPath(path, admissionFile) { + let absolute; + try { + absolute = checkoutPath(path, 'SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED', { mustExist: false }); + if (absolute !== resolve(dirname(admissionFile), PRIVATE_RECEIPT_FILES.rehearsal) + || existsSync(absolute)) fail('SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED'); + const parent = lstatSync(dirname(absolute)); + if (!parent.isDirectory() + || parent.isSymbolicLink() + || (process.platform !== 'win32' && (parent.mode & 0o077) !== 0)) { + fail('SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED'); + } + } catch { + fail('SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED'); + } + return absolute; +} + +function assertRehearsalJournalPath(path, admissionFile) { + const absolute = assertJournalPath(path); + if (absolute !== resolve(dirname(admissionFile), PRIVATE_RECEIPT_FILES.rehearsalJournal)) { + fail('SUCCESSOR_JOURNAL_BINDING_MISMATCH'); + } + return absolute; +} + +function assertPendingRehearsalReceiptPath(receiptFile, attemptId) { + let pendingFile; + try { + pendingFile = checkoutPath( + `${receiptFile}.${attemptId}.pending`, + 'SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED', + { mustExist: false }, + ); + if (dirname(pendingFile) !== dirname(receiptFile) || existsSync(pendingFile)) { + fail('SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED'); + } + } catch { + fail('SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED'); + } + return pendingFile; +} + +function writePendingReceipt(path, receipt) { + let descriptor; + try { + descriptor = openSync(path, 'wx', 0o600); + writeAllSync( + descriptor, + canonicalReceiptBytes(receipt), + 'SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED', + ); + fsyncSync(descriptor); + closeSync(descriptor); + } catch { + if (descriptor !== undefined) { + try { closeSync(descriptor); } catch { /* fixed outcome below */ } + } + fail('SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED'); + } +} + +function publishPendingReceipt(pendingPath, receiptPath) { + let directoryDescriptor; + try { + linkSync(pendingPath, receiptPath); + } catch { + const error = operationError('SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED'); + error.publicationReadbackEligible = false; + throw error; + } + try { + directoryDescriptor = openSync(dirname(receiptPath), 'r'); + fsyncSync(directoryDescriptor); + } catch { + if (directoryDescriptor !== undefined) { + try { closeSync(directoryDescriptor); } catch { /* fixed outcome below */ } + } + const error = operationError('SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED'); + error.publicationReadbackEligible = false; + throw error; + } + try { + closeSync(directoryDescriptor); + } catch { + const error = operationError('SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED'); + error.publicationReadbackEligible = true; + throw error; + } +} + +function publishedReceiptMatches(path, expectedBytes) { + try { + const status = lstatSync(path); + if (!status.isFile() + || status.isSymbolicLink() + || (process.platform !== 'win32' && (status.mode & 0o077) !== 0)) return false; + return readFileSync(path).equals(expectedBytes); + } catch { + return false; + } +} + +function canonicalJournalBytes(entries) { + return Buffer.from(entries + .map(entry => JSON.stringify(canonical(entry))) + .join('\n') + '\n'); +} + +function publishedJournalMatches(path, entries) { + try { + const status = lstatSync(path); + if (!status.isFile() + || status.isSymbolicLink() + || (process.platform !== 'win32' && (status.mode & 0o077) !== 0) + || dirname(realpathSync(path)) !== realpathSync(dirname(path))) return false; + return readFileSync(path).equals(canonicalJournalBytes(entries)); + } catch { + return false; + } +} + +function appendRehearsalTerminal(descriptor, admission, code, now, { + binding, + appendJournalImpl = appendJournal, +} = {}) { + const entry = { + attemptId: admission.attemptId, + code, + completedAt: now().toISOString(), + event: 'terminal', + ...(binding ?? {}), + }; + try { + appendJournalImpl(descriptor, entry); + } catch { + try { closeSync(descriptor); } catch { /* append implementation may already have closed it */ } + throw journalFailure(code); + } +} + +function appendRehearsalVerification(descriptor, entry, { + appendJournalImpl = appendJournal, +} = {}) { + try { + appendJournalImpl(descriptor, entry, { close: false }); + } catch { + try { closeSync(descriptor); } catch { /* append implementation may already have closed it */ } + fail('SUCCESSOR_JOURNAL_APPEND_FAILED'); + } +} + +export function runReviewMediaSuccessorRehearsal({ + admissionPath, + journalPath, + receiptPath, + environment = process.env, +}, { + now = () => new Date(), + gitFactsImpl = currentSuccessorGitFacts, + loadManifestImpl = loadSuccessorManifest, + protectedMainReadbackImpl = currentProtectedMainReadback, + rollbackReadbackImpl = captureVercelRollbackReadbackSync, + runPsqlImpl, + writePendingReceiptImpl = writePendingReceipt, + publishReceiptImpl = publishPendingReceipt, + appendJournalImpl = appendJournal, + fsyncImpl = fsyncSync, +} = {}) { + const manifestRecord = loadManifestImpl(); + const admissionFile = assertPrivateAdmissionPath(admissionPath); + const admissionBytes = readFileSync(admissionFile); + const admission = parseJsonObject(admissionBytes, 'SUCCESSOR_ADMISSION_INVALID'); + const admissionWindow = validateSuccessorRehearsalAdmission(admission, manifestRecord, { + now: now(), + receiptDirectory: dirname(admissionFile), + }); + assertCheckout(admission, gitFactsImpl()); + assertProtectedSourceReadback( + admission, + manifestRecord.manifest, + protectedMainReadbackImpl(manifestRecord.manifest), + ); + let rollbackReadback; + try { + rollbackReadback = rollbackReadbackImpl({ + expected: { + deploymentId: admission.rollback.deploymentId, + deploymentUrl: admission.rollback.deploymentUrl, + gitRef: admission.rollback.gitRef, + gitSha: admission.rollback.deploymentSha, + }, + now, + }); + } catch { + fail('SUCCESSOR_ROLLBACK_READBACK_INVALID'); + } + const rollbackCheckedAt = now(); + assertAdmissionFresh(admissionWindow.expiresAt, rollbackCheckedAt); + assertLiveRollbackReadback(admission, rollbackReadback, { + ...admissionWindow, + checkedAt: rollbackCheckedAt, + }); + + const journalFile = assertRehearsalJournalPath(journalPath, admissionFile); + if (sha256(Buffer.from(journalFile)) !== admission.journalPathSha256) { + fail('SUCCESSOR_JOURNAL_BINDING_MISMATCH'); + } + const receiptFile = assertRehearsalReceiptPath(receiptPath, admissionFile); + if (sha256(Buffer.from(receiptFile)) !== admission.receiptPathSha256) { + fail('SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED'); + } + const pendingReceiptFile = assertPendingRehearsalReceiptPath(receiptFile, admission.attemptId); + const { databaseUrl } = selectDirectDatabaseTransport(environment); + assertSuccessorDatabaseTarget(databaseUrl, manifestRecord.manifest); + const transport = runPsqlImpl ?? createSuccessorPsqlRunner(databaseUrl, { environment }); + const readPlan = compileSuccessorStateReadQuery(manifestRecord); + + let preflight; + try { + preflight = parseRehearsalReadback( + transport(databaseUrl, readPlan.sql, true), + manifestRecord.manifest, + ); + } catch (error) { + const code = safeCode(error) === 'SUCCESSOR_REHEARSAL_BASELINE_INVALID' + ? 'SUCCESSOR_REHEARSAL_BASELINE_INVALID' + : boundedSuccessorError(error).code; + fail(code); + } + + let finalRollbackReadback; + try { + finalRollbackReadback = rollbackReadbackImpl({ + expected: { + deploymentId: admission.rollback.deploymentId, + deploymentUrl: admission.rollback.deploymentUrl, + gitRef: admission.rollback.gitRef, + gitSha: admission.rollback.deploymentSha, + }, + now, + }); + } catch { + fail('SUCCESSOR_ROLLBACK_READBACK_INVALID'); + } + const finalRollbackCheckedAt = now(); + assertAdmissionFresh(admissionWindow.expiresAt, finalRollbackCheckedAt); + assertLiveRollbackReadback(admission, finalRollbackReadback, { + ...admissionWindow, + checkedAt: finalRollbackCheckedAt, + }); + + const plan = compileSuccessorRehearsalPlan(manifestRecord, admission, preflight.state); + const admissionSha256 = sha256(admissionBytes); + const startedEntry = { + admissionSha256, + attemptId: admission.attemptId, + event: 'started', + manifestSha256: manifestRecord.manifestSha256, + sourceRevision: admission.sourceRevision, + sourceRoot: manifestRecord.manifest.sourceRoot, + startedAt: now().toISOString(), + }; + const descriptor = writeJournalStart(journalFile, startedEntry, { fsyncImpl }); + + let transactionOutput; + let transactionError; + try { + assertAdmissionFresh(admissionWindow.expiresAt, now()); + transactionOutput = transport(databaseUrl, plan.sql, false); + } catch (error) { + transactionError = error; + } + + let restored; + try { + restored = parseRehearsalReadback( + transport(databaseUrl, readPlan.sql, true), + manifestRecord.manifest, + ); + } catch { + appendRehearsalTerminal(descriptor, admission, 'SUCCESSOR_REHEARSAL_RESTORE_UNKNOWN', now, { + appendJournalImpl, + }); + fail('SUCCESSOR_REHEARSAL_RESTORE_UNKNOWN'); + } + if (restored.serverVersionNum !== preflight.serverVersionNum + || !equal(restored.state, preflight.state)) { + appendRehearsalTerminal(descriptor, admission, 'SUCCESSOR_REHEARSAL_RESTORE_CONFLICT', now, { + appendJournalImpl, + }); + fail('SUCCESSOR_REHEARSAL_RESTORE_CONFLICT'); + } + if (transactionError) { + const bounded = boundedSuccessorError(transactionError); + const code = bounded.code === 'SUCCESSOR_ADMISSION_EXPIRED' + ? bounded.code + : 'SUCCESSOR_REHEARSAL_OUTCOME_UNCONFIRMED'; + appendRehearsalTerminal(descriptor, admission, code, now, { + appendJournalImpl, + }); + fail(code); + } + + let stageStates; + try { + stageStates = parseRehearsalStages( + transactionOutput, + manifestRecord.manifest, + preflight.state, + preflight.serverVersionNum, + ); + } catch { + appendRehearsalTerminal(descriptor, admission, 'SUCCESSOR_REHEARSAL_OUTPUT_INVALID', now, { + appendJournalImpl, + }); + fail('SUCCESSOR_REHEARSAL_OUTPUT_INVALID'); + } + const receiptObservedAt = now(); + try { + assertAdmissionFresh(admissionWindow.expiresAt, receiptObservedAt); + } catch { + appendRehearsalTerminal(descriptor, admission, 'SUCCESSOR_ADMISSION_EXPIRED', now, { + appendJournalImpl, + }); + fail('SUCCESSOR_ADMISSION_EXPIRED'); + } + const priorStateSha256 = sha256(JSON.stringify(canonical(preflight.state))); + const receipt = { + admissionSha256, + attemptId: admission.attemptId, + expiresAt: admission.expiresAt, + id: manifestRecord.manifest.id, + kind: 'successor-rehearsal-receipt', + journalPathSha256: sha256(Buffer.from(journalFile)), + manifestSha256: manifestRecord.manifestSha256, + observedAt: receiptObservedAt.toISOString(), + passed: true, + preflightStateSha256: priorStateSha256, + projectRef: manifestRecord.manifest.projectRef, + purpose: manifestRecord.manifest.purpose, + restoredStateSha256: priorStateSha256, + schemaVersion: 1, + serverVersionNum: preflight.serverVersionNum, + sourceRevision: admission.sourceRevision, + sourceRoot: manifestRecord.manifest.sourceRoot, + stageStates, + stageStatesSha256: sha256(JSON.stringify(canonical(stageStates))), + suffixRoot: plan.suffixRoot, + }; + const receiptSha256 = sha256(canonicalReceiptBytes(receipt)); + try { + writePendingReceiptImpl(pendingReceiptFile, receipt); + } catch { + appendRehearsalTerminal(descriptor, admission, 'SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED', now, { + appendJournalImpl, + }); + fail('SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED'); + } + const verificationBinding = { + admissionSha256, + manifestSha256: manifestRecord.manifestSha256, + receiptSha256, + sourceRevision: admission.sourceRevision, + sourceRoot: manifestRecord.manifest.sourceRoot, + }; + const verificationEntry = { + attemptId: admission.attemptId, + code: 'SUCCESSOR_REHEARSAL_VERIFIED', + event: 'verification', + verifiedAt: now().toISOString(), + ...verificationBinding, + }; + appendRehearsalVerification(descriptor, verificationEntry, { + appendJournalImpl, + }); + const receiptBytes = canonicalReceiptBytes(receipt); + let publishFailed = false; + let publicationReadbackEligible = false; + try { + publishReceiptImpl(pendingReceiptFile, receiptFile); + } catch (error) { + publishFailed = true; + publicationReadbackEligible = error?.publicationReadbackEligible === true; + } + if ((publishFailed && !publicationReadbackEligible) + || !publishedReceiptMatches(receiptFile, receiptBytes)) { + appendRehearsalTerminal( + descriptor, + admission, + 'SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED', + now, + { appendJournalImpl }, + ); + fail('SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED'); + } + const publicationEntry = { + attemptId: admission.attemptId, + code: 'SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED', + event: 'publication', + publishedAt: now().toISOString(), + ...verificationBinding, + }; + let publicationAuditReadback = 'confirmed'; + try { + appendJournalImpl(descriptor, publicationEntry); + } catch (error) { + publicationAuditReadback = error?.publicationAuditReadbackEligible === true && publishedJournalMatches( + journalFile, + [startedEntry, verificationEntry, publicationEntry], + ) ? 'reconciled' : 'unconfirmed'; + try { closeSync(descriptor); } catch { /* append implementation may already have closed it */ } + if (publicationAuditReadback === 'unconfirmed') fail('SUCCESSOR_JOURNAL_APPEND_FAILED'); + } + return Object.freeze({ + attemptId: admission.attemptId, + code: 'SUCCESSOR_REHEARSAL_PASSED', + publicationAuditReadback, + receiptSha256, + publicationReadback: publishFailed ? 'reconciled' : 'confirmed', + sourceRevision: admission.sourceRevision, + status: 'rolled_back', + }); +} + export function runReviewMediaSuccessor({ admissionPath, journalPath, @@ -1046,6 +1874,8 @@ export function runReviewMediaSuccessor({ protectedMainReadbackImpl = currentProtectedMainReadback, rollbackReadbackImpl = captureVercelRollbackReadbackSync, runPsqlImpl, + fsyncImpl = fsyncSync, + appendJournalImpl = appendJournal, } = {}) { const manifestRecord = loadManifestImpl(); assertLaunchReady(manifestRecord.manifest); @@ -1127,6 +1957,7 @@ export function runReviewMediaSuccessor({ }); const admissionSha256 = sha256(admissionBytes); + assertDurableRehearsalArtifacts(dirname(admissionFile), admission, fsyncImpl); const descriptor = writeJournalStart(journalFile, { admissionSha256, attemptId: admission.attemptId, @@ -1135,22 +1966,27 @@ export function runReviewMediaSuccessor({ sourceRevision: admission.sourceRevision, sourceRoot: manifestRecord.manifest.sourceRoot, startedAt: now().toISOString(), - }); + }, { fsyncImpl }); let result; try { assertAdmissionFresh(admissionWindow.expiresAt, now()); result = executeMigrationBundle(databaseUrl, plan, { runPsqlImpl: transport }); } catch (error) { const bounded = boundedSuccessorError(error); - appendJournal(descriptor, { - attemptId: admission.attemptId, - code: bounded.code, - completedAt: now().toISOString(), - event: 'terminal', - }); + try { + appendJournalImpl(descriptor, { + attemptId: admission.attemptId, + code: bounded.code, + completedAt: now().toISOString(), + event: 'terminal', + }); + } catch { + try { closeSync(descriptor); } catch { /* append may already have closed it */ } + throw journalFailure(bounded.code); + } throw bounded; } - appendJournal(descriptor, { + appendJournalImpl(descriptor, { attemptId: admission.attemptId, code: 'SUCCESSOR_COMMITTED', completedAt: now().toISOString(), @@ -1191,8 +2027,7 @@ if (process.argv[1] && resolve(process.argv[1]) === SCRIPT_PATH) { try { console.log(JSON.stringify(main())); } catch (error) { - const bounded = boundedSuccessorError(error); - console.error(JSON.stringify({ code: bounded.code, status: 'failed' })); + console.error(JSON.stringify(successorFailureResult(error))); process.exitCode = 1; } } diff --git a/apps/web/scripts/supabase-migration-bundle.mjs b/apps/web/scripts/supabase-migration-bundle.mjs index 9c8826ce95..83b25d8b89 100644 --- a/apps/web/scripts/supabase-migration-bundle.mjs +++ b/apps/web/scripts/supabase-migration-bundle.mjs @@ -28,6 +28,7 @@ function boundedApplyError(error) { if (['MIGRATION_BUNDLE_READBACK_INVALID', 'MIGRATION_BUNDLE_TERMINAL_READBACK_FAILED'].includes(code)) { return operationError(code); } + if (code === 'SUCCESSOR_ADMISSION_EXPIRED') return operationError(code); return boundedMigrationError(error); } @@ -286,6 +287,53 @@ export function compileElevenMigrationBundle(bundle, materials) { return compileMigrationSequence(bundle, materials, REVIEW_MEDIA_SUCCESSOR_BUNDLE_SIZE); } +/** + * Validate and unwrap the exact eleven migration sources for a rollback-only + * rehearsal. The returned execution strings preserve every admitted source + * statement byte; only an admitted file-level BEGIN/COMMIT envelope is removed + * by migrationEnvelope so the caller can own one outer transaction. + */ +export function compileElevenMigrationRehearsalSources(migrations, materials) { + if (!Array.isArray(migrations) + || migrations.length !== REVIEW_MEDIA_SUCCESSOR_BUNDLE_SIZE + || !Array.isArray(materials) + || materials.length !== REVIEW_MEDIA_SUCCESSOR_BUNDLE_SIZE) { + fail('MIGRATION_BUNDLE_SIZE_INVALID'); + } + const ids = new Set(); + const paths = new Set(); + let previousVersion = ''; + const plans = migrations.map((migration, index) => { + if (!migration || typeof migration !== 'object' || Array.isArray(migration)) { + fail('MIGRATION_BUNDLE_INVALID'); + } + const match = /^backend\/supabase\/migrations\/(\d{14})_[a-z0-9_]+\.sql$/.exec(migration.path ?? ''); + const material = materials[index]; + if (!MIGRATION_ID.test(migration.id ?? '') + || !match + || !HEX_SHA256.test(migration.sha256 ?? '') + || !HEX_SHA256.test(migration.statementVectorSha256 ?? '') + || ids.has(migration.id) + || paths.has(migration.path) + || (previousVersion && match[1] <= previousVersion) + || !material + || !Buffer.isBuffer(material.bytes) + || !Array.isArray(material.originalVector) + || material.path !== migration.path) { + fail('MIGRATION_BUNDLE_ORDER_OR_IDENTITY_INVALID'); + } + const plan = migrationEnvelope(material.bytes, migration, material.originalVector); + if (plan.vectorSha256 !== migration.statementVectorSha256) { + fail('MIGRATION_BUNDLE_VECTOR_DIGEST_MISMATCH'); + } + ids.add(migration.id); + paths.add(migration.path); + previousVersion = match[1]; + return Object.freeze(plan); + }); + return Object.freeze(plans); +} + export function bundleReconciliationSql(compiled) { const { query } = validatedReadbackParts(compiled.finalReadback); return `SET TRANSACTION READ ONLY; SET LOCAL standard_conforming_strings=on; SET LOCAL statement_timeout='15s';\n${query};\n`; diff --git a/apps/web/tests-unit/review-media-migration-bundle.test.ts b/apps/web/tests-unit/review-media-migration-bundle.test.ts index 25c779f42b..213ddc23df 100644 --- a/apps/web/tests-unit/review-media-migration-bundle.test.ts +++ b/apps/web/tests-unit/review-media-migration-bundle.test.ts @@ -4,6 +4,7 @@ import { readFileSync } from 'node:fs'; import { bundleReconciliationOutcome, compileElevenMigrationBundle, + compileElevenMigrationRehearsalSources, elevenMigrationBundleSuffixRoot, executeMigrationBundle, } from '../scripts/supabase-migration-bundle.mjs'; @@ -99,6 +100,34 @@ test('eleven-stage compiler rejects source, vector, order and cardinality drift } }); +test('rollback-only source compiler preserves exact vectors and rejects source identity drift', () => { + const input = fixture(); + const plans = compileElevenMigrationRehearsalSources(input.bundle.migrations, input.materials); + expect(plans).toHaveLength(11); + expect(plans.map(plan => plan.sourceSha256)).toEqual(manifest.migrations.map((migration: any) => migration.sha256)); + expect(plans.map(plan => plan.vectorSha256)).toEqual(manifest.migrations.map((migration: any) => migration.statementVectorSha256)); + { + const drifted = fixture(); + drifted.materials[0].bytes = Buffer.concat([drifted.materials[0].bytes, Buffer.from(' ')]); + expect(() => compileElevenMigrationRehearsalSources( + drifted.bundle.migrations, + drifted.materials, + )).toThrow('MIGRATION_FILE_DIGEST_MISMATCH'); + } + { + const drifted = fixture(); + [drifted.bundle.migrations[0], drifted.bundle.migrations[1]] = [ + drifted.bundle.migrations[1], + drifted.bundle.migrations[0], + ]; + [drifted.materials[0], drifted.materials[1]] = [drifted.materials[1], drifted.materials[0]]; + expect(() => compileElevenMigrationRehearsalSources( + drifted.bundle.migrations, + drifted.materials, + )).toThrow('MIGRATION_BUNDLE_ORDER_OR_IDENTITY_INVALID'); + } +}); + test('lost acknowledgement reconciles exactly once and never resends the eleven mutations', () => { const input = fixture(); const compiled = compileElevenMigrationBundle(input.bundle, input.materials); @@ -114,3 +143,20 @@ test('lost acknowledgement reconciles exactly once and never resends the eleven expect(bundleReconciliationOutcome(compiled.finalReadback.expected, compiled)).toBe('committed'); expect(bundleReconciliationOutcome(compiled.priorReadback.expected, compiled)).toBe('not_applied'); }); + +test('post-lock successor expiry survives bounded reconciliation unchanged', () => { + const input = fixture(); + const compiled = compileElevenMigrationBundle(input.bundle, input.materials); + const calls: string[] = []; + const expired = Object.assign(new Error('provider detail must not escape'), { + code: 'SUCCESSOR_ADMISSION_EXPIRED', + }); + expect(() => executeMigrationBundle('fixture-db', compiled, { runPsqlImpl: (_url, sql) => { + calls.push(sql); + if (calls.length === 1) throw expired; + return JSON.stringify(compiled.priorReadback.expected); + } })).toThrow('SUCCESSOR_ADMISSION_EXPIRED'); + expect(calls).toHaveLength(2); + expect(calls[0].match(/INSERT INTO supabase_migrations\.schema_migrations/g)).toHaveLength(11); + expect(calls[1]).not.toContain('INSERT INTO'); +}); diff --git a/apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts b/apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts new file mode 100644 index 0000000000..669a821aa7 --- /dev/null +++ b/apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts @@ -0,0 +1,305 @@ +import { expect, test } from 'bun:test'; +import { createHash } from 'node:crypto'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join, resolve } from 'node:path'; + +import { + createOwnedContainer, + inspectRetainedProof, + parseArguments, + publishCurrentProof, + removeOwnedContainer, +} from '../performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs'; + +const retainedProofRepositoryPath = 'apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json'; +const retainedProofSha256 = 'cb766e279baad0315e9d7055bc326b41b78a7d50a4972dac3214bc071aee0e49'; +const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex'); + +type Ownership = { + created: boolean; + id: string | null; + name: string | null; + token: string | null; + removed?: boolean; + creationAcknowledged?: boolean; + creationReconciled?: boolean; + removalAcknowledged?: boolean; +}; + +type DockerOptions = { + ackLostOnCreate?: boolean; + ackMismatchOnCreate?: boolean; + failBeforeCreate?: boolean; + wrongOwnerLabel?: boolean; + keepAfterRemoveFailure?: boolean; + ackLostOnRemove?: boolean; +}; + +function dockerFixture(options: DockerOptions = {}) { + const containers = new Map(); + const calls: string[][] = []; + const id = 'a'.repeat(64); + const dockerImpl = (args: string[]) => { + calls.push([...args]); + if (args[0] === 'run') { + if (options.failBeforeCreate) { + return { status: 1, stdout: '', stderr: 'daemon unavailable' }; + } + const name = args[args.indexOf('--name') + 1]; + const label = args[args.indexOf('--label') + 1]; + const token = label.slice(label.indexOf('=') + 1); + containers.set(id, { + id, + name, + token: options.wrongOwnerLabel ? '0'.repeat(32) : token, + }); + if (options.ackLostOnCreate) { + return { status: 1, stdout: '', stderr: 'response lost after create' }; + } + return { + status: 0, + stdout: `${options.ackMismatchOnCreate ? 'b'.repeat(64) : id}\n`, + stderr: '', + }; + } + if (args[0] === 'container' && args[1] === 'inspect') { + const target = args[2]; + const value = [...containers.values()].find(container => ( + container.id === target || container.name === target + )); + if (!value) return { status: 1, stdout: '', stderr: `No such container: ${target}` }; + return { + status: 0, + stdout: JSON.stringify([{ + Id: value.id, + Name: `/${value.name}`, + Config: { Labels: { 'tzudong.review-rehearsal.owner': value.token } }, + }]), + stderr: '', + }; + } + if (args[0] === 'container' && args[1] === 'rm' && args[2] === '--force') { + const target = args[3]; + if (options.keepAfterRemoveFailure) { + return { status: 1, stdout: '', stderr: 'response failed before remove' }; + } + containers.delete(target); + if (options.ackLostOnRemove) { + return { status: 1, stdout: '', stderr: 'response lost after remove' }; + } + return { status: 0, stdout: `${target}\n`, stderr: '' }; + } + throw new Error(`unexpected docker call: ${args.join(' ')}`); + }; + return { calls, containers, dockerImpl, id }; +} + +function freshOwnership(): Ownership { + return { created: false, id: null, name: null, token: null }; +} + +test('checked-in rollback proof is checksum-verified but never reused as current-run output', () => { + const manifestBytes = readFileSync(new URL('../../../.github/review-media-sql-successor.v1.json', import.meta.url)); + const manifest = JSON.parse(manifestBytes.toString('utf8')); + const harnessBytes = readFileSync(new URL( + '../performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs', + import.meta.url, + )); + const retained = inspectRetainedProof({ + bindings: { + manifestSha256: sha256(manifestBytes), + rehearsalHarnessSha256: sha256(harnessBytes), + sourceRoot: manifest.sourceRoot, + }, + }); + + expect(parseArguments([])).toEqual({ outputPath: null }); + expect(retained).toMatchObject({ + present: true, + classification: 'retained-historical-evidence', + repositoryPath: retainedProofRepositoryPath, + expectedSha256: retainedProofSha256, + actualSha256: retainedProofSha256, + currentSourceMatch: false, + usedAsCurrentProof: false, + }); + + const stdout: string[] = []; + const writes: unknown[][] = []; + const proof = { status: 'passed', sourceRoot: manifest.sourceRoot }; + const envelope = publishCurrentProof( + { retainedProof: retained, proof, outputPath: null }, + { + writeFileImpl: (...args: unknown[]) => { writes.push(args); }, + writeStdoutImpl: (value: string) => { stdout.push(value); }, + }, + ); + expect(writes).toHaveLength(0); + expect(stdout).toHaveLength(1); + expect(JSON.parse(stdout[0])).toEqual(envelope); + expect(envelope).toMatchObject({ + kind: 'review-media-sql-local-rehearsal', + currentProof: proof, + currentOutputPath: null, + }); +}); + +test('retained rollback proof rejects bytes not authorized by SHA256SUMS', () => { + const directory = mkdtempSync(join(tmpdir(), 'tzudong-rollback-proof-')); + try { + const outputPath = join(directory, 'pg17-rollback-rehearsal.json'); + const checksumsPath = join(directory, 'SHA256SUMS'); + const proof = Buffer.from(`${JSON.stringify({ + manifestSha256: '1'.repeat(64), + rehearsalHarnessSha256: '2'.repeat(64), + sourceRoot: '3'.repeat(64), + })}\n`); + writeFileSync(outputPath, proof); + writeFileSync(checksumsPath, `${sha256(proof)} pg17-rollback-rehearsal.json\n`); + writeFileSync(outputPath, Buffer.concat([proof, Buffer.from(' ')])); + + expect(() => inspectRetainedProof({ + outputPath, + checksumsPath, + repositoryRoot: directory, + bindings: { + manifestSha256: '1'.repeat(64), + rehearsalHarnessSha256: '2'.repeat(64), + sourceRoot: '3'.repeat(64), + }, + })).toThrow('LOCAL_REHEARSAL_RETAINED_SHA256_MISMATCH'); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +test('explicit current output is exclusive and separate from retained proof', () => { + const outputPath = resolve('/tmp/tzudong-current-rehearsal-proof.json'); + const writes: unknown[][] = []; + const stdout: string[] = []; + publishCurrentProof( + { retainedProof: { present: true }, proof: { status: 'passed' }, outputPath }, + { + writeFileImpl: (...args: unknown[]) => { writes.push(args); }, + writeStdoutImpl: (value: string) => { stdout.push(value); }, + }, + ); + expect(writes).toHaveLength(1); + expect(writes[0][0]).toBe(outputPath); + expect(writes[0][2]).toEqual({ flag: 'wx', mode: 0o600 }); + expect(JSON.parse(stdout[0]).currentOutputPath).toBe(outputPath); +}); + +test('container create acknowledgement loss is reconciled by unique name and owner label', () => { + const fixture = dockerFixture({ ackLostOnCreate: true }); + const ownership = freshOwnership(); + const createdId = createOwnedContainer(ownership, { + dockerImpl: fixture.dockerImpl, + generateToken: () => '1'.repeat(32), + processId: 4242, + }); + + expect(createdId).toBe(fixture.id); + expect(ownership).toMatchObject({ + created: true, + id: fixture.id, + name: 'tzudong-review-rollback-rehearsal-4242-111111111111', + token: '1'.repeat(32), + creationAcknowledged: false, + creationReconciled: true, + }); + const run = fixture.calls.find(args => args[0] === 'run'); + expect(run).toContain(`tzudong.review-rehearsal.owner=${'1'.repeat(32)}`); +}); + +test('same-process rehearsal runs derive distinct container names from their owner tokens', () => { + const firstFixture = dockerFixture(); + const secondFixture = dockerFixture(); + const first = freshOwnership(); + const second = freshOwnership(); + createOwnedContainer(first, { + dockerImpl: firstFixture.dockerImpl, + generateToken: () => '7'.repeat(32), + processId: 4242, + }); + createOwnedContainer(second, { + dockerImpl: secondFixture.dockerImpl, + generateToken: () => '8'.repeat(32), + processId: 4242, + }); + expect(first.name).not.toBe(second.name); + expect(first.token).not.toBe(second.token); +}); + +test('create failure without an owned readback never authorizes cleanup', () => { + const fixture = dockerFixture({ failBeforeCreate: true }); + const ownership = freshOwnership(); + expect(() => createOwnedContainer(ownership, { + dockerImpl: fixture.dockerImpl, + generateToken: () => '2'.repeat(32), + processId: 4243, + })).toThrow('LOCAL_REHEARSAL_CONTAINER_CREATION_UNCONFIRMED'); + expect(removeOwnedContainer(ownership, { dockerImpl: fixture.dockerImpl })).toBe(false); + expect(fixture.calls.some(args => args[0] === 'container' && args[1] === 'rm')).toBe(false); +}); + +test('owner-label mismatch is rejected and the unowned container is not removed', () => { + const fixture = dockerFixture({ wrongOwnerLabel: true }); + const ownership = freshOwnership(); + expect(() => createOwnedContainer(ownership, { + dockerImpl: fixture.dockerImpl, + generateToken: () => '3'.repeat(32), + processId: 4244, + })).toThrow('LOCAL_REHEARSAL_CONTAINER_OWNERSHIP_UNPROVEN'); + expect(removeOwnedContainer(ownership, { dockerImpl: fixture.dockerImpl })).toBe(false); + expect(fixture.containers.size).toBe(1); + expect(fixture.calls.some(args => args[0] === 'container' && args[1] === 'rm')).toBe(false); +}); + +test('acknowledgement mismatch retains exact owned ID for safe finally cleanup', () => { + const fixture = dockerFixture({ ackMismatchOnCreate: true }); + const ownership = freshOwnership(); + expect(() => createOwnedContainer(ownership, { + dockerImpl: fixture.dockerImpl, + generateToken: () => '4'.repeat(32), + processId: 4245, + })).toThrow('LOCAL_REHEARSAL_CONTAINER_ACK_MISMATCH'); + expect(ownership).toMatchObject({ created: true, id: fixture.id, token: '4'.repeat(32) }); + expect(removeOwnedContainer(ownership, { dockerImpl: fixture.dockerImpl })).toBe(true); + const removal = fixture.calls.find(args => args[0] === 'container' && args[1] === 'rm'); + expect(removal).toEqual(['container', 'rm', '--force', fixture.id]); + expect(fixture.containers.size).toBe(0); +}); + +test('cleanup failure leaves ownership live and cannot be misreported as removed', () => { + const fixture = dockerFixture({ keepAfterRemoveFailure: true }); + const ownership = freshOwnership(); + createOwnedContainer(ownership, { + dockerImpl: fixture.dockerImpl, + generateToken: () => '5'.repeat(32), + processId: 4246, + }); + expect(() => removeOwnedContainer(ownership, { dockerImpl: fixture.dockerImpl })) + .toThrow('LOCAL_REHEARSAL_CONTAINER_REMOVAL_UNCONFIRMED'); + expect(ownership.created).toBe(true); + expect(ownership.removed).not.toBe(true); + expect(fixture.containers.size).toBe(1); +}); + +test('cleanup acknowledgement loss succeeds only after exact-ID absence readback', () => { + const fixture = dockerFixture({ ackLostOnRemove: true }); + const ownership = freshOwnership(); + createOwnedContainer(ownership, { + dockerImpl: fixture.dockerImpl, + generateToken: () => '6'.repeat(32), + processId: 4247, + }); + expect(removeOwnedContainer(ownership, { dockerImpl: fixture.dockerImpl })).toBe(true); + expect(ownership).toMatchObject({ + created: false, + removed: true, + removalAcknowledged: false, + }); + expect(fixture.containers.size).toBe(0); +}); diff --git a/apps/web/tests-unit/review-media-sql-successor-harness.test.ts b/apps/web/tests-unit/review-media-sql-successor-harness.test.ts new file mode 100644 index 0000000000..453a792834 --- /dev/null +++ b/apps/web/tests-unit/review-media-sql-successor-harness.test.ts @@ -0,0 +1,146 @@ +import { expect, test } from 'bun:test'; +import { createHash } from 'node:crypto'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { + createOwnedDatabase, + dropOwnedDatabase, + inspectRetainedProof, +} from '../performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs'; + +const retainedProofRepositoryPath = 'apps/web/performance/review-db-successor-20261012/pg17-local-replay.json'; +const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex'); + +test('retained proof is checksum-verified and distinguished from the current harness source', () => { + const manifestBytes = readFileSync(new URL('../../../.github/review-media-sql-successor.v1.json', import.meta.url)); + const manifest = JSON.parse(manifestBytes.toString('utf8')); + const harnessBytes = readFileSync(new URL( + '../performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs', + import.meta.url, + )); + const retained = inspectRetainedProof({ + currentBindings: { + manifestSha256: sha256(manifestBytes), + rehearsalHarnessSha256: sha256(harnessBytes), + sourceRoot: manifest.sourceRoot, + }, + }); + + expect(retained).toMatchObject({ + present: true, + classification: 'retained-historical-evidence', + repositoryPath: retainedProofRepositoryPath, + expectedSha256: '6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f', + actualSha256: '6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f', + currentSourceMatch: false, + usedAsCurrentProof: false, + }); + expect(retained.bindingDifferences.some(({ field }: { field: string }) => ( + field === 'rehearsalHarnessSha256' + ))).toBe(true); +}); + +test('retained proof with bytes not authorized by SHA256SUMS is rejected', () => { + const directory = mkdtempSync(join(tmpdir(), 'tzudong-retained-proof-')); + try { + const outputPath = join(directory, 'pg17-local-replay.json'); + const checksumsPath = join(directory, 'SHA256SUMS'); + const proof = Buffer.from(`${JSON.stringify({ + status: 'passed', + temporaryDatabaseDropped: true, + hostedWrites: false, + operatingWrites: false, + manifestSha256: '1'.repeat(64), + rehearsalHarnessSha256: '2'.repeat(64), + sourceRoot: '3'.repeat(64), + })}\n`); + writeFileSync(outputPath, proof); + writeFileSync(checksumsPath, `${sha256(proof)} ${retainedProofRepositoryPath}\n`); + writeFileSync(outputPath, Buffer.concat([proof, Buffer.from(' ')])); + + expect(() => inspectRetainedProof({ + outputPath, + checksumsPath, + currentBindings: { + manifestSha256: '1'.repeat(64), + rehearsalHarnessSha256: '2'.repeat(64), + sourceRoot: '3'.repeat(64), + }, + })).toThrow('LOCAL_REHEARSAL_RETAINED_SHA256_MISMATCH'); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +for (const [label, failingDatabaseCall, expectedCode] of [ + ['predecessor', 1, 'LOCAL_REHEARSAL_PREDECESSOR_FAILED'], + ['fixture', 7, 'fixture failed'], +] as const) { + test(`owned database is cleaned after ${label} SQL fails`, () => { + const ownership = { database: false }; + let databaseExists = false; + let databaseCalls = 0; + let drops = 0; + const dependencies = { + scalarImpl: () => (databaseExists ? '1' : '0'), + readFileImpl: () => Buffer.from('-- predecessor fixture'), + psqlImpl: (database: string, sql: string | Buffer) => { + const statement = sql.toString(); + if (database === 'postgres' && statement.startsWith('CREATE DATABASE ')) { + databaseExists = true; + return {}; + } + if (database === 'postgres' && statement.startsWith('DROP DATABASE ')) { + databaseExists = false; + drops += 1; + return {}; + } + databaseCalls += 1; + if (databaseCalls === failingDatabaseCall) throw new Error(`${label} failed`); + return {}; + }, + }; + + try { + createOwnedDatabase(ownership, dependencies); + throw new Error('expected fixture construction to fail'); + } catch (error) { + expect((error as Error).message).toContain(expectedCode); + expect(ownership.database).toBe(true); + } finally { + dropOwnedDatabase(ownership, dependencies); + } + + expect(databaseExists).toBe(false); + expect(ownership.database).toBe(false); + expect(drops).toBe(1); + }); +} + +test('pre-existing database is never acquired or dropped', () => { + const ownership = { database: false }; + let databaseExists = true; + let scalarCalls = 0; + let psqlCalls = 0; + const dependencies = { + scalarImpl: () => { + scalarCalls += 1; + return databaseExists ? '1' : '0'; + }, + readFileImpl: () => Buffer.from(''), + psqlImpl: () => { + psqlCalls += 1; + databaseExists = false; + return {}; + }, + }; + + expect(() => createOwnedDatabase(ownership, dependencies)).toThrow('LOCAL_REHEARSAL_TARGET_ALREADY_EXISTS'); + expect(dropOwnedDatabase(ownership, dependencies)).toBe(false); + expect(databaseExists).toBe(true); + expect(ownership.database).toBe(false); + expect(scalarCalls).toBe(1); + expect(psqlCalls).toBe(0); +}); diff --git a/apps/web/tests-unit/review-media-sql-successor.test.ts b/apps/web/tests-unit/review-media-sql-successor.test.ts index df0ab216b5..00941884d7 100644 --- a/apps/web/tests-unit/review-media-sql-successor.test.ts +++ b/apps/web/tests-unit/review-media-sql-successor.test.ts @@ -1,16 +1,36 @@ import { afterAll, expect, test } from 'bun:test'; import { createHash } from 'node:crypto'; -import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { + chmodSync, + closeSync, + existsSync, + fsyncSync, + linkSync, + mkdtempSync, + openSync, + readFileSync, + rmSync, + writeFileSync, + writeSync, +} from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { + appendJournal, + successorFailureResult, + compileSuccessorRehearsalPlan, + compileSuccessorStateReadQuery, compileSuccessorPlan, + createSuccessorPsqlRunner, loadSuccessorManifest, protectedSourceReadbackBinding, runReviewMediaSuccessor, + runReviewMediaSuccessorRehearsal, validateSuccessorAdmission, + validateSuccessorRehearsalAdmission, } from '../scripts/review-media-sql-successor.mjs'; +import { parseRehearsalArguments } from '../scripts/review-media-sql-rehearsal.mjs'; import { elevenMigrationBundleSuffixRoot } from '../scripts/supabase-migration-bundle.mjs'; import { statementSpans } from '../scripts/supabase-migration-transaction.mjs'; @@ -39,7 +59,6 @@ const databaseUrlFixture = new URL('postgresql://localhost/postgres'); databaseUrlFixture.hostname = 'aws-0-ap-southeast-1.pooler.supabase.com'; databaseUrlFixture.username = 'postgres.aqlcofblfxdrjhhdmarw'; databaseUrlFixture.port = '6543'; -databaseUrlFixture.password = 'fixture-only'; const databaseUrl = databaseUrlFixture.toString(); const rollback = Object.freeze({ deploymentId: 'dpl_EUxJnWmDDozCSMWb2sqNSPgKJLM5', @@ -62,6 +81,11 @@ const canonical = (value: any): any => Array.isArray(value) ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) : value; const canonicalBytes = (value: any) => Buffer.from(`${JSON.stringify(canonical(value))}\n`); +const writeCanonicalLine = (descriptor: number, entry: any, { sync = true } = {}) => { + const bytes = canonicalBytes(entry); + expect(writeSync(descriptor, bytes)).toBe(bytes.length); + if (sync) fsyncSync(descriptor); +}; afterAll(() => { for (const directory of directories) rmSync(directory, { recursive: true, force: true }); @@ -116,6 +140,7 @@ function admission() { projectRef: manifest.projectRef, protectedMainReceiptSha256: '1'.repeat(64), purpose: manifest.purpose, + rehearsalJournalSha256: '5'.repeat(64), rehearsalReceiptSha256: '2'.repeat(64), rollback: { ...structuredClone(rollback), readbackSha256: '3'.repeat(64) }, schemaVersion: 1, @@ -133,6 +158,7 @@ function custodyFiles(document = admission()) { chmodSync(directory, 0o700); const admissionPath = join(directory, 'admission.json'); const journalPath = join(directory, 'attempt.jsonl'); + const rehearsalJournalPath = join(directory, 'rehearsal-journal.jsonl'); document.journalPathSha256 = sha256(journalPath); const common = { schemaVersion: 1, @@ -144,6 +170,8 @@ function custodyFiles(document = admission()) { observedAt: document.createdAt, expiresAt: document.expiresAt, }; + const rehearsalAttemptId = '123e4567-e89b-42d3-a456-426614174001'; + const rehearsalAdmissionSha256 = '5'.repeat(64); const receipts: Record = { 'source-receipt.json': { ...common, kind: 'successor-source-receipt', sourceRoot: document.sourceRoot }, 'protected-main-receipt.json': { @@ -156,10 +184,17 @@ function custodyFiles(document = admission()) { }, 'rehearsal-receipt.json': { ...common, + admissionSha256: rehearsalAdmissionSha256, + attemptId: rehearsalAttemptId, kind: 'successor-rehearsal-receipt', + journalPathSha256: sha256(rehearsalJournalPath), + observedAt: new Date(Date.parse(document.createdAt) - 60_000).toISOString(), passed: true, + preflightStateSha256: sha256(JSON.stringify(canonical(document.stageStates[0]))), + restoredStateSha256: sha256(JSON.stringify(canonical(document.stageStates[0]))), serverVersionNum: 170006, sourceRoot: document.sourceRoot, + stageStates: document.stageStates, stageStatesSha256: sha256(JSON.stringify(canonical(document.stageStates))), suffixRoot: elevenMigrationBundleSuffixRoot(manifest.migrations), }, @@ -182,6 +217,43 @@ function custodyFiles(document = admission()) { document.sourceReceiptSha256 = sha256(canonicalBytes(receipts['source-receipt.json'])); document.protectedMainReceiptSha256 = sha256(canonicalBytes(receipts['protected-main-receipt.json'])); document.rehearsalReceiptSha256 = sha256(canonicalBytes(receipts['rehearsal-receipt.json'])); + const rehearsalJournal = [ + { + admissionSha256: rehearsalAdmissionSha256, + attemptId: rehearsalAttemptId, + event: 'started', + manifestSha256: document.manifestSha256, + sourceRevision: document.sourceRevision, + sourceRoot: document.sourceRoot, + startedAt: new Date(Date.parse(document.createdAt) - 120_000).toISOString(), + }, + { + admissionSha256: rehearsalAdmissionSha256, + attemptId: rehearsalAttemptId, + code: 'SUCCESSOR_REHEARSAL_VERIFIED', + event: 'verification', + manifestSha256: document.manifestSha256, + receiptSha256: document.rehearsalReceiptSha256, + sourceRevision: document.sourceRevision, + sourceRoot: document.sourceRoot, + verifiedAt: new Date(Date.parse(document.createdAt) - 60_000).toISOString(), + }, + { + admissionSha256: rehearsalAdmissionSha256, + attemptId: rehearsalAttemptId, + code: 'SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED', + event: 'publication', + manifestSha256: document.manifestSha256, + publishedAt: new Date(Date.parse(document.createdAt) - 30_000).toISOString(), + receiptSha256: document.rehearsalReceiptSha256, + sourceRevision: document.sourceRevision, + sourceRoot: document.sourceRoot, + }, + ]; + const rehearsalJournalBytes = Buffer.from(rehearsalJournal + .map(entry => JSON.stringify(canonical(entry))) + .join('\n') + '\n'); + document.rehearsalJournalSha256 = sha256(rehearsalJournalBytes); document.rollback.readbackSha256 = sha256(canonicalBytes(receipts['rollback-readback-receipt.json'])); for (const [key, state] of Object.entries(manifest.requiredWriterFences)) { const name = `writer-fence-${key}.json`; @@ -193,9 +265,117 @@ function custodyFiles(document = admission()) { writeFileSync(path, canonicalBytes(receipt), { mode: 0o600 }); chmodSync(path, 0o600); } + writeFileSync(rehearsalJournalPath, rehearsalJournalBytes, { mode: 0o600 }); + chmodSync(rehearsalJournalPath, 0o600); writeFileSync(admissionPath, `${JSON.stringify(document)}\n`, { mode: 0o600 }); chmodSync(admissionPath, 0o600); - return { admissionPath, directory, document, journalPath }; + return { admissionPath, directory, document, journalPath, rehearsalJournalPath }; +} + +function rehearsalAdmission() { + const writerFences = Object.fromEntries(Object.entries(manifest.requiredWriterFences).map( + ([key, state], index) => [key, { evidenceSha256: (index + 8).toString(16).repeat(64), state }], + )); + return { + attemptId: '123e4567-e89b-42d3-a456-426614174001', + createdAt: '2026-10-12T07:59:00.000Z', + expiresAt: '2026-10-12T08:10:00.000Z', + id: manifest.id, + journalPathSha256: 'f'.repeat(64), + kind: 'successor-rehearsal-admission', + manifestSha256: record.manifestSha256, + projectRef: manifest.projectRef, + protectedMainReceiptSha256: '1'.repeat(64), + purpose: manifest.purpose, + receiptPathSha256: '2'.repeat(64), + rollback: { ...structuredClone(rollback), readbackSha256: '3'.repeat(64) }, + schemaVersion: 1, + sourceReceiptSha256: '4'.repeat(64), + sourceRevision: revision, + sourceRoot: manifest.sourceRoot, + writerFences, + }; +} + +function rehearsalCustodyFiles(document = rehearsalAdmission()) { + const directory = mkdtempSync(join(tmpdir(), 'tzudong-review-rehearsal-')); + directories.push(directory); + chmodSync(directory, 0o700); + const admissionPath = join(directory, 'rehearsal-admission.json'); + const journalPath = join(directory, 'rehearsal-journal.jsonl'); + const receiptPath = join(directory, 'rehearsal-receipt.json'); + const pendingReceiptPath = `${receiptPath}.${document.attemptId}.pending`; + document.journalPathSha256 = sha256(journalPath); + document.receiptPathSha256 = sha256(receiptPath); + const common = { + schemaVersion: 1, + id: document.id, + projectRef: document.projectRef, + purpose: document.purpose, + manifestSha256: document.manifestSha256, + sourceRevision: document.sourceRevision, + observedAt: document.createdAt, + expiresAt: document.expiresAt, + }; + const receipts: Record = { + 'source-receipt.json': { ...common, kind: 'successor-source-receipt', sourceRoot: document.sourceRoot }, + 'protected-main-receipt.json': { + ...common, + kind: 'protected-main-readback-receipt', + ref: manifest.protectedSource.ref, + remote: manifest.protectedSource.remote, + repositoryUrl: manifest.protectedSource.repositoryUrl, + revision: document.sourceRevision, + }, + 'rollback-readback-receipt.json': { + ...common, + kind: 'rollback-readback-receipt', + deploymentId: document.rollback.deploymentId, + deploymentSha: document.rollback.deploymentSha, + deploymentUrl: document.rollback.deploymentUrl, + gitRef: document.rollback.gitRef, + productionAliases: document.rollback.productionAliases, + projectId: document.rollback.projectId, + readyState: document.rollback.readyState, + repository: document.rollback.repository, + state: document.rollback.state, + target: document.rollback.target, + teamId: document.rollback.teamId, + }, + }; + document.sourceReceiptSha256 = sha256(canonicalBytes(receipts['source-receipt.json'])); + document.protectedMainReceiptSha256 = sha256(canonicalBytes(receipts['protected-main-receipt.json'])); + document.rollback.readbackSha256 = sha256(canonicalBytes(receipts['rollback-readback-receipt.json'])); + for (const [key, state] of Object.entries(manifest.requiredWriterFences)) { + const name = `writer-fence-${key}.json`; + receipts[name] = { ...common, fence: key, kind: 'writer-fence-readback-receipt', state }; + document.writerFences[key].evidenceSha256 = sha256(canonicalBytes(receipts[name])); + } + for (const [name, receipt] of Object.entries(receipts)) { + const path = join(directory, name); + writeFileSync(path, canonicalBytes(receipt), { mode: 0o600 }); + chmodSync(path, 0o600); + } + writeFileSync(admissionPath, `${JSON.stringify(document)}\n`, { mode: 0o600 }); + chmodSync(admissionPath, 0o600); + return { admissionPath, directory, document, journalPath, pendingReceiptPath, receiptPath }; +} + +function rehearsalReadback(state = stageStates()[0]) { + return JSON.stringify({ + kind: 'successor-rehearsal-readback', + serverVersionNum: 170006, + state, + }); +} + +function rehearsalStageOutput(stages = stageStates()) { + return stages.map((state, index) => JSON.stringify({ + index, + kind: 'successor-rehearsal-stage', + serverVersionNum: 170006, + state, + })).join('\n'); } function rollbackReadback(now = fixedNow) { @@ -226,6 +406,26 @@ test('manifest binds the exact eleven sources and remains held without fresh adm expect(() => loadSuccessorManifest({ readVectorImpl: () => ['drift'] })).toThrow('SUCCESSOR_SOURCE_DRIFT'); }); +test('rehearsal CLI requires one admission, journal and receipt path', () => { + expect(parseRehearsalArguments([ + '--admission', '/private/admission.json', + '--journal', '/private/journal.jsonl', + '--receipt', '/private/rehearsal-receipt.json', + ])).toEqual({ + admission: '/private/admission.json', + journal: '/private/journal.jsonl', + receipt: '/private/rehearsal-receipt.json', + }); + expect(() => parseRehearsalArguments(['--admission', '/private/admission.json'])).toThrow( + 'SUCCESSOR_REHEARSAL_ARGUMENT_INVALID', + ); + expect(() => parseRehearsalArguments([ + '--admission', '/private/admission.json', + '--admission', '/private/duplicate.json', + '--receipt', '/private/rehearsal-receipt.json', + ])).toThrow('SUCCESSOR_REHEARSAL_ARGUMENT_INVALID'); +}); + test('compiled plan has one advisory lock, one outer transaction and eleven ledger writes', () => { const admitted = admission(); const plan = compileSuccessorPlan(executableRecord, admitted); @@ -259,6 +459,679 @@ test('compiled plan has one advisory lock, one outer transaction and eleven ledg expect(storageState).not.toContain('updated_at'); }); +test('actual psql runner with a spy transport preserves the post-lock expiry code', () => { + let spawnCalls = 0; + const runner = createSuccessorPsqlRunner(databaseUrl, { + environment: {}, + psql: '/fixture/psql', + spawnImpl: (command: string, args: string[], options: any) => { + spawnCalls += 1; + expect(command).toBe('/fixture/psql'); + expect(args).toContain('--file=-'); + expect(options.input).toContain('SUCCESSOR_ADMISSION_EXPIRED'); + return { + status: 1, + stderr: 'ERROR: P0001: SUCCESSOR_ADMISSION_EXPIRED\n', + stdout: '', + }; + }, + }); + let observedCode = ''; + try { + runner(databaseUrl, "DO $$ BEGIN RAISE EXCEPTION 'SUCCESSOR_ADMISSION_EXPIRED'; END $$;", false); + } catch (error: any) { + observedCode = error?.code; + } + expect(spawnCalls).toBe(1); + expect(observedCode).toBe('SUCCESSOR_ADMISSION_EXPIRED'); +}); + +test('rollback-only rehearsal plan owns one transaction and captures twelve safe stages', () => { + const admitted = rehearsalAdmission(); + const stages = stageStates(); + const plan = compileSuccessorRehearsalPlan(record, admitted, stages[0]); + expect(plan.transactionMode).toBe('explicit-rollback-only'); + expect(plan.sql.match(/^BEGIN;$/gm)).toHaveLength(1); + expect(plan.sql.match(/^ROLLBACK;$/gm)).toHaveLength(1); + expect(plan.sql.trim().endsWith('ROLLBACK;')).toBe(true); + expect(plan.sql.match(/pg_advisory_xact_lock\(7311754282260211::bigint\)/g)).toHaveLength(1); + expect(plan.sql.match(/INSERT INTO supabase_migrations\.schema_migrations/g)).toHaveLength(11); + expect(plan.sql.match(/'successor-rehearsal-stage'/g)).toHaveLength(12); + expect(plan.sql.match(/^\\o \/dev\/null$/gm)).toHaveLength(11); + expect(plan.sql.match(/^\\o$/gm)).toHaveLength(11); + const lockTimeout = plan.sql.indexOf("SET LOCAL lock_timeout='2s'"); + const statementTimeout = plan.sql.indexOf("SET LOCAL statement_timeout='15s'"); + const advisoryLock = plan.sql.indexOf('pg_advisory_xact_lock(7311754282260211::bigint)'); + const expiryCheck = plan.sql.indexOf('SUCCESSOR_ADMISSION_EXPIRED'); + const baselineGuard = plan.sql.indexOf('SUCCESSOR_REHEARSAL_BASELINE_INVALID'); + expect(lockTimeout).toBeGreaterThanOrEqual(0); + expect(statementTimeout).toBeGreaterThan(lockTimeout); + expect(advisoryLock).toBeGreaterThan(statementTimeout); + expect(expiryCheck).toBeGreaterThan(advisoryLock); + expect(baselineGuard).toBeGreaterThan(expiryCheck); + expect(plan.sql).toContain(`clock_timestamp() >= '${admitted.expiresAt}'::timestamptz`); + expect(plan.sql).toContain(manifest.migrations[0].originalStatementVector[1]); + expect(plan.sql).toContain(manifest.migrations.at(-1).originalStatementVector.at(-2)); + expect(compileSuccessorStateReadQuery(record).sql).toContain('successor-rehearsal-readback'); +}); + +test('rehearsal admission is distinct and rejects stale source or any wrong writer fence', () => { + { + const files = rehearsalCustodyFiles(); + expect(validateSuccessorRehearsalAdmission(files.document, record, { + now: fixedNow, + receiptDirectory: files.directory, + })).toMatchObject({ expiresAt: Date.parse(files.document.expiresAt) }); + } + { + const files = rehearsalCustodyFiles(); + files.document.sourceRoot = '0'.repeat(64); + expect(() => validateSuccessorRehearsalAdmission(files.document, record, { + now: fixedNow, + receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); + } + for (const key of Object.keys(manifest.requiredWriterFences)) { + const files = rehearsalCustodyFiles(); + files.document.writerFences[key].state = 'wrong'; + expect(() => validateSuccessorRehearsalAdmission(files.document, record, { + now: fixedNow, + receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); + } +}); + +test('rehearsal rolls back, separately confirms restoration and emits an apply-bound receipt', () => { + const files = rehearsalCustodyFiles(); + const calls: string[] = []; + let rollbackReads = 0; + const result = runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => { rollbackReads += 1; return rollbackReadback(); }, + runPsqlImpl: (_url, sql) => { + calls.push(sql); + if (calls.length === 2) return rehearsalStageOutput(); + return rehearsalReadback(); + }, + }); + expect(result).toMatchObject({ + code: 'SUCCESSOR_REHEARSAL_PASSED', + publicationAuditReadback: 'confirmed', + publicationReadback: 'confirmed', + status: 'rolled_back', + }); + expect(rollbackReads).toBe(2); + expect(calls).toHaveLength(3); + expect(calls[0]).toContain('READ ONLY'); + expect(calls[1].match(/INSERT INTO supabase_migrations\.schema_migrations/g)).toHaveLength(11); + expect(calls[1].trim().endsWith('ROLLBACK;')).toBe(true); + expect(calls[2]).toContain('READ ONLY'); + const receipt = JSON.parse(readFileSync(files.receiptPath, 'utf8')); + expect(receipt.stageStates).toEqual(stageStates()); + expect(receipt.attemptId).toBe(files.document.attemptId); + expect(receipt.admissionSha256).toBe(sha256(readFileSync(files.admissionPath))); + expect(receipt.journalPathSha256).toBe(sha256(files.journalPath)); + expect(receipt.preflightStateSha256).toBe(receipt.restoredStateSha256); + expect(receipt.stageStatesSha256).toBe(sha256(JSON.stringify(canonical(receipt.stageStates)))); + const journal = readFileSync(files.journalPath, 'utf8').trim().split('\n').map(JSON.parse); + expect(journal).toHaveLength(3); + expect(journal[1]).toMatchObject({ + admissionSha256: receipt.admissionSha256, + attemptId: receipt.attemptId, + code: 'SUCCESSOR_REHEARSAL_VERIFIED', + event: 'verification', + manifestSha256: record.manifestSha256, + receiptSha256: result.receiptSha256, + sourceRevision: revision, + sourceRoot: manifest.sourceRoot, + }); + expect(journal[2]).toMatchObject({ + admissionSha256: receipt.admissionSha256, + attemptId: receipt.attemptId, + code: 'SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED', + event: 'publication', + manifestSha256: record.manifestSha256, + receiptSha256: result.receiptSha256, + sourceRevision: revision, + sourceRoot: manifest.sourceRoot, + }); + expect(existsSync(files.pendingReceiptPath)).toBe(true); +}); + +test('lost rehearsal response performs restoration readback once and never resends', () => { + const files = rehearsalCustodyFiles(); + const calls: string[] = []; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: (_url, sql) => { + calls.push(sql); + if (calls.length === 2) throw new Error('lost response'); + return rehearsalReadback(); + }, + })).toThrow('SUCCESSOR_REHEARSAL_OUTCOME_UNCONFIRMED'); + expect(calls).toHaveLength(3); + expect(calls.filter(sql => /INSERT INTO supabase_migrations\.schema_migrations/.test(sql))).toHaveLength(1); + expect(() => readFileSync(files.receiptPath)).toThrow(); + expect(readFileSync(files.journalPath, 'utf8')).toContain('SUCCESSOR_REHEARSAL_OUTCOME_UNCONFIRMED'); +}); + +test('post-lock rehearsal expiry keeps its exact bounded code after restoration readback', () => { + const files = rehearsalCustodyFiles(); + let calls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + if (calls === 2) { + throw Object.assign(new Error('provider diagnostic must not escape'), { + code: 'SUCCESSOR_ADMISSION_EXPIRED', + }); + } + return rehearsalReadback(); + }, + })).toThrow('SUCCESSOR_ADMISSION_EXPIRED'); + expect(calls).toBe(3); + const journal = readFileSync(files.journalPath, 'utf8'); + expect(journal).toContain('SUCCESSOR_ADMISSION_EXPIRED'); + expect(journal).not.toContain('SUCCESSOR_REHEARSAL_OUTCOME_UNCONFIRMED'); +}); + +test('incomplete rehearsal output cannot create an apply admission receipt', () => { + const files = rehearsalCustodyFiles(); + let calls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + if (calls === 2) return rehearsalStageOutput().split('\n').slice(0, 11).join('\n'); + return rehearsalReadback(); + }, + })).toThrow('SUCCESSOR_REHEARSAL_OUTPUT_INVALID'); + expect(calls).toBe(3); + expect(() => readFileSync(files.receiptPath)).toThrow(); +}); + +test('verification line write-success then fsync-failure never publishes the canonical receipt', () => { + const files = rehearsalCustodyFiles(); + let calls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + appendJournalImpl: (descriptor: number, entry: any) => { + const bytes = Buffer.from(`${JSON.stringify(canonical(entry))}\n`); + expect(writeSync(descriptor, bytes)).toBe(bytes.length); + throw new Error('verification fsync failed after full write'); + }, + })).toThrow('SUCCESSOR_JOURNAL_APPEND_FAILED'); + expect(existsSync(files.receiptPath)).toBe(false); + expect(JSON.parse(readFileSync(files.pendingReceiptPath, 'utf8')).passed).toBe(true); + const orphanJournal = readFileSync(files.journalPath); + const journalEntries = orphanJournal.toString('utf8').trim().split('\n').map(JSON.parse); + expect(journalEntries).toHaveLength(2); + expect(journalEntries[1]).toMatchObject({ + code: 'SUCCESSOR_REHEARSAL_VERIFIED', + event: 'verification', + }); + + const applyFiles = custodyFiles(); + writeFileSync(applyFiles.rehearsalJournalPath, orphanJournal, { mode: 0o600 }); + applyFiles.document.rehearsalJournalSha256 = sha256(orphanJournal); + expect(() => validateSuccessorAdmission(applyFiles.document, executableRecord, { + now: fixedNow, + receiptDirectory: applyFiles.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); +}); + +test('pending receipt full-write then fsync-failure records failure without canonical publication', () => { + const files = rehearsalCustodyFiles(); + let calls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + writePendingReceiptImpl: (path: string, receipt: any) => { + const descriptor = openSync(path, 'wx', 0o600); + const bytes = canonicalBytes(receipt); + expect(writeSync(descriptor, bytes)).toBe(bytes.length); + closeSync(descriptor); + throw new Error('pending receipt fsync failed after full write'); + }, + })).toThrow('SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED'); + expect(existsSync(files.receiptPath)).toBe(false); + expect(JSON.parse(readFileSync(files.pendingReceiptPath, 'utf8')).passed).toBe(true); + const journal = readFileSync(files.journalPath, 'utf8'); + expect(journal).toContain('SUCCESSOR_REHEARSAL_RECEIPT_WRITE_FAILED'); + expect(journal).not.toContain('SUCCESSOR_REHEARSAL_VERIFIED'); + const applyFiles = custodyFiles(); + const failedJournal = Buffer.from(journal); + writeFileSync(applyFiles.rehearsalJournalPath, failedJournal, { mode: 0o600 }); + applyFiles.document.rehearsalJournalSha256 = sha256(failedJournal); + expect(() => validateSuccessorAdmission(applyFiles.document, executableRecord, { + now: fixedNow, + receiptDirectory: applyFiles.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); +}); + +test('prepublication interruption leaves verified journal and pending receipt apply-invalid', () => { + const files = rehearsalCustodyFiles(); + let calls = 0; + let appendCalls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + appendJournalImpl: (descriptor: number, entry: any) => { + appendCalls += 1; + if (appendCalls > 1) throw new Error('interrupted before failure audit'); + const bytes = Buffer.from(`${JSON.stringify(canonical(entry))}\n`); + expect(writeSync(descriptor, bytes)).toBe(bytes.length); + fsyncSync(descriptor); + }, + publishReceiptImpl: () => { throw new Error('interrupted before atomic publication'); }, + })).toThrow('SUCCESSOR_JOURNAL_APPEND_FAILED'); + expect(appendCalls).toBe(2); + expect(existsSync(files.pendingReceiptPath)).toBe(true); + expect(existsSync(files.receiptPath)).toBe(false); + expect(readFileSync(files.journalPath, 'utf8')).toContain('SUCCESSOR_REHEARSAL_VERIFIED'); +}); + +test('failed canonical publication is audited as failure, never as passed', () => { + const files = rehearsalCustodyFiles(); + let calls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + publishReceiptImpl: () => { throw new Error('canonical publication failed'); }, + })).toThrow('SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED'); + expect(existsSync(files.pendingReceiptPath)).toBe(true); + expect(existsSync(files.receiptPath)).toBe(false); + const journal = readFileSync(files.journalPath, 'utf8'); + expect(journal).toContain('SUCCESSOR_REHEARSAL_VERIFIED'); + expect(journal).toContain('SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED'); + expect(journal).not.toContain('SUCCESSOR_REHEARSAL_PASSED'); +}); + +test('pre-existing exact canonical bytes do not make an unperformed link durable', () => { + const files = rehearsalCustodyFiles(); + let calls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + writePendingReceiptImpl: (pendingPath: string, receipt: any) => { + const bytes = canonicalBytes(receipt); + writeFileSync(pendingPath, bytes, { flag: 'wx', mode: 0o600 }); + writeFileSync(files.receiptPath, bytes, { flag: 'wx', mode: 0o600 }); + }, + })).toThrow('SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED'); + expect(readFileSync(files.receiptPath)).toEqual(readFileSync(files.pendingReceiptPath)); + const journal = readFileSync(files.journalPath, 'utf8'); + expect(journal).toContain('SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED'); + expect(journal).not.toContain('SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED'); +}); + +test('unconfirmed directory durability cannot be reconciled from visible canonical bytes', () => { + const files = rehearsalCustodyFiles(); + let calls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + publishReceiptImpl: (pendingPath: string, receiptPath: string) => { + linkSync(pendingPath, receiptPath); + throw new Error('directory fsync not confirmed'); + }, + })).toThrow('SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED'); + expect(existsSync(files.receiptPath)).toBe(true); + const journal = readFileSync(files.journalPath, 'utf8'); + expect(journal).toContain('SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED'); + + const applyFiles = custodyFiles(); + const entries = readFileSync(applyFiles.rehearsalJournalPath, 'utf8').trim().split('\n').map(JSON.parse); + entries.push({ + attemptId: entries[0].attemptId, + code: 'SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED', + completedAt: fixedNow.toISOString(), + event: 'terminal', + }); + const failed = Buffer.from(entries.map(entry => JSON.stringify(canonical(entry))).join('\n') + '\n'); + writeFileSync(applyFiles.rehearsalJournalPath, failed, { mode: 0o600 }); + applyFiles.document.rehearsalJournalSha256 = sha256(failed); + expect(() => validateSuccessorAdmission(applyFiles.document, executableRecord, { + now: fixedNow, + receiptDirectory: applyFiles.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); +}); + +test('directory fsync failure plus failure-audit loss leaves visible receipt apply-invalid', () => { + const files = rehearsalCustodyFiles(); + let transportCalls = 0; + let appendCalls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + transportCalls += 1; + return transportCalls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + appendJournalImpl: (descriptor: number, entry: any) => { + appendCalls += 1; + if (appendCalls === 1) return writeCanonicalLine(descriptor, entry); + throw new Error('failure audit unavailable after directory fsync failure'); + }, + publishReceiptImpl: (pendingPath: string, receiptPath: string) => { + linkSync(pendingPath, receiptPath); + const error: Error & { publicationReadbackEligible?: boolean } = new Error( + 'directory fsync failed after canonical link', + ); + error.publicationReadbackEligible = false; + throw error; + }, + })).toThrow('SUCCESSOR_JOURNAL_APPEND_FAILED'); + expect(appendCalls).toBe(2); + expect(readFileSync(files.receiptPath)).toEqual(readFileSync(files.pendingReceiptPath)); + const orphanJournal = readFileSync(files.journalPath); + expect(orphanJournal.toString('utf8').trim().split('\n')).toHaveLength(2); + + const applyDocument = admission(); + applyDocument.createdAt = fixedNow.toISOString(); + const applyFiles = custodyFiles(applyDocument); + const rehearsalReceipt = readFileSync(files.receiptPath); + writeFileSync(join(applyFiles.directory, 'rehearsal-receipt.json'), rehearsalReceipt, { mode: 0o600 }); + writeFileSync(applyFiles.rehearsalJournalPath, orphanJournal, { mode: 0o600 }); + applyFiles.document.rehearsalReceiptSha256 = sha256(rehearsalReceipt); + applyFiles.document.rehearsalJournalSha256 = sha256(orphanJournal); + expect(() => validateSuccessorAdmission(applyFiles.document, executableRecord, { + now: fixedNow, + receiptDirectory: applyFiles.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); +}); + +for (const failureAt of ['fsync', 'close'] as const) { + test(`publication audit ${failureAt} failure requires proven durability before reconciliation`, () => { + const files = rehearsalCustodyFiles(); + let transportCalls = 0; + let appendCalls = 0; + const run = () => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + transportCalls += 1; + return transportCalls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + appendJournalImpl: (descriptor: number, entry: any, options: any) => { + appendCalls += 1; + if (appendCalls === 1) return appendJournal(descriptor, entry, options); + return appendJournal(descriptor, entry, { + fsyncImpl: (fd: number) => { + if (failureAt === 'fsync') throw new Error('fixture sync failure'); + fsyncSync(fd); + }, + closeImpl: () => { throw new Error('fixture close acknowledgement loss'); }, + }); + }, + }); + if (failureAt === 'fsync') expect(run).toThrow('SUCCESSOR_JOURNAL_APPEND_FAILED'); + else expect(run()).toMatchObject({ + code: 'SUCCESSOR_REHEARSAL_PASSED', + publicationAuditReadback: 'reconciled', + publicationReadback: 'confirmed', + }); + expect(transportCalls).toBe(3); + expect(appendCalls).toBe(2); + // Both failures leave visible bytes; only the completed fsync can reconcile. + const journal = readFileSync(files.journalPath, 'utf8').trim().split('\n').map(JSON.parse); + expect(journal).toHaveLength(3); + expect(journal[2].code).toBe('SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED'); + }); +} + +test('publication audit failure before any bytes leaves durable receipt apply-invalid', () => { + const files = rehearsalCustodyFiles(); + let transportCalls = 0; + let appendCalls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + transportCalls += 1; + return transportCalls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + appendJournalImpl: (descriptor: number, entry: any) => { + appendCalls += 1; + if (appendCalls === 1) return writeCanonicalLine(descriptor, entry); + throw new Error('publication audit unavailable before write'); + }, + })).toThrow('SUCCESSOR_JOURNAL_APPEND_FAILED'); + expect(appendCalls).toBe(2); + expect(readFileSync(files.receiptPath)).toEqual(readFileSync(files.pendingReceiptPath)); + const journal = readFileSync(files.journalPath); + expect(journal.toString('utf8').trim().split('\n')).toHaveLength(2); + + const applyFiles = custodyFiles(); + const validEntries = readFileSync(applyFiles.rehearsalJournalPath, 'utf8') + .trim().split('\n').map(JSON.parse); + const missingPublication = Buffer.from(validEntries.slice(0, 2) + .map(entry => JSON.stringify(canonical(entry))).join('\n') + '\n'); + writeFileSync(applyFiles.rehearsalJournalPath, missingPublication, { mode: 0o600 }); + applyFiles.document.rehearsalJournalSha256 = sha256(missingPublication); + expect(() => validateSuccessorAdmission(applyFiles.document, executableRecord, { + now: fixedNow, + receiptDirectory: applyFiles.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); +}); + +test('atomic publication acknowledgement loss reconciles exact receipt without retry', () => { + const files = rehearsalCustodyFiles(); + let calls = 0; + let publishCalls = 0; + const result = runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + publishReceiptImpl: (pendingPath: string, receiptPath: string) => { + publishCalls += 1; + linkSync(pendingPath, receiptPath); + const directoryDescriptor = openSync(files.directory, 'r'); + fsyncSync(directoryDescriptor); + closeSync(directoryDescriptor); + const error: Error & { publicationReadbackEligible?: boolean } = new Error( + 'publication acknowledgement lost', + ); + error.publicationReadbackEligible = true; + throw error; + }, + }); + expect(publishCalls).toBe(1); + expect(result).toMatchObject({ + code: 'SUCCESSOR_REHEARSAL_PASSED', + publicationAuditReadback: 'confirmed', + publicationReadback: 'reconciled', + }); + expect(readFileSync(files.receiptPath)).toEqual(readFileSync(files.pendingReceiptPath)); +}); + +test('apply rejects orphan and semantically tampered rehearsal journals even when rehashed', () => { + { + const files = custodyFiles(); + const firstLine = readFileSync(files.rehearsalJournalPath, 'utf8').split('\n')[0]; + const orphan = Buffer.from(`${firstLine}\n`); + writeFileSync(files.rehearsalJournalPath, orphan, { mode: 0o600 }); + files.document.rehearsalJournalSha256 = sha256(orphan); + expect(() => validateSuccessorAdmission(files.document, executableRecord, { + now: fixedNow, + receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); + } + { + const files = custodyFiles(); + const entries = readFileSync(files.rehearsalJournalPath, 'utf8').trim().split('\n').map(JSON.parse); + entries[1].receiptSha256 = '0'.repeat(64); + const tampered = Buffer.from(entries.map(entry => JSON.stringify(canonical(entry))).join('\n') + '\n'); + writeFileSync(files.rehearsalJournalPath, tampered, { mode: 0o600 }); + files.document.rehearsalJournalSha256 = sha256(tampered); + expect(() => validateSuccessorAdmission(files.document, executableRecord, { + now: fixedNow, + receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); + } + { + const files = custodyFiles(); + const entries = readFileSync(files.rehearsalJournalPath, 'utf8').trim().split('\n').map(JSON.parse); + entries[2] = { + attemptId: entries[0].attemptId, + code: 'SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED', + completedAt: fixedNow.toISOString(), + event: 'terminal', + }; + const failed = Buffer.from(entries.map(entry => JSON.stringify(canonical(entry))).join('\n') + '\n'); + writeFileSync(files.rehearsalJournalPath, failed, { mode: 0o600 }); + files.document.rehearsalJournalSha256 = sha256(failed); + expect(() => validateSuccessorAdmission(files.document, executableRecord, { + now: fixedNow, + receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); + } +}); + test('admission rejects wrong preimage and every wrong writer fence', () => { { const files = custodyFiles(); @@ -390,3 +1263,76 @@ test('lost apply acknowledgement reconciles once and never resends eleven mutati expect(calls[2]).not.toMatch(/^INSERT INTO supabase_migrations\.schema_migrations/gm); expect(readFileSync(files.journalPath, 'utf8')).toContain('MIGRATION_BUNDLE_OUTCOME_UNCONFIRMED'); }); + +for (const failureAt of [1, 2, 3, 4, 5]) { + test(`apply fsync failure at custody/start boundary ${failureAt} prevents database mutation`, () => { + const files = custodyFiles(); + const plan = compileSuccessorPlan(executableRecord, files.document); + let syncs = 0; + let transports = 0; + expect(() => runReviewMediaSuccessor({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => executableRecord, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + fsyncImpl: (fd: number) => { + syncs += 1; + if (syncs === failureAt) throw new Error('fixture durability failure'); + fsyncSync(fd); + }, + runPsqlImpl: (_url, sql) => { + transports += 1; + expect(sql).toStartWith('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;'); + return JSON.stringify(plan.priorReadback.expected); + }, + })).toThrow(failureAt <= 3 ? 'SUCCESSOR_ADMISSION_INVALID' : 'SUCCESSOR_JOURNAL_INVALID'); + expect(transports).toBe(1); + expect(syncs).toBe(failureAt); + }); +} + +test('apply preserves expiry and audit failure as separate bounded outcomes', () => { + const files = custodyFiles(); + const plan = compileSuccessorPlan(executableRecord, files.document); + let transports = 0; + let observed: any; + try { + runReviewMediaSuccessor({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => executableRecord, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + transports += 1; + if (transports === 2) throw Object.assign(new Error('fixture diagnostic omitted'), { + code: 'SUCCESSOR_ADMISSION_EXPIRED', + }); + return JSON.stringify(plan.priorReadback.expected); + }, + appendJournalImpl: () => { throw new Error('fixture audit failure omitted'); }, + }); + } catch (error) { observed = successorFailureResult(error); } + expect(observed).toEqual({ + code: 'SUCCESSOR_JOURNAL_APPEND_FAILED', + operationCode: 'SUCCESSOR_ADMISSION_EXPIRED', + status: 'failed', + }); + expect(transports).toBe(3); + expect(successorFailureResult({ + code: 'SUCCESSOR_JOURNAL_APPEND_FAILED', operationCode: 'arbitrary diagnostic', + })).toEqual({ code: 'SUCCESSOR_JOURNAL_APPEND_FAILED', status: 'failed' }); + expect(successorFailureResult({ + code: 'SUCCESSOR_JOURNAL_APPEND_FAILED', + get operationCode() { throw new Error('fixture diagnostic must not escape'); }, + })).toEqual({ code: 'SUCCESSOR_JOURNAL_APPEND_FAILED', status: 'failed' }); +}); From 6dc0c6a2a8a77f80162ea1dc74e6a2ee87f837f1 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:20:21 +0900 Subject: [PATCH 03/14] fix(map): show live provider failures and allow explicit retry --- apps/web/components/map/NaverMapView.tsx | 2 +- .../components/map/map-view-status-panels.tsx | 4 +- apps/web/hooks/use-naver-maps.tsx | 63 ++------- .../naver-loader-failure-browser.test.ts | 121 ++++++++++++++++++ 4 files changed, 134 insertions(+), 56 deletions(-) create mode 100644 apps/web/tests-unit/naver-loader-failure-browser.test.ts diff --git a/apps/web/components/map/NaverMapView.tsx b/apps/web/components/map/NaverMapView.tsx index c1dc803486..465145a75f 100644 --- a/apps/web/components/map/NaverMapView.tsx +++ b/apps/web/components/map/NaverMapView.tsx @@ -3687,7 +3687,7 @@ const NaverMapView = memo(({ // 로딩 에러 처리 if (loadError || mapInitError) { - return ; + return ; } // 로딩 중 diff --git a/apps/web/components/map/map-view-status-panels.tsx b/apps/web/components/map/map-view-status-panels.tsx index 4c0ad92e3c..ff11f84c53 100644 --- a/apps/web/components/map/map-view-status-panels.tsx +++ b/apps/web/components/map/map-view-status-panels.tsx @@ -24,6 +24,6 @@ export function MapViewGoogleLoadErrorState() { return ; } -export function NaverMapLoadErrorState(_props: { message: string }) { - return ; +export function NaverMapLoadErrorState({ onRetry }: { message: string; onRetry?: () => void }) { + return ; } diff --git a/apps/web/hooks/use-naver-maps.tsx b/apps/web/hooks/use-naver-maps.tsx index eb48694944..8f06f35c64 100644 --- a/apps/web/hooks/use-naver-maps.tsx +++ b/apps/web/hooks/use-naver-maps.tsx @@ -4,7 +4,6 @@ import { useEffect, useState, useCallback } from "react"; import { NAVER_MAPS_CONFIG } from "@/config/maps"; import { isLocalNaverStyleFallbackHost, - LOCAL_NAVER_MAP_STUB_SCRIPT_URL, resolveNaverMapScriptUrl, rewriteNaverStyleUrlForLocalhost, shouldUseLocalNaverMapStub, @@ -106,48 +105,6 @@ function hasUsableNaverMaps() { && maps.Event ); } -function loadLocalNaverStubFallback( - setIsLoaded: (value: boolean) => void, - setIsLoading: (value: boolean) => void, - setLoadError: (value: Error | null) => void, -) { - if (hasUsableNaverMaps()) { - setIsLoaded(true); - setIsLoading(false); - return; - } - - const existingStub = document.querySelector('script[data-local-naver-maps="true"]'); - const attachStub = (script: HTMLScriptElement) => { - script.addEventListener('load', () => { - if (hasUsableNaverMaps()) { - setIsLoaded(true); - setLoadError(null); - } else { - setLoadError(new Error('네이버 지도 API 초기화 실패')); - } - setIsLoading(false); - }); - script.addEventListener('error', () => { - setLoadError(new Error('네이버 지도 API 로딩 실패 - 네트워크 오류')); - setIsLoading(false); - }); - }; - - if (existingStub instanceof HTMLScriptElement) { - attachStub(existingStub); - return; - } - - const script = document.createElement('script'); - script.type = 'text/javascript'; - script.dataset.localNaverMaps = 'true'; - script.src = LOCAL_NAVER_MAP_STUB_SCRIPT_URL; - script.async = true; - attachStub(script); - document.head.appendChild(script); -} - const EFFECTIVE_SCRIPT_URL = resolveNaverMapScriptUrl({ clientId: NAVER_MAPS_CONFIG.clientId, configuredScriptUrl: process.env.NEXT_PUBLIC_NAVER_MAPS_SCRIPT_URL, @@ -169,6 +126,8 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { // 이미 로드되었거나 로딩 중이면 무시 if (isLoaded || isLoading) return; + setLoadError(null); + // Client ID 검증 if (!NAVER_MAPS_CONFIG.clientId && !LOCAL_OFFLINE_MAP) { setLoadError(new Error("네이버 지도 Client ID가 설정되지 않았습니다.")); @@ -224,19 +183,15 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { setIsLoading(false); return; } - if (!LOCAL_OFFLINE_MAP) { - loadLocalNaverStubFallback(setIsLoaded, setIsLoading, setLoadError); - return; - } + // A failed live provider must never become a test map. Remove + // this failed owned script so a user retry can request it again. + script.remove(); setLoadError(new Error("네이버 지도 API 초기화 실패")); setIsLoading(false); }; script.onerror = () => { - if (!LOCAL_OFFLINE_MAP) { - loadLocalNaverStubFallback(setIsLoaded, setIsLoading, setLoadError); - return; - } + script.remove(); setLoadError( new Error( "네이버 지도 API 스크립트 로드 실패 - Client ID 또는 웹 서비스 URL을 확인해주세요." @@ -268,10 +223,12 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { // autoLoad가 true면 로드 시도 useEffect(() => { - if (autoLoad) { + // After a provider failure, wait for an explicit retry. A loading-state + // change must not create an unbounded automatic request loop. + if (autoLoad && !loadError) { load(); } - }, [autoLoad, load]); + }, [autoLoad, load, loadError]); return { isLoaded, loadError, isLoading, load }; } diff --git a/apps/web/tests-unit/naver-loader-failure-browser.test.ts b/apps/web/tests-unit/naver-loader-failure-browser.test.ts new file mode 100644 index 0000000000..b08c08c3d6 --- /dev/null +++ b/apps/web/tests-unit/naver-loader-failure-browser.test.ts @@ -0,0 +1,121 @@ +import { afterAll, beforeAll, expect, test } from 'bun:test'; +import { existsSync } from 'node:fs'; +import { join } from 'node:path'; +import { execFileSync } from 'node:child_process'; +import { chromium, expect as browserExpect, type Browser, type Page } from '@playwright/test'; + +const app = join(import.meta.dir, '..'); +const chromeAvailable = [ + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/opt/google/chrome/chrome', + join(process.env.ProgramFiles ?? process.env.PROGRAMFILES ?? '', 'Google/Chrome/Application/chrome.exe'), + join(process.env.LOCALAPPDATA ?? '', 'Google/Chrome/Application/chrome.exe'), +].some(existsSync); +const browserTest = chromeAvailable ? test : test.skip; +const ui = browserExpect.configure({ timeout: 1500 }); +const baseline = process.env.NAVER_LOADER_BASELINE === '056a86600f412eb42df4304e3206841679012f3e'; +let browser: Browser; +const scripts: Record = {}; +const fakeSdk = 'window.naver={maps:{Map:function(){},LatLng:function(){},Point:function(){},Event:{}}};'; +const fixture = ` +import React from 'react'; +import {createRoot} from 'react-dom/client'; +import {useNaverMaps} from './hooks/use-naver-maps'; +import {NaverMapLoadErrorState} from './components/map/map-view-status-panels'; +function Consumer({id}) { + const {isLoaded,isLoading,loadError,load}=useNaverMaps({autoLoad:true}); + return
+ {loadError?'error':isLoaded?'ready':isLoading?'loading':'idle'} + {loadError&&} +
; +} +const count=Number(new URLSearchParams(location.search).get('consumers')||1); +createRoot(document.getElementById('root')).render(<>{Array.from({length:count},(_,i)=>)}); +`; + +beforeAll(async () => { + if (!chromeAvailable) return; + for (const mode of ['live', 'local']) { + const bundle = await Bun.build({ entrypoints: ['naver-loader-fixture'], target: 'browser', format: 'iife', write: false, + define: { 'process.env': JSON.stringify({ NODE_ENV: 'production', NEXT_PUBLIC_NAVER_CLIENT_ID: mode === 'live' ? 'browser-fixture-client' : '', NEXT_PUBLIC_TZUDONG_LOCAL_RUNTIME: mode === 'local' ? '1' : '0' }) }, + plugins: [{ name: 'naver-loader-fixture', setup(builder) { + builder.onResolve({ filter: /^naver-loader-fixture$/ }, () => ({ path: 'fixture.tsx', namespace: 'fixture' })); + builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: fixture, loader: 'tsx', resolveDir: app })); + if (baseline) { + builder.onResolve({ filter: /hooks\/use-naver-maps$/ }, () => ({ path: 'use-naver-maps.tsx', namespace: 'baseline' })); + builder.onLoad({ filter: /.*/, namespace: 'baseline' }, () => ({ contents: execFileSync('git', ['show', '056a86600f412eb42df4304e3206841679012f3e:apps/web/hooks/use-naver-maps.tsx'], { cwd: app, encoding: 'utf8' }), loader: 'tsx', resolveDir: app })); + } + builder.onResolve({ filter: /^@\// }, ({ path }) => { + const resolved = ['', '.ts', '.tsx'].map(suffix => join(app, path.slice(2)) + suffix).find(existsSync); + return resolved ? { path: resolved } : undefined; + }); + } }] }); + expect(bundle.success).toBe(true); + scripts[mode] = await bundle.outputs[0].text(); + } + browser = await chromium.launch({ channel: 'chrome', headless: true }); +}); +afterAll(async () => { await browser?.close(); }); + +type Outcome = 'network-error' | 'missing-api' | 'success'; +async function run(outcomes: Outcome[], check: (page: Page, counts: { live: number; stub: number }) => Promise, consumers = 1, mode = 'live') { + const context = await browser.newContext(); + const page = await context.newPage(); + const counts = { live: 0, stub: 0 }; + await page.route('**/*', async route => { + const url = new URL(route.request().url()); + if (route.request().isNavigationRequest()) return route.fulfill({ contentType: 'text/html', body: '
' }); + if (url.hostname === 'oapi.map.naver.com' && url.pathname === '/openapi/v3/maps.js') { + const outcome = outcomes[Math.min(counts.live++, outcomes.length - 1)]; + if (outcome === 'network-error') return route.abort('failed'); + return route.fulfill({ contentType: 'application/javascript', body: outcome === 'success' ? fakeSdk : '/* provider loaded without an API */' }); + } + if (url.pathname === '/__local/naver-maps.js') { + counts.stub++; + return route.fulfill({ contentType: 'application/javascript', body: fakeSdk }); + } + return route.abort(); + }); + try { + await page.goto(`http://naver-loader.fixture.test/?consumers=${consumers}`); + await page.addScriptTag({ content: scripts[mode] }); + await check(page, counts); + } finally { await context.close(); } +} + +browserTest('live SDK success becomes ready without requesting the local stub', async () => run(['success'], async (page, counts) => { + await ui(page.getByTestId('state-0')).toHaveText('ready'); + expect(counts).toEqual({ live: 1, stub: 0 }); +})); + +for (const failure of ['network-error', 'missing-api'] as const) { + browserTest(`live ${failure} remains a visible error and never becomes a simulated map`, async () => run([failure], async (page, counts) => { + await ui(page.getByTestId('state-0')).toHaveText('error'); + await ui(page.getByRole('heading', { name: '지도를 불러오지 못했어요' })).toBeVisible(); + await page.waitForTimeout(250); // Observe automatic retries; no product delay. + expect(counts).toEqual({ live: 1, stub: 0 }); + expect(await page.locator('script[data-local-naver-maps="true"]').count()).toBe(0); + })); + + browserTest(`explicit retry recovers after ${failure} without a page reload`, async () => run([failure, 'success'], async (page, counts) => { + await ui(page.getByTestId('state-0')).toHaveText('error'); + await page.getByRole('button', { name: '다시 시도', exact: true }).click(); + await ui(page.getByTestId('state-0')).toHaveText('ready'); + expect(counts).toEqual({ live: 2, stub: 0 }); + })); +} + +browserTest('concurrent consumers share a failure and one explicit recovery request', async () => run(['network-error', 'success'], async (page, counts) => { + await ui(page.getByTestId('state-0')).toHaveText('error'); + await ui(page.getByTestId('state-1')).toHaveText('error'); + expect(counts).toEqual({ live: 1, stub: 0 }); + await page.getByRole('button', { name: '다시 시도', exact: true }).evaluateAll(buttons => buttons.forEach(button => (button as HTMLButtonElement).click())); + await ui(page.getByTestId('state-0')).toHaveText('ready'); + await ui(page.getByTestId('state-1')).toHaveText('ready'); + expect(counts).toEqual({ live: 2, stub: 0 }); +}, 2)); + +browserTest('explicit local offline mode still uses its deterministic stub', async () => run([], async (page, counts) => { + await ui(page.getByTestId('state-0')).toHaveText('ready'); + expect(counts).toEqual({ live: 0, stub: 1 }); +}, 1, 'local')); From 073a9de51da22f7aeddd3b4c18ae299afef9540b Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Tue, 13 Oct 2026 14:10:29 +0900 Subject: [PATCH 04/14] fix(web): prepare marker styles before deferred map initialization --- apps/web/app/layout.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 8f3e1bc7b8..175c69711d 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -8,6 +8,8 @@ import { VIEWPORT_HEIGHT_BOOTSTRAP_SOURCE } from "@/lib/viewport-height-bootstra import { RootSpeedInsights } from "./root-speed-insights"; import { LocalWorkspaceBanner } from "@/components/home/LocalWorkspaceBanner"; import "./globals.css"; +// Marker styles are tiny and must be ready before the deferred map initializes. +import "@/components/map/marker-icons.css"; export const dynamic = "force-dynamic"; // 카카오톡 OG 이미지 표시를 위해 절대 URL 필요 From 80063fe840c9c8589b440f5a8b288d144d032e2c Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:09:23 +0900 Subject: [PATCH 05/14] Revert "fix(web): prepare marker styles before deferred map initialization" This reverts commit 073a9de51da22f7aeddd3b4c18ae299afef9540b. --- apps/web/app/layout.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 175c69711d..8f3e1bc7b8 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -8,8 +8,6 @@ import { VIEWPORT_HEIGHT_BOOTSTRAP_SOURCE } from "@/lib/viewport-height-bootstra import { RootSpeedInsights } from "./root-speed-insights"; import { LocalWorkspaceBanner } from "@/components/home/LocalWorkspaceBanner"; import "./globals.css"; -// Marker styles are tiny and must be ready before the deferred map initializes. -import "@/components/map/marker-icons.css"; export const dynamic = "force-dynamic"; // 카카오톡 OG 이미지 표시를 위해 절대 URL 필요 From 0833ed81843ebdf9744d4e64f088b7843f62e2e3 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Tue, 13 Oct 2026 16:45:41 +0900 Subject: [PATCH 06/14] Preserve terminal outcomes and reconcile fixture ownership after lost acknowledgements --- .github/review-media-sql-successor.v1.json | 2 +- .../review-db-rehearsal-20261012/README.md | 27 +- .../review-db-rehearsal-20261012/SHA256SUMS | 20 +- .../SHA256SUMS-4d4ecf60-historical | 16 + .../SHA256SUMS.sha256 | 1 + .../interrupted-attempt-20261013.json | 13 + .../pg17-terminal-boundaries-20261013.json | 65 ++ ...l-boundary-unit-verification-20261013.json | 67 ++ .../review-db-successor-20261012/README.md | 36 +- .../review-db-successor-20261012/SHA256SUMS | 17 +- .../SHA256SUMS-4d4ecf60-historical | 6 + .../SHA256SUMS.sha256 | 1 + ...rship-failure-interpretation-20261013.json | 7 + ...ture-ownership-first-attempt-20261013.json | 14 + ...e-ownership-protocol-failure-20261013.json | 36 + .../pg17-fixture-ownership-20261013.json | 60 ++ ...17-fixture-ownership-current-20261013.json | 60 ++ .../verify-fixture-ownership.mjs | 157 ++++ .../verify-review-media-sql-successor.mjs | 372 ++++++++- .../SHA256SUMS | 5 + .../SHA256SUMS.sha256 | 1 + .../controller-reverification.json | 32 + .../verification.json | 57 ++ .../review-media-operating-admission.mjs | 717 ++++++++++++++++++ .../scripts/review-media-sql-successor.mjs | 99 ++- .../review-media-operating-admission.test.ts | 523 +++++++++++++ ...review-media-sql-successor-harness.test.ts | 472 +++++++++++- .../review-media-sql-successor.test.ts | 292 +++++++ ...view-media-operating-admission-20261013.md | 24 + 29 files changed, 3121 insertions(+), 78 deletions(-) create mode 100644 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4d4ecf60-historical create mode 100644 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 create mode 100644 apps/web/performance/review-db-rehearsal-20261012/interrupted-attempt-20261013.json create mode 100644 apps/web/performance/review-db-rehearsal-20261012/pg17-terminal-boundaries-20261013.json create mode 100644 apps/web/performance/review-db-rehearsal-20261012/terminal-boundary-unit-verification-20261013.json create mode 100644 apps/web/performance/review-db-successor-20261012/SHA256SUMS-4d4ecf60-historical create mode 100644 apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 create mode 100644 apps/web/performance/review-db-successor-20261012/fixture-ownership-failure-interpretation-20261013.json create mode 100644 apps/web/performance/review-db-successor-20261012/fixture-ownership-first-attempt-20261013.json create mode 100644 apps/web/performance/review-db-successor-20261012/fixture-ownership-protocol-failure-20261013.json create mode 100644 apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-20261013.json create mode 100644 apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-current-20261013.json create mode 100644 apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs create mode 100644 apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS create mode 100644 apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 create mode 100644 apps/web/performance/review-media-operating-admission-20261013/controller-reverification.json create mode 100644 apps/web/performance/review-media-operating-admission-20261013/verification.json create mode 100644 apps/web/scripts/review-media-operating-admission.mjs create mode 100644 apps/web/tests-unit/review-media-operating-admission.test.ts create mode 100644 docs/operations/review-media-operating-admission-20261013.md diff --git a/.github/review-media-sql-successor.v1.json b/.github/review-media-sql-successor.v1.json index 1bc26aa656..810888e14e 100644 --- a/.github/review-media-sql-successor.v1.json +++ b/.github/review-media-sql-successor.v1.json @@ -309,7 +309,7 @@ "toolchain": [ { "path": "apps/web/scripts/review-media-sql-successor.mjs", - "sha256": "2080947cbc3c92934c98cee259d4f41e9c1eebc80e437f20862849fd36bd4f40" + "sha256": "dcf552697251c2f10d859c7a2f56937a0f6e92ee9467e5b919807e498eff1d49" }, { "path": "apps/web/scripts/review-media-sql-rehearsal.mjs", diff --git a/apps/web/performance/review-db-rehearsal-20261012/README.md b/apps/web/performance/review-db-rehearsal-20261012/README.md index 917f4e8e4a..3d4e20dbcd 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/README.md +++ b/apps/web/performance/review-db-rehearsal-20261012/README.md @@ -30,10 +30,13 @@ and durable publication protocol. Operating application remains unperformed. ## Current verification -Pinned Node 24 and Bun 1.4.0 ran four affected suites: **53 passed, 0 failed, -284 assertions**. The checks include publication fsync/close failures, five +Pinned Node 24 and Bun 1.4.0 ran five affected suites: **81 passed, 0 failed, +450 assertions**. The checks include publication fsync/close failures, five pre-mutation durability failures, expiry plus audit failure, tampered/orphan -journals, retained evidence, unique container ownership and lost acknowledgements. +journals, retained evidence, private admission formatting, database/role ownership +and lost acknowledgements. Filesystem expiry is rechecked before verification, +publication, and successful return. A committed database outcome survives terminal +audit failure; only proven durability plus exact journal bytes permits reconciliation. Targeted ESLint and Node syntax checks passed. The final source was replayed once in an isolated Supabase PostgreSQL 17.6 @@ -49,10 +52,10 @@ The verifier explicitly checks `public.profiles` and `public.reviews` are empty; it does not claim a universal personal-data census. Its synthetic ledger has 86 entries, not an operating ledger readback. -- Current proof: `pg17-rollback-rehearsal-final-20261013.json` -- Proof SHA-256: `7f74c175be3eee72b5e93eb75dac07bfefc0ef88d7d9957c26d877ee125ff590` -- Manifest SHA-256: `8297b18793513956118c720e3aa5bd481d0769645c263f680a5272825b70569e` -- Toolchain root: `91a7166b76b0f867b4e74dd0e2258d1f22457c75a61e55b161148378f08ac739` +- Current proof: `pg17-terminal-boundaries-20261013.json` +- Proof SHA-256: `0cfc9c10ff7b70c606f6eeef33fff8dbd14636dc15b7c963925097454ee454a2` +- Manifest SHA-256: `ae764bb760102a6d7d066be669c85248feed1823eafded4bf8c246a062090a8c` +- Toolchain root: `a0eeeb623c80b8a5e416b84397bdb07e4bd65c670d98b70707cc816d37e9a18b` - Source root: `61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544` - Verifier SHA-256: `8088aa57f39045e2bf93cba084711119395098d053318b70aaa676b5c13fea26` - Compiled SQL SHA-256: `285d0c859117006646389dbd2a22de9aef1c0b97bc796a9e4d4448a8a232b093` @@ -89,3 +92,13 @@ source/rollback and writer-fence receipts, private admission and a new journal. Storage projections exclude timestamps and cannot distinguish recreation of an otherwise identical bucket. The retained evidence and source hashes are in `SHA256SUMS`; raw SQL, credentials and operating rows are absent from the proof. + +The earlier `pg17-rollback-rehearsal-final-20261013.json` remains immutable +historical evidence for the earlier durability implementation. +`interrupted-attempt-20261013.json` records a stopped attempt without a result; +its exact owned container was removed after the process was confirmed absent. +It is not counted as a successful replay. + +Private admission formatting does not authenticate observation origin. The +operating capture adapter, fresh writer-fence evidence, protected promotion, +hosted application and live user flows remain separate unfinished steps. diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS index 2fbd3f4e24..3db2e6c74b 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS @@ -1,16 +1,20 @@ -8297b18793513956118c720e3aa5bd481d0769645c263f680a5272825b70569e .github/review-media-sql-successor.v1.json -2080947cbc3c92934c98cee259d4f41e9c1eebc80e437f20862849fd36bd4f40 apps/web/scripts/review-media-sql-successor.mjs -c893c294719776bf5b064c3355866ab97e0fa9412082d9b47d7de5266094c65f apps/web/scripts/review-media-sql-rehearsal.mjs -fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs -7ab17835c727c22bad4138b86f4d3695d55bd1b4511141d1d374b2ca4694d080 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs -0489bf7de7252e652c9c4967be77664cdeacb8afc07dcf846d73f0cf7c25dbe9 apps/web/performance/review-db-rehearsal-20261012/README.md +ae764bb760102a6d7d066be669c85248feed1823eafded4bf8c246a062090a8c .github/review-media-sql-successor.v1.json +9a3fd226f9506f83b3be3f09c64d31770536d99ee3e4167bce081b4e5c3d3ad0 apps/web/performance/review-db-rehearsal-20261012/README.md +6d9cbca375e6790ee1051aa9d9bf1c45beeeece8d919651281885b4c04962981 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4d4ecf60-historical 6c474d8e78c0eba3f15c077dbdd8634f073524c3b5f842213372bf0f1fed84fc apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical +a12f8880f2a0b8b5493e37ea4b2619962d42643581c14c889f0c6f6e8d528129 apps/web/performance/review-db-rehearsal-20261012/interrupted-attempt-20261013.json b279a6b900dd3be10b9084a6362d169e87435bae955b2a1419858709645a6883 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json 7f74c175be3eee72b5e93eb75dac07bfefc0ef88d7d9957c26d877ee125ff590 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json cb766e279baad0315e9d7055bc326b41b78a7d50a4972dac3214bc071aee0e49 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json +0cfc9c10ff7b70c606f6eeef33fff8dbd14636dc15b7c963925097454ee454a2 apps/web/performance/review-db-rehearsal-20261012/pg17-terminal-boundaries-20261013.json c53a467eaf9b929647bc1d66248308ba1ff73eba89fc9221dbf7db4711eb8562 apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json +fad17a393e5fc74d06dec2be58c41265d21f355d5ddce92a3947bb67a823e73a apps/web/performance/review-db-rehearsal-20261012/terminal-boundary-unit-verification-20261013.json 8088aa57f39045e2bf93cba084711119395098d053318b70aaa676b5c13fea26 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs -099e0b728cbfde7f1b292ec5c90e1ab14af54bdf9e87d07ef44ead9bb885883e apps/web/tests-unit/review-media-sql-successor.test.ts +8c3d7df52c281a9df1d6a4bd2acefb59f9a23b60185559fc6a8298cd65fde370 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +c893c294719776bf5b064c3355866ab97e0fa9412082d9b47d7de5266094c65f apps/web/scripts/review-media-sql-rehearsal.mjs +dcf552697251c2f10d859c7a2f56937a0f6e92ee9467e5b919807e498eff1d49 apps/web/scripts/review-media-sql-successor.mjs +fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs dc1f2594e629d5af16541db93e2e51f9b98d8282dd7fbeb802dd240fa6f522c0 apps/web/tests-unit/review-media-migration-bundle.test.ts -c2c833a6fb2529477e02bbdc353a1e840d0c079d045a81dbc8a0e905ef16a601 apps/web/tests-unit/review-media-sql-successor-harness.test.ts b25b5dcc6516435a1c74a038c20856cc5dd719b303d9c3f3e600dc314197c7cd apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts +31bd78568b7bff5196626d87cf7f0fe9b84d695da8cef597fa93e4d742f99845 apps/web/tests-unit/review-media-sql-successor-harness.test.ts +7d32dccb70c6b1b79379ee69864d2c092d3a2e39863ab365494622a0effeba62 apps/web/tests-unit/review-media-sql-successor.test.ts diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4d4ecf60-historical b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4d4ecf60-historical new file mode 100644 index 0000000000..2fbd3f4e24 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4d4ecf60-historical @@ -0,0 +1,16 @@ +8297b18793513956118c720e3aa5bd481d0769645c263f680a5272825b70569e .github/review-media-sql-successor.v1.json +2080947cbc3c92934c98cee259d4f41e9c1eebc80e437f20862849fd36bd4f40 apps/web/scripts/review-media-sql-successor.mjs +c893c294719776bf5b064c3355866ab97e0fa9412082d9b47d7de5266094c65f apps/web/scripts/review-media-sql-rehearsal.mjs +fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs +7ab17835c727c22bad4138b86f4d3695d55bd1b4511141d1d374b2ca4694d080 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +0489bf7de7252e652c9c4967be77664cdeacb8afc07dcf846d73f0cf7c25dbe9 apps/web/performance/review-db-rehearsal-20261012/README.md +6c474d8e78c0eba3f15c077dbdd8634f073524c3b5f842213372bf0f1fed84fc apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical +b279a6b900dd3be10b9084a6362d169e87435bae955b2a1419858709645a6883 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json +7f74c175be3eee72b5e93eb75dac07bfefc0ef88d7d9957c26d877ee125ff590 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json +cb766e279baad0315e9d7055bc326b41b78a7d50a4972dac3214bc071aee0e49 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json +c53a467eaf9b929647bc1d66248308ba1ff73eba89fc9221dbf7db4711eb8562 apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json +8088aa57f39045e2bf93cba084711119395098d053318b70aaa676b5c13fea26 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs +099e0b728cbfde7f1b292ec5c90e1ab14af54bdf9e87d07ef44ead9bb885883e apps/web/tests-unit/review-media-sql-successor.test.ts +dc1f2594e629d5af16541db93e2e51f9b98d8282dd7fbeb802dd240fa6f522c0 apps/web/tests-unit/review-media-migration-bundle.test.ts +c2c833a6fb2529477e02bbdc353a1e840d0c079d045a81dbc8a0e905ef16a601 apps/web/tests-unit/review-media-sql-successor-harness.test.ts +b25b5dcc6516435a1c74a038c20856cc5dd719b303d9c3f3e600dc314197c7cd apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 new file mode 100644 index 0000000000..626730f211 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 @@ -0,0 +1 @@ +ea529addcf42cdb766f622786ccfffb13fe337f70faae6ade6da83736be58f57 diff --git a/apps/web/performance/review-db-rehearsal-20261012/interrupted-attempt-20261013.json b/apps/web/performance/review-db-rehearsal-20261012/interrupted-attempt-20261013.json new file mode 100644 index 0000000000..c3c408a521 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/interrupted-attempt-20261013.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": 1, + "kind": "interrupted-local-rehearsal", + "recordedAtUTC": "2026-10-13T06:50:57.884Z", + "processAbsent": true, + "proofPresent": false, + "containerId": "b856aa37d9d1d3963b4b6a8a01591e80bff4cb98de4e89681d206d76fed643f3", + "containerName": "tzudong-review-rollback-rehearsal-59656-300e3940feb7", + "ownershipVerified": true, + "removed": true, + "mutationOutcome": "not-established", + "sourceContainerRemoved": false +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/pg17-terminal-boundaries-20261013.json b/apps/web/performance/review-db-rehearsal-20261012/pg17-terminal-boundaries-20261013.json new file mode 100644 index 0000000000..d09caf27bd --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/pg17-terminal-boundaries-20261013.json @@ -0,0 +1,65 @@ +{ + "schemaVersion": 1, + "status": "passed", + "environment": { + "engine": "17.6", + "image": "supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab", + "emptyRowChecks": [ + "public.profiles", + "public.reviews" + ] + }, + "manifestSha256": "ae764bb760102a6d7d066be669c85248feed1823eafded4bf8c246a062090a8c", + "manifestToolchainRoot": "a0eeeb623c80b8a5e416b84397bdb07e4bd65c670d98b70707cc816d37e9a18b", + "rehearsalHarnessSha256": "8088aa57f39045e2bf93cba084711119395098d053318b70aaa676b5c13fea26", + "sourceRoot": "61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544", + "fixtureDumpSha256": "b7777306a43cc84408434e9835973604b2eb520cf9e9e8d0ee4bba6dcb41257f", + "predecessorSources": [ + { + "path": "backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql", + "sha256": "b373b7ea472c0352a33a4d4043cf8d6aa8474c04cc1ca5778805edfa77ac9c95" + }, + { + "path": "backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql", + "sha256": "66eace1d6fb0dd55c1d780776bab855cc37690335ad40b0fdc1294c610e07d3a" + }, + { + "path": "backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql", + "sha256": "e1c105df82c4f814d3e6adad807ff9d072b8cd42ae770b4b78f75b89a9387020" + }, + { + "path": "backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql", + "sha256": "8acf6d1428764260ed57dac5fe09711868a82896f0a6d631d502128004c168a3" + }, + { + "path": "backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql", + "sha256": "b596b200e52c6813a4cfa1b0a2818625f067864549e8854f3497afdcdab706da" + }, + { + "path": "backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql", + "sha256": "2067538f89c9f90d28e784672c7a1288306ba22d5ae92b087c8692503da9b1ae" + } + ], + "migrationCount": 11, + "statementCount": 112, + "baselineStateSha256": "3802888324e1651ad7b2f71e7cf15fac78ab77aa57637d0c60431f8da67e6298", + "stageStatesSha256": "bd25ecbd82759ca75e994ca0f1da38b49acc848d5dd556e702e4e43024edaa65", + "terminalStateSha256": "9cf59a5d983e78f6f7d9a594267c6feda3276611bf3539289b8a3de3295f700a", + "compiledSqlBytes": 2510560, + "compiledSqlSha256": "285d0c859117006646389dbd2a22de9aef1c0b97bc796a9e4d4448a8a232b093", + "results": { + "rollbackStageCount": 12, + "rollbackExact": true, + "forcedErrorRejected": true, + "forcedErrorRollbackExact": true, + "lostResponseMutationAttempts": 1, + "lostResponseRestorationExact": true, + "contendedExpiryRejected": true, + "contendedExpiryRollbackExact": true + }, + "hostedWrites": false, + "operatingRowsRead": false, + "rawSqlInProof": false, + "sourceMutation": false, + "temporaryContainerRemoved": true +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/terminal-boundary-unit-verification-20261013.json b/apps/web/performance/review-db-rehearsal-20261012/terminal-boundary-unit-verification-20261013.json new file mode 100644 index 0000000000..5d077cf1eb --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/terminal-boundary-unit-verification-20261013.json @@ -0,0 +1,67 @@ +{ + "schemaVersion": 1, + "kind": "local-branch-verification", + "recordedAtUTC": "2026-10-13T07:10:46.250689+00:00", + "environment": { + "node": "24.21.0", + "bun": "1.4.0", + "platform": "macOS" + }, + "tests": { + "passed": 81, + "failed": 0, + "assertions": 450, + "files": 5 + }, + "sources": [ + { + "path": "apps/web/scripts/review-media-sql-successor.mjs", + "sha256": "dcf552697251c2f10d859c7a2f56937a0f6e92ee9467e5b919807e498eff1d49" + }, + { + "path": "apps/web/scripts/review-media-operating-admission.mjs", + "sha256": "4ad55ed526b7af8c0ee1282057dbfa3706249deea04472a2749909af45492064" + }, + { + "path": "apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs", + "sha256": "8c3d7df52c281a9df1d6a4bd2acefb59f9a23b60185559fc6a8298cd65fde370" + }, + { + "path": ".github/review-media-sql-successor.v1.json", + "sha256": "ae764bb760102a6d7d066be669c85248feed1823eafded4bf8c246a062090a8c" + }, + { + "path": "apps/web/tests-unit/review-media-sql-successor.test.ts", + "sha256": "7d32dccb70c6b1b79379ee69864d2c092d3a2e39863ab365494622a0effeba62" + }, + { + "path": "apps/web/tests-unit/review-media-migration-bundle.test.ts", + "sha256": "dc1f2594e629d5af16541db93e2e51f9b98d8282dd7fbeb802dd240fa6f522c0" + }, + { + "path": "apps/web/tests-unit/review-media-sql-successor-harness.test.ts", + "sha256": "31bd78568b7bff5196626d87cf7f0fe9b84d695da8cef597fa93e4d742f99845" + }, + { + "path": "apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts", + "sha256": "b25b5dcc6516435a1c74a038c20856cc5dd719b303d9c3f3e600dc314197c7cd" + }, + { + "path": "apps/web/tests-unit/review-media-operating-admission.test.ts", + "sha256": "26d7fe68effda2813b3f9384dd2d7e4f9e8998447cd988b89820d93be7f6bdc7" + } + ], + "scope": [ + "rehearsal filesystem expiry", + "committed outcome plus terminal audit failure", + "private admission formatting", + "database/role creation and cleanup acknowledgement loss", + "rollback verifier ownership" + ], + "performanceClaim": false, + "hostedClaim": false, + "confidenceInterval": { + "applicable": false, + "reason": "Deterministic branch checks; not a reliability or performance sample." + } +} diff --git a/apps/web/performance/review-db-successor-20261012/README.md b/apps/web/performance/review-db-successor-20261012/README.md index fb7c8bdf65..9b3e71102e 100644 --- a/apps/web/performance/review-db-successor-20261012/README.md +++ b/apps/web/performance/review-db-successor-20261012/README.md @@ -3,10 +3,11 @@ This directory contains local-only evidence for the held 11-stage review-media successor. It does not authorize or prove a hosted database write. -The harness clones the owned, schema-only `ranking_clone` template into the -fixed database `review_successor_local_20261012_v1`, refuses an existing target, -and drops that database before writing the proof. The template contains zero -profiles and zero reviews. Six already-applied predecessor sources reconstruct +The retained pre-hardening replay cloned the schema-only `ranking_clone` +template into a fixed database and checked zero profiles and zero reviews. +Current code uses a process-specific database name with a 128-bit random suffix. +It preserves a pre-create absence baseline and verifies exact owner/template +metadata before cleanup, including lost CREATE acknowledgement and first readback. Six already-applied predecessor sources reconstruct the relevant pre-state; 86 synthetic ledger rows model the observed predecessor cardinality without claiming hosted parity. This harness does not read or prove the hosted ledger. A hosted attempt must freshly confirm that the actual ledger @@ -20,7 +21,7 @@ Run with Node 24: apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs ``` -The checked proof records 11 migrations, 112 source statements, exact stage +The retained historical proof records 11 migrations, 112 source statements, exact stage roots, ledger transition 86 to 97, atomic success, forced-failure rollback, wrong `finish_review_media_cleanup` preimage rejection, post-lock admission expiry rejection under real advisory-lock contention, and independent catalog @@ -41,3 +42,28 @@ The production manifest remains `held`. Hosted execution still requires a protected-main source receipt, fresh 12-state rehearsal receipt, current Vercel rollback readback, all seven writer-fence receipts, a private admission file, and a new journal path. + +## Current ownership verification + +`verify-fixture-ownership.mjs` creates a separate labelled PostgreSQL 17.6 +container and exercises six real ownership scenarios: normal role/database +creation, lost creation acknowledgement, lost first readback, and lost role +deletion acknowledgement. Each scenario attempts CREATE once and confirms +cleanup. Database predecessor/fixture application is deliberately excluded from +this ownership check; the separate rollback verifier covers SQL execution. + +Use `--output /absolute/new-proof.json` to reproduce without overwriting any +retained evidence. The verifier refuses existing output before creating a +container and removes only its exact per-run labelled container. + +The first startup attempt used Unix socket readiness too early. Another attempt +expected the injected error instead of the harness bounded error. Their failure +records and the interpretation are retained; neither establishes a production +failure or performance result. Official role privilege semantics are described +in [PostgreSQL 17 role attributes](https://www.postgresql.org/docs/17/role-attributes.html). + +Current ownership proof: `pg17-fixture-ownership-current-20261013.json` +(SHA-256 `0e7f96493ba9ad63959fa25c775fb6e95d0616d47fa4bdae973e248f5eab0349`). +The earlier `pg17-fixture-ownership-20261013.json` is retained for the verifier +before optional exclusive output support. Both establish six isolated scenarios; +the current proof matches the current helper and verifier bytes. diff --git a/apps/web/performance/review-db-successor-20261012/SHA256SUMS b/apps/web/performance/review-db-successor-20261012/SHA256SUMS index 1e6628b406..a33aadc18b 100644 --- a/apps/web/performance/review-db-successor-20261012/SHA256SUMS +++ b/apps/web/performance/review-db-successor-20261012/SHA256SUMS @@ -1,6 +1,13 @@ -9fac81d6e3ec81207b9c8985ccfdaf9e300bb7d44a7617d95579d4e8f96af19b .github/review-media-sql-successor.v1.json -928d7b8bd601ab585870d4e04ee468e952f6053df81e403de925e032924f40d1 apps/web/scripts/review-media-sql-successor.mjs -a374211be376e3a9af8215751accb52eb43943fc93b8651ba4e186e412588ba7 apps/web/scripts/supabase-migration-bundle.mjs -03e999caeda7b57925126ba8d6bcec4ac5a671fedb2c69e07e988924ccb9f895 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +ae764bb760102a6d7d066be669c85248feed1823eafded4bf8c246a062090a8c .github/review-media-sql-successor.v1.json +0fddfa7996df84c1b2e7463b2be696ffff442d97fd24abb476c2f701a9d3d946 apps/web/performance/review-db-successor-20261012/README.md +b9f3fca7e506e195312c2ca3e4da97f04c7767e42b2a0f980aab68dfc6395e09 apps/web/performance/review-db-successor-20261012/SHA256SUMS-4d4ecf60-historical +dc27a75b296716c612e9e3b9cb35f0fd113129dad5215f72a76f3ea94747ee95 apps/web/performance/review-db-successor-20261012/fixture-ownership-failure-interpretation-20261013.json +d9cdeba56f0c38a0c1c46f50b48645d827b47df2ba09f3101ff01dcc828cf118 apps/web/performance/review-db-successor-20261012/fixture-ownership-first-attempt-20261013.json +0a3b893331ff16732dd79c6bfca9f88689f7be3065777e62b0dcf3528cea05df apps/web/performance/review-db-successor-20261012/fixture-ownership-protocol-failure-20261013.json +ef0af48a51ee8e3b9a57725eb7504c88401d2b59c3470dec0da11b5257187fbc apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-20261013.json +0e7f96493ba9ad63959fa25c775fb6e95d0616d47fa4bdae973e248f5eab0349 apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-current-20261013.json 6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f apps/web/performance/review-db-successor-20261012/pg17-local-replay.json -05176617c49dbf5343b0b9d0587dbf41ffa32e6f9497321517474fbb347c373e apps/web/performance/review-db-successor-20261012/README.md +7f6ded6fa4078fb0239b1c4d8cefe87dd8407c174e747ef118c2c98a1b481d9e apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs +8c3d7df52c281a9df1d6a4bd2acefb59f9a23b60185559fc6a8298cd65fde370 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +dcf552697251c2f10d859c7a2f56937a0f6e92ee9467e5b919807e498eff1d49 apps/web/scripts/review-media-sql-successor.mjs +fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs diff --git a/apps/web/performance/review-db-successor-20261012/SHA256SUMS-4d4ecf60-historical b/apps/web/performance/review-db-successor-20261012/SHA256SUMS-4d4ecf60-historical new file mode 100644 index 0000000000..1e6628b406 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/SHA256SUMS-4d4ecf60-historical @@ -0,0 +1,6 @@ +9fac81d6e3ec81207b9c8985ccfdaf9e300bb7d44a7617d95579d4e8f96af19b .github/review-media-sql-successor.v1.json +928d7b8bd601ab585870d4e04ee468e952f6053df81e403de925e032924f40d1 apps/web/scripts/review-media-sql-successor.mjs +a374211be376e3a9af8215751accb52eb43943fc93b8651ba4e186e412588ba7 apps/web/scripts/supabase-migration-bundle.mjs +03e999caeda7b57925126ba8d6bcec4ac5a671fedb2c69e07e988924ccb9f895 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f apps/web/performance/review-db-successor-20261012/pg17-local-replay.json +05176617c49dbf5343b0b9d0587dbf41ffa32e6f9497321517474fbb347c373e apps/web/performance/review-db-successor-20261012/README.md diff --git a/apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 b/apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 new file mode 100644 index 0000000000..17a8b03e78 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 @@ -0,0 +1 @@ +1a3aa2b9a3e1faef9916cce3d7d3b11246683dadce15da97b3a0e3a75fbb9048 diff --git a/apps/web/performance/review-db-successor-20261012/fixture-ownership-failure-interpretation-20261013.json b/apps/web/performance/review-db-successor-20261012/fixture-ownership-failure-interpretation-20261013.json new file mode 100644 index 0000000000..68b48e3921 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/fixture-ownership-failure-interpretation-20261013.json @@ -0,0 +1,7 @@ +{ + "schemaVersion": 1, + "retainedFailureSha256": "0a3b893331ff16732dd79c6bfca9f88689f7be3065777e62b0dcf3528cea05df", + "classification": "fault-injection expectation mismatch", + "correction": "The harness intentionally wraps the injected first-readback loss as LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_FAILED. The verifier must match that bounded result, not the injected implementation error.", + "productionClaim": false +} diff --git a/apps/web/performance/review-db-successor-20261012/fixture-ownership-first-attempt-20261013.json b/apps/web/performance/review-db-successor-20261012/fixture-ownership-first-attempt-20261013.json new file mode 100644 index 0000000000..401de6291b --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/fixture-ownership-first-attempt-20261013.json @@ -0,0 +1,14 @@ +{ + "schemaVersion": 1, + "kind": "failed-local-fixture-verification", + "status": { + "state": "completed", + "runnerPid": 35642, + "exitCode": 1, + "finishedAtUTC": "2026-10-13T07:06:00.847303+00:00" + }, + "failureCode": "FIXTURE_OWNERSHIP_COMMAND_FAILED", + "phase": "server-version query after Unix socket readiness", + "roleAndDatabaseScenariosEstablished": false, + "correction": "Wait for final TCP server, matching the existing rollback verifier." +} diff --git a/apps/web/performance/review-db-successor-20261012/fixture-ownership-protocol-failure-20261013.json b/apps/web/performance/review-db-successor-20261012/fixture-ownership-protocol-failure-20261013.json new file mode 100644 index 0000000000..6aa7b9c818 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/fixture-ownership-protocol-failure-20261013.json @@ -0,0 +1,36 @@ +{ + "schemaVersion": 1, + "status": "failed", + "code": "LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_FAILED", + "observed": { + "actorSuperuser": true, + "roles": [ + { + "name": "review_successor_fixture_parent_20261012", + "inherit": false, + "connlimit": -1, + "validuntil": null, + "config": null, + "markerPresent": true + }, + { + "name": "review_successor_fixture_member_20261012", + "inherit": true, + "connlimit": -1, + "validuntil": null, + "config": null, + "markerPresent": true + } + ], + "memberships": [ + { + "parent": "review_successor_fixture_parent_20261012", + "member": "review_successor_fixture_member_20261012", + "grantor": "supabase_admin", + "admin": false, + "inherit": true, + "set": true + } + ] + } +} diff --git a/apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-20261013.json b/apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-20261013.json new file mode 100644 index 0000000000..6b3f05be4c --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-20261013.json @@ -0,0 +1,60 @@ +{ + "schemaVersion": 1, + "kind": "isolated-fixture-ownership-verification", + "status": "passed", + "recordedAtUTC": "2026-10-13T07:18:48.134Z", + "sourceSha256": "8c3d7df52c281a9df1d6a4bd2acefb59f9a23b60185559fc6a8298cd65fde370", + "verifierSha256": "14af945a8e2d5d64a33bc6917da06fc6325a8c13af5e948d03c91e7af319d870", + "environment": { + "engine": "17.6", + "image": "supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab" + }, + "results": [ + { + "kind": "roles", + "mode": "normal", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "roles", + "mode": "lost-create-and-drop-ack", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "roles", + "mode": "lost-create-ack-and-first-readback", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "database", + "mode": "normal", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "database", + "mode": "lost-create-ack", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "database", + "mode": "lost-create-ack-and-first-readback", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + } + ], + "temporaryContainerRemoved": true, + "sourceContainerMutation": false, + "hostedWrites": false, + "predecessorAndFixtureSqlExercised": false +} diff --git a/apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-current-20261013.json b/apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-current-20261013.json new file mode 100644 index 0000000000..0d0a7759c0 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-current-20261013.json @@ -0,0 +1,60 @@ +{ + "schemaVersion": 1, + "kind": "isolated-fixture-ownership-verification", + "status": "passed", + "recordedAtUTC": "2026-10-13T07:39:57.597Z", + "sourceSha256": "8c3d7df52c281a9df1d6a4bd2acefb59f9a23b60185559fc6a8298cd65fde370", + "verifierSha256": "7f6ded6fa4078fb0239b1c4d8cefe87dd8407c174e747ef118c2c98a1b481d9e", + "environment": { + "engine": "17.6", + "image": "supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab" + }, + "results": [ + { + "kind": "roles", + "mode": "normal", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "roles", + "mode": "lost-create-and-drop-ack", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "roles", + "mode": "lost-create-ack-and-first-readback", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "database", + "mode": "normal", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "database", + "mode": "lost-create-ack", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "database", + "mode": "lost-create-ack-and-first-readback", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + } + ], + "temporaryContainerRemoved": true, + "sourceContainerMutation": false, + "hostedWrites": false, + "predecessorAndFixtureSqlExercised": false +} diff --git a/apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs b/apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs new file mode 100644 index 0000000000..b167ceeefd --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs @@ -0,0 +1,157 @@ +#!/usr/bin/env node +/** Exercise fixture ownership on a new, labelled PostgreSQL 17.6 container. */ +import { spawnSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import { readFileSync, existsSync, writeFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createOwnedContainer, removeOwnedContainer } from '../review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs'; +import { createFixtureMembership, dropFixtureMembership, createOwnedDatabase, dropOwnedDatabase } from './verify-review-media-sql-successor.mjs'; + +const here = dirname(fileURLToPath(import.meta.url)); +const root = resolve(here, '../../../..'); +const args = process.argv.slice(2); +if (args.length !== 0 && (args.length !== 2 || args[0] !== '--output' + || !args[1] || args[1].startsWith('--'))) { + throw Object.assign(new Error('FIXTURE_OWNERSHIP_ARGUMENT_INVALID'), { code: 'FIXTURE_OWNERSHIP_ARGUMENT_INVALID' }); +} +const output = args.length === 0 + ? resolve(here, 'pg17-fixture-ownership-20261013.json') : resolve(args[1]); +const context = process.env.TZUDONG_REHEARSAL_DOCKER_CONTEXT || 'colima-tzudong-catalog-20261007'; +const hash = bytes => createHash('sha256').update(bytes).digest('hex'); +const source = resolve(here, 'verify-review-media-sql-successor.mjs'); +const sourceHash = hash(readFileSync(source)); +let password; +const fail = code => { throw Object.assign(new Error(code), { code }); }; +const check = value => { if (!value) fail('FIXTURE_OWNERSHIP_ASSERTION_FAILED'); }; + +function docker(args, options = {}) { + if (args[0] === 'run') password = args.find(value => value.startsWith('POSTGRES_PASSWORD='))?.slice(18); + const result = spawnSync('docker', ['--context', context, ...args], { + input: options.input, encoding: 'utf8', maxBuffer: 4 * 1024 * 1024, + }); + if (!options.allowFailure && (result.error || result.status !== 0)) fail('FIXTURE_OWNERSHIP_COMMAND_FAILED'); + return result; +} + +const container = { created: false, id: null, name: null, token: null }; +function sql(database, statement, actor = 'supabase_admin') { + return docker(['exec', '-i', '-e', `PGPASSWORD=${password}`, container.id, + 'psql', '-XAtq', '-v', 'ON_ERROR_STOP=1', '-h', '127.0.0.1', '-U', actor, '-d', database, '--file=-'], + { input: String(statement) }).stdout.trim(); +} + +const results = []; +try { + if (existsSync(output)) fail('FIXTURE_OWNERSHIP_OUTPUT_EXISTS'); + createOwnedContainer(container, { dockerImpl: docker }); + let ready = false; + for (let attempt = 0; attempt < 60; attempt += 1) { + const result = docker(['exec', '-e', `PGPASSWORD=${password}`, container.id, + 'pg_isready', '-h', '127.0.0.1', '-U', 'postgres', '-d', 'postgres'], { allowFailure: true }); + if (result.status === 0) { ready = true; break; } + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250); + } + check(ready && sql('postgres', 'SHOW server_version;').startsWith('17.6')); + + for (const mode of ['normal', 'lost-create-and-drop-ack', 'lost-create-ack-and-first-readback']) { + let created = false; + let lostReadback = false; + let createAttempts = 0; + let dropAttempts = 0; + const ownership = { database: false, fixtureMembership: false, fixtureMembershipAttempted: false, + fixtureMembershipToken: null, fixtureMembershipBaseline: null }; + const dependencies = { + scalarImpl: (database, statement, actor) => { + if (mode.endsWith('first-readback') && created && !lostReadback) { + lostReadback = true; fail('FIXTURE_FAULT_READBACK_LOSS'); + } + return sql(database, statement, actor); + }, + psqlImpl: (database, statement, options) => { + const text = String(statement); + sql(database, text, options.actor); + if (text.includes('CREATE ROLE')) { + created = true; createAttempts += 1; + if (mode !== 'normal') fail('FIXTURE_FAULT_CREATE_ACK_LOSS'); + } else if (text.includes('DROP ROLE')) { + dropAttempts += 1; + if (mode.includes('drop-ack')) fail('FIXTURE_FAULT_DROP_ACK_LOSS'); + } + }, + }; + try { createFixtureMembership(ownership, dependencies); } + catch (error) { + if (!(mode.endsWith('first-readback') && lostReadback + && error.message === 'LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_FAILED')) { + const code = /^[A-Z][A-Z0-9_]{3,96}/.exec(String(error.message))?.[0] || 'UNKNOWN'; + const observed = JSON.parse(sql('postgres', `SELECT json_build_object( + 'actorSuperuser',(SELECT rolsuper FROM pg_roles WHERE rolname='supabase_admin'), + 'roles',(SELECT json_agg(json_build_object('name',rolname,'inherit',rolinherit, + 'connlimit',rolconnlimit,'validuntil',rolvaliduntil,'config',rolconfig, + 'markerPresent',shobj_description(oid,'pg_authid') IS NOT NULL)) FROM pg_roles + WHERE rolname IN('review_successor_fixture_parent_20261012','review_successor_fixture_member_20261012')), + 'memberships',(SELECT json_agg(json_build_object('parent',p.rolname,'member',m.rolname, + 'grantor',g.rolname,'admin',a.admin_option,'inherit',a.inherit_option,'set',a.set_option)) + FROM pg_auth_members a JOIN pg_roles p ON p.oid=a.roleid + JOIN pg_roles m ON m.oid=a.member JOIN pg_roles g ON g.oid=a.grantor + WHERE p.rolname IN('review_successor_fixture_parent_20261012','review_successor_fixture_member_20261012')) + )::text;`)); + writeFileSync(`${output}.failure.json`, + `${JSON.stringify({ schemaVersion: 1, status: 'failed', code, observed }, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); + fail('FIXTURE_OWNERSHIP_ROLE_PROTOCOL_FAILED'); + } + } + check(dropFixtureMembership(ownership, dependencies)); + check(!ownership.fixtureMembership && !ownership.fixtureMembershipAttempted && createAttempts === 1 && dropAttempts === 1); + results.push({ kind: 'roles', mode, createAttempts, dropAttempts, cleaned: true }); + } + + sql('postgres', 'CREATE DATABASE ranking_clone OWNER postgres TEMPLATE template0;'); + for (const mode of ['normal', 'lost-create-ack', 'lost-create-ack-and-first-readback']) { + let created = false; + let lostReadback = false; + let createAttempts = 0; + let dropAttempts = 0; + const ownership = { database: false }; + const dependencies = { + readFileImpl: () => Buffer.from('-- ownership-only fixture: schema application excluded'), + scalarImpl: (database, statement, actor) => { + if (mode.endsWith('first-readback') && created && !lostReadback) { + lostReadback = true; fail('FIXTURE_FAULT_READBACK_LOSS'); + } + return sql(database, statement, actor); + }, + psqlImpl: (database, statement, options = {}) => { + // The ownership protocol is exercised against PG; predecessor/fixture SQL is outside this check. + if (database !== 'postgres') return {}; + const text = String(statement); + sql(database, text, options.actor); + if (/CREATE DATABASE/.test(text)) { + created = true; createAttempts += 1; + if (mode !== 'normal') fail('FIXTURE_FAULT_CREATE_ACK_LOSS'); + } else if (/DROP DATABASE/.test(text)) dropAttempts += 1; + return {}; + }, + }; + try { createOwnedDatabase(ownership, dependencies); } + catch (error) { check(mode.endsWith('first-readback') && created && lostReadback + && error.message === 'LOCAL_REHEARSAL_DATABASE_READBACK_FAILED'); } + dropOwnedDatabase(ownership, dependencies); + check(!ownership.database && !ownership.databaseAttempted && createAttempts === 1 && dropAttempts === 1); + results.push({ kind: 'database', mode, createAttempts, dropAttempts, cleaned: true }); + } + check(hash(readFileSync(source)) === sourceHash); +} finally { + removeOwnedContainer(container, { dockerImpl: docker }); +} + +check(container.removed === true); +const proof = { schemaVersion: 1, kind: 'isolated-fixture-ownership-verification', status: 'passed', + recordedAtUTC: new Date().toISOString(), sourceSha256: sourceHash, + verifierSha256: hash(readFileSync(fileURLToPath(import.meta.url))), + environment: { engine: '17.6', image: 'supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab' }, + results, temporaryContainerRemoved: true, sourceContainerMutation: false, hostedWrites: false, + predecessorAndFixtureSqlExercised: false }; +writeFileSync(output, `${JSON.stringify(proof, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); +console.log(JSON.stringify({ status: proof.status, scenarios: results.length, output: output.slice(root.length + 1) })); diff --git a/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs index e1dafc077f..658fec8642 100644 --- a/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +++ b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs @@ -4,10 +4,11 @@ * Local-only PostgreSQL 17.6 rehearsal for the held eleven-stage successor. * * The harness clones the existing schema-only ranking_clone template into one - * fixed, owned database name. It refuses a reused target, never connects to a - * hosted endpoint, and drops the owned database before emitting its current-source result. + * per-process, owned database name. It refuses a reused target, never connects + * to a hosted endpoint, and drops the owned database before emitting its + * current-source result. */ -import { createHash } from 'node:crypto'; +import { createHash, randomBytes } from 'node:crypto'; import { existsSync, readFileSync } from 'node:fs'; import { dirname, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -27,10 +28,11 @@ const RETAINED_PROOF_REPOSITORY_PATH = 'apps/web/performance/review-db-successor const DOCKER_CONTEXT = process.env.TZUDONG_REHEARSAL_DOCKER_CONTEXT || 'colima-tzudong-catalog-20261007'; const DOCKER_CONTAINER = process.env.TZUDONG_REHEARSAL_DOCKER_CONTAINER || 'tzudong-ranking-clone-20261008'; const TEMPLATE_DATABASE = 'ranking_clone'; -const DATABASE = 'review_successor_local_20261012_v1'; +const DATABASE = `review_successor_local_${randomBytes(16).toString('hex')}`; const FIXTURE_PASSWORD = 'fixture-only'; const FIXTURE_PARENT_ROLE = 'review_successor_fixture_parent_20261012'; const FIXTURE_MEMBER_ROLE = 'review_successor_fixture_member_20261012'; +const FIXTURE_OWNER_PREFIX = 'tzudong-review-successor-fixture'; const EXPIRY_APPLICATION_NAME = 'tzudong-review-successor-expiry-fixture'; const PREDECESSOR_PATHS = Object.freeze([ 'backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql', @@ -126,16 +128,115 @@ ON CONFLICT(singleton) DO NOTHING; `; } +function databaseMetadataSql(databaseName) { + const projection = alias => `jsonb_build_object( + 'name',${alias}.datname, + 'owner',pg_get_userbyid(${alias}.datdba), + 'encoding',pg_encoding_to_char(${alias}.encoding), + 'collate',${alias}.datcollate, + 'ctype',${alias}.datctype, + 'localeProvider',${alias}.datlocprovider::text, + 'allowConnections',${alias}.datallowconn, + 'isTemplate',${alias}.datistemplate + )`; + return `SELECT jsonb_build_object( + 'target',(SELECT ${projection('target_database')} FROM pg_database target_database + WHERE target_database.datname=${quote(databaseName)}), + 'template',(SELECT ${projection('template_database')} FROM pg_database template_database + WHERE template_database.datname=${quote(TEMPLATE_DATABASE)}) +)::text;`; +} + +function inspectDatabaseOwnership(databaseName, { scalarImpl = scalar } = {}) { + let state; + try { + state = JSON.parse(scalarImpl('postgres', databaseMetadataSql(databaseName), 'supabase_admin')); + } catch { + fail('LOCAL_REHEARSAL_DATABASE_STATE_INVALID'); + } + if (!state + || typeof state !== 'object' + || !Object.hasOwn(state, 'target') + || !databaseMetadataValid(state.template) + || state.template.name !== TEMPLATE_DATABASE + || (state.target !== null && !databaseMetadataValid(state.target))) { + fail('LOCAL_REHEARSAL_DATABASE_STATE_INVALID'); + } + return state; +} + +function databaseMetadataValid(metadata) { + return metadata + && typeof metadata === 'object' + && ['name', 'owner', 'encoding', 'collate', 'ctype', 'localeProvider'] + .every(field => typeof metadata[field] === 'string' && metadata[field].length > 0) + && typeof metadata.allowConnections === 'boolean' + && typeof metadata.isTemplate === 'boolean'; +} + +function databaseTargetAbsent(state, databaseName) { + return state?.target === null && state?.template?.name === TEMPLATE_DATABASE && databaseName === DATABASE; +} + +function databaseTargetOwned(state, baseline, databaseName) { + const target = state?.target; + const template = state?.template; + return databaseTargetAbsent(baseline, databaseName) + && target?.name === databaseName + && target.owner === 'postgres' + && target.allowConnections === true + && target.isTemplate === false + && template?.name === TEMPLATE_DATABASE + && equal(template, baseline.template) + && target.encoding === template.encoding + && target.collate === template.collate + && target.ctype === template.ctype + && target.localeProvider === template.localeProvider; +} + export function createOwnedDatabase(ownership, { scalarImpl = scalar, psqlImpl = psql, readFileImpl = readFileSync, } = {}) { - if (!ownership || ownership.database !== false) fail('LOCAL_REHEARSAL_OWNERSHIP_STATE_INVALID'); - const exists = scalarImpl('postgres', `SELECT count(*) FROM pg_database WHERE datname=${quote(DATABASE)};`, 'supabase_admin'); - if (exists !== '0') fail('LOCAL_REHEARSAL_TARGET_ALREADY_EXISTS'); - psqlImpl('postgres', `CREATE DATABASE ${DATABASE} TEMPLATE ${TEMPLATE_DATABASE} OWNER postgres;`, { actor: 'supabase_admin' }); + if (!ownership + || ownership.database !== false + || ownership.databaseAttempted + || ownership.databaseName + || ownership.databaseBaseline) { + fail('LOCAL_REHEARSAL_OWNERSHIP_STATE_INVALID'); + } + const baseline = inspectDatabaseOwnership(DATABASE, { scalarImpl }); + if (!databaseTargetAbsent(baseline, DATABASE)) fail('LOCAL_REHEARSAL_TARGET_ALREADY_EXISTS'); + ownership.databaseName = DATABASE; + ownership.databaseBaseline = baseline; + ownership.databaseAttempted = true; + let creationAcknowledged = false; + try { + psqlImpl('postgres', `CREATE DATABASE ${DATABASE} TEMPLATE ${TEMPLATE_DATABASE} OWNER postgres;`, { actor: 'supabase_admin' }); + creationAcknowledged = true; + } catch { + // CREATE is intentionally never replayed. The exact catalog readback below + // distinguishes a committed CREATE from a failure before mutation. + } + let observed; + try { + observed = inspectDatabaseOwnership(DATABASE, { scalarImpl }); + } catch { + fail('LOCAL_REHEARSAL_DATABASE_READBACK_FAILED'); + } + if (!databaseTargetOwned(observed, baseline, DATABASE)) { + if (databaseTargetAbsent(observed, DATABASE)) { + ownership.databaseAttempted = false; + ownership.databaseName = null; + ownership.databaseBaseline = null; + fail('LOCAL_REHEARSAL_DATABASE_CREATION_UNCONFIRMED'); + } + fail('LOCAL_REHEARSAL_DATABASE_OWNERSHIP_UNPROVEN'); + } ownership.database = true; + ownership.databaseCreationAcknowledged = creationAcknowledged; + ownership.databaseCreationReconciled = !creationAcknowledged; PREDECESSOR_PATHS.forEach((path, index) => { try { psqlImpl(DATABASE, readFileImpl(resolve(REPOSITORY_ROOT, path))); @@ -144,18 +245,63 @@ export function createOwnedDatabase(ownership, { } }); psqlImpl(DATABASE, fixtureSql()); + return true; } export function dropOwnedDatabase(ownership, { scalarImpl = scalar, psqlImpl = psql } = {}) { - if (!ownership?.database) return false; - const exists = scalarImpl('postgres', `SELECT count(*) FROM pg_database WHERE datname=${quote(DATABASE)};`, 'supabase_admin'); - if (exists === '1') { + if (!ownership?.database && !ownership?.databaseAttempted) return false; + if (ownership.databaseName !== DATABASE + || !ownership.databaseBaseline + || !databaseTargetAbsent(ownership.databaseBaseline, DATABASE)) { + fail('LOCAL_REHEARSAL_DATABASE_OWNERSHIP_STATE_INVALID'); + } + let before; + try { + before = inspectDatabaseOwnership(DATABASE, { scalarImpl }); + } catch { + fail('LOCAL_REHEARSAL_DATABASE_CLEANUP_READBACK_FAILED'); + } + if (databaseTargetAbsent(before, DATABASE)) { + const wasOwned = ownership.database === true; + ownership.database = false; + ownership.databaseAttempted = false; + ownership.databaseDropped = wasOwned; + ownership.databaseCleanupReconciled = true; + ownership.databaseName = null; + ownership.databaseBaseline = null; + return false; + } + if (!databaseTargetOwned(before, ownership.databaseBaseline, DATABASE)) { + fail('LOCAL_REHEARSAL_DATABASE_OWNERSHIP_UNPROVEN'); + } + ownership.database = true; + let removalAcknowledged = false; + try { psqlImpl('postgres', `DROP DATABASE ${DATABASE} WITH (FORCE);`, { actor: 'supabase_admin' }); - } else if (exists !== '0') { - fail('LOCAL_REHEARSAL_TARGET_CARDINALITY_INVALID'); + removalAcknowledged = true; + } catch { + // A lost DROP acknowledgement is reconciled once; DROP is not replayed. + } + let after; + try { + after = inspectDatabaseOwnership(DATABASE, { scalarImpl }); + } catch { + fail('LOCAL_REHEARSAL_DATABASE_CLEANUP_READBACK_FAILED'); + } + if (!databaseTargetAbsent(after, DATABASE)) { + if (!databaseTargetOwned(after, ownership.databaseBaseline, DATABASE)) { + fail('LOCAL_REHEARSAL_DATABASE_OWNERSHIP_UNPROVEN'); + } + fail('LOCAL_REHEARSAL_DATABASE_DROP_UNCONFIRMED'); } ownership.database = false; - return exists === '1'; + ownership.databaseAttempted = false; + ownership.databaseDropped = true; + ownership.databaseRemovalAcknowledged = removalAcknowledged; + ownership.databaseCleanupReconciled = !removalAcknowledged; + ownership.databaseName = null; + ownership.databaseBaseline = null; + return true; } function resetOwnedDatabase(ownership) { @@ -163,30 +309,193 @@ function resetOwnedDatabase(ownership) { createOwnedDatabase(ownership); } -function createFixtureMembership() { - const existing = scalar('postgres', `SELECT count(*) FROM pg_roles WHERE rolname IN(${quote(FIXTURE_PARENT_ROLE)},${quote(FIXTURE_MEMBER_ROLE)});`, 'supabase_admin'); - if (existing !== '0') fail('LOCAL_REHEARSAL_FIXTURE_ROLE_EXISTS'); - psql('postgres', ` +function fixtureRoleStateQuery() { + return `WITH fixture_roles AS ( + SELECT fixture_role.rolname, + fixture_role.rolcanlogin, + fixture_role.rolsuper, + fixture_role.rolcreatedb, + fixture_role.rolcreaterole, + fixture_role.rolinherit, + fixture_role.rolreplication, + fixture_role.rolbypassrls, + fixture_role.rolconnlimit, + fixture_role.rolvaliduntil, + fixture_role.rolconfig, + coalesce(shobj_description(fixture_role.oid,'pg_authid'),'') AS owner_marker + FROM pg_roles fixture_role + WHERE fixture_role.rolname IN(${quote(FIXTURE_PARENT_ROLE)},${quote(FIXTURE_MEMBER_ROLE)}) +), fixture_memberships AS ( + SELECT parent.rolname AS parent, + member.rolname AS member, + grantor.rolname AS grantor, + membership.admin_option, + membership.inherit_option, + membership.set_option + FROM pg_auth_members membership + JOIN pg_roles parent ON parent.oid=membership.roleid + JOIN pg_roles member ON member.oid=membership.member + JOIN pg_roles grantor ON grantor.oid=membership.grantor + WHERE parent.rolname IN(${quote(FIXTURE_PARENT_ROLE)},${quote(FIXTURE_MEMBER_ROLE)}) + OR member.rolname IN(${quote(FIXTURE_PARENT_ROLE)},${quote(FIXTURE_MEMBER_ROLE)}) +) +SELECT jsonb_build_object( + 'roles',coalesce((SELECT jsonb_agg(to_jsonb(fixture_role) ORDER BY fixture_role.rolname) FROM fixture_roles fixture_role),'[]'::jsonb), + 'memberships',coalesce((SELECT jsonb_agg(to_jsonb(fixture_membership) ORDER BY fixture_membership.parent,fixture_membership.member,fixture_membership.grantor) FROM fixture_memberships fixture_membership),'[]'::jsonb) +)::text;`; +} + +function inspectFixtureMembership({ scalarImpl = scalar } = {}) { + let output; + try { + output = scalarImpl('postgres', fixtureRoleStateQuery(), 'supabase_admin'); + } catch { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_FAILED'); + } + let state; + try { + state = JSON.parse(output); + } catch { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_INVALID'); + } + if (!Array.isArray(state?.roles) || !Array.isArray(state?.memberships)) { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_INVALID'); + } + return Object.freeze({ + roles: Object.freeze(state.roles.map(role => Object.freeze({ ...role }))), + memberships: Object.freeze(state.memberships.map(membership => Object.freeze({ ...membership }))), + }); +} + +function fixtureMembershipAbsent(state) { + return state.roles.length === 0 && state.memberships.length === 0; +} + +function fixtureMembershipOwned(state, token) { + const roles = new Map(state.roles.map(role => [role.rolname, role])); + const parent = roles.get(FIXTURE_PARENT_ROLE); + const member = roles.get(FIXTURE_MEMBER_ROLE); + const membership = state.memberships[0]; + const sharedRoleShape = role => role + && role.rolcanlogin === false + && role.rolsuper === false + && role.rolcreatedb === false + && role.rolcreaterole === false + && role.rolreplication === false + && role.rolbypassrls === false + && role.rolconnlimit === -1 + && role.rolvaliduntil === null + && role.rolconfig === null; + return state.roles.length === 2 + && sharedRoleShape(parent) + && parent.rolinherit === false + && parent.owner_marker === `${FIXTURE_OWNER_PREFIX}:${token}:parent` + && sharedRoleShape(member) + && member.rolinherit === true + && member.owner_marker === `${FIXTURE_OWNER_PREFIX}:${token}:member` + && state.memberships.length === 1 + && membership.parent === FIXTURE_PARENT_ROLE + && membership.member === FIXTURE_MEMBER_ROLE + && membership.grantor === 'supabase_admin' + && membership.admin_option === false + && membership.inherit_option === true + && membership.set_option === true; +} + +export function createFixtureMembership(ownership, { + scalarImpl = scalar, + psqlImpl = psql, + generateToken = () => randomBytes(16).toString('hex'), +} = {}) { + if (!ownership + || ownership.fixtureMembership !== false + || ownership.fixtureMembershipAttempted + || ownership.fixtureMembershipToken + || ownership.fixtureMembershipBaseline) { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_STATE_INVALID'); + } + const baseline = inspectFixtureMembership({ scalarImpl }); + ownership.fixtureMembershipBaseline = baseline; + if (!fixtureMembershipAbsent(baseline)) fail('LOCAL_REHEARSAL_FIXTURE_ROLE_EXISTS'); + const token = generateToken(); + if (!/^[0-9a-f]{32}$/.test(token)) fail('LOCAL_REHEARSAL_FIXTURE_ROLE_TOKEN_INVALID'); + ownership.fixtureMembershipToken = token; + ownership.fixtureMembershipAttempted = true; + let creationAcknowledged = false; + try { + psqlImpl('postgres', ` BEGIN; CREATE ROLE ${FIXTURE_PARENT_ROLE} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS; CREATE ROLE ${FIXTURE_MEMBER_ROLE} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOREPLICATION NOBYPASSRLS; +COMMENT ON ROLE ${FIXTURE_PARENT_ROLE} IS ${quote(`${FIXTURE_OWNER_PREFIX}:${token}:parent`)}; +COMMENT ON ROLE ${FIXTURE_MEMBER_ROLE} IS ${quote(`${FIXTURE_OWNER_PREFIX}:${token}:member`)}; GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} WITH ADMIN FALSE, INHERIT TRUE, SET TRUE GRANTED BY supabase_admin; COMMIT; `, { actor: 'supabase_admin' }); + creationAcknowledged = true; + } catch {} + const observed = inspectFixtureMembership({ scalarImpl }); + if (!fixtureMembershipOwned(observed, token)) { + ownership.fixtureMembershipAttempted = false; + if (fixtureMembershipAbsent(observed)) fail('LOCAL_REHEARSAL_FIXTURE_ROLE_CREATION_UNCONFIRMED'); + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_UNPROVEN'); + } + ownership.fixtureMembership = true; + ownership.fixtureMembershipCreationAcknowledged = creationAcknowledged; + ownership.fixtureMembershipCreationReconciled = !creationAcknowledged; + return true; } -function dropFixtureMembership() { - const existing = scalar('postgres', `SELECT count(*) FROM pg_roles WHERE rolname IN(${quote(FIXTURE_PARENT_ROLE)},${quote(FIXTURE_MEMBER_ROLE)});`, 'supabase_admin'); - if (existing === '0') return; - if (existing !== '2') fail('LOCAL_REHEARSAL_FIXTURE_ROLE_CARDINALITY_INVALID'); - psql('postgres', ` +export function dropFixtureMembership(ownership, { + scalarImpl = scalar, + psqlImpl = psql, +} = {}) { + if (!ownership?.fixtureMembership && !ownership?.fixtureMembershipAttempted) return false; + const token = ownership.fixtureMembershipToken; + if (!/^[0-9a-f]{32}$/.test(token || '') + || !ownership.fixtureMembershipBaseline + || !fixtureMembershipAbsent(ownership.fixtureMembershipBaseline)) { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_STATE_INVALID'); + } + const before = inspectFixtureMembership({ scalarImpl }); + if (fixtureMembershipAbsent(before)) { + const wasOwned = ownership.fixtureMembership === true; + ownership.fixtureMembership = false; + ownership.fixtureMembershipAttempted = false; + ownership.fixtureMembershipDropped = wasOwned; + ownership.fixtureMembershipCleanupReconciled = true; + return wasOwned; + } + if (!fixtureMembershipOwned(before, token)) { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_UNPROVEN'); + } + // An earlier creation readback can fail after COMMIT. Exact token, shape, + // membership, and empty preimage establish this run's cleanup authority. + ownership.fixtureMembership = true; + let removalAcknowledged = false; + try { + psqlImpl('postgres', ` BEGIN; REVOKE ${FIXTURE_PARENT_ROLE} FROM ${FIXTURE_MEMBER_ROLE} GRANTED BY supabase_admin; DROP ROLE ${FIXTURE_MEMBER_ROLE}; DROP ROLE ${FIXTURE_PARENT_ROLE}; COMMIT; `, { actor: 'supabase_admin' }); + removalAcknowledged = true; + } catch {} + const after = inspectFixtureMembership({ scalarImpl }); + if (!fixtureMembershipAbsent(after)) { + if (!fixtureMembershipOwned(after, token)) { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_UNPROVEN'); + } + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_REMOVAL_UNCONFIRMED'); + } + ownership.fixtureMembership = false; + ownership.fixtureMembershipAttempted = false; + ownership.fixtureMembershipDropped = true; + ownership.fixtureMembershipRemovalAcknowledged = removalAcknowledged; + return true; } function cleanupOwnedFixtures(ownership) { @@ -197,8 +506,7 @@ function cleanupOwnedFixtures(ownership) { failures.push(`database:${error.message}`); } try { - if (ownership.fixtureMembership) dropFixtureMembership(); - ownership.fixtureMembership = false; + dropFixtureMembership(ownership); } catch (error) { failures.push(`roles:${error.message}`); } @@ -531,11 +839,19 @@ export async function run() { || template.profiles !== 0 || template.reviews !== 0) fail('LOCAL_REHEARSAL_TEMPLATE_INVALID'); - const ownership = { database: false, fixtureMembership: false }; + const ownership = { + database: false, + databaseAttempted: false, + databaseName: null, + databaseBaseline: null, + fixtureMembership: false, + fixtureMembershipAttempted: false, + fixtureMembershipToken: null, + fixtureMembershipBaseline: null, + }; let proof; try { - createFixtureMembership(); - ownership.fixtureMembership = true; + createFixtureMembership(ownership); createOwnedDatabase(ownership); if (scalar(DATABASE, `SELECT coalesce('security_invoker=true'=ANY(c.reloptions),false) FROM pg_class c WHERE c.oid='public.admin_evaluation_related_rows'::regclass;`) !== 't') { diff --git a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS new file mode 100644 index 0000000000..3e7285e293 --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS @@ -0,0 +1,5 @@ +d29f5fa8ddc27573b3ea2d2a144c9d9a1c24a6c5ac4d7aeef30e3bda514fc575 apps/web/performance/review-media-operating-admission-20261013/controller-reverification.json +ad4d439f9609db73d5879e655380d640b3b52274dbfe80654f6d715bd7a68ab7 apps/web/performance/review-media-operating-admission-20261013/verification.json +4ad55ed526b7af8c0ee1282057dbfa3706249deea04472a2749909af45492064 apps/web/scripts/review-media-operating-admission.mjs +26d7fe68effda2813b3f9384dd2d7e4f9e8998447cd988b89820d93be7f6bdc7 apps/web/tests-unit/review-media-operating-admission.test.ts +b4fbd520413f59e82743c545f521df5a8adde04970f610d4d06b432955f07518 docs/operations/review-media-operating-admission-20261013.md diff --git a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 new file mode 100644 index 0000000000..e602e3ac87 --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 @@ -0,0 +1 @@ +31e775a669d4a1295fc8b391653f26d0559695937f4cc357818ef53faa7c8e67 diff --git a/apps/web/performance/review-media-operating-admission-20261013/controller-reverification.json b/apps/web/performance/review-media-operating-admission-20261013/controller-reverification.json new file mode 100644 index 0000000000..7a8d215929 --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/controller-reverification.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "kind": "local-unit-reverification", + "checks": { + "passed": 10, + "failed": 0, + "assertions": 59, + "command": "bun test tests-unit/review-media-operating-admission.test.ts", + "cwd": "apps/web" + }, + "reason": "Imported controller and manifest changed for terminal boundary fixes.", + "sources": [ + { + "path": "apps/web/scripts/review-media-operating-admission.mjs", + "sha256": "4ad55ed526b7af8c0ee1282057dbfa3706249deea04472a2749909af45492064" + }, + { + "path": "apps/web/tests-unit/review-media-operating-admission.test.ts", + "sha256": "26d7fe68effda2813b3f9384dd2d7e4f9e8998447cd988b89820d93be7f6bdc7" + }, + { + "path": "apps/web/scripts/review-media-sql-successor.mjs", + "sha256": "dcf552697251c2f10d859c7a2f56937a0f6e92ee9467e5b919807e498eff1d49" + }, + { + "path": ".github/review-media-sql-successor.v1.json", + "sha256": "ae764bb760102a6d7d066be669c85248feed1823eafded4bf8c246a062090a8c" + } + ], + "operatingEvidence": false, + "performanceEvidence": false +} diff --git a/apps/web/performance/review-media-operating-admission-20261013/verification.json b/apps/web/performance/review-media-operating-admission-20261013/verification.json new file mode 100644 index 0000000000..55a3bf9d13 --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/verification.json @@ -0,0 +1,57 @@ +{ + "schemaVersion": 1, + "kind": "local-unit-verification", + "recordedAtUTC": "2026-10-13T05:42:51.792097+00:00", + "sources": [ + { + "path": "apps/web/scripts/review-media-operating-admission.mjs", + "sha256": "4ad55ed526b7af8c0ee1282057dbfa3706249deea04472a2749909af45492064", + "bytes": 28152 + }, + { + "path": "apps/web/tests-unit/review-media-operating-admission.test.ts", + "sha256": "26d7fe68effda2813b3f9384dd2d7e4f9e8998447cd988b89820d93be7f6bdc7", + "bytes": 20142 + } + ], + "environment": { + "platform": "macOS", + "node": "24.21.0", + "bun": "1.4.0" + }, + "checks": [ + { + "command": "bun test tests-unit/review-media-operating-admission.test.ts", + "cwd": "apps/web", + "passed": 10, + "failed": 0, + "assertions": 59 + }, + { + "command": "node --check apps/web/scripts/review-media-operating-admission.mjs", + "exitCode": 0 + }, + { + "command": "node node_modules/eslint/bin/eslint.js scripts/review-media-operating-admission.mjs tests-unit/review-media-operating-admission.test.ts", + "cwd": "apps/web", + "exitCode": 0 + } + ], + "scope": [ + "CLI input validation", + "private exclusive output", + "receipt schema/hash binding", + "unchanged admission window", + "fsync failure", + "symlink and oversize input rejection" + ], + "limitations": [ + "Synthetic receipts test format and internal consistency only.", + "The producer does not authenticate observation origin.", + "No hosted mutation, trusted live capture, or performance improvement is established." + ], + "confidenceInterval": { + "applicable": false, + "reason": "Deterministic branch checks, not a sampled performance or reliability estimate." + } +} diff --git a/apps/web/scripts/review-media-operating-admission.mjs b/apps/web/scripts/review-media-operating-admission.mjs new file mode 100644 index 0000000000..9d46d0df5e --- /dev/null +++ b/apps/web/scripts/review-media-operating-admission.mjs @@ -0,0 +1,717 @@ +#!/usr/bin/env node +/** Pure, local producer for private review-media successor admissions. */ +import { createHash } from 'node:crypto'; +import { + closeSync, + constants, + fstatSync, + fsyncSync, + lstatSync, + mkdirSync, + openSync, + readSync, + realpathSync, + writeSync, +} from 'node:fs'; +import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { + REPOSITORY_ROOT, + loadSuccessorManifest, + validateSuccessorAdmission, + validateSuccessorRehearsalAdmission, +} from './review-media-sql-successor.mjs'; +import { + TZUDONG_GITHUB_REPOSITORY, + TZUDONG_PRODUCTION_ALIASES, + TZUDONG_VERCEL_PROJECT_ID, + TZUDONG_VERCEL_TEAM_ID, + normalizeVercelRollbackExpected, +} from './vercel-rollback-readback.mjs'; +import { elevenMigrationBundleSuffixRoot } from './supabase-migration-bundle.mjs'; + +const SCRIPT_PATH = fileURLToPath(import.meta.url); +const MAX_WINDOW_MS = 15 * 60 * 1000; +const CLOCK_SKEW_MS = 30_000; +const MAX_PRIVATE_FILE_BYTES = 1024 * 1024; +const HEX_SHA256 = /^[0-9a-f]{64}$/; +const REVISION = /^[0-9a-f]{40}$/; +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +const FILES = Object.freeze({ + applyAdmission: 'apply-admission.json', + applyJournal: 'apply-journal.jsonl', + checkout: 'checkout-readback.json', + protectedMain: 'protected-main-receipt.json', + rehearsalAdmission: 'rehearsal-admission.json', + rehearsalJournal: 'rehearsal-journal.jsonl', + rehearsalReceipt: 'rehearsal-receipt.json', + rollback: 'rollback-readback-receipt.json', + source: 'source-receipt.json', +}); + +const PUBLIC_CODES = new Set([ + 'SUCCESSOR_OPERATING_ADMISSION_ARGUMENT_INVALID', + 'SUCCESSOR_OPERATING_ADMISSION_CUSTODY_INVALID', + 'SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID', + 'SUCCESSOR_OPERATING_ADMISSION_INPUT_INVALID', + 'SUCCESSOR_OPERATING_ADMISSION_OUTPUT_EXISTS', + 'SUCCESSOR_OPERATING_ADMISSION_WRITE_FAILED', +]); + +const operationError = code => { + const error = new Error(code); + error.code = code; + return error; +}; +const fail = code => { throw operationError(code); }; +const sha256 = value => createHash('sha256').update(value).digest('hex'); + +function safeCode(error) { + try { return typeof error?.code === 'string' ? error.code : ''; } catch { return ''; } +} + +function canonical(value) { + if (Array.isArray(value)) return value.map(canonical); + if (value && typeof value === 'object') { + return Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])); + } + return value; +} + +const canonicalBytes = value => Buffer.from(`${JSON.stringify(canonical(value))}\n`); +const equal = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right)); + +function exactKeys(value, keys, code = 'SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID') { + if (!value || typeof value !== 'object' || Array.isArray(value)) fail(code); + const actual = Object.keys(value).sort(); + const expected = [...keys].sort(); + if (actual.length !== expected.length || actual.some((key, index) => key !== expected[index])) fail(code); +} + +function assertHex(value) { + if (typeof value !== 'string' || !HEX_SHA256.test(value)) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } +} + +function assertUuid(value) { + if (typeof value !== 'string' || !UUID.test(value)) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } +} + +function pathEntryExists(path) { + try { + lstatSync(path); + return true; + } catch (error) { + if (safeCode(error) === 'ENOENT') return false; + fail('SUCCESSOR_OPERATING_ADMISSION_CUSTODY_INVALID'); + } +} + +function assertOutsideRepository(path, code, { mustExist = true } = {}) { + if (typeof path !== 'string' || !isAbsolute(path)) fail(code); + const absolute = resolve(path); + let comparison; + try { + comparison = mustExist ? realpathSync(absolute) : realpathSync(dirname(absolute)); + } catch { + fail(code); + } + const repository = realpathSync(REPOSITORY_ROOT); + const rel = relative(repository, comparison); + if (!rel || (rel !== '..' && !rel.startsWith(`..${sep}`))) fail(code); + return absolute; +} + +function assertPrivateDirectory(path, code) { + let status; + try { status = lstatSync(path); } catch { fail(code); } + if (!status.isDirectory() + || status.isSymbolicLink() + || (process.platform !== 'win32' && (status.mode & 0o077) !== 0)) fail(code); + return realpathSync(path); +} + +function readBoundedDescriptor(descriptor, status, code) { + if (!Number.isSafeInteger(status.size) + || status.size <= 0 + || status.size > MAX_PRIVATE_FILE_BYTES) fail(code); + const buffer = Buffer.allocUnsafe(status.size + 1); + let offset = 0; + while (offset < buffer.length) { + const count = readSync(descriptor, buffer, offset, buffer.length - offset, null); + if (!Number.isSafeInteger(count) || count < 0) fail(code); + if (count === 0) break; + offset += count; + } + const finalStatus = fstatSync(descriptor); + if (offset !== status.size || finalStatus.size !== status.size) fail(code); + return buffer.subarray(0, offset); +} + +function readPrivateCanonicalJson(path, code = 'SUCCESSOR_OPERATING_ADMISSION_INPUT_INVALID') { + const absolute = assertOutsideRepository(path, code); + const parent = assertPrivateDirectory(dirname(absolute), code); + let descriptor; + try { + descriptor = openSync(absolute, constants.O_RDONLY | constants.O_NOFOLLOW); + const status = fstatSync(descriptor); + const named = lstatSync(absolute); + if (!status.isFile() + || named.isSymbolicLink() + || status.dev !== named.dev + || status.ino !== named.ino + || dirname(realpathSync(absolute)) !== parent + || (process.platform !== 'win32' && (status.mode & 0o077) !== 0)) fail(code); + const bytes = readBoundedDescriptor(descriptor, status, code); + let value; + try { value = JSON.parse(bytes.toString('utf8')); } catch { fail(code); } + if (!value || typeof value !== 'object' || Array.isArray(value) + || !bytes.equals(canonicalBytes(value))) fail(code); + return Object.freeze({ absolute, bytes, value }); + } catch (error) { + if (safeCode(error) === code) throw error; + fail(code); + } finally { + if (descriptor !== undefined) { + try { closeSync(descriptor); } catch { /* bounded failure above */ } + } + } +} + +function writeAll(descriptor, bytes) { + let offset = 0; + while (offset < bytes.length) { + const written = writeSync(descriptor, bytes, offset, bytes.length - offset); + if (!Number.isSafeInteger(written) || written <= 0) { + fail('SUCCESSOR_OPERATING_ADMISSION_WRITE_FAILED'); + } + offset += written; + } +} + +function writeExclusiveCanonical(path, value, fsyncImpl = fsyncSync) { + const bytes = canonicalBytes(value); + if (bytes.length === 0 || bytes.length > MAX_PRIVATE_FILE_BYTES) { + fail('SUCCESSOR_OPERATING_ADMISSION_WRITE_FAILED'); + } + let descriptor; + try { + descriptor = openSync(path, 'wx', 0o600); + writeAll(descriptor, bytes); + fsyncImpl(descriptor); + return bytes; + } catch (error) { + const errorCode = safeCode(error); + if (errorCode === 'EEXIST') fail('SUCCESSOR_OPERATING_ADMISSION_OUTPUT_EXISTS'); + if (errorCode.startsWith('SUCCESSOR_OPERATING_ADMISSION_')) throw error; + fail('SUCCESSOR_OPERATING_ADMISSION_WRITE_FAILED'); + } finally { + if (descriptor !== undefined) { + try { closeSync(descriptor); } catch { /* file fsync already established the bounded outcome */ } + } + } +} + +function syncDirectory(path, fsyncImpl = fsyncSync) { + let descriptor; + try { + descriptor = openSync(path, 'r'); + fsyncImpl(descriptor); + } catch { + fail('SUCCESSOR_OPERATING_ADMISSION_WRITE_FAILED'); + } finally { + if (descriptor !== undefined) { + try { closeSync(descriptor); } catch { /* bounded outcome above */ } + } + } +} + +function assertWindow(createdAtValue, expiresAtValue, now) { + const createdAt = Date.parse(createdAtValue); + const expiresAt = Date.parse(expiresAtValue); + const nowMs = now instanceof Date ? now.getTime() : NaN; + if (!Number.isFinite(createdAt) + || !Number.isFinite(expiresAt) + || !Number.isFinite(nowMs) + || expiresAt <= createdAt + || expiresAt - createdAt > MAX_WINDOW_MS + || createdAt > nowMs + CLOCK_SKEW_MS + || nowMs >= expiresAt) fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + return Object.freeze({ createdAt, expiresAt, nowMs }); +} + +function assertObservedWindow(document, expiresAtValue, window) { + const observedAt = Date.parse(document.observedAt); + if (document.expiresAt !== expiresAtValue + || !Number.isFinite(observedAt) + || observedAt < window.createdAt + || observedAt >= window.expiresAt + || observedAt > window.nowMs + CLOCK_SKEW_MS) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } +} + +function assertCommonReceipt(receipt, input, manifestRecord, window) { + const { manifest, manifestSha256 } = manifestRecord; + if (receipt.schemaVersion !== 1 + || receipt.id !== manifest.id + || receipt.projectRef !== manifest.projectRef + || receipt.purpose !== manifest.purpose + || receipt.manifestSha256 !== manifestSha256 + || receipt.sourceRevision !== input.sourceRevision) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + assertObservedWindow(receipt, input.expiresAt, window); +} + +function validateRehearsalInput(input, manifestRecord, now) { + exactKeys(input, [ + 'attemptId', 'checkoutReadback', 'createdAt', 'expiresAt', 'kind', 'manifestSha256', + 'protectedMainReceipt', 'rollbackReadbackReceipt', 'schemaVersion', 'sourceReceipt', + 'sourceRevision', 'sourceRoot', 'writerFenceReceipts', + ], 'SUCCESSOR_OPERATING_ADMISSION_INPUT_INVALID'); + const { manifest, manifestSha256 } = manifestRecord; + if (input.schemaVersion !== 1 + || input.kind !== 'review-media-successor-rehearsal-admission-input' + || input.manifestSha256 !== manifestSha256 + || input.sourceRoot !== manifest.sourceRoot + || !REVISION.test(input.sourceRevision ?? '')) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + assertUuid(input.attemptId); + const window = assertWindow(input.createdAt, input.expiresAt, now); + + exactKeys(input.checkoutReadback, [ + 'clean', 'detached', 'expiresAt', 'kind', 'observedAt', 'revision', 'schemaVersion', + ]); + if (input.checkoutReadback.schemaVersion !== 1 + || input.checkoutReadback.kind !== 'successor-checkout-readback' + || input.checkoutReadback.revision !== input.sourceRevision + || input.checkoutReadback.clean !== true + || input.checkoutReadback.detached !== true) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + assertObservedWindow(input.checkoutReadback, input.expiresAt, window); + + exactKeys(input.sourceReceipt, [ + 'expiresAt', 'id', 'kind', 'manifestSha256', 'observedAt', 'projectRef', 'purpose', + 'schemaVersion', 'sourceRevision', 'sourceRoot', + ]); + assertCommonReceipt(input.sourceReceipt, input, manifestRecord, window); + if (input.sourceReceipt.kind !== 'successor-source-receipt' + || input.sourceReceipt.sourceRoot !== manifest.sourceRoot) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + + exactKeys(input.protectedMainReceipt, [ + 'expiresAt', 'id', 'kind', 'manifestSha256', 'observedAt', 'projectRef', 'purpose', + 'ref', 'remote', 'repositoryUrl', 'revision', 'schemaVersion', 'sourceRevision', + ]); + assertCommonReceipt(input.protectedMainReceipt, input, manifestRecord, window); + if (input.protectedMainReceipt.kind !== 'protected-main-readback-receipt' + || input.protectedMainReceipt.remote !== manifest.protectedSource.remote + || input.protectedMainReceipt.ref !== manifest.protectedSource.ref + || input.protectedMainReceipt.repositoryUrl !== manifest.protectedSource.repositoryUrl + || input.protectedMainReceipt.revision !== input.sourceRevision) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + + const rollback = input.rollbackReadbackReceipt; + exactKeys(rollback, [ + 'deploymentId', 'deploymentSha', 'deploymentUrl', 'expiresAt', 'gitRef', 'id', 'kind', + 'manifestSha256', 'observedAt', 'productionAliases', 'projectId', 'projectRef', + 'purpose', 'readyState', 'repository', 'schemaVersion', 'sourceRevision', 'state', + 'target', 'teamId', + ]); + assertCommonReceipt(rollback, input, manifestRecord, window); + let normalizedRollback; + try { + normalizedRollback = normalizeVercelRollbackExpected({ + deploymentId: rollback.deploymentId, + deploymentUrl: rollback.deploymentUrl, + gitRef: rollback.gitRef, + gitSha: rollback.deploymentSha, + }); + } catch { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + if (rollback.kind !== 'rollback-readback-receipt' + || normalizedRollback.deploymentUrl !== rollback.deploymentUrl + || rollback.projectId !== TZUDONG_VERCEL_PROJECT_ID + || rollback.teamId !== TZUDONG_VERCEL_TEAM_ID + || rollback.repository !== TZUDONG_GITHUB_REPOSITORY + || !equal(rollback.productionAliases, TZUDONG_PRODUCTION_ALIASES) + || rollback.readyState !== 'READY' + || rollback.target !== 'production' + || rollback.state !== 'ready') { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + + const fenceKeys = Object.keys(manifest.requiredWriterFences).sort(); + exactKeys(input.writerFenceReceipts, fenceKeys); + for (const key of fenceKeys) { + const receipt = input.writerFenceReceipts[key]; + exactKeys(receipt, [ + 'expiresAt', 'fence', 'id', 'kind', 'manifestSha256', 'observedAt', 'projectRef', + 'purpose', 'schemaVersion', 'sourceRevision', 'state', + ]); + assertCommonReceipt(receipt, input, manifestRecord, window); + if (receipt.kind !== 'writer-fence-readback-receipt' + || receipt.fence !== key + || receipt.state !== manifest.requiredWriterFences[key]) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + } + return window; +} + +function createCustodyDirectory(path, fsyncImpl) { + const absolute = assertOutsideRepository(path, 'SUCCESSOR_OPERATING_ADMISSION_CUSTODY_INVALID', { + mustExist: false, + }); + assertPrivateDirectory(dirname(absolute), 'SUCCESSOR_OPERATING_ADMISSION_CUSTODY_INVALID'); + if (pathEntryExists(absolute)) fail('SUCCESSOR_OPERATING_ADMISSION_OUTPUT_EXISTS'); + try { mkdirSync(absolute, { mode: 0o700 }); } catch (error) { + if (safeCode(error) === 'EEXIST') fail('SUCCESSOR_OPERATING_ADMISSION_OUTPUT_EXISTS'); + fail('SUCCESSOR_OPERATING_ADMISSION_WRITE_FAILED'); + } + syncDirectory(dirname(absolute), fsyncImpl); + return assertPrivateDirectory(absolute, 'SUCCESSOR_OPERATING_ADMISSION_CUSTODY_INVALID'); +} + +function existingCustodyDirectory(path) { + const absolute = assertOutsideRepository(path, 'SUCCESSOR_OPERATING_ADMISSION_CUSTODY_INVALID'); + return assertPrivateDirectory(absolute, 'SUCCESSOR_OPERATING_ADMISSION_CUSTODY_INVALID'); +} + +export function prepareReviewMediaRehearsalAdmission({ + custodyDirectory, + fsyncImpl = fsyncSync, + inputPath, + manifestRecord = loadSuccessorManifest(), + now = new Date(), +} = {}) { + const inputRecord = readPrivateCanonicalJson(inputPath); + const input = inputRecord.value; + validateRehearsalInput(input, manifestRecord, now); + + const custody = createCustodyDirectory(custodyDirectory, fsyncImpl); + const sourceBytes = canonicalBytes(input.sourceReceipt); + const protectedMainBytes = canonicalBytes(input.protectedMainReceipt); + const rollbackBytes = canonicalBytes(input.rollbackReadbackReceipt); + const writerFences = {}; + const receiptDocuments = [ + [FILES.checkout, input.checkoutReadback], + [FILES.source, input.sourceReceipt], + [FILES.protectedMain, input.protectedMainReceipt], + [FILES.rollback, input.rollbackReadbackReceipt], + ]; + for (const key of Object.keys(manifestRecord.manifest.requiredWriterFences).sort()) { + const receipt = input.writerFenceReceipts[key]; + const bytes = canonicalBytes(receipt); + writerFences[key] = Object.freeze({ + evidenceSha256: sha256(bytes), + state: receipt.state, + }); + receiptDocuments.push([`writer-fence-${key}.json`, receipt]); + } + + const journalPath = join(custody, FILES.rehearsalJournal); + const receiptPath = join(custody, FILES.rehearsalReceipt); + const admissionPath = join(custody, FILES.rehearsalAdmission); + const rollback = input.rollbackReadbackReceipt; + const admission = { + attemptId: input.attemptId, + createdAt: input.createdAt, + expiresAt: input.expiresAt, + id: manifestRecord.manifest.id, + journalPathSha256: sha256(Buffer.from(journalPath)), + kind: 'successor-rehearsal-admission', + manifestSha256: manifestRecord.manifestSha256, + projectRef: manifestRecord.manifest.projectRef, + protectedMainReceiptSha256: sha256(protectedMainBytes), + purpose: manifestRecord.manifest.purpose, + receiptPathSha256: sha256(Buffer.from(receiptPath)), + rollback: { + deploymentId: rollback.deploymentId, + deploymentSha: rollback.deploymentSha, + deploymentUrl: rollback.deploymentUrl, + gitRef: rollback.gitRef, + productionAliases: rollback.productionAliases, + projectId: rollback.projectId, + readbackSha256: sha256(rollbackBytes), + readyState: rollback.readyState, + repository: rollback.repository, + state: rollback.state, + target: rollback.target, + teamId: rollback.teamId, + }, + schemaVersion: 1, + sourceReceiptSha256: sha256(sourceBytes), + sourceRevision: input.sourceRevision, + sourceRoot: input.sourceRoot, + writerFences, + }; + + for (const [name, document] of receiptDocuments) { + writeExclusiveCanonical(join(custody, name), document, fsyncImpl); + } + const admissionBytes = writeExclusiveCanonical(admissionPath, admission, fsyncImpl); + syncDirectory(custody, fsyncImpl); + validateSuccessorRehearsalAdmission(admission, manifestRecord, { + now, + receiptDirectory: custody, + }); + if (pathEntryExists(journalPath) || pathEntryExists(receiptPath)) { + fail('SUCCESSOR_OPERATING_ADMISSION_OUTPUT_EXISTS'); + } + return Object.freeze({ + admissionPath, + admissionSha256: sha256(admissionBytes), + custodyDirectory: custody, + expiresAt: admission.expiresAt, + journalPath, + mode: 'rehearsal', + receiptPath, + status: 'prepared', + }); +} + +function readCanonicalCustodyFile(custody, name) { + return readPrivateCanonicalJson(join(custody, name), 'SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); +} + +function readCanonicalCustodyJournal(custody, name) { + const path = assertOutsideRepository( + join(custody, name), + 'SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID', + ); + let descriptor; + try { + descriptor = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW); + const status = fstatSync(descriptor); + const named = lstatSync(path); + if (!status.isFile() + || named.isSymbolicLink() + || status.dev !== named.dev + || status.ino !== named.ino + || dirname(realpathSync(path)) !== custody + || (process.platform !== 'win32' && (status.mode & 0o077) !== 0)) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + const bytes = readBoundedDescriptor( + descriptor, + status, + 'SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID', + ); + const text = bytes.toString('utf8'); + const lines = text.endsWith('\n') ? text.slice(0, -1).split('\n') : []; + if (lines.length !== 3 || lines.some(line => !line)) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + const entries = lines.map(line => { + let entry; + try { entry = JSON.parse(line); } catch { fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); } + if (!entry || typeof entry !== 'object' || Array.isArray(entry) + || `${JSON.stringify(canonical(entry))}\n` !== `${line}\n`) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + return entry; + }); + return Object.freeze({ bytes, entries, path }); + } catch (error) { + if (safeCode(error) === 'SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID') throw error; + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } finally { + if (descriptor !== undefined) { + try { closeSync(descriptor); } catch { /* bounded failure above */ } + } + } +} + +function validateApplyInput(input, rehearsal, manifestRecord, now) { + exactKeys(input, [ + 'attemptId', 'expiresAt', 'kind', 'manifestSha256', 'observedAt', + 'rehearsalAdmissionSha256', 'rehearsalJournalSha256', 'rehearsalReceiptSha256', + 'schemaVersion', 'sourceRevision', + ], 'SUCCESSOR_OPERATING_ADMISSION_INPUT_INVALID'); + if (input.schemaVersion !== 1 + || input.kind !== 'review-media-successor-apply-admission-input' + || input.manifestSha256 !== manifestRecord.manifestSha256 + || input.sourceRevision !== rehearsal.sourceRevision + || input.expiresAt !== rehearsal.expiresAt) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + assertUuid(input.attemptId); + assertHex(input.rehearsalAdmissionSha256); + assertHex(input.rehearsalJournalSha256); + assertHex(input.rehearsalReceiptSha256); + const window = assertWindow(rehearsal.createdAt, rehearsal.expiresAt, now); + const observedAt = Date.parse(input.observedAt); + if (!Number.isFinite(observedAt) + || observedAt < window.createdAt + || observedAt >= window.expiresAt + || observedAt > window.nowMs + CLOCK_SKEW_MS) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + return Object.freeze({ ...window, observedAt }); +} + +export function prepareReviewMediaApplyAdmission({ + custodyDirectory, + fsyncImpl = fsyncSync, + inputPath, + manifestRecord = loadSuccessorManifest(), + now = new Date(), +} = {}) { + const inputRecord = readPrivateCanonicalJson(inputPath); + const custody = existingCustodyDirectory(custodyDirectory); + const rehearsalAdmissionRecord = readCanonicalCustodyFile(custody, FILES.rehearsalAdmission); + const rehearsal = rehearsalAdmissionRecord.value; + const window = validateApplyInput(inputRecord.value, rehearsal, manifestRecord, now); + if (sha256(rehearsalAdmissionRecord.bytes) !== inputRecord.value.rehearsalAdmissionSha256) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + validateSuccessorRehearsalAdmission(rehearsal, manifestRecord, { + now, + receiptDirectory: custody, + }); + + const rehearsalReceiptRecord = readCanonicalCustodyFile(custody, FILES.rehearsalReceipt); + const rehearsalJournalRecord = readCanonicalCustodyJournal(custody, FILES.rehearsalJournal); + const rehearsalJournalBytes = rehearsalJournalRecord.bytes; + if (sha256(rehearsalReceiptRecord.bytes) !== inputRecord.value.rehearsalReceiptSha256 + || sha256(rehearsalJournalBytes) !== inputRecord.value.rehearsalJournalSha256) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + const receipt = rehearsalReceiptRecord.value; + if (receipt.admissionSha256 !== inputRecord.value.rehearsalAdmissionSha256 + || receipt.expiresAt !== rehearsal.expiresAt) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + const stageStatesSha256 = sha256(JSON.stringify(canonical(receipt.stageStates))); + const priorStateSha256 = sha256(JSON.stringify(canonical(receipt.stageStates?.[0]))); + const receiptBindings = { + admissionSha256: HEX_SHA256.test(receipt.admissionSha256 ?? ''), + attemptId: UUID.test(receipt.attemptId ?? ''), + common: receipt.schemaVersion === 1 + && receipt.id === manifestRecord.manifest.id + && receipt.projectRef === manifestRecord.manifest.projectRef + && receipt.purpose === manifestRecord.manifest.purpose + && receipt.manifestSha256 === manifestRecord.manifestSha256 + && receipt.sourceRevision === rehearsal.sourceRevision, + kind: receipt.kind === 'successor-rehearsal-receipt', + journalPathSha256: receipt.journalPathSha256 === sha256(Buffer.from(rehearsalJournalRecord.path)), + passed: receipt.passed === true, + preflightStateSha256: receipt.preflightStateSha256 === priorStateSha256, + restoredStateSha256: receipt.restoredStateSha256 === priorStateSha256, + serverMajor: Number.isSafeInteger(receipt.serverVersionNum) + && Math.floor(receipt.serverVersionNum / 10_000) === manifestRecord.manifest.serverMajor, + sourceRoot: receipt.sourceRoot === manifestRecord.manifest.sourceRoot, + stageStatesSha256: receipt.stageStatesSha256 === stageStatesSha256, + suffixRoot: receipt.suffixRoot === elevenMigrationBundleSuffixRoot(manifestRecord.manifest.migrations), + }; + if (Object.values(receiptBindings).some(value => !value)) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + const publication = rehearsalJournalRecord.entries[2]; + const publishedAt = Date.parse(publication.publishedAt); + if (!Number.isFinite(publishedAt) || window.observedAt < publishedAt) { + fail('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + } + + const applyAdmissionPath = join(custody, FILES.applyAdmission); + const applyJournalPath = join(custody, FILES.applyJournal); + if (pathEntryExists(applyAdmissionPath) || pathEntryExists(applyJournalPath)) { + fail('SUCCESSOR_OPERATING_ADMISSION_OUTPUT_EXISTS'); + } + const admission = { + attemptId: inputRecord.value.attemptId, + createdAt: rehearsal.createdAt, + expiresAt: rehearsal.expiresAt, + id: rehearsal.id, + journalPathSha256: sha256(Buffer.from(applyJournalPath)), + manifestSha256: rehearsal.manifestSha256, + projectRef: rehearsal.projectRef, + protectedMainReceiptSha256: rehearsal.protectedMainReceiptSha256, + purpose: rehearsal.purpose, + rehearsalJournalSha256: inputRecord.value.rehearsalJournalSha256, + rehearsalReceiptSha256: inputRecord.value.rehearsalReceiptSha256, + rollback: structuredClone(rehearsal.rollback), + schemaVersion: 1, + sourceReceiptSha256: rehearsal.sourceReceiptSha256, + sourceRevision: rehearsal.sourceRevision, + sourceRoot: rehearsal.sourceRoot, + stageStates: receipt.stageStates, + writerFences: structuredClone(rehearsal.writerFences), + }; + validateSuccessorAdmission(admission, manifestRecord, { now, receiptDirectory: custody }); + const admissionBytes = writeExclusiveCanonical(applyAdmissionPath, admission, fsyncImpl); + syncDirectory(custody, fsyncImpl); + if (pathEntryExists(applyJournalPath)) fail('SUCCESSOR_OPERATING_ADMISSION_OUTPUT_EXISTS'); + return Object.freeze({ + admissionPath: applyAdmissionPath, + admissionSha256: sha256(admissionBytes), + custodyDirectory: custody, + expiresAt: admission.expiresAt, + journalPath: applyJournalPath, + mode: 'apply', + status: 'prepared', + }); +} + +export function parseOperatingAdmissionArguments(argv) { + const result = {}; + for (let index = 0; index < argv.length; index += 2) { + const option = argv[index]; + const value = argv[index + 1]; + if (!['--mode', '--input', '--custody-dir'].includes(option) + || !value + || value.startsWith('--') + || option.slice(2) in result) { + fail('SUCCESSOR_OPERATING_ADMISSION_ARGUMENT_INVALID'); + } + result[option.slice(2)] = value; + } + if (argv.length !== 6 + || !['rehearsal', 'apply'].includes(result.mode) + || !result.input + || !result['custody-dir']) { + fail('SUCCESSOR_OPERATING_ADMISSION_ARGUMENT_INVALID'); + } + return Object.freeze({ + custodyDirectory: result['custody-dir'], + inputPath: result.input, + mode: result.mode, + }); +} + +export function operatingAdmissionFailureResult(error) { + const observedCode = safeCode(error); + const code = PUBLIC_CODES.has(observedCode) + ? observedCode + : 'SUCCESSOR_OPERATING_ADMISSION_INPUT_INVALID'; + return Object.freeze({ code, status: 'failed' }); +} + +export function main(argv = process.argv.slice(2), dependencies = {}) { + const args = parseOperatingAdmissionArguments(argv); + const producer = args.mode === 'rehearsal' + ? prepareReviewMediaRehearsalAdmission + : prepareReviewMediaApplyAdmission; + return producer({ ...dependencies, ...args }); +} + +if (process.argv[1] && resolve(process.argv[1]) === SCRIPT_PATH) { + try { + console.log(JSON.stringify(main())); + } catch (error) { + console.error(JSON.stringify(operatingAdmissionFailureResult(error))); + process.exitCode = 1; + } +} diff --git a/apps/web/scripts/review-media-sql-successor.mjs b/apps/web/scripts/review-media-sql-successor.mjs index 2db595b9b3..dd9a50e3af 100644 --- a/apps/web/scripts/review-media-sql-successor.mjs +++ b/apps/web/scripts/review-media-sql-successor.mjs @@ -86,6 +86,10 @@ const FIXED_CONTROLLER_CODES = new Set([ 'SUCCESSOR_ROLLBACK_READBACK_INVALID', 'SUCCESSOR_SOURCE_DRIFT', ]); +const FIXED_JOURNAL_OPERATION_CODES = new Set([ + ...FIXED_CONTROLLER_CODES, + 'SUCCESSOR_COMMITTED', +]); const operationError = code => { const error = new Error(code); @@ -122,13 +126,17 @@ function safeCode(error, key = 'code') { } catch { return ''; } } +function safeTrue(error, key) { + try { return error?.[key] === true; } catch { return false; } +} + export function boundedSuccessorError(error) { const code = safeCode(error); if (FIXED_CONTROLLER_CODES.has(code)) { const bounded = operationError(code); if (code === 'SUCCESSOR_JOURNAL_APPEND_FAILED') { const operationCode = safeCode(error, 'operationCode'); - if (FIXED_CONTROLLER_CODES.has(operationCode)) bounded.operationCode = operationCode; + if (FIXED_JOURNAL_OPERATION_CODES.has(operationCode)) bounded.operationCode = operationCode; } return bounded; } @@ -146,7 +154,7 @@ export function successorFailureResult(error) { function journalFailure(operationCode) { const error = operationError('SUCCESSOR_JOURNAL_APPEND_FAILED'); - if (FIXED_CONTROLLER_CODES.has(operationCode)) error.operationCode = operationCode; + if (FIXED_JOURNAL_OPERATION_CODES.has(operationCode)) error.operationCode = operationCode; return error; } @@ -1590,6 +1598,26 @@ function appendRehearsalVerification(descriptor, entry, { } } +function assertFreshRehearsalBoundary(descriptor, admission, expiresAt, now, { + binding, + appendJournalImpl = appendJournal, +} = {}) { + const checkedAt = now(); + try { + assertAdmissionFresh(expiresAt, checkedAt); + } catch { + appendRehearsalTerminal( + descriptor, + admission, + 'SUCCESSOR_ADMISSION_EXPIRED', + () => checkedAt, + { binding, appendJournalImpl }, + ); + fail('SUCCESSOR_ADMISSION_EXPIRED'); + } + return checkedAt; +} + export function runReviewMediaSuccessorRehearsal({ admissionPath, journalPath, @@ -1756,15 +1784,13 @@ export function runReviewMediaSuccessorRehearsal({ }); fail('SUCCESSOR_REHEARSAL_OUTPUT_INVALID'); } - const receiptObservedAt = now(); - try { - assertAdmissionFresh(admissionWindow.expiresAt, receiptObservedAt); - } catch { - appendRehearsalTerminal(descriptor, admission, 'SUCCESSOR_ADMISSION_EXPIRED', now, { - appendJournalImpl, - }); - fail('SUCCESSOR_ADMISSION_EXPIRED'); - } + const receiptObservedAt = assertFreshRehearsalBoundary( + descriptor, + admission, + admissionWindow.expiresAt, + now, + { appendJournalImpl }, + ); const priorStateSha256 = sha256(JSON.stringify(canonical(preflight.state))); const receipt = { admissionSha256, @@ -1804,16 +1830,30 @@ export function runReviewMediaSuccessorRehearsal({ sourceRevision: admission.sourceRevision, sourceRoot: manifestRecord.manifest.sourceRoot, }; + const verificationCheckedAt = assertFreshRehearsalBoundary( + descriptor, + admission, + admissionWindow.expiresAt, + now, + { binding: verificationBinding, appendJournalImpl }, + ); const verificationEntry = { attemptId: admission.attemptId, code: 'SUCCESSOR_REHEARSAL_VERIFIED', event: 'verification', - verifiedAt: now().toISOString(), + verifiedAt: verificationCheckedAt.toISOString(), ...verificationBinding, }; appendRehearsalVerification(descriptor, verificationEntry, { appendJournalImpl, }); + assertFreshRehearsalBoundary( + descriptor, + admission, + admissionWindow.expiresAt, + now, + { binding: verificationBinding, appendJournalImpl }, + ); const receiptBytes = canonicalReceiptBytes(receipt); let publishFailed = false; let publicationReadbackEligible = false; @@ -1821,7 +1861,7 @@ export function runReviewMediaSuccessorRehearsal({ publishReceiptImpl(pendingReceiptFile, receiptFile); } catch (error) { publishFailed = true; - publicationReadbackEligible = error?.publicationReadbackEligible === true; + publicationReadbackEligible = safeTrue(error, 'publicationReadbackEligible'); } if ((publishFailed && !publicationReadbackEligible) || !publishedReceiptMatches(receiptFile, receiptBytes)) { @@ -1834,24 +1874,32 @@ export function runReviewMediaSuccessorRehearsal({ ); fail('SUCCESSOR_REHEARSAL_RECEIPT_PUBLISH_FAILED'); } + const publicationCheckedAt = assertFreshRehearsalBoundary( + descriptor, + admission, + admissionWindow.expiresAt, + now, + { binding: verificationBinding, appendJournalImpl }, + ); const publicationEntry = { attemptId: admission.attemptId, code: 'SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED', event: 'publication', - publishedAt: now().toISOString(), + publishedAt: publicationCheckedAt.toISOString(), ...verificationBinding, }; let publicationAuditReadback = 'confirmed'; try { appendJournalImpl(descriptor, publicationEntry); } catch (error) { - publicationAuditReadback = error?.publicationAuditReadbackEligible === true && publishedJournalMatches( + publicationAuditReadback = safeTrue(error, 'publicationAuditReadbackEligible') && publishedJournalMatches( journalFile, [startedEntry, verificationEntry, publicationEntry], ) ? 'reconciled' : 'unconfirmed'; try { closeSync(descriptor); } catch { /* append implementation may already have closed it */ } if (publicationAuditReadback === 'unconfirmed') fail('SUCCESSOR_JOURNAL_APPEND_FAILED'); } + assertAdmissionFresh(admissionWindow.expiresAt, now()); return Object.freeze({ attemptId: admission.attemptId, code: 'SUCCESSOR_REHEARSAL_PASSED', @@ -1958,7 +2006,7 @@ export function runReviewMediaSuccessor({ const admissionSha256 = sha256(admissionBytes); assertDurableRehearsalArtifacts(dirname(admissionFile), admission, fsyncImpl); - const descriptor = writeJournalStart(journalFile, { + const startedEntry = { admissionSha256, attemptId: admission.attemptId, event: 'started', @@ -1966,7 +2014,8 @@ export function runReviewMediaSuccessor({ sourceRevision: admission.sourceRevision, sourceRoot: manifestRecord.manifest.sourceRoot, startedAt: now().toISOString(), - }, { fsyncImpl }); + }; + const descriptor = writeJournalStart(journalFile, startedEntry, { fsyncImpl }); let result; try { assertAdmissionFresh(admissionWindow.expiresAt, now()); @@ -1986,17 +2035,29 @@ export function runReviewMediaSuccessor({ } throw bounded; } - appendJournalImpl(descriptor, { + const terminalEntry = { attemptId: admission.attemptId, code: 'SUCCESSOR_COMMITTED', completedAt: now().toISOString(), event: 'terminal', - }); + }; + let terminalAuditReadback = 'confirmed'; + try { + appendJournalImpl(descriptor, terminalEntry); + } catch (error) { + terminalAuditReadback = safeTrue(error, 'publicationAuditReadbackEligible') && publishedJournalMatches( + journalFile, + [startedEntry, terminalEntry], + ) ? 'reconciled' : 'unconfirmed'; + try { closeSync(descriptor); } catch { /* append implementation may already have closed it */ } + if (terminalAuditReadback === 'unconfirmed') throw journalFailure('SUCCESSOR_COMMITTED'); + } return Object.freeze({ attemptId: admission.attemptId, code: 'SUCCESSOR_COMMITTED', sourceRevision: admission.sourceRevision, status: 'committed', + terminalAuditReadback, terminal: result, }); } diff --git a/apps/web/tests-unit/review-media-operating-admission.test.ts b/apps/web/tests-unit/review-media-operating-admission.test.ts new file mode 100644 index 0000000000..bc16f2a9f6 --- /dev/null +++ b/apps/web/tests-unit/review-media-operating-admission.test.ts @@ -0,0 +1,523 @@ +import { afterAll, expect, test } from 'bun:test'; +import { createHash } from 'node:crypto'; +import { + chmodSync, + existsSync, + fsyncSync, + mkdtempSync, + readFileSync, + rmSync, + statSync, + symlinkSync, + unlinkSync, + writeFileSync, +} from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { + operatingAdmissionFailureResult, + parseOperatingAdmissionArguments, + prepareReviewMediaApplyAdmission, + prepareReviewMediaRehearsalAdmission, +} from '../scripts/review-media-operating-admission.mjs'; +import { + loadSuccessorManifest, + validateSuccessorAdmission, + validateSuccessorRehearsalAdmission, +} from '../scripts/review-media-sql-successor.mjs'; +import { elevenMigrationBundleSuffixRoot } from '../scripts/supabase-migration-bundle.mjs'; + +const manifestRecord = loadSuccessorManifest(); +const { manifest } = manifestRecord; +const fixedNow = new Date('2026-10-13T08:00:20.000Z'); +const createdAt = '2026-10-13T08:00:00.000Z'; +const expiresAt = '2026-10-13T08:10:00.000Z'; +const observedAt = '2026-10-13T08:00:10.000Z'; +const sourceRevision = '4d4ecf608a7922df5a68beaf3e1ea2dcc250a3b7'; +const roots: string[] = []; + +const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex'); +const canonical = (value: any): any => Array.isArray(value) + ? value.map(canonical) + : value && typeof value === 'object' + ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) + : value; +const canonicalBytes = (value: any) => Buffer.from(`${JSON.stringify(canonical(value))}\n`); + +afterAll(() => { + for (const root of roots) rmSync(root, { recursive: true, force: true }); +}); + +function privateRoot() { + const root = mkdtempSync(join(tmpdir(), 'tzudong-operating-admission-test-')); + roots.push(root); + chmodSync(root, 0o700); + return root; +} + +function writePrivateCanonical(path: string, value: any) { + writeFileSync(path, canonicalBytes(value), { mode: 0o600 }); + chmodSync(path, 0o600); +} + +function rehearsalInput() { + const common = { + expiresAt, + id: manifest.id, + manifestSha256: manifestRecord.manifestSha256, + observedAt, + projectRef: manifest.projectRef, + purpose: manifest.purpose, + schemaVersion: 1, + sourceRevision, + }; + return { + attemptId: '123e4567-e89b-42d3-a456-426614174100', + checkoutReadback: { + clean: true, + detached: true, + expiresAt, + kind: 'successor-checkout-readback', + observedAt, + revision: sourceRevision, + schemaVersion: 1, + }, + createdAt, + expiresAt, + kind: 'review-media-successor-rehearsal-admission-input', + manifestSha256: manifestRecord.manifestSha256, + protectedMainReceipt: { + ...common, + kind: 'protected-main-readback-receipt', + ref: manifest.protectedSource.ref, + remote: manifest.protectedSource.remote, + repositoryUrl: manifest.protectedSource.repositoryUrl, + revision: sourceRevision, + }, + rollbackReadbackReceipt: { + ...common, + deploymentId: 'dpl_TestOnlyAdmissionEvidence123', + deploymentSha: '1884f87e53c4567ead7c9a0fb394b09b91a70396', + deploymentUrl: 'https://tzudong-test-only-twoimos-projects.vercel.app/', + gitRef: 'main', + kind: 'rollback-readback-receipt', + productionAliases: ['tzudong.app', 'www.tzudong.app'], + projectId: 'prj_sau35J5uUtShIQ9OKofRtOVVnTSl', + readyState: 'READY', + repository: 'twoimo/tzudong', + state: 'ready', + target: 'production', + teamId: 'team_OUj64KeLxJI3PkEbOaFZnorA', + }, + schemaVersion: 1, + sourceReceipt: { + ...common, + kind: 'successor-source-receipt', + sourceRoot: manifest.sourceRoot, + }, + sourceRevision, + sourceRoot: manifest.sourceRoot, + writerFenceReceipts: Object.fromEntries(Object.entries(manifest.requiredWriterFences).map( + ([fence, state]) => [fence, { + ...common, + fence, + kind: 'writer-fence-readback-receipt', + state, + }], + )), + }; +} + +function prepareRehearsal(input = rehearsalInput()) { + const root = privateRoot(); + const inputPath = join(root, 'rehearsal-input.json'); + const custodyDirectory = join(root, 'custody'); + writePrivateCanonical(inputPath, input); + const result = prepareReviewMediaRehearsalAdmission({ + custodyDirectory, + inputPath, + manifestRecord, + now: fixedNow, + }); + return { custodyDirectory: result.custodyDirectory, input, inputPath, result, root }; +} + +function stageStates() { + const prefixRoot = '1'.repeat(64); + return Array.from({ length: 12 }, (_, index) => ({ + bundle: { + ledgerCount: 86 + index, + prefixRoot, + priorCount: 86, + suffixRoot: index === 11 ? elevenMigrationBundleSuffixRoot(manifest.migrations) : null, + targetCount: index, + }, + state: { + activeReviewItems: 0, + currentUser: 'postgres', + databaseName: 'postgres', + finishCleanupBodySha256: index === 10 + ? manifest.terminalContract.finishPreimageSha256 + : index === 11 + ? manifest.terminalContract.finishPostimageSha256 + : index === 0 ? null : (index + 2).toString(16).repeat(64), + g014Root: (index < 3 ? 'a' : index === 3 ? 'b' : 'c').repeat(64), + ledgerRoot: index === 0 ? prefixRoot : (index + 1).toString(16).repeat(64), + ownerAuthorityReady: true, + reviewItems: 0, + reviewPolicyEnabled: false, + reviewRuns: 0, + schemaRoot: (index + 3).toString(16).repeat(64), + serverMajor: 17, + sessionUser: 'postgres', + storageRoot: (index + 4).toString(16).repeat(64), + terminalContractReady: index === 11, + }, + })); +} + +function publishSyntheticRehearsal(custodyDirectory: string) { + const admissionPath = join(custodyDirectory, 'rehearsal-admission.json'); + const admissionBytes = readFileSync(admissionPath); + const admission = JSON.parse(admissionBytes.toString('utf8')); + const admissionSha256 = sha256(admissionBytes); + const stages = stageStates(); + const rehearsalReceiptPath = join(custodyDirectory, 'rehearsal-receipt.json'); + const rehearsalJournalPath = join(custodyDirectory, 'rehearsal-journal.jsonl'); + const receipt = { + admissionSha256, + attemptId: admission.attemptId, + expiresAt: admission.expiresAt, + id: admission.id, + journalPathSha256: sha256(rehearsalJournalPath), + kind: 'successor-rehearsal-receipt', + manifestSha256: admission.manifestSha256, + observedAt, + passed: true, + preflightStateSha256: sha256(JSON.stringify(canonical(stages[0]))), + projectRef: admission.projectRef, + purpose: admission.purpose, + restoredStateSha256: sha256(JSON.stringify(canonical(stages[0]))), + schemaVersion: 1, + serverVersionNum: 170006, + sourceRevision: admission.sourceRevision, + sourceRoot: admission.sourceRoot, + stageStates: stages, + stageStatesSha256: sha256(JSON.stringify(canonical(stages))), + suffixRoot: elevenMigrationBundleSuffixRoot(manifest.migrations), + }; + const receiptBytes = canonicalBytes(receipt); + const receiptSha256 = sha256(receiptBytes); + const journal = [ + { + admissionSha256, + attemptId: admission.attemptId, + event: 'started', + manifestSha256: admission.manifestSha256, + sourceRevision: admission.sourceRevision, + sourceRoot: admission.sourceRoot, + startedAt: createdAt, + }, + { + admissionSha256, + attemptId: admission.attemptId, + code: 'SUCCESSOR_REHEARSAL_VERIFIED', + event: 'verification', + manifestSha256: admission.manifestSha256, + receiptSha256, + sourceRevision: admission.sourceRevision, + sourceRoot: admission.sourceRoot, + verifiedAt: observedAt, + }, + { + admissionSha256, + attemptId: admission.attemptId, + code: 'SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED', + event: 'publication', + manifestSha256: admission.manifestSha256, + publishedAt: '2026-10-13T08:00:15.000Z', + receiptSha256, + sourceRevision: admission.sourceRevision, + sourceRoot: admission.sourceRoot, + }, + ]; + const journalBytes = Buffer.from(`${journal.map(entry => JSON.stringify(canonical(entry))).join('\n')}\n`); + writeFileSync(rehearsalReceiptPath, receiptBytes, { mode: 0o600 }); + writeFileSync(rehearsalJournalPath, journalBytes, { mode: 0o600 }); + chmodSync(rehearsalReceiptPath, 0o600); + chmodSync(rehearsalJournalPath, 0o600); + return { + admission, + admissionSha256, + journalSha256: sha256(journalBytes), + receipt, + receiptSha256, + }; +} + +function applyInput(published: ReturnType, overrides = {}) { + return { + attemptId: '123e4567-e89b-42d3-a456-426614174101', + expiresAt, + kind: 'review-media-successor-apply-admission-input', + manifestSha256: manifestRecord.manifestSha256, + observedAt: '2026-10-13T08:00:20.000Z', + rehearsalAdmissionSha256: published.admissionSha256, + rehearsalJournalSha256: published.journalSha256, + rehearsalReceiptSha256: published.receiptSha256, + schemaVersion: 1, + sourceRevision, + ...overrides, + }; +} + +test('CLI contract accepts only the explicit rehearsal or apply producer modes', () => { + expect(parseOperatingAdmissionArguments([ + '--mode', 'rehearsal', + '--input', '/private/input.json', + '--custody-dir', '/private/custody', + ])).toEqual({ + custodyDirectory: '/private/custody', + inputPath: '/private/input.json', + mode: 'rehearsal', + }); + expect(() => parseOperatingAdmissionArguments([ + '--mode', 'ready', + '--input', '/private/input.json', + '--custody-dir', '/private/custody', + ])).toThrow('SUCCESSOR_OPERATING_ADMISSION_ARGUMENT_INVALID'); + const throwingCode = {}; + Object.defineProperty(throwingCode, 'code', { get() { throw new Error('secret'); } }); + expect(operatingAdmissionFailureResult(throwingCode)).toEqual({ + code: 'SUCCESSOR_OPERATING_ADMISSION_INPUT_INVALID', + status: 'failed', + }); +}); + +test('rehearsal producer preserves proven receipts and creates a controller-valid private admission', () => { + const prepared = prepareRehearsal(); + const admissionBytes = readFileSync(prepared.result.admissionPath); + const admission = JSON.parse(admissionBytes.toString('utf8')); + expect(prepared.result.status).toBe('prepared'); + expect(prepared.result.admissionSha256).toBe(sha256(admissionBytes)); + expect(statSync(prepared.custodyDirectory).mode & 0o077).toBe(0); + expect(statSync(prepared.result.admissionPath).mode & 0o077).toBe(0); + expect(readFileSync(join(prepared.custodyDirectory, 'source-receipt.json'))) + .toEqual(canonicalBytes(prepared.input.sourceReceipt)); + expect(readFileSync(join(prepared.custodyDirectory, 'checkout-readback.json'))) + .toEqual(canonicalBytes(prepared.input.checkoutReadback)); + expect(existsSync(prepared.result.journalPath)).toBe(false); + expect(existsSync(prepared.result.receiptPath)).toBe(false); + expect(validateSuccessorRehearsalAdmission(admission, manifestRecord, { + now: fixedNow, + receiptDirectory: prepared.custodyDirectory, + })).toEqual({ + createdAt: Date.parse(createdAt), + expiresAt: Date.parse(expiresAt), + }); +}); + +test('unknown, missing, stale, and mismatched evidence is rejected before custody creation', () => { + const cases = [ + (input: any) => { input.writerFenceReceipts.unknownFence = input.writerFenceReceipts.dashboard; }, + (input: any) => { delete input.writerFenceReceipts.dashboard; }, + (input: any) => { input.sourceReceipt.observedAt = '2026-10-13T07:59:59.999Z'; }, + (input: any) => { input.protectedMainReceipt.revision = 'a'.repeat(40); }, + (input: any) => { input.expiresAt = fixedNow.toISOString(); }, + (input: any) => { input.expiresAt = '2026-10-13T08:16:00.000Z'; }, + ]; + for (const mutate of cases) { + const root = privateRoot(); + const input = rehearsalInput(); + mutate(input); + const inputPath = join(root, 'input.json'); + const custodyDirectory = join(root, 'custody'); + writePrivateCanonical(inputPath, input); + expect(() => prepareReviewMediaRehearsalAdmission({ + custodyDirectory, + inputPath, + manifestRecord, + now: fixedNow, + })).toThrow('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + expect(existsSync(custodyDirectory)).toBe(false); + } +}); + +test('rehearsal producer never overwrites an existing custody directory', () => { + const prepared = prepareRehearsal(); + const before = sha256(readFileSync(prepared.result.admissionPath)); + expect(() => prepareReviewMediaRehearsalAdmission({ + custodyDirectory: prepared.custodyDirectory, + inputPath: prepared.inputPath, + manifestRecord, + now: fixedNow, + })).toThrow('SUCCESSOR_OPERATING_ADMISSION_OUTPUT_EXISTS'); + expect(sha256(readFileSync(prepared.result.admissionPath))).toBe(before); +}); + +test('rehearsal producer fails closed on parent, file, and custody fsync failures', () => { + for (const failAt of [1, 2, 14]) { + const root = privateRoot(); + const inputPath = join(root, `input-${failAt}.json`); + const custodyDirectory = join(root, `custody-${failAt}`); + writePrivateCanonical(inputPath, rehearsalInput()); + let fsyncCalls = 0; + expect(() => prepareReviewMediaRehearsalAdmission({ + custodyDirectory, + fsyncImpl: (descriptor: number) => { + fsyncCalls += 1; + if (fsyncCalls === failAt) throw new Error('fixture-fsync-failure'); + fsyncSync(descriptor); + }, + inputPath, + manifestRecord, + now: fixedNow, + })).toThrow('SUCCESSOR_OPERATING_ADMISSION_WRITE_FAILED'); + expect(fsyncCalls).toBe(failAt); + } +}); + +test('rehearsal producer rejects symlink and oversized inputs before custody creation', () => { + const root = privateRoot(); + const targetPath = join(root, 'target.json'); + const symlinkPath = join(root, 'symlink.json'); + const oversizedPath = join(root, 'oversized.json'); + writePrivateCanonical(targetPath, rehearsalInput()); + symlinkSync(targetPath, symlinkPath); + writeFileSync(oversizedPath, Buffer.alloc(1024 * 1024 + 1, 0x20), { mode: 0o600 }); + chmodSync(oversizedPath, 0o600); + for (const [inputPath, name] of [[symlinkPath, 'symlink'], [oversizedPath, 'oversized']]) { + const custodyDirectory = join(root, `custody-${name}`); + expect(() => prepareReviewMediaRehearsalAdmission({ + custodyDirectory, + inputPath, + manifestRecord, + now: fixedNow, + })).toThrow('SUCCESSOR_OPERATING_ADMISSION_INPUT_INVALID'); + expect(existsSync(custodyDirectory)).toBe(false); + } +}); + +test('apply producer binds the verified rehearsal artifacts without extending their time window', () => { + const prepared = prepareRehearsal(); + const published = publishSyntheticRehearsal(prepared.custodyDirectory); + const applyInputDocument = applyInput(published); + const inputPath = join(prepared.root, 'apply-input.json'); + writePrivateCanonical(inputPath, applyInputDocument); + expect(published.receipt.admissionSha256).toBe(published.admissionSha256); + expect(published.receipt.journalPathSha256) + .toBe(sha256(join(prepared.custodyDirectory, 'rehearsal-journal.jsonl'))); + expect(published.receipt.sourceRoot).toBe(manifest.sourceRoot); + expect({ + id: published.receipt.id, + kind: published.receipt.kind, + manifestSha256: published.receipt.manifestSha256, + passed: published.receipt.passed, + projectRef: published.receipt.projectRef, + purpose: published.receipt.purpose, + schemaVersion: published.receipt.schemaVersion, + serverMajor: Math.floor(published.receipt.serverVersionNum / 10_000), + sourceRevision: published.receipt.sourceRevision, + }).toEqual({ + id: manifest.id, + kind: 'successor-rehearsal-receipt', + manifestSha256: manifestRecord.manifestSha256, + passed: true, + projectRef: manifest.projectRef, + purpose: manifest.purpose, + schemaVersion: 1, + serverMajor: manifest.serverMajor, + sourceRevision, + }); + expect(published.receipt.suffixRoot).toBe(elevenMigrationBundleSuffixRoot(manifest.migrations)); + expect(published.receipt.stageStatesSha256) + .toBe(sha256(JSON.stringify(canonical(published.receipt.stageStates)))); + expect(published.receipt.preflightStateSha256) + .toBe(sha256(JSON.stringify(canonical(published.receipt.stageStates[0])))); + const result = prepareReviewMediaApplyAdmission({ + custodyDirectory: prepared.custodyDirectory, + inputPath, + manifestRecord, + now: fixedNow, + }); + const admission = JSON.parse(readFileSync(result.admissionPath, 'utf8')); + expect(admission.createdAt).toBe(createdAt); + expect(admission.expiresAt).toBe(expiresAt); + expect(admission.stageStates).toEqual(published.receipt.stageStates); + expect(existsSync(result.journalPath)).toBe(false); + expect(validateSuccessorAdmission(admission, manifestRecord, { + now: fixedNow, + receiptDirectory: prepared.custodyDirectory, + }).suffixRoot).toBe(elevenMigrationBundleSuffixRoot(manifest.migrations)); + expect(() => prepareReviewMediaApplyAdmission({ + custodyDirectory: prepared.custodyDirectory, + inputPath, + manifestRecord, + now: fixedNow, + })).toThrow('SUCCESSOR_OPERATING_ADMISSION_OUTPUT_EXISTS'); +}); + +test('apply producer rejects a relabeled or tampered rehearsal before writing an admission', () => { + for (const [name, overrides] of [ + ['extended', { expiresAt: '2026-10-13T08:11:00.000Z' }], + ['tampered', { rehearsalJournalSha256: 'f'.repeat(64) }], + ] as const) { + const prepared = prepareRehearsal(); + const published = publishSyntheticRehearsal(prepared.custodyDirectory); + const inputPath = join(prepared.root, `apply-input-${name}.json`); + writePrivateCanonical(inputPath, applyInput(published, overrides)); + expect(() => prepareReviewMediaApplyAdmission({ + custodyDirectory: prepared.custodyDirectory, + inputPath, + manifestRecord, + now: fixedNow, + })).toThrow('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + expect(existsSync(join(prepared.custodyDirectory, 'apply-admission.json'))).toBe(false); + } +}); + +test('apply producer treats a dangling reserved journal symlink as an existing output', () => { + const prepared = prepareRehearsal(); + const published = publishSyntheticRehearsal(prepared.custodyDirectory); + const inputPath = join(prepared.root, 'apply-input.json'); + writePrivateCanonical(inputPath, applyInput(published)); + symlinkSync(join(prepared.root, 'missing-target'), join(prepared.custodyDirectory, 'apply-journal.jsonl')); + expect(() => prepareReviewMediaApplyAdmission({ + custodyDirectory: prepared.custodyDirectory, + inputPath, + manifestRecord, + now: fixedNow, + })).toThrow('SUCCESSOR_OPERATING_ADMISSION_OUTPUT_EXISTS'); + expect(existsSync(join(prepared.custodyDirectory, 'apply-admission.json'))).toBe(false); +}); + +test('apply producer reads rehearsal journals through the no-follow bounded reader', () => { + for (const mode of ['symlink', 'oversized'] as const) { + const prepared = prepareRehearsal(); + const published = publishSyntheticRehearsal(prepared.custodyDirectory); + const journalPath = join(prepared.custodyDirectory, 'rehearsal-journal.jsonl'); + let journalSha256 = published.journalSha256; + if (mode === 'symlink') { + const targetPath = join(prepared.root, 'journal-target.jsonl'); + writeFileSync(targetPath, readFileSync(journalPath), { mode: 0o600 }); + chmodSync(targetPath, 0o600); + unlinkSync(journalPath); + symlinkSync(targetPath, journalPath); + } else { + const oversized = Buffer.alloc(1024 * 1024 + 1, 0x20); + writeFileSync(journalPath, oversized, { mode: 0o600 }); + chmodSync(journalPath, 0o600); + journalSha256 = sha256(oversized); + } + const inputPath = join(prepared.root, `apply-input-${mode}.json`); + writePrivateCanonical(inputPath, applyInput(published, { rehearsalJournalSha256: journalSha256 })); + expect(() => prepareReviewMediaApplyAdmission({ + custodyDirectory: prepared.custodyDirectory, + inputPath, + manifestRecord, + now: fixedNow, + })).toThrow('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + expect(existsSync(join(prepared.custodyDirectory, 'apply-admission.json'))).toBe(false); + } +}); diff --git a/apps/web/tests-unit/review-media-sql-successor-harness.test.ts b/apps/web/tests-unit/review-media-sql-successor-harness.test.ts index 453a792834..c14ae23c9b 100644 --- a/apps/web/tests-unit/review-media-sql-successor-harness.test.ts +++ b/apps/web/tests-unit/review-media-sql-successor-harness.test.ts @@ -5,13 +5,133 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { + createFixtureMembership, createOwnedDatabase, + dropFixtureMembership, dropOwnedDatabase, inspectRetainedProof, } from '../performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs'; const retainedProofRepositoryPath = 'apps/web/performance/review-db-successor-20261012/pg17-local-replay.json'; const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex'); +const fixtureParentRole = 'review_successor_fixture_parent_20261012'; +const fixtureMemberRole = 'review_successor_fixture_member_20261012'; +const fixtureOwnerPrefix = 'tzudong-review-successor-fixture'; + +type FixtureRoleState = { + roles: Record[]; + memberships: Record[]; +}; + +const emptyFixtureRoleState = (): FixtureRoleState => ({ roles: [], memberships: [] }); + +function ownedFixtureRoleState(token: string): FixtureRoleState { + const common = { + rolcanlogin: false, + rolsuper: false, + rolcreatedb: false, + rolcreaterole: false, + rolreplication: false, + rolbypassrls: false, + rolconnlimit: -1, + rolvaliduntil: null, + rolconfig: null, + }; + return { + roles: [ + { + ...common, + rolname: fixtureMemberRole, + rolinherit: true, + owner_marker: `${fixtureOwnerPrefix}:${token}:member`, + }, + { + ...common, + rolname: fixtureParentRole, + rolinherit: false, + owner_marker: `${fixtureOwnerPrefix}:${token}:parent`, + }, + ], + memberships: [{ + parent: fixtureParentRole, + member: fixtureMemberRole, + grantor: 'supabase_admin', + admin_option: false, + inherit_option: true, + set_option: true, + }], + }; +} + +function fixtureOwnership() { + return { + database: false, + fixtureMembership: false, + fixtureMembershipAttempted: false, + fixtureMembershipToken: null, + fixtureMembershipBaseline: null, + }; +} + +type DatabaseMetadata = { + name: string; + owner: string; + encoding: string; + collate: string; + ctype: string; + localeProvider: string; + allowConnections: boolean; + isTemplate: boolean; +}; + +type DatabaseState = { + target: DatabaseMetadata | null; + template: DatabaseMetadata; +}; + +const templateDatabaseMetadata: DatabaseMetadata = { + name: 'ranking_clone', + owner: 'postgres', + encoding: 'UTF8', + collate: 'C', + ctype: 'C', + localeProvider: 'c', + allowConnections: true, + isTemplate: false, +}; + +function databaseNameFromSql(sql: string) { + const queryMatch = sql.match(/target_database\.datname='([^']+)'/); + const commandMatch = sql.match(/(?:CREATE|DROP) DATABASE ([a-z0-9_]+)/); + const databaseName = queryMatch?.[1] || commandMatch?.[1]; + if (!databaseName) throw new Error(`database name missing from SQL: ${sql.slice(0, 80)}`); + return databaseName; +} + +function databaseState(databaseName: string, targetOwner = 'postgres'): DatabaseState { + return { + target: { + ...templateDatabaseMetadata, + name: databaseName, + owner: targetOwner, + isTemplate: false, + }, + template: { ...templateDatabaseMetadata }, + }; +} + +function emptyDatabaseState(): DatabaseState { + return { target: null, template: { ...templateDatabaseMetadata } }; +} + +function databaseOwnership() { + return { + database: false, + databaseAttempted: false, + databaseName: null, + databaseBaseline: null, + }; +} test('retained proof is checksum-verified and distinguished from the current harness source', () => { const manifestBytes = readFileSync(new URL('../../../.github/review-media-sql-successor.v1.json', import.meta.url)); @@ -79,16 +199,21 @@ for (const [label, failingDatabaseCall, expectedCode] of [ ['fixture', 7, 'fixture failed'], ] as const) { test(`owned database is cleaned after ${label} SQL fails`, () => { - const ownership = { database: false }; + const ownership = databaseOwnership(); let databaseExists = false; + let databaseName = ''; let databaseCalls = 0; let drops = 0; const dependencies = { - scalarImpl: () => (databaseExists ? '1' : '0'), + scalarImpl: (_database: string, sql: string) => { + const requestedName = databaseNameFromSql(sql); + return JSON.stringify(databaseExists ? databaseState(requestedName) : emptyDatabaseState()); + }, readFileImpl: () => Buffer.from('-- predecessor fixture'), psqlImpl: (database: string, sql: string | Buffer) => { const statement = sql.toString(); if (database === 'postgres' && statement.startsWith('CREATE DATABASE ')) { + databaseName = databaseNameFromSql(statement); databaseExists = true; return {}; } @@ -120,14 +245,15 @@ for (const [label, failingDatabaseCall, expectedCode] of [ } test('pre-existing database is never acquired or dropped', () => { - const ownership = { database: false }; + const ownership = databaseOwnership(); let databaseExists = true; let scalarCalls = 0; let psqlCalls = 0; const dependencies = { - scalarImpl: () => { + scalarImpl: (_database: string, sql: string) => { scalarCalls += 1; - return databaseExists ? '1' : '0'; + const databaseName = databaseNameFromSql(sql); + return JSON.stringify(databaseExists ? databaseState(databaseName) : emptyDatabaseState()); }, readFileImpl: () => Buffer.from(''), psqlImpl: () => { @@ -144,3 +270,339 @@ test('pre-existing database is never acquired or dropped', () => { expect(scalarCalls).toBe(1); expect(psqlCalls).toBe(0); }); + +test('database CREATE acknowledgement loss is reconciled without replay and cleaned by exact ownership', () => { + const ownership = databaseOwnership(); + let state = emptyDatabaseState(); + let createCalls = 0; + let dropCalls = 0; + const dependencies = { + scalarImpl: () => JSON.stringify(state), + readFileImpl: () => Buffer.from('-- predecessor fixture'), + psqlImpl: (database: string, sql: string | Buffer) => { + const statement = sql.toString(); + if (database === 'postgres' && statement.startsWith('CREATE DATABASE ')) { + createCalls += 1; + state = databaseState(databaseNameFromSql(statement)); + throw new Error('response lost after CREATE commit'); + } + if (database === 'postgres' && statement.startsWith('DROP DATABASE ')) { + dropCalls += 1; + state = emptyDatabaseState(); + return {}; + } + return {}; + }, + }; + + expect(createOwnedDatabase(ownership, dependencies)).toBe(true); + expect(ownership).toMatchObject({ + database: true, + databaseAttempted: true, + databaseCreationAcknowledged: false, + databaseCreationReconciled: true, + }); + expect(ownership.databaseName).toMatch(/^review_successor_local_[0-9a-f]{32}$/); + expect(createCalls).toBe(1); + expect(dropOwnedDatabase(ownership, dependencies)).toBe(true); + expect(createCalls).toBe(1); + expect(dropCalls).toBe(1); + expect(state).toEqual(emptyDatabaseState()); + expect(ownership.database).toBe(false); + expect(ownership.databaseAttempted).toBe(false); +}); + +test('cleanup re-reads and removes the exact owned database after the first CREATE readback fails', () => { + const ownership = databaseOwnership(); + let state = emptyDatabaseState(); + let readbacks = 0; + let createCalls = 0; + let dropCalls = 0; + const dependencies = { + scalarImpl: () => { + readbacks += 1; + if (readbacks === 2) throw new Error('readback transport failed'); + return JSON.stringify(state); + }, + readFileImpl: () => Buffer.from('-- predecessor fixture'), + psqlImpl: (database: string, sql: string | Buffer) => { + const statement = sql.toString(); + if (database === 'postgres' && statement.startsWith('CREATE DATABASE ')) { + createCalls += 1; + state = databaseState(databaseNameFromSql(statement)); + throw new Error('response lost after CREATE commit'); + } + if (database === 'postgres' && statement.startsWith('DROP DATABASE ')) { + dropCalls += 1; + state = emptyDatabaseState(); + return {}; + } + throw new Error('unexpected SQL'); + }, + }; + + expect(() => createOwnedDatabase(ownership, dependencies)) + .toThrow('LOCAL_REHEARSAL_DATABASE_READBACK_FAILED'); + expect(ownership).toMatchObject({ database: false, databaseAttempted: true }); + expect(dropOwnedDatabase(ownership, dependencies)).toBe(true); + expect(createCalls).toBe(1); + expect(dropCalls).toBe(1); + expect(readbacks).toBe(4); + expect(state).toEqual(emptyDatabaseState()); +}); + +test('cleanup refuses a database whose exact owner metadata does not match', () => { + const ownership = databaseOwnership(); + let state = emptyDatabaseState(); + let dropCalls = 0; + const dependencies = { + scalarImpl: () => JSON.stringify(state), + readFileImpl: () => Buffer.from('-- predecessor fixture'), + psqlImpl: (database: string, sql: string | Buffer) => { + const statement = sql.toString(); + if (database === 'postgres' && statement.startsWith('CREATE DATABASE ')) { + state = databaseState(databaseNameFromSql(statement)); + return {}; + } + if (database === 'postgres' && statement.startsWith('DROP DATABASE ')) { + dropCalls += 1; + state = emptyDatabaseState(); + return {}; + } + return {}; + }, + }; + + createOwnedDatabase(ownership, dependencies); + state = databaseState(String(ownership.databaseName), 'foreign_owner'); + expect(() => dropOwnedDatabase(ownership, dependencies)) + .toThrow('LOCAL_REHEARSAL_DATABASE_OWNERSHIP_UNPROVEN'); + expect(dropCalls).toBe(0); + expect(state.target?.owner).toBe('foreign_owner'); +}); + +test('fixture role creation acknowledgement loss is reconciled and exact owned roles are cleaned', () => { + const token = 'a'.repeat(32); + const ownership = fixtureOwnership(); + let state = emptyFixtureRoleState(); + const statements: string[] = []; + const dependencies = { + scalarImpl: () => JSON.stringify(state), + psqlImpl: (_database: string, sql: string) => { + statements.push(sql); + if (sql.includes(`CREATE ROLE ${fixtureParentRole}`)) { + state = ownedFixtureRoleState(token); + throw new Error('response lost after commit'); + } + if (sql.includes(`DROP ROLE ${fixtureMemberRole}`)) { + state = emptyFixtureRoleState(); + throw new Error('response lost after drop commit'); + } + throw new Error('unexpected SQL'); + }, + generateToken: () => token, + }; + + expect(createFixtureMembership(ownership, dependencies)).toBe(true); + expect(ownership).toMatchObject({ + fixtureMembership: true, + fixtureMembershipToken: token, + fixtureMembershipCreationAcknowledged: false, + fixtureMembershipCreationReconciled: true, + }); + expect(dropFixtureMembership(ownership, dependencies)).toBe(true); + expect(ownership).toMatchObject({ + fixtureMembership: false, + fixtureMembershipDropped: true, + fixtureMembershipRemovalAcknowledged: false, + }); + expect(state).toEqual(emptyFixtureRoleState()); + expect(statements).toHaveLength(2); + expect(statements[0]).toContain( + `COMMENT ON ROLE ${fixtureParentRole} IS '${fixtureOwnerPrefix}:${token}:parent'`, + ); + expect(statements[0]).toContain( + `COMMENT ON ROLE ${fixtureMemberRole} IS '${fixtureOwnerPrefix}:${token}:member'`, + ); + expect(statements[1]).toContain(`DROP ROLE ${fixtureMemberRole}`); + expect(statements[1]).toContain(`DROP ROLE ${fixtureParentRole}`); +}); + +test('cleanup re-reads exact owned roles when the first post-create readback fails', () => { + const token = '4'.repeat(32); + const ownership = fixtureOwnership(); + let state = emptyFixtureRoleState(); + let readbacks = 0; + const statements: string[] = []; + const dependencies = { + scalarImpl: () => { + readbacks += 1; + if (readbacks === 2) throw new Error('readback transport failed'); + return JSON.stringify(state); + }, + psqlImpl: (_database: string, sql: string) => { + statements.push(sql); + if (sql.includes(`CREATE ROLE ${fixtureParentRole}`)) { + state = ownedFixtureRoleState(token); + throw new Error('response lost after create commit'); + } + if (sql.includes(`DROP ROLE ${fixtureMemberRole}`)) { + state = emptyFixtureRoleState(); + return {}; + } + throw new Error('unexpected SQL'); + }, + generateToken: () => token, + }; + + expect(() => createFixtureMembership(ownership, dependencies)) + .toThrow('LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_FAILED'); + expect(ownership).toMatchObject({ + fixtureMembership: false, + fixtureMembershipAttempted: true, + fixtureMembershipToken: token, + fixtureMembershipBaseline: emptyFixtureRoleState(), + }); + expect(dropFixtureMembership(ownership, dependencies)).toBe(true); + expect(ownership).toMatchObject({ + fixtureMembership: false, + fixtureMembershipAttempted: false, + fixtureMembershipDropped: true, + }); + expect(state).toEqual(emptyFixtureRoleState()); + expect(readbacks).toBe(4); + expect(statements).toHaveLength(2); +}); + +test('fixture role creation failure without owned readback never authorizes cleanup', () => { + const ownership = fixtureOwnership(); + let state = emptyFixtureRoleState(); + const statements: string[] = []; + const dependencies = { + scalarImpl: () => JSON.stringify(state), + psqlImpl: (_database: string, sql: string) => { + statements.push(sql); + throw new Error('transaction failed before commit'); + }, + generateToken: () => 'b'.repeat(32), + }; + + expect(() => createFixtureMembership(ownership, dependencies)) + .toThrow('LOCAL_REHEARSAL_FIXTURE_ROLE_CREATION_UNCONFIRMED'); + expect(ownership.fixtureMembership).toBe(false); + expect(ownership.fixtureMembershipAttempted).toBe(false); + expect(ownership.fixtureMembershipToken).toBe('b'.repeat(32)); + expect(dropFixtureMembership(ownership, dependencies)).toBe(false); + expect(statements).toHaveLength(1); + expect(statements.some(statement => statement.includes('DROP ROLE'))).toBe(false); + expect(state).toEqual(emptyFixtureRoleState()); +}); + +test('pre-existing fixture role collision is preserved and never acquired for cleanup', () => { + const ownership = fixtureOwnership(); + const state: FixtureRoleState = { + roles: [{ + ...ownedFixtureRoleState('c'.repeat(32)).roles[0], + owner_marker: 'foreign-owner', + }], + memberships: [], + }; + let psqlCalls = 0; + const dependencies = { + scalarImpl: () => JSON.stringify(state), + psqlImpl: () => { + psqlCalls += 1; + return {}; + }, + generateToken: () => 'd'.repeat(32), + }; + + expect(() => createFixtureMembership(ownership, dependencies)) + .toThrow('LOCAL_REHEARSAL_FIXTURE_ROLE_EXISTS'); + expect(dropFixtureMembership(ownership, dependencies)).toBe(false); + expect(psqlCalls).toBe(0); + expect(state.roles[0].owner_marker).toBe('foreign-owner'); +}); + +test('uncertain create collision with another owner is not guessed or removed', () => { + const ownership = fixtureOwnership(); + let state = emptyFixtureRoleState(); + const statements: string[] = []; + const dependencies = { + scalarImpl: () => JSON.stringify(state), + psqlImpl: (_database: string, sql: string) => { + statements.push(sql); + state = ownedFixtureRoleState('e'.repeat(32)); + throw new Error('ambiguous create response'); + }, + generateToken: () => 'f'.repeat(32), + }; + + expect(() => createFixtureMembership(ownership, dependencies)) + .toThrow('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_UNPROVEN'); + expect(ownership.fixtureMembership).toBe(false); + expect(ownership.fixtureMembershipAttempted).toBe(false); + expect(ownership.fixtureMembershipToken).toBe('f'.repeat(32)); + expect(dropFixtureMembership(ownership, dependencies)).toBe(false); + expect(statements).toHaveLength(1); + expect(statements.some(statement => statement.includes('DROP ROLE'))).toBe(false); + expect(state).toEqual(ownedFixtureRoleState('e'.repeat(32))); +}); + +test('cleanup refuses roles whose ownership marker changed after creation', () => { + const token = '1'.repeat(32); + const ownership = fixtureOwnership(); + let state = emptyFixtureRoleState(); + const statements: string[] = []; + const dependencies = { + scalarImpl: () => JSON.stringify(state), + psqlImpl: (_database: string, sql: string) => { + statements.push(sql); + state = ownedFixtureRoleState(token); + return {}; + }, + generateToken: () => token, + }; + + createFixtureMembership(ownership, dependencies); + state = ownedFixtureRoleState('2'.repeat(32)); + expect(() => dropFixtureMembership(ownership, dependencies)) + .toThrow('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_UNPROVEN'); + expect(ownership.fixtureMembership).toBe(true); + expect(statements).toHaveLength(1); + expect(statements.some(statement => statement.includes('DROP ROLE'))).toBe(false); + expect(state).toEqual(ownedFixtureRoleState('2'.repeat(32))); +}); + +test('cleanup refuses to remove owned roles after an out-of-scope membership is added', () => { + const token = '3'.repeat(32); + const ownership = fixtureOwnership(); + let state = emptyFixtureRoleState(); + const statements: string[] = []; + const dependencies = { + scalarImpl: () => JSON.stringify(state), + psqlImpl: (_database: string, sql: string) => { + statements.push(sql); + state = ownedFixtureRoleState(token); + return {}; + }, + generateToken: () => token, + }; + + createFixtureMembership(ownership, dependencies); + state = ownedFixtureRoleState(token); + state.memberships.push({ + parent: 'foreign_parent_role', + member: fixtureMemberRole, + grantor: 'postgres', + admin_option: false, + inherit_option: true, + set_option: true, + }); + expect(() => dropFixtureMembership(ownership, dependencies)) + .toThrow('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_UNPROVEN'); + expect(ownership.fixtureMembership).toBe(true); + expect(statements).toHaveLength(1); + expect(statements.some(statement => statement.includes('DROP ROLE'))).toBe(false); + expect(state.memberships).toHaveLength(2); +}); diff --git a/apps/web/tests-unit/review-media-sql-successor.test.ts b/apps/web/tests-unit/review-media-sql-successor.test.ts index 00941884d7..442030e158 100644 --- a/apps/web/tests-unit/review-media-sql-successor.test.ts +++ b/apps/web/tests-unit/review-media-sql-successor.test.ts @@ -770,6 +770,199 @@ test('pending receipt full-write then fsync-failure records failure without cano })).toThrow('SUCCESSOR_ADMISSION_INVALID'); }); +test('pending receipt storage delay cannot verify after the admission expires', () => { + const document = rehearsalAdmission(); + const expiresAt = new Date(fixedNow.getTime() + 1_000); + document.expiresAt = expiresAt.toISOString(); + const files = rehearsalCustodyFiles(document); + let clock = fixedNow; + let calls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => clock, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + writePendingReceiptImpl: (path: string, receipt: any) => { + const descriptor = openSync(path, 'wx', 0o600); + const bytes = canonicalBytes(receipt); + expect(writeSync(descriptor, bytes)).toBe(bytes.length); + fsyncSync(descriptor); + closeSync(descriptor); + clock = expiresAt; + }, + })).toThrow('SUCCESSOR_ADMISSION_EXPIRED'); + expect(existsSync(files.pendingReceiptPath)).toBe(true); + expect(existsSync(files.receiptPath)).toBe(false); + const journal = readFileSync(files.journalPath, 'utf8'); + expect(journal).toContain('SUCCESSOR_ADMISSION_EXPIRED'); + expect(journal).not.toContain('SUCCESSOR_REHEARSAL_VERIFIED'); + expect(journal).not.toContain('SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED'); +}); + +test('expiry terminal audit failure preserves the bounded rehearsal outcome', () => { + const document = rehearsalAdmission(); + const expiresAt = new Date(fixedNow.getTime() + 1_000); + document.expiresAt = expiresAt.toISOString(); + const files = rehearsalCustodyFiles(document); + let clock = fixedNow; + let calls = 0; + let observed: any; + try { + runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => clock, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + writePendingReceiptImpl: (path: string, receipt: any) => { + writeFileSync(path, canonicalBytes(receipt), { flag: 'wx', mode: 0o600 }); + clock = expiresAt; + }, + appendJournalImpl: () => { throw new Error('fixture expiry audit unavailable'); }, + }); + } catch (error) { + observed = successorFailureResult(error); + } + expect(observed).toEqual({ + code: 'SUCCESSOR_JOURNAL_APPEND_FAILED', + operationCode: 'SUCCESSOR_ADMISSION_EXPIRED', + status: 'failed', + }); + expect(existsSync(files.receiptPath)).toBe(false); +}); + +test('verification journal storage delay cannot publish after the admission expires', () => { + const document = rehearsalAdmission(); + const expiresAt = new Date(fixedNow.getTime() + 1_000); + document.expiresAt = expiresAt.toISOString(); + const files = rehearsalCustodyFiles(document); + let clock = fixedNow; + let calls = 0; + let appendCalls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => clock, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + appendJournalImpl: (descriptor: number, entry: any, options: any) => { + appendCalls += 1; + const result = appendJournal(descriptor, entry, options); + if (appendCalls === 1) clock = expiresAt; + return result; + }, + })).toThrow('SUCCESSOR_ADMISSION_EXPIRED'); + expect(appendCalls).toBe(2); + expect(existsSync(files.pendingReceiptPath)).toBe(true); + expect(existsSync(files.receiptPath)).toBe(false); + const journal = readFileSync(files.journalPath, 'utf8'); + expect(journal).toContain('SUCCESSOR_REHEARSAL_VERIFIED'); + expect(journal).toContain('SUCCESSOR_ADMISSION_EXPIRED'); + expect(journal).not.toContain('SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED'); +}); + +test('canonical publication storage delay cannot record an expired passed result', () => { + const document = rehearsalAdmission(); + const expiresAt = new Date(fixedNow.getTime() + 1_000); + document.expiresAt = expiresAt.toISOString(); + const files = rehearsalCustodyFiles(document); + let clock = fixedNow; + let calls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => clock, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + publishReceiptImpl: (pendingPath: string, receiptPath: string) => { + linkSync(pendingPath, receiptPath); + const directoryDescriptor = openSync(files.directory, 'r'); + fsyncSync(directoryDescriptor); + closeSync(directoryDescriptor); + clock = expiresAt; + }, + })).toThrow('SUCCESSOR_ADMISSION_EXPIRED'); + expect(readFileSync(files.receiptPath)).toEqual(readFileSync(files.pendingReceiptPath)); + const journal = readFileSync(files.journalPath, 'utf8'); + expect(journal).toContain('SUCCESSOR_REHEARSAL_VERIFIED'); + expect(journal).toContain('SUCCESSOR_ADMISSION_EXPIRED'); + expect(journal).not.toContain('SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED'); +}); + +test('publication journal completion cannot return passed after the admission expires', () => { + const document = rehearsalAdmission(); + const expiresAt = new Date(fixedNow.getTime() + 1_000); + document.expiresAt = expiresAt.toISOString(); + const files = rehearsalCustodyFiles(document); + let clock = fixedNow; + let calls = 0; + let appendCalls = 0; + expect(() => runReviewMediaSuccessorRehearsal({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + receiptPath: files.receiptPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => clock, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => record, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: () => { + calls += 1; + return calls === 2 ? rehearsalStageOutput() : rehearsalReadback(); + }, + appendJournalImpl: (descriptor: number, entry: any, options: any) => { + appendCalls += 1; + const result = appendJournal(descriptor, entry, options); + if (appendCalls === 2) clock = expiresAt; + return result; + }, + })).toThrow('SUCCESSOR_ADMISSION_EXPIRED'); + expect(appendCalls).toBe(2); + expect(readFileSync(files.receiptPath)).toEqual(readFileSync(files.pendingReceiptPath)); + const journal = readFileSync(files.journalPath, 'utf8').trim().split('\n').map(JSON.parse); + expect(journal).toHaveLength(3); + expect(journal[2].code).toBe('SUCCESSOR_REHEARSAL_PUBLICATION_CONFIRMED'); +}); + test('prepublication interruption leaves verified journal and pending receipt apply-invalid', () => { const files = rehearsalCustodyFiles(); let calls = 0; @@ -1188,6 +1381,105 @@ test('success path performs preflight, rechecks rollback and commits all eleven expect(readFileSync(files.journalPath, 'utf8')).toContain('SUCCESSOR_COMMITTED'); }); +for (const failureAt of ['fsync', 'close'] as const) { + test(`committed terminal audit ${failureAt} failure preserves the committed outcome`, () => { + const files = custodyFiles(); + const plan = compileSuccessorPlan(executableRecord, files.document); + const calls: string[] = []; + let observed: any; + try { + observed = runReviewMediaSuccessor({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => executableRecord, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: (_url, sql) => { + calls.push(sql); + return JSON.stringify(calls.length === 1 + ? plan.priorReadback.expected + : plan.finalReadback.expected); + }, + appendJournalImpl: (descriptor: number, entry: any) => appendJournal(descriptor, entry, { + fsyncImpl: (fd: number) => { + if (failureAt === 'fsync') throw new Error('fixture committed audit sync failure'); + fsyncSync(fd); + }, + closeImpl: () => { throw new Error('fixture committed audit close acknowledgement loss'); }, + }), + }); + } catch (error) { + observed = successorFailureResult(error); + } + if (failureAt === 'fsync') { + expect(observed).toEqual({ + code: 'SUCCESSOR_JOURNAL_APPEND_FAILED', + operationCode: 'SUCCESSOR_COMMITTED', + status: 'failed', + }); + } else { + expect(observed).toMatchObject({ + code: 'SUCCESSOR_COMMITTED', + status: 'committed', + terminalAuditReadback: 'reconciled', + }); + } + expect(calls).toHaveLength(2); + expect(calls[1].match(/INSERT INTO supabase_migrations\.schema_migrations/g)).toHaveLength(11); + const journal = readFileSync(files.journalPath, 'utf8').trim().split('\n').map(JSON.parse); + expect(journal).toHaveLength(2); + expect(journal[1].code).toBe('SUCCESSOR_COMMITTED'); + }); +} + +test('committed terminal audit readback requires both durability and exact bytes', () => { + const files = custodyFiles(); + const plan = compileSuccessorPlan(executableRecord, files.document); + const calls: string[] = []; + let observed: any; + try { + runReviewMediaSuccessor({ + admissionPath: files.admissionPath, + journalPath: files.journalPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => executableRecord, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => rollbackReadback(), + runPsqlImpl: (_url, sql) => { + calls.push(sql); + return JSON.stringify(calls.length === 1 + ? plan.priorReadback.expected + : plan.finalReadback.expected); + }, + appendJournalImpl: () => { + const error: Error & { publicationAuditReadbackEligible?: boolean } = new Error( + 'fixture durable acknowledgement without terminal bytes', + ); + error.publicationAuditReadbackEligible = true; + throw error; + }, + }); + } catch (error) { + observed = successorFailureResult(error); + } + expect(observed).toEqual({ + code: 'SUCCESSOR_JOURNAL_APPEND_FAILED', + operationCode: 'SUCCESSOR_COMMITTED', + status: 'failed', + }); + expect(calls).toHaveLength(2); + const journal = readFileSync(files.journalPath, 'utf8').trim().split('\n').map(JSON.parse); + expect(journal).toHaveLength(1); + expect(journal[0].event).toBe('started'); +}); + test('concurrent rollback deployment change rejects before journal or mutation', () => { const files = custodyFiles(); const plan = compileSuccessorPlan(executableRecord, files.document); diff --git a/docs/operations/review-media-operating-admission-20261013.md b/docs/operations/review-media-operating-admission-20261013.md new file mode 100644 index 0000000000..fd7d42c60d --- /dev/null +++ b/docs/operations/review-media-operating-admission-20261013.md @@ -0,0 +1,24 @@ +# Review-media 운영 입력 준비 + +`apps/web/scripts/review-media-operating-admission.mjs`는 별도 수집기가 제공한 근거 파일을 검증하고 기존 적용기의 private admission 형식으로 변환한다. GitHub·Vercel·DB를 조회하거나 SQL을 실행하지 않는다. 형식·해시 검증 성공은 입력에 적힌 외부 상태를 직접 확인했다는 뜻이 아니다. + +이 도구는 호출자가 제공한 JSON의 관측 출처를 인증하지 않는다. 실제 관측을 수집하는 adapter의 출처 검증 없이 결과를 운영 승인으로 사용하지 않는다. 합성 단위 검사의 receipt·journal은 형식 검증용이며 운영 증빙으로 사용하지 않는다. + +## 입력과 실행 순서 + +1. 보호된 `main`의 깨끗한 detached checkout, 현재 production rollback 대상, manifest의 writer fence 7개를 실제로 확인한다. 실행 창과 원래 `observedAt`을 포함한 canonical JSON 입력을 저장소 밖 private 디렉터리에 저장한다. 프로바이더 원문·인증 정보는 입력에 넣지 않는다. +2. `--mode rehearsal --input --custody-dir `로 리허설 admission을 만든다. 입력은 schema, source revision/root, manifest hash, checkout readback, source/protected-main/rollback receipt와 writer-fence receipt 전체를 포함해야 한다. +3. 준비된 admission을 기존 `review-media-sql-rehearsal.mjs`에 전달한다. 기존 적용기는 source와 rollback을 다시 읽고 DB 대상·중지 조건을 확인한 뒤 rollback-only 리허설을 수행한다. 이 생성기만 실행해서는 리허설 receipt가 생기지 않는다. +4. 성공한 리허설의 admission·receipt·3행 journal SHA와 원래 만료 시각을 결속한 apply 입력을 준비한다. `--mode apply --input --custody-dir `로 apply admission을 만든다. +5. 기존 적용기의 launch policy, 보호 source, 실제 rollback과 writer-fence 근거가 충족된 경우에만 `review-media-sql-successor.mjs`를 실행한다. 출력 존재·실패·응답 유실은 기존 산출물을 확인한 뒤 처리하며, 파일을 지워 같은 작업을 자동 재시도하지 않는다. + +두 mode 모두 Node 24로 `apps/web/scripts/review-media-operating-admission.mjs`를 실행한다. 성공 출력은 준비한 경로·SHA·만료 시각이며, 운영 적용 완료 영수증이 아니다. + +## 보존 조건 + +- 전체 창은 최대 15분이다. apply 준비는 rehearsal의 `createdAt`·`expiresAt`을 그대로 유지하며 창을 연장하지 않는다. 만료됐으면 실제 상태를 새로 확인하고 새로운 시도 전체를 준비해야 한다. +- 적용 admission의 stage states는 검증된 rehearsal receipt에서만 가져온다. 임의 기대값이나 로컬 fixture 값을 운영 결과로 승격하지 않는다. +- 파일은 배타적으로 생성한다. 기존 custody, admission, journal을 덮거나 삭제하지 않는다. private 경로와 파일·디렉터리 저장 검증 실패 시 준비 성공을 반환하지 않는다. +- 입력을 수집하는 운영 adapter와 live 인증은 별도다. 저장소에 Vercel 토큰이 없다는 사실만으로 새 토큰 생성·저장 권한을 추정하지 않는다. 현재 manifest의 `held` 상태를 이 도구가 변경하지 않는다. + +현재 통과한 PG17.6 proof는 controller와 SQL의 격리 재생이다. 이 생성기의 검증은 별도 단위 검사로 기록하며 hosted DB 적용·Storage 물리 삭제·운영 성능 증거로 합산하지 않는다. From f489e438b1c0d986510d1fc013f9c98459f9f622 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Tue, 13 Oct 2026 17:33:36 +0900 Subject: [PATCH 07/14] Bind review-media expiry, attempt identity, and owned cleanup evidence --- .github/review-media-sql-successor.v1.json | 2 +- .../review-db-rehearsal-20261012/README.md | 35 ++- .../review-db-rehearsal-20261012/SHA256SUMS | 21 +- .../SHA256SUMS-0833ed81-historical | 20 ++ .../SHA256SUMS.sha256 | 2 +- .../pg17-reviewed-boundaries-20261013.json | 72 +++++ ...ned-current-proof-0833ed81-historical.json | 6 + .../retained-current-proof.json | 6 + ...boundaries-unit-verification-20261013.json | 89 ++++++ .../verify-review-media-sql-rehearsal.mjs | 254 +++++++++++++++-- .../review-db-successor-20261012/README.md | 14 + .../review-db-successor-20261012/SHA256SUMS | 14 +- .../SHA256SUMS-0833ed81-historical | 13 + .../SHA256SUMS.sha256 | 2 +- ...7-fixture-ownership-reviewed-20261013.json | 61 ++++ .../verify-fixture-ownership.mjs | 253 +++++++++-------- .../verify-review-media-sql-successor.mjs | 59 ++-- .../SHA256SUMS | 5 +- .../SHA256SUMS-0833ed81-historical | 5 + .../SHA256SUMS.sha256 | 2 +- .../review-media-operating-admission.mjs | 2 +- .../scripts/review-media-sql-successor.mjs | 49 +++- ...w-media-fixture-ownership-verifier.test.ts | 39 +++ .../review-media-operating-admission.test.ts | 46 +++ ...eview-media-sql-rehearsal-verifier.test.ts | 268 +++++++++++++++--- ...review-media-sql-successor-harness.test.ts | 58 ++++ .../review-media-sql-successor.test.ts | 86 ++++++ 27 files changed, 1249 insertions(+), 234 deletions(-) create mode 100644 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-0833ed81-historical create mode 100644 apps/web/performance/review-db-rehearsal-20261012/pg17-reviewed-boundaries-20261013.json create mode 100644 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof-0833ed81-historical.json create mode 100644 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json create mode 100644 apps/web/performance/review-db-rehearsal-20261012/reviewed-boundaries-unit-verification-20261013.json create mode 100644 apps/web/performance/review-db-successor-20261012/SHA256SUMS-0833ed81-historical create mode 100644 apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-reviewed-20261013.json create mode 100644 apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-0833ed81-historical create mode 100644 apps/web/tests-unit/review-media-fixture-ownership-verifier.test.ts diff --git a/.github/review-media-sql-successor.v1.json b/.github/review-media-sql-successor.v1.json index 810888e14e..557b276779 100644 --- a/.github/review-media-sql-successor.v1.json +++ b/.github/review-media-sql-successor.v1.json @@ -309,7 +309,7 @@ "toolchain": [ { "path": "apps/web/scripts/review-media-sql-successor.mjs", - "sha256": "dcf552697251c2f10d859c7a2f56937a0f6e92ee9467e5b919807e498eff1d49" + "sha256": "5d61808b45661d412937fbfdfd3bfb75b1cf92c7c285e4d8ad061a2f667c3c0b" }, { "path": "apps/web/scripts/review-media-sql-rehearsal.mjs", diff --git a/apps/web/performance/review-db-rehearsal-20261012/README.md b/apps/web/performance/review-db-rehearsal-20261012/README.md index 3d4e20dbcd..35a4bb422e 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/README.md +++ b/apps/web/performance/review-db-rehearsal-20261012/README.md @@ -24,14 +24,16 @@ and durable publication protocol. Operating application remains unperformed. - A failed terminal audit preserves its bounded operation code alongside the journal failure. Arbitrary diagnostics and throwing getters cannot escape the bounded failure JSON. -- Both SQL paths check expiry after acquiring the advisory lock. Rehearsal also - checks before each migration; apply checks once after the lock. These are not - claims that all eleven migrations finish before the admission expiry. +- Both SQL paths check expiry after acquiring the advisory lock. Apply arms + PostgreSQL 17 transaction_timeout against the original absolute deadline and + checks after each migration and after the final readback, before COMMIT. The + receipt timestamps use the original admitted window rather than a 30-second + execution limit; 30 seconds remains only a current-clock skew allowance. ## Current verification -Pinned Node 24 and Bun 1.4.0 ran five affected suites: **81 passed, 0 failed, -450 assertions**. The checks include publication fsync/close failures, five +Pinned Node 24 and Bun 1.4.0 verified six affected suites across targeted runs: **94 passed, 0 failed, +518 assertions**. The checks include publication fsync/close failures, five pre-mutation durability failures, expiry plus audit failure, tampered/orphan journals, retained evidence, private admission formatting, database/role ownership and lost acknowledgements. Filesystem expiry is rechecked before verification, @@ -52,12 +54,12 @@ The verifier explicitly checks `public.profiles` and `public.reviews` are empty; it does not claim a universal personal-data census. Its synthetic ledger has 86 entries, not an operating ledger readback. -- Current proof: `pg17-terminal-boundaries-20261013.json` -- Proof SHA-256: `0cfc9c10ff7b70c606f6eeef33fff8dbd14636dc15b7c963925097454ee454a2` -- Manifest SHA-256: `ae764bb760102a6d7d066be669c85248feed1823eafded4bf8c246a062090a8c` -- Toolchain root: `a0eeeb623c80b8a5e416b84397bdb07e4bd65c670d98b70707cc816d37e9a18b` +- Current proof (selector-bound): `pg17-reviewed-boundaries-20261013.json` +- Proof SHA-256: `9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076` +- Manifest SHA-256: `23bf7e7d7e9e5d7962021dd3ec860e87b47d07e119ba0e39de2e423a0a601f4e` +- Toolchain root: `579f315ba6285ea7ccc562cf2cf8949203a0a3289e4de5f82be1687ddea4170b` - Source root: `61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544` -- Verifier SHA-256: `8088aa57f39045e2bf93cba084711119395098d053318b70aaa676b5c13fea26` +- Verifier SHA-256: `f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431` - Compiled SQL SHA-256: `285d0c859117006646389dbd2a22de9aef1c0b97bc796a9e4d4448a8a232b093` ## Reproduce without overwriting evidence @@ -69,8 +71,10 @@ From the repository root: apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs ``` -The existing proof is checksum-verified and reported as historical when its -bindings differ. Each execution prints a current proof to stdout. Optional +The stable `retained-current-proof.json` selector declares the current retained +proof. The detached map digest, selector, every mapped artifact and selected proof +are verified from the same byte snapshots; mismatched source bindings are reported +as historical. Each execution prints a current proof to stdout. Optional `--output /absolute/new-proof.json` creates a separate private file exclusively; it refuses any existing output. The verifier does not delete the source container or a fixed-name container: creation and cleanup require a unique name, @@ -102,3 +106,10 @@ It is not counted as a successful replay. Private admission formatting does not authenticate observation origin. The operating capture adapter, fresh writer-fence evidence, protected promotion, hosted application and live user flows remain separate unfinished steps. + +The reviewed replay additionally reached the actual eleven-stage apply terminal +readback, injected an expired timestamp in only the final wrapper guard, observed +SUCCESSOR_ADMISSION_EXPIRED and confirmed exact baseline restoration from a separate +read-only connection. This explicitly labelled fault injection proves the final +guard/rollback path; it does not measure an actual fifteen-minute operating lease. +The original migration statements and statement vectors remain unchanged. diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS index 3db2e6c74b..a86feae869 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS @@ -1,20 +1,25 @@ -ae764bb760102a6d7d066be669c85248feed1823eafded4bf8c246a062090a8c .github/review-media-sql-successor.v1.json -9a3fd226f9506f83b3be3f09c64d31770536d99ee3e4167bce081b4e5c3d3ad0 apps/web/performance/review-db-rehearsal-20261012/README.md +23bf7e7d7e9e5d7962021dd3ec860e87b47d07e119ba0e39de2e423a0a601f4e .github/review-media-sql-successor.v1.json +ac14e363f38d6cb6acba0e24a5ad6c6943c8e8c15152cff49fcd86a2d014bd9d apps/web/performance/review-db-rehearsal-20261012/README.md +ea529addcf42cdb766f622786ccfffb13fe337f70faae6ade6da83736be58f57 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-0833ed81-historical 6d9cbca375e6790ee1051aa9d9bf1c45beeeece8d919651281885b4c04962981 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4d4ecf60-historical 6c474d8e78c0eba3f15c077dbdd8634f073524c3b5f842213372bf0f1fed84fc apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical a12f8880f2a0b8b5493e37ea4b2619962d42643581c14c889f0c6f6e8d528129 apps/web/performance/review-db-rehearsal-20261012/interrupted-attempt-20261013.json +9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076 apps/web/performance/review-db-rehearsal-20261012/pg17-reviewed-boundaries-20261013.json b279a6b900dd3be10b9084a6362d169e87435bae955b2a1419858709645a6883 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json 7f74c175be3eee72b5e93eb75dac07bfefc0ef88d7d9957c26d877ee125ff590 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json cb766e279baad0315e9d7055bc326b41b78a7d50a4972dac3214bc071aee0e49 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json 0cfc9c10ff7b70c606f6eeef33fff8dbd14636dc15b7c963925097454ee454a2 apps/web/performance/review-db-rehearsal-20261012/pg17-terminal-boundaries-20261013.json +128773e5dabbbb120889716ffbba41e498a37a437aa6799311a81af729703880 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof-0833ed81-historical.json +78f3e0ad5dbc4e3c53cf8dce3b2a9cb2f484f8f72683cbf1a5c659c8f142acb6 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json +7bdf3cd525427da824674ce50cb3ba7c5d54bf6cb0ef8ebe85b21bb2372eae0a apps/web/performance/review-db-rehearsal-20261012/reviewed-boundaries-unit-verification-20261013.json c53a467eaf9b929647bc1d66248308ba1ff73eba89fc9221dbf7db4711eb8562 apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json fad17a393e5fc74d06dec2be58c41265d21f355d5ddce92a3947bb67a823e73a apps/web/performance/review-db-rehearsal-20261012/terminal-boundary-unit-verification-20261013.json -8088aa57f39045e2bf93cba084711119395098d053318b70aaa676b5c13fea26 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs -8c3d7df52c281a9df1d6a4bd2acefb59f9a23b60185559fc6a8298cd65fde370 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs +768b8887a30c2b8f496a73a21d373b85a0b64fadaed3a1485034a729ee704d39 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs c893c294719776bf5b064c3355866ab97e0fa9412082d9b47d7de5266094c65f apps/web/scripts/review-media-sql-rehearsal.mjs -dcf552697251c2f10d859c7a2f56937a0f6e92ee9467e5b919807e498eff1d49 apps/web/scripts/review-media-sql-successor.mjs +5d61808b45661d412937fbfdfd3bfb75b1cf92c7c285e4d8ad061a2f667c3c0b apps/web/scripts/review-media-sql-successor.mjs fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs dc1f2594e629d5af16541db93e2e51f9b98d8282dd7fbeb802dd240fa6f522c0 apps/web/tests-unit/review-media-migration-bundle.test.ts -b25b5dcc6516435a1c74a038c20856cc5dd719b303d9c3f3e600dc314197c7cd apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts -31bd78568b7bff5196626d87cf7f0fe9b84d695da8cef597fa93e4d742f99845 apps/web/tests-unit/review-media-sql-successor-harness.test.ts -7d32dccb70c6b1b79379ee69864d2c092d3a2e39863ab365494622a0effeba62 apps/web/tests-unit/review-media-sql-successor.test.ts +54a9560b77a3afb9eb70631224ba4f481caf82eb8b5d2cdc1a076d8831c1ec7e apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts +983a62a3f81973e18914835b2d483a7a05fef3fa9f7d129ffd7a60ed5cd4e551 apps/web/tests-unit/review-media-sql-successor-harness.test.ts +b9fbfa0fdc249e3c495043679c39a8a98cd14f8de6a225f20d926d83702fd196 apps/web/tests-unit/review-media-sql-successor.test.ts diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-0833ed81-historical b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-0833ed81-historical new file mode 100644 index 0000000000..3db2e6c74b --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-0833ed81-historical @@ -0,0 +1,20 @@ +ae764bb760102a6d7d066be669c85248feed1823eafded4bf8c246a062090a8c .github/review-media-sql-successor.v1.json +9a3fd226f9506f83b3be3f09c64d31770536d99ee3e4167bce081b4e5c3d3ad0 apps/web/performance/review-db-rehearsal-20261012/README.md +6d9cbca375e6790ee1051aa9d9bf1c45beeeece8d919651281885b4c04962981 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4d4ecf60-historical +6c474d8e78c0eba3f15c077dbdd8634f073524c3b5f842213372bf0f1fed84fc apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical +a12f8880f2a0b8b5493e37ea4b2619962d42643581c14c889f0c6f6e8d528129 apps/web/performance/review-db-rehearsal-20261012/interrupted-attempt-20261013.json +b279a6b900dd3be10b9084a6362d169e87435bae955b2a1419858709645a6883 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json +7f74c175be3eee72b5e93eb75dac07bfefc0ef88d7d9957c26d877ee125ff590 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json +cb766e279baad0315e9d7055bc326b41b78a7d50a4972dac3214bc071aee0e49 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json +0cfc9c10ff7b70c606f6eeef33fff8dbd14636dc15b7c963925097454ee454a2 apps/web/performance/review-db-rehearsal-20261012/pg17-terminal-boundaries-20261013.json +c53a467eaf9b929647bc1d66248308ba1ff73eba89fc9221dbf7db4711eb8562 apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json +fad17a393e5fc74d06dec2be58c41265d21f355d5ddce92a3947bb67a823e73a apps/web/performance/review-db-rehearsal-20261012/terminal-boundary-unit-verification-20261013.json +8088aa57f39045e2bf93cba084711119395098d053318b70aaa676b5c13fea26 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs +8c3d7df52c281a9df1d6a4bd2acefb59f9a23b60185559fc6a8298cd65fde370 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +c893c294719776bf5b064c3355866ab97e0fa9412082d9b47d7de5266094c65f apps/web/scripts/review-media-sql-rehearsal.mjs +dcf552697251c2f10d859c7a2f56937a0f6e92ee9467e5b919807e498eff1d49 apps/web/scripts/review-media-sql-successor.mjs +fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs +dc1f2594e629d5af16541db93e2e51f9b98d8282dd7fbeb802dd240fa6f522c0 apps/web/tests-unit/review-media-migration-bundle.test.ts +b25b5dcc6516435a1c74a038c20856cc5dd719b303d9c3f3e600dc314197c7cd apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts +31bd78568b7bff5196626d87cf7f0fe9b84d695da8cef597fa93e4d742f99845 apps/web/tests-unit/review-media-sql-successor-harness.test.ts +7d32dccb70c6b1b79379ee69864d2c092d3a2e39863ab365494622a0effeba62 apps/web/tests-unit/review-media-sql-successor.test.ts diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 index 626730f211..fcebbfbfad 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 @@ -1 +1 @@ -ea529addcf42cdb766f622786ccfffb13fe337f70faae6ade6da83736be58f57 +5df2ade9cb3618b774eb4d3796df06ade6ba069409de49d0bdd542b34b6ec4ba diff --git a/apps/web/performance/review-db-rehearsal-20261012/pg17-reviewed-boundaries-20261013.json b/apps/web/performance/review-db-rehearsal-20261012/pg17-reviewed-boundaries-20261013.json new file mode 100644 index 0000000000..8bb6d809cc --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/pg17-reviewed-boundaries-20261013.json @@ -0,0 +1,72 @@ +{ + "schemaVersion": 1, + "status": "passed", + "environment": { + "engine": "17.6", + "image": "supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab", + "emptyRowChecks": [ + "public.profiles", + "public.reviews" + ] + }, + "manifestSha256": "23bf7e7d7e9e5d7962021dd3ec860e87b47d07e119ba0e39de2e423a0a601f4e", + "manifestToolchainRoot": "579f315ba6285ea7ccc562cf2cf8949203a0a3289e4de5f82be1687ddea4170b", + "rehearsalHarnessSha256": "f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431", + "sourceRoot": "61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544", + "fixtureDumpSha256": "b7777306a43cc84408434e9835973604b2eb520cf9e9e8d0ee4bba6dcb41257f", + "predecessorSources": [ + { + "path": "backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql", + "sha256": "b373b7ea472c0352a33a4d4043cf8d6aa8474c04cc1ca5778805edfa77ac9c95" + }, + { + "path": "backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql", + "sha256": "66eace1d6fb0dd55c1d780776bab855cc37690335ad40b0fdc1294c610e07d3a" + }, + { + "path": "backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql", + "sha256": "e1c105df82c4f814d3e6adad807ff9d072b8cd42ae770b4b78f75b89a9387020" + }, + { + "path": "backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql", + "sha256": "8acf6d1428764260ed57dac5fe09711868a82896f0a6d631d502128004c168a3" + }, + { + "path": "backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql", + "sha256": "b596b200e52c6813a4cfa1b0a2818625f067864549e8854f3497afdcdab706da" + }, + { + "path": "backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql", + "sha256": "2067538f89c9f90d28e784672c7a1288306ba22d5ae92b087c8692503da9b1ae" + } + ], + "migrationCount": 11, + "statementCount": 112, + "baselineStateSha256": "3802888324e1651ad7b2f71e7cf15fac78ab77aa57637d0c60431f8da67e6298", + "stageStatesSha256": "bd25ecbd82759ca75e994ca0f1da38b49acc848d5dd556e702e4e43024edaa65", + "terminalStateSha256": "9cf59a5d983e78f6f7d9a594267c6feda3276611bf3539289b8a3de3295f700a", + "compiledSqlBytes": 2510560, + "compiledSqlSha256": "285d0c859117006646389dbd2a22de9aef1c0b97bc796a9e4d4448a8a232b093", + "applyCompiledSqlSha256": "91e378c58d58d7d7f818464c9c92aff02c7b01ca528e9db858e2b1d2d00bd92f", + "applyExpiryFaultedSqlSha256": "727ff0f6be2dbf8ee06551d4efd6ebe665f7187189f7a0a1159d5281ae90635b", + "results": { + "rollbackStageCount": 12, + "rollbackExact": true, + "forcedErrorRejected": true, + "forcedErrorRollbackExact": true, + "lostResponseMutationAttempts": 1, + "lostResponseRestorationExact": true, + "contendedExpiryRejected": true, + "contendedExpiryRollbackExact": true, + "applyFinalGuardDeadlineFaultInjected": true, + "applyTerminalReadbackObserved": true, + "applyExpiryRejectedBeforeCommit": true, + "applyExpiryMutationAttempts": 1, + "applyExpiryRollbackExact": true + }, + "hostedWrites": false, + "operatingRowsRead": false, + "rawSqlInProof": false, + "sourceMutation": false, + "temporaryContainerRemoved": true +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof-0833ed81-historical.json b/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof-0833ed81-historical.json new file mode 100644 index 0000000000..66de073ccf --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof-0833ed81-historical.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "kind": "review-media-sql-retained-current-proof", + "proofPath": "pg17-terminal-boundaries-20261013.json", + "proofSha256": "0cfc9c10ff7b70c606f6eeef33fff8dbd14636dc15b7c963925097454ee454a2" +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json b/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json new file mode 100644 index 0000000000..dc2a656581 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "kind": "review-media-sql-retained-current-proof", + "proofPath": "pg17-reviewed-boundaries-20261013.json", + "proofSha256": "9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076" +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/reviewed-boundaries-unit-verification-20261013.json b/apps/web/performance/review-db-rehearsal-20261012/reviewed-boundaries-unit-verification-20261013.json new file mode 100644 index 0000000000..675d3826f6 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/reviewed-boundaries-unit-verification-20261013.json @@ -0,0 +1,89 @@ +{ + "schemaVersion": 1, + "kind": "local-reviewed-successor-unit-verification", + "recordedAtUTC": "2026-10-13T08:32:39.695495+00:00", + "environment": { + "node": "24.21.0", + "bun": "1.4.0", + "platform": "macOS" + }, + "finalAffectedModules": 6, + "finalUniqueTestsPassed": 94, + "finalUniqueAssertions": 518, + "unresolvedFailures": 0, + "verificationRuns": [ + { + "modules": 5, + "passed": 80, + "failed": 0, + "assertions": 463 + }, + { + "modules": 6, + "passed": 93, + "failed": 1, + "assertions": 514, + "failure": "selector unit assertion still named prior immutable proof; corrected to verify authenticated selection independently of current filename" + }, + { + "modules": 1, + "passed": 14, + "failed": 0, + "assertions": 55, + "afterCorrection": true + } + ], + "sourceBindings": [ + { + "path": ".github/review-media-sql-successor.v1.json", + "sha256": "23bf7e7d7e9e5d7962021dd3ec860e87b47d07e119ba0e39de2e423a0a601f4e" + }, + { + "path": "apps/web/scripts/review-media-sql-successor.mjs", + "sha256": "5d61808b45661d412937fbfdfd3bfb75b1cf92c7c285e4d8ad061a2f667c3c0b" + }, + { + "path": "apps/web/scripts/review-media-operating-admission.mjs", + "sha256": "586847d5030cedc6575328cbe78b5eb9f1d9ff47d91e8d8f1aef40076b6d2277" + }, + { + "path": "apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs", + "sha256": "f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431" + }, + { + "path": "apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs", + "sha256": "768b8887a30c2b8f496a73a21d373b85a0b64fadaed3a1485034a729ee704d39" + }, + { + "path": "apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs", + "sha256": "41972cf6c4ac2b819b42cf9f06b762d54fb9d161f4594192492fccf79cca7418" + }, + { + "path": "apps/web/tests-unit/review-media-sql-successor.test.ts", + "sha256": "b9fbfa0fdc249e3c495043679c39a8a98cd14f8de6a225f20d926d83702fd196" + }, + { + "path": "apps/web/tests-unit/review-media-migration-bundle.test.ts", + "sha256": "dc1f2594e629d5af16541db93e2e51f9b98d8282dd7fbeb802dd240fa6f522c0" + }, + { + "path": "apps/web/tests-unit/review-media-sql-successor-harness.test.ts", + "sha256": "983a62a3f81973e18914835b2d483a7a05fef3fa9f7d129ffd7a60ed5cd4e551" + }, + { + "path": "apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts", + "sha256": "54a9560b77a3afb9eb70631224ba4f481caf82eb8b5d2cdc1a076d8831c1ec7e" + }, + { + "path": "apps/web/tests-unit/review-media-operating-admission.test.ts", + "sha256": "184368546c3668da99449f3d7ec0f08b95e1c06f44ca85100a6d2dfa533bc84a" + }, + { + "path": "apps/web/tests-unit/review-media-fixture-ownership-verifier.test.ts", + "sha256": "92651102154fd4d573ca841137bd31c319ef545b2527e7da17398bd2e6b97d47" + } + ], + "hostedWrites": false, + "providerCalls": 0, + "performanceClaim": false +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs b/apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs index b6266528ec..d35f1e6de9 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs +++ b/apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs @@ -16,15 +16,18 @@ import { spawn, spawnSync } from 'node:child_process'; import { buildSuccessorStateQuery, + compileSuccessorPlan, compileSuccessorRehearsalPlan, loadSuccessorManifest, + SUCCESSOR_PRE_COMMIT_EXPIRY_MARKER, validateSuccessorStageStates, } from '../../scripts/review-media-sql-successor.mjs'; const HERE = dirname(fileURLToPath(import.meta.url)); const REPOSITORY_ROOT = resolve(HERE, '../../../..'); -const OUTPUT_PATH = resolve(HERE, 'pg17-rollback-rehearsal.json'); +export const DEFAULT_RETAINED_PROOF_SELECTOR_PATH = resolve(HERE, 'retained-current-proof.json'); const CHECKSUMS_PATH = resolve(HERE, 'SHA256SUMS'); +const CHECKSUMS_DIGEST_PATH = resolve(HERE, 'SHA256SUMS.sha256'); const DOCKER_CONTEXT = process.env.TZUDONG_REHEARSAL_DOCKER_CONTEXT || 'colima-tzudong-catalog-20261007'; const SOURCE_CONTAINER = process.env.TZUDONG_REHEARSAL_SOURCE_CONTAINER || 'tzudong-ranking-clone-20261008'; const CONTAINER_PREFIX = 'tzudong-review-rollback-rehearsal'; @@ -74,31 +77,178 @@ function currentBindings(record, manifestBytes, harnessBytes) { }); } -function checksumEntry(checksumsBytes, repositoryPath) { +function checksumEntries(checksumsBytes) { const entries = checksumsBytes.toString('utf8').split(/\r?\n/).filter(Boolean).map(line => { const match = /^([0-9a-f]{64}) (.+)$/.exec(line); if (!match) fail('LOCAL_REHEARSAL_CHECKSUMS_INVALID'); return { sha256: match[1], path: match[2] }; }); + if (new Set(entries.map(entry => entry.path)).size !== entries.length) { + fail('LOCAL_REHEARSAL_CHECKSUMS_DUPLICATE_PATH'); + } + return entries; +} + +function checksumEntry(entries, repositoryPath, missingCode = 'LOCAL_REHEARSAL_RETAINED_CHECKSUM_MISSING') { const matches = entries.filter(entry => entry.path === repositoryPath); - if (matches.length !== 1) fail('LOCAL_REHEARSAL_RETAINED_CHECKSUM_MISSING'); + if (matches.length !== 1) fail(missingCode); return matches[0]; } +function repositoryPathFor(repositoryRoot, absolutePath, invalidCode) { + const repositoryPath = relative(repositoryRoot, absolutePath).replaceAll('\\', '/'); + if (!repositoryPath || repositoryPath.startsWith('../')) fail(invalidCode); + return repositoryPath; +} + +function parseRetainedProofSelector(bytes, { selectorPath, selectorRepositoryPath, repositoryRoot }) { + let selector; + try { selector = JSON.parse(bytes.toString('utf8')); } + catch { fail('LOCAL_REHEARSAL_SELECTOR_JSON_INVALID'); } + const keys = selector && typeof selector === 'object' && !Array.isArray(selector) + ? Object.keys(selector).sort() + : []; + if (!equal(keys, ['kind', 'proofPath', 'proofSha256', 'schemaVersion']) + || selector.schemaVersion !== 1 + || selector.kind !== 'review-media-sql-retained-current-proof' + || typeof selector.proofPath !== 'string' + || !/^[A-Za-z0-9][A-Za-z0-9._-]*\.json$/.test(selector.proofPath) + || !/^[0-9a-f]{64}$/.test(selector.proofSha256)) { + fail('LOCAL_REHEARSAL_SELECTOR_INVALID'); + } + const proofAbsolutePath = resolve(dirname(selectorPath), selector.proofPath); + const proofRepositoryPath = repositoryPathFor( + repositoryRoot, + proofAbsolutePath, + 'LOCAL_REHEARSAL_SELECTOR_PATH_INVALID', + ); + if (dirname(proofRepositoryPath) !== dirname(selectorRepositoryPath)) { + fail('LOCAL_REHEARSAL_SELECTOR_PATH_INVALID'); + } + return Object.freeze({ ...selector, proofAbsolutePath, proofRepositoryPath }); +} + +function readArtifactMap({ + checksumsPath, + checksumsDigestPath, + repositoryRoot, + retainedRepositoryPath, + selectorPath, +}) { + const checksumsBytes = readFileSync(checksumsPath); + if (checksumsDigestPath) { + if (!existsSync(checksumsDigestPath)) fail('LOCAL_REHEARSAL_CHECKSUMS_DIGEST_MISSING'); + const digest = readFileSync(checksumsDigestPath, 'utf8').trim(); + if (!/^[0-9a-f]{64}$/.test(digest)) fail('LOCAL_REHEARSAL_CHECKSUMS_DIGEST_INVALID'); + if (sha256(checksumsBytes) !== digest) fail('LOCAL_REHEARSAL_CHECKSUMS_DIGEST_MISMATCH'); + } + const entries = checksumEntries(checksumsBytes); + const snapshots = new Map(); + const checksumsAbsolute = resolve(checksumsPath); + const digestAbsolute = checksumsDigestPath ? resolve(checksumsDigestPath) : null; + let selector = null; + let selectorRepositoryPath = null; + let selectedRepositoryPath = retainedRepositoryPath; + if (selectorPath) { + selectorRepositoryPath = repositoryPathFor( + repositoryRoot, + resolve(selectorPath), + 'LOCAL_REHEARSAL_SELECTOR_PATH_INVALID', + ); + const selectorEntry = checksumEntry( + entries, + selectorRepositoryPath, + 'LOCAL_REHEARSAL_SELECTOR_CHECKSUM_MISSING', + ); + if (!existsSync(selectorPath)) fail('LOCAL_REHEARSAL_SELECTOR_MISSING'); + const selectorBytes = readFileSync(selectorPath); + if (sha256(selectorBytes) !== selectorEntry.sha256) fail('LOCAL_REHEARSAL_SELECTOR_SHA256_MISMATCH'); + snapshots.set(selectorRepositoryPath, selectorBytes); + selector = parseRetainedProofSelector(selectorBytes, { + selectorPath: resolve(selectorPath), + selectorRepositoryPath, + repositoryRoot, + }); + const proofEntry = checksumEntry( + entries, + selector.proofRepositoryPath, + 'LOCAL_REHEARSAL_SELECTOR_PROOF_CHECKSUM_MISSING', + ); + if (proofEntry.sha256 !== selector.proofSha256) fail('LOCAL_REHEARSAL_SELECTOR_PROOF_SHA256_MISMATCH'); + selectedRepositoryPath = selector.proofRepositoryPath; + } + const expected = checksumEntry(entries, selectedRepositoryPath); + for (const entry of entries) { + const absolutePath = resolve(repositoryRoot, entry.path); + const normalizedPath = relative(repositoryRoot, absolutePath).replaceAll('\\', '/'); + if (!normalizedPath + || normalizedPath.startsWith('../') + || normalizedPath !== entry.path + || absolutePath === checksumsAbsolute + || absolutePath === digestAbsolute) { + fail('LOCAL_REHEARSAL_ARTIFACT_PATH_INVALID'); + } + if (!existsSync(absolutePath)) { + fail(entry.path === selectedRepositoryPath + ? 'LOCAL_REHEARSAL_CURRENT_PROOF_MISSING' + : 'LOCAL_REHEARSAL_ARTIFACT_MISSING'); + } + const bytes = snapshots.get(entry.path) ?? readFileSync(absolutePath); + const actualSha256 = sha256(bytes); + if (actualSha256 !== entry.sha256) { + fail(entry.path === selectedRepositoryPath + ? 'LOCAL_REHEARSAL_RETAINED_SHA256_MISMATCH' + : selectorPath && resolve(absolutePath) === resolve(selectorPath) + ? 'LOCAL_REHEARSAL_SELECTOR_SHA256_MISMATCH' + : 'LOCAL_REHEARSAL_ARTIFACT_SHA256_MISMATCH'); + } + snapshots.set(entry.path, bytes); + } + return Object.freeze({ + expected, + retainedRepositoryPath: selectedRepositoryPath, + selector: selector && Object.freeze({ + repositoryPath: selectorRepositoryPath, + sha256: sha256(snapshots.get(selectorRepositoryPath)), + proofPath: selector.proofPath, + proofSha256: selector.proofSha256, + }), + snapshots, + }); +} + export function inspectRetainedProof({ - outputPath = OUTPUT_PATH, + outputPath, + selectorPath = outputPath ? null : DEFAULT_RETAINED_PROOF_SELECTOR_PATH, checksumsPath = CHECKSUMS_PATH, + checksumsDigestPath, bindings, repositoryRoot = REPOSITORY_ROOT, + required = Boolean(selectorPath), } = {}) { - if (!existsSync(outputPath)) return Object.freeze({ present: false, classification: 'missing' }); + if (outputPath && selectorPath) fail('LOCAL_REHEARSAL_RETAINED_SOURCE_AMBIGUOUS'); + if (outputPath && !existsSync(outputPath)) { + if (required) fail('LOCAL_REHEARSAL_CURRENT_PROOF_MISSING'); + return Object.freeze({ present: false, classification: 'missing' }); + } if (!existsSync(checksumsPath)) fail('LOCAL_REHEARSAL_CHECKSUMS_MISSING'); - const proofBytes = readFileSync(outputPath); - const repositoryPath = relative(repositoryRoot, outputPath).replaceAll('\\', '/'); - if (!repositoryPath || repositoryPath.startsWith('../')) fail('LOCAL_REHEARSAL_RETAINED_PATH_INVALID'); - const expected = checksumEntry(readFileSync(checksumsPath), repositoryPath); + const repositoryPath = outputPath + ? repositoryPathFor(repositoryRoot, outputPath, 'LOCAL_REHEARSAL_RETAINED_PATH_INVALID') + : null; + const effectiveDigestPath = checksumsDigestPath === undefined && resolve(checksumsPath) === CHECKSUMS_PATH + ? CHECKSUMS_DIGEST_PATH + : checksumsDigestPath; + const artifactMap = readArtifactMap({ + checksumsPath, + checksumsDigestPath: effectiveDigestPath, + repositoryRoot, + retainedRepositoryPath: repositoryPath, + selectorPath, + }); + const selectedRepositoryPath = artifactMap.retainedRepositoryPath; + const proofBytes = artifactMap.snapshots.get(selectedRepositoryPath); + const expected = artifactMap.expected; const actualSha256 = sha256(proofBytes); - if (actualSha256 !== expected.sha256) fail('LOCAL_REHEARSAL_RETAINED_SHA256_MISMATCH'); let proof; try { proof = JSON.parse(proofBytes.toString('utf8')); } catch { fail('LOCAL_REHEARSAL_RETAINED_JSON_INVALID'); } const fields = ['manifestSha256', 'rehearsalHarnessSha256', 'sourceRoot']; @@ -114,7 +264,8 @@ export function inspectRetainedProof({ classification: bindingDifferences.length === 0 ? 'retained-current-source-evidence' : 'retained-historical-evidence', - repositoryPath, + repositoryPath: selectedRepositoryPath, + selector: artifactMap.selector, expectedSha256: expected.sha256, actualSha256, currentSourceMatch: bindingDifferences.length === 0, @@ -238,7 +389,7 @@ export function createOwnedContainer(ownership, { generateToken = () => randomBytes(16).toString('hex'), processId = process.pid, } = {}) { - if (ownership.created || ownership.id || ownership.name || ownership.token) { + if (ownership.attempted || ownership.created || ownership.id || ownership.name || ownership.token) { fail('LOCAL_REHEARSAL_CONTAINER_OWNERSHIP_STATE_INVALID'); } const token = generateToken(); @@ -246,6 +397,7 @@ export function createOwnedContainer(ownership, { const name = `${CONTAINER_PREFIX}-${processId}-${token.slice(0, 12)}`; ownership.name = name; ownership.token = token; + ownership.attempted = true; const creation = dockerImpl([ 'run', '-d', '--rm', '--name', name, '--label', `${OWNER_LABEL}=${token}`, '-e', `POSTGRES_PASSWORD=${BOOTSTRAP_PASSWORD}`, IMAGE, @@ -265,15 +417,23 @@ export function createOwnedContainer(ownership, { } export function removeOwnedContainer(ownership, { dockerImpl = docker } = {}) { - if (!ownership.created || !ownership.id || !ownership.name || !ownership.token) return false; - const observed = inspectContainer(ownership.id, ownership, dockerImpl); + if ((!ownership.attempted && !ownership.created) || !ownership.name || !ownership.token) return false; + let observed; + try { + observed = inspectContainer(ownership.id || ownership.name, ownership, dockerImpl); + } catch (error) { + if (!ownership.id && error?.code === 'LOCAL_REHEARSAL_CONTAINER_OWNERSHIP_UNPROVEN') return false; + throw error; + } if (!observed) { ownership.created = false; ownership.removed = true; return true; } - const removal = dockerImpl(['container', 'rm', '--force', ownership.id], { allowFailure: true }); - const after = inspectContainer(ownership.id, ownership, dockerImpl); + ownership.id = observed.id; + ownership.created = true; + const removal = dockerImpl(['container', 'rm', '--force', observed.id], { allowFailure: true }); + const after = inspectContainer(observed.id, ownership, dockerImpl); if (after) fail('LOCAL_REHEARSAL_CONTAINER_REMOVAL_UNCONFIRMED'); ownership.created = false; ownership.removed = true; @@ -332,6 +492,34 @@ function parseStages(output, manifest) { return stages; } +export function injectFinalApplyExpiryFault(sql, expiresAt, { + expiredAt = '1970-01-01T00:00:00.000Z', +} = {}) { + if (typeof sql !== 'string' + || !Number.isFinite(Date.parse(expiresAt)) + || !Number.isFinite(Date.parse(expiredAt))) { + fail('LOCAL_REHEARSAL_APPLY_EXPIRY_FAULT_INVALID'); + } + const markerIndex = sql.indexOf(SUCCESSOR_PRE_COMMIT_EXPIRY_MARKER); + if (markerIndex < 0 + || sql.indexOf(SUCCESSOR_PRE_COMMIT_EXPIRY_MARKER, markerIndex + 1) >= 0) { + fail('LOCAL_REHEARSAL_APPLY_EXPIRY_MARKER_INVALID'); + } + const prefix = sql.slice(0, markerIndex + SUCCESSOR_PRE_COMMIT_EXPIRY_MARKER.length); + const suffix = sql.slice(prefix.length); + const deadline = `'${expiresAt}'::timestamptz`; + const matches = suffix.split(deadline).length - 1; + if (matches !== 1) fail('LOCAL_REHEARSAL_APPLY_EXPIRY_MARKER_INVALID'); + const faultedSuffix = suffix.replace(deadline, `'${expiredAt}'::timestamptz`); + return Object.freeze({ + sql: `${prefix}${faultedSuffix}`, + marker: SUCCESSOR_PRE_COMMIT_EXPIRY_MARKER, + originalDeadline: expiresAt, + injectedDeadline: expiredAt, + wrapperOnlyFaultInjection: true, + }); +} + function startLockHolder(container) { const args = [ '--context', DOCKER_CONTEXT, @@ -395,7 +583,7 @@ export async function run(argv = process.argv.slice(2)) { const harnessBytes = readFileSync(fileURLToPath(import.meta.url)); const bindings = currentBindings(record, manifestBytes, harnessBytes); const retainedProof = inspectRetainedProof({ bindings }); - const ownership = { created: false, id: null, name: null, token: null }; + const ownership = { attempted: false, created: false, id: null, name: null, token: null }; let proof; try { const environment = initializeFixture(ownership); @@ -441,6 +629,31 @@ export async function run(argv = process.argv.slice(2)) { } if (!equal(state(container, 'postgres', stateQuery), expiryBaseline)) fail('LOCAL_REHEARSAL_EXPIRY_DRIFT'); + const applyExpiryBaseline = state(container, 'postgres', stateQuery); + const applyExpiresAt = '9999-12-31T23:59:59.999Z'; + const applyPlan = compileSuccessorPlan(record, { expiresAt: applyExpiresAt, stageStates: stages }); + const applyExpiryFault = injectFinalApplyExpiryFault(applyPlan.sql, applyExpiresAt); + const applyExpiryResult = psql( + container, + 'postgres', + `BEGIN;\n${applyExpiryFault.sql}\nCOMMIT;`, + { allowFailure: true }, + ); + if (applyExpiryResult.status === 0 + || !/ERROR:\s+(?:[0-9A-Z]{5}:\s*)?SUCCESSOR_ADMISSION_EXPIRED\b/m.test(applyExpiryResult.stderr) + || /transaction timeout/i.test(applyExpiryResult.stderr)) { + fail('LOCAL_REHEARSAL_APPLY_EXPIRY_NOT_REJECTED'); + } + let applyTerminalReadback; + try { applyTerminalReadback = JSON.parse(applyExpiryResult.stdout.trim()); } + catch { fail('LOCAL_REHEARSAL_APPLY_TERMINAL_READBACK_INVALID'); } + if (!equal(applyTerminalReadback, stages.at(-1))) { + fail('LOCAL_REHEARSAL_APPLY_TERMINAL_READBACK_INVALID'); + } + if (!equal(state(container, 'postgres', stateQuery), applyExpiryBaseline)) { + fail('LOCAL_REHEARSAL_APPLY_EXPIRY_ROLLBACK_DRIFT'); + } + const toolchain = record.manifest.toolchain.map(entry => ({ ...entry })); proof = { schemaVersion: 1, @@ -466,6 +679,8 @@ export async function run(argv = process.argv.slice(2)) { terminalStateSha256: sha256(JSON.stringify(canonical(stages.at(-1)))), compiledSqlBytes: Buffer.byteLength(plan.sql), compiledSqlSha256: sha256(plan.sql), + applyCompiledSqlSha256: sha256(applyPlan.sql), + applyExpiryFaultedSqlSha256: sha256(applyExpiryFault.sql), results: { rollbackStageCount: stages.length, rollbackExact: true, @@ -475,6 +690,11 @@ export async function run(argv = process.argv.slice(2)) { lostResponseRestorationExact: true, contendedExpiryRejected: true, contendedExpiryRollbackExact: true, + applyFinalGuardDeadlineFaultInjected: true, + applyTerminalReadbackObserved: true, + applyExpiryRejectedBeforeCommit: true, + applyExpiryMutationAttempts: 1, + applyExpiryRollbackExact: true, }, hostedWrites: false, operatingRowsRead: false, diff --git a/apps/web/performance/review-db-successor-20261012/README.md b/apps/web/performance/review-db-successor-20261012/README.md index 9b3e71102e..f32f250e81 100644 --- a/apps/web/performance/review-db-successor-20261012/README.md +++ b/apps/web/performance/review-db-successor-20261012/README.md @@ -67,3 +67,17 @@ Current ownership proof: `pg17-fixture-ownership-current-20261013.json` The earlier `pg17-fixture-ownership-20261013.json` is retained for the verifier before optional exclusive output support. Both establish six isolated scenarios; the current proof matches the current helper and verifier bytes. + +## Reviewed helper binding — 2026-10-13 + +Current ownership evidence is `pg17-fixture-ownership-reviewed-20261013.json`. +It binds the fixture helper, this ownership verifier and the imported rollback +container helper. All three source byte hashes are checked before/after the run, +including cleanup. Six scenarios again recorded one CREATE and one confirmed +cleanup each. Earlier proofs remain immutable historical evidence. + +The forward fixture harness now sends the full SQL once. A failed acknowledgement +is reconciled only by an exact read-only baseline/terminal-state read; it never +performs a diagnostic mutation replay. Two injected outcome cases cover terminal +commit acknowledgement loss and a rolled-back baseline. This change has local +fault tests; no fresh forward-harness replay is claimed. diff --git a/apps/web/performance/review-db-successor-20261012/SHA256SUMS b/apps/web/performance/review-db-successor-20261012/SHA256SUMS index a33aadc18b..0e1233e64c 100644 --- a/apps/web/performance/review-db-successor-20261012/SHA256SUMS +++ b/apps/web/performance/review-db-successor-20261012/SHA256SUMS @@ -1,13 +1,17 @@ -ae764bb760102a6d7d066be669c85248feed1823eafded4bf8c246a062090a8c .github/review-media-sql-successor.v1.json -0fddfa7996df84c1b2e7463b2be696ffff442d97fd24abb476c2f701a9d3d946 apps/web/performance/review-db-successor-20261012/README.md +23bf7e7d7e9e5d7962021dd3ec860e87b47d07e119ba0e39de2e423a0a601f4e .github/review-media-sql-successor.v1.json +f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs +830e2f3d6baaf95ee42201b46ad703640b0caea8ae31d5db395adf74d9067f09 apps/web/performance/review-db-successor-20261012/README.md +1a3aa2b9a3e1faef9916cce3d7d3b11246683dadce15da97b3a0e3a75fbb9048 apps/web/performance/review-db-successor-20261012/SHA256SUMS-0833ed81-historical b9f3fca7e506e195312c2ca3e4da97f04c7767e42b2a0f980aab68dfc6395e09 apps/web/performance/review-db-successor-20261012/SHA256SUMS-4d4ecf60-historical dc27a75b296716c612e9e3b9cb35f0fd113129dad5215f72a76f3ea94747ee95 apps/web/performance/review-db-successor-20261012/fixture-ownership-failure-interpretation-20261013.json d9cdeba56f0c38a0c1c46f50b48645d827b47df2ba09f3101ff01dcc828cf118 apps/web/performance/review-db-successor-20261012/fixture-ownership-first-attempt-20261013.json 0a3b893331ff16732dd79c6bfca9f88689f7be3065777e62b0dcf3528cea05df apps/web/performance/review-db-successor-20261012/fixture-ownership-protocol-failure-20261013.json ef0af48a51ee8e3b9a57725eb7504c88401d2b59c3470dec0da11b5257187fbc apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-20261013.json 0e7f96493ba9ad63959fa25c775fb6e95d0616d47fa4bdae973e248f5eab0349 apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-current-20261013.json +ad6917e416ff20e074eaedd523baa3f0366e3c865f62849a7df6793c2fbd758d apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-reviewed-20261013.json 6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f apps/web/performance/review-db-successor-20261012/pg17-local-replay.json -7f6ded6fa4078fb0239b1c4d8cefe87dd8407c174e747ef118c2c98a1b481d9e apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs -8c3d7df52c281a9df1d6a4bd2acefb59f9a23b60185559fc6a8298cd65fde370 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs -dcf552697251c2f10d859c7a2f56937a0f6e92ee9467e5b919807e498eff1d49 apps/web/scripts/review-media-sql-successor.mjs +41972cf6c4ac2b819b42cf9f06b762d54fb9d161f4594192492fccf79cca7418 apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs +768b8887a30c2b8f496a73a21d373b85a0b64fadaed3a1485034a729ee704d39 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +5d61808b45661d412937fbfdfd3bfb75b1cf92c7c285e4d8ad061a2f667c3c0b apps/web/scripts/review-media-sql-successor.mjs fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs +92651102154fd4d573ca841137bd31c319ef545b2527e7da17398bd2e6b97d47 apps/web/tests-unit/review-media-fixture-ownership-verifier.test.ts diff --git a/apps/web/performance/review-db-successor-20261012/SHA256SUMS-0833ed81-historical b/apps/web/performance/review-db-successor-20261012/SHA256SUMS-0833ed81-historical new file mode 100644 index 0000000000..a33aadc18b --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/SHA256SUMS-0833ed81-historical @@ -0,0 +1,13 @@ +ae764bb760102a6d7d066be669c85248feed1823eafded4bf8c246a062090a8c .github/review-media-sql-successor.v1.json +0fddfa7996df84c1b2e7463b2be696ffff442d97fd24abb476c2f701a9d3d946 apps/web/performance/review-db-successor-20261012/README.md +b9f3fca7e506e195312c2ca3e4da97f04c7767e42b2a0f980aab68dfc6395e09 apps/web/performance/review-db-successor-20261012/SHA256SUMS-4d4ecf60-historical +dc27a75b296716c612e9e3b9cb35f0fd113129dad5215f72a76f3ea94747ee95 apps/web/performance/review-db-successor-20261012/fixture-ownership-failure-interpretation-20261013.json +d9cdeba56f0c38a0c1c46f50b48645d827b47df2ba09f3101ff01dcc828cf118 apps/web/performance/review-db-successor-20261012/fixture-ownership-first-attempt-20261013.json +0a3b893331ff16732dd79c6bfca9f88689f7be3065777e62b0dcf3528cea05df apps/web/performance/review-db-successor-20261012/fixture-ownership-protocol-failure-20261013.json +ef0af48a51ee8e3b9a57725eb7504c88401d2b59c3470dec0da11b5257187fbc apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-20261013.json +0e7f96493ba9ad63959fa25c775fb6e95d0616d47fa4bdae973e248f5eab0349 apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-current-20261013.json +6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f apps/web/performance/review-db-successor-20261012/pg17-local-replay.json +7f6ded6fa4078fb0239b1c4d8cefe87dd8407c174e747ef118c2c98a1b481d9e apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs +8c3d7df52c281a9df1d6a4bd2acefb59f9a23b60185559fc6a8298cd65fde370 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +dcf552697251c2f10d859c7a2f56937a0f6e92ee9467e5b919807e498eff1d49 apps/web/scripts/review-media-sql-successor.mjs +fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs diff --git a/apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 b/apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 index 17a8b03e78..c0ba176f4b 100644 --- a/apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 +++ b/apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 @@ -1 +1 @@ -1a3aa2b9a3e1faef9916cce3d7d3b11246683dadce15da97b3a0e3a75fbb9048 +d19614c3478ddbe90c38b856ebcbdba7a3a4dc9599ce259c2eabd60dcb0337a6 diff --git a/apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-reviewed-20261013.json b/apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-reviewed-20261013.json new file mode 100644 index 0000000000..68409043f7 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-reviewed-20261013.json @@ -0,0 +1,61 @@ +{ + "schemaVersion": 1, + "kind": "isolated-fixture-ownership-verification", + "status": "passed", + "recordedAtUTC": "2026-10-13T08:25:50.698Z", + "sourceSha256": "768b8887a30c2b8f496a73a21d373b85a0b64fadaed3a1485034a729ee704d39", + "containerHelperSha256": "f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431", + "verifierSha256": "41972cf6c4ac2b819b42cf9f06b762d54fb9d161f4594192492fccf79cca7418", + "environment": { + "engine": "17.6", + "image": "supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab" + }, + "results": [ + { + "kind": "roles", + "mode": "normal", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "roles", + "mode": "lost-create-and-drop-ack", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "roles", + "mode": "lost-create-ack-and-first-readback", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "database", + "mode": "normal", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "database", + "mode": "lost-create-ack", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + }, + { + "kind": "database", + "mode": "lost-create-ack-and-first-readback", + "createAttempts": 1, + "dropAttempts": 1, + "cleaned": true + } + ], + "temporaryContainerRemoved": true, + "sourceContainerMutation": false, + "hostedWrites": false, + "predecessorAndFixtureSqlExercised": false +} diff --git a/apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs b/apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs index b167ceeefd..8b6a58d21d 100644 --- a/apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs +++ b/apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs @@ -10,17 +10,29 @@ import { createFixtureMembership, dropFixtureMembership, createOwnedDatabase, dr const here = dirname(fileURLToPath(import.meta.url)); const root = resolve(here, '../../../..'); -const args = process.argv.slice(2); -if (args.length !== 0 && (args.length !== 2 || args[0] !== '--output' - || !args[1] || args[1].startsWith('--'))) { - throw Object.assign(new Error('FIXTURE_OWNERSHIP_ARGUMENT_INVALID'), { code: 'FIXTURE_OWNERSHIP_ARGUMENT_INVALID' }); -} -const output = args.length === 0 - ? resolve(here, 'pg17-fixture-ownership-20261013.json') : resolve(args[1]); const context = process.env.TZUDONG_REHEARSAL_DOCKER_CONTEXT || 'colima-tzudong-catalog-20261007'; const hash = bytes => createHash('sha256').update(bytes).digest('hex'); -const source = resolve(here, 'verify-review-media-sql-successor.mjs'); -const sourceHash = hash(readFileSync(source)); +const bindingPaths = Object.freeze({ + sourceSha256: resolve(here, 'verify-review-media-sql-successor.mjs'), + containerHelperSha256: resolve(here, '../review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs'), + verifierSha256: fileURLToPath(import.meta.url), +}); + +export function fixtureOwnershipBindings({ readFileImpl = readFileSync } = {}) { + return Object.freeze(Object.fromEntries(Object.entries(bindingPaths) + .map(([key, path]) => [key, hash(readFileImpl(path))]))); +} + +export function assertFixtureOwnershipBindings(expected, dependencies = {}) { + const actual = fixtureOwnershipBindings(dependencies); + if (Object.keys(bindingPaths).some(key => expected?.[key] !== actual[key])) { + throw Object.assign(new Error('FIXTURE_OWNERSHIP_SOURCE_CHANGED'), { + code: 'FIXTURE_OWNERSHIP_SOURCE_CHANGED', + }); + } + return actual; +} + let password; const fail = code => { throw Object.assign(new Error(code), { code }); }; const check = value => { if (!value) fail('FIXTURE_OWNERSHIP_ASSERTION_FAILED'); }; @@ -41,117 +53,128 @@ function sql(database, statement, actor = 'supabase_admin') { { input: String(statement) }).stdout.trim(); } -const results = []; -try { - if (existsSync(output)) fail('FIXTURE_OWNERSHIP_OUTPUT_EXISTS'); - createOwnedContainer(container, { dockerImpl: docker }); - let ready = false; - for (let attempt = 0; attempt < 60; attempt += 1) { - const result = docker(['exec', '-e', `PGPASSWORD=${password}`, container.id, - 'pg_isready', '-h', '127.0.0.1', '-U', 'postgres', '-d', 'postgres'], { allowFailure: true }); - if (result.status === 0) { ready = true; break; } - Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250); - } - check(ready && sql('postgres', 'SHOW server_version;').startsWith('17.6')); +export function runFixtureOwnershipVerification(args = process.argv.slice(2)) { + if (args.length !== 0 && (args.length !== 2 || args[0] !== '--output' + || !args[1] || args[1].startsWith('--'))) fail('FIXTURE_OWNERSHIP_ARGUMENT_INVALID'); + const output = args.length === 0 + ? resolve(here, 'pg17-fixture-ownership-20261013.json') : resolve(args[1]); + const sourceBindings = fixtureOwnershipBindings(); + const results = []; + try { + if (existsSync(output)) fail('FIXTURE_OWNERSHIP_OUTPUT_EXISTS'); + createOwnedContainer(container, { dockerImpl: docker }); + let ready = false; + for (let attempt = 0; attempt < 60; attempt += 1) { + const result = docker(['exec', '-e', `PGPASSWORD=${password}`, container.id, + 'pg_isready', '-h', '127.0.0.1', '-U', 'postgres', '-d', 'postgres'], { allowFailure: true }); + if (result.status === 0) { ready = true; break; } + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 250); + } + check(ready && sql('postgres', 'SHOW server_version;').startsWith('17.6')); - for (const mode of ['normal', 'lost-create-and-drop-ack', 'lost-create-ack-and-first-readback']) { - let created = false; - let lostReadback = false; - let createAttempts = 0; - let dropAttempts = 0; - const ownership = { database: false, fixtureMembership: false, fixtureMembershipAttempted: false, - fixtureMembershipToken: null, fixtureMembershipBaseline: null }; - const dependencies = { - scalarImpl: (database, statement, actor) => { - if (mode.endsWith('first-readback') && created && !lostReadback) { - lostReadback = true; fail('FIXTURE_FAULT_READBACK_LOSS'); - } - return sql(database, statement, actor); - }, - psqlImpl: (database, statement, options) => { - const text = String(statement); - sql(database, text, options.actor); - if (text.includes('CREATE ROLE')) { - created = true; createAttempts += 1; - if (mode !== 'normal') fail('FIXTURE_FAULT_CREATE_ACK_LOSS'); - } else if (text.includes('DROP ROLE')) { - dropAttempts += 1; - if (mode.includes('drop-ack')) fail('FIXTURE_FAULT_DROP_ACK_LOSS'); + for (const mode of ['normal', 'lost-create-and-drop-ack', 'lost-create-ack-and-first-readback']) { + let created = false; + let lostReadback = false; + let createAttempts = 0; + let dropAttempts = 0; + const ownership = { database: false, fixtureMembership: false, fixtureMembershipAttempted: false, + fixtureMembershipToken: null, fixtureMembershipBaseline: null }; + const dependencies = { + scalarImpl: (database, statement, actor) => { + if (mode.endsWith('first-readback') && created && !lostReadback) { + lostReadback = true; fail('FIXTURE_FAULT_READBACK_LOSS'); + } + return sql(database, statement, actor); + }, + psqlImpl: (database, statement, options) => { + const text = String(statement); + sql(database, text, options.actor); + if (text.includes('CREATE ROLE')) { + created = true; createAttempts += 1; + if (mode !== 'normal') fail('FIXTURE_FAULT_CREATE_ACK_LOSS'); + } else if (text.includes('DROP ROLE')) { + dropAttempts += 1; + if (mode.includes('drop-ack')) fail('FIXTURE_FAULT_DROP_ACK_LOSS'); + } + }, + }; + try { createFixtureMembership(ownership, dependencies); } + catch (error) { + if (!(mode.endsWith('first-readback') && lostReadback + && error.message === 'LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_FAILED')) { + const code = /^[A-Z][A-Z0-9_]{3,96}/.exec(String(error.message))?.[0] || 'UNKNOWN'; + const observed = JSON.parse(sql('postgres', `SELECT json_build_object( + 'actorSuperuser',(SELECT rolsuper FROM pg_roles WHERE rolname='supabase_admin'), + 'roles',(SELECT json_agg(json_build_object('name',rolname,'inherit',rolinherit, + 'connlimit',rolconnlimit,'validuntil',rolvaliduntil,'config',rolconfig, + 'markerPresent',shobj_description(oid,'pg_authid') IS NOT NULL)) FROM pg_roles + WHERE rolname IN('review_successor_fixture_parent_20261012','review_successor_fixture_member_20261012')), + 'memberships',(SELECT json_agg(json_build_object('parent',p.rolname,'member',m.rolname, + 'grantor',g.rolname,'admin',a.admin_option,'inherit',a.inherit_option,'set',a.set_option)) + FROM pg_auth_members a JOIN pg_roles p ON p.oid=a.roleid + JOIN pg_roles m ON m.oid=a.member JOIN pg_roles g ON g.oid=a.grantor + WHERE p.rolname IN('review_successor_fixture_parent_20261012','review_successor_fixture_member_20261012')) + )::text;`)); + writeFileSync(`${output}.failure.json`, + `${JSON.stringify({ schemaVersion: 1, status: 'failed', code, observed }, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); + fail('FIXTURE_OWNERSHIP_ROLE_PROTOCOL_FAILED'); } - }, - }; - try { createFixtureMembership(ownership, dependencies); } - catch (error) { - if (!(mode.endsWith('first-readback') && lostReadback - && error.message === 'LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_FAILED')) { - const code = /^[A-Z][A-Z0-9_]{3,96}/.exec(String(error.message))?.[0] || 'UNKNOWN'; - const observed = JSON.parse(sql('postgres', `SELECT json_build_object( - 'actorSuperuser',(SELECT rolsuper FROM pg_roles WHERE rolname='supabase_admin'), - 'roles',(SELECT json_agg(json_build_object('name',rolname,'inherit',rolinherit, - 'connlimit',rolconnlimit,'validuntil',rolvaliduntil,'config',rolconfig, - 'markerPresent',shobj_description(oid,'pg_authid') IS NOT NULL)) FROM pg_roles - WHERE rolname IN('review_successor_fixture_parent_20261012','review_successor_fixture_member_20261012')), - 'memberships',(SELECT json_agg(json_build_object('parent',p.rolname,'member',m.rolname, - 'grantor',g.rolname,'admin',a.admin_option,'inherit',a.inherit_option,'set',a.set_option)) - FROM pg_auth_members a JOIN pg_roles p ON p.oid=a.roleid - JOIN pg_roles m ON m.oid=a.member JOIN pg_roles g ON g.oid=a.grantor - WHERE p.rolname IN('review_successor_fixture_parent_20261012','review_successor_fixture_member_20261012')) - )::text;`)); - writeFileSync(`${output}.failure.json`, - `${JSON.stringify({ schemaVersion: 1, status: 'failed', code, observed }, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); - fail('FIXTURE_OWNERSHIP_ROLE_PROTOCOL_FAILED'); } + check(dropFixtureMembership(ownership, dependencies)); + check(!ownership.fixtureMembership && !ownership.fixtureMembershipAttempted && createAttempts === 1 && dropAttempts === 1); + results.push({ kind: 'roles', mode, createAttempts, dropAttempts, cleaned: true }); } - check(dropFixtureMembership(ownership, dependencies)); - check(!ownership.fixtureMembership && !ownership.fixtureMembershipAttempted && createAttempts === 1 && dropAttempts === 1); - results.push({ kind: 'roles', mode, createAttempts, dropAttempts, cleaned: true }); - } - sql('postgres', 'CREATE DATABASE ranking_clone OWNER postgres TEMPLATE template0;'); - for (const mode of ['normal', 'lost-create-ack', 'lost-create-ack-and-first-readback']) { - let created = false; - let lostReadback = false; - let createAttempts = 0; - let dropAttempts = 0; - const ownership = { database: false }; - const dependencies = { - readFileImpl: () => Buffer.from('-- ownership-only fixture: schema application excluded'), - scalarImpl: (database, statement, actor) => { - if (mode.endsWith('first-readback') && created && !lostReadback) { - lostReadback = true; fail('FIXTURE_FAULT_READBACK_LOSS'); - } - return sql(database, statement, actor); - }, - psqlImpl: (database, statement, options = {}) => { - // The ownership protocol is exercised against PG; predecessor/fixture SQL is outside this check. - if (database !== 'postgres') return {}; - const text = String(statement); - sql(database, text, options.actor); - if (/CREATE DATABASE/.test(text)) { - created = true; createAttempts += 1; - if (mode !== 'normal') fail('FIXTURE_FAULT_CREATE_ACK_LOSS'); - } else if (/DROP DATABASE/.test(text)) dropAttempts += 1; - return {}; - }, - }; - try { createOwnedDatabase(ownership, dependencies); } - catch (error) { check(mode.endsWith('first-readback') && created && lostReadback - && error.message === 'LOCAL_REHEARSAL_DATABASE_READBACK_FAILED'); } - dropOwnedDatabase(ownership, dependencies); - check(!ownership.database && !ownership.databaseAttempted && createAttempts === 1 && dropAttempts === 1); - results.push({ kind: 'database', mode, createAttempts, dropAttempts, cleaned: true }); + sql('postgres', 'CREATE DATABASE ranking_clone OWNER postgres TEMPLATE template0;'); + for (const mode of ['normal', 'lost-create-ack', 'lost-create-ack-and-first-readback']) { + let created = false; + let lostReadback = false; + let createAttempts = 0; + let dropAttempts = 0; + const ownership = { database: false }; + const dependencies = { + readFileImpl: () => Buffer.from('-- ownership-only fixture: schema application excluded'), + scalarImpl: (database, statement, actor) => { + if (mode.endsWith('first-readback') && created && !lostReadback) { + lostReadback = true; fail('FIXTURE_FAULT_READBACK_LOSS'); + } + return sql(database, statement, actor); + }, + psqlImpl: (database, statement, options = {}) => { + // The ownership protocol is exercised against PG; predecessor/fixture SQL is outside this check. + if (database !== 'postgres') return {}; + const text = String(statement); + sql(database, text, options.actor); + if (/CREATE DATABASE/.test(text)) { + created = true; createAttempts += 1; + if (mode !== 'normal') fail('FIXTURE_FAULT_CREATE_ACK_LOSS'); + } else if (/DROP DATABASE/.test(text)) dropAttempts += 1; + return {}; + }, + }; + try { createOwnedDatabase(ownership, dependencies); } + catch (error) { check(mode.endsWith('first-readback') && created && lostReadback + && error.message === 'LOCAL_REHEARSAL_DATABASE_READBACK_FAILED'); } + dropOwnedDatabase(ownership, dependencies); + check(!ownership.database && !ownership.databaseAttempted && createAttempts === 1 && dropAttempts === 1); + results.push({ kind: 'database', mode, createAttempts, dropAttempts, cleaned: true }); + } + assertFixtureOwnershipBindings(sourceBindings); + } finally { + removeOwnedContainer(container, { dockerImpl: docker }); } - check(hash(readFileSync(source)) === sourceHash); -} finally { - removeOwnedContainer(container, { dockerImpl: docker }); + + check(container.removed === true); + assertFixtureOwnershipBindings(sourceBindings); + const proof = { schemaVersion: 1, kind: 'isolated-fixture-ownership-verification', status: 'passed', + recordedAtUTC: new Date().toISOString(), ...sourceBindings, + environment: { engine: '17.6', image: 'supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab' }, + results, temporaryContainerRemoved: true, sourceContainerMutation: false, hostedWrites: false, + predecessorAndFixtureSqlExercised: false }; + writeFileSync(output, `${JSON.stringify(proof, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); + console.log(JSON.stringify({ status: proof.status, scenarios: results.length, output: output.slice(root.length + 1) })); } -check(container.removed === true); -const proof = { schemaVersion: 1, kind: 'isolated-fixture-ownership-verification', status: 'passed', - recordedAtUTC: new Date().toISOString(), sourceSha256: sourceHash, - verifierSha256: hash(readFileSync(fileURLToPath(import.meta.url))), - environment: { engine: '17.6', image: 'supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab' }, - results, temporaryContainerRemoved: true, sourceContainerMutation: false, hostedWrites: false, - predecessorAndFixtureSqlExercised: false }; -writeFileSync(output, `${JSON.stringify(proof, null, 2)}\n`, { flag: 'wx', mode: 0o600 }); -console.log(JSON.stringify({ status: proof.status, scenarios: results.length, output: output.slice(root.length + 1) })); +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + runFixtureOwnershipVerification(); +} diff --git a/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs index 658fec8642..779cf90ec7 100644 --- a/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +++ b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs @@ -98,6 +98,40 @@ function readState(database, stateQuery) { } } +export function applySuccessorPlanOnce({ + database, + sql, + stateQuery, + baselineState, + terminalState, + psqlImpl = psql, + readStateImpl = readState, +}) { + try { + return Object.freeze({ + outcome: 'acknowledged', + result: psqlImpl(database, sql, { singleTransaction: true }), + }); + } catch { + let observedState; + try { + observedState = readStateImpl(database, stateQuery); + } catch { + fail('LOCAL_REHEARSAL_APPLICATION_OUTCOME_UNCERTAIN'); + } + if (equal(observedState, terminalState)) { + return Object.freeze({ + outcome: 'committed-after-acknowledgement-loss', + result: Object.freeze({ stdout: `${JSON.stringify(observedState)}\n` }), + }); + } + if (equal(observedState, baselineState)) { + fail('LOCAL_REHEARSAL_APPLICATION_ROLLED_BACK'); + } + fail('LOCAL_REHEARSAL_APPLICATION_STATE_AMBIGUOUS'); + } +} + function ledgerInsert(migration) { const vector = JSON.stringify(migration.originalStatementVector); const tag = `$vector_${sha256(vector).slice(0, 20)}$`; @@ -872,22 +906,14 @@ export async function run() { }); resetOwnedDatabase(ownership); - let success; - try { - success = psql(DATABASE, plan.sql, { singleTransaction: true }); - } catch (error) { - let stage = 0; - const diagnosticSql = plan.sql.replaceAll('MIGRATION_TERMINAL_READBACK_FAILED', () => { - stage += 1; - return `MIGRATION_TERMINAL_READBACK_FAILED_STAGE_${stage}`; - }); - try { - psql(DATABASE, diagnosticSql, { singleTransaction: true }); - } catch (diagnostic) { - fail('LOCAL_REHEARSAL_ATOMIC_STAGE_FAILED', diagnostic.message); - } - throw error; - } + const application = applySuccessorPlanOnce({ + database: DATABASE, + sql: plan.sql, + stateQuery, + baselineState: stages[0], + terminalState: stages[11], + }); + const success = application.result; const successLines = success.stdout.trim().split(/\r?\n/).filter(Boolean); const successReadback = JSON.parse(successLines.at(-1)); const committed = readState(DATABASE, stateQuery); @@ -982,6 +1008,7 @@ export async function run() { results: { advisoryContentionObserved: true, atomicCommit: true, + atomicCommitOutcome: application.outcome, atomicRollbackExact: true, catalogDriftStatePreserved: true, columnAclDriftRejected, diff --git a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS index 3e7285e293..9fdb091e51 100644 --- a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS +++ b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS @@ -1,5 +1,6 @@ +31e775a669d4a1295fc8b391653f26d0559695937f4cc357818ef53faa7c8e67 apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-0833ed81-historical d29f5fa8ddc27573b3ea2d2a144c9d9a1c24a6c5ac4d7aeef30e3bda514fc575 apps/web/performance/review-media-operating-admission-20261013/controller-reverification.json ad4d439f9609db73d5879e655380d640b3b52274dbfe80654f6d715bd7a68ab7 apps/web/performance/review-media-operating-admission-20261013/verification.json -4ad55ed526b7af8c0ee1282057dbfa3706249deea04472a2749909af45492064 apps/web/scripts/review-media-operating-admission.mjs -26d7fe68effda2813b3f9384dd2d7e4f9e8998447cd988b89820d93be7f6bdc7 apps/web/tests-unit/review-media-operating-admission.test.ts +586847d5030cedc6575328cbe78b5eb9f1d9ff47d91e8d8f1aef40076b6d2277 apps/web/scripts/review-media-operating-admission.mjs +184368546c3668da99449f3d7ec0f08b95e1c06f44ca85100a6d2dfa533bc84a apps/web/tests-unit/review-media-operating-admission.test.ts b4fbd520413f59e82743c545f521df5a8adde04970f610d4d06b432955f07518 docs/operations/review-media-operating-admission-20261013.md diff --git a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-0833ed81-historical b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-0833ed81-historical new file mode 100644 index 0000000000..3e7285e293 --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-0833ed81-historical @@ -0,0 +1,5 @@ +d29f5fa8ddc27573b3ea2d2a144c9d9a1c24a6c5ac4d7aeef30e3bda514fc575 apps/web/performance/review-media-operating-admission-20261013/controller-reverification.json +ad4d439f9609db73d5879e655380d640b3b52274dbfe80654f6d715bd7a68ab7 apps/web/performance/review-media-operating-admission-20261013/verification.json +4ad55ed526b7af8c0ee1282057dbfa3706249deea04472a2749909af45492064 apps/web/scripts/review-media-operating-admission.mjs +26d7fe68effda2813b3f9384dd2d7e4f9e8998447cd988b89820d93be7f6bdc7 apps/web/tests-unit/review-media-operating-admission.test.ts +b4fbd520413f59e82743c545f521df5a8adde04970f610d4d06b432955f07518 docs/operations/review-media-operating-admission-20261013.md diff --git a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 index e602e3ac87..c7d4a5cec2 100644 --- a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 +++ b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 @@ -1 +1 @@ -31e775a669d4a1295fc8b391653f26d0559695937f4cc357818ef53faa7c8e67 +5a41205af7546221083956ddac62488f51254b253de1d738f39ab714d4835026 diff --git a/apps/web/scripts/review-media-operating-admission.mjs b/apps/web/scripts/review-media-operating-admission.mjs index 9d46d0df5e..037b9e52a0 100644 --- a/apps/web/scripts/review-media-operating-admission.mjs +++ b/apps/web/scripts/review-media-operating-admission.mjs @@ -598,7 +598,7 @@ export function prepareReviewMediaApplyAdmission({ const priorStateSha256 = sha256(JSON.stringify(canonical(receipt.stageStates?.[0]))); const receiptBindings = { admissionSha256: HEX_SHA256.test(receipt.admissionSha256 ?? ''), - attemptId: UUID.test(receipt.attemptId ?? ''), + attemptId: receipt.attemptId === rehearsal.attemptId, common: receipt.schemaVersion === 1 && receipt.id === manifestRecord.manifest.id && receipt.projectRef === manifestRecord.manifest.projectRef diff --git a/apps/web/scripts/review-media-sql-successor.mjs b/apps/web/scripts/review-media-sql-successor.mjs index dd9a50e3af..4e86b241e2 100644 --- a/apps/web/scripts/review-media-sql-successor.mjs +++ b/apps/web/scripts/review-media-sql-successor.mjs @@ -29,6 +29,10 @@ import { executeMigrationBundle, elevenMigrationBundleSuffixRoot, } from './supabase-migration-bundle.mjs'; +import { + atomicMigrationSql, + statementSpans, +} from './supabase-migration-transaction.mjs'; import { TZUDONG_GITHUB_REPOSITORY, TZUDONG_PRODUCTION_ALIASES, @@ -41,6 +45,7 @@ import { const SCRIPT_PATH = fileURLToPath(import.meta.url); export const REPOSITORY_ROOT = resolve(dirname(SCRIPT_PATH), '../../..'); export const SUCCESSOR_MANIFEST_PATH = resolve(REPOSITORY_ROOT, '.github/review-media-sql-successor.v1.json'); +export const SUCCESSOR_PRE_COMMIT_EXPIRY_MARKER = '-- successor-pre-commit-expiry-guard'; const HEX_SHA256 = /^[0-9a-f]{64}$/; const REVISION = /^[0-9a-f]{40}$/; @@ -636,15 +641,15 @@ function assertReceiptWindow(receipt, admission, createdAt, expiresAt, nowMs) { || Date.parse(receipt.expiresAt) !== expiresAt) fail('SUCCESSOR_ADMISSION_INVALID'); } -function assertPriorReceiptWindow(receipt, createdAt, expiresAt, nowMs) { +function assertPriorReceiptWindow(receipt, expiresAt, nowMs) { const observedAt = Date.parse(receipt.observedAt); const receiptExpiresAt = Date.parse(receipt.expiresAt); + const receiptLeaseStartedAt = receiptExpiresAt - ADMISSION_MAX_AGE_MS; if (!Number.isFinite(observedAt) || !Number.isFinite(receiptExpiresAt) || receiptExpiresAt <= observedAt || receiptExpiresAt - observedAt > ADMISSION_MAX_AGE_MS - || observedAt < createdAt - ADMISSION_MAX_AGE_MS - || observedAt > createdAt + 30_000 + || observedAt < receiptLeaseStartedAt || observedAt > nowMs + 30_000 || receiptExpiresAt < expiresAt || nowMs >= receiptExpiresAt) fail('SUCCESSOR_ADMISSION_INVALID'); @@ -730,7 +735,7 @@ function validatePrivateReceipts(admission, manifestRecord, { || !equal(rehearsal.stageStates, admission.stageStates)) { fail('SUCCESSOR_ADMISSION_INVALID'); } - assertPriorReceiptWindow(rehearsal, createdAt, expiresAt, nowMs); + assertPriorReceiptWindow(rehearsal, expiresAt, nowMs); const journal = readPrivateCanonicalJournal( receiptDirectory, @@ -754,6 +759,7 @@ function validatePrivateReceipts(admission, manifestRecord, { const verifiedAt = Date.parse(verification.verifiedAt); const publishedAt = Date.parse(publication.publishedAt); const observedAt = Date.parse(rehearsal.observedAt); + const rehearsalLeaseStartedAt = Date.parse(rehearsal.expiresAt) - ADMISSION_MAX_AGE_MS; if (started.event !== 'started' || verification.event !== 'verification' || verification.code !== 'SUCCESSOR_REHEARSAL_VERIFIED' @@ -779,13 +785,15 @@ function validatePrivateReceipts(admission, manifestRecord, { || !Number.isFinite(startedAt) || !Number.isFinite(verifiedAt) || !Number.isFinite(publishedAt) + || startedAt < rehearsalLeaseStartedAt + || observedAt < rehearsalLeaseStartedAt + || verifiedAt < rehearsalLeaseStartedAt + || publishedAt < rehearsalLeaseStartedAt || startedAt > observedAt || verifiedAt < observedAt || publishedAt < verifiedAt || verifiedAt >= Date.parse(rehearsal.expiresAt) || publishedAt >= Date.parse(rehearsal.expiresAt) - || verifiedAt > createdAt + 30_000 - || publishedAt > createdAt + 30_000 || verifiedAt > nowMs + 30_000 || publishedAt > nowMs + 30_000) fail('SUCCESSOR_ADMISSION_INVALID'); } @@ -1044,6 +1052,31 @@ export function assertLaunchReady(manifest) { if (manifest.launchPolicy.state !== 'ready') fail('SUCCESSOR_LAUNCH_HELD'); } +function successorExpirySql(admission, suffix, { armTransactionTimeout = false } = {}) { + const tag = `$successor_expiry_${sha256(`${admission.expiresAt}:${suffix}`).slice(0, 18)}$`; + const armTimeout = armTransactionTimeout + ? `\n PERFORM set_config('transaction_timeout',least(${ADMISSION_MAX_AGE_MS},greatest(1,ceil(extract(epoch FROM (${quote(admission.expiresAt)}::timestamptz-transaction_timestamp()))*1000)::bigint))::text,true);` + : ''; + return `DO ${tag} BEGIN IF clock_timestamp() >= ${quote(admission.expiresAt)}::timestamptz THEN RAISE EXCEPTION 'SUCCESSOR_ADMISSION_EXPIRED'; END IF;${armTimeout} END ${tag};`; +} + +function stripCompiledStageReadback(sql) { + const spans = statementSpans(sql); + const read = spans.at(-1); + if (!read || !/^SELECT\b/i.test(read.token)) fail('SUCCESSOR_SOURCE_DRIFT'); + return sql.slice(0, read.start).trimEnd(); +} + +function guardCompiledSuccessorStages(compiled, migrations, admission) { + const stages = compiled.plans.map((plan, index) => stripCompiledStageReadback( + atomicMigrationSql(plan, migrations[index]), + )); + const compiledPrefix = stages.join('\n'); + if (!compiled.sql.startsWith(`${compiledPrefix}\n`)) fail('SUCCESSOR_SOURCE_DRIFT'); + const tail = compiled.sql.slice(compiledPrefix.length + 1); + return `${stages.map((stage, index) => `${stage}\n${successorExpirySql(admission, `stage-${index + 1}`)}`).join('\n')}\n${tail}${SUCCESSOR_PRE_COMMIT_EXPIRY_MARKER}\n${successorExpirySql(admission, 'pre-commit')}\n`; +} + export function compileSuccessorPlan(manifestRecord, admission) { const { manifest, materials } = manifestRecord; const suffixRoot = elevenMigrationBundleSuffixRoot(manifest.migrations); @@ -1069,8 +1102,8 @@ export function compileSuccessorPlan(manifestRecord, admission) { finalReadback: { query: stateQuery, expected: terminal }, }, materials); const lockTag = `$successor_${sha256(manifest.advisoryLockKey).slice(0, 20)}$`; - const expiryTag = `$successor_expiry_${sha256(admission.expiresAt).slice(0, 20)}$`; - const guardedSql = `SET LOCAL lock_timeout='2s';\nSET LOCAL statement_timeout='15s';\nSET LOCAL idle_in_transaction_session_timeout='30s';\nDO ${lockTag} BEGIN PERFORM pg_advisory_xact_lock(${manifest.advisoryLockKey}::bigint); END ${lockTag};\nDO ${expiryTag} BEGIN IF clock_timestamp() >= ${quote(admission.expiresAt)}::timestamptz THEN RAISE EXCEPTION 'SUCCESSOR_ADMISSION_EXPIRED'; END IF; END ${expiryTag};\n${compiled.sql}`; + const guardedStages = guardCompiledSuccessorStages(compiled, migrations, admission); + const guardedSql = `SET LOCAL lock_timeout='2s';\nSET LOCAL statement_timeout='15s';\nSET LOCAL idle_in_transaction_session_timeout='30s';\nDO ${lockTag} BEGIN PERFORM pg_advisory_xact_lock(${manifest.advisoryLockKey}::bigint); END ${lockTag};\n${successorExpirySql(admission, 'post-lock', { armTransactionTimeout: true })}\n${guardedStages}`; return Object.freeze({ ...compiled, sql: guardedSql, diff --git a/apps/web/tests-unit/review-media-fixture-ownership-verifier.test.ts b/apps/web/tests-unit/review-media-fixture-ownership-verifier.test.ts new file mode 100644 index 0000000000..2f483a14cd --- /dev/null +++ b/apps/web/tests-unit/review-media-fixture-ownership-verifier.test.ts @@ -0,0 +1,39 @@ +import { expect, test } from 'bun:test'; +import { createHash } from 'node:crypto'; +import { basename } from 'node:path'; +import { + assertFixtureOwnershipBindings, + fixtureOwnershipBindings, +} from '../performance/review-db-successor-20261012/verify-fixture-ownership.mjs'; + +const sha256 = (bytes: Buffer) => createHash('sha256').update(bytes).digest('hex'); +const sourceBytes = new Map([ + ['verify-review-media-sql-successor.mjs', Buffer.from('fixture role and database helper')], + ['verify-review-media-sql-rehearsal.mjs', Buffer.from('container ownership helper')], + ['verify-fixture-ownership.mjs', Buffer.from('ownership verifier')], +]); +const readFileImpl = (path: string) => { + const bytes = sourceBytes.get(basename(path)); + if (!bytes) throw new Error('unexpected source path'); + return bytes; +}; + +test('ownership evidence binds both imported implementations and the verifier', () => { + const bindings = fixtureOwnershipBindings({ readFileImpl }); + expect(bindings).toEqual({ + sourceSha256: sha256(sourceBytes.get('verify-review-media-sql-successor.mjs')!), + containerHelperSha256: sha256(sourceBytes.get('verify-review-media-sql-rehearsal.mjs')!), + verifierSha256: sha256(sourceBytes.get('verify-fixture-ownership.mjs')!), + }); + expect(assertFixtureOwnershipBindings(bindings, { readFileImpl })).toEqual(bindings); +}); + +for (const path of sourceBytes.keys()) { + test(`changed ${path} invalidates ownership evidence`, () => { + const bindings = fixtureOwnershipBindings({ readFileImpl }); + const changedReader = (sourcePath: string) => basename(sourcePath) === path + ? Buffer.from('changed implementation') : readFileImpl(sourcePath); + expect(() => assertFixtureOwnershipBindings(bindings, { readFileImpl: changedReader })) + .toThrow('FIXTURE_OWNERSHIP_SOURCE_CHANGED'); + }); +} diff --git a/apps/web/tests-unit/review-media-operating-admission.test.ts b/apps/web/tests-unit/review-media-operating-admission.test.ts index bc16f2a9f6..b20c191380 100644 --- a/apps/web/tests-unit/review-media-operating-admission.test.ts +++ b/apps/web/tests-unit/review-media-operating-admission.test.ts @@ -442,6 +442,9 @@ test('apply producer binds the verified rehearsal artifacts without extending th now: fixedNow, }); const admission = JSON.parse(readFileSync(result.admissionPath, 'utf8')); + expect(published.receipt.attemptId).toBe(published.admission.attemptId); + expect(admission.attemptId).toBe(applyInputDocument.attemptId); + expect(admission.attemptId).not.toBe(published.admission.attemptId); expect(admission.createdAt).toBe(createdAt); expect(admission.expiresAt).toBe(expiresAt); expect(admission.stageStates).toEqual(published.receipt.stageStates); @@ -477,6 +480,49 @@ test('apply producer rejects a relabeled or tampered rehearsal before writing an } }); +test('apply producer rejects a fully rehashed receipt and journal relabeled to another rehearsal attempt', () => { + const prepared = prepareRehearsal(); + const published = publishSyntheticRehearsal(prepared.custodyDirectory); + const relabeledAttemptId = '123e4567-e89b-42d3-a456-426614174102'; + const receiptPath = join(prepared.custodyDirectory, 'rehearsal-receipt.json'); + const journalPath = join(prepared.custodyDirectory, 'rehearsal-journal.jsonl'); + const relabeledReceipt = { + ...published.receipt, + attemptId: relabeledAttemptId, + }; + const relabeledReceiptBytes = canonicalBytes(relabeledReceipt); + const relabeledReceiptSha256 = sha256(relabeledReceiptBytes); + const relabeledJournal = readFileSync(journalPath, 'utf8').trimEnd().split('\n').map(line => { + const entry = JSON.parse(line); + return { + ...entry, + attemptId: relabeledAttemptId, + ...(entry.receiptSha256 ? { receiptSha256: relabeledReceiptSha256 } : {}), + }; + }); + const relabeledJournalBytes = Buffer.from( + `${relabeledJournal.map(entry => JSON.stringify(canonical(entry))).join('\n')}\n`, + ); + writeFileSync(receiptPath, relabeledReceiptBytes, { mode: 0o600 }); + writeFileSync(journalPath, relabeledJournalBytes, { mode: 0o600 }); + chmodSync(receiptPath, 0o600); + chmodSync(journalPath, 0o600); + + const inputPath = join(prepared.root, 'apply-input-relabeled-attempt.json'); + writePrivateCanonical(inputPath, applyInput(published, { + rehearsalJournalSha256: sha256(relabeledJournalBytes), + rehearsalReceiptSha256: relabeledReceiptSha256, + })); + + expect(() => prepareReviewMediaApplyAdmission({ + custodyDirectory: prepared.custodyDirectory, + inputPath, + manifestRecord, + now: fixedNow, + })).toThrow('SUCCESSOR_OPERATING_ADMISSION_EVIDENCE_INVALID'); + expect(existsSync(join(prepared.custodyDirectory, 'apply-admission.json'))).toBe(false); +}); + test('apply producer treats a dangling reserved journal symlink as an existing output', () => { const prepared = prepareRehearsal(); const published = publishSyntheticRehearsal(prepared.custodyDirectory); diff --git a/apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts b/apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts index 669a821aa7..e27f2446f9 100644 --- a/apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts +++ b/apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts @@ -2,21 +2,22 @@ import { expect, test } from 'bun:test'; import { createHash } from 'node:crypto'; import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; -import { join, resolve } from 'node:path'; +import { basename, join, resolve } from 'node:path'; import { createOwnedContainer, + DEFAULT_RETAINED_PROOF_SELECTOR_PATH, + injectFinalApplyExpiryFault, inspectRetainedProof, parseArguments, publishCurrentProof, removeOwnedContainer, } from '../performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs'; -const retainedProofRepositoryPath = 'apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json'; -const retainedProofSha256 = 'cb766e279baad0315e9d7055bc326b41b78a7d50a4972dac3214bc071aee0e49'; const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex'); type Ownership = { + attempted?: boolean; created: boolean; id: string | null; name: string | null; @@ -34,12 +35,14 @@ type DockerOptions = { wrongOwnerLabel?: boolean; keepAfterRemoveFailure?: boolean; ackLostOnRemove?: boolean; + transientInspectFailures?: number; }; function dockerFixture(options: DockerOptions = {}) { const containers = new Map(); const calls: string[][] = []; const id = 'a'.repeat(64); + let transientInspectFailures = options.transientInspectFailures ?? 0; const dockerImpl = (args: string[]) => { calls.push([...args]); if (args[0] === 'run') { @@ -64,6 +67,10 @@ function dockerFixture(options: DockerOptions = {}) { }; } if (args[0] === 'container' && args[1] === 'inspect') { + if (transientInspectFailures > 0) { + transientInspectFailures -= 1; + return { status: 1, stdout: '', stderr: 'temporary daemon read failure' }; + } const target = args[2]; const value = [...containers.values()].find(container => ( container.id === target || container.name === target @@ -99,50 +106,173 @@ function freshOwnership(): Ownership { return { created: false, id: null, name: null, token: null }; } -test('checked-in rollback proof is checksum-verified but never reused as current-run output', () => { - const manifestBytes = readFileSync(new URL('../../../.github/review-media-sql-successor.v1.json', import.meta.url)); - const manifest = JSON.parse(manifestBytes.toString('utf8')); - const harnessBytes = readFileSync(new URL( - '../performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs', - import.meta.url, - )); - const retained = inspectRetainedProof({ - bindings: { - manifestSha256: sha256(manifestBytes), - rehearsalHarnessSha256: sha256(harnessBytes), - sourceRoot: manifest.sourceRoot, - }, +test('default retained proof uses a stable current-proof selector', () => { + expect(basename(DEFAULT_RETAINED_PROOF_SELECTOR_PATH)).toBe('retained-current-proof.json'); + const selector = JSON.parse(readFileSync(DEFAULT_RETAINED_PROOF_SELECTOR_PATH, 'utf8')); + expect(selector).toMatchObject({ + schemaVersion: 1, + kind: 'review-media-sql-retained-current-proof', }); - + const retained = inspectRetainedProof(); + expect(basename(retained.repositoryPath)).toBe(selector.proofPath); + expect(retained.actualSha256).toBe(selector.proofSha256); + expect(retained.usedAsCurrentProof).toBe(false); expect(parseArguments([])).toEqual({ outputPath: null }); - expect(retained).toMatchObject({ - present: true, - classification: 'retained-historical-evidence', - repositoryPath: retainedProofRepositoryPath, - expectedSha256: retainedProofSha256, - actualSha256: retainedProofSha256, - currentSourceMatch: false, - usedAsCurrentProof: false, - }); +}); - const stdout: string[] = []; - const writes: unknown[][] = []; - const proof = { status: 'passed', sourceRoot: manifest.sourceRoot }; - const envelope = publishCurrentProof( - { retainedProof: retained, proof, outputPath: null }, - { - writeFileImpl: (...args: unknown[]) => { writes.push(args); }, - writeStdoutImpl: (value: string) => { stdout.push(value); }, - }, - ); - expect(writes).toHaveLength(0); - expect(stdout).toHaveLength(1); - expect(JSON.parse(stdout[0])).toEqual(envelope); - expect(envelope).toMatchObject({ - kind: 'review-media-sql-local-rehearsal', - currentProof: proof, - currentOutputPath: null, - }); +test('selector rejects unauthenticated bytes, path escape, and proof-SHA relabelling', () => { + const directory = mkdtempSync(join(tmpdir(), 'tzudong-proof-selector-')); + try { + const proofPath = join(directory, 'proof.json'); + const selectorPath = join(directory, 'retained-current-proof.json'); + const checksumsPath = join(directory, 'SHA256SUMS'); + const checksumsDigestPath = join(directory, 'SHA256SUMS.sha256'); + const proof = Buffer.from('{}\n'); + writeFileSync(proofPath, proof); + const writeAuthenticatedMap = (selector: Buffer) => { + writeFileSync(selectorPath, selector); + const checksums = Buffer.from([ + `${sha256(proof)} proof.json`, + `${sha256(selector)} retained-current-proof.json`, + '', + ].join('\n')); + writeFileSync(checksumsPath, checksums); + writeFileSync(checksumsDigestPath, `${sha256(checksums)}\n`); + }; + + const escaped = Buffer.from(`${JSON.stringify({ + schemaVersion: 1, + kind: 'review-media-sql-retained-current-proof', + proofPath: '../proof.json', + proofSha256: sha256(proof), + })}\n`); + writeAuthenticatedMap(escaped); + expect(() => inspectRetainedProof({ + selectorPath, + checksumsPath, + checksumsDigestPath, + repositoryRoot: directory, + })).toThrow('LOCAL_REHEARSAL_SELECTOR_INVALID'); + + const relabelled = Buffer.from(`${JSON.stringify({ + schemaVersion: 1, + kind: 'review-media-sql-retained-current-proof', + proofPath: 'proof.json', + proofSha256: '0'.repeat(64), + })}\n`); + writeAuthenticatedMap(relabelled); + expect(() => inspectRetainedProof({ + selectorPath, + checksumsPath, + checksumsDigestPath, + repositoryRoot: directory, + })).toThrow('LOCAL_REHEARSAL_SELECTOR_PROOF_SHA256_MISMATCH'); + + const valid = Buffer.from(`${JSON.stringify({ + schemaVersion: 1, + kind: 'review-media-sql-retained-current-proof', + proofPath: 'proof.json', + proofSha256: sha256(proof), + })}\n`); + writeAuthenticatedMap(valid); + writeFileSync(selectorPath, Buffer.concat([valid, Buffer.from(' ')])); + expect(() => inspectRetainedProof({ + selectorPath, + checksumsPath, + checksumsDigestPath, + repositoryRoot: directory, + })).toThrow('LOCAL_REHEARSAL_SELECTOR_SHA256_MISMATCH'); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +test('retained proof verifies the complete non-circular artifact map from one byte snapshot', () => { + const directory = mkdtempSync(join(tmpdir(), 'tzudong-current-proof-map-')); + try { + const outputPath = join(directory, 'pg17-terminal-boundaries-20261013.json'); + const selectorPath = join(directory, 'retained-current-proof.json'); + const dependencyPath = join(directory, 'source.mjs'); + const checksumsPath = join(directory, 'SHA256SUMS'); + const checksumsDigestPath = join(directory, 'SHA256SUMS.sha256'); + const proof = Buffer.from(`${JSON.stringify({ + manifestSha256: '1'.repeat(64), + rehearsalHarnessSha256: '2'.repeat(64), + sourceRoot: '3'.repeat(64), + })}\n`); + const dependency = Buffer.from('export const current = true;\n'); + const selector = Buffer.from(`${JSON.stringify({ + schemaVersion: 1, + kind: 'review-media-sql-retained-current-proof', + proofPath: 'pg17-terminal-boundaries-20261013.json', + proofSha256: sha256(proof), + })}\n`); + writeFileSync(outputPath, proof); + writeFileSync(selectorPath, selector); + writeFileSync(dependencyPath, dependency); + const checksums = Buffer.from([ + `${sha256(proof)} pg17-terminal-boundaries-20261013.json`, + `${sha256(selector)} retained-current-proof.json`, + `${sha256(dependency)} source.mjs`, + '', + ].join('\n')); + writeFileSync(checksumsPath, checksums); + writeFileSync(checksumsDigestPath, `${sha256(checksums)}\n`); + + const retained = inspectRetainedProof({ + selectorPath, + checksumsPath, + checksumsDigestPath, + repositoryRoot: directory, + bindings: { + manifestSha256: '1'.repeat(64), + rehearsalHarnessSha256: '2'.repeat(64), + sourceRoot: '3'.repeat(64), + }, + }); + expect(retained).toMatchObject({ + present: true, + classification: 'retained-current-source-evidence', + repositoryPath: 'pg17-terminal-boundaries-20261013.json', + selector: { + repositoryPath: 'retained-current-proof.json', + sha256: sha256(selector), + proofPath: 'pg17-terminal-boundaries-20261013.json', + proofSha256: sha256(proof), + }, + expectedSha256: sha256(proof), + actualSha256: sha256(proof), + currentSourceMatch: true, + usedAsCurrentProof: false, + }); + + writeFileSync(dependencyPath, 'tampered\n'); + expect(() => inspectRetainedProof({ + selectorPath, + checksumsPath, + checksumsDigestPath, + repositoryRoot: directory, + })).toThrow('LOCAL_REHEARSAL_ARTIFACT_SHA256_MISMATCH'); + + rmSync(dependencyPath); + expect(() => inspectRetainedProof({ + selectorPath, + checksumsPath, + checksumsDigestPath, + repositoryRoot: directory, + })).toThrow('LOCAL_REHEARSAL_ARTIFACT_MISSING'); + + rmSync(outputPath); + expect(() => inspectRetainedProof({ + selectorPath, + checksumsPath, + checksumsDigestPath, + repositoryRoot: directory, + required: true, + })).toThrow('LOCAL_REHEARSAL_CURRENT_PROOF_MISSING'); + } finally { + rmSync(directory, { recursive: true, force: true }); + } }); test('retained rollback proof rejects bytes not authorized by SHA256SUMS', () => { @@ -162,6 +292,7 @@ test('retained rollback proof rejects bytes not authorized by SHA256SUMS', () => expect(() => inspectRetainedProof({ outputPath, checksumsPath, + checksumsDigestPath: null, repositoryRoot: directory, bindings: { manifestSha256: '1'.repeat(64), @@ -191,6 +322,26 @@ test('explicit current output is exclusive and separate from retained proof', () expect(JSON.parse(stdout[0]).currentOutputPath).toBe(outputPath); }); +test('apply expiry fault changes only the final wrapper guard after the exported marker', () => { + const expiresAt = '9999-12-31T23:59:59.999Z'; + const marker = '-- successor-pre-commit-expiry-guard'; + const before = `DO $stage$ BEGIN IF clock_timestamp() >= '${expiresAt}'::timestamptz THEN NULL; END IF; END $stage$;\n`; + const final = `DO $final$ BEGIN IF clock_timestamp() >= '${expiresAt}'::timestamptz THEN RAISE EXCEPTION 'SUCCESSOR_ADMISSION_EXPIRED'; END IF; END $final$;\n`; + const source = `${before}${marker}\n${final}`; + const fault = injectFinalApplyExpiryFault(source, expiresAt); + expect(fault).toMatchObject({ + marker, + originalDeadline: expiresAt, + injectedDeadline: '1970-01-01T00:00:00.000Z', + wrapperOnlyFaultInjection: true, + }); + expect(fault.sql.slice(0, fault.sql.indexOf(marker))).toBe(before); + expect(fault.sql.slice(fault.sql.indexOf(marker))).toContain("'1970-01-01T00:00:00.000Z'::timestamptz"); + expect(fault.sql.match(new RegExp(expiresAt.replaceAll('.', '\\.'), 'g'))).toHaveLength(1); + expect(() => injectFinalApplyExpiryFault(before, expiresAt)) + .toThrow('LOCAL_REHEARSAL_APPLY_EXPIRY_MARKER_INVALID'); +}); + test('container create acknowledgement loss is reconciled by unique name and owner label', () => { const fixture = dockerFixture({ ackLostOnCreate: true }); const ownership = freshOwnership(); @@ -213,6 +364,30 @@ test('container create acknowledgement loss is reconciled by unique name and own expect(run).toContain(`tzudong.review-rehearsal.owner=${'1'.repeat(32)}`); }); +test('first inspect failure after uncertain create is reconciled and cleaned by exact owned ID', () => { + const fixture = dockerFixture({ ackLostOnCreate: true, transientInspectFailures: 1 }); + const ownership = freshOwnership(); + expect(() => createOwnedContainer(ownership, { + dockerImpl: fixture.dockerImpl, + generateToken: () => '9'.repeat(32), + processId: 4248, + })).toThrow('LOCAL_REHEARSAL_CONTAINER_INSPECTION_FAILED'); + expect(ownership).toMatchObject({ + attempted: true, + created: false, + id: null, + name: 'tzudong-review-rollback-rehearsal-4248-999999999999', + token: '9'.repeat(32), + }); + + expect(removeOwnedContainer(ownership, { dockerImpl: fixture.dockerImpl })).toBe(true); + expect(ownership).toMatchObject({ created: false, removed: true, id: fixture.id }); + expect(fixture.calls.filter(args => args[0] === 'run')).toHaveLength(1); + expect(fixture.calls.find(args => args[0] === 'container' && args[1] === 'rm')) + .toEqual(['container', 'rm', '--force', fixture.id]); + expect(fixture.containers.size).toBe(0); +}); + test('same-process rehearsal runs derive distinct container names from their owner tokens', () => { const firstFixture = dockerFixture(); const secondFixture = dockerFixture(); @@ -232,7 +407,7 @@ test('same-process rehearsal runs derive distinct container names from their own expect(first.token).not.toBe(second.token); }); -test('create failure without an owned readback never authorizes cleanup', () => { +test('create failure reconciles proven absence without issuing a removal', () => { const fixture = dockerFixture({ failBeforeCreate: true }); const ownership = freshOwnership(); expect(() => createOwnedContainer(ownership, { @@ -240,7 +415,8 @@ test('create failure without an owned readback never authorizes cleanup', () => generateToken: () => '2'.repeat(32), processId: 4243, })).toThrow('LOCAL_REHEARSAL_CONTAINER_CREATION_UNCONFIRMED'); - expect(removeOwnedContainer(ownership, { dockerImpl: fixture.dockerImpl })).toBe(false); + expect(removeOwnedContainer(ownership, { dockerImpl: fixture.dockerImpl })).toBe(true); + expect(ownership).toMatchObject({ created: false, removed: true }); expect(fixture.calls.some(args => args[0] === 'container' && args[1] === 'rm')).toBe(false); }); diff --git a/apps/web/tests-unit/review-media-sql-successor-harness.test.ts b/apps/web/tests-unit/review-media-sql-successor-harness.test.ts index c14ae23c9b..12c5d61b85 100644 --- a/apps/web/tests-unit/review-media-sql-successor-harness.test.ts +++ b/apps/web/tests-unit/review-media-sql-successor-harness.test.ts @@ -5,6 +5,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { + applySuccessorPlanOnce, createFixtureMembership, createOwnedDatabase, dropFixtureMembership, @@ -133,6 +134,63 @@ function databaseOwnership() { }; } +test('successor acknowledgement loss reconciles a committed terminal state without SQL replay', () => { + const baselineState = { bundle: { targetCount: 0 }, state: { terminalContractReady: false } }; + const terminalState = { bundle: { targetCount: 11 }, state: { terminalContractReady: true } }; + let state = baselineState; + const sqlCalls: string[] = []; + let readbacks = 0; + + const application = applySuccessorPlanOnce({ + database: 'fixture_database', + sql: 'BEGIN; SELECT successor_mutation(); COMMIT;', + stateQuery: 'SELECT successor_state();', + baselineState, + terminalState, + psqlImpl: (_database: string, sql: string) => { + sqlCalls.push(sql); + state = terminalState; + throw new Error('response lost after commit'); + }, + readStateImpl: (_database: string, query: string) => { + expect(query).toBe('SELECT successor_state();'); + readbacks += 1; + return state; + }, + }); + + expect(application.outcome).toBe('committed-after-acknowledgement-loss'); + expect(JSON.parse(application.result.stdout.trim())).toEqual(terminalState); + expect(sqlCalls).toEqual(['BEGIN; SELECT successor_mutation(); COMMIT;']); + expect(readbacks).toBe(1); +}); + +test('successor failure at the baseline is classified without diagnostic SQL replay', () => { + const baselineState = { bundle: { targetCount: 0 }, state: { terminalContractReady: false } }; + const terminalState = { bundle: { targetCount: 11 }, state: { terminalContractReady: true } }; + const sqlCalls: string[] = []; + let readbacks = 0; + + expect(() => applySuccessorPlanOnce({ + database: 'fixture_database', + sql: 'BEGIN; SELECT successor_mutation(); COMMIT;', + stateQuery: 'SELECT successor_state();', + baselineState, + terminalState, + psqlImpl: (_database: string, sql: string) => { + sqlCalls.push(sql); + throw new Error('transaction rolled back before acknowledgement'); + }, + readStateImpl: () => { + readbacks += 1; + return baselineState; + }, + })).toThrow('LOCAL_REHEARSAL_APPLICATION_ROLLED_BACK'); + + expect(sqlCalls).toEqual(['BEGIN; SELECT successor_mutation(); COMMIT;']); + expect(readbacks).toBe(1); +}); + test('retained proof is checksum-verified and distinguished from the current harness source', () => { const manifestBytes = readFileSync(new URL('../../../.github/review-media-sql-successor.v1.json', import.meta.url)); const manifest = JSON.parse(manifestBytes.toString('utf8')); diff --git a/apps/web/tests-unit/review-media-sql-successor.test.ts b/apps/web/tests-unit/review-media-sql-successor.test.ts index 442030e158..ca9c240ea2 100644 --- a/apps/web/tests-unit/review-media-sql-successor.test.ts +++ b/apps/web/tests-unit/review-media-sql-successor.test.ts @@ -27,6 +27,7 @@ import { protectedSourceReadbackBinding, runReviewMediaSuccessor, runReviewMediaSuccessorRehearsal, + SUCCESSOR_PRE_COMMIT_EXPIRY_MARKER, validateSuccessorAdmission, validateSuccessorRehearsalAdmission, } from '../scripts/review-media-sql-successor.mjs'; @@ -272,6 +273,38 @@ function custodyFiles(document = admission()) { return { admissionPath, directory, document, journalPath, rehearsalJournalPath }; } +function rewriteRehearsalTimeline(files: ReturnType, { + expiresAt = files.document.expiresAt, + observedAt, + publishedAt, + startedAt, + verifiedAt, +}: { + expiresAt?: string; + observedAt: string; + publishedAt: string; + startedAt: string; + verifiedAt: string; +}) { + const receiptPath = join(files.directory, 'rehearsal-receipt.json'); + const receipt = JSON.parse(readFileSync(receiptPath, 'utf8')); + receipt.expiresAt = expiresAt; + receipt.observedAt = observedAt; + const receiptBytes = canonicalBytes(receipt); + writeFileSync(receiptPath, receiptBytes, { mode: 0o600 }); + files.document.rehearsalReceiptSha256 = sha256(receiptBytes); + + const journal = readFileSync(files.rehearsalJournalPath, 'utf8').trim().split('\n').map(JSON.parse); + journal[0].startedAt = startedAt; + journal[1].verifiedAt = verifiedAt; + journal[1].receiptSha256 = files.document.rehearsalReceiptSha256; + journal[2].publishedAt = publishedAt; + journal[2].receiptSha256 = files.document.rehearsalReceiptSha256; + const journalBytes = Buffer.from(`${journal.map(entry => JSON.stringify(canonical(entry))).join('\n')}\n`); + writeFileSync(files.rehearsalJournalPath, journalBytes, { mode: 0o600 }); + files.document.rehearsalJournalSha256 = sha256(journalBytes); +} + function rehearsalAdmission() { const writerFences = Object.fromEntries(Object.entries(manifest.requiredWriterFences).map( ([key, state], index) => [key, { evidenceSha256: (index + 8).toString(16).repeat(64), state }], @@ -432,6 +465,10 @@ test('compiled plan has one advisory lock, one outer transaction and eleven ledg expect(plan.transactionMode).toBe('single-outer-transaction'); expect(plan.sql.match(/pg_advisory_xact_lock\(7311754282260211::bigint\)/g)).toHaveLength(1); expect(plan.sql.match(/INSERT INTO supabase_migrations\.schema_migrations/g)).toHaveLength(11); + expect(plan.sql.match(/SUCCESSOR_ADMISSION_EXPIRED/g)).toHaveLength(13); + expect(plan.sql.match(/set_config\('transaction_timeout'/g)).toHaveLength(1); + expect(plan.sql).toContain('transaction_timestamp()'); + expect(plan.sql.match(new RegExp(SUCCESSOR_PRE_COMMIT_EXPIRY_MARKER, 'g'))).toHaveLength(1); expect(statementSpans(plan.sql).some(span => /^(?:BEGIN|COMMIT|ROLLBACK)\b/i.test(span.token))).toBe(false); const lockTimeout = plan.sql.indexOf("SET LOCAL lock_timeout='2s'"); const statementTimeout = plan.sql.indexOf("SET LOCAL statement_timeout='15s'"); @@ -443,6 +480,18 @@ test('compiled plan has one advisory lock, one outer transaction and eleven ledg expect(advisoryLock).toBeGreaterThan(statementTimeout); expect(expiryCheck).toBeGreaterThan(advisoryLock); expect(firstMigration).toBeGreaterThan(expiryCheck); + const ledgerWrites = [...plan.sql.matchAll(/INSERT INTO supabase_migrations\.schema_migrations/g)] + .map(match => match.index ?? -1); + const expiryChecks = [...plan.sql.matchAll(/SUCCESSOR_ADMISSION_EXPIRED/g)] + .map(match => match.index ?? -1); + ledgerWrites.forEach((writeAt, index) => { + expect(expiryChecks[index + 1]).toBeGreaterThan(writeAt); + if (index + 1 < ledgerWrites.length) expect(expiryChecks[index + 1]).toBeLessThan(ledgerWrites[index + 1]); + }); + const preCommitMarker = plan.sql.indexOf(SUCCESSOR_PRE_COMMIT_EXPIRY_MARKER); + expect(preCommitMarker).toBeGreaterThan(ledgerWrites.at(-1) ?? -1); + expect(expiryChecks.at(-1) ?? -1).toBeGreaterThan(preCommitMarker); + expect(statementSpans(plan.sql).at(-1)?.token).toContain('SUCCESSOR_ADMISSION_EXPIRED'); expect(plan.sql).toContain(`clock_timestamp() >= '${admitted.expiresAt}'::timestamptz`); expect(plan.stateQuery).toContain('terminalContractReady'); expect(plan.stateQuery).toContain(manifest.terminalContract.finishPostimageSha256); @@ -459,6 +508,43 @@ test('compiled plan has one advisory lock, one outer transaction and eleven ledg expect(storageState).not.toContain('updated_at'); }); +test('apply accepts rehearsal evidence completed after thirty seconds within its original lease', () => { + const document = admission(); + document.createdAt = '2026-10-12T07:55:00.000Z'; + document.expiresAt = '2026-10-12T08:10:00.000Z'; + const files = custodyFiles(document); + rewriteRehearsalTimeline(files, { + startedAt: '2026-10-12T07:55:00.000Z', + observedAt: '2026-10-12T08:08:00.000Z', + verifiedAt: '2026-10-12T08:08:05.000Z', + publishedAt: '2026-10-12T08:08:10.000Z', + }); + expect(validateSuccessorAdmission(files.document, executableRecord, { + now: new Date('2026-10-12T08:09:00.000Z'), + receiptDirectory: files.directory, + })).toMatchObject({ + createdAt: Date.parse(document.createdAt), + expiresAt: Date.parse(document.expiresAt), + }); +}); + +test('apply rejects rehearsal evidence outside the original fifteen-minute lease', () => { + const document = admission(); + document.createdAt = '2026-10-12T07:55:00.000Z'; + document.expiresAt = '2026-10-12T08:10:00.000Z'; + const files = custodyFiles(document); + rewriteRehearsalTimeline(files, { + startedAt: '2026-10-12T07:54:59.999Z', + observedAt: '2026-10-12T08:08:00.000Z', + verifiedAt: '2026-10-12T08:08:05.000Z', + publishedAt: '2026-10-12T08:08:10.000Z', + }); + expect(() => validateSuccessorAdmission(files.document, executableRecord, { + now: new Date('2026-10-12T08:09:00.000Z'), + receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); +}); + test('actual psql runner with a spy transport preserves the post-lock expiry code', () => { let spawnCalls = 0; const runner = createSuccessorPsqlRunner(databaseUrl, { From ffc8efa6f3cf504c9459ef22ec82b055c7b0ab54 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:11:14 +0900 Subject: [PATCH 08/14] Recover all SDK consumers from one retry and expose admin recovery --- .../admin/AdminOverviewDashboard.tsx | 13 +---- .../admin/admin-map-load-error-state.tsx | 13 +++++ apps/web/hooks/use-naver-maps.tsx | 48 ++++++++++++----- .../naver-loader-failure-browser.test.ts | 54 ++++++++++++++++--- 4 files changed, 97 insertions(+), 31 deletions(-) create mode 100644 apps/web/components/admin/admin-map-load-error-state.tsx diff --git a/apps/web/components/admin/AdminOverviewDashboard.tsx b/apps/web/components/admin/AdminOverviewDashboard.tsx index b2076683ee..b5b8ded97d 100644 --- a/apps/web/components/admin/AdminOverviewDashboard.tsx +++ b/apps/web/components/admin/AdminOverviewDashboard.tsx @@ -9,6 +9,7 @@ import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Skeleton } from "@/components/ui/skeleton"; import { useNaverMaps } from "@/hooks/use-naver-maps"; +import { AdminMapLoadErrorState } from "@/components/admin/admin-map-load-error-state"; import { readAdminNaverCoordinate } from "@/lib/admin/naver-coordinate"; import { REGION_MAP_CONFIG } from "@/config/maps"; import { getNaverIndividualMarkerVisual } from "@/lib/naver-map-marker-visuals"; @@ -882,17 +883,7 @@ function AdminNaverMapSurface({ {isMapPreparing && } {loadError && !isMapPreparing && ( -
-
-

- 네이버 지도를 불러오지 못했습니다 -

-

- {loadError.message} 지도 키가 준비되면 홈과 같은 마커와 클러스터로 - 자동 표시됩니다. -

-
-
+ )} ); diff --git a/apps/web/components/admin/admin-map-load-error-state.tsx b/apps/web/components/admin/admin-map-load-error-state.tsx new file mode 100644 index 0000000000..c1be18d687 --- /dev/null +++ b/apps/web/components/admin/admin-map-load-error-state.tsx @@ -0,0 +1,13 @@ +import { Button } from '@/components/ui/button'; + +export function AdminMapLoadErrorState({ onRetry }: { onRetry: () => void }) { + return ( +
+
+

네이버 지도를 불러오지 못했습니다

+

연결 상태를 확인한 뒤 다시 시도해 주세요.

+ +
+
+ ); +} diff --git a/apps/web/hooks/use-naver-maps.tsx b/apps/web/hooks/use-naver-maps.tsx index 8f06f35c64..a5bd429e45 100644 --- a/apps/web/hooks/use-naver-maps.tsx +++ b/apps/web/hooks/use-naver-maps.tsx @@ -121,6 +121,36 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { const [loadError, setLoadError] = useState(null); const [isLoading, setIsLoading] = useState(false); + // A retry belongs to the shared provider, not only to the consumer that + // requested it. Capture script completion for every mounted consumer; + // remove the subscription when that consumer leaves. + useEffect(() => { + const observeScript = (event: Event) => { + const script = event.target; + if (!(script instanceof HTMLScriptElement)) return; + if (!script.matches(LOCAL_OFFLINE_MAP + ? 'script[data-local-naver-maps="true"]' + : 'script[src*="oapi.map.naver.com"]')) return; + + setIsLoading(false); + if (event.type === 'load' && hasUsableNaverMaps()) { + installLocalNaverStyleHostFallback(); + setIsLoaded(true); + setLoadError(null); + } else if (!hasUsableNaverMaps()) { + setLoadError(new Error(event.type === 'load' + ? '네이버 지도 API 초기화 실패' + : '네이버 지도 API 로딩 실패 - 네트워크 오류')); + } + }; + document.addEventListener('load', observeScript, true); + document.addEventListener('error', observeScript, true); + return () => { + document.removeEventListener('load', observeScript, true); + document.removeEventListener('error', observeScript, true); + }; + }, []); + // 수동으로 스크립트 로드하는 함수 const load = useCallback(() => { // 이미 로드되었거나 로딩 중이면 무시 @@ -149,20 +179,6 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { if (existingScript) { setIsLoading(true); - existingScript.addEventListener("load", () => { - installLocalNaverStyleHostFallback(); - if (hasUsableNaverMaps()) { - setIsLoaded(true); - setIsLoading(false); - } else { - setLoadError(new Error("네이버 지도 API 초기화 실패")); - setIsLoading(false); - } - }); - existingScript.addEventListener("error", () => { - setLoadError(new Error("네이버 지도 API 로딩 실패 - 네트워크 오류")); - setIsLoading(false); - }); return; } @@ -177,6 +193,8 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { script.async = true; script.onload = () => { + script.onload = null; + script.onerror = null; installLocalNaverStyleHostFallback(); if (hasUsableNaverMaps()) { setIsLoaded(true); @@ -191,6 +209,8 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { }; script.onerror = () => { + script.onload = null; + script.onerror = null; script.remove(); setLoadError( new Error( diff --git a/apps/web/tests-unit/naver-loader-failure-browser.test.ts b/apps/web/tests-unit/naver-loader-failure-browser.test.ts index b08c08c3d6..8d4956ff47 100644 --- a/apps/web/tests-unit/naver-loader-failure-browser.test.ts +++ b/apps/web/tests-unit/naver-loader-failure-browser.test.ts @@ -22,15 +22,19 @@ import React from 'react'; import {createRoot} from 'react-dom/client'; import {useNaverMaps} from './hooks/use-naver-maps'; import {NaverMapLoadErrorState} from './components/map/map-view-status-panels'; +import {AdminMapLoadErrorState} from './components/admin/admin-map-load-error-state'; function Consumer({id}) { const {isLoaded,isLoading,loadError,load}=useNaverMaps({autoLoad:true}); + const admin=new URLSearchParams(location.search).get('ui')==='admin'; return
{loadError?'error':isLoaded?'ready':isLoading?'loading':'idle'} - {loadError&&} + {loadError&&(admin?:)}
; } const count=Number(new URLSearchParams(location.search).get('consumers')||1); -createRoot(document.getElementById('root')).render(<>{Array.from({length:count},(_,i)=>)}); +const root=createRoot(document.getElementById('root')); +window.__unmountNaverFixture=()=>root.unmount(); +root.render(<>{Array.from({length:count},(_,i)=>)}); `; beforeAll(async () => { @@ -58,10 +62,26 @@ beforeAll(async () => { afterAll(async () => { await browser?.close(); }); type Outcome = 'network-error' | 'missing-api' | 'success'; -async function run(outcomes: Outcome[], check: (page: Page, counts: { live: number; stub: number }) => Promise, consumers = 1, mode = 'live') { +async function run(outcomes: Outcome[], check: (page: Page, counts: { live: number; stub: number }) => Promise, consumers = 1, mode = 'live', uiMode = 'public') { const context = await browser.newContext(); const page = await context.newPage(); const counts = { live: 0, stub: 0 }; + await page.addInitScript(() => { + const active = new Map>(); + const add = document.addEventListener.bind(document); + const remove = document.removeEventListener.bind(document); + document.addEventListener = ((type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions) => { + if (type === 'load' || type === 'error') { + const listeners = active.get(type) ?? new Set(); + listeners.add(listener); active.set(type, listeners); + } + add(type, listener, options); + }) as typeof document.addEventListener; + document.removeEventListener = ((type: string, listener: EventListenerOrEventListenerObject, options?: boolean | EventListenerOptions) => { + active.get(type)?.delete(listener); remove(type, listener, options); + }) as typeof document.removeEventListener; + Object.assign(window, { __documentScriptListenerCount: () => [...active.values()].reduce((sum, listeners) => sum + listeners.size, 0) }); + }); await page.route('**/*', async route => { const url = new URL(route.request().url()); if (route.request().isNavigationRequest()) return route.fulfill({ contentType: 'text/html', body: '
' }); @@ -77,7 +97,7 @@ async function run(outcomes: Outcome[], check: (page: Page, counts: { live: numb return route.abort(); }); try { - await page.goto(`http://naver-loader.fixture.test/?consumers=${consumers}`); + await page.goto(`http://naver-loader.fixture.test/?consumers=${consumers}&ui=${uiMode}`); await page.addScriptTag({ content: scripts[mode] }); await check(page, counts); } finally { await context.close(); } @@ -105,17 +125,39 @@ for (const failure of ['network-error', 'missing-api'] as const) { })); } -browserTest('concurrent consumers share a failure and one explicit recovery request', async () => run(['network-error', 'success'], async (page, counts) => { +for (const failure of ['network-error', 'missing-api'] as const) { +browserTest(`one retry after ${failure} recovers every concurrent consumer`, async () => run([failure, 'success'], async (page, counts) => { await ui(page.getByTestId('state-0')).toHaveText('error'); await ui(page.getByTestId('state-1')).toHaveText('error'); expect(counts).toEqual({ live: 1, stub: 0 }); - await page.getByRole('button', { name: '다시 시도', exact: true }).evaluateAll(buttons => buttons.forEach(button => (button as HTMLButtonElement).click())); + await page.getByRole('button', { name: '다시 시도', exact: true }).first().click(); await ui(page.getByTestId('state-0')).toHaveText('ready'); await ui(page.getByTestId('state-1')).toHaveText('ready'); expect(counts).toEqual({ live: 2, stub: 0 }); }, 2)); +browserTest(`admin error control recovers after ${failure}`, async () => run([failure, 'success'], async (page, counts) => { + await ui(page.getByTestId('state-0')).toHaveText('error'); + await ui(page.getByRole('alert')).toContainText('네이버 지도를 불러오지 못했습니다'); + await page.getByRole('button', { name: '다시 시도', exact: true }).click(); + await ui(page.getByTestId('state-0')).toHaveText('ready'); + await ui(page.getByRole('alert')).toHaveCount(0); + expect(counts).toEqual({ live: 2, stub: 0 }); +}, 1, 'live', 'admin')); +} + browserTest('explicit local offline mode still uses its deterministic stub', async () => run([], async (page, counts) => { await ui(page.getByTestId('state-0')).toHaveText('ready'); expect(counts).toEqual({ live: 0, stub: 1 }); }, 1, 'local')); + +browserTest('unmount releases provider subscriptions and settled script callbacks', async () => run(['success'], async page => { + await ui(page.getByTestId('state-0')).toHaveText('ready'); + expect(await page.evaluate('window.__documentScriptListenerCount()')).toBe(2); + expect(await page.evaluate(() => { + const script = document.querySelector('script[src*="oapi.map.naver.com"]'); + return Boolean(script?.onload || script?.onerror); + })).toBe(false); + await page.evaluate('window.__unmountNaverFixture()'); + expect(await page.evaluate('window.__documentScriptListenerCount()')).toBe(0); +})); From 7a9b19b19416ce098f2f1a6fa5d36c8674b409b0 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Tue, 13 Oct 2026 19:21:04 +0900 Subject: [PATCH 09/14] Bound deferred SDK loading and cancel unmounted consumers --- apps/web/hooks/use-naver-maps.tsx | 40 ++++++++++++++++-- .../naver-loader-failure-browser.test.ts | 42 +++++++++++++++++-- 2 files changed, 75 insertions(+), 7 deletions(-) diff --git a/apps/web/hooks/use-naver-maps.tsx b/apps/web/hooks/use-naver-maps.tsx index a5bd429e45..ac58c21c53 100644 --- a/apps/web/hooks/use-naver-maps.tsx +++ b/apps/web/hooks/use-naver-maps.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useEffect, useState, useCallback } from "react"; +import { useEffect, useState, useCallback, useRef } from "react"; import { NAVER_MAPS_CONFIG } from "@/config/maps"; import { isLocalNaverStyleFallbackHost, @@ -120,6 +120,7 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { }); const [loadError, setLoadError] = useState(null); const [isLoading, setIsLoading] = useState(false); + const scheduledLoadRef = useRef<(() => void) | null>(null); // A retry belongs to the shared provider, not only to the consumer that // requested it. Capture script completion for every mounted consumer; @@ -132,6 +133,8 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { ? 'script[data-local-naver-maps="true"]' : 'script[src*="oapi.map.naver.com"]')) return; + scheduledLoadRef.current?.(); + scheduledLoadRef.current = null; setIsLoading(false); if (event.type === 'load' && hasUsableNaverMaps()) { installLocalNaverStyleHostFallback(); @@ -146,6 +149,8 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { document.addEventListener('load', observeScript, true); document.addEventListener('error', observeScript, true); return () => { + scheduledLoadRef.current?.(); + scheduledLoadRef.current = null; document.removeEventListener('load', observeScript, true); document.removeEventListener('error', observeScript, true); }; @@ -154,7 +159,7 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { // 수동으로 스크립트 로드하는 함수 const load = useCallback(() => { // 이미 로드되었거나 로딩 중이면 무시 - if (isLoaded || isLoading) return; + if (isLoaded || isLoading || scheduledLoadRef.current) return; setLoadError(null); @@ -184,6 +189,16 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { // 스크립트 로드 실행 함수 const injectScript = () => { + // Another consumer may have completed or injected the shared SDK + // while this consumer was waiting for an idle callback. + if (hasUsableNaverMaps()) { + setIsLoaded(true); + setIsLoading(false); + return; + } + if (document.querySelector(LOCAL_OFFLINE_MAP + ? 'script[data-local-naver-maps="true"]' + : 'script[src*="oapi.map.naver.com"]')) return; installLocalNaverStyleHostFallback(); setIsLoading(true); const script = document.createElement("script"); @@ -225,14 +240,31 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { // 전략에 따른 로드 실행 if (strategy === 'lazyOnload') { + setIsLoading(true); + let cancelled = false; + const run = () => { + scheduledLoadRef.current = null; + if (!cancelled) injectScript(); + }; // 브라우저 유휴 상태일 때 로드하거나 2초 후 로드 const idleWindow = window as Window & { requestIdleCallback?: RequestIdleCallbackLike; + cancelIdleCallback?: (handle: IdleCallbackHandle) => void; }; if (typeof idleWindow.requestIdleCallback === 'function') { - idleWindow.requestIdleCallback(() => injectScript(), { timeout: 2000 }); + let handle: IdleCallbackHandle | undefined; + scheduledLoadRef.current = () => { + cancelled = true; + if (handle !== undefined) idleWindow.cancelIdleCallback?.(handle); + }; + handle = idleWindow.requestIdleCallback(run, { timeout: 2000 }); } else { - setTimeout(injectScript, 2000); + let handle: number | undefined; + scheduledLoadRef.current = () => { + cancelled = true; + if (handle !== undefined) window.clearTimeout(handle); + }; + handle = window.setTimeout(run, 2000); } } else { // afterInteractive: 즉시 비동기 로드 diff --git a/apps/web/tests-unit/naver-loader-failure-browser.test.ts b/apps/web/tests-unit/naver-loader-failure-browser.test.ts index 8d4956ff47..d8744f48c5 100644 --- a/apps/web/tests-unit/naver-loader-failure-browser.test.ts +++ b/apps/web/tests-unit/naver-loader-failure-browser.test.ts @@ -24,7 +24,8 @@ import {useNaverMaps} from './hooks/use-naver-maps'; import {NaverMapLoadErrorState} from './components/map/map-view-status-panels'; import {AdminMapLoadErrorState} from './components/admin/admin-map-load-error-state'; function Consumer({id}) { - const {isLoaded,isLoading,loadError,load}=useNaverMaps({autoLoad:true}); + const strategy=new URLSearchParams(location.search).get('strategy')==='lazyOnload'?'lazyOnload':'afterInteractive'; + const {isLoaded,isLoading,loadError,load}=useNaverMaps({autoLoad:true,strategy}); const admin=new URLSearchParams(location.search).get('ui')==='admin'; return
{loadError?'error':isLoaded?'ready':isLoading?'loading':'idle'} @@ -62,10 +63,23 @@ beforeAll(async () => { afterAll(async () => { await browser?.close(); }); type Outcome = 'network-error' | 'missing-api' | 'success'; -async function run(outcomes: Outcome[], check: (page: Page, counts: { live: number; stub: number }) => Promise, consumers = 1, mode = 'live', uiMode = 'public') { +async function run(outcomes: Outcome[], check: (page: Page, counts: { live: number; stub: number }) => Promise, consumers = 1, mode = 'live', uiMode = 'public', strategy = 'afterInteractive') { const context = await browser.newContext(); const page = await context.newPage(); const counts = { live: 0, stub: 0 }; + if (strategy === 'lazyOnload') await page.addInitScript(() => { + let id = 0; + const callbacks = new Map(); + Object.assign(window, { + requestIdleCallback: (callback: IdleRequestCallback) => { callbacks.set(++id, callback); return id; }, + cancelIdleCallback: (handle: number) => { callbacks.delete(handle); }, + __idleCallbackCount: () => callbacks.size, + __flushIdle: () => { + const pending = [...callbacks.values()]; callbacks.clear(); + pending.forEach(callback => callback({ didTimeout: false, timeRemaining: () => 50 })); + }, + }); + }); await page.addInitScript(() => { const active = new Map>(); const add = document.addEventListener.bind(document); @@ -97,7 +111,7 @@ async function run(outcomes: Outcome[], check: (page: Page, counts: { live: numb return route.abort(); }); try { - await page.goto(`http://naver-loader.fixture.test/?consumers=${consumers}&ui=${uiMode}`); + await page.goto(`http://naver-loader.fixture.test/?consumers=${consumers}&ui=${uiMode}&strategy=${strategy}`); await page.addScriptTag({ content: scripts[mode] }); await check(page, counts); } finally { await context.close(); } @@ -161,3 +175,25 @@ browserTest('unmount releases provider subscriptions and settled script callback await page.evaluate('window.__unmountNaverFixture()'); expect(await page.evaluate('window.__documentScriptListenerCount()')).toBe(0); })); + +browserTest('lazy consumers share one deferred request and one explicit retry', async () => run(['network-error', 'success'], async (page, counts) => { + await page.waitForFunction('window.__idleCallbackCount() === 2'); + await page.evaluate('window.__flushIdle()'); + await ui(page.getByTestId('state-0')).toHaveText('error'); + await ui(page.getByTestId('state-1')).toHaveText('error'); + expect(counts).toEqual({ live: 1, stub: 0 }); + await page.getByRole('button', { name: '다시 시도', exact: true }).first().click(); + await page.waitForFunction('window.__idleCallbackCount() === 1'); + await page.evaluate('window.__flushIdle()'); + await ui(page.getByTestId('state-0')).toHaveText('ready'); + await ui(page.getByTestId('state-1')).toHaveText('ready'); + expect(counts).toEqual({ live: 2, stub: 0 }); +}, 2, 'live', 'public', 'lazyOnload')); + +browserTest('unmount cancels deferred provider work before script injection', async () => run(['success'], async (page, counts) => { + await page.waitForFunction('window.__idleCallbackCount() === 1'); + await page.evaluate('window.__unmountNaverFixture()'); + expect(await page.evaluate('window.__idleCallbackCount()')).toBe(0); + await page.evaluate('window.__flushIdle()'); + expect(counts).toEqual({ live: 0, stub: 0 }); +}, 1, 'live', 'public', 'lazyOnload')); From 9fba35f92b9b0f517d1f726461f38bc4a91e49e6 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:01:13 +0900 Subject: [PATCH 10/14] Track owned SDK scripts across configured provider URLs --- apps/web/hooks/use-naver-maps.tsx | 18 +++++-------- .../naver-loader-failure-browser.test.ts | 26 +++++++++++++++---- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/apps/web/hooks/use-naver-maps.tsx b/apps/web/hooks/use-naver-maps.tsx index ac58c21c53..5798382f67 100644 --- a/apps/web/hooks/use-naver-maps.tsx +++ b/apps/web/hooks/use-naver-maps.tsx @@ -110,6 +110,9 @@ const EFFECTIVE_SCRIPT_URL = resolveNaverMapScriptUrl({ configuredScriptUrl: process.env.NEXT_PUBLIC_NAVER_MAPS_SCRIPT_URL, useLocalStub: LOCAL_OFFLINE_MAP, }); +const SHARED_SCRIPT_SELECTOR = LOCAL_OFFLINE_MAP + ? 'script[data-local-naver-maps="true"]' + : 'script[data-tzudong-naver-maps="true"], script[src*="oapi.map.naver.com"]'; export function useNaverMaps(options: UseNaverMapsOptions = {}) { const { autoLoad = false, strategy = 'afterInteractive' } = options; @@ -129,9 +132,7 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { const observeScript = (event: Event) => { const script = event.target; if (!(script instanceof HTMLScriptElement)) return; - if (!script.matches(LOCAL_OFFLINE_MAP - ? 'script[data-local-naver-maps="true"]' - : 'script[src*="oapi.map.naver.com"]')) return; + if (!script.matches(SHARED_SCRIPT_SELECTOR)) return; scheduledLoadRef.current?.(); scheduledLoadRef.current = null; @@ -176,11 +177,7 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { } // 이미 로딩 중인 스크립트가 있는지 확인 - const existingScript = document.querySelector( - LOCAL_OFFLINE_MAP - ? 'script[data-local-naver-maps="true"]' - : 'script[src*="oapi.map.naver.com"]' - ); + const existingScript = document.querySelector(SHARED_SCRIPT_SELECTOR); if (existingScript) { setIsLoading(true); @@ -196,13 +193,12 @@ export function useNaverMaps(options: UseNaverMapsOptions = {}) { setIsLoading(false); return; } - if (document.querySelector(LOCAL_OFFLINE_MAP - ? 'script[data-local-naver-maps="true"]' - : 'script[src*="oapi.map.naver.com"]')) return; + if (document.querySelector(SHARED_SCRIPT_SELECTOR)) return; installLocalNaverStyleHostFallback(); setIsLoading(true); const script = document.createElement("script"); script.type = "text/javascript"; + script.dataset.tzudongNaverMaps = "true"; if (LOCAL_OFFLINE_MAP) script.dataset.localNaverMaps = "true"; script.src = EFFECTIVE_SCRIPT_URL; script.async = true; diff --git a/apps/web/tests-unit/naver-loader-failure-browser.test.ts b/apps/web/tests-unit/naver-loader-failure-browser.test.ts index d8744f48c5..5b6c10a021 100644 --- a/apps/web/tests-unit/naver-loader-failure-browser.test.ts +++ b/apps/web/tests-unit/naver-loader-failure-browser.test.ts @@ -13,7 +13,9 @@ const chromeAvailable = [ ].some(existsSync); const browserTest = chromeAvailable ? test : test.skip; const ui = browserExpect.configure({ timeout: 1500 }); -const baseline = process.env.NAVER_LOADER_BASELINE === '056a86600f412eb42df4304e3206841679012f3e'; +const baselineInput = process.env.NAVER_LOADER_BASELINE ?? ''; +const baseline = ['056a86600f412eb42df4304e3206841679012f3e', '7a9b19b19416ce098f2f1a6fa5d36c8674b409b0'].includes(baselineInput) + ? baselineInput : null; let browser: Browser; const scripts: Record = {}; const fakeSdk = 'window.naver={maps:{Map:function(){},LatLng:function(){},Point:function(){},Event:{}}};'; @@ -40,15 +42,15 @@ root.render(<>{Array.from({length:count},(_,i)=>)}) beforeAll(async () => { if (!chromeAvailable) return; - for (const mode of ['live', 'local']) { + for (const mode of ['live', 'local', 'configured']) { const bundle = await Bun.build({ entrypoints: ['naver-loader-fixture'], target: 'browser', format: 'iife', write: false, - define: { 'process.env': JSON.stringify({ NODE_ENV: 'production', NEXT_PUBLIC_NAVER_CLIENT_ID: mode === 'live' ? 'browser-fixture-client' : '', NEXT_PUBLIC_TZUDONG_LOCAL_RUNTIME: mode === 'local' ? '1' : '0' }) }, + define: { 'process.env': JSON.stringify({ NODE_ENV: 'production', NEXT_PUBLIC_NAVER_CLIENT_ID: mode !== 'local' ? 'browser-fixture-client' : '', NEXT_PUBLIC_TZUDONG_LOCAL_RUNTIME: mode === 'local' ? '1' : '0', NEXT_PUBLIC_NAVER_MAPS_SCRIPT_URL: mode === 'configured' ? 'https://sdk.fixture.test/naver.js' : undefined }) }, plugins: [{ name: 'naver-loader-fixture', setup(builder) { builder.onResolve({ filter: /^naver-loader-fixture$/ }, () => ({ path: 'fixture.tsx', namespace: 'fixture' })); builder.onLoad({ filter: /.*/, namespace: 'fixture' }, () => ({ contents: fixture, loader: 'tsx', resolveDir: app })); if (baseline) { builder.onResolve({ filter: /hooks\/use-naver-maps$/ }, () => ({ path: 'use-naver-maps.tsx', namespace: 'baseline' })); - builder.onLoad({ filter: /.*/, namespace: 'baseline' }, () => ({ contents: execFileSync('git', ['show', '056a86600f412eb42df4304e3206841679012f3e:apps/web/hooks/use-naver-maps.tsx'], { cwd: app, encoding: 'utf8' }), loader: 'tsx', resolveDir: app })); + builder.onLoad({ filter: /.*/, namespace: 'baseline' }, () => ({ contents: execFileSync('git', ['show', baseline + ':apps/web/hooks/use-naver-maps.tsx'], { cwd: app, encoding: 'utf8' }), loader: 'tsx', resolveDir: app })); } builder.onResolve({ filter: /^@\// }, ({ path }) => { const resolved = ['', '.ts', '.tsx'].map(suffix => join(app, path.slice(2)) + suffix).find(existsSync); @@ -99,7 +101,8 @@ async function run(outcomes: Outcome[], check: (page: Page, counts: { live: numb await page.route('**/*', async route => { const url = new URL(route.request().url()); if (route.request().isNavigationRequest()) return route.fulfill({ contentType: 'text/html', body: '
' }); - if (url.hostname === 'oapi.map.naver.com' && url.pathname === '/openapi/v3/maps.js') { + if ((url.hostname === 'oapi.map.naver.com' && url.pathname === '/openapi/v3/maps.js') + || (url.hostname === 'sdk.fixture.test' && url.pathname === '/naver.js')) { const outcome = outcomes[Math.min(counts.live++, outcomes.length - 1)]; if (outcome === 'network-error') return route.abort('failed'); return route.fulfill({ contentType: 'application/javascript', body: outcome === 'success' ? fakeSdk : '/* provider loaded without an API */' }); @@ -114,6 +117,9 @@ async function run(outcomes: Outcome[], check: (page: Page, counts: { live: numb await page.goto(`http://naver-loader.fixture.test/?consumers=${consumers}&ui=${uiMode}&strategy=${strategy}`); await page.addScriptTag({ content: scripts[mode] }); await check(page, counts); + } catch (error) { + console.error(JSON.stringify({ mode, consumers, strategy, uiMode, liveRequests: counts.live, stubRequests: counts.stub })); + throw error; } finally { await context.close(); } } @@ -158,6 +164,16 @@ browserTest(`admin error control recovers after ${failure}`, async () => run([fa await ui(page.getByRole('alert')).toHaveCount(0); expect(counts).toEqual({ live: 2, stub: 0 }); }, 1, 'live', 'admin')); + +browserTest(`configured SDK URL shares one retry after ${failure}`, async () => run([failure, 'success'], async (page, counts) => { + await ui(page.getByTestId('state-0')).toHaveText('error'); + await ui(page.getByTestId('state-1')).toHaveText('error'); + expect(counts).toEqual({ live: 1, stub: 0 }); + await page.getByRole('button', { name: '다시 시도', exact: true }).first().click(); + await ui(page.getByTestId('state-0')).toHaveText('ready'); + await ui(page.getByTestId('state-1')).toHaveText('ready'); + expect(counts).toEqual({ live: 2, stub: 0 }); +}, 2, 'configured')); } browserTest('explicit local offline mode still uses its deterministic stub', async () => run([], async (page, counts) => { From 55a47b105c8709726d578cc935afc253bf6d2cc7 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Tue, 13 Oct 2026 20:45:55 +0900 Subject: [PATCH 11/14] Bind credential-free SQL test transport to its manifest --- apps/web/tests-unit/review-media-sql-successor.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/web/tests-unit/review-media-sql-successor.test.ts b/apps/web/tests-unit/review-media-sql-successor.test.ts index ca9c240ea2..a6491bc25b 100644 --- a/apps/web/tests-unit/review-media-sql-successor.test.ts +++ b/apps/web/tests-unit/review-media-sql-successor.test.ts @@ -56,11 +56,11 @@ const executableRecord = Object.freeze({ }); const fixedNow = new Date('2026-10-12T08:00:00.000Z'); const revision = '056a86600f412eb42df4304e3206841679012f3e'; -const databaseUrlFixture = new URL('postgresql://localhost/postgres'); -databaseUrlFixture.hostname = 'aws-0-ap-southeast-1.pooler.supabase.com'; -databaseUrlFixture.username = 'postgres.aqlcofblfxdrjhhdmarw'; -databaseUrlFixture.port = '6543'; -const databaseUrl = databaseUrlFixture.toString(); +// This injected transport has no credential. Keep its project/role/database +// identity bound to the same manifest that the controller validates. +const databaseUrl = new URL( + `postgresql://${record.manifest.executorRole}.${record.manifest.projectRef}@aws-0-ap-southeast-1.pooler.supabase.com:6543/${record.manifest.databaseName}`, +).toString(); const rollback = Object.freeze({ deploymentId: 'dpl_EUxJnWmDDozCSMWb2sqNSPgKJLM5', deploymentSha: '1884f87e53c4567ead7c9a0fb394b09b91a70396', From 3bca78263ef086f71ba0b38eec35eb212768e4ea Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Tue, 13 Oct 2026 21:29:12 +0900 Subject: [PATCH 12/14] Refresh current proof index while preserving the prior checksum snapshot --- .../review-db-rehearsal-20261012/SHA256SUMS | 5 +++- .../SHA256SUMS-7d111349-historical | 25 +++++++++++++++++++ .../SHA256SUMS-7d111349-historical.sha256 | 1 + .../SHA256SUMS.sha256 | 2 +- ...ixture-url-unit-verification-20261013.json | 22 ++++++++++++++++ 5 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical create mode 100644 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical.sha256 create mode 100644 apps/web/performance/review-db-rehearsal-20261012/fixture-url-unit-verification-20261013.json diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS index a86feae869..f12e0be4cc 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS @@ -2,7 +2,10 @@ ac14e363f38d6cb6acba0e24a5ad6c6943c8e8c15152cff49fcd86a2d014bd9d apps/web/performance/review-db-rehearsal-20261012/README.md ea529addcf42cdb766f622786ccfffb13fe337f70faae6ade6da83736be58f57 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-0833ed81-historical 6d9cbca375e6790ee1051aa9d9bf1c45beeeece8d919651281885b4c04962981 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4d4ecf60-historical +5df2ade9cb3618b774eb4d3796df06ade6ba069409de49d0bdd542b34b6ec4ba apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical +7d395c9246782322ccc326dd99ab0d5e94e0d82e203eb519a6baf91a4a62ece2 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical.sha256 6c474d8e78c0eba3f15c077dbdd8634f073524c3b5f842213372bf0f1fed84fc apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical +75b801b80a2d23a7c415085e8fb9b187b03469d2c2d861ba3279d52b2ba7545c apps/web/performance/review-db-rehearsal-20261012/fixture-url-unit-verification-20261013.json a12f8880f2a0b8b5493e37ea4b2619962d42643581c14c889f0c6f6e8d528129 apps/web/performance/review-db-rehearsal-20261012/interrupted-attempt-20261013.json 9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076 apps/web/performance/review-db-rehearsal-20261012/pg17-reviewed-boundaries-20261013.json b279a6b900dd3be10b9084a6362d169e87435bae955b2a1419858709645a6883 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json @@ -22,4 +25,4 @@ fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scrip dc1f2594e629d5af16541db93e2e51f9b98d8282dd7fbeb802dd240fa6f522c0 apps/web/tests-unit/review-media-migration-bundle.test.ts 54a9560b77a3afb9eb70631224ba4f481caf82eb8b5d2cdc1a076d8831c1ec7e apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts 983a62a3f81973e18914835b2d483a7a05fef3fa9f7d129ffd7a60ed5cd4e551 apps/web/tests-unit/review-media-sql-successor-harness.test.ts -b9fbfa0fdc249e3c495043679c39a8a98cd14f8de6a225f20d926d83702fd196 apps/web/tests-unit/review-media-sql-successor.test.ts +f74f30aa6f1237a757c7fd18693fa839cb4c199409a87d870260ec574bfc5d33 apps/web/tests-unit/review-media-sql-successor.test.ts diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical new file mode 100644 index 0000000000..a86feae869 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical @@ -0,0 +1,25 @@ +23bf7e7d7e9e5d7962021dd3ec860e87b47d07e119ba0e39de2e423a0a601f4e .github/review-media-sql-successor.v1.json +ac14e363f38d6cb6acba0e24a5ad6c6943c8e8c15152cff49fcd86a2d014bd9d apps/web/performance/review-db-rehearsal-20261012/README.md +ea529addcf42cdb766f622786ccfffb13fe337f70faae6ade6da83736be58f57 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-0833ed81-historical +6d9cbca375e6790ee1051aa9d9bf1c45beeeece8d919651281885b4c04962981 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4d4ecf60-historical +6c474d8e78c0eba3f15c077dbdd8634f073524c3b5f842213372bf0f1fed84fc apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical +a12f8880f2a0b8b5493e37ea4b2619962d42643581c14c889f0c6f6e8d528129 apps/web/performance/review-db-rehearsal-20261012/interrupted-attempt-20261013.json +9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076 apps/web/performance/review-db-rehearsal-20261012/pg17-reviewed-boundaries-20261013.json +b279a6b900dd3be10b9084a6362d169e87435bae955b2a1419858709645a6883 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json +7f74c175be3eee72b5e93eb75dac07bfefc0ef88d7d9957c26d877ee125ff590 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json +cb766e279baad0315e9d7055bc326b41b78a7d50a4972dac3214bc071aee0e49 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json +0cfc9c10ff7b70c606f6eeef33fff8dbd14636dc15b7c963925097454ee454a2 apps/web/performance/review-db-rehearsal-20261012/pg17-terminal-boundaries-20261013.json +128773e5dabbbb120889716ffbba41e498a37a437aa6799311a81af729703880 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof-0833ed81-historical.json +78f3e0ad5dbc4e3c53cf8dce3b2a9cb2f484f8f72683cbf1a5c659c8f142acb6 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json +7bdf3cd525427da824674ce50cb3ba7c5d54bf6cb0ef8ebe85b21bb2372eae0a apps/web/performance/review-db-rehearsal-20261012/reviewed-boundaries-unit-verification-20261013.json +c53a467eaf9b929647bc1d66248308ba1ff73eba89fc9221dbf7db4711eb8562 apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json +fad17a393e5fc74d06dec2be58c41265d21f355d5ddce92a3947bb67a823e73a apps/web/performance/review-db-rehearsal-20261012/terminal-boundary-unit-verification-20261013.json +f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs +768b8887a30c2b8f496a73a21d373b85a0b64fadaed3a1485034a729ee704d39 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +c893c294719776bf5b064c3355866ab97e0fa9412082d9b47d7de5266094c65f apps/web/scripts/review-media-sql-rehearsal.mjs +5d61808b45661d412937fbfdfd3bfb75b1cf92c7c285e4d8ad061a2f667c3c0b apps/web/scripts/review-media-sql-successor.mjs +fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs +dc1f2594e629d5af16541db93e2e51f9b98d8282dd7fbeb802dd240fa6f522c0 apps/web/tests-unit/review-media-migration-bundle.test.ts +54a9560b77a3afb9eb70631224ba4f481caf82eb8b5d2cdc1a076d8831c1ec7e apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts +983a62a3f81973e18914835b2d483a7a05fef3fa9f7d129ffd7a60ed5cd4e551 apps/web/tests-unit/review-media-sql-successor-harness.test.ts +b9fbfa0fdc249e3c495043679c39a8a98cd14f8de6a225f20d926d83702fd196 apps/web/tests-unit/review-media-sql-successor.test.ts diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical.sha256 b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical.sha256 new file mode 100644 index 0000000000..fcebbfbfad --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical.sha256 @@ -0,0 +1 @@ +5df2ade9cb3618b774eb4d3796df06ade6ba069409de49d0bdd542b34b6ec4ba diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 index fcebbfbfad..98e05b4991 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 @@ -1 +1 @@ -5df2ade9cb3618b774eb4d3796df06ade6ba069409de49d0bdd542b34b6ec4ba +d8ad511edf7d3832aa432e499b27f33e2c9ad423530ffdff06dea42869eaff85 diff --git a/apps/web/performance/review-db-rehearsal-20261012/fixture-url-unit-verification-20261013.json b/apps/web/performance/review-db-rehearsal-20261012/fixture-url-unit-verification-20261013.json new file mode 100644 index 0000000000..dce33ee5f0 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/fixture-url-unit-verification-20261013.json @@ -0,0 +1,22 @@ +{ + "observedAtUtc": "2026-10-13T12:27:16.299025+00:00", + "sourceHead": "55a47b105c8709726d578cc935afc253bf6d2cc7", + "baselineHead": "7d11134938d79010073bf69c4970c9144a5308c5", + "changedSource": [ + "apps/web/tests-unit/review-media-sql-successor.test.ts" + ], + "finalFixtureUriByteEqual": true, + "passwordPresent": false, + "hostedConnectionsPerformed": 0, + "testPass": 54, + "testFail": 0, + "expectCalls": 339, + "durationS": 28.39, + "lintExit": 0, + "actualPostgresReplayRerun": false, + "sqlControllerManifestAndReplayDriverUnchanged": true, + "retainedPg17ProofUnchanged": true, + "oldMapSha256": "5df2ade9cb3618b774eb4d3796df06ade6ba069409de49d0bdd542b34b6ec4ba", + "oldDetachedPinSha256": "7d395c9246782322ccc326dd99ab0d5e94e0d82e203eb519a6baf91a4a62ece2", + "reason": "Refresh mutable current artifact index for one semantically identical fixture source. Frozen proof results and historical index bytes are retained; no fresh database run is claimed." +} From 71dcfc98ab03a016d05449780574a4b9edd81e36 Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:19:06 +0900 Subject: [PATCH 13/14] Bind review-media apply proofs and bound preflight execution --- .github/review-media-sql-successor.v1.json | 2 +- .../review-db-successor-20261012/README.md | 17 + .../review-db-successor-20261012/SHA256SUMS | 16 +- .../SHA256SUMS-4ba9c6d2-historical | 17 + .../SHA256SUMS-4ba9c6d2-historical.sha256 | 1 + .../SHA256SUMS.sha256 | 2 +- ...pg17-current-review-fixes-20261014-v1.json | 0 ...urrent-review-fixes-20261014-v1.stderr.log | 12 + ...pg17-current-review-fixes-20261014-v2.json | 1 + ...urrent-review-fixes-20261014-v2.stderr.log | 0 .../review-fix-harness-units-20261014-v2.log | 28 + ...l-successor-4ba9c6d2-historical.source.txt | 1050 +++++++++++++++++ .../verify-review-media-sql-successor.mjs | 50 +- .../SHA256SUMS | 13 +- .../SHA256SUMS-4ba9c6d2-historical | 6 + .../SHA256SUMS-4ba9c6d2-historical.sha256 | 1 + .../SHA256SUMS.sha256 | 2 +- .../controller-reverification-v2.json | 53 + .../current-evidence.json | 12 + .../review-fix-parity-20261014-v1.log | 5 + .../review-fix-parity-20261014-v2.log | 5 + .../review-fix-units-20261014-v1.log | 112 ++ .../review-media-operating-admission.mjs | 1 + .../scripts/review-media-sql-successor.mjs | 68 +- .../review-media-operating-admission.test.ts | 4 + ...review-media-sql-successor-harness.test.ts | 47 +- .../review-media-sql-successor.test.ts | 239 +++- ...dmission-fixes-20261014-validation-v1.json | 26 + ...view-media-operating-admission-20261013.md | 6 +- 29 files changed, 1758 insertions(+), 38 deletions(-) create mode 100644 apps/web/performance/review-db-successor-20261012/SHA256SUMS-4ba9c6d2-historical create mode 100644 apps/web/performance/review-db-successor-20261012/SHA256SUMS-4ba9c6d2-historical.sha256 create mode 100644 apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v1.json create mode 100644 apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v1.stderr.log create mode 100644 apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v2.json create mode 100644 apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v2.stderr.log create mode 100644 apps/web/performance/review-db-successor-20261012/review-fix-harness-units-20261014-v2.log create mode 100644 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor-4ba9c6d2-historical.source.txt create mode 100644 apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-4ba9c6d2-historical create mode 100644 apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-4ba9c6d2-historical.sha256 create mode 100644 apps/web/performance/review-media-operating-admission-20261013/controller-reverification-v2.json create mode 100644 apps/web/performance/review-media-operating-admission-20261013/current-evidence.json create mode 100644 apps/web/performance/review-media-operating-admission-20261013/review-fix-parity-20261014-v1.log create mode 100644 apps/web/performance/review-media-operating-admission-20261013/review-fix-parity-20261014-v2.log create mode 100644 apps/web/performance/review-media-operating-admission-20261013/review-fix-units-20261014-v1.log create mode 100644 docs/evidence/review-admission-fixes-20261014-validation-v1.json diff --git a/.github/review-media-sql-successor.v1.json b/.github/review-media-sql-successor.v1.json index 557b276779..d60b3b3dcb 100644 --- a/.github/review-media-sql-successor.v1.json +++ b/.github/review-media-sql-successor.v1.json @@ -309,7 +309,7 @@ "toolchain": [ { "path": "apps/web/scripts/review-media-sql-successor.mjs", - "sha256": "5d61808b45661d412937fbfdfd3bfb75b1cf92c7c285e4d8ad061a2f667c3c0b" + "sha256": "4f1b2798e0cfc13db4f11cd6eb4a8653320aa8516eccd71d410fcff6d06a7f24" }, { "path": "apps/web/scripts/review-media-sql-rehearsal.mjs", diff --git a/apps/web/performance/review-db-successor-20261012/README.md b/apps/web/performance/review-db-successor-20261012/README.md index f32f250e81..323d2519b2 100644 --- a/apps/web/performance/review-db-successor-20261012/README.md +++ b/apps/web/performance/review-db-successor-20261012/README.md @@ -81,3 +81,20 @@ is reconciled only by an exact read-only baseline/terminal-state read; it never performs a diagnostic mutation replay. Two injected outcome cases cover terminal commit acknowledgement loss and a rolled-back baseline. This change has local fault tests; no fresh forward-harness replay is claimed. + +## Current review-fix replay — 2026-10-14 + +`pg17-current-review-fixes-20261014-v2.json` binds the current manifest and harness. +It passed the 11-stage PG17.6 atomic commit, exact rollback, wrong preimage, +view/column ACL/membership drift, and real advisory-wait expiry rejection. The +per-run database and fixture roles were removed. The template still records +1,963 manifest rows and zero profiles/reviews; no hosted state was accessed. + +The v1 attempt failed to observe contention in its 700ms observation window. +Its stdout and stderr are retained. The local fixture now confirms the holder +is ready before starting the same 1.5-second expiry window, observes an actual +wait before expiry, and releases the lock just after expiry. Production timeouts +are unchanged; this is a fixture-order correction, not a performance result. +The historical proof and old checksum indexes remain available. The current +retained-proof reader verifies the independently pinned checksum map before +accepting any entry and still classifies old source bindings as historical. diff --git a/apps/web/performance/review-db-successor-20261012/SHA256SUMS b/apps/web/performance/review-db-successor-20261012/SHA256SUMS index 0e1233e64c..d57f554868 100644 --- a/apps/web/performance/review-db-successor-20261012/SHA256SUMS +++ b/apps/web/performance/review-db-successor-20261012/SHA256SUMS @@ -1,17 +1,25 @@ -23bf7e7d7e9e5d7962021dd3ec860e87b47d07e119ba0e39de2e423a0a601f4e .github/review-media-sql-successor.v1.json +295b2354c562ed87102c5e256497a2705f90941587fefa7e9ad8a4ba8e40787f .github/review-media-sql-successor.v1.json f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs -830e2f3d6baaf95ee42201b46ad703640b0caea8ae31d5db395adf74d9067f09 apps/web/performance/review-db-successor-20261012/README.md +c592db68bb0da972e91035d42543b0023571d378f14ce6da0771a21624212512 apps/web/performance/review-db-successor-20261012/README.md 1a3aa2b9a3e1faef9916cce3d7d3b11246683dadce15da97b3a0e3a75fbb9048 apps/web/performance/review-db-successor-20261012/SHA256SUMS-0833ed81-historical +d19614c3478ddbe90c38b856ebcbdba7a3a4dc9599ce259c2eabd60dcb0337a6 apps/web/performance/review-db-successor-20261012/SHA256SUMS-4ba9c6d2-historical +fe70a536d2fde48ad6332c76b8e64ee8e03f3784398022f190e45827e1c58983 apps/web/performance/review-db-successor-20261012/SHA256SUMS-4ba9c6d2-historical.sha256 b9f3fca7e506e195312c2ca3e4da97f04c7767e42b2a0f980aab68dfc6395e09 apps/web/performance/review-db-successor-20261012/SHA256SUMS-4d4ecf60-historical dc27a75b296716c612e9e3b9cb35f0fd113129dad5215f72a76f3ea94747ee95 apps/web/performance/review-db-successor-20261012/fixture-ownership-failure-interpretation-20261013.json d9cdeba56f0c38a0c1c46f50b48645d827b47df2ba09f3101ff01dcc828cf118 apps/web/performance/review-db-successor-20261012/fixture-ownership-first-attempt-20261013.json 0a3b893331ff16732dd79c6bfca9f88689f7be3065777e62b0dcf3528cea05df apps/web/performance/review-db-successor-20261012/fixture-ownership-protocol-failure-20261013.json +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v1.json +701edcd5b4d76bfef560aa280ec80b812274058fb358375106f80ba90c526849 apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v1.stderr.log +5a1aa0fb9f45ad4510377455b82b6940f18fa43b25123a4fbafcdb8fcf235ee6 apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v2.json +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v2.stderr.log ef0af48a51ee8e3b9a57725eb7504c88401d2b59c3470dec0da11b5257187fbc apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-20261013.json 0e7f96493ba9ad63959fa25c775fb6e95d0616d47fa4bdae973e248f5eab0349 apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-current-20261013.json ad6917e416ff20e074eaedd523baa3f0366e3c865f62849a7df6793c2fbd758d apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-reviewed-20261013.json 6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f apps/web/performance/review-db-successor-20261012/pg17-local-replay.json +7815d90d73d6b3508a355aee06743c22ff802e78ffca46172cda18ca83ed51b4 apps/web/performance/review-db-successor-20261012/review-fix-harness-units-20261014-v2.log 41972cf6c4ac2b819b42cf9f06b762d54fb9d161f4594192492fccf79cca7418 apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs -768b8887a30c2b8f496a73a21d373b85a0b64fadaed3a1485034a729ee704d39 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs -5d61808b45661d412937fbfdfd3bfb75b1cf92c7c285e4d8ad061a2f667c3c0b apps/web/scripts/review-media-sql-successor.mjs +768b8887a30c2b8f496a73a21d373b85a0b64fadaed3a1485034a729ee704d39 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor-4ba9c6d2-historical.source.txt +7e750e9d0126f6f10afe9056d747a2ea854d462161ea4e8f0168e2751d554c0a apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +4f1b2798e0cfc13db4f11cd6eb4a8653320aa8516eccd71d410fcff6d06a7f24 apps/web/scripts/review-media-sql-successor.mjs fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs 92651102154fd4d573ca841137bd31c319ef545b2527e7da17398bd2e6b97d47 apps/web/tests-unit/review-media-fixture-ownership-verifier.test.ts diff --git a/apps/web/performance/review-db-successor-20261012/SHA256SUMS-4ba9c6d2-historical b/apps/web/performance/review-db-successor-20261012/SHA256SUMS-4ba9c6d2-historical new file mode 100644 index 0000000000..0e1233e64c --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/SHA256SUMS-4ba9c6d2-historical @@ -0,0 +1,17 @@ +23bf7e7d7e9e5d7962021dd3ec860e87b47d07e119ba0e39de2e423a0a601f4e .github/review-media-sql-successor.v1.json +f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs +830e2f3d6baaf95ee42201b46ad703640b0caea8ae31d5db395adf74d9067f09 apps/web/performance/review-db-successor-20261012/README.md +1a3aa2b9a3e1faef9916cce3d7d3b11246683dadce15da97b3a0e3a75fbb9048 apps/web/performance/review-db-successor-20261012/SHA256SUMS-0833ed81-historical +b9f3fca7e506e195312c2ca3e4da97f04c7767e42b2a0f980aab68dfc6395e09 apps/web/performance/review-db-successor-20261012/SHA256SUMS-4d4ecf60-historical +dc27a75b296716c612e9e3b9cb35f0fd113129dad5215f72a76f3ea94747ee95 apps/web/performance/review-db-successor-20261012/fixture-ownership-failure-interpretation-20261013.json +d9cdeba56f0c38a0c1c46f50b48645d827b47df2ba09f3101ff01dcc828cf118 apps/web/performance/review-db-successor-20261012/fixture-ownership-first-attempt-20261013.json +0a3b893331ff16732dd79c6bfca9f88689f7be3065777e62b0dcf3528cea05df apps/web/performance/review-db-successor-20261012/fixture-ownership-protocol-failure-20261013.json +ef0af48a51ee8e3b9a57725eb7504c88401d2b59c3470dec0da11b5257187fbc apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-20261013.json +0e7f96493ba9ad63959fa25c775fb6e95d0616d47fa4bdae973e248f5eab0349 apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-current-20261013.json +ad6917e416ff20e074eaedd523baa3f0366e3c865f62849a7df6793c2fbd758d apps/web/performance/review-db-successor-20261012/pg17-fixture-ownership-reviewed-20261013.json +6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f apps/web/performance/review-db-successor-20261012/pg17-local-replay.json +41972cf6c4ac2b819b42cf9f06b762d54fb9d161f4594192492fccf79cca7418 apps/web/performance/review-db-successor-20261012/verify-fixture-ownership.mjs +768b8887a30c2b8f496a73a21d373b85a0b64fadaed3a1485034a729ee704d39 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +5d61808b45661d412937fbfdfd3bfb75b1cf92c7c285e4d8ad061a2f667c3c0b apps/web/scripts/review-media-sql-successor.mjs +fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs +92651102154fd4d573ca841137bd31c319ef545b2527e7da17398bd2e6b97d47 apps/web/tests-unit/review-media-fixture-ownership-verifier.test.ts diff --git a/apps/web/performance/review-db-successor-20261012/SHA256SUMS-4ba9c6d2-historical.sha256 b/apps/web/performance/review-db-successor-20261012/SHA256SUMS-4ba9c6d2-historical.sha256 new file mode 100644 index 0000000000..c0ba176f4b --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/SHA256SUMS-4ba9c6d2-historical.sha256 @@ -0,0 +1 @@ +d19614c3478ddbe90c38b856ebcbdba7a3a4dc9599ce259c2eabd60dcb0337a6 diff --git a/apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 b/apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 index c0ba176f4b..28f2abcf82 100644 --- a/apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 +++ b/apps/web/performance/review-db-successor-20261012/SHA256SUMS.sha256 @@ -1 +1 @@ -d19614c3478ddbe90c38b856ebcbdba7a3a4dc9599ce259c2eabd60dcb0337a6 +aff9374a0e6a6964b2d32f6cb5c6069296c5155b94c2ec1015460d2386fb4e88 diff --git a/apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v1.json b/apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v1.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v1.stderr.log b/apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v1.stderr.log new file mode 100644 index 0000000000..dba3aafb39 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v1.stderr.log @@ -0,0 +1,12 @@ +file:///Users/twoimo/.codex/worktrees/mobile-nav-read-20261012/tzudong/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs:55 + throw new Error(detail ? `${code}:${detail}` : code); + ^ + +Error: LOCAL_REHEARSAL_ADVISORY_CONTENTION_NOT_OBSERVED + at fail (file:///Users/twoimo/.codex/worktrees/mobile-nav-read-20261012/tzudong/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs:55:9) + at waitForAdvisoryContention (file:///Users/twoimo/.codex/worktrees/mobile-nav-read-20261012/tzudong/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs:823:3) + at async assertContendedExpiryRejected (file:///Users/twoimo/.codex/worktrees/mobile-nav-read-20261012/tzudong/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs:842:34) + at async run (file:///Users/twoimo/.codex/worktrees/mobile-nav-read-20261012/tzudong/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs:993:46) + at async file:///Users/twoimo/.codex/worktrees/mobile-nav-read-20261012/tzudong/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs:1057:85 + +Node.js v24.21.0 diff --git a/apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v2.json b/apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v2.json new file mode 100644 index 0000000000..b7085ca571 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v2.json @@ -0,0 +1 @@ +{"current":{"classification":"current-source-verification","proofSha256":"9a4985689d121979e3d7559c32b8f27c88ae3d1bb82d559fb9bb8ada6e706ac2","bindings":{"manifestSha256":"295b2354c562ed87102c5e256497a2705f90941587fefa7e9ad8a4ba8e40787f","rehearsalHarnessSha256":"7e750e9d0126f6f10afe9056d747a2ea854d462161ea4e8f0168e2751d554c0a","sourceRoot":"61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544"},"proof":{"schemaVersion":1,"status":"passed","environment":{"dockerContext":"colima-tzudong-catalog-20261007","dockerContainer":"tzudong-ranking-clone-20261008","engine":"17.6","templateDatabase":"ranking_clone","temporaryDatabase":"review_successor_local_437a74bbfd2f4629d996b901cf050c29"},"input":{"engine":"17.6","ledger":0,"manifest":1963,"profiles":0,"reviews":0},"manifestSha256":"295b2354c562ed87102c5e256497a2705f90941587fefa7e9ad8a4ba8e40787f","rehearsalHarnessSha256":"7e750e9d0126f6f10afe9056d747a2ea854d462161ea4e8f0168e2751d554c0a","sourceRoot":"61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544","predecessorSources":[{"path":"backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql","sha256":"b373b7ea472c0352a33a4d4043cf8d6aa8474c04cc1ca5778805edfa77ac9c95"},{"path":"backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql","sha256":"66eace1d6fb0dd55c1d780776bab855cc37690335ad40b0fdc1294c610e07d3a"},{"path":"backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql","sha256":"e1c105df82c4f814d3e6adad807ff9d072b8cd42ae770b4b78f75b89a9387020"},{"path":"backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql","sha256":"8acf6d1428764260ed57dac5fe09711868a82896f0a6d631d502128004c168a3"},{"path":"backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql","sha256":"b596b200e52c6813a4cfa1b0a2818625f067864549e8854f3497afdcdab706da"},{"path":"backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql","sha256":"2067538f89c9f90d28e784672c7a1288306ba22d5ae92b087c8692503da9b1ae"}],"migrationCount":11,"statementCount":112,"stageStatesSha256":"e62c3e811bceec08cecb1785950e53cb1cd5a22ed7e80df2fdd79b290962ec04","stages":[{"index":0,"ledgerCount":86,"targetCount":0,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":null,"schemaRoot":"70ef24069d2f6ceeec28db7f715b2bc1e1830e51007df5f97476a64f7583c2be","ledgerRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","g014Root":"4f3886411c6329ec0a6a61743e7e89de21cf519848bc145be2f3e96f1e685149","storageRoot":"2e4b000d1012f1df1e6e6fd3e19cc66e8718a5be1d8cdc3695677bd2cc3bbda8","finishCleanupBodySha256":null,"ownerAuthorityReady":true,"terminalContractReady":false},{"index":1,"ledgerCount":87,"targetCount":1,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":null,"schemaRoot":"6660c658b956479a86b85dc5f6c257bdfde7b94d1e6e7e675b989303bc776de4","ledgerRoot":"9dff2943b2fb957d82b7ac325a999fef829c077e5738f178e5bc19da67609701","g014Root":"4f3886411c6329ec0a6a61743e7e89de21cf519848bc145be2f3e96f1e685149","storageRoot":"145e14b4467ccabbbd0d1009df236b3cc734174a9b287811a55a29058257111f","finishCleanupBodySha256":"1a0eb803e2f2ee4098960ad2fbe2b99b363405536c03a09c0465b8a8ffddecc7","ownerAuthorityReady":true,"terminalContractReady":false},{"index":2,"ledgerCount":88,"targetCount":2,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":null,"schemaRoot":"7574cc0bd42e93dd1112e9068b6538fc2401b5de8d7de3e7ce40762405abbe35","ledgerRoot":"5f3dcac3e6820c2571b2f04221cbdf34ee18085363c434605c445a76ea59061a","g014Root":"4f3886411c6329ec0a6a61743e7e89de21cf519848bc145be2f3e96f1e685149","storageRoot":"98ad8115175e671fce1f853cb48b2e1bb7e430892107fd333bc74df24799cdea","finishCleanupBodySha256":"f7e925a25dedb987a3166c5ca641ba7db1568a7583bdeca3cee04f27d9189f20","ownerAuthorityReady":true,"terminalContractReady":false},{"index":3,"ledgerCount":89,"targetCount":3,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":null,"schemaRoot":"fe8ca8917b834397b49253154bed7074372bc403ffc2a0e06037a650c7cadfb2","ledgerRoot":"928e46ee03f4f2ed492be89617cf8b455f2931f79fdf3c218adbc92c5fa3d21e","g014Root":"810c5888b257c16ea07881e99914a718eede2e2339cbd805381fe7bf346aef34","storageRoot":"4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f","finishCleanupBodySha256":"f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1","ownerAuthorityReady":true,"terminalContractReady":false},{"index":4,"ledgerCount":90,"targetCount":4,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":null,"schemaRoot":"2085388afbef4445f615ee40e5e00837c526e2f29a2e1c3e0786afe8a569fb22","ledgerRoot":"73b30ad98e4cbace156815cd214091312364302e9403065b24a2d1e86227a2c0","g014Root":"68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00","storageRoot":"4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f","finishCleanupBodySha256":"f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1","ownerAuthorityReady":true,"terminalContractReady":false},{"index":5,"ledgerCount":91,"targetCount":5,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":null,"schemaRoot":"0f3f4ed4b7547cc154e5beaeb2621db1c05ff0d4dc7325c47246dcfd4be6fbc5","ledgerRoot":"150bb9f1c5a4b447dd50a86097923fb1c78e522fb5f2bf8ca16b9f08ca6d2b58","g014Root":"68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00","storageRoot":"4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f","finishCleanupBodySha256":"f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1","ownerAuthorityReady":true,"terminalContractReady":false},{"index":6,"ledgerCount":92,"targetCount":6,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":null,"schemaRoot":"8aa69a0aa43e60ab1b6680ebb7b3fb4e4f53b07aa8231847883a181375554690","ledgerRoot":"f57304a9d3122d643535464cf72565aa04c450dbc1d8fe0a3a64add17b5c7c97","g014Root":"68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00","storageRoot":"4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f","finishCleanupBodySha256":"f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1","ownerAuthorityReady":true,"terminalContractReady":false},{"index":7,"ledgerCount":93,"targetCount":7,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":null,"schemaRoot":"cee1ac952c3bd3aa8fff26a625a4140c5ce2028a08a8510eecdc1e760aaf5166","ledgerRoot":"3da3b76460e0ad7249d50a1f4ddf0cb433edd0ca7b3b6b7a8f6e334974888065","g014Root":"68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00","storageRoot":"4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f","finishCleanupBodySha256":"f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1","ownerAuthorityReady":true,"terminalContractReady":false},{"index":8,"ledgerCount":94,"targetCount":8,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":null,"schemaRoot":"ab7f4f4d84c2d759d570669641131f149db2a1a023765fb468e04d7f162348be","ledgerRoot":"c8920bba0db232a705ddf012ee5da809df71d72e3ddfb3f2fbc91d156b87cca2","g014Root":"68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00","storageRoot":"4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f","finishCleanupBodySha256":"f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1","ownerAuthorityReady":true,"terminalContractReady":false},{"index":9,"ledgerCount":95,"targetCount":9,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":null,"schemaRoot":"7409744a943050706edaf080c59b069c10bb3b471296728991c589b2ce3d18bb","ledgerRoot":"b6f0aa150d0a5177828767ca5ad31796f6ae4cc034fcd5c943e15cea3d876a9b","g014Root":"68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00","storageRoot":"4c1c8a63f3cb8553acfcac1c62c5452ab53568724f662e43291d63037910e34f","finishCleanupBodySha256":"f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1","ownerAuthorityReady":true,"terminalContractReady":false},{"index":10,"ledgerCount":96,"targetCount":10,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":null,"schemaRoot":"284d8a28560707a02167e6f9131c2ca4e0658808ace9061b5251d8ff12a236cd","ledgerRoot":"e56731bbe2c7bbdfa826e594d468082c9f16354350f66325e7c58cfdc549fe1a","g014Root":"68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00","storageRoot":"6bf06a15d9ef5cc3abbe210657b6a21b3b50f18e0dde58bea9af5ba958b46a60","finishCleanupBodySha256":"f6c4e71988ed2b789b887484af5fdf2e482bde98f1d1c5b5d4e7ad124c314bd1","ownerAuthorityReady":true,"terminalContractReady":false},{"index":11,"ledgerCount":97,"targetCount":11,"prefixRoot":"c975c881e8eb3af88c4b0fc87f1109ac4e1507258f29363de88760f20713dba2","suffixRoot":"1ecd299cb2525a1d66738d2c0adf01a482e47c44f881f6d09ddc2141d469d32b","schemaRoot":"a255fb9f415627e139762649c4e4230c4f6c56e541be3a09d9f986120287ea7e","ledgerRoot":"fa142da48d834923b4702a302d44ea0f9818d7713da76a316d5959b8852065f2","g014Root":"68e78dd7263864584a6b1b4778ef1aa13942b4d54825910fab1101a989025c00","storageRoot":"6bf06a15d9ef5cc3abbe210657b6a21b3b50f18e0dde58bea9af5ba958b46a60","finishCleanupBodySha256":"a6b4211b2d8486d482f0758ce249eec157249e1a5176be286b76af7e7a775ee9","ownerAuthorityReady":true,"terminalContractReady":true}],"results":{"advisoryContentionObserved":true,"atomicCommit":true,"atomicCommitOutcome":"acknowledged","atomicRollbackExact":true,"catalogDriftStatePreserved":true,"columnAclDriftRejected":true,"compiledSqlBytes":6495623,"compiledSqlSha256":"267a09fdcb24ce8183425dfb847281a2fea9e04226aa94de01b13f3544b353fd","contendedAdmissionExpiryRejected":true,"contendedAdmissionExpiryStatePreserved":true,"membershipOptionsBaseline":{"inherit":true,"set":true},"membershipInheritOptionDriftRejected":true,"membershipSetOptionDriftRejected":true,"securityInvokerBaseline":true,"securityInvokerDriftRejected":true,"wrongFinishPreimageRejected":true,"wrongFinishPreimageRollbackExact":true,"terminalLedgerCount":97,"terminalContractReady":true},"sourceMutation":false,"hostedWrites":false,"operatingWrites":false,"temporaryDatabaseDropped":true,"fixtureRolesDropped":true}},"retained":{"present":true,"classification":"retained-historical-evidence","repositoryPath":"apps/web/performance/review-db-successor-20261012/pg17-local-replay.json","expectedSha256":"6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f","actualSha256":"6f5dc5c5603c8a4d8b601d3edb28009e454e955292a65707747e66f274557b3f","checksumMapSha256":"d19614c3478ddbe90c38b856ebcbdba7a3a4dc9599ce259c2eabd60dcb0337a6","bindings":{"manifestSha256":"9fac81d6e3ec81207b9c8985ccfdaf9e300bb7d44a7617d95579d4e8f96af19b","rehearsalHarnessSha256":"03e999caeda7b57925126ba8d6bcec4ac5a671fedb2c69e07e988924ccb9f895","sourceRoot":"61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544"},"currentSourceMatch":false,"bindingDifferences":[{"field":"manifestSha256","retained":"9fac81d6e3ec81207b9c8985ccfdaf9e300bb7d44a7617d95579d4e8f96af19b","current":"295b2354c562ed87102c5e256497a2705f90941587fefa7e9ad8a4ba8e40787f"},{"field":"rehearsalHarnessSha256","retained":"03e999caeda7b57925126ba8d6bcec4ac5a671fedb2c69e07e988924ccb9f895","current":"7e750e9d0126f6f10afe9056d747a2ea854d462161ea4e8f0168e2751d554c0a"}],"usedAsCurrentProof":false}} diff --git a/apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v2.stderr.log b/apps/web/performance/review-db-successor-20261012/pg17-current-review-fixes-20261014-v2.stderr.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/web/performance/review-db-successor-20261012/review-fix-harness-units-20261014-v2.log b/apps/web/performance/review-db-successor-20261012/review-fix-harness-units-20261014-v2.log new file mode 100644 index 0000000000..30a3f294b0 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/review-fix-harness-units-20261014-v2.log @@ -0,0 +1,28 @@ +bun test v1.4.0 (34cbb9a40) + +tests-unit/review-media-sql-successor-harness.test.ts: +(pass) fixture observes delayed advisory contention before expiry without the old 700ms cutoff [0.18ms] +(pass) fixture rejects contention returned by a slow observation after expiry [0.22ms] +(pass) successor acknowledgement loss reconciles a committed terminal state without SQL replay [0.24ms] +(pass) successor failure at the baseline is classified without diagnostic SQL replay [0.06ms] +(pass) retained proof is checksum-verified and distinguished from the current harness source [1.73ms] +(pass) retained proof with bytes not authorized by SHA256SUMS is rejected [1.26ms] +(pass) jointly altered retained proof and checksum map are rejected by the independent digest [1.29ms] +(pass) owned database is cleaned after predecessor SQL fails [0.55ms] +(pass) owned database is cleaned after fixture SQL fails [0.05ms] +(pass) pre-existing database is never acquired or dropped [0.06ms] +(pass) database CREATE acknowledgement loss is reconciled without replay and cleaned by exact ownership [0.16ms] +(pass) cleanup re-reads and removes the exact owned database after the first CREATE readback fails [0.10ms] +(pass) cleanup refuses a database whose exact owner metadata does not match [0.12ms] +(pass) fixture role creation acknowledgement loss is reconciled and exact owned roles are cleaned [0.38ms] +(pass) cleanup re-reads exact owned roles when the first post-create readback fails [0.10ms] +(pass) fixture role creation failure without owned readback never authorizes cleanup [0.05ms] +(pass) pre-existing fixture role collision is preserved and never acquired for cleanup [0.06ms] +(pass) uncertain create collision with another owner is not guessed or removed [0.05ms] +(pass) cleanup refuses roles whose ownership marker changed after creation [0.26ms] +(pass) cleanup refuses to remove owned roles after an out-of-scope membership is added [0.07ms] + + 20 pass + 0 fail + 98 expect() calls +Ran 20 tests across 1 file. [161.00ms] diff --git a/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor-4ba9c6d2-historical.source.txt b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor-4ba9c6d2-historical.source.txt new file mode 100644 index 0000000000..779cf90ec7 --- /dev/null +++ b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor-4ba9c6d2-historical.source.txt @@ -0,0 +1,1050 @@ +#!/usr/bin/env node + +/** + * Local-only PostgreSQL 17.6 rehearsal for the held eleven-stage successor. + * + * The harness clones the existing schema-only ranking_clone template into one + * per-process, owned database name. It refuses a reused target, never connects + * to a hosted endpoint, and drops the owned database before emitting its + * current-source result. + */ +import { createHash, randomBytes } from 'node:crypto'; +import { existsSync, readFileSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawn, spawnSync } from 'node:child_process'; + +import { + buildSuccessorStateQuery, + compileSuccessorPlan, + loadSuccessorManifest, +} from '../../scripts/review-media-sql-successor.mjs'; + +const HERE = dirname(fileURLToPath(import.meta.url)); +const REPOSITORY_ROOT = resolve(HERE, '../../../..'); +const OUTPUT_PATH = resolve(HERE, 'pg17-local-replay.json'); +const CHECKSUMS_PATH = resolve(HERE, 'SHA256SUMS'); +const RETAINED_PROOF_REPOSITORY_PATH = 'apps/web/performance/review-db-successor-20261012/pg17-local-replay.json'; +const DOCKER_CONTEXT = process.env.TZUDONG_REHEARSAL_DOCKER_CONTEXT || 'colima-tzudong-catalog-20261007'; +const DOCKER_CONTAINER = process.env.TZUDONG_REHEARSAL_DOCKER_CONTAINER || 'tzudong-ranking-clone-20261008'; +const TEMPLATE_DATABASE = 'ranking_clone'; +const DATABASE = `review_successor_local_${randomBytes(16).toString('hex')}`; +const FIXTURE_PASSWORD = 'fixture-only'; +const FIXTURE_PARENT_ROLE = 'review_successor_fixture_parent_20261012'; +const FIXTURE_MEMBER_ROLE = 'review_successor_fixture_member_20261012'; +const FIXTURE_OWNER_PREFIX = 'tzudong-review-successor-fixture'; +const EXPIRY_APPLICATION_NAME = 'tzudong-review-successor-expiry-fixture'; +const PREDECESSOR_PATHS = Object.freeze([ + 'backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql', + 'backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql', + 'backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql', + 'backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql', + 'backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql', + 'backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql', +]); +const sha256 = value => createHash('sha256').update(value).digest('hex'); +const quote = value => `'${String(value).replaceAll("'", "''")}'`; +const canonical = value => Array.isArray(value) + ? value.map(canonical) + : value && typeof value === 'object' + ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) + : value; +const equal = (left, right) => JSON.stringify(canonical(left)) === JSON.stringify(canonical(right)); + +function fail(code, detail = '') { + throw new Error(detail ? `${code}:${detail}` : code); +} + +function docker(args, { input, allowFailure = false } = {}) { + const result = spawnSync('docker', ['--context', DOCKER_CONTEXT, ...args], { + encoding: 'utf8', + input, + maxBuffer: 64 * 1024 * 1024, + }); + if (!allowFailure && (result.error || result.status !== 0)) { + const sqlstates = [...(result.stderr || '').matchAll(/(?:ERROR|FATAL):\s+([0-9A-Z]{5}):/g)] + .map(match => match[1]); + const diagnostic = (result.stderr || '').split(/\r?\n/) + .find(line => /(?:ERROR|FATAL):/.test(line)) + ?.replace(/\s+/g, ' ').slice(0, 240) || ''; + fail('LOCAL_REHEARSAL_COMMAND_FAILED', [sqlstates.join(',') || String(result.status), diagnostic] + .filter(Boolean).join(':')); + } + return result; +} + +function psql(database, sql, { actor = 'postgres', singleTransaction = false, allowFailure = false } = {}) { + const options = [ + 'exec', '-i', '-e', `PGPASSWORD=${FIXTURE_PASSWORD}`, DOCKER_CONTAINER, + 'psql', '-XAtq', '-v', 'ON_ERROR_STOP=1', '-v', 'VERBOSITY=terse', + ]; + if (singleTransaction) options.push('--single-transaction'); + options.push('-h', '127.0.0.1', '-U', actor, '-d', database, '--file=-'); + return docker(options, { input: sql, allowFailure }); +} + +function scalar(database, sql, actor = 'postgres') { + return psql(database, `${sql}\n`, { actor }).stdout.trim(); +} + +function readState(database, stateQuery) { + const output = scalar(database, `${stateQuery};`); + const lines = output.split(/\r?\n/).filter(Boolean); + if (lines.length !== 1) fail('LOCAL_REHEARSAL_STATE_SHAPE_INVALID', String(lines.length)); + try { + return JSON.parse(lines[0]); + } catch { + fail('LOCAL_REHEARSAL_STATE_JSON_INVALID'); + } +} + +export function applySuccessorPlanOnce({ + database, + sql, + stateQuery, + baselineState, + terminalState, + psqlImpl = psql, + readStateImpl = readState, +}) { + try { + return Object.freeze({ + outcome: 'acknowledged', + result: psqlImpl(database, sql, { singleTransaction: true }), + }); + } catch { + let observedState; + try { + observedState = readStateImpl(database, stateQuery); + } catch { + fail('LOCAL_REHEARSAL_APPLICATION_OUTCOME_UNCERTAIN'); + } + if (equal(observedState, terminalState)) { + return Object.freeze({ + outcome: 'committed-after-acknowledgement-loss', + result: Object.freeze({ stdout: `${JSON.stringify(observedState)}\n` }), + }); + } + if (equal(observedState, baselineState)) { + fail('LOCAL_REHEARSAL_APPLICATION_ROLLED_BACK'); + } + fail('LOCAL_REHEARSAL_APPLICATION_STATE_AMBIGUOUS'); + } +} + +function ledgerInsert(migration) { + const vector = JSON.stringify(migration.originalStatementVector); + const tag = `$vector_${sha256(vector).slice(0, 20)}$`; + return `INSERT INTO supabase_migrations.schema_migrations(version,statements,name) +SELECT ${quote(migration.version)},ARRAY(SELECT jsonb_array_elements_text(${tag}${vector}${tag}::jsonb)),${quote(migration.name)};`; +} + +function fixtureSql() { + return ` +DO $fixture$ +BEGIN + IF (SELECT count(*) FROM supabase_migrations.schema_migrations)<>0 THEN + RAISE EXCEPTION 'LOCAL_REHEARSAL_LEDGER_NOT_EMPTY'; + END IF; + IF (SELECT count(*) FROM public.profiles)<>0 OR (SELECT count(*) FROM public.reviews)<>0 THEN + RAISE EXCEPTION 'LOCAL_REHEARSAL_PERSONAL_ROWS_PRESENT'; + END IF; +END $fixture$; +INSERT INTO supabase_migrations.schema_migrations(version,statements,name) +SELECT lpad(value::text,14,'0'),ARRAY['local schema-only predecessor fixture'],format('local_predecessor_%s',value) +FROM generate_series(1,86) value; +INSERT INTO pipeline_control.restaurant_review_policy(singleton,enabled) +VALUES(true,false) +ON CONFLICT(singleton) DO UPDATE SET enabled=false,operator_id=NULL,last_run_at=NULL; +INSERT INTO pipeline_control.admin_evaluation_catalog_revision(singleton,revision) +VALUES(true,1) +ON CONFLICT(singleton) DO NOTHING; +`; +} + +function databaseMetadataSql(databaseName) { + const projection = alias => `jsonb_build_object( + 'name',${alias}.datname, + 'owner',pg_get_userbyid(${alias}.datdba), + 'encoding',pg_encoding_to_char(${alias}.encoding), + 'collate',${alias}.datcollate, + 'ctype',${alias}.datctype, + 'localeProvider',${alias}.datlocprovider::text, + 'allowConnections',${alias}.datallowconn, + 'isTemplate',${alias}.datistemplate + )`; + return `SELECT jsonb_build_object( + 'target',(SELECT ${projection('target_database')} FROM pg_database target_database + WHERE target_database.datname=${quote(databaseName)}), + 'template',(SELECT ${projection('template_database')} FROM pg_database template_database + WHERE template_database.datname=${quote(TEMPLATE_DATABASE)}) +)::text;`; +} + +function inspectDatabaseOwnership(databaseName, { scalarImpl = scalar } = {}) { + let state; + try { + state = JSON.parse(scalarImpl('postgres', databaseMetadataSql(databaseName), 'supabase_admin')); + } catch { + fail('LOCAL_REHEARSAL_DATABASE_STATE_INVALID'); + } + if (!state + || typeof state !== 'object' + || !Object.hasOwn(state, 'target') + || !databaseMetadataValid(state.template) + || state.template.name !== TEMPLATE_DATABASE + || (state.target !== null && !databaseMetadataValid(state.target))) { + fail('LOCAL_REHEARSAL_DATABASE_STATE_INVALID'); + } + return state; +} + +function databaseMetadataValid(metadata) { + return metadata + && typeof metadata === 'object' + && ['name', 'owner', 'encoding', 'collate', 'ctype', 'localeProvider'] + .every(field => typeof metadata[field] === 'string' && metadata[field].length > 0) + && typeof metadata.allowConnections === 'boolean' + && typeof metadata.isTemplate === 'boolean'; +} + +function databaseTargetAbsent(state, databaseName) { + return state?.target === null && state?.template?.name === TEMPLATE_DATABASE && databaseName === DATABASE; +} + +function databaseTargetOwned(state, baseline, databaseName) { + const target = state?.target; + const template = state?.template; + return databaseTargetAbsent(baseline, databaseName) + && target?.name === databaseName + && target.owner === 'postgres' + && target.allowConnections === true + && target.isTemplate === false + && template?.name === TEMPLATE_DATABASE + && equal(template, baseline.template) + && target.encoding === template.encoding + && target.collate === template.collate + && target.ctype === template.ctype + && target.localeProvider === template.localeProvider; +} + +export function createOwnedDatabase(ownership, { + scalarImpl = scalar, + psqlImpl = psql, + readFileImpl = readFileSync, +} = {}) { + if (!ownership + || ownership.database !== false + || ownership.databaseAttempted + || ownership.databaseName + || ownership.databaseBaseline) { + fail('LOCAL_REHEARSAL_OWNERSHIP_STATE_INVALID'); + } + const baseline = inspectDatabaseOwnership(DATABASE, { scalarImpl }); + if (!databaseTargetAbsent(baseline, DATABASE)) fail('LOCAL_REHEARSAL_TARGET_ALREADY_EXISTS'); + ownership.databaseName = DATABASE; + ownership.databaseBaseline = baseline; + ownership.databaseAttempted = true; + let creationAcknowledged = false; + try { + psqlImpl('postgres', `CREATE DATABASE ${DATABASE} TEMPLATE ${TEMPLATE_DATABASE} OWNER postgres;`, { actor: 'supabase_admin' }); + creationAcknowledged = true; + } catch { + // CREATE is intentionally never replayed. The exact catalog readback below + // distinguishes a committed CREATE from a failure before mutation. + } + let observed; + try { + observed = inspectDatabaseOwnership(DATABASE, { scalarImpl }); + } catch { + fail('LOCAL_REHEARSAL_DATABASE_READBACK_FAILED'); + } + if (!databaseTargetOwned(observed, baseline, DATABASE)) { + if (databaseTargetAbsent(observed, DATABASE)) { + ownership.databaseAttempted = false; + ownership.databaseName = null; + ownership.databaseBaseline = null; + fail('LOCAL_REHEARSAL_DATABASE_CREATION_UNCONFIRMED'); + } + fail('LOCAL_REHEARSAL_DATABASE_OWNERSHIP_UNPROVEN'); + } + ownership.database = true; + ownership.databaseCreationAcknowledged = creationAcknowledged; + ownership.databaseCreationReconciled = !creationAcknowledged; + PREDECESSOR_PATHS.forEach((path, index) => { + try { + psqlImpl(DATABASE, readFileImpl(resolve(REPOSITORY_ROOT, path))); + } catch (error) { + fail('LOCAL_REHEARSAL_PREDECESSOR_FAILED', `${index + 1}:${error.message}`); + } + }); + psqlImpl(DATABASE, fixtureSql()); + return true; +} + +export function dropOwnedDatabase(ownership, { scalarImpl = scalar, psqlImpl = psql } = {}) { + if (!ownership?.database && !ownership?.databaseAttempted) return false; + if (ownership.databaseName !== DATABASE + || !ownership.databaseBaseline + || !databaseTargetAbsent(ownership.databaseBaseline, DATABASE)) { + fail('LOCAL_REHEARSAL_DATABASE_OWNERSHIP_STATE_INVALID'); + } + let before; + try { + before = inspectDatabaseOwnership(DATABASE, { scalarImpl }); + } catch { + fail('LOCAL_REHEARSAL_DATABASE_CLEANUP_READBACK_FAILED'); + } + if (databaseTargetAbsent(before, DATABASE)) { + const wasOwned = ownership.database === true; + ownership.database = false; + ownership.databaseAttempted = false; + ownership.databaseDropped = wasOwned; + ownership.databaseCleanupReconciled = true; + ownership.databaseName = null; + ownership.databaseBaseline = null; + return false; + } + if (!databaseTargetOwned(before, ownership.databaseBaseline, DATABASE)) { + fail('LOCAL_REHEARSAL_DATABASE_OWNERSHIP_UNPROVEN'); + } + ownership.database = true; + let removalAcknowledged = false; + try { + psqlImpl('postgres', `DROP DATABASE ${DATABASE} WITH (FORCE);`, { actor: 'supabase_admin' }); + removalAcknowledged = true; + } catch { + // A lost DROP acknowledgement is reconciled once; DROP is not replayed. + } + let after; + try { + after = inspectDatabaseOwnership(DATABASE, { scalarImpl }); + } catch { + fail('LOCAL_REHEARSAL_DATABASE_CLEANUP_READBACK_FAILED'); + } + if (!databaseTargetAbsent(after, DATABASE)) { + if (!databaseTargetOwned(after, ownership.databaseBaseline, DATABASE)) { + fail('LOCAL_REHEARSAL_DATABASE_OWNERSHIP_UNPROVEN'); + } + fail('LOCAL_REHEARSAL_DATABASE_DROP_UNCONFIRMED'); + } + ownership.database = false; + ownership.databaseAttempted = false; + ownership.databaseDropped = true; + ownership.databaseRemovalAcknowledged = removalAcknowledged; + ownership.databaseCleanupReconciled = !removalAcknowledged; + ownership.databaseName = null; + ownership.databaseBaseline = null; + return true; +} + +function resetOwnedDatabase(ownership) { + dropOwnedDatabase(ownership); + createOwnedDatabase(ownership); +} + +function fixtureRoleStateQuery() { + return `WITH fixture_roles AS ( + SELECT fixture_role.rolname, + fixture_role.rolcanlogin, + fixture_role.rolsuper, + fixture_role.rolcreatedb, + fixture_role.rolcreaterole, + fixture_role.rolinherit, + fixture_role.rolreplication, + fixture_role.rolbypassrls, + fixture_role.rolconnlimit, + fixture_role.rolvaliduntil, + fixture_role.rolconfig, + coalesce(shobj_description(fixture_role.oid,'pg_authid'),'') AS owner_marker + FROM pg_roles fixture_role + WHERE fixture_role.rolname IN(${quote(FIXTURE_PARENT_ROLE)},${quote(FIXTURE_MEMBER_ROLE)}) +), fixture_memberships AS ( + SELECT parent.rolname AS parent, + member.rolname AS member, + grantor.rolname AS grantor, + membership.admin_option, + membership.inherit_option, + membership.set_option + FROM pg_auth_members membership + JOIN pg_roles parent ON parent.oid=membership.roleid + JOIN pg_roles member ON member.oid=membership.member + JOIN pg_roles grantor ON grantor.oid=membership.grantor + WHERE parent.rolname IN(${quote(FIXTURE_PARENT_ROLE)},${quote(FIXTURE_MEMBER_ROLE)}) + OR member.rolname IN(${quote(FIXTURE_PARENT_ROLE)},${quote(FIXTURE_MEMBER_ROLE)}) +) +SELECT jsonb_build_object( + 'roles',coalesce((SELECT jsonb_agg(to_jsonb(fixture_role) ORDER BY fixture_role.rolname) FROM fixture_roles fixture_role),'[]'::jsonb), + 'memberships',coalesce((SELECT jsonb_agg(to_jsonb(fixture_membership) ORDER BY fixture_membership.parent,fixture_membership.member,fixture_membership.grantor) FROM fixture_memberships fixture_membership),'[]'::jsonb) +)::text;`; +} + +function inspectFixtureMembership({ scalarImpl = scalar } = {}) { + let output; + try { + output = scalarImpl('postgres', fixtureRoleStateQuery(), 'supabase_admin'); + } catch { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_FAILED'); + } + let state; + try { + state = JSON.parse(output); + } catch { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_INVALID'); + } + if (!Array.isArray(state?.roles) || !Array.isArray(state?.memberships)) { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_READBACK_INVALID'); + } + return Object.freeze({ + roles: Object.freeze(state.roles.map(role => Object.freeze({ ...role }))), + memberships: Object.freeze(state.memberships.map(membership => Object.freeze({ ...membership }))), + }); +} + +function fixtureMembershipAbsent(state) { + return state.roles.length === 0 && state.memberships.length === 0; +} + +function fixtureMembershipOwned(state, token) { + const roles = new Map(state.roles.map(role => [role.rolname, role])); + const parent = roles.get(FIXTURE_PARENT_ROLE); + const member = roles.get(FIXTURE_MEMBER_ROLE); + const membership = state.memberships[0]; + const sharedRoleShape = role => role + && role.rolcanlogin === false + && role.rolsuper === false + && role.rolcreatedb === false + && role.rolcreaterole === false + && role.rolreplication === false + && role.rolbypassrls === false + && role.rolconnlimit === -1 + && role.rolvaliduntil === null + && role.rolconfig === null; + return state.roles.length === 2 + && sharedRoleShape(parent) + && parent.rolinherit === false + && parent.owner_marker === `${FIXTURE_OWNER_PREFIX}:${token}:parent` + && sharedRoleShape(member) + && member.rolinherit === true + && member.owner_marker === `${FIXTURE_OWNER_PREFIX}:${token}:member` + && state.memberships.length === 1 + && membership.parent === FIXTURE_PARENT_ROLE + && membership.member === FIXTURE_MEMBER_ROLE + && membership.grantor === 'supabase_admin' + && membership.admin_option === false + && membership.inherit_option === true + && membership.set_option === true; +} + +export function createFixtureMembership(ownership, { + scalarImpl = scalar, + psqlImpl = psql, + generateToken = () => randomBytes(16).toString('hex'), +} = {}) { + if (!ownership + || ownership.fixtureMembership !== false + || ownership.fixtureMembershipAttempted + || ownership.fixtureMembershipToken + || ownership.fixtureMembershipBaseline) { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_STATE_INVALID'); + } + const baseline = inspectFixtureMembership({ scalarImpl }); + ownership.fixtureMembershipBaseline = baseline; + if (!fixtureMembershipAbsent(baseline)) fail('LOCAL_REHEARSAL_FIXTURE_ROLE_EXISTS'); + const token = generateToken(); + if (!/^[0-9a-f]{32}$/.test(token)) fail('LOCAL_REHEARSAL_FIXTURE_ROLE_TOKEN_INVALID'); + ownership.fixtureMembershipToken = token; + ownership.fixtureMembershipAttempted = true; + let creationAcknowledged = false; + try { + psqlImpl('postgres', ` +BEGIN; +CREATE ROLE ${FIXTURE_PARENT_ROLE} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOREPLICATION NOBYPASSRLS; +CREATE ROLE ${FIXTURE_MEMBER_ROLE} NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOREPLICATION NOBYPASSRLS; +COMMENT ON ROLE ${FIXTURE_PARENT_ROLE} IS ${quote(`${FIXTURE_OWNER_PREFIX}:${token}:parent`)}; +COMMENT ON ROLE ${FIXTURE_MEMBER_ROLE} IS ${quote(`${FIXTURE_OWNER_PREFIX}:${token}:member`)}; +GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} + WITH ADMIN FALSE, INHERIT TRUE, SET TRUE GRANTED BY supabase_admin; +COMMIT; +`, { actor: 'supabase_admin' }); + creationAcknowledged = true; + } catch {} + const observed = inspectFixtureMembership({ scalarImpl }); + if (!fixtureMembershipOwned(observed, token)) { + ownership.fixtureMembershipAttempted = false; + if (fixtureMembershipAbsent(observed)) fail('LOCAL_REHEARSAL_FIXTURE_ROLE_CREATION_UNCONFIRMED'); + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_UNPROVEN'); + } + ownership.fixtureMembership = true; + ownership.fixtureMembershipCreationAcknowledged = creationAcknowledged; + ownership.fixtureMembershipCreationReconciled = !creationAcknowledged; + return true; +} + +export function dropFixtureMembership(ownership, { + scalarImpl = scalar, + psqlImpl = psql, +} = {}) { + if (!ownership?.fixtureMembership && !ownership?.fixtureMembershipAttempted) return false; + const token = ownership.fixtureMembershipToken; + if (!/^[0-9a-f]{32}$/.test(token || '') + || !ownership.fixtureMembershipBaseline + || !fixtureMembershipAbsent(ownership.fixtureMembershipBaseline)) { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_STATE_INVALID'); + } + const before = inspectFixtureMembership({ scalarImpl }); + if (fixtureMembershipAbsent(before)) { + const wasOwned = ownership.fixtureMembership === true; + ownership.fixtureMembership = false; + ownership.fixtureMembershipAttempted = false; + ownership.fixtureMembershipDropped = wasOwned; + ownership.fixtureMembershipCleanupReconciled = true; + return wasOwned; + } + if (!fixtureMembershipOwned(before, token)) { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_UNPROVEN'); + } + // An earlier creation readback can fail after COMMIT. Exact token, shape, + // membership, and empty preimage establish this run's cleanup authority. + ownership.fixtureMembership = true; + let removalAcknowledged = false; + try { + psqlImpl('postgres', ` +BEGIN; +REVOKE ${FIXTURE_PARENT_ROLE} FROM ${FIXTURE_MEMBER_ROLE} GRANTED BY supabase_admin; +DROP ROLE ${FIXTURE_MEMBER_ROLE}; +DROP ROLE ${FIXTURE_PARENT_ROLE}; +COMMIT; +`, { actor: 'supabase_admin' }); + removalAcknowledged = true; + } catch {} + const after = inspectFixtureMembership({ scalarImpl }); + if (!fixtureMembershipAbsent(after)) { + if (!fixtureMembershipOwned(after, token)) { + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_OWNERSHIP_UNPROVEN'); + } + fail('LOCAL_REHEARSAL_FIXTURE_ROLE_REMOVAL_UNCONFIRMED'); + } + ownership.fixtureMembership = false; + ownership.fixtureMembershipAttempted = false; + ownership.fixtureMembershipDropped = true; + ownership.fixtureMembershipRemovalAcknowledged = removalAcknowledged; + return true; +} + +function cleanupOwnedFixtures(ownership) { + const failures = []; + try { + dropOwnedDatabase(ownership); + } catch (error) { + failures.push(`database:${error.message}`); + } + try { + dropFixtureMembership(ownership); + } catch (error) { + failures.push(`roles:${error.message}`); + } + if (failures.length > 0) fail('LOCAL_REHEARSAL_CLEANUP_FAILED', failures.join(',')); +} + +export function inspectRetainedProof({ + outputPath = OUTPUT_PATH, + checksumsPath = CHECKSUMS_PATH, + currentBindings, + existsImpl = existsSync, + readFileImpl = readFileSync, +} = {}) { + if (!existsImpl(outputPath)) return Object.freeze({ present: false }); + if (!currentBindings || typeof currentBindings !== 'object') { + fail('LOCAL_REHEARSAL_CURRENT_BINDINGS_INVALID'); + } + const checksumLines = readFileImpl(checksumsPath, 'utf8').split(/\r?\n/).filter(Boolean); + const matchingLines = checksumLines.filter(line => line.endsWith(` ${RETAINED_PROOF_REPOSITORY_PATH}`)); + if (matchingLines.length !== 1) fail('LOCAL_REHEARSAL_RETAINED_CHECKSUM_MISSING'); + const checksumMatch = matchingLines[0].match(/^([0-9a-f]{64}) (.+)$/); + if (!checksumMatch || checksumMatch[2] !== RETAINED_PROOF_REPOSITORY_PATH) { + fail('LOCAL_REHEARSAL_RETAINED_CHECKSUM_INVALID'); + } + const bytes = readFileImpl(outputPath); + const actualSha256 = sha256(bytes); + if (actualSha256 !== checksumMatch[1]) fail('LOCAL_REHEARSAL_RETAINED_SHA256_MISMATCH'); + let retained; + try { + retained = JSON.parse(bytes.toString('utf8')); + } catch { + fail('LOCAL_REHEARSAL_RETAINED_JSON_INVALID'); + } + const retainedBindings = { + manifestSha256: retained.manifestSha256, + rehearsalHarnessSha256: retained.rehearsalHarnessSha256, + sourceRoot: retained.sourceRoot, + }; + if (retained.status !== 'passed' + || retained.temporaryDatabaseDropped !== true + || retained.hostedWrites !== false + || retained.operatingWrites !== false + || Object.values(retainedBindings).some(value => !/^[0-9a-f]{64}$/.test(value))) { + fail('LOCAL_REHEARSAL_RETAINED_SHAPE_INVALID'); + } + const bindingDifferences = Object.keys(retainedBindings) + .filter(key => retainedBindings[key] !== currentBindings[key]) + .map(key => ({ + field: key, + retained: retainedBindings[key], + current: currentBindings[key], + })); + return Object.freeze({ + present: true, + classification: 'retained-historical-evidence', + repositoryPath: RETAINED_PROOF_REPOSITORY_PATH, + expectedSha256: checksumMatch[1], + actualSha256, + bindings: Object.freeze(retainedBindings), + currentSourceMatch: bindingDifferences.length === 0, + bindingDifferences: Object.freeze(bindingDifferences), + usedAsCurrentProof: false, + }); +} + +function applyRawMigration(record, index) { + const migration = record.manifest.migrations[index]; + const material = record.materials[index]; + try { + psql(DATABASE, material.bytes); + psql(DATABASE, `${ledgerInsert(migration)}\n`); + } catch (error) { + fail('LOCAL_REHEARSAL_MIGRATION_FAILED', `${index + 1}:${error.message}`); + } +} + +function collectStageStates(record, stateQuery) { + const states = [readState(DATABASE, stateQuery)]; + for (let index = 0; index < record.manifest.migrations.length; index += 1) { + applyRawMigration(record, index); + states.push(readState(DATABASE, stateQuery)); + } + return states; +} + +function summarizeStage(stage, index) { + return { + index, + ledgerCount: stage.bundle.ledgerCount, + targetCount: stage.bundle.targetCount, + prefixRoot: stage.bundle.prefixRoot, + suffixRoot: stage.bundle.suffixRoot, + schemaRoot: stage.state.schemaRoot, + ledgerRoot: stage.state.ledgerRoot, + g014Root: stage.state.g014Root, + storageRoot: stage.state.storageRoot, + finishCleanupBodySha256: stage.state.finishCleanupBodySha256, + ownerAuthorityReady: stage.state.ownerAuthorityReady, + terminalContractReady: stage.state.terminalContractReady, + }; +} + +function assertRehearsalStages(record, stages) { + const { manifest } = record; + if (stages.length !== 12) fail('LOCAL_REHEARSAL_STAGE_COUNT_INVALID'); + const prefixRoot = stages[0].bundle.prefixRoot; + stages.forEach((stage, index) => { + if (stage.bundle.ledgerCount !== 86 + index + || stage.bundle.priorCount !== 86 + || stage.bundle.prefixRoot !== prefixRoot + || stage.bundle.targetCount !== index + || stage.state.activeReviewItems !== 0 + || stage.state.reviewItems !== 0 + || stage.state.reviewRuns !== 0 + || stage.state.reviewPolicyEnabled !== false + || stage.state.currentUser !== 'postgres' + || stage.state.sessionUser !== 'postgres' + || stage.state.databaseName !== DATABASE + || stage.state.serverMajor !== 17 + || stage.state.ownerAuthorityReady !== true + || stage.state.terminalContractReady !== (index === 11)) { + fail('LOCAL_REHEARSAL_STAGE_INVALID', String(index)); + } + }); + if (stages[10].state.finishCleanupBodySha256 !== manifest.terminalContract.finishPreimageSha256 + || stages[11].state.finishCleanupBodySha256 !== manifest.terminalContract.finishPostimageSha256) { + fail('LOCAL_REHEARSAL_FINISH_PREIMAGE_INVALID'); + } + if (!equal(stages.slice(0, 3).map(stage => stage.state.g014Root), Array(3).fill(stages[0].state.g014Root)) + || !equal(stages.slice(4).map(stage => stage.state.g014Root), Array(8).fill(stages[4].state.g014Root))) { + fail('LOCAL_REHEARSAL_G014_TRANSITION_INVALID'); + } +} + +function tamperFinishCleanupPreimage() { + psql(DATABASE, ` +BEGIN; +DO $guard$ BEGIN + IF EXISTS(SELECT 1 FROM pg_auth_members + WHERE roleid='privacy_workflow_owner'::regrole AND member='postgres'::regrole AND grantor='postgres'::regrole) + THEN RAISE EXCEPTION 'LOCAL_REHEARSAL_SELF_GRANT_EXISTS'; END IF; +END $guard$; +GRANT privacy_workflow_owner TO postgres WITH ADMIN FALSE,INHERIT FALSE,SET TRUE GRANTED BY postgres; +SET LOCAL ROLE privacy_workflow_owner; +DO $tamper$ +DECLARE definition text; source text; +BEGIN + SELECT pg_get_functiondef(oid),prosrc INTO definition,source + FROM pg_proc WHERE oid='public.finish_review_media_cleanup()'::regprocedure; + EXECUTE replace(definition,source,source || E'\\n-- local-only preimage drift'); +END $tamper$; +RESET ROLE; +REVOKE privacy_workflow_owner FROM postgres GRANTED BY postgres; +COMMIT; +`); +} + +function assertCatalogDriftRejected({ label, ownership, plan, stages, stateQuery, tamperSql, restoreSql = null }) { + resetOwnedDatabase(ownership); + const baseline = readState(DATABASE, stateQuery); + if (!equal(baseline, stages[0])) fail('LOCAL_REHEARSAL_DRIFT_BASELINE_INVALID', label); + psql(DATABASE, tamperSql, { actor: 'supabase_admin' }); + const driftedBefore = readState(DATABASE, stateQuery); + if (driftedBefore.state.schemaRoot === baseline.state.schemaRoot + || driftedBefore.state.g014Root !== baseline.state.g014Root + || driftedBefore.state.storageRoot !== baseline.state.storageRoot + || driftedBefore.state.ledgerRoot !== baseline.state.ledgerRoot) { + fail('LOCAL_REHEARSAL_DRIFT_PROJECTION_INVALID', label); + } + const rejected = psql(DATABASE, plan.sql, { singleTransaction: true, allowFailure: true }); + if (rejected.status === 0 || !/MIGRATION_PRIOR_STATE_MISMATCH/.test(rejected.stderr)) { + fail('LOCAL_REHEARSAL_CATALOG_DRIFT_NOT_REJECTED', label); + } + const driftedAfter = readState(DATABASE, stateQuery); + if (!equal(driftedBefore, driftedAfter) + || driftedAfter.bundle.targetCount !== 0 + || driftedAfter.bundle.ledgerCount !== 86) { + fail('LOCAL_REHEARSAL_CATALOG_DRIFT_STATE_CHANGED', label); + } + if (restoreSql) { + psql(DATABASE, restoreSql, { actor: 'supabase_admin' }); + if (!equal(readState(DATABASE, stateQuery), baseline)) { + fail('LOCAL_REHEARSAL_CATALOG_DRIFT_RESTORE_FAILED', label); + } + } + return true; +} + +function startAdvisoryLockHolder() { + const args = [ + '--context', DOCKER_CONTEXT, + 'exec', '-i', '-e', `PGPASSWORD=${FIXTURE_PASSWORD}`, DOCKER_CONTAINER, + 'psql', '-XAtq', '-v', 'ON_ERROR_STOP=1', '-h', '127.0.0.1', '-U', 'postgres', '-d', DATABASE, + ]; + const child = spawn('docker', args, { stdio: ['pipe', 'pipe', 'pipe'] }); + let stdout = ''; + let stderr = ''; + let settled = false; + const ready = new Promise((resolveReady, rejectReady) => { + const reject = detail => { + if (!settled) { + settled = true; + rejectReady(new Error(`LOCAL_REHEARSAL_LOCK_HOLDER_FAILED:${detail}`)); + } + }; + child.on('error', error => reject(error.code || 'SPAWN')); + child.stdout.on('data', chunk => { + stdout += chunk.toString(); + if (!settled && stdout.includes('LOCK_HELD')) { + settled = true; + resolveReady(); + } + }); + child.stderr.on('data', chunk => { stderr += chunk.toString(); }); + child.on('exit', code => { + if (!settled) reject(`${code}:${stderr.replace(/\s+/g, ' ').slice(0, 120)}`); + }); + }); + const completion = new Promise((resolveCompletion, rejectCompletion) => { + child.on('error', rejectCompletion); + child.on('close', code => { + if (code === 0) resolveCompletion(); + else rejectCompletion(new Error(`LOCAL_REHEARSAL_LOCK_HOLDER_FAILED:${code}:${stderr.replace(/\s+/g, ' ').slice(0, 120)}`)); + }); + }); + child.stdin.end(`BEGIN;\nSELECT pg_advisory_xact_lock(7311754282260211::bigint);\nSELECT 'LOCK_HELD';\nSELECT pg_sleep(1.55);\nCOMMIT;\n`); + return { child, completion, ready }; +} + +function startPsqlProcess(database, sql, { applicationName, singleTransaction = false } = {}) { + const args = [ + '--context', DOCKER_CONTEXT, + 'exec', '-i', '-e', `PGPASSWORD=${FIXTURE_PASSWORD}`, + ]; + if (applicationName) args.push('-e', `PGAPPNAME=${applicationName}`); + args.push( + DOCKER_CONTAINER, + 'psql', '-XAtq', '-v', 'ON_ERROR_STOP=1', '-v', 'VERBOSITY=terse', + ); + if (singleTransaction) args.push('--single-transaction'); + args.push('-h', '127.0.0.1', '-U', 'postgres', '-d', database, '--file=-'); + const child = spawn('docker', args, { stdio: ['pipe', 'pipe', 'pipe'] }); + let stdout = ''; + let stderr = ''; + let inputError = null; + child.stdout.on('data', chunk => { stdout += chunk.toString(); }); + child.stderr.on('data', chunk => { stderr += chunk.toString(); }); + child.stdin.on('error', error => { + if (error.code !== 'EPIPE') inputError = error; + }); + const completion = new Promise((resolveCompletion, rejectCompletion) => { + child.on('error', rejectCompletion); + child.on('close', status => { + if (inputError) rejectCompletion(inputError); + else resolveCompletion({ status, stderr, stdout }); + }); + }); + child.stdin.end(sql); + return { child, completion }; +} + +async function waitForAdvisoryContention(expiresAt) { + const deadline = Math.min(expiresAt - 25, Date.now() + 700); + while (Date.now() < deadline) { + const waiting = scalar(DATABASE, `SELECT count(*) FROM pg_stat_activity a + JOIN pg_locks l ON l.pid=a.pid + WHERE a.application_name=${quote(EXPIRY_APPLICATION_NAME)} + AND l.locktype='advisory' AND NOT l.granted;`, 'supabase_admin'); + if (waiting === '1') return Date.now(); + if (waiting !== '0') fail('LOCAL_REHEARSAL_CONTENTION_CARDINALITY_INVALID'); + await new Promise(resolveDelay => setTimeout(resolveDelay, 20)); + } + fail('LOCAL_REHEARSAL_ADVISORY_CONTENTION_NOT_OBSERVED'); +} + +async function assertContendedExpiryRejected(record, stages, stateQuery, ownership) { + resetOwnedDatabase(ownership); + const baseline = readState(DATABASE, stateQuery); + if (!equal(baseline, stages[0])) fail('LOCAL_REHEARSAL_EXPIRY_BASELINE_INVALID'); + const expiresAt = Date.now() + 1500; + const expiry = new Date(expiresAt).toISOString(); + const expiringPlan = compileSuccessorPlan(record, { expiresAt: expiry, stageStates: stages }); + const holder = startAdvisoryLockHolder(); + let apply = null; + try { + await holder.ready; + if (Date.now() >= expiresAt) fail('LOCAL_REHEARSAL_EXPIRY_WINDOW_TOO_SHORT'); + apply = startPsqlProcess(DATABASE, expiringPlan.sql, { + applicationName: EXPIRY_APPLICATION_NAME, + singleTransaction: true, + }); + const contentionObservedAt = await waitForAdvisoryContention(expiresAt); + if (contentionObservedAt >= expiresAt) fail('LOCAL_REHEARSAL_CONTENTION_OBSERVED_AFTER_EXPIRY'); + const [rejected] = await Promise.all([apply.completion, holder.completion]); + if (rejected.status === 0 + || !/SUCCESSOR_ADMISSION_EXPIRED/.test(rejected.stderr) + || /lock timeout/.test(rejected.stderr)) { + fail('LOCAL_REHEARSAL_CONTENDED_EXPIRY_NOT_REJECTED'); + } + } finally { + if (apply?.child.exitCode === null) apply.child.kill('SIGTERM'); + if (holder.child.exitCode === null) holder.child.kill('SIGTERM'); + } + const after = readState(DATABASE, stateQuery); + if (!equal(after, baseline) || after.bundle.targetCount !== 0 || after.bundle.ledgerCount !== 86) { + fail('LOCAL_REHEARSAL_CONTENDED_EXPIRY_STATE_CHANGED'); + } + return true; +} + +export async function run() { + const manifestBytes = readFileSync(resolve(REPOSITORY_ROOT, '.github/review-media-sql-successor.v1.json')); + const record = loadSuccessorManifest(); + const currentBindings = Object.freeze({ + manifestSha256: sha256(manifestBytes), + rehearsalHarnessSha256: sha256(readFileSync(fileURLToPath(import.meta.url))), + sourceRoot: record.manifest.sourceRoot, + }); + const retainedProof = inspectRetainedProof({ currentBindings }); + const stateQuery = buildSuccessorStateQuery(record.manifest); + const template = JSON.parse(scalar(TEMPLATE_DATABASE, `SELECT json_build_object( + 'engine',current_setting('server_version'), + 'ledger',(SELECT count(*) FROM supabase_migrations.schema_migrations), + 'manifest',(SELECT count(*) FROM privacy_retention.g014_catalog_contract_manifest), + 'profiles',(SELECT count(*) FROM public.profiles), + 'reviews',(SELECT count(*) FROM public.reviews) + )::text;`, 'supabase_admin')); + if (!String(template.engine).startsWith('17.6') + || template.ledger !== 0 + || template.profiles !== 0 + || template.reviews !== 0) fail('LOCAL_REHEARSAL_TEMPLATE_INVALID'); + + const ownership = { + database: false, + databaseAttempted: false, + databaseName: null, + databaseBaseline: null, + fixtureMembership: false, + fixtureMembershipAttempted: false, + fixtureMembershipToken: null, + fixtureMembershipBaseline: null, + }; + let proof; + try { + createFixtureMembership(ownership); + createOwnedDatabase(ownership); + if (scalar(DATABASE, `SELECT coalesce('security_invoker=true'=ANY(c.reloptions),false) + FROM pg_class c WHERE c.oid='public.admin_evaluation_related_rows'::regclass;`) !== 't') { + fail('LOCAL_REHEARSAL_SECURITY_INVOKER_BASELINE_INVALID'); + } + if (scalar(DATABASE, `SELECT m.inherit_option::text || ':' || m.set_option::text + FROM pg_auth_members m + WHERE m.roleid=${quote(FIXTURE_PARENT_ROLE)}::regrole + AND m.member=${quote(FIXTURE_MEMBER_ROLE)}::regrole + AND m.grantor='supabase_admin'::regrole;`, 'supabase_admin') !== 'true:true') { + fail('LOCAL_REHEARSAL_MEMBERSHIP_OPTIONS_BASELINE_INVALID'); + } + const stages = collectStageStates(record, stateQuery); + assertRehearsalStages(record, stages); + const plan = compileSuccessorPlan(record, { + expiresAt: '9999-12-31T23:59:59.999Z', + stageStates: stages, + }); + + resetOwnedDatabase(ownership); + const application = applySuccessorPlanOnce({ + database: DATABASE, + sql: plan.sql, + stateQuery, + baselineState: stages[0], + terminalState: stages[11], + }); + const success = application.result; + const successLines = success.stdout.trim().split(/\r?\n/).filter(Boolean); + const successReadback = JSON.parse(successLines.at(-1)); + const committed = readState(DATABASE, stateQuery); + if (!equal(successReadback, stages[11]) || !equal(committed, stages[11])) { + fail('LOCAL_REHEARSAL_COMMIT_READBACK_INVALID'); + } + + resetOwnedDatabase(ownership); + const rollback = psql(DATABASE, `${plan.sql}\nDO $forced$ BEGIN RAISE EXCEPTION 'LOCAL_FORCED_ROLLBACK'; END $forced$;\n`, { + singleTransaction: true, + allowFailure: true, + }); + if (rollback.status === 0 || !/LOCAL_FORCED_ROLLBACK/.test(rollback.stderr)) { + fail('LOCAL_REHEARSAL_FORCED_ROLLBACK_MISSING'); + } + const rolledBack = readState(DATABASE, stateQuery); + if (!equal(rolledBack, stages[0])) fail('LOCAL_REHEARSAL_ROLLBACK_DRIFT'); + + resetOwnedDatabase(ownership); + for (let index = 0; index < 10; index += 1) applyRawMigration(record, index); + const cleanPreimage = readState(DATABASE, stateQuery); + if (!equal(cleanPreimage, stages[10])) fail('LOCAL_REHEARSAL_STAGE10_REPLAY_DRIFT'); + tamperFinishCleanupPreimage(); + const tamperedBefore = readState(DATABASE, stateQuery); + const wrongPreimage = psql(DATABASE, record.materials[10].bytes, { allowFailure: true }); + if (wrongPreimage.status === 0 || !/REVIEW_SHARED_SOURCE_DRIFT/.test(wrongPreimage.stderr)) { + fail('LOCAL_REHEARSAL_WRONG_PREIMAGE_NOT_REJECTED'); + } + const tamperedAfter = readState(DATABASE, stateQuery); + if (!equal(tamperedBefore, tamperedAfter) + || tamperedAfter.bundle.targetCount !== 10 + || tamperedAfter.bundle.ledgerCount !== 96) { + fail('LOCAL_REHEARSAL_WRONG_PREIMAGE_ROLLBACK_DRIFT'); + } + + const securityInvokerDriftRejected = assertCatalogDriftRejected({ + label: 'security-invoker', + ownership, + plan, + stages, + stateQuery, + tamperSql: `ALTER VIEW public.admin_evaluation_related_rows SET (security_invoker=false);`, + }); + const columnAclDriftRejected = assertCatalogDriftRejected({ + label: 'column-acl', + ownership, + plan, + stages, + stateQuery, + tamperSql: `GRANT UPDATE(approved_name) ON TABLE public.restaurants TO anon;`, + }); + const membershipInheritOptionDriftRejected = assertCatalogDriftRejected({ + label: 'membership-inherit-option', + ownership, + plan, + stages, + stateQuery, + tamperSql: `GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} WITH ADMIN FALSE, INHERIT FALSE, SET TRUE GRANTED BY supabase_admin;`, + restoreSql: `GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} WITH ADMIN FALSE, INHERIT TRUE, SET TRUE GRANTED BY supabase_admin;`, + }); + const membershipSetOptionDriftRejected = assertCatalogDriftRejected({ + label: 'membership-set-option', + ownership, + plan, + stages, + stateQuery, + tamperSql: `GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} WITH ADMIN FALSE, INHERIT TRUE, SET FALSE GRANTED BY supabase_admin;`, + restoreSql: `GRANT ${FIXTURE_PARENT_ROLE} TO ${FIXTURE_MEMBER_ROLE} WITH ADMIN FALSE, INHERIT TRUE, SET TRUE GRANTED BY supabase_admin;`, + }); + const contendedAdmissionExpiryRejected = await assertContendedExpiryRejected(record, stages, stateQuery, ownership); + + proof = { + schemaVersion: 1, + status: 'passed', + environment: { + dockerContext: DOCKER_CONTEXT, + dockerContainer: DOCKER_CONTAINER, + engine: template.engine, + templateDatabase: TEMPLATE_DATABASE, + temporaryDatabase: DATABASE, + }, + input: template, + ...currentBindings, + predecessorSources: PREDECESSOR_PATHS.map(path => ({ + path, + sha256: sha256(readFileSync(resolve(REPOSITORY_ROOT, path))), + })), + migrationCount: record.manifest.migrations.length, + statementCount: record.manifest.migrations.reduce((sum, migration) => sum + migration.statementCount, 0), + stageStatesSha256: sha256(JSON.stringify(canonical(stages))), + stages: stages.map(summarizeStage), + results: { + advisoryContentionObserved: true, + atomicCommit: true, + atomicCommitOutcome: application.outcome, + atomicRollbackExact: true, + catalogDriftStatePreserved: true, + columnAclDriftRejected, + compiledSqlBytes: Buffer.byteLength(plan.sql), + compiledSqlSha256: sha256(plan.sql), + contendedAdmissionExpiryRejected, + contendedAdmissionExpiryStatePreserved: true, + membershipOptionsBaseline: { inherit: true, set: true }, + membershipInheritOptionDriftRejected, + membershipSetOptionDriftRejected, + securityInvokerBaseline: true, + securityInvokerDriftRejected, + wrongFinishPreimageRejected: true, + wrongFinishPreimageRollbackExact: true, + terminalLedgerCount: committed.bundle.ledgerCount, + terminalContractReady: committed.state.terminalContractReady, + }, + sourceMutation: false, + hostedWrites: false, + operatingWrites: false, + }; + } finally { + cleanupOwnedFixtures(ownership); + } + proof.temporaryDatabaseDropped = true; + proof.fixtureRolesDropped = true; + const currentProofBytes = Buffer.from(`${JSON.stringify(canonical(proof))}\n`); + process.stdout.write(`${JSON.stringify({ + current: { + classification: 'current-source-verification', + proofSha256: sha256(currentProofBytes), + bindings: currentBindings, + proof, + }, + retained: retainedProof, + })}\n`); +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await run(); diff --git a/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs index 779cf90ec7..6f7c1560ba 100644 --- a/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +++ b/apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs @@ -550,6 +550,7 @@ function cleanupOwnedFixtures(ownership) { export function inspectRetainedProof({ outputPath = OUTPUT_PATH, checksumsPath = CHECKSUMS_PATH, + checksumsDigestPath = `${checksumsPath}.sha256`, currentBindings, existsImpl = existsSync, readFileImpl = readFileSync, @@ -558,7 +559,12 @@ export function inspectRetainedProof({ if (!currentBindings || typeof currentBindings !== 'object') { fail('LOCAL_REHEARSAL_CURRENT_BINDINGS_INVALID'); } - const checksumLines = readFileImpl(checksumsPath, 'utf8').split(/\r?\n/).filter(Boolean); + if (!existsImpl(checksumsDigestPath)) fail('LOCAL_REHEARSAL_CHECKSUMS_DIGEST_MISSING'); + const checksumBytes = readFileImpl(checksumsPath); + const checksumDigest = readFileImpl(checksumsDigestPath, 'utf8').trim(); + if (!/^[a-f0-9]{64}$/.test(checksumDigest)) fail('LOCAL_REHEARSAL_CHECKSUMS_DIGEST_INVALID'); + if (sha256(checksumBytes) !== checksumDigest) fail('LOCAL_REHEARSAL_CHECKSUMS_SHA256_MISMATCH'); + const checksumLines = checksumBytes.toString('utf8').split(/\r?\n/).filter(Boolean); const matchingLines = checksumLines.filter(line => line.endsWith(` ${RETAINED_PROOF_REPOSITORY_PATH}`)); if (matchingLines.length !== 1) fail('LOCAL_REHEARSAL_RETAINED_CHECKSUM_MISSING'); const checksumMatch = matchingLines[0].match(/^([0-9a-f]{64}) (.+)$/); @@ -599,6 +605,7 @@ export function inspectRetainedProof({ repositoryPath: RETAINED_PROOF_REPOSITORY_PATH, expectedSha256: checksumMatch[1], actualSha256, + checksumMapSha256: checksumDigest, bindings: Object.freeze(retainedBindings), currentSourceMatch: bindingDifferences.length === 0, bindingDifferences: Object.freeze(bindingDifferences), @@ -766,8 +773,15 @@ function startAdvisoryLockHolder() { else rejectCompletion(new Error(`LOCAL_REHEARSAL_LOCK_HOLDER_FAILED:${code}:${stderr.replace(/\s+/g, ' ').slice(0, 120)}`)); }); }); - child.stdin.end(`BEGIN;\nSELECT pg_advisory_xact_lock(7311754282260211::bigint);\nSELECT 'LOCK_HELD';\nSELECT pg_sleep(1.55);\nCOMMIT;\n`); - return { child, completion, ready }; + child.stdin.write(`BEGIN;\nSET LOCAL statement_timeout='10s';\nSET LOCAL idle_in_transaction_session_timeout='10s';\nSELECT pg_advisory_xact_lock(7311754282260211::bigint);\nSELECT 'LOCK_HELD';\n`); + let released = false; + const release = () => { + if (!released) { + released = true; + child.stdin.end('COMMIT;\n'); + } + }; + return { child, completion, ready, release }; } function startPsqlProcess(database, sql, { applicationName, singleTransaction = false } = {}) { @@ -802,16 +816,23 @@ function startPsqlProcess(database, sql, { applicationName, singleTransaction = return { child, completion }; } -async function waitForAdvisoryContention(expiresAt) { - const deadline = Math.min(expiresAt - 25, Date.now() + 700); - while (Date.now() < deadline) { - const waiting = scalar(DATABASE, `SELECT count(*) FROM pg_stat_activity a +export async function waitForAdvisoryContention(expiresAt, { + nowMs = Date.now, + readWaiting = () => scalar(DATABASE, `SELECT count(*) FROM pg_stat_activity a JOIN pg_locks l ON l.pid=a.pid WHERE a.application_name=${quote(EXPIRY_APPLICATION_NAME)} - AND l.locktype='advisory' AND NOT l.granted;`, 'supabase_admin'); - if (waiting === '1') return Date.now(); + AND l.locktype='advisory' AND NOT l.granted;`, 'supabase_admin'), + delayImpl = ms => new Promise(resolveDelay => setTimeout(resolveDelay, ms)), +} = {}) { + const deadline = Math.min(expiresAt - 25, nowMs() + 2000); + while (nowMs() < deadline) { + const waiting = readWaiting(); + if (waiting === '1') { + if (nowMs() >= expiresAt) fail('LOCAL_REHEARSAL_CONTENTION_OBSERVED_AFTER_EXPIRY'); + return nowMs(); + } if (waiting !== '0') fail('LOCAL_REHEARSAL_CONTENTION_CARDINALITY_INVALID'); - await new Promise(resolveDelay => setTimeout(resolveDelay, 20)); + await delayImpl(20); } fail('LOCAL_REHEARSAL_ADVISORY_CONTENTION_NOT_OBSERVED'); } @@ -820,13 +841,14 @@ async function assertContendedExpiryRejected(record, stages, stateQuery, ownersh resetOwnedDatabase(ownership); const baseline = readState(DATABASE, stateQuery); if (!equal(baseline, stages[0])) fail('LOCAL_REHEARSAL_EXPIRY_BASELINE_INVALID'); - const expiresAt = Date.now() + 1500; - const expiry = new Date(expiresAt).toISOString(); - const expiringPlan = compileSuccessorPlan(record, { expiresAt: expiry, stageStates: stages }); const holder = startAdvisoryLockHolder(); let apply = null; try { await holder.ready; + // Fixture setup and Docker startup must not consume the admitted lock-wait window. + const expiresAt = Date.now() + 1500; + const expiry = new Date(expiresAt).toISOString(); + const expiringPlan = compileSuccessorPlan(record, { expiresAt: expiry, stageStates: stages }); if (Date.now() >= expiresAt) fail('LOCAL_REHEARSAL_EXPIRY_WINDOW_TOO_SHORT'); apply = startPsqlProcess(DATABASE, expiringPlan.sql, { applicationName: EXPIRY_APPLICATION_NAME, @@ -834,6 +856,8 @@ async function assertContendedExpiryRejected(record, stages, stateQuery, ownersh }); const contentionObservedAt = await waitForAdvisoryContention(expiresAt); if (contentionObservedAt >= expiresAt) fail('LOCAL_REHEARSAL_CONTENTION_OBSERVED_AFTER_EXPIRY'); + await new Promise(resolveDelay => setTimeout(resolveDelay, Math.max(0, expiresAt - Date.now() + 25))); + holder.release(); const [rejected] = await Promise.all([apply.completion, holder.completion]); if (rejected.status === 0 || !/SUCCESSOR_ADMISSION_EXPIRED/.test(rejected.stderr) diff --git a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS index 9fdb091e51..cce48e79e1 100644 --- a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS +++ b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS @@ -1,6 +1,13 @@ 31e775a669d4a1295fc8b391653f26d0559695937f4cc357818ef53faa7c8e67 apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-0833ed81-historical +5a41205af7546221083956ddac62488f51254b253de1d738f39ab714d4835026 apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-4ba9c6d2-historical +0d7c993375a3015143cc03b84fd7b369e68713cd49926e87205c023a773def76 apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-4ba9c6d2-historical.sha256 +ad9406090dea1f2c1f8a146f5e241aae2651449f0c58af50201116e39ccfa2b6 apps/web/performance/review-media-operating-admission-20261013/controller-reverification-v2.json d29f5fa8ddc27573b3ea2d2a144c9d9a1c24a6c5ac4d7aeef30e3bda514fc575 apps/web/performance/review-media-operating-admission-20261013/controller-reverification.json +66634350db141610eca229f7fa4a45ca2b9733541a25c9bcbdff0a492d746225 apps/web/performance/review-media-operating-admission-20261013/current-evidence.json +af2b42d1ef05343b00bf58c699aff20bb6d80194de40b7f128d3fcf823f5f9a3 apps/web/performance/review-media-operating-admission-20261013/review-fix-parity-20261014-v1.log +af2b42d1ef05343b00bf58c699aff20bb6d80194de40b7f128d3fcf823f5f9a3 apps/web/performance/review-media-operating-admission-20261013/review-fix-parity-20261014-v2.log +6ccafd88858b8613ab199e22f14eac97cdadb2f9a69e73d2edd6afb1fd1fefd3 apps/web/performance/review-media-operating-admission-20261013/review-fix-units-20261014-v1.log ad4d439f9609db73d5879e655380d640b3b52274dbfe80654f6d715bd7a68ab7 apps/web/performance/review-media-operating-admission-20261013/verification.json -586847d5030cedc6575328cbe78b5eb9f1d9ff47d91e8d8f1aef40076b6d2277 apps/web/scripts/review-media-operating-admission.mjs -184368546c3668da99449f3d7ec0f08b95e1c06f44ca85100a6d2dfa533bc84a apps/web/tests-unit/review-media-operating-admission.test.ts -b4fbd520413f59e82743c545f521df5a8adde04970f610d4d06b432955f07518 docs/operations/review-media-operating-admission-20261013.md +b30f841c858edfc4865dd704ae7f5463d5570117bc3543fd43a63208b2b34ea9 apps/web/scripts/review-media-operating-admission.mjs +dfccda6dc30761a7ce0be2c3c15344cf66d9fbca9781907c4d8ac48a96024d9b apps/web/tests-unit/review-media-operating-admission.test.ts +36242344c78d292a5818ab5b28bec5ce9af9895b9e84cc900cc17a4c56f813b1 docs/operations/review-media-operating-admission-20261013.md diff --git a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-4ba9c6d2-historical b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-4ba9c6d2-historical new file mode 100644 index 0000000000..9fdb091e51 --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-4ba9c6d2-historical @@ -0,0 +1,6 @@ +31e775a669d4a1295fc8b391653f26d0559695937f4cc357818ef53faa7c8e67 apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-0833ed81-historical +d29f5fa8ddc27573b3ea2d2a144c9d9a1c24a6c5ac4d7aeef30e3bda514fc575 apps/web/performance/review-media-operating-admission-20261013/controller-reverification.json +ad4d439f9609db73d5879e655380d640b3b52274dbfe80654f6d715bd7a68ab7 apps/web/performance/review-media-operating-admission-20261013/verification.json +586847d5030cedc6575328cbe78b5eb9f1d9ff47d91e8d8f1aef40076b6d2277 apps/web/scripts/review-media-operating-admission.mjs +184368546c3668da99449f3d7ec0f08b95e1c06f44ca85100a6d2dfa533bc84a apps/web/tests-unit/review-media-operating-admission.test.ts +b4fbd520413f59e82743c545f521df5a8adde04970f610d4d06b432955f07518 docs/operations/review-media-operating-admission-20261013.md diff --git a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-4ba9c6d2-historical.sha256 b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-4ba9c6d2-historical.sha256 new file mode 100644 index 0000000000..c7d4a5cec2 --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS-4ba9c6d2-historical.sha256 @@ -0,0 +1 @@ +5a41205af7546221083956ddac62488f51254b253de1d738f39ab714d4835026 diff --git a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 index c7d4a5cec2..3cb06f2b4c 100644 --- a/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 +++ b/apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS.sha256 @@ -1 +1 @@ -5a41205af7546221083956ddac62488f51254b253de1d738f39ab714d4835026 +f7345de87e22378811fb0bfef0f57461702a4178ec92cd13e82812dcb921138e diff --git a/apps/web/performance/review-media-operating-admission-20261013/controller-reverification-v2.json b/apps/web/performance/review-media-operating-admission-20261013/controller-reverification-v2.json new file mode 100644 index 0000000000..ae1b1028f6 --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/controller-reverification-v2.json @@ -0,0 +1,53 @@ +{ + "schemaVersion": 1, + "kind": "local-unit-reverification", + "recordedAtUTC": "2026-10-13T20:18:10.822481+00:00", + "sourceBaseRevision": "4ba9c6d2f54b6e0c8458b2518977665f606eb4ff", + "checks": { + "passed": 98, + "failed": 0, + "assertions": 808, + "command": "bun test tests-unit/review-media-operating-admission.test.ts tests-unit/review-media-sql-successor.test.ts tests-unit/review-media-sql-successor-harness.test.ts tests-unit/supabase-migration-bundle.test.ts", + "cwd": "apps/web", + "log": "apps/web/performance/review-media-operating-admission-20261013/review-fix-units-20261014-v1.log", + "logSha256": "6ccafd88858b8613ab199e22f14eac97cdadb2f9a69e73d2edd6afb1fd1fefd3" + }, + "sources": [ + { + "path": "apps/web/scripts/review-media-operating-admission.mjs", + "sha256": "b30f841c858edfc4865dd704ae7f5463d5570117bc3543fd43a63208b2b34ea9" + }, + { + "path": "apps/web/tests-unit/review-media-operating-admission.test.ts", + "sha256": "dfccda6dc30761a7ce0be2c3c15344cf66d9fbca9781907c4d8ac48a96024d9b" + }, + { + "path": "apps/web/scripts/review-media-sql-successor.mjs", + "sha256": "4f1b2798e0cfc13db4f11cd6eb4a8653320aa8516eccd71d410fcff6d06a7f24" + }, + { + "path": ".github/review-media-sql-successor.v1.json", + "sha256": "295b2354c562ed87102c5e256497a2705f90941587fefa7e9ad8a4ba8e40787f" + } + ], + "supplementalHarnessVerification": { + "passed": 20, + "failed": 0, + "assertions": 98, + "log": "apps/web/performance/review-db-successor-20261012/review-fix-harness-units-20261014-v2.log", + "logSha256": "7815d90d73d6b3508a355aee06743c22ff802e78ffca46172cda18ca83ed51b4", + "sources": [ + { + "path": "apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs", + "sha256": "7e750e9d0126f6f10afe9056d747a2ea854d462161ea4e8f0168e2751d554c0a" + }, + { + "path": "apps/web/tests-unit/review-media-sql-successor-harness.test.ts", + "sha256": "99c6bc0221188f93317640d4d65affdfeb91fea233bb0f0cc0f409407e419d84" + } + ] + }, + "reason": "Bound stored rehearsal admission, finite preflight and transport timeouts; verify detached checksum pin. The 98-test run covers the four primary source hashes. The later 20-test run replaces its 18 harness tests after the contention fixture change; these runs are not 118 independent tests.", + "operatingEvidence": false, + "performanceEvidence": false +} diff --git a/apps/web/performance/review-media-operating-admission-20261013/current-evidence.json b/apps/web/performance/review-media-operating-admission-20261013/current-evidence.json new file mode 100644 index 0000000000..f81b23fdd1 --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/current-evidence.json @@ -0,0 +1,12 @@ +{ + "schemaVersion": 1, + "current": "controller-reverification-v2.json", + "sha256": "ad9406090dea1f2c1f8a146f5e241aae2651449f0c58af50201116e39ccfa2b6", + "historical": [ + "verification.json", + "controller-reverification.json" + ], + "historicalSourceMatch": false, + "operatingEvidence": false, + "performanceEvidence": false +} diff --git a/apps/web/performance/review-media-operating-admission-20261013/review-fix-parity-20261014-v1.log b/apps/web/performance/review-media-operating-admission-20261013/review-fix-parity-20261014-v1.log new file mode 100644 index 0000000000..1bfe59481e --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/review-fix-parity-20261014-v1.log @@ -0,0 +1,5 @@ + +> web@1.2.4 typecheck:parity +> node scripts/run-typecheck.mjs --compiler parity + +{"status":"passed","diagnostics":0,"logicalInputs":3148,"logicalInputNamesSha256":"4f11b9a9b98661e4b0b52aade8d12be721b72ad229a7bb851ed39c0d94520158","nonLibraryContentSha256":"047401fde3b7a83508e1f431b0321cdf14d0a8d722ec5f126d460b8876799ccd","standardLibraryContentSha256":{"native":"d328ef5dcbac7d4c183cd37a26eb383e8e6921ab2147b20e5d73f3c149223a64","compat":"82afdd90154e9b315a83eb66f2d4ee59972c492094bbf66d7f0791f584e41eb0"}} diff --git a/apps/web/performance/review-media-operating-admission-20261013/review-fix-parity-20261014-v2.log b/apps/web/performance/review-media-operating-admission-20261013/review-fix-parity-20261014-v2.log new file mode 100644 index 0000000000..1bfe59481e --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/review-fix-parity-20261014-v2.log @@ -0,0 +1,5 @@ + +> web@1.2.4 typecheck:parity +> node scripts/run-typecheck.mjs --compiler parity + +{"status":"passed","diagnostics":0,"logicalInputs":3148,"logicalInputNamesSha256":"4f11b9a9b98661e4b0b52aade8d12be721b72ad229a7bb851ed39c0d94520158","nonLibraryContentSha256":"047401fde3b7a83508e1f431b0321cdf14d0a8d722ec5f126d460b8876799ccd","standardLibraryContentSha256":{"native":"d328ef5dcbac7d4c183cd37a26eb383e8e6921ab2147b20e5d73f3c149223a64","compat":"82afdd90154e9b315a83eb66f2d4ee59972c492094bbf66d7f0791f584e41eb0"}} diff --git a/apps/web/performance/review-media-operating-admission-20261013/review-fix-units-20261014-v1.log b/apps/web/performance/review-media-operating-admission-20261013/review-fix-units-20261014-v1.log new file mode 100644 index 0000000000..a1ff521480 --- /dev/null +++ b/apps/web/performance/review-media-operating-admission-20261013/review-fix-units-20261014-v1.log @@ -0,0 +1,112 @@ +bun test v1.4.0 (34cbb9a40) + +tests-unit/review-media-operating-admission.test.ts: +(pass) CLI contract accepts only the explicit rehearsal or apply producer modes [0.27ms] +(pass) rehearsal producer preserves proven receipts and creates a controller-valid private admission [10.67ms] +(pass) unknown, missing, stale, and mismatched evidence is rejected before custody creation [4.01ms] +(pass) rehearsal producer never overwrites an existing custody directory [6.03ms] +(pass) rehearsal producer fails closed on parent, file, and custody fsync failures [5.23ms] +(pass) rehearsal producer rejects symlink and oversized inputs before custody creation [3.71ms] +(pass) apply producer binds the verified rehearsal artifacts without extending their time window [55.83ms] +(pass) apply producer rejects a relabeled or tampered rehearsal before writing an admission [16.40ms] +(pass) apply producer rejects a fully rehashed receipt and journal relabeled to another rehearsal attempt [8.22ms] +(pass) apply producer treats a dangling reserved journal symlink as an existing output [8.53ms] +(pass) apply producer reads rehearsal journals through the no-follow bounded reader [22.06ms] + +tests-unit/supabase-migration-bundle.test.ts: +(pass) actual four source files compile in exact identity/hash/vector order with one output SELECT [158.11ms] +(pass) order, source identity, source hash, vector and suffix drift reject before an executor exists [665.11ms] +(pass) five-stage compilation is explicit while the historical four-stage API stays exact [141.48ms] +(pass) fresh prior N and prefix/suffix roots are mandatory and partial target ledger is rejected by the first transaction guard [239.72ms] +(pass) reconciliation classifies only exact prior/final objects; all intermediate stages conflict [130.57ms] +(pass) ambiguous transport performs one read-only reconciliation without claiming or resending a commit [131.87ms] +(pass) server terminal guard rollback cannot become success after a matching external commit [126.23ms] +(pass) exit-zero malformed or mismatched readback is confirmed once by exact final state [125.48ms] +(pass) not-applied preserves failure while partial and unknown are fixed fail-closed outcomes [140.83ms] +(pass) transport and malformed-output diagnostics never escape through errors or causes [133.84ms] +(pass) successful output must be exactly one JSON object matching the final bundle readback [137.83ms] + +tests-unit/review-media-sql-successor-harness.test.ts: +(pass) successor acknowledgement loss reconciles a committed terminal state without SQL replay [0.23ms] +(pass) successor failure at the baseline is classified without diagnostic SQL replay [0.20ms] +(pass) retained proof is checksum-verified and distinguished from the current harness source [1.04ms] +(pass) retained proof with bytes not authorized by SHA256SUMS is rejected [10.52ms] +(pass) jointly altered retained proof and checksum map are rejected by the independent digest [1.59ms] +(pass) owned database is cleaned after predecessor SQL fails [1.18ms] +(pass) owned database is cleaned after fixture SQL fails [0.08ms] +(pass) pre-existing database is never acquired or dropped [0.07ms] +(pass) database CREATE acknowledgement loss is reconciled without replay and cleaned by exact ownership [0.28ms] +(pass) cleanup re-reads and removes the exact owned database after the first CREATE readback fails [0.23ms] +(pass) cleanup refuses a database whose exact owner metadata does not match [0.18ms] +(pass) fixture role creation acknowledgement loss is reconciled and exact owned roles are cleaned [0.86ms] +(pass) cleanup re-reads exact owned roles when the first post-create readback fails [0.34ms] +(pass) fixture role creation failure without owned readback never authorizes cleanup [0.17ms] +(pass) pre-existing fixture role collision is preserved and never acquired for cleanup [0.05ms] +(pass) uncertain create collision with another owner is not guessed or removed [0.13ms] +(pass) cleanup refuses roles whose ownership marker changed after creation [0.12ms] +(pass) cleanup refuses to remove owned roles after an out-of-scope membership is added [0.07ms] + +tests-unit/review-media-sql-successor.test.ts: +(pass) manifest binds the exact eleven sources and remains held without fresh admission [275.17ms] +(pass) rehearsal CLI requires one admission, journal and receipt path [0.16ms] +(pass) compiled plan has one advisory lock, one outer transaction and eleven ledger writes [1502.58ms] +(pass) apply accepts rehearsal evidence completed after thirty seconds within its original lease [11.98ms] +(pass) apply rejects rehearsal evidence outside the original fifteen-minute lease [10.41ms] +(pass) actual psql runner with a spy transport preserves the post-lock expiry code [0.63ms] +(pass) rollback-only rehearsal plan owns one transaction and captures twelve safe stages [10.28ms] +(pass) rehearsal admission is distinct and rejects stale source or any wrong writer fence [39.32ms] +(pass) rehearsal rolls back, separately confirms restoration and emits an apply-bound receipt [21.46ms] +(pass) lost rehearsal response performs restoration readback once and never resends [13.33ms] +(pass) post-lock rehearsal expiry keeps its exact bounded code after restoration readback [21.21ms] +(pass) incomplete rehearsal output cannot create an apply admission receipt [14.22ms] +(pass) verification line write-success then fsync-failure never publishes the canonical receipt [23.71ms] +(pass) pending receipt full-write then fsync-failure records failure without canonical publication [24.98ms] +(pass) pending receipt storage delay cannot verify after the admission expires [15.38ms] +(pass) expiry terminal audit failure preserves the bounded rehearsal outcome [21.93ms] +(pass) verification journal storage delay cannot publish after the admission expires [16.64ms] +(pass) canonical publication storage delay cannot record an expired passed result [23.34ms] +(pass) publication journal completion cannot return passed after the admission expires [22.64ms] +(pass) prepublication interruption leaves verified journal and pending receipt apply-invalid [21.36ms] +(pass) failed canonical publication is audited as failure, never as passed [15.72ms] +(pass) pre-existing exact canonical bytes do not make an unperformed link durable [24.08ms] +(pass) unconfirmed directory durability cannot be reconciled from visible canonical bytes [36.73ms] +(pass) directory fsync failure plus failure-audit loss leaves visible receipt apply-invalid [19.83ms] +(pass) publication audit fsync failure requires proven durability before reconciliation [17.45ms] +(pass) publication audit close failure requires proven durability before reconciliation [14.76ms] +(pass) publication audit failure before any bytes leaves durable receipt apply-invalid [31.55ms] +(pass) atomic publication acknowledgement loss reconciles exact receipt without retry [18.86ms] +(pass) apply rejects orphan and semantically tampered rehearsal journals even when rehashed [36.19ms] +(pass) apply pins the stored admission against a valid fully rehashed rehearsal from another attempt [27.42ms] +(pass) apply rejects a rehashed rehearsal with mismatched attempt binding [21.98ms] +(pass) apply rejects a rehashed rehearsal with mismatched rollback binding [35.08ms] +(pass) apply rejects a rehashed rehearsal with mismatched writerFence binding [12.37ms] +(pass) apply requires the pinned private canonical rehearsal admission before any external read [85.43ms] +(pass) admission rejects wrong preimage and every wrong writer fence [50.10ms] +(pass) success path performs preflight, rechecks rollback and commits all eleven once [2270.70ms] +(pass) bounded preflight clientStall with 600000ms left cannot journal or write [2157.13ms] +(pass) bounded preflight statementTimeout with 600000ms left cannot journal or write [2206.43ms] +(pass) bounded preflight lockTimeout with 600000ms left cannot journal or write [2367.47ms] +(pass) bounded preflight expiry with 600000ms left cannot journal or write [2590.32ms] +(pass) bounded preflight clientStall with 5000ms left cannot journal or write [2403.03ms] +(pass) bounded preflight statementTimeout with 5000ms left cannot journal or write [2490.51ms] +(pass) bounded preflight lockTimeout with 5000ms left cannot journal or write [2588.69ms] +(pass) bounded preflight expiry with 5000ms left cannot journal or write [2337.19ms] +(pass) psql runner keeps a finite mutation/reconciliation cap and rejects unbounded overrides [0.27ms] +(pass) committed terminal audit fsync failure preserves the committed outcome [2451.85ms] +(pass) committed terminal audit close failure preserves the committed outcome [2423.49ms] +(pass) committed terminal audit readback requires both durability and exact bytes [2375.40ms] +(pass) concurrent rollback deployment change rejects before journal or mutation [2325.17ms] +(pass) prior readback mismatch fails before journal and mutating transport [2315.87ms] +(pass) lost apply acknowledgement reconciles once and never resends eleven mutations [2978.11ms] +(pass) apply fsync failure at custody/start boundary 1 prevents database mutation [2518.75ms] +(pass) apply fsync failure at custody/start boundary 2 prevents database mutation [2217.47ms] +(pass) apply fsync failure at custody/start boundary 3 prevents database mutation [2145.64ms] +(pass) apply fsync failure at custody/start boundary 4 prevents database mutation [2141.04ms] +(pass) apply fsync failure at custody/start boundary 5 prevents database mutation [2253.09ms] +(pass) apply fsync failure at custody/start boundary 6 prevents database mutation [2234.35ms] +(pass) apply preserves expiry and audit failure as separate bounded outcomes [2260.59ms] + + 98 pass + 0 fail + 808 expect() calls +Ran 98 tests across 4 files. [57.48s] diff --git a/apps/web/scripts/review-media-operating-admission.mjs b/apps/web/scripts/review-media-operating-admission.mjs index 037b9e52a0..40da2080ce 100644 --- a/apps/web/scripts/review-media-operating-admission.mjs +++ b/apps/web/scripts/review-media-operating-admission.mjs @@ -640,6 +640,7 @@ export function prepareReviewMediaApplyAdmission({ projectRef: rehearsal.projectRef, protectedMainReceiptSha256: rehearsal.protectedMainReceiptSha256, purpose: rehearsal.purpose, + rehearsalAdmissionSha256: sha256(rehearsalAdmissionRecord.bytes), rehearsalJournalSha256: inputRecord.value.rehearsalJournalSha256, rehearsalReceiptSha256: inputRecord.value.rehearsalReceiptSha256, rollback: structuredClone(rehearsal.rollback), diff --git a/apps/web/scripts/review-media-sql-successor.mjs b/apps/web/scripts/review-media-sql-successor.mjs index 4e86b241e2..1d3d78f632 100644 --- a/apps/web/scripts/review-media-sql-successor.mjs +++ b/apps/web/scripts/review-media-sql-successor.mjs @@ -53,10 +53,13 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a- const PROJECT_REF = 'aqlcofblfxdrjhhdmarw'; const PURPOSE = 'review-media-current-state-eleven-stage'; const ADMISSION_MAX_AGE_MS = 15 * 60 * 1000; +const PREFLIGHT_TIMEOUT_MS = 30_000; +const PSQL_MAX_TIMEOUT_MS = ADMISSION_MAX_AGE_MS + 30_000; const GIT_REMOTE_READ_TIMEOUT_MS = 15_000; const PRIVATE_RECEIPT_FILES = Object.freeze({ protectedMain: 'protected-main-receipt.json', rehearsal: 'rehearsal-receipt.json', + rehearsalAdmission: 'rehearsal-admission.json', rehearsalJournal: 'rehearsal-journal.jsonl', rollback: 'rollback-readback-receipt.json', source: 'source-receipt.json', @@ -702,6 +705,27 @@ function validatePrivateReceipts(admission, manifestRecord, { assertReceiptWindow(protectedMain, admission, createdAt, expiresAt, nowMs); if (includeRehearsal) { + const rehearsalAdmission = readPrivateCanonicalReceipt( + receiptDirectory, + PRIVATE_RECEIPT_FILES.rehearsalAdmission, + admission.rehearsalAdmissionSha256, + ); + const rehearsalWindow = validateSuccessorRehearsalAdmission(rehearsalAdmission, manifestRecord, { + now: new Date(nowMs), + receiptDirectory, + }); + if (rehearsalWindow.createdAt > createdAt + || rehearsalWindow.expiresAt < expiresAt + || rehearsalAdmission.sourceReceiptSha256 !== admission.sourceReceiptSha256 + || rehearsalAdmission.protectedMainReceiptSha256 !== admission.protectedMainReceiptSha256 + || !equal(rehearsalAdmission.rollback, admission.rollback) + || !equal(rehearsalAdmission.writerFences, admission.writerFences) + || rehearsalAdmission.journalPathSha256 !== sha256(Buffer.from(resolve( + receiptDirectory, PRIVATE_RECEIPT_FILES.rehearsalJournal, + ))) + || rehearsalAdmission.receiptPathSha256 !== sha256(Buffer.from(resolve( + receiptDirectory, PRIVATE_RECEIPT_FILES.rehearsal, + )))) fail('SUCCESSOR_ADMISSION_INVALID'); const rehearsal = readPrivateCanonicalReceipt( receiptDirectory, PRIVATE_RECEIPT_FILES.rehearsal, @@ -719,8 +743,9 @@ function validatePrivateReceipts(admission, manifestRecord, { if (!common(rehearsal) || rehearsal.kind !== 'successor-rehearsal-receipt' || rehearsal.passed !== true - || !UUID.test(rehearsal.attemptId) - || !HEX_SHA256.test(rehearsal.admissionSha256) + || rehearsal.attemptId !== rehearsalAdmission.attemptId + || rehearsal.admissionSha256 !== admission.rehearsalAdmissionSha256 + || rehearsal.expiresAt !== rehearsalAdmission.expiresAt || rehearsal.journalPathSha256 !== sha256(Buffer.from(resolve( receiptDirectory, PRIVATE_RECEIPT_FILES.rehearsalJournal, @@ -759,7 +784,7 @@ function validatePrivateReceipts(admission, manifestRecord, { const verifiedAt = Date.parse(verification.verifiedAt); const publishedAt = Date.parse(publication.publishedAt); const observedAt = Date.parse(rehearsal.observedAt); - const rehearsalLeaseStartedAt = Date.parse(rehearsal.expiresAt) - ADMISSION_MAX_AGE_MS; + const rehearsalLeaseStartedAt = rehearsalWindow.createdAt; if (started.event !== 'started' || verification.event !== 'verification' || verification.code !== 'SUCCESSOR_REHEARSAL_VERIFIED' @@ -929,6 +954,7 @@ export function validateSuccessorAdmission(admission, manifestRecord, { 'projectRef', 'protectedMainReceiptSha256', 'purpose', + 'rehearsalAdmissionSha256', 'rehearsalJournalSha256', 'rehearsalReceiptSha256', 'rollback', @@ -945,6 +971,7 @@ export function validateSuccessorAdmission(admission, manifestRecord, { receiptDirectory, requiredReceiptHashes: [ 'protectedMainReceiptSha256', + 'rehearsalAdmissionSha256', 'rehearsalJournalSha256', 'rehearsalReceiptSha256', 'sourceReceiptSha256', @@ -1052,10 +1079,13 @@ export function assertLaunchReady(manifest) { if (manifest.launchPolicy.state !== 'ready') fail('SUCCESSOR_LAUNCH_HELD'); } -function successorExpirySql(admission, suffix, { armTransactionTimeout = false } = {}) { +function successorExpirySql(admission, suffix, { + armTransactionTimeout = false, + maxTransactionTimeoutMs = ADMISSION_MAX_AGE_MS, +} = {}) { const tag = `$successor_expiry_${sha256(`${admission.expiresAt}:${suffix}`).slice(0, 18)}$`; const armTimeout = armTransactionTimeout - ? `\n PERFORM set_config('transaction_timeout',least(${ADMISSION_MAX_AGE_MS},greatest(1,ceil(extract(epoch FROM (${quote(admission.expiresAt)}::timestamptz-transaction_timestamp()))*1000)::bigint))::text,true);` + ? `\n PERFORM set_config('transaction_timeout',least(${maxTransactionTimeoutMs},greatest(1,ceil(extract(epoch FROM (${quote(admission.expiresAt)}::timestamptz-transaction_timestamp()))*1000)::bigint))::text,true);` : ''; return `DO ${tag} BEGIN IF clock_timestamp() >= ${quote(admission.expiresAt)}::timestamptz THEN RAISE EXCEPTION 'SUCCESSOR_ADMISSION_EXPIRED'; END IF;${armTimeout} END ${tag};`; } @@ -1280,6 +1310,7 @@ function assertDurableRehearsalArtifacts(directory, admission, fsyncImpl) { fail('SUCCESSOR_ADMISSION_INVALID'); } for (const [fileName, expected] of [ + [PRIVATE_RECEIPT_FILES.rehearsalAdmission, admission.rehearsalAdmissionSha256], [PRIVATE_RECEIPT_FILES.rehearsal, admission.rehearsalReceiptSha256], [PRIVATE_RECEIPT_FILES.rehearsalJournal, admission.rehearsalJournalSha256], ]) { @@ -1418,7 +1449,10 @@ export function createSuccessorPsqlRunner(databaseUrl, { psql = environment.TZUDONG_SUCCESSOR_PSQL || 'psql', spawnImpl = spawnSync, } = {}) { - return (_databaseUrl, sql, singleTransaction) => { + return (_databaseUrl, sql, singleTransaction, { timeoutMs = PSQL_MAX_TIMEOUT_MS } = {}) => { + if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > PSQL_MAX_TIMEOUT_MS) { + fail('SUCCESSOR_ARGUMENT_INVALID'); + } const args = ['--no-psqlrc', '--set=ON_ERROR_STOP=1', '--quiet', '--tuples-only', '--no-align']; if (singleTransaction) args.splice(1, 0, '--single-transaction'); args.push('--file=-'); @@ -1427,6 +1461,9 @@ export function createSuccessorPsqlRunner(databaseUrl, { env: psqlConnectionEnvironment(databaseUrl, environment), input: `\\set VERBOSITY verbose\n${sql}`, maxBuffer: 16 * 1024 * 1024, + timeout: timeoutMs, + // A stalled client must not keep spawnSync waiting after SIGTERM. + killSignal: 'SIGKILL', }); if (result.error || result.status !== 0) throw transportError(result); return result.stdout || ''; @@ -2006,10 +2043,27 @@ export function runReviewMediaSuccessor({ let observed; try { + const preflightAt = now(); + assertAdmissionFresh(admissionWindow.expiresAt, preflightAt); + const timeoutMs = Math.min(PREFLIGHT_TIMEOUT_MS, admissionWindow.expiresAt - preflightAt.getTime()); + const preflightSql = [ + 'SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;', + 'SET LOCAL standard_conforming_strings=on;', + "SET LOCAL lock_timeout='2s';", + "SET LOCAL statement_timeout='15s';", + "SET LOCAL idle_in_transaction_session_timeout='30s';", + successorExpirySql(admission, 'preflight', { + armTransactionTimeout: true, + maxTransactionTimeoutMs: PREFLIGHT_TIMEOUT_MS, + }), + `${plan.stateQuery};`, + successorExpirySql(admission, 'post-preflight'), + ].join('\n'); observed = parseReadback(transport( databaseUrl, - `SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;\n${plan.stateQuery};`, + preflightSql, true, + { timeoutMs }, )); } catch (error) { throw boundedSuccessorError(error); diff --git a/apps/web/tests-unit/review-media-operating-admission.test.ts b/apps/web/tests-unit/review-media-operating-admission.test.ts index b20c191380..413ef82cbc 100644 --- a/apps/web/tests-unit/review-media-operating-admission.test.ts +++ b/apps/web/tests-unit/review-media-operating-admission.test.ts @@ -445,6 +445,10 @@ test('apply producer binds the verified rehearsal artifacts without extending th expect(published.receipt.attemptId).toBe(published.admission.attemptId); expect(admission.attemptId).toBe(applyInputDocument.attemptId); expect(admission.attemptId).not.toBe(published.admission.attemptId); + expect(admission.rehearsalAdmissionSha256).toBe(sha256(readFileSync( + join(prepared.custodyDirectory, 'rehearsal-admission.json'), + ))); + expect(admission.rehearsalAdmissionSha256).toBe(published.receipt.admissionSha256); expect(admission.createdAt).toBe(createdAt); expect(admission.expiresAt).toBe(expiresAt); expect(admission.stageStates).toEqual(published.receipt.stageStates); diff --git a/apps/web/tests-unit/review-media-sql-successor-harness.test.ts b/apps/web/tests-unit/review-media-sql-successor-harness.test.ts index 12c5d61b85..c8c87aa837 100644 --- a/apps/web/tests-unit/review-media-sql-successor-harness.test.ts +++ b/apps/web/tests-unit/review-media-sql-successor-harness.test.ts @@ -11,10 +11,31 @@ import { dropFixtureMembership, dropOwnedDatabase, inspectRetainedProof, + waitForAdvisoryContention, } from '../performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs'; const retainedProofRepositoryPath = 'apps/web/performance/review-db-successor-20261012/pg17-local-replay.json'; const sha256 = (value: string | Buffer) => createHash('sha256').update(value).digest('hex'); + +test('fixture observes delayed advisory contention before expiry without the old 700ms cutoff', async () => { + let time = 0; + const observed = await waitForAdvisoryContention(1500, { + nowMs: () => time, + readWaiting: () => time >= 950 ? '1' : '0', + delayImpl: async ms => { time += ms; }, + }); + expect(observed).toBe(960); + expect(observed).toBeLessThan(1500); +}); + +test('fixture rejects contention returned by a slow observation after expiry', async () => { + let time = 0; + await expect(waitForAdvisoryContention(1500, { + nowMs: () => time, + readWaiting: () => { time = 1600; return '1'; }, + delayImpl: async () => {}, + })).rejects.toThrow('LOCAL_REHEARSAL_CONTENTION_OBSERVED_AFTER_EXPIRY'); +}); const fixtureParentRole = 'review_successor_fixture_parent_20261012'; const fixtureMemberRole = 'review_successor_fixture_member_20261012'; const fixtureOwnerPrefix = 'tzudong-review-successor-fixture'; @@ -235,7 +256,9 @@ test('retained proof with bytes not authorized by SHA256SUMS is rejected', () => sourceRoot: '3'.repeat(64), })}\n`); writeFileSync(outputPath, proof); - writeFileSync(checksumsPath, `${sha256(proof)} ${retainedProofRepositoryPath}\n`); + const checksumBytes = `${sha256(proof)} ${retainedProofRepositoryPath}\n`; + writeFileSync(checksumsPath, checksumBytes); + writeFileSync(`${checksumsPath}.sha256`, `${sha256(checksumBytes)}\n`); writeFileSync(outputPath, Buffer.concat([proof, Buffer.from(' ')])); expect(() => inspectRetainedProof({ @@ -252,6 +275,28 @@ test('retained proof with bytes not authorized by SHA256SUMS is rejected', () => } }); +test('jointly altered retained proof and checksum map are rejected by the independent digest', () => { + const directory = mkdtempSync(join(tmpdir(), 'tzudong-retained-map-tamper-')); + try { + const outputPath = join(directory, 'pg17-local-replay.json'); + const checksumsPath = join(directory, 'SHA256SUMS'); + const proof = { status: 'passed', temporaryDatabaseDropped: true, hostedWrites: false, + operatingWrites: false, manifestSha256: '1'.repeat(64), + rehearsalHarnessSha256: '2'.repeat(64), sourceRoot: '3'.repeat(64) }; + const original = Buffer.from(`${JSON.stringify(proof)}\n`); + const originalMap = `${sha256(original)} ${retainedProofRepositoryPath}\n`; + writeFileSync(`${checksumsPath}.sha256`, `${sha256(originalMap)}\n`); + const tampered = Buffer.from(`${JSON.stringify({ ...proof, sourceRoot: '4'.repeat(64) })}\n`); + writeFileSync(outputPath, tampered); + writeFileSync(checksumsPath, `${sha256(tampered)} ${retainedProofRepositoryPath}\n`); + expect(() => inspectRetainedProof({ outputPath, checksumsPath, currentBindings: { + manifestSha256: '1'.repeat(64), rehearsalHarnessSha256: '2'.repeat(64), sourceRoot: '4'.repeat(64), + } })).toThrow('LOCAL_REHEARSAL_CHECKSUMS_SHA256_MISMATCH'); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + for (const [label, failingDatabaseCall, expectedCode] of [ ['predecessor', 1, 'LOCAL_REHEARSAL_PREDECESSOR_FAILED'], ['fixture', 7, 'fixture failed'], diff --git a/apps/web/tests-unit/review-media-sql-successor.test.ts b/apps/web/tests-unit/review-media-sql-successor.test.ts index a6491bc25b..16a1647038 100644 --- a/apps/web/tests-unit/review-media-sql-successor.test.ts +++ b/apps/web/tests-unit/review-media-sql-successor.test.ts @@ -141,6 +141,7 @@ function admission() { projectRef: manifest.projectRef, protectedMainReceiptSha256: '1'.repeat(64), purpose: manifest.purpose, + rehearsalAdmissionSha256: '6'.repeat(64), rehearsalJournalSha256: '5'.repeat(64), rehearsalReceiptSha256: '2'.repeat(64), rollback: { ...structuredClone(rollback), readbackSha256: '3'.repeat(64) }, @@ -172,7 +173,7 @@ function custodyFiles(document = admission()) { expiresAt: document.expiresAt, }; const rehearsalAttemptId = '123e4567-e89b-42d3-a456-426614174001'; - const rehearsalAdmissionSha256 = '5'.repeat(64); + let rehearsalAdmissionSha256 = ''; const receipts: Record = { 'source-receipt.json': { ...common, kind: 'successor-source-receipt', sourceRoot: document.sourceRoot }, 'protected-main-receipt.json': { @@ -261,14 +262,40 @@ function custodyFiles(document = admission()) { receipts[name] = { ...common, fence: key, kind: 'writer-fence-readback-receipt', state }; document.writerFences[key].evidenceSha256 = sha256(canonicalBytes(receipts[name])); } + const storedRehearsalAdmission = { + ...rehearsalAdmission(), + attemptId: rehearsalAttemptId, + createdAt: new Date(Math.max( + Date.parse(document.createdAt) - 120_000, Date.parse(document.expiresAt) - 15 * 60_000, + )).toISOString(), + expiresAt: document.expiresAt, + journalPathSha256: sha256(rehearsalJournalPath), + receiptPathSha256: sha256(join(directory, 'rehearsal-receipt.json')), + protectedMainReceiptSha256: document.protectedMainReceiptSha256, + rollback: structuredClone(document.rollback), + sourceReceiptSha256: document.sourceReceiptSha256, + writerFences: structuredClone(document.writerFences), + }; + const storedRehearsalBytes = canonicalBytes(storedRehearsalAdmission); + rehearsalAdmissionSha256 = sha256(storedRehearsalBytes); + document.rehearsalAdmissionSha256 = rehearsalAdmissionSha256; + receipts['rehearsal-receipt.json'].admissionSha256 = rehearsalAdmissionSha256; + document.rehearsalReceiptSha256 = sha256(canonicalBytes(receipts['rehearsal-receipt.json'])); + for (const entry of rehearsalJournal) { + entry.admissionSha256 = rehearsalAdmissionSha256; + if ('receiptSha256' in entry) entry.receiptSha256 = document.rehearsalReceiptSha256; + } + const boundJournalBytes = Buffer.from(`${rehearsalJournal.map(entry => JSON.stringify(canonical(entry))).join('\n')}\n`); + document.rehearsalJournalSha256 = sha256(boundJournalBytes); + writeFileSync(join(directory, 'rehearsal-admission.json'), storedRehearsalBytes, { mode: 0o600 }); for (const [name, receipt] of Object.entries(receipts)) { const path = join(directory, name); writeFileSync(path, canonicalBytes(receipt), { mode: 0o600 }); chmodSync(path, 0o600); } - writeFileSync(rehearsalJournalPath, rehearsalJournalBytes, { mode: 0o600 }); + writeFileSync(rehearsalJournalPath, boundJournalBytes, { mode: 0o600 }); chmodSync(rehearsalJournalPath, 0o600); - writeFileSync(admissionPath, `${JSON.stringify(document)}\n`, { mode: 0o600 }); + writeFileSync(admissionPath, canonicalBytes(document), { mode: 0o600 }); chmodSync(admissionPath, 0o600); return { admissionPath, directory, document, journalPath, rehearsalJournalPath }; } @@ -305,6 +332,28 @@ function rewriteRehearsalTimeline(files: ReturnType, { files.document.rehearsalJournalSha256 = sha256(journalBytes); } +function rebindRehearsalEvidence(files: ReturnType, stored: any) { + const admissionBytes = canonicalBytes(stored); + writeFileSync(join(files.directory, 'rehearsal-admission.json'), admissionBytes, { mode: 0o600 }); + const digest = sha256(admissionBytes); + const receiptPath = join(files.directory, 'rehearsal-receipt.json'); + const receipt = JSON.parse(readFileSync(receiptPath, 'utf8')); + Object.assign(receipt, { admissionSha256: digest, attemptId: stored.attemptId }); + const receiptBytes = canonicalBytes(receipt); + writeFileSync(receiptPath, receiptBytes, { mode: 0o600 }); + files.document.rehearsalReceiptSha256 = sha256(receiptBytes); + const journal = readFileSync(files.rehearsalJournalPath, 'utf8').trim().split('\n').map(JSON.parse); + for (const entry of journal) { + entry.admissionSha256 = digest; + entry.attemptId = stored.attemptId; + if ('receiptSha256' in entry) entry.receiptSha256 = files.document.rehearsalReceiptSha256; + } + const journalBytes = Buffer.from(`${journal.map(entry => JSON.stringify(canonical(entry))).join('\n')}\n`); + writeFileSync(files.rehearsalJournalPath, journalBytes, { mode: 0o600 }); + files.document.rehearsalJournalSha256 = sha256(journalBytes); + return digest; +} + function rehearsalAdmission() { const writerFences = Object.fromEntries(Object.entries(manifest.requiredWriterFences).map( ([key, state], index) => [key, { evidenceSha256: (index + 8).toString(16).repeat(64), state }], @@ -389,7 +438,7 @@ function rehearsalCustodyFiles(document = rehearsalAdmission()) { writeFileSync(path, canonicalBytes(receipt), { mode: 0o600 }); chmodSync(path, 0o600); } - writeFileSync(admissionPath, `${JSON.stringify(document)}\n`, { mode: 0o600 }); + writeFileSync(admissionPath, canonicalBytes(document), { mode: 0o600 }); chmodSync(admissionPath, 0o600); return { admissionPath, directory, document, journalPath, pendingReceiptPath, receiptPath }; } @@ -1411,6 +1460,107 @@ test('apply rejects orphan and semantically tampered rehearsal journals even whe } }); +test('apply pins the stored admission against a valid fully rehashed rehearsal from another attempt', () => { + const files = custodyFiles(); + expect(validateSuccessorAdmission(files.document, executableRecord, { + now: fixedNow, receiptDirectory: files.directory, + }).expiresAt).toBe(Date.parse(files.document.expiresAt)); + const storedPath = join(files.directory, 'rehearsal-admission.json'); + const originalBytes = readFileSync(storedPath); + const other = JSON.parse(originalBytes.toString('utf8')); + other.attemptId = '123e4567-e89b-42d3-a456-426614174002'; + const otherDigest = rebindRehearsalEvidence(files, other); + expect(otherDigest).not.toBe(files.document.rehearsalAdmissionSha256); + expect(validateSuccessorRehearsalAdmission(other, record, { + now: fixedNow, receiptDirectory: files.directory, + }).expiresAt).toBe(Date.parse(other.expiresAt)); + expect(() => validateSuccessorAdmission(files.document, executableRecord, { + now: fixedNow, receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); + // Restore the pinned file, leaving the internally consistent other receipt/journal. + writeFileSync(storedPath, originalBytes, { mode: 0o600 }); + expect(() => validateSuccessorAdmission(files.document, executableRecord, { + now: fixedNow, receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); +}); + +for (const changed of ['attempt', 'rollback', 'writerFence'] as const) { + test(`apply rejects a rehashed rehearsal with mismatched ${changed} binding`, () => { + const files = custodyFiles(); + const stored = JSON.parse(readFileSync(join(files.directory, 'rehearsal-admission.json'), 'utf8')); + if (changed === 'attempt') { + // Receipt and all journal entries agree on B, but the actual admission is A. + const other = { ...stored, attemptId: '123e4567-e89b-42d3-a456-426614174002' }; + rebindRehearsalEvidence(files, other); + const receiptPath = join(files.directory, 'rehearsal-receipt.json'); + const receipt = JSON.parse(readFileSync(receiptPath, 'utf8')); + receipt.admissionSha256 = files.document.rehearsalAdmissionSha256; + writeFileSync(receiptPath, canonicalBytes(receipt), { mode: 0o600 }); + files.document.rehearsalReceiptSha256 = sha256(readFileSync(receiptPath)); + const journal = readFileSync(files.rehearsalJournalPath, 'utf8').trim().split('\n').map(JSON.parse); + for (const entry of journal) { + entry.admissionSha256 = files.document.rehearsalAdmissionSha256; + if ('receiptSha256' in entry) entry.receiptSha256 = files.document.rehearsalReceiptSha256; + } + const bytes = Buffer.from(`${journal.map(entry => JSON.stringify(canonical(entry))).join('\n')}\n`); + writeFileSync(files.rehearsalJournalPath, bytes, { mode: 0o600 }); + files.document.rehearsalJournalSha256 = sha256(bytes); + writeFileSync(join(files.directory, 'rehearsal-admission.json'), canonicalBytes(stored), { mode: 0o600 }); + } else { + stored.attemptId = '123e4567-e89b-42d3-a456-426614174002'; + const fileName = changed === 'rollback' ? 'rollback-readback-receipt.json' : 'writer-fence-directSql.json'; + const receiptPath = join(files.directory, fileName); + const receipt = JSON.parse(readFileSync(receiptPath, 'utf8')); + if (changed === 'rollback') { + receipt.deploymentSha = '2'.repeat(40); + stored.rollback.deploymentSha = receipt.deploymentSha; + } else receipt.observedAt = fixedNow.toISOString(); + const bytes = canonicalBytes(receipt); + writeFileSync(receiptPath, bytes, { mode: 0o600 }); + if (changed === 'rollback') stored.rollback.readbackSha256 = sha256(bytes); + else stored.writerFences.directSql.evidenceSha256 = sha256(bytes); + // Even rebinding the apply digest cannot mix B's rollback/fence with A's. + files.document.rehearsalAdmissionSha256 = rebindRehearsalEvidence(files, stored); + } + expect(validateSuccessorRehearsalAdmission(stored, record, { + now: fixedNow, receiptDirectory: files.directory, + }).expiresAt).toBe(Date.parse(stored.expiresAt)); + expect(() => validateSuccessorAdmission(files.document, executableRecord, { + now: fixedNow, receiptDirectory: files.directory, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); + }); +} + +test('apply requires the pinned private canonical rehearsal admission before any external read', () => { + for (const invalid of ['missing', 'public', 'noncanonical', 'wrongDigest'] as const) { + const files = custodyFiles(); + const path = join(files.directory, 'rehearsal-admission.json'); + if (invalid === 'missing') rmSync(path); + if (invalid === 'public') chmodSync(path, 0o644); + if (invalid === 'noncanonical') { + const stored = JSON.parse(readFileSync(path, 'utf8')); + writeFileSync(path, JSON.stringify(stored, null, 2), { mode: 0o600 }); + files.document.rehearsalAdmissionSha256 = sha256(readFileSync(path)); + } + if (invalid === 'wrongDigest') files.document.rehearsalAdmissionSha256 = '0'.repeat(64); + writeFileSync(files.admissionPath, canonicalBytes(files.document), { mode: 0o600 }); + let externalCalls = 0; + const unexpected = () => { externalCalls += 1; throw new Error('unexpected external read'); }; + expect(() => runReviewMediaSuccessor({ + admissionPath: files.admissionPath, journalPath: files.journalPath, environment: {}, + }, { + now: () => fixedNow, + loadManifestImpl: () => executableRecord, + gitFactsImpl: unexpected, + protectedMainReadbackImpl: unexpected, + rollbackReadbackImpl: unexpected, + runPsqlImpl: unexpected, + })).toThrow('SUCCESSOR_ADMISSION_INVALID'); + expect(externalCalls).toBe(0); + expect(existsSync(files.journalPath)).toBe(false); + } +}); + test('admission rejects wrong preimage and every wrong writer fence', () => { { const files = custodyFiles(); @@ -1467,6 +1617,83 @@ test('success path performs preflight, rechecks rollback and commits all eleven expect(readFileSync(files.journalPath, 'utf8')).toContain('SUCCESSOR_COMMITTED'); }); +for (const remainingMs of [600_000, 5_000]) { + for (const failure of ['clientStall', 'statementTimeout', 'lockTimeout', 'expiry'] as const) { + test(`bounded preflight ${failure} with ${remainingMs}ms left cannot journal or write`, () => { + const document = admission(); + document.expiresAt = new Date(fixedNow.getTime() + remainingMs).toISOString(); + const files = custodyFiles(document); + const plan = compileSuccessorPlan(executableRecord, files.document); + let spawnCalls = 0; + let rollbackReads = 0; + const runner = createSuccessorPsqlRunner(databaseUrl, { + environment: {}, psql: '/fixture/psql', + spawnImpl: (_command: string, args: string[], options: any) => { + spawnCalls += 1; + expect(args).toContain('--single-transaction'); + expect(options.timeout).toBe(Math.min(30_000, remainingMs)); + expect(options.killSignal).toBe('SIGKILL'); + const sql = options.input; + const queryAt = sql.indexOf(plan.stateQuery); + for (const setting of [ + 'REPEATABLE READ READ ONLY', 'standard_conforming_strings=on', + "lock_timeout='2s'", "statement_timeout='15s'", + "idle_in_transaction_session_timeout='30s'", "set_config('transaction_timeout',least(30000", + ]) { + expect(sql.indexOf(setting)).toBeGreaterThanOrEqual(0); + expect(sql.indexOf(setting)).toBeLessThan(queryAt); + } + expect(sql).toContain('transaction_timestamp()'); + expect(sql).toContain(`clock_timestamp() >= '${document.expiresAt}'::timestamptz`); + expect(sql.match(/SUCCESSOR_ADMISSION_EXPIRED/g)).toHaveLength(2); + expect(sql).not.toMatch(/INSERT INTO supabase_migrations\.schema_migrations/); + if (failure === 'clientStall') return { + error: Object.assign(new Error('private transport diagnostic'), { code: 'ETIMEDOUT' }), + status: null, stdout: '', stderr: 'private transport diagnostic', + }; + const sqlstate = failure === 'statementTimeout' ? '57014' : failure === 'lockTimeout' ? '55P03' : 'P0001'; + return { status: 1, stdout: '', stderr: `ERROR: ${sqlstate}: ${failure === 'expiry' ? 'SUCCESSOR_ADMISSION_EXPIRED' : 'private diagnostic'}\n` }; + }, + }); + const expectedCode = failure === 'clientStall' ? 'MIGRATION_PSQL_EXECUTION_FAILED' + : failure === 'statementTimeout' ? 'MIGRATION_PSQL_FAILED_57014' + : failure === 'lockTimeout' ? 'MIGRATION_PSQL_FAILED_55P03' : 'SUCCESSOR_ADMISSION_EXPIRED'; + expect(() => runReviewMediaSuccessor({ + admissionPath: files.admissionPath, journalPath: files.journalPath, + environment: { SUPABASE_DB_URL: databaseUrl }, + }, { + now: () => fixedNow, + gitFactsImpl: () => ({ clean: true, detached: true, revision }), + loadManifestImpl: () => executableRecord, + protectedMainReadbackImpl: () => protectedSourceReadbackBinding(manifest, revision), + rollbackReadbackImpl: () => { rollbackReads += 1; return rollbackReadback(); }, + runPsqlImpl: runner, + })).toThrow(expectedCode); + expect(spawnCalls).toBe(1); + expect(rollbackReads).toBe(1); + expect(existsSync(files.journalPath)).toBe(false); + }); + } +} + +test('psql runner keeps a finite mutation/reconciliation cap and rejects unbounded overrides', () => { + let calls = 0; + const runner = createSuccessorPsqlRunner(databaseUrl, { + environment: {}, + spawnImpl: (_command: string, _args: string[], options: any) => { + calls += 1; + expect(options.timeout).toBe(930_000); + expect(options.killSignal).toBe('SIGKILL'); + return { status: 0, stdout: 'fixture', stderr: '' }; + }, + }); + expect(runner(databaseUrl, 'SELECT 1;', true)).toBe('fixture'); + for (const timeoutMs of [0, -1, Infinity, NaN, 930_001]) { + expect(() => runner(databaseUrl, 'SELECT 1;', true, { timeoutMs })).toThrow('SUCCESSOR_ARGUMENT_INVALID'); + } + expect(calls).toBe(1); +}); + for (const failureAt of ['fsync', 'close'] as const) { test(`committed terminal audit ${failureAt} failure preserves the committed outcome`, () => { const files = custodyFiles(); @@ -1642,7 +1869,7 @@ test('lost apply acknowledgement reconciles once and never resends eleven mutati expect(readFileSync(files.journalPath, 'utf8')).toContain('MIGRATION_BUNDLE_OUTCOME_UNCONFIRMED'); }); -for (const failureAt of [1, 2, 3, 4, 5]) { +for (const failureAt of [1, 2, 3, 4, 5, 6]) { test(`apply fsync failure at custody/start boundary ${failureAt} prevents database mutation`, () => { const files = custodyFiles(); const plan = compileSuccessorPlan(executableRecord, files.document); @@ -1668,7 +1895,7 @@ for (const failureAt of [1, 2, 3, 4, 5]) { expect(sql).toStartWith('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY;'); return JSON.stringify(plan.priorReadback.expected); }, - })).toThrow(failureAt <= 3 ? 'SUCCESSOR_ADMISSION_INVALID' : 'SUCCESSOR_JOURNAL_INVALID'); + })).toThrow(failureAt <= 4 ? 'SUCCESSOR_ADMISSION_INVALID' : 'SUCCESSOR_JOURNAL_INVALID'); expect(transports).toBe(1); expect(syncs).toBe(failureAt); }); diff --git a/docs/evidence/review-admission-fixes-20261014-validation-v1.json b/docs/evidence/review-admission-fixes-20261014-validation-v1.json new file mode 100644 index 0000000000..9c490991d2 --- /dev/null +++ b/docs/evidence/review-admission-fixes-20261014-validation-v1.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 1, + "observedAtUTC": "2026-10-13T20:18:46.314544+00:00", + "verified": true, + "checksumMaps": [ + { + "path": "apps/web/performance/review-db-successor-20261012/SHA256SUMS", + "sha256": "aff9374a0e6a6964b2d32f6cb5c6069296c5155b94c2ec1015460d2386fb4e88", + "files": 25 + }, + { + "path": "apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS", + "sha256": "f7345de87e22378811fb0bfef0f57461702a4178ec92cd13e82812dcb921138e", + "files": 13 + } + ], + "primaryUnitTests": 98, + "subsequentHarnessTests": 20, + "overlapNotSummed": true, + "pg17ProofSha256": "9a4985689d121979e3d7559c32b8f27c88ae3d1bb82d559fb9bb8ada6e706ac2", + "originalProofPreserved": true, + "heldLaunchPolicy": true, + "hostedWrites": false, + "operatingWrites": false, + "performanceEvidence": false +} diff --git a/docs/operations/review-media-operating-admission-20261013.md b/docs/operations/review-media-operating-admission-20261013.md index fd7d42c60d..ce7c69760c 100644 --- a/docs/operations/review-media-operating-admission-20261013.md +++ b/docs/operations/review-media-operating-admission-20261013.md @@ -9,7 +9,7 @@ 1. 보호된 `main`의 깨끗한 detached checkout, 현재 production rollback 대상, manifest의 writer fence 7개를 실제로 확인한다. 실행 창과 원래 `observedAt`을 포함한 canonical JSON 입력을 저장소 밖 private 디렉터리에 저장한다. 프로바이더 원문·인증 정보는 입력에 넣지 않는다. 2. `--mode rehearsal --input --custody-dir `로 리허설 admission을 만든다. 입력은 schema, source revision/root, manifest hash, checkout readback, source/protected-main/rollback receipt와 writer-fence receipt 전체를 포함해야 한다. 3. 준비된 admission을 기존 `review-media-sql-rehearsal.mjs`에 전달한다. 기존 적용기는 source와 rollback을 다시 읽고 DB 대상·중지 조건을 확인한 뒤 rollback-only 리허설을 수행한다. 이 생성기만 실행해서는 리허설 receipt가 생기지 않는다. -4. 성공한 리허설의 admission·receipt·3행 journal SHA와 원래 만료 시각을 결속한 apply 입력을 준비한다. `--mode apply --input --custody-dir `로 apply admission을 만든다. +4. 성공한 리허설의 admission·receipt·3행 journal SHA와 원래 만료 시각을 결속한 apply 입력을 준비한다. `--mode apply --input --custody-dir `로 apply admission을 만든다. 적용 admission의 `rehearsalAdmissionSha256`는 저장된 `rehearsal-admission.json`의 실제 바이트이며 receipt·journal의 원래 실행과 source·rollback·writer fence가 같아야 한다. 5. 기존 적용기의 launch policy, 보호 source, 실제 rollback과 writer-fence 근거가 충족된 경우에만 `review-media-sql-successor.mjs`를 실행한다. 출력 존재·실패·응답 유실은 기존 산출물을 확인한 뒤 처리하며, 파일을 지워 같은 작업을 자동 재시도하지 않는다. 두 mode 모두 Node 24로 `apps/web/scripts/review-media-operating-admission.mjs`를 실행한다. 성공 출력은 준비한 경로·SHA·만료 시각이며, 운영 적용 완료 영수증이 아니다. @@ -22,3 +22,7 @@ - 입력을 수집하는 운영 adapter와 live 인증은 별도다. 저장소에 Vercel 토큰이 없다는 사실만으로 새 토큰 생성·저장 권한을 추정하지 않는다. 현재 manifest의 `held` 상태를 이 도구가 변경하지 않는다. 현재 통과한 PG17.6 proof는 controller와 SQL의 격리 재생이다. 이 생성기의 검증은 별도 단위 검사로 기록하며 hosted DB 적용·Storage 물리 삭제·운영 성능 증거로 합산하지 않는다. + +최신 단위 검사 기록은 `apps/web/performance/review-media-operating-admission-20261013/current-evidence.json`이 선택한다. 이전 `verification.json`과 `controller-reverification.json`은 해당 시점의 기록이며 현재 소스 증명으로 사용하지 않는다. 새 기록은 검사 로그와 source 바이트를 결속한다. + +적용 전 state read는 lock 2초, statement 15초, transaction 최대 30초와 남은 admission 시간, client 최대 30초로 제한한다. 실패·만료는 apply journal 및 mutation 전에 거부한다. 다른 psql 작업도 유한 client 제한을 사용하지만 이 로컬 검사만으로 운영 timeout 동작을 입증하지 않는다. From 33cda47d32857ec60b9843074a526b77b634c23e Mon Sep 17 00:00:00 2001 From: twoimo <32544727+twoimo@users.noreply.github.com> Date: Wed, 14 Oct 2026 05:34:16 +0900 Subject: [PATCH 14/14] Rebind dependent rollback evidence to fresh PG17 replay --- .../review-db-rehearsal-20261012/README.md | 14 ++++ .../review-db-rehearsal-20261012/SHA256SUMS | 22 ++++-- .../SHA256SUMS-4ba9c6d2-historical | 28 ++++++++ .../SHA256SUMS-4ba9c6d2-historical.sha256 | 1 + .../SHA256SUMS.sha256 | 2 +- .../ci-npm-failure-20261014-v1.json | 19 +++++ .../pg17-review-fixes-20261014-v1.json | 72 +++++++++++++++++++ .../pg17-review-fixes-20261014-v1.stderr.log | 0 .../pg17-review-fixes-20261014-v1.stdout.log | 1 + .../retained-current-proof.json | 4 +- ...ned-current-proof.json-4ba9c6d2-historical | 6 ++ .../review-fix-index-units-20261014-v1.log | 44 ++++++++++++ ...dmission-fixes-20261014-validation-v2.json | 28 ++++++++ 13 files changed, 231 insertions(+), 10 deletions(-) create mode 100644 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4ba9c6d2-historical create mode 100644 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4ba9c6d2-historical.sha256 create mode 100644 apps/web/performance/review-db-rehearsal-20261012/ci-npm-failure-20261014-v1.json create mode 100644 apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.json create mode 100644 apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.stderr.log create mode 100644 apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.stdout.log create mode 100644 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json-4ba9c6d2-historical create mode 100644 apps/web/performance/review-db-rehearsal-20261012/review-fix-index-units-20261014-v1.log create mode 100644 docs/evidence/review-admission-fixes-20261014-validation-v2.json diff --git a/apps/web/performance/review-db-rehearsal-20261012/README.md b/apps/web/performance/review-db-rehearsal-20261012/README.md index 35a4bb422e..781d456487 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/README.md +++ b/apps/web/performance/review-db-rehearsal-20261012/README.md @@ -113,3 +113,17 @@ SUCCESSOR_ADMISSION_EXPIRED and confirmed exact baseline restoration from a sepa read-only connection. This explicitly labelled fault injection proves the final guard/rollback path; it does not measure an actual fifteen-minute operating lease. The original migration statements and statement vectors remain unchanged. +# Review-fix verification — 2026-10-14 + +The current selector now points to `pg17-review-fixes-20261014-v1.json`. +It binds the changed successor manifest and the unchanged rollback harness. +Twelve stage readbacks, exact rollback, forced failure, lost acknowledgement +with one mutation attempt, real contended expiry, and apply-final deadline +rejection passed on isolated PostgreSQL 17.6. The owned container was removed. +No hosted rows or operating changes were involved. + +The first PR3202 Ubuntu npm full suite correctly rejected stale dependency +hashes in this directory's complete artifact map. Its bounded failure receipt +and the old map, pin and selector are retained. The current map binds current +source bytes while historical proofs remain historical; the new selector is +backed by a fresh replay, not by relabelling an older proof. diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS index f12e0be4cc..a70e1ddfd0 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS @@ -1,28 +1,36 @@ -23bf7e7d7e9e5d7962021dd3ec860e87b47d07e119ba0e39de2e423a0a601f4e .github/review-media-sql-successor.v1.json -ac14e363f38d6cb6acba0e24a5ad6c6943c8e8c15152cff49fcd86a2d014bd9d apps/web/performance/review-db-rehearsal-20261012/README.md +295b2354c562ed87102c5e256497a2705f90941587fefa7e9ad8a4ba8e40787f .github/review-media-sql-successor.v1.json +ef4c24b489343213733ca719b1fca4cb456d4cdd8510e21e6f5c038d5b968b63 apps/web/performance/review-db-rehearsal-20261012/README.md ea529addcf42cdb766f622786ccfffb13fe337f70faae6ade6da83736be58f57 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-0833ed81-historical +d8ad511edf7d3832aa432e499b27f33e2c9ad423530ffdff06dea42869eaff85 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4ba9c6d2-historical +756ead633ae6d2a7805043203c6781d0726cdd3042980031ebc508028f0d3621 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4ba9c6d2-historical.sha256 6d9cbca375e6790ee1051aa9d9bf1c45beeeece8d919651281885b4c04962981 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4d4ecf60-historical 5df2ade9cb3618b774eb4d3796df06ade6ba069409de49d0bdd542b34b6ec4ba apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical 7d395c9246782322ccc326dd99ab0d5e94e0d82e203eb519a6baf91a4a62ece2 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical.sha256 6c474d8e78c0eba3f15c077dbdd8634f073524c3b5f842213372bf0f1fed84fc apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical +88a814a1fc0d9b8a8fa7bc6df299d9374bef5f97595736e72579deeb2c475456 apps/web/performance/review-db-rehearsal-20261012/ci-npm-failure-20261014-v1.json 75b801b80a2d23a7c415085e8fb9b187b03469d2c2d861ba3279d52b2ba7545c apps/web/performance/review-db-rehearsal-20261012/fixture-url-unit-verification-20261013.json a12f8880f2a0b8b5493e37ea4b2619962d42643581c14c889f0c6f6e8d528129 apps/web/performance/review-db-rehearsal-20261012/interrupted-attempt-20261013.json +23fb9b471a06dbd36aafab3a678152ca897893b3cdc968f2d623035d217b1e38 apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.json +e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.stderr.log +97682860c19c701a9062a409160bd0e9f2f692f7e296118c48cfba75ad2cc5f9 apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.stdout.log 9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076 apps/web/performance/review-db-rehearsal-20261012/pg17-reviewed-boundaries-20261013.json b279a6b900dd3be10b9084a6362d169e87435bae955b2a1419858709645a6883 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json 7f74c175be3eee72b5e93eb75dac07bfefc0ef88d7d9957c26d877ee125ff590 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json cb766e279baad0315e9d7055bc326b41b78a7d50a4972dac3214bc071aee0e49 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json 0cfc9c10ff7b70c606f6eeef33fff8dbd14636dc15b7c963925097454ee454a2 apps/web/performance/review-db-rehearsal-20261012/pg17-terminal-boundaries-20261013.json 128773e5dabbbb120889716ffbba41e498a37a437aa6799311a81af729703880 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof-0833ed81-historical.json -78f3e0ad5dbc4e3c53cf8dce3b2a9cb2f484f8f72683cbf1a5c659c8f142acb6 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json +71ff7b8dde2ff6ef936a012b84453fcc0990bd9724d89b046bb1ebaa7bfddf1e apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json +78f3e0ad5dbc4e3c53cf8dce3b2a9cb2f484f8f72683cbf1a5c659c8f142acb6 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json-4ba9c6d2-historical +20aa7a87b82f034eac71b843b6d5ab4547c4b6e0f6bba950b3e0b7484b2952ff apps/web/performance/review-db-rehearsal-20261012/review-fix-index-units-20261014-v1.log 7bdf3cd525427da824674ce50cb3ba7c5d54bf6cb0ef8ebe85b21bb2372eae0a apps/web/performance/review-db-rehearsal-20261012/reviewed-boundaries-unit-verification-20261013.json c53a467eaf9b929647bc1d66248308ba1ff73eba89fc9221dbf7db4711eb8562 apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json fad17a393e5fc74d06dec2be58c41265d21f355d5ddce92a3947bb67a823e73a apps/web/performance/review-db-rehearsal-20261012/terminal-boundary-unit-verification-20261013.json f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs -768b8887a30c2b8f496a73a21d373b85a0b64fadaed3a1485034a729ee704d39 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +7e750e9d0126f6f10afe9056d747a2ea854d462161ea4e8f0168e2751d554c0a apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs c893c294719776bf5b064c3355866ab97e0fa9412082d9b47d7de5266094c65f apps/web/scripts/review-media-sql-rehearsal.mjs -5d61808b45661d412937fbfdfd3bfb75b1cf92c7c285e4d8ad061a2f667c3c0b apps/web/scripts/review-media-sql-successor.mjs +4f1b2798e0cfc13db4f11cd6eb4a8653320aa8516eccd71d410fcff6d06a7f24 apps/web/scripts/review-media-sql-successor.mjs fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs dc1f2594e629d5af16541db93e2e51f9b98d8282dd7fbeb802dd240fa6f522c0 apps/web/tests-unit/review-media-migration-bundle.test.ts 54a9560b77a3afb9eb70631224ba4f481caf82eb8b5d2cdc1a076d8831c1ec7e apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts -983a62a3f81973e18914835b2d483a7a05fef3fa9f7d129ffd7a60ed5cd4e551 apps/web/tests-unit/review-media-sql-successor-harness.test.ts -f74f30aa6f1237a757c7fd18693fa839cb4c199409a87d870260ec574bfc5d33 apps/web/tests-unit/review-media-sql-successor.test.ts +99c6bc0221188f93317640d4d65affdfeb91fea233bb0f0cc0f409407e419d84 apps/web/tests-unit/review-media-sql-successor-harness.test.ts +f1ce764e0bffea5c05cbca0017574b305c353f9498623df928a177fee3b780ff apps/web/tests-unit/review-media-sql-successor.test.ts diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4ba9c6d2-historical b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4ba9c6d2-historical new file mode 100644 index 0000000000..f12e0be4cc --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4ba9c6d2-historical @@ -0,0 +1,28 @@ +23bf7e7d7e9e5d7962021dd3ec860e87b47d07e119ba0e39de2e423a0a601f4e .github/review-media-sql-successor.v1.json +ac14e363f38d6cb6acba0e24a5ad6c6943c8e8c15152cff49fcd86a2d014bd9d apps/web/performance/review-db-rehearsal-20261012/README.md +ea529addcf42cdb766f622786ccfffb13fe337f70faae6ade6da83736be58f57 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-0833ed81-historical +6d9cbca375e6790ee1051aa9d9bf1c45beeeece8d919651281885b4c04962981 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4d4ecf60-historical +5df2ade9cb3618b774eb4d3796df06ade6ba069409de49d0bdd542b34b6ec4ba apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical +7d395c9246782322ccc326dd99ab0d5e94e0d82e203eb519a6baf91a4a62ece2 apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-7d111349-historical.sha256 +6c474d8e78c0eba3f15c077dbdd8634f073524c3b5f842213372bf0f1fed84fc apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-pre-hardening-historical +75b801b80a2d23a7c415085e8fb9b187b03469d2c2d861ba3279d52b2ba7545c apps/web/performance/review-db-rehearsal-20261012/fixture-url-unit-verification-20261013.json +a12f8880f2a0b8b5493e37ea4b2619962d42643581c14c889f0c6f6e8d528129 apps/web/performance/review-db-rehearsal-20261012/interrupted-attempt-20261013.json +9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076 apps/web/performance/review-db-rehearsal-20261012/pg17-reviewed-boundaries-20261013.json +b279a6b900dd3be10b9084a6362d169e87435bae955b2a1419858709645a6883 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-current-20261013.json +7f74c175be3eee72b5e93eb75dac07bfefc0ef88d7d9957c26d877ee125ff590 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal-final-20261013.json +cb766e279baad0315e9d7055bc326b41b78a7d50a4972dac3214bc071aee0e49 apps/web/performance/review-db-rehearsal-20261012/pg17-rollback-rehearsal.json +0cfc9c10ff7b70c606f6eeef33fff8dbd14636dc15b7c963925097454ee454a2 apps/web/performance/review-db-rehearsal-20261012/pg17-terminal-boundaries-20261013.json +128773e5dabbbb120889716ffbba41e498a37a437aa6799311a81af729703880 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof-0833ed81-historical.json +78f3e0ad5dbc4e3c53cf8dce3b2a9cb2f484f8f72683cbf1a5c659c8f142acb6 apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json +7bdf3cd525427da824674ce50cb3ba7c5d54bf6cb0ef8ebe85b21bb2372eae0a apps/web/performance/review-db-rehearsal-20261012/reviewed-boundaries-unit-verification-20261013.json +c53a467eaf9b929647bc1d66248308ba1ff73eba89fc9221dbf7db4711eb8562 apps/web/performance/review-db-rehearsal-20261012/scanner-readback.json +fad17a393e5fc74d06dec2be58c41265d21f355d5ddce92a3947bb67a823e73a apps/web/performance/review-db-rehearsal-20261012/terminal-boundary-unit-verification-20261013.json +f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431 apps/web/performance/review-db-rehearsal-20261012/verify-review-media-sql-rehearsal.mjs +768b8887a30c2b8f496a73a21d373b85a0b64fadaed3a1485034a729ee704d39 apps/web/performance/review-db-successor-20261012/verify-review-media-sql-successor.mjs +c893c294719776bf5b064c3355866ab97e0fa9412082d9b47d7de5266094c65f apps/web/scripts/review-media-sql-rehearsal.mjs +5d61808b45661d412937fbfdfd3bfb75b1cf92c7c285e4d8ad061a2f667c3c0b apps/web/scripts/review-media-sql-successor.mjs +fcebc20dc4846606dc14bdbe658a07daa32430ad632e52383151f5c22d22efb9 apps/web/scripts/supabase-migration-bundle.mjs +dc1f2594e629d5af16541db93e2e51f9b98d8282dd7fbeb802dd240fa6f522c0 apps/web/tests-unit/review-media-migration-bundle.test.ts +54a9560b77a3afb9eb70631224ba4f481caf82eb8b5d2cdc1a076d8831c1ec7e apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts +983a62a3f81973e18914835b2d483a7a05fef3fa9f7d129ffd7a60ed5cd4e551 apps/web/tests-unit/review-media-sql-successor-harness.test.ts +f74f30aa6f1237a757c7fd18693fa839cb4c199409a87d870260ec574bfc5d33 apps/web/tests-unit/review-media-sql-successor.test.ts diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4ba9c6d2-historical.sha256 b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4ba9c6d2-historical.sha256 new file mode 100644 index 0000000000..98e05b4991 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS-4ba9c6d2-historical.sha256 @@ -0,0 +1 @@ +d8ad511edf7d3832aa432e499b27f33e2c9ad423530ffdff06dea42869eaff85 diff --git a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 index 98e05b4991..290cf460a0 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 +++ b/apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS.sha256 @@ -1 +1 @@ -d8ad511edf7d3832aa432e499b27f33e2c9ad423530ffdff06dea42869eaff85 +c1af1afd832bd2b0bbdcd4419589de778525e1b18a4dff196e457b82381c97fb diff --git a/apps/web/performance/review-db-rehearsal-20261012/ci-npm-failure-20261014-v1.json b/apps/web/performance/review-db-rehearsal-20261012/ci-npm-failure-20261014-v1.json new file mode 100644 index 0000000000..970d468726 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/ci-npm-failure-20261014-v1.json @@ -0,0 +1,19 @@ +{ + "observedAtUTC": "2026-10-13T20:31:32.330565+00:00", + "sourceRevision": "71dcfc98ab03a016d05449780574a4b9edd81e36", + "runId": 38461911693, + "jobId": 115518972088, + "job": "Ubuntu npm", + "boundedFailure": "LOCAL_REHEARSAL_ARTIFACT_SHA256_MISMATCH", + "failedTest": "default retained proof uses a stable current-proof selector", + "summary": [ + "(pass) verified review count pagination > thrown page 2 failure rejects instead of returning partial counts [0.01ms]", + " 3318 pass", + " 21 skip", + " 1 fail", + "Ran 3340 tests across 420 files. [164.92s]" + ], + "cause": "Rollback evidence map retained hashes for the changed successor controller, manifest and fixture helper. The full-map validator correctly rejected stale dependencies.", + "fullProviderLogPersistedInRepository": false, + "operatingEvidence": false +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.json b/apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.json new file mode 100644 index 0000000000..fac22e271d --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.json @@ -0,0 +1,72 @@ +{ + "schemaVersion": 1, + "status": "passed", + "environment": { + "engine": "17.6", + "image": "supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab", + "emptyRowChecks": [ + "public.profiles", + "public.reviews" + ] + }, + "manifestSha256": "295b2354c562ed87102c5e256497a2705f90941587fefa7e9ad8a4ba8e40787f", + "manifestToolchainRoot": "6409387ff1da0bb5e499dab76e4b0b7326a9a81aa71fd21f2d51682a48df0a2f", + "rehearsalHarnessSha256": "f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431", + "sourceRoot": "61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544", + "fixtureDumpSha256": "b7777306a43cc84408434e9835973604b2eb520cf9e9e8d0ee4bba6dcb41257f", + "predecessorSources": [ + { + "path": "backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql", + "sha256": "b373b7ea472c0352a33a4d4043cf8d6aa8474c04cc1ca5778805edfa77ac9c95" + }, + { + "path": "backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql", + "sha256": "66eace1d6fb0dd55c1d780776bab855cc37690335ad40b0fdc1294c610e07d3a" + }, + { + "path": "backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql", + "sha256": "e1c105df82c4f814d3e6adad807ff9d072b8cd42ae770b4b78f75b89a9387020" + }, + { + "path": "backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql", + "sha256": "8acf6d1428764260ed57dac5fe09711868a82896f0a6d631d502128004c168a3" + }, + { + "path": "backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql", + "sha256": "b596b200e52c6813a4cfa1b0a2818625f067864549e8854f3497afdcdab706da" + }, + { + "path": "backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql", + "sha256": "2067538f89c9f90d28e784672c7a1288306ba22d5ae92b087c8692503da9b1ae" + } + ], + "migrationCount": 11, + "statementCount": 112, + "baselineStateSha256": "3802888324e1651ad7b2f71e7cf15fac78ab77aa57637d0c60431f8da67e6298", + "stageStatesSha256": "bd25ecbd82759ca75e994ca0f1da38b49acc848d5dd556e702e4e43024edaa65", + "terminalStateSha256": "9cf59a5d983e78f6f7d9a594267c6feda3276611bf3539289b8a3de3295f700a", + "compiledSqlBytes": 2510560, + "compiledSqlSha256": "285d0c859117006646389dbd2a22de9aef1c0b97bc796a9e4d4448a8a232b093", + "applyCompiledSqlSha256": "91e378c58d58d7d7f818464c9c92aff02c7b01ca528e9db858e2b1d2d00bd92f", + "applyExpiryFaultedSqlSha256": "727ff0f6be2dbf8ee06551d4efd6ebe665f7187189f7a0a1159d5281ae90635b", + "results": { + "rollbackStageCount": 12, + "rollbackExact": true, + "forcedErrorRejected": true, + "forcedErrorRollbackExact": true, + "lostResponseMutationAttempts": 1, + "lostResponseRestorationExact": true, + "contendedExpiryRejected": true, + "contendedExpiryRollbackExact": true, + "applyFinalGuardDeadlineFaultInjected": true, + "applyTerminalReadbackObserved": true, + "applyExpiryRejectedBeforeCommit": true, + "applyExpiryMutationAttempts": 1, + "applyExpiryRollbackExact": true + }, + "hostedWrites": false, + "operatingRowsRead": false, + "rawSqlInProof": false, + "sourceMutation": false, + "temporaryContainerRemoved": true +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.stderr.log b/apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.stderr.log new file mode 100644 index 0000000000..e69de29bb2 diff --git a/apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.stdout.log b/apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.stdout.log new file mode 100644 index 0000000000..429c11d1fc --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.stdout.log @@ -0,0 +1 @@ +{"kind":"review-media-sql-local-rehearsal","retainedProof":{"present":true,"classification":"retained-historical-evidence","repositoryPath":"apps/web/performance/review-db-rehearsal-20261012/pg17-reviewed-boundaries-20261013.json","selector":{"repositoryPath":"apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json","sha256":"78f3e0ad5dbc4e3c53cf8dce3b2a9cb2f484f8f72683cbf1a5c659c8f142acb6","proofPath":"pg17-reviewed-boundaries-20261013.json","proofSha256":"9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076"},"expectedSha256":"9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076","actualSha256":"9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076","currentSourceMatch":false,"usedAsCurrentProof":false,"bindingDifferences":[{"field":"manifestSha256","current":"295b2354c562ed87102c5e256497a2705f90941587fefa7e9ad8a4ba8e40787f","retained":"23bf7e7d7e9e5d7962021dd3ec860e87b47d07e119ba0e39de2e423a0a601f4e"}]},"currentProof":{"schemaVersion":1,"status":"passed","environment":{"engine":"17.6","image":"supabase/postgres@sha256:fbe6c858a2ea9616aead3c39e132afcc0660decc5cb7dc656208ed145f5331ab","emptyRowChecks":["public.profiles","public.reviews"]},"manifestSha256":"295b2354c562ed87102c5e256497a2705f90941587fefa7e9ad8a4ba8e40787f","manifestToolchainRoot":"6409387ff1da0bb5e499dab76e4b0b7326a9a81aa71fd21f2d51682a48df0a2f","rehearsalHarnessSha256":"f7321f607a5bf3a24ba099d51abc67254db52fcc385945692426d1b2dfa6e431","sourceRoot":"61f9d7637c0714a7b0d9108c6f7959ea820175a7870dba681498b570296a1544","fixtureDumpSha256":"b7777306a43cc84408434e9835973604b2eb520cf9e9e8d0ee4bba6dcb41257f","predecessorSources":[{"path":"backend/supabase/migrations/20261004190259_admin_record_guarded_actions.sql","sha256":"b373b7ea472c0352a33a4d4043cf8d6aa8474c04cc1ca5778805edfa77ac9c95"},{"path":"backend/supabase/migrations/20261004192657_admin_evaluation_raw_warning_groups.sql","sha256":"66eace1d6fb0dd55c1d780776bab855cc37690335ad40b0fdc1294c610e07d3a"},{"path":"backend/supabase/migrations/20261004194715_admin_evaluation_raw_warning_invoker_contract.sql","sha256":"e1c105df82c4f814d3e6adad807ff9d072b8cd42ae770b4b78f75b89a9387020"},{"path":"backend/supabase/migrations/20261009022915_restaurant_review_manual_preview_eligibility.sql","sha256":"8acf6d1428764260ed57dac5fe09711868a82896f0a6d631d502128004c168a3"},{"path":"backend/supabase/migrations/20261009091342_admin_record_private_verification_cleanup.sql","sha256":"b596b200e52c6813a4cfa1b0a2818625f067864549e8854f3497afdcdab706da"},{"path":"backend/supabase/migrations/20261009101645_admin_user_management_rpc_forward.sql","sha256":"2067538f89c9f90d28e784672c7a1288306ba22d5ae92b087c8692503da9b1ae"}],"migrationCount":11,"statementCount":112,"baselineStateSha256":"3802888324e1651ad7b2f71e7cf15fac78ab77aa57637d0c60431f8da67e6298","stageStatesSha256":"bd25ecbd82759ca75e994ca0f1da38b49acc848d5dd556e702e4e43024edaa65","terminalStateSha256":"9cf59a5d983e78f6f7d9a594267c6feda3276611bf3539289b8a3de3295f700a","compiledSqlBytes":2510560,"compiledSqlSha256":"285d0c859117006646389dbd2a22de9aef1c0b97bc796a9e4d4448a8a232b093","applyCompiledSqlSha256":"91e378c58d58d7d7f818464c9c92aff02c7b01ca528e9db858e2b1d2d00bd92f","applyExpiryFaultedSqlSha256":"727ff0f6be2dbf8ee06551d4efd6ebe665f7187189f7a0a1159d5281ae90635b","results":{"rollbackStageCount":12,"rollbackExact":true,"forcedErrorRejected":true,"forcedErrorRollbackExact":true,"lostResponseMutationAttempts":1,"lostResponseRestorationExact":true,"contendedExpiryRejected":true,"contendedExpiryRollbackExact":true,"applyFinalGuardDeadlineFaultInjected":true,"applyTerminalReadbackObserved":true,"applyExpiryRejectedBeforeCommit":true,"applyExpiryMutationAttempts":1,"applyExpiryRollbackExact":true},"hostedWrites":false,"operatingRowsRead":false,"rawSqlInProof":false,"sourceMutation":false,"temporaryContainerRemoved":true},"currentOutputPath":"/Users/twoimo/.codex/worktrees/mobile-nav-read-20261012/tzudong/apps/web/performance/review-db-rehearsal-20261012/pg17-review-fixes-20261014-v1.json"} diff --git a/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json b/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json index dc2a656581..9f1af55bac 100644 --- a/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json +++ b/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, "kind": "review-media-sql-retained-current-proof", - "proofPath": "pg17-reviewed-boundaries-20261013.json", - "proofSha256": "9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076" + "proofPath": "pg17-review-fixes-20261014-v1.json", + "proofSha256": "23fb9b471a06dbd36aafab3a678152ca897893b3cdc968f2d623035d217b1e38" } diff --git a/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json-4ba9c6d2-historical b/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json-4ba9c6d2-historical new file mode 100644 index 0000000000..dc2a656581 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/retained-current-proof.json-4ba9c6d2-historical @@ -0,0 +1,6 @@ +{ + "schemaVersion": 1, + "kind": "review-media-sql-retained-current-proof", + "proofPath": "pg17-reviewed-boundaries-20261013.json", + "proofSha256": "9c73b47c0841f2585d7ceee77423d27410a488e61080004cf227d4329dd88076" +} diff --git a/apps/web/performance/review-db-rehearsal-20261012/review-fix-index-units-20261014-v1.log b/apps/web/performance/review-db-rehearsal-20261012/review-fix-index-units-20261014-v1.log new file mode 100644 index 0000000000..79f44e6288 --- /dev/null +++ b/apps/web/performance/review-db-rehearsal-20261012/review-fix-index-units-20261014-v1.log @@ -0,0 +1,44 @@ +bun test v1.4.0 (34cbb9a40) + +apps/web/tests-unit/review-media-sql-successor-harness.test.ts: +(pass) fixture observes delayed advisory contention before expiry without the old 700ms cutoff [0.12ms] +(pass) fixture rejects contention returned by a slow observation after expiry [0.09ms] +(pass) successor acknowledgement loss reconciles a committed terminal state without SQL replay [0.18ms] +(pass) successor failure at the baseline is classified without diagnostic SQL replay [0.05ms] +(pass) retained proof is checksum-verified and distinguished from the current harness source [1.16ms] +(pass) retained proof with bytes not authorized by SHA256SUMS is rejected [1476.90ms] +(pass) jointly altered retained proof and checksum map are rejected by the independent digest [2.89ms] +(pass) owned database is cleaned after predecessor SQL fails [0.75ms] +(pass) owned database is cleaned after fixture SQL fails [0.12ms] +(pass) pre-existing database is never acquired or dropped [0.08ms] +(pass) database CREATE acknowledgement loss is reconciled without replay and cleaned by exact ownership [0.23ms] +(pass) cleanup re-reads and removes the exact owned database after the first CREATE readback fails [0.09ms] +(pass) cleanup refuses a database whose exact owner metadata does not match [0.18ms] +(pass) fixture role creation acknowledgement loss is reconciled and exact owned roles are cleaned [0.60ms] +(pass) cleanup re-reads exact owned roles when the first post-create readback fails [0.20ms] +(pass) fixture role creation failure without owned readback never authorizes cleanup [0.11ms] +(pass) pre-existing fixture role collision is preserved and never acquired for cleanup [0.10ms] +(pass) uncertain create collision with another owner is not guessed or removed [0.09ms] +(pass) cleanup refuses roles whose ownership marker changed after creation [0.08ms] +(pass) cleanup refuses to remove owned roles after an out-of-scope membership is added [0.06ms] + +apps/web/tests-unit/review-media-sql-rehearsal-verifier.test.ts: +(pass) default retained proof uses a stable current-proof selector [2.44ms] +(pass) selector rejects unauthenticated bytes, path escape, and proof-SHA relabelling [3523.67ms] +(pass) retained proof verifies the complete non-circular artifact map from one byte snapshot [1.82ms] +(pass) retained rollback proof rejects bytes not authorized by SHA256SUMS [0.76ms] +(pass) explicit current output is exclusive and separate from retained proof [0.10ms] +(pass) apply expiry fault changes only the final wrapper guard after the exported marker [0.10ms] +(pass) container create acknowledgement loss is reconciled by unique name and owner label [0.23ms] +(pass) first inspect failure after uncertain create is reconciled and cleaned by exact owned ID [0.09ms] +(pass) same-process rehearsal runs derive distinct container names from their owner tokens [0.03ms] +(pass) create failure reconciles proven absence without issuing a removal [0.03ms] +(pass) owner-label mismatch is rejected and the unowned container is not removed [0.10ms] +(pass) acknowledgement mismatch retains exact owned ID for safe finally cleanup [0.10ms] +(pass) cleanup failure leaves ownership live and cannot be misreported as removed [0.05ms] +(pass) cleanup acknowledgement loss succeeds only after exact-ID absence readback [0.08ms] + + 34 pass + 0 fail + 153 expect() calls +Ran 34 tests across 2 files. [5.12s] diff --git a/docs/evidence/review-admission-fixes-20261014-validation-v2.json b/docs/evidence/review-admission-fixes-20261014-validation-v2.json new file mode 100644 index 0000000000..c8096a8c88 --- /dev/null +++ b/docs/evidence/review-admission-fixes-20261014-validation-v2.json @@ -0,0 +1,28 @@ +{ + "observedAtUTC": "2026-10-13T20:34:16.074395+00:00", + "verified": true, + "checksumMaps": [ + { + "path": "apps/web/performance/review-db-rehearsal-20261012/SHA256SUMS", + "sha256": "c1af1afd832bd2b0bbdcd4419589de778525e1b18a4dff196e457b82381c97fb", + "files": 36 + }, + { + "path": "apps/web/performance/review-db-successor-20261012/SHA256SUMS", + "sha256": "aff9374a0e6a6964b2d32f6cb5c6069296c5155b94c2ec1015460d2386fb4e88", + "files": 25 + }, + { + "path": "apps/web/performance/review-media-operating-admission-20261013/SHA256SUMS", + "sha256": "f7345de87e22378811fb0bfef0f57461702a4178ec92cd13e82812dcb921138e", + "files": 13 + } + ], + "relatedIndexTests": 34, + "assertions": 153, + "currentRollbackProofSha256": "23fb9b471a06dbd36aafab3a678152ca897893b3cdc968f2d623035d217b1e38", + "fullSuiteFailedPriorHead": "71dcfc98ab03a016d05449780574a4b9edd81e36", + "fullSuiteNewHeadPending": true, + "operatingEvidence": false, + "performanceEvidence": false +}