From a1acf8de436141d888ccc32ad6ef7c25a58c1432 Mon Sep 17 00:00:00 2001 From: Postil Maintainer Date: Sat, 25 Jul 2026 00:29:40 +0000 Subject: [PATCH 1/4] Exit cleanly when resolving with no alerting key configured A resolve with no configured integration key has nothing to resolve; failing the step marked the run failed and re-triggered the resolve job on every following scheduled run, producing a self-perpetuating failure loop until the key exists. --- .github/actions/ilert-event/action.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/actions/ilert-event/action.yml b/.github/actions/ilert-event/action.yml index f8f854a5..f5099a69 100644 --- a/.github/actions/ilert-event/action.yml +++ b/.github/actions/ilert-event/action.yml @@ -34,6 +34,13 @@ runs: run: | set -euo pipefail if [[ -z "${ILERT_INTEGRATION_KEY:-}" ]]; then + if [[ "${EVENT_TYPE}" == "RESOLVE" ]]; then + # No alert can exist without a configured key, so there is + # nothing to resolve; exiting nonzero here would mark the run + # failed and re-trigger the resolve job on every following run. + echo "ILERT_INTEGRATION_KEY is not configured; nothing to resolve" + exit 0 + fi echo "ILERT_INTEGRATION_KEY is missing from the environment; external alerting is not configured" exit 1 fi From 268dc2a7b3929825eaf193c026cb3b71851d6853 Mon Sep 17 00:00:00 2001 From: Postil Maintainer Date: Sun, 18 Oct 2026 20:29:10 +0000 Subject: [PATCH 2/4] Persist review attempt receipts and recovery ownership Bind queue claims, staged reviews and recovery to immutable receipts. Settle trusted usage for the originating review and repair terminal advisory state when ownership changes. Preserve legacy recovery and additive schema compatibility. --- drizzle/0045_job_attempt_receipts.sql | 146 + drizzle/meta/0045_snapshot.json | 6866 +++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + scripts/inspect-review-ledger.ts | 24 +- src/lib/db/schema.ts | 84 + src/lib/github/webhook-handler.ts | 22 +- src/lib/hosted-review-pause.ts | 26 + src/lib/job-attempt-receipts.ts | 373 ++ src/lib/private-worker-rehearsal.ts | 18 +- src/lib/queue.ts | 216 +- src/lib/release-job-rollout.ts | 20 +- src/lib/review-completion.ts | 159 +- src/worker/index.ts | 6 + src/worker/review.ts | 543 +- src/worker/runner.ts | 1 + src/worker/watchdog.ts | 143 +- tests/job-attempt-receipts.test.ts | 1054 ++++ tests/worker-runner.test.ts | 6 +- 18 files changed, 9327 insertions(+), 387 deletions(-) create mode 100644 drizzle/0045_job_attempt_receipts.sql create mode 100644 drizzle/meta/0045_snapshot.json create mode 100644 src/lib/job-attempt-receipts.ts create mode 100644 tests/job-attempt-receipts.test.ts diff --git a/drizzle/0045_job_attempt_receipts.sql b/drizzle/0045_job_attempt_receipts.sql new file mode 100644 index 00000000..4f986403 --- /dev/null +++ b/drizzle/0045_job_attempt_receipts.sql @@ -0,0 +1,146 @@ +CREATE SEQUENCE job_attempt_receipt_ids AS bigint START 1 INCREMENT 1 CACHE 1 NO CYCLE; +--> statement-breakpoint +CREATE TABLE "job_attempt_receipts" ( + "receipt_id" bigint PRIMARY KEY NOT NULL, + "predecessor_id" bigint, + "source_epoch" uuid, + "schema_version" integer DEFAULT 1 NOT NULL, + "claim_receipt_id" bigint, + "claim_kind" text DEFAULT 'claim' NOT NULL, + "binding_receipt_id" bigint, + "binding_kind" text DEFAULT 'review_bound' NOT NULL, + "job_id" bigint, + "ordinal" integer, + "job_kind" text, + "review_id" bigint, + "review_public_id" uuid, + "event_kind" text NOT NULL, + "outcome" text, + "phase" text, + "category" text, + "recovered" boolean, + "recovery" text, + "occurred_at" timestamp with time zone DEFAULT clock_timestamp() NOT NULL, + CONSTRAINT "job_attempt_receipts_predecessor_id_unique" UNIQUE("predecessor_id"), + CONSTRAINT "job_attempt_receipts_receipt_id_job_id_ordinal_job_kind_event_kind_unique" UNIQUE("receipt_id","job_id","ordinal","job_kind","event_kind"), + CONSTRAINT "job_attempt_receipts_receipt_id_claim_receipt_id_job_id_review_id_review_public_id_event_kind_unique" UNIQUE("receipt_id","claim_receipt_id","job_id","review_id","review_public_id","event_kind"), + CONSTRAINT "job_attempt_receipt_check_1" CHECK (schema_version = 1), + CONSTRAINT "job_attempt_receipt_check_2" CHECK (claim_kind = 'claim'), + CONSTRAINT "job_attempt_receipt_check_3" CHECK (binding_kind = 'review_bound'), + CONSTRAINT "job_attempt_receipt_check_4" CHECK (job_kind IN ('review','review-feedback')), + CONSTRAINT "job_attempt_receipt_check_5" CHECK (event_kind IN ('genesis','claim','review_bound','review_observed','incident','queue_closed')), + CONSTRAINT "job_attempt_receipt_check_6" CHECK (phase IN ('planner','review','scorer','respond')), + CONSTRAINT "job_attempt_receipt_check_7" CHECK (category IN ('providerError','invalidOutput','timeout','deadline')), + CONSTRAINT "job_attempt_receipt_check_8" CHECK (recovery IN ('repair','fallback')), + CONSTRAINT "job_attempt_receipt_check_9" CHECK (isfinite(occurred_at)), + CONSTRAINT "job_attempt_receipt_check_10" CHECK ( + (event_kind = 'genesis' AND receipt_id = 0 AND source_epoch IS NOT NULL + AND predecessor_id IS NULL AND claim_receipt_id IS NULL AND binding_receipt_id IS NULL + AND job_id IS NULL AND ordinal IS NULL AND job_kind IS NULL AND review_id IS NULL + AND review_public_id IS NULL AND outcome IS NULL AND phase IS NULL AND category IS NULL + AND recovered IS NULL AND recovery IS NULL) + OR + (event_kind <> 'genesis' AND receipt_id > 0 AND source_epoch IS NULL + AND predecessor_id IS NOT NULL AND predecessor_id < receipt_id + AND claim_receipt_id IS NOT NULL AND claim_receipt_id <= receipt_id + AND job_id IS NOT NULL AND job_id > 0 AND ordinal IS NOT NULL AND ordinal > 0 + AND job_kind IS NOT NULL AND outcome IS NOT NULL) + ), + CONSTRAINT "job_attempt_receipt_check_11" CHECK ((review_id IS NULL) = (review_public_id IS NULL)), + CONSTRAINT "job_attempt_receipt_check_12" CHECK (review_id IS NULL OR review_id > 0), + CONSTRAINT "job_attempt_receipt_check_13" CHECK ( + (event_kind = 'genesis') OR + (event_kind = 'claim' AND claim_receipt_id = receipt_id AND review_id IS NULL AND outcome = 'started') OR + (event_kind = 'review_bound' AND review_id IS NOT NULL AND outcome = 'bound') OR + (event_kind = 'review_observed' AND review_id IS NOT NULL AND outcome IN ('staged','completed','failed','interrupted','cancelled')) OR + (event_kind = 'incident' AND review_id IS NOT NULL AND outcome = 'observed') OR + (event_kind = 'queue_closed' AND review_id IS NULL AND outcome IN ('succeeded','retry','failed','continued','interrupted','cancelled','coalesced','deferred')) + ), + CONSTRAINT "job_attempt_receipt_check_14" CHECK ((binding_receipt_id IS NOT NULL) = (event_kind IN ('review_observed','incident'))), + CONSTRAINT "job_attempt_receipt_check_15" CHECK ( + (event_kind = 'incident' AND phase IS NOT NULL AND category IS NOT NULL AND recovered IS NOT NULL + AND recovered = (recovery IS NOT NULL)) + OR (event_kind <> 'incident' AND phase IS NULL AND category IS NULL AND recovered IS NULL AND recovery IS NULL) + ) +); +--> statement-breakpoint +ALTER TABLE "jobs" ADD COLUMN "active_claim_receipt_id" bigint;--> statement-breakpoint +ALTER TABLE "jobs" ADD COLUMN "staged_review_id" bigint;--> statement-breakpoint +ALTER TABLE "reviews" ADD COLUMN "origin_claim_receipt_id" bigint;--> statement-breakpoint +ALTER TABLE "job_attempt_receipts" ADD CONSTRAINT "job_attempt_receipts_predecessor_id_job_attempt_receipts_receipt_id_fk" FOREIGN KEY ("predecessor_id") REFERENCES "public"."job_attempt_receipts"("receipt_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "job_attempt_receipts" ADD CONSTRAINT "job_attempt_receipts_claim_receipt_id_job_id_ordinal_job_kind_claim_kind_job_attempt_receipts_receipt_id_job_id_ordinal_job_kind_event_kind_fk" FOREIGN KEY ("claim_receipt_id","job_id","ordinal","job_kind","claim_kind") REFERENCES "public"."job_attempt_receipts"("receipt_id","job_id","ordinal","job_kind","event_kind") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "job_attempt_receipts" ADD CONSTRAINT "job_attempt_receipts_binding_receipt_id_claim_receipt_id_job_id_review_id_review_public_id_binding_kind_job_attempt_receipts_receipt_id_claim_receipt_id_job_id_review_id_review_public_id_event_kind_fk" FOREIGN KEY ("binding_receipt_id","claim_receipt_id","job_id","review_id","review_public_id","binding_kind") REFERENCES "public"."job_attempt_receipts"("receipt_id","claim_receipt_id","job_id","review_id","review_public_id","event_kind") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +CREATE UNIQUE INDEX "job_attempt_one_closure" ON "job_attempt_receipts" USING btree ("claim_receipt_id") WHERE "job_attempt_receipts"."event_kind" = 'queue_closed';--> statement-breakpoint +CREATE UNIQUE INDEX "job_attempt_one_binding" ON "job_attempt_receipts" USING btree ("claim_receipt_id","review_id") WHERE "job_attempt_receipts"."event_kind" = 'review_bound';--> statement-breakpoint +CREATE UNIQUE INDEX "job_attempt_one_observation" ON "job_attempt_receipts" USING btree ("claim_receipt_id","review_id","outcome") WHERE "job_attempt_receipts"."event_kind" = 'review_observed';--> statement-breakpoint +ALTER TABLE "jobs" ADD CONSTRAINT "jobs_active_claim_receipt_id_job_attempt_receipts_receipt_id_fk" FOREIGN KEY ("active_claim_receipt_id") REFERENCES "public"."job_attempt_receipts"("receipt_id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "reviews" ADD CONSTRAINT "reviews_origin_claim_receipt_id_job_attempt_receipts_receipt_id_fk" FOREIGN KEY ("origin_claim_receipt_id") REFERENCES "public"."job_attempt_receipts"("receipt_id") ON DELETE no action ON UPDATE no action; +--> statement-breakpoint +INSERT INTO job_attempt_receipts(receipt_id,source_epoch,event_kind) VALUES (0,gen_random_uuid(),'genesis'); + +CREATE FUNCTION reject_receipt_mutation() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN RAISE EXCEPTION 'immutable receipt'; END $$; +CREATE TRIGGER job_attempt_receipts_immutable BEFORE UPDATE OR DELETE ON job_attempt_receipts +FOR EACH ROW EXECUTE FUNCTION reject_receipt_mutation(); +CREATE TRIGGER job_attempt_receipts_no_truncate BEFORE TRUNCATE ON job_attempt_receipts +FOR EACH STATEMENT EXECUTE FUNCTION reject_receipt_mutation(); + +CREATE FUNCTION validate_receipt_append() RETURNS trigger LANGUAGE plpgsql AS $$ +DECLARE current_head bigint; origin_job bigint; +BEGIN + IF NEW.event_kind = 'genesis' OR current_setting('transaction_isolation') <> 'read committed' + OR NOT EXISTS (SELECT 1 FROM pg_locks WHERE pid = pg_backend_pid() AND locktype = 'advisory' + AND classid = 17491 AND objid = 1 AND objsubid = 2 AND granted) + OR NOT EXISTS (SELECT 1 FROM pg_sequence WHERE seqrelid = 'job_attempt_receipt_ids'::regclass + AND seqcache = 1 AND NOT seqcycle AND seqincrement = 1) + THEN RAISE EXCEPTION 'invalid receipt append'; END IF; + SELECT receipt_id INTO current_head FROM job_attempt_receipts ORDER BY receipt_id DESC LIMIT 1; + IF NEW.predecessor_id IS DISTINCT FROM current_head THEN RAISE EXCEPTION 'invalid receipt predecessor'; END IF; + IF NEW.event_kind = 'claim' AND NOT EXISTS ( + SELECT 1 FROM jobs WHERE id = NEW.job_id AND kind = NEW.job_kind + AND status = 'running' AND attempts = NEW.ordinal AND active_claim_receipt_id IS NULL + ) THEN RAISE EXCEPTION 'invalid receipt claim'; END IF; + IF NEW.event_kind = 'review_bound' THEN + SELECT c.job_id INTO origin_job FROM reviews r JOIN job_attempt_receipts c + ON c.receipt_id = r.origin_claim_receipt_id AND c.event_kind = 'claim' + WHERE r.id = NEW.review_id AND r.public_id = NEW.review_public_id; + IF origin_job IS DISTINCT FROM NEW.job_id OR NOT EXISTS ( + SELECT 1 FROM reviews r JOIN jobs j ON j.id = NEW.job_id + WHERE r.id = NEW.review_id AND r.public_id = NEW.review_public_id + AND (r.origin_claim_receipt_id = NEW.claim_receipt_id OR j.staged_review_id = r.id) + ) THEN RAISE EXCEPTION 'invalid review binding'; END IF; + END IF; + RETURN NEW; +END $$; +CREATE TRIGGER job_attempt_receipts_append BEFORE INSERT ON job_attempt_receipts +FOR EACH ROW EXECUTE FUNCTION validate_receipt_append(); + +CREATE FUNCTION validate_job_receipt_pointers() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF NEW.active_claim_receipt_id IS NOT NULL AND NEW.active_claim_receipt_id IS DISTINCT FROM OLD.active_claim_receipt_id AND NOT EXISTS ( + SELECT 1 FROM job_attempt_receipts c WHERE c.receipt_id = NEW.active_claim_receipt_id + AND c.event_kind = 'claim' AND c.job_id = NEW.id AND c.job_kind = NEW.kind + AND c.ordinal = NEW.attempts AND NEW.status = 'running' + ) THEN RAISE EXCEPTION 'invalid active claim'; END IF; + IF NEW.staged_review_id IS NOT NULL AND NEW.staged_review_id IS DISTINCT FROM OLD.staged_review_id AND NOT EXISTS ( + SELECT 1 FROM reviews r JOIN job_attempt_receipts b ON b.review_id = r.id + AND b.review_public_id = r.public_id AND b.event_kind = 'review_bound' + WHERE r.id = NEW.staged_review_id AND b.job_id = NEW.id AND b.claim_receipt_id = NEW.active_claim_receipt_id + ) THEN RAISE EXCEPTION 'invalid staged review'; END IF; + RETURN NEW; +END $$; +CREATE TRIGGER jobs_receipt_pointers BEFORE UPDATE ON jobs FOR EACH ROW EXECUTE FUNCTION validate_job_receipt_pointers(); + +CREATE FUNCTION validate_review_origin() RETURNS trigger LANGUAGE plpgsql AS $$ +BEGIN + IF TG_OP = 'UPDATE' AND NEW.origin_claim_receipt_id IS DISTINCT FROM OLD.origin_claim_receipt_id THEN + RAISE EXCEPTION 'immutable review origin'; + END IF; + IF NEW.origin_claim_receipt_id IS NOT NULL AND NOT EXISTS ( + SELECT 1 FROM job_attempt_receipts c WHERE c.receipt_id = NEW.origin_claim_receipt_id AND c.event_kind = 'claim' + ) THEN RAISE EXCEPTION 'invalid review origin'; END IF; + RETURN NEW; +END $$; +CREATE TRIGGER reviews_receipt_origin BEFORE INSERT OR UPDATE ON reviews +FOR EACH ROW EXECUTE FUNCTION validate_review_origin(); +--> statement-breakpoint diff --git a/drizzle/meta/0045_snapshot.json b/drizzle/meta/0045_snapshot.json new file mode 100644 index 00000000..75ced9ff --- /dev/null +++ b/drizzle/meta/0045_snapshot.json @@ -0,0 +1,6866 @@ +{ + "id": "c1d6cc9b-79c6-4f08-8848-513a01173f51", + "prevId": "2c6b2daf-e966-4b44-b64a-96ed676bede2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.billing_author_settlements": { + "name": "billing_author_settlements", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "period_starts_at": { + "name": "period_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "period_ends_at": { + "name": "period_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "active_author_count": { + "name": "active_author_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "unit_amount_cents": { + "name": "unit_amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 600 + }, + "total_amount_cents": { + "name": "total_amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attempt_started_at": { + "name": "attempt_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_reconcile_at": { + "name": "next_reconcile_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_category": { + "name": "last_error_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_author_settlements_org_period_idx": { + "name": "billing_author_settlements_org_period_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_starts_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "period_ends_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_author_settlements_provider_transaction_idx": { + "name": "billing_author_settlements_provider_transaction_idx", + "columns": [ + { + "expression": "provider_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_author_settlements_status_reconcile_idx": { + "name": "billing_author_settlements_status_reconcile_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "next_reconcile_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_author_settlements_org_id_organizations_id_fk": { + "name": "billing_author_settlements_org_id_organizations_id_fk", + "tableFrom": "billing_author_settlements", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_author_settlements_period_check": { + "name": "billing_author_settlements_period_check", + "value": "\"billing_author_settlements\".\"period_starts_at\" < \"billing_author_settlements\".\"period_ends_at\"" + }, + "billing_author_settlements_author_count_check": { + "name": "billing_author_settlements_author_count_check", + "value": "\"billing_author_settlements\".\"active_author_count\" >= 0" + }, + "billing_author_settlements_amount_check": { + "name": "billing_author_settlements_amount_check", + "value": "\"billing_author_settlements\".\"unit_amount_cents\" = 600 AND \"billing_author_settlements\".\"total_amount_cents\" = \"billing_author_settlements\".\"active_author_count\" * \"billing_author_settlements\".\"unit_amount_cents\"" + }, + "billing_author_settlements_status_check": { + "name": "billing_author_settlements_status_check", + "value": "\"billing_author_settlements\".\"status\" IN ('pending', 'charging', 'reconciling', 'charged', 'no_charge', 'failed')" + }, + "billing_author_settlements_attempt_count_check": { + "name": "billing_author_settlements_attempt_count_check", + "value": "\"billing_author_settlements\".\"attempt_count\" >= 0" + }, + "billing_author_settlements_subscription_nonempty": { + "name": "billing_author_settlements_subscription_nonempty", + "value": "length(btrim(\"billing_author_settlements\".\"provider_subscription_id\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.billing_checkout_transactions": { + "name": "billing_checkout_transactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paddle'" + }, + "provider_transaction_id": { + "name": "provider_transaction_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_url": { + "name": "checkout_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'creating'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_error_category": { + "name": "last_error_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_checkout_transactions_provider_transaction_idx": { + "name": "billing_checkout_transactions_provider_transaction_idx", + "columns": [ + { + "expression": "provider_transaction_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_checkout_transactions_open_org_idx": { + "name": "billing_checkout_transactions_open_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"billing_checkout_transactions\".\"status\" IN ('creating', 'pending')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_checkout_transactions_status_expiry_idx": { + "name": "billing_checkout_transactions_status_expiry_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_checkout_transactions_org_id_organizations_id_fk": { + "name": "billing_checkout_transactions_org_id_organizations_id_fk", + "tableFrom": "billing_checkout_transactions", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "billing_checkout_transactions_requested_by_user_id_users_id_fk": { + "name": "billing_checkout_transactions_requested_by_user_id_users_id_fk", + "tableFrom": "billing_checkout_transactions", + "tableTo": "users", + "columnsFrom": [ + "requested_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_checkout_transactions_provider_check": { + "name": "billing_checkout_transactions_provider_check", + "value": "\"billing_checkout_transactions\".\"provider\" = 'paddle'" + }, + "billing_checkout_transactions_status_check": { + "name": "billing_checkout_transactions_status_check", + "value": "\"billing_checkout_transactions\".\"status\" IN ('creating', 'pending', 'completed', 'failed', 'expired', 'canceled')" + }, + "billing_checkout_transactions_provider_transaction_nonempty": { + "name": "billing_checkout_transactions_provider_transaction_nonempty", + "value": "\"billing_checkout_transactions\".\"provider_transaction_id\" IS NULL OR length(btrim(\"billing_checkout_transactions\".\"provider_transaction_id\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.billing_credit_grants": { + "name": "billing_credit_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "billing_credit_grants_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor": { + "name": "actor", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'admin_script'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applies_at": { + "name": "applies_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_credit_grants_org_created_idx": { + "name": "billing_credit_grants_org_created_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_credit_grants_org_idempotency_idx": { + "name": "billing_credit_grants_org_idempotency_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_credit_grants_org_id_organizations_id_fk": { + "name": "billing_credit_grants_org_id_organizations_id_fk", + "tableFrom": "billing_credit_grants", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_credit_grants_amount_cents_positive": { + "name": "billing_credit_grants_amount_cents_positive", + "value": "\"billing_credit_grants\".\"amount_cents\" > 0" + }, + "billing_credit_grants_reason_nonempty": { + "name": "billing_credit_grants_reason_nonempty", + "value": "length(btrim(\"billing_credit_grants\".\"reason\")) > 0" + }, + "billing_credit_grants_actor_nonempty": { + "name": "billing_credit_grants_actor_nonempty", + "value": "length(btrim(\"billing_credit_grants\".\"actor\")) > 0" + }, + "billing_credit_grants_source_nonempty": { + "name": "billing_credit_grants_source_nonempty", + "value": "length(btrim(\"billing_credit_grants\".\"source\")) > 0" + }, + "billing_credit_grants_idempotency_key_nonempty": { + "name": "billing_credit_grants_idempotency_key_nonempty", + "value": "length(btrim(\"billing_credit_grants\".\"idempotency_key\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.billing_provider_events": { + "name": "billing_provider_events", + "schema": "", + "columns": { + "event_id": { + "name": "event_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paddle'" + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_object_id": { + "name": "provider_object_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "processed_at": { + "name": "processed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_provider_events_org_occurred_idx": { + "name": "billing_provider_events_org_occurred_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_provider_events_type_occurred_idx": { + "name": "billing_provider_events_type_occurred_idx", + "columns": [ + { + "expression": "event_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_provider_events_org_id_organizations_id_fk": { + "name": "billing_provider_events_org_id_organizations_id_fk", + "tableFrom": "billing_provider_events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_provider_events_provider_check": { + "name": "billing_provider_events_provider_check", + "value": "\"billing_provider_events\".\"provider\" = 'paddle'" + }, + "billing_provider_events_outcome_check": { + "name": "billing_provider_events_outcome_check", + "value": "\"billing_provider_events\".\"outcome\" IN ('processing', 'applied', 'stale', 'ignored', 'unmatched')" + }, + "billing_provider_events_event_type_nonempty": { + "name": "billing_provider_events_event_type_nonempty", + "value": "length(btrim(\"billing_provider_events\".\"event_type\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.billing_provider_subscriptions": { + "name": "billing_provider_subscriptions", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paddle'" + }, + "provider_subscription_id": { + "name": "provider_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_customer_id": { + "name": "provider_customer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_period_starts_at": { + "name": "current_period_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "current_period_ends_at": { + "name": "current_period_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "latest_event_occurred_at": { + "name": "latest_event_occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "latest_event_id": { + "name": "latest_event_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "billing_provider_subscriptions_provider_id_idx": { + "name": "billing_provider_subscriptions_provider_id_idx", + "columns": [ + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "billing_provider_subscriptions_status_period_idx": { + "name": "billing_provider_subscriptions_status_period_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "current_period_ends_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "billing_provider_subscriptions_org_id_organizations_id_fk": { + "name": "billing_provider_subscriptions_org_id_organizations_id_fk", + "tableFrom": "billing_provider_subscriptions", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "billing_provider_subscriptions_provider_check": { + "name": "billing_provider_subscriptions_provider_check", + "value": "\"billing_provider_subscriptions\".\"provider\" = 'paddle'" + }, + "billing_provider_subscriptions_status_check": { + "name": "billing_provider_subscriptions_status_check", + "value": "\"billing_provider_subscriptions\".\"status\" IN ('active', 'trialing', 'past_due', 'paused', 'canceled')" + }, + "billing_provider_subscriptions_provider_subscription_nonempty": { + "name": "billing_provider_subscriptions_provider_subscription_nonempty", + "value": "length(btrim(\"billing_provider_subscriptions\".\"provider_subscription_id\")) > 0" + }, + "billing_provider_subscriptions_provider_customer_nonempty": { + "name": "billing_provider_subscriptions_provider_customer_nonempty", + "value": "length(btrim(\"billing_provider_subscriptions\".\"provider_customer_id\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.customer_notification_email_deliveries": { + "name": "customer_notification_email_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "email_category": { + "name": "email_category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "event_count": { + "name": "event_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "customer_notification_email_deliveries_status_created_idx": { + "name": "customer_notification_email_deliveries_status_created_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customer_notification_email_deliveries_org_created_idx": { + "name": "customer_notification_email_deliveries_org_created_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customer_notification_email_deliveries_org_id_organizations_id_fk": { + "name": "customer_notification_email_deliveries_org_id_organizations_id_fk", + "tableFrom": "customer_notification_email_deliveries", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "customer_notification_email_deliveries_category_check": { + "name": "customer_notification_email_deliveries_category_check", + "value": "\"customer_notification_email_deliveries\".\"email_category\" IN ('security', 'payment_failure', 'trial_expiry', 'service_incident', 'billing_summary')" + }, + "customer_notification_email_deliveries_status_check": { + "name": "customer_notification_email_deliveries_status_check", + "value": "\"customer_notification_email_deliveries\".\"status\" IN ('queued', 'retrying', 'sending', 'delivered', 'suppressed', 'failed')" + }, + "customer_notification_email_deliveries_event_count_check": { + "name": "customer_notification_email_deliveries_event_count_check", + "value": "\"customer_notification_email_deliveries\".\"event_count\" BETWEEN 1 AND 20" + } + }, + "isRLSEnabled": false + }, + "public.customer_notification_email_delivery_events": { + "name": "customer_notification_email_delivery_events", + "schema": "", + "columns": { + "event_id": { + "name": "event_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "delivery_id": { + "name": "delivery_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "customer_notification_email_delivery_events_delivery_idx": { + "name": "customer_notification_email_delivery_events_delivery_idx", + "columns": [ + { + "expression": "delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customer_notification_email_delivery_events_delivery_id_customer_notification_email_deliveries_id_fk": { + "name": "customer_notification_email_delivery_events_delivery_id_customer_notification_email_deliveries_id_fk", + "tableFrom": "customer_notification_email_delivery_events", + "tableTo": "customer_notification_email_deliveries", + "columnsFrom": [ + "delivery_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.customer_notification_events": { + "name": "customer_notification_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "customer_notification_events_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action_label": { + "name": "action_label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "action_href": { + "name": "action_href", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "customer_notification_events_org_key_idx": { + "name": "customer_notification_events_org_key_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customer_notification_events_org_created_idx": { + "name": "customer_notification_events_org_created_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customer_notification_events_expiry_idx": { + "name": "customer_notification_events_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customer_notification_events_org_id_organizations_id_fk": { + "name": "customer_notification_events_org_id_organizations_id_fk", + "tableFrom": "customer_notification_events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "customer_notification_events_severity_check": { + "name": "customer_notification_events_severity_check", + "value": "\"customer_notification_events\".\"severity\" IN ('info', 'warning', 'critical')" + }, + "customer_notification_events_category_check": { + "name": "customer_notification_events_category_check", + "value": "\"customer_notification_events\".\"category\" IN ('trial', 'billing', 'service', 'security')" + }, + "customer_notification_events_visibility_check": { + "name": "customer_notification_events_visibility_check", + "value": "\"customer_notification_events\".\"visibility\" IN ('members', 'admins')" + }, + "customer_notification_events_content_check": { + "name": "customer_notification_events_content_check", + "value": "length(btrim(\"customer_notification_events\".\"idempotency_key\")) BETWEEN 1 AND 200 AND length(btrim(\"customer_notification_events\".\"title\")) BETWEEN 1 AND 120 AND length(btrim(\"customer_notification_events\".\"body\")) BETWEEN 1 AND 500" + }, + "customer_notification_events_action_check": { + "name": "customer_notification_events_action_check", + "value": "(\"customer_notification_events\".\"action_label\" IS NULL AND \"customer_notification_events\".\"action_href\" IS NULL) OR (\"customer_notification_events\".\"action_label\" IS NOT NULL AND \"customer_notification_events\".\"action_href\" IS NOT NULL AND length(btrim(\"customer_notification_events\".\"action_label\")) BETWEEN 1 AND 60 AND \"customer_notification_events\".\"action_href\" ~ '^/orgs/')" + }, + "customer_notification_events_expiry_check": { + "name": "customer_notification_events_expiry_check", + "value": "\"customer_notification_events\".\"expires_at\" > \"customer_notification_events\".\"created_at\"" + } + }, + "isRLSEnabled": false + }, + "public.customer_notification_reads": { + "name": "customer_notification_reads", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "customer_notification_reads_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "event_id": { + "name": "event_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "read_at": { + "name": "read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "customer_notification_reads_event_user_idx": { + "name": "customer_notification_reads_event_user_idx", + "columns": [ + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "customer_notification_reads_user_event_idx": { + "name": "customer_notification_reads_user_event_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "customer_notification_reads_event_id_customer_notification_events_id_fk": { + "name": "customer_notification_reads_event_id_customer_notification_events_id_fk", + "tableFrom": "customer_notification_reads", + "tableTo": "customer_notification_events", + "columnsFrom": [ + "event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "customer_notification_reads_user_id_users_id_fk": { + "name": "customer_notification_reads_user_id_users_id_fk", + "tableFrom": "customer_notification_reads", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finding_approvals": { + "name": "finding_approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "actor_github_id": { + "name": "actor_github_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_login_snapshot": { + "name": "actor_login_snapshot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_role_snapshot": { + "name": "actor_role_snapshot", + "type": "finding_approval_role", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "rationale": { + "name": "rationale", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "finding_approval_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source_comment_id": { + "name": "source_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_url": { + "name": "source_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_org_id": { + "name": "source_org_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_repository_id": { + "name": "source_repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_github_installation_id": { + "name": "source_github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_github_repo_id": { + "name": "source_github_repo_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_pr_number": { + "name": "source_pr_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "source_head_sha": { + "name": "source_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_webhook_delivery_id": { + "name": "source_webhook_delivery_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_github_comment_id": { + "name": "source_github_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_comment_kind": { + "name": "source_comment_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_binding_state": { + "name": "source_binding_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'exact'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_by_user_id": { + "name": "revoked_by_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "finding_approvals_active_idx": { + "name": "finding_approvals_active_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"finding_approvals\".\"revoked_at\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "finding_approvals_github_comment_idx": { + "name": "finding_approvals_github_comment_idx", + "columns": [ + { + "expression": "source_github_installation_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_github_repo_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_comment_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_github_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"finding_approvals\".\"source\" = 'github'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "finding_approvals_github_delivery_idx": { + "name": "finding_approvals_github_delivery_idx", + "columns": [ + { + "expression": "source_webhook_delivery_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"finding_approvals\".\"source\" = 'github'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "finding_approvals_review_idx": { + "name": "finding_approvals_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finding_approvals_review_id_reviews_id_fk": { + "name": "finding_approvals_review_id_reviews_id_fk", + "tableFrom": "finding_approvals", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "finding_approvals_actor_user_id_users_id_fk": { + "name": "finding_approvals_actor_user_id_users_id_fk", + "tableFrom": "finding_approvals", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "finding_approvals_revoked_by_user_id_users_id_fk": { + "name": "finding_approvals_revoked_by_user_id_users_id_fk", + "tableFrom": "finding_approvals", + "tableTo": "users", + "columnsFrom": [ + "revoked_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "finding_approvals_rationale_nonempty": { + "name": "finding_approvals_rationale_nonempty", + "value": "length(btrim(\"finding_approvals\".\"rationale\")) > 0" + }, + "finding_approvals_binding_check": { + "name": "finding_approvals_binding_check", + "value": "(\"finding_approvals\".\"source_binding_state\" = 'legacy' AND \"finding_approvals\".\"source_org_id\" IS NULL AND \"finding_approvals\".\"source_repository_id\" IS NULL AND \"finding_approvals\".\"source_github_installation_id\" IS NULL AND \"finding_approvals\".\"source_github_repo_id\" IS NULL AND \"finding_approvals\".\"source_pr_number\" IS NULL AND \"finding_approvals\".\"source_head_sha\" IS NULL) OR (\"finding_approvals\".\"source_binding_state\" = 'exact' AND \"finding_approvals\".\"source_org_id\" > 0 AND \"finding_approvals\".\"source_repository_id\" > 0 AND \"finding_approvals\".\"source_github_installation_id\" > 0 AND \"finding_approvals\".\"source_github_repo_id\" > 0 AND \"finding_approvals\".\"source_pr_number\" > 0 AND length(btrim(\"finding_approvals\".\"source_head_sha\")) BETWEEN 1 AND 200)" + }, + "finding_approvals_github_source_check": { + "name": "finding_approvals_github_source_check", + "value": "\"finding_approvals\".\"source\" <> 'github' OR (\"finding_approvals\".\"source_webhook_delivery_id\" IS NULL AND \"finding_approvals\".\"source_github_comment_id\" IS NULL AND \"finding_approvals\".\"source_comment_kind\" IS NULL) OR (length(btrim(\"finding_approvals\".\"source_webhook_delivery_id\")) BETWEEN 1 AND 200 AND \"finding_approvals\".\"source_github_comment_id\" > 0 AND \"finding_approvals\".\"source_comment_kind\" IN ('issue_comment', 'pull_request_review_comment'))" + } + }, + "isRLSEnabled": false + }, + "public.finding_publications": { + "name": "finding_publications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "finding_publications_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "finding_id": { + "name": "finding_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stable_identity": { + "name": "stable_identity", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "initial_state": { + "name": "initial_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "current_state": { + "name": "current_state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_comment_id": { + "name": "github_comment_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lifecycle_observed_at": { + "name": "lifecycle_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "finding_publications_review_finding_idx": { + "name": "finding_publications_review_finding_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finding_publications_comment_idx": { + "name": "finding_publications_comment_idx", + "columns": [ + { + "expression": "github_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finding_publications_stable_finding_idx": { + "name": "finding_publications_stable_finding_idx", + "columns": [ + { + "expression": "finding_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stable_identity", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finding_publications_review_id_reviews_id_fk": { + "name": "finding_publications_review_id_reviews_id_fk", + "tableFrom": "finding_publications", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "finding_publications_finding_id_check": { + "name": "finding_publications_finding_id_check", + "value": "length(btrim(\"finding_publications\".\"finding_id\")) BETWEEN 1 AND 500" + }, + "finding_publications_initial_state_check": { + "name": "finding_publications_initial_state_check", + "value": "\"finding_publications\".\"initial_state\" IN ('inline', 'summaryOnly', 'carried', 'resolved', 'suppressed', 'inlineRejected', 'unknown')" + }, + "finding_publications_current_state_check": { + "name": "finding_publications_current_state_check", + "value": "\"finding_publications\".\"current_state\" IN ('inline', 'summaryOnly', 'carried', 'resolved', 'suppressed', 'inlineRejected', 'outdated', 'deleted', 'unknown')" + }, + "finding_publications_github_comment_id_check": { + "name": "finding_publications_github_comment_id_check", + "value": "\"finding_publications\".\"github_comment_id\" IS NULL OR \"finding_publications\".\"github_comment_id\" ~ '^[1-9][0-9]{0,19}$'" + } + }, + "isRLSEnabled": false + }, + "public.github_webhook_delivery_recoveries": { + "name": "github_webhook_delivery_recoveries", + "schema": "", + "columns": { + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "delivery_guid": { + "name": "delivery_guid", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redelivery": { + "name": "redelivery", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status_code": { + "name": "status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "request_state": { + "name": "request_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_attempts": { + "name": "request_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_requested_at": { + "name": "last_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "request_status_code": { + "name": "request_status_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "recovery_delivery_id": { + "name": "recovery_delivery_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error_category": { + "name": "last_error_category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "github_webhook_delivery_recoveries_guid_idx": { + "name": "github_webhook_delivery_recoveries_guid_idx", + "columns": [ + { + "expression": "delivery_guid", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "github_webhook_delivery_recoveries_retry_idx": { + "name": "github_webhook_delivery_recoveries_retry_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivered_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"github_webhook_delivery_recoveries\".\"outcome\" = 'failure' AND \"github_webhook_delivery_recoveries\".\"recovery_delivery_id\" IS NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_webhook_delivery_recoveries_outcome_check": { + "name": "github_webhook_delivery_recoveries_outcome_check", + "value": "\"github_webhook_delivery_recoveries\".\"outcome\" IN ('success', 'failure', 'pending')" + }, + "github_webhook_delivery_recoveries_request_state_check": { + "name": "github_webhook_delivery_recoveries_request_state_check", + "value": "\"github_webhook_delivery_recoveries\".\"request_state\" IS NULL OR \"github_webhook_delivery_recoveries\".\"request_state\" IN ('requesting', 'retryable', 'accepted', 'terminal', 'exhausted', 'recovered')" + }, + "github_webhook_delivery_recoveries_attempts_check": { + "name": "github_webhook_delivery_recoveries_attempts_check", + "value": "\"github_webhook_delivery_recoveries\".\"request_attempts\" >= 0 AND \"github_webhook_delivery_recoveries\".\"request_attempts\" <= 2" + } + }, + "isRLSEnabled": false + }, + "public.github_webhook_redelivery_state": { + "name": "github_webhook_redelivery_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "cursor": { + "name": "cursor", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_owner": { + "name": "lease_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "sweep_started_at": { + "name": "sweep_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_page_at": { + "name": "last_page_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_sweep_completed_at": { + "name": "last_sweep_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rate_limited_until": { + "name": "rate_limited_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error_category": { + "name": "last_error_category", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "github_webhook_redelivery_state_singleton_check": { + "name": "github_webhook_redelivery_state_singleton_check", + "value": "\"github_webhook_redelivery_state\".\"id\" = 1" + } + }, + "isRLSEnabled": false + }, + "public.hosted_usage_reservations": { + "name": "hosted_usage_reservations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "operation": { + "name": "operation", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'review'" + }, + "reserved_micros": { + "name": "reserved_micros", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "actual_micros": { + "name": "actual_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "hosted_usage_reservations_review_idx": { + "name": "hosted_usage_reservations_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hosted_usage_reservations_active_org_expiry_idx": { + "name": "hosted_usage_reservations_active_org_expiry_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"hosted_usage_reservations\".\"status\" = 'active'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "hosted_usage_reservations_org_id_organizations_id_fk": { + "name": "hosted_usage_reservations_org_id_organizations_id_fk", + "tableFrom": "hosted_usage_reservations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "hosted_usage_reservations_review_id_reviews_id_fk": { + "name": "hosted_usage_reservations_review_id_reviews_id_fk", + "tableFrom": "hosted_usage_reservations", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "hosted_usage_reservations_status_check": { + "name": "hosted_usage_reservations_status_check", + "value": "\"hosted_usage_reservations\".\"status\" IN ('active', 'reconciled', 'released')" + }, + "hosted_usage_reservations_operation_check": { + "name": "hosted_usage_reservations_operation_check", + "value": "\"hosted_usage_reservations\".\"operation\" IN ('review', 'respond')" + }, + "hosted_usage_reservations_operation_reference_check": { + "name": "hosted_usage_reservations_operation_reference_check", + "value": "(\"hosted_usage_reservations\".\"operation\" = 'review' AND \"hosted_usage_reservations\".\"review_id\" IS NOT NULL) OR (\"hosted_usage_reservations\".\"operation\" = 'respond' AND \"hosted_usage_reservations\".\"review_id\" IS NULL)" + }, + "hosted_usage_reservations_reserved_positive": { + "name": "hosted_usage_reservations_reserved_positive", + "value": "\"hosted_usage_reservations\".\"reserved_micros\" > 0" + }, + "hosted_usage_reservations_actual_nonnegative": { + "name": "hosted_usage_reservations_actual_nonnegative", + "value": "\"hosted_usage_reservations\".\"actual_micros\" IS NULL OR \"hosted_usage_reservations\".\"actual_micros\" >= 0" + } + }, + "isRLSEnabled": false + }, + "public.installations": { + "name": "installations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "installations_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "account_login": { + "name": "account_login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "account_type": { + "name": "account_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "suspended": { + "name": "suspended", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "installations_org_idx": { + "name": "installations_org_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "installations_org_id_organizations_id_fk": { + "name": "installations_org_id_organizations_id_fk", + "tableFrom": "installations", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "installations_github_installation_id_unique": { + "name": "installations_github_installation_id_unique", + "nullsNotDistinct": false, + "columns": [ + "github_installation_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.job_attempt_receipts": { + "name": "job_attempt_receipts", + "schema": "", + "columns": { + "receipt_id": { + "name": "receipt_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "predecessor_id": { + "name": "predecessor_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_epoch": { + "name": "source_epoch", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "claim_receipt_id": { + "name": "claim_receipt_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "claim_kind": { + "name": "claim_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claim'" + }, + "binding_receipt_id": { + "name": "binding_receipt_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "binding_kind": { + "name": "binding_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'review_bound'" + }, + "job_id": { + "name": "job_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "ordinal": { + "name": "ordinal", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "job_kind": { + "name": "job_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "review_public_id": { + "name": "review_public_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "recovered": { + "name": "recovered", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "recovery": { + "name": "recovery", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "clock_timestamp()" + } + }, + "indexes": { + "job_attempt_one_closure": { + "name": "job_attempt_one_closure", + "columns": [ + { + "expression": "claim_receipt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"job_attempt_receipts\".\"event_kind\" = 'queue_closed'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_attempt_one_binding": { + "name": "job_attempt_one_binding", + "columns": [ + { + "expression": "claim_receipt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"job_attempt_receipts\".\"event_kind\" = 'review_bound'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "job_attempt_one_observation": { + "name": "job_attempt_one_observation", + "columns": [ + { + "expression": "claim_receipt_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "outcome", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"job_attempt_receipts\".\"event_kind\" = 'review_observed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "job_attempt_receipts_predecessor_id_job_attempt_receipts_receipt_id_fk": { + "name": "job_attempt_receipts_predecessor_id_job_attempt_receipts_receipt_id_fk", + "tableFrom": "job_attempt_receipts", + "tableTo": "job_attempt_receipts", + "columnsFrom": [ + "predecessor_id" + ], + "columnsTo": [ + "receipt_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "job_attempt_receipts_claim_receipt_id_job_id_ordinal_job_kind_claim_kind_job_attempt_receipts_receipt_id_job_id_ordinal_job_kind_event_kind_fk": { + "name": "job_attempt_receipts_claim_receipt_id_job_id_ordinal_job_kind_claim_kind_job_attempt_receipts_receipt_id_job_id_ordinal_job_kind_event_kind_fk", + "tableFrom": "job_attempt_receipts", + "tableTo": "job_attempt_receipts", + "columnsFrom": [ + "claim_receipt_id", + "job_id", + "ordinal", + "job_kind", + "claim_kind" + ], + "columnsTo": [ + "receipt_id", + "job_id", + "ordinal", + "job_kind", + "event_kind" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "job_attempt_receipts_binding_receipt_id_claim_receipt_id_job_id_review_id_review_public_id_binding_kind_job_attempt_receipts_receipt_id_claim_receipt_id_job_id_review_id_review_public_id_event_kind_fk": { + "name": "job_attempt_receipts_binding_receipt_id_claim_receipt_id_job_id_review_id_review_public_id_binding_kind_job_attempt_receipts_receipt_id_claim_receipt_id_job_id_review_id_review_public_id_event_kind_fk", + "tableFrom": "job_attempt_receipts", + "tableTo": "job_attempt_receipts", + "columnsFrom": [ + "binding_receipt_id", + "claim_receipt_id", + "job_id", + "review_id", + "review_public_id", + "binding_kind" + ], + "columnsTo": [ + "receipt_id", + "claim_receipt_id", + "job_id", + "review_id", + "review_public_id", + "event_kind" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "job_attempt_receipts_predecessor_id_unique": { + "name": "job_attempt_receipts_predecessor_id_unique", + "nullsNotDistinct": false, + "columns": [ + "predecessor_id" + ] + }, + "job_attempt_receipts_receipt_id_job_id_ordinal_job_kind_event_kind_unique": { + "name": "job_attempt_receipts_receipt_id_job_id_ordinal_job_kind_event_kind_unique", + "nullsNotDistinct": false, + "columns": [ + "receipt_id", + "job_id", + "ordinal", + "job_kind", + "event_kind" + ] + }, + "job_attempt_receipts_receipt_id_claim_receipt_id_job_id_review_id_review_public_id_event_kind_unique": { + "name": "job_attempt_receipts_receipt_id_claim_receipt_id_job_id_review_id_review_public_id_event_kind_unique", + "nullsNotDistinct": false, + "columns": [ + "receipt_id", + "claim_receipt_id", + "job_id", + "review_id", + "review_public_id", + "event_kind" + ] + } + }, + "policies": {}, + "checkConstraints": { + "job_attempt_receipt_check_1": { + "name": "job_attempt_receipt_check_1", + "value": "schema_version = 1" + }, + "job_attempt_receipt_check_2": { + "name": "job_attempt_receipt_check_2", + "value": "claim_kind = 'claim'" + }, + "job_attempt_receipt_check_3": { + "name": "job_attempt_receipt_check_3", + "value": "binding_kind = 'review_bound'" + }, + "job_attempt_receipt_check_4": { + "name": "job_attempt_receipt_check_4", + "value": "job_kind IN ('review','review-feedback')" + }, + "job_attempt_receipt_check_5": { + "name": "job_attempt_receipt_check_5", + "value": "event_kind IN ('genesis','claim','review_bound','review_observed','incident','queue_closed')" + }, + "job_attempt_receipt_check_6": { + "name": "job_attempt_receipt_check_6", + "value": "phase IN ('planner','review','scorer','respond')" + }, + "job_attempt_receipt_check_7": { + "name": "job_attempt_receipt_check_7", + "value": "category IN ('providerError','invalidOutput','timeout','deadline')" + }, + "job_attempt_receipt_check_8": { + "name": "job_attempt_receipt_check_8", + "value": "recovery IN ('repair','fallback')" + }, + "job_attempt_receipt_check_9": { + "name": "job_attempt_receipt_check_9", + "value": "isfinite(occurred_at)" + }, + "job_attempt_receipt_check_10": { + "name": "job_attempt_receipt_check_10", + "value": "\n (event_kind = 'genesis' AND receipt_id = 0 AND source_epoch IS NOT NULL\n AND predecessor_id IS NULL AND claim_receipt_id IS NULL AND binding_receipt_id IS NULL\n AND job_id IS NULL AND ordinal IS NULL AND job_kind IS NULL AND review_id IS NULL\n AND review_public_id IS NULL AND outcome IS NULL AND phase IS NULL AND category IS NULL\n AND recovered IS NULL AND recovery IS NULL)\n OR\n (event_kind <> 'genesis' AND receipt_id > 0 AND source_epoch IS NULL\n AND predecessor_id IS NOT NULL AND predecessor_id < receipt_id\n AND claim_receipt_id IS NOT NULL AND claim_receipt_id <= receipt_id\n AND job_id IS NOT NULL AND job_id > 0 AND ordinal IS NOT NULL AND ordinal > 0\n AND job_kind IS NOT NULL AND outcome IS NOT NULL)\n " + }, + "job_attempt_receipt_check_11": { + "name": "job_attempt_receipt_check_11", + "value": "(review_id IS NULL) = (review_public_id IS NULL)" + }, + "job_attempt_receipt_check_12": { + "name": "job_attempt_receipt_check_12", + "value": "review_id IS NULL OR review_id > 0" + }, + "job_attempt_receipt_check_13": { + "name": "job_attempt_receipt_check_13", + "value": "\n (event_kind = 'genesis') OR\n (event_kind = 'claim' AND claim_receipt_id = receipt_id AND review_id IS NULL AND outcome = 'started') OR\n (event_kind = 'review_bound' AND review_id IS NOT NULL AND outcome = 'bound') OR\n (event_kind = 'review_observed' AND review_id IS NOT NULL AND outcome IN ('staged','completed','failed','interrupted','cancelled')) OR\n (event_kind = 'incident' AND review_id IS NOT NULL AND outcome = 'observed') OR\n (event_kind = 'queue_closed' AND review_id IS NULL AND outcome IN ('succeeded','retry','failed','continued','interrupted','cancelled','coalesced','deferred'))\n " + }, + "job_attempt_receipt_check_14": { + "name": "job_attempt_receipt_check_14", + "value": "(binding_receipt_id IS NOT NULL) = (event_kind IN ('review_observed','incident'))" + }, + "job_attempt_receipt_check_15": { + "name": "job_attempt_receipt_check_15", + "value": "\n (event_kind = 'incident' AND phase IS NOT NULL AND category IS NOT NULL AND recovered IS NOT NULL\n AND recovered = (recovery IS NOT NULL))\n OR (event_kind <> 'incident' AND phase IS NULL AND category IS NULL AND recovered IS NULL AND recovery IS NULL)\n " + } + }, + "isRLSEnabled": false + }, + "public.jobs": { + "name": "jobs", + "schema": "", + "columns": { + "active_claim_receipt_id": { + "name": "active_claim_receipt_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "staged_review_id": { + "name": "staged_review_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "jobs_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "job_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "max_attempts": { + "name": "max_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 3 + }, + "run_after": { + "name": "run_after", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "locked_at": { + "name": "locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "locked_by": { + "name": "locked_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "jobs_claim_idx": { + "name": "jobs_claim_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_after", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "jobs_running_locked_at_idx": { + "name": "jobs_running_locked_at_idx", + "columns": [ + { + "expression": "locked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"jobs\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "jobs_active_claim_receipt_id_job_attempt_receipts_receipt_id_fk": { + "name": "jobs_active_claim_receipt_id_job_attempt_receipts_receipt_id_fk", + "tableFrom": "jobs", + "tableTo": "job_attempt_receipts", + "columnsFrom": [ + "active_claim_receipt_id" + ], + "columnsTo": [ + "receipt_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.large_review_attempts": { + "name": "large_review_attempts", + "schema": "", + "columns": { + "attempt_key": { + "name": "attempt_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "run_key": { + "name": "run_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request_sha256": { + "name": "request_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "batch_identity": { + "name": "batch_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "attempt": { + "name": "attempt", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lease_id": { + "name": "lease_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_headers": { + "name": "response_headers", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "large_review_attempts_run_request_attempt_idx": { + "name": "large_review_attempts_run_request_attempt_idx", + "columns": [ + { + "expression": "run_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "attempt", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "large_review_attempts_pending_request_idx": { + "name": "large_review_attempts_pending_request_idx", + "columns": [ + { + "expression": "run_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"large_review_attempts\".\"state\" = 'pending'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "large_review_attempts_run_idx": { + "name": "large_review_attempts_run_idx", + "columns": [ + { + "expression": "run_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "large_review_attempts_run_key_large_review_runs_run_key_fk": { + "name": "large_review_attempts_run_key_large_review_runs_run_key_fk", + "tableFrom": "large_review_attempts", + "tableTo": "large_review_runs", + "columnsFrom": [ + "run_key" + ], + "columnsTo": [ + "run_key" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "large_review_attempts_key_check": { + "name": "large_review_attempts_key_check", + "value": "\"large_review_attempts\".\"attempt_key\" ~ '^[0-9a-f]{64}$'" + }, + "large_review_attempts_request_check": { + "name": "large_review_attempts_request_check", + "value": "\"large_review_attempts\".\"request_sha256\" ~ '^[0-9a-f]{64}$' AND \"large_review_attempts\".\"batch_identity\" ~ '^[0-9a-f]{64}$'" + }, + "large_review_attempts_attempt_check": { + "name": "large_review_attempts_attempt_check", + "value": "\"large_review_attempts\".\"attempt\" BETWEEN 1 AND 10" + }, + "large_review_attempts_state_check": { + "name": "large_review_attempts_state_check", + "value": "\"large_review_attempts\".\"state\" IN ('pending', 'completed')" + }, + "large_review_attempts_response_check": { + "name": "large_review_attempts_response_check", + "value": "(\"large_review_attempts\".\"state\" = 'pending' AND \"large_review_attempts\".\"response_status\" IS NULL AND \"large_review_attempts\".\"response_headers\" IS NULL AND \"large_review_attempts\".\"response_body\" IS NULL AND \"large_review_attempts\".\"completed_at\" IS NULL) OR (\"large_review_attempts\".\"state\" = 'completed' AND \"large_review_attempts\".\"response_status\" BETWEEN 200 AND 299 AND \"large_review_attempts\".\"response_headers\" IS NOT NULL AND \"large_review_attempts\".\"response_body\" IS NOT NULL AND \"large_review_attempts\".\"completed_at\" IS NOT NULL)" + }, + "large_review_attempts_model_check": { + "name": "large_review_attempts_model_check", + "value": "length(btrim(\"large_review_attempts\".\"model\")) BETWEEN 1 AND 500" + } + }, + "isRLSEnabled": false + }, + "public.large_review_runs": { + "name": "large_review_runs", + "schema": "", + "columns": { + "run_key": { + "name": "run_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "current_review_id": { + "name": "current_review_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cli_version": { + "name": "cli_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "configuration_sha256": { + "name": "configuration_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_identity": { + "name": "provider_identity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "retry_lineage": { + "name": "retry_lineage", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plan_sha256": { + "name": "plan_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "hosted_reservation_id": { + "name": "hosted_reservation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_state": { + "name": "billing_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "conservatively_settled_at": { + "name": "conservatively_settled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "large_review_runs_expiry_idx": { + "name": "large_review_runs_expiry_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "large_review_runs_resume_identity_idx": { + "name": "large_review_runs_resume_identity_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "head_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "base_sha", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cli_version", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "configuration_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "retry_lineage", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "large_review_runs_current_review_id_reviews_id_fk": { + "name": "large_review_runs_current_review_id_reviews_id_fk", + "tableFrom": "large_review_runs", + "tableTo": "reviews", + "columnsFrom": [ + "current_review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "large_review_runs_repository_id_repositories_id_fk": { + "name": "large_review_runs_repository_id_repositories_id_fk", + "tableFrom": "large_review_runs", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "large_review_runs_key_check": { + "name": "large_review_runs_key_check", + "value": "\"large_review_runs\".\"run_key\" ~ '^[0-9a-f]{64}$'" + }, + "large_review_runs_configuration_check": { + "name": "large_review_runs_configuration_check", + "value": "\"large_review_runs\".\"configuration_sha256\" ~ '^[0-9a-f]{64}$'" + }, + "large_review_runs_plan_check": { + "name": "large_review_runs_plan_check", + "value": "\"large_review_runs\".\"plan_sha256\" ~ '^[0-9a-f]{64}$'" + }, + "large_review_runs_identity_lengths_check": { + "name": "large_review_runs_identity_lengths_check", + "value": "\"large_review_runs\".\"pr_number\" > 0 AND length(btrim(\"large_review_runs\".\"cli_version\")) BETWEEN 1 AND 100 AND length(btrim(\"large_review_runs\".\"provider_identity\")) BETWEEN 1 AND 2048 AND length(btrim(\"large_review_runs\".\"head_sha\")) BETWEEN 1 AND 200 AND length(btrim(\"large_review_runs\".\"base_sha\")) BETWEEN 1 AND 200 AND length(btrim(\"large_review_runs\".\"retry_lineage\")) BETWEEN 1 AND 200" + }, + "large_review_runs_billing_state_check": { + "name": "large_review_runs_billing_state_check", + "value": "(\"large_review_runs\".\"billing_state\" = 'active' AND \"large_review_runs\".\"conservatively_settled_at\" IS NULL) OR (\"large_review_runs\".\"billing_state\" = 'conservative' AND \"large_review_runs\".\"conservatively_settled_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + }, + "public.operator_alert_deliveries": { + "name": "operator_alert_deliveries", + "schema": "", + "columns": { + "event_key": { + "name": "event_key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "github_installation_id": { + "name": "github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "operator_alert_deliveries_status_created_idx": { + "name": "operator_alert_deliveries_status_created_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "operator_alert_deliveries_org_created_idx": { + "name": "operator_alert_deliveries_org_created_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "operator_alert_deliveries_org_id_organizations_id_fk": { + "name": "operator_alert_deliveries_org_id_organizations_id_fk", + "tableFrom": "operator_alert_deliveries", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "operator_alert_deliveries_event_check": { + "name": "operator_alert_deliveries_event_check", + "value": "\"operator_alert_deliveries\".\"event\" IN ('trial_started', 'trial_expired', 'installation_removed', 'subscription_started', 'subscription_past_due', 'subscription_paused', 'subscription_canceled', 'billing_anomaly')" + }, + "operator_alert_deliveries_status_check": { + "name": "operator_alert_deliveries_status_check", + "value": "\"operator_alert_deliveries\".\"status\" IN ('queued', 'retrying', 'delivered', 'failed')" + }, + "operator_alert_deliveries_event_key_nonempty": { + "name": "operator_alert_deliveries_event_key_nonempty", + "value": "length(btrim(\"operator_alert_deliveries\".\"event_key\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.org_config_probe_refreshes": { + "name": "org_config_probe_refreshes", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "refreshed_at": { + "name": "refreshed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "org_config_probe_refreshes_org_id_organizations_id_fk": { + "name": "org_config_probe_refreshes_org_id_organizations_id_fk", + "tableFrom": "org_config_probe_refreshes", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_config_snapshots": { + "name": "org_config_snapshots", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "source_repository_id": { + "name": "source_repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_github_repo_id": { + "name": "source_github_repo_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "source_full_name": { + "name": "source_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commit_sha": { + "name": "commit_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config_yaml": { + "name": "config_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "guardrails_md": { + "name": "guardrails_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_policy_md": { + "name": "content_policy_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "text[]", + "primaryKey": false, + "notNull": true + }, + "loaded_files": { + "name": "loaded_files", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "stale": { + "name": "stale", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fetched_at": { + "name": "fetched_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_config_snapshots_org_id_organizations_id_fk": { + "name": "org_config_snapshots_org_id_organizations_id_fk", + "tableFrom": "org_config_snapshots", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_config_snapshots_source_repository_id_repositories_id_fk": { + "name": "org_config_snapshots_source_repository_id_repositories_id_fk", + "tableFrom": "org_config_snapshots", + "tableTo": "repositories", + "columnsFrom": [ + "source_repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_members": { + "name": "org_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "org_members_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + } + }, + "indexes": { + "org_members_org_user_idx": { + "name": "org_members_org_user_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "org_members_user_idx": { + "name": "org_members_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "org_members_org_id_organizations_id_fk": { + "name": "org_members_org_id_organizations_id_fk", + "tableFrom": "org_members", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "org_members_user_id_users_id_fk": { + "name": "org_members_user_id_users_id_fk", + "tableFrom": "org_members", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.org_settings": { + "name": "org_settings", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "api_base": { + "name": "api_base", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "api_key_ciphertext": { + "name": "api_key_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "api_format": { + "name": "api_format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openai-compatible'" + }, + "api_auth_header_ciphertext": { + "name": "api_auth_header_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "api_auth_value_ciphertext": { + "name": "api_auth_value_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model_cascade": { + "name": "model_cascade", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config_yaml": { + "name": "config_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "guardrails_md": { + "name": "guardrails_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_policy_md": { + "name": "content_policy_md", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_config_enabled": { + "name": "shared_config_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "gate_enabled": { + "name": "gate_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "escalation_email": { + "name": "escalation_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "escalation_email_pending": { + "name": "escalation_email_pending", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verified_at": { + "name": "escalation_email_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_token_digest": { + "name": "escalation_email_verification_token_digest", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_token_ciphertext": { + "name": "escalation_email_verification_token_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_expires_at": { + "name": "escalation_email_verification_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_requested_at": { + "name": "escalation_email_verification_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_sent_at": { + "name": "escalation_email_verification_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "escalation_email_verification_message_id": { + "name": "escalation_email_verification_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "org_settings_org_id_organizations_id_fk": { + "name": "org_settings_org_id_organizations_id_fk", + "tableFrom": "org_settings", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_entitlements": { + "name": "organization_entitlements", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "subscription_mode": { + "name": "subscription_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trial_ends_at": { + "name": "trial_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "past_due_grace_ends_at": { + "name": "past_due_grace_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "period_starts_at": { + "name": "period_starts_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "period_ends_at": { + "name": "period_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "included_usage_micros": { + "name": "included_usage_micros", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "overage_hard_cap_micros": { + "name": "overage_hard_cap_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "included_usage_cents": { + "name": "included_usage_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "overage_hard_cap_cents": { + "name": "overage_hard_cap_cents", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "billing_contact_email": { + "name": "billing_contact_email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verified_at": { + "name": "billing_contact_verified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "billing_contact_pending": { + "name": "billing_contact_pending", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_token_digest": { + "name": "billing_contact_verification_token_digest", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_token_ciphertext": { + "name": "billing_contact_verification_token_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_expires_at": { + "name": "billing_contact_verification_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_requested_at": { + "name": "billing_contact_verification_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_sent_at": { + "name": "billing_contact_verification_sent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "billing_contact_verification_message_id": { + "name": "billing_contact_verification_message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "promotional_eligible": { + "name": "promotional_eligible", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "promotional_ends_at": { + "name": "promotional_ends_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_by": { + "name": "updated_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_entitlements_org_id_organizations_id_fk": { + "name": "organization_entitlements_org_id_organizations_id_fk", + "tableFrom": "organization_entitlements", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_entitlements_subscription_mode_check": { + "name": "organization_entitlements_subscription_mode_check", + "value": "\"organization_entitlements\".\"subscription_mode\" IN ('hosted', 'byok')" + }, + "organization_entitlements_status_check": { + "name": "organization_entitlements_status_check", + "value": "\"organization_entitlements\".\"status\" IN ('active', 'trialing', 'past_due', 'suspended')" + }, + "organization_entitlements_included_usage_micros_nonnegative": { + "name": "organization_entitlements_included_usage_micros_nonnegative", + "value": "\"organization_entitlements\".\"included_usage_micros\" >= 0" + }, + "organization_entitlements_overage_cap_micros_nonnegative": { + "name": "organization_entitlements_overage_cap_micros_nonnegative", + "value": "\"organization_entitlements\".\"overage_hard_cap_micros\" IS NULL OR \"organization_entitlements\".\"overage_hard_cap_micros\" >= 0" + }, + "organization_entitlements_included_usage_nonnegative": { + "name": "organization_entitlements_included_usage_nonnegative", + "value": "\"organization_entitlements\".\"included_usage_cents\" >= 0" + }, + "organization_entitlements_overage_cap_nonnegative": { + "name": "organization_entitlements_overage_cap_nonnegative", + "value": "\"organization_entitlements\".\"overage_hard_cap_cents\" IS NULL OR \"organization_entitlements\".\"overage_hard_cap_cents\" >= 0" + }, + "organization_entitlements_updated_by_nonempty": { + "name": "organization_entitlements_updated_by_nonempty", + "value": "length(btrim(\"organization_entitlements\".\"updated_by\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.organization_notification_preferences": { + "name": "organization_notification_preferences", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "billing_summary_email": { + "name": "billing_summary_email", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "service_summary_email": { + "name": "service_summary_email", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "organization_notification_preferences_org_id_organizations_id_fk": { + "name": "organization_notification_preferences_org_id_organizations_id_fk", + "tableFrom": "organization_notification_preferences", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization_setting_events": { + "name": "organization_setting_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "organization_setting_events_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "setting": { + "name": "setting", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'dashboard'" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organization_setting_events_org_time_idx": { + "name": "organization_setting_events_org_time_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "organization_setting_events_org_id_organizations_id_fk": { + "name": "organization_setting_events_org_id_organizations_id_fk", + "tableFrom": "organization_setting_events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "organization_setting_events_actor_user_id_users_id_fk": { + "name": "organization_setting_events_actor_user_id_users_id_fk", + "tableFrom": "organization_setting_events", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "organization_setting_events_setting_check": { + "name": "organization_setting_events_setting_check", + "value": "\"organization_setting_events\".\"setting\" IN ('gate_enabled', 'billing_summary_email', 'service_summary_email')" + }, + "organization_setting_events_value_check": { + "name": "organization_setting_events_value_check", + "value": "\"organization_setting_events\".\"value\" IN ('enabled', 'disabled', 'advisory')" + }, + "organization_setting_events_source_check": { + "name": "organization_setting_events_source_check", + "value": "\"organization_setting_events\".\"source\" IN ('dashboard')" + } + }, + "isRLSEnabled": false + }, + "public.organizations": { + "name": "organizations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "organizations_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "github_org_id": { + "name": "github_org_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'beta'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "organizations_github_org_id_idx": { + "name": "organizations_github_org_id_idx", + "columns": [ + { + "expression": "github_org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "organizations_slug_unique": { + "name": "organizations_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.private_monitor_incidents": { + "name": "private_monitor_incidents", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "group": { + "name": "group", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "severity": { + "name": "severity", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "detail": { + "name": "detail", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "occurrence_count": { + "name": "occurrence_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "first_detected_at": { + "name": "first_detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_detected_at": { + "name": "last_detected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "pending_notification_key": { + "name": "pending_notification_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pending_notification_kind": { + "name": "pending_notification_kind", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notification_attempts": { + "name": "notification_attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "notification_available_at": { + "name": "notification_available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "notification_lease_owner": { + "name": "notification_lease_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notification_lease_expires_at": { + "name": "notification_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_notified_at": { + "name": "last_notified_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_notification_error": { + "name": "last_notification_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "private_monitor_incidents_state_updated_idx": { + "name": "private_monitor_incidents_state_updated_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "private_monitor_incidents_notification_idx": { + "name": "private_monitor_incidents_notification_idx", + "columns": [ + { + "expression": "notification_available_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "notification_lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"private_monitor_incidents\".\"pending_notification_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "private_monitor_incidents_state_check": { + "name": "private_monitor_incidents_state_check", + "value": "\"private_monitor_incidents\".\"state\" IN ('open', 'resolved')" + }, + "private_monitor_incidents_severity_check": { + "name": "private_monitor_incidents_severity_check", + "value": "\"private_monitor_incidents\".\"severity\" IN ('warning', 'critical')" + }, + "private_monitor_incidents_notification_kind_check": { + "name": "private_monitor_incidents_notification_kind_check", + "value": "\"private_monitor_incidents\".\"pending_notification_kind\" IS NULL OR \"private_monitor_incidents\".\"pending_notification_kind\" IN ('opened', 'reminder', 'resolved')" + }, + "private_monitor_incidents_notification_pair_check": { + "name": "private_monitor_incidents_notification_pair_check", + "value": "(\"private_monitor_incidents\".\"pending_notification_key\" IS NULL) = (\"private_monitor_incidents\".\"pending_notification_kind\" IS NULL)" + }, + "private_monitor_incidents_occurrence_count_check": { + "name": "private_monitor_incidents_occurrence_count_check", + "value": "\"private_monitor_incidents\".\"occurrence_count\" > 0 AND \"private_monitor_incidents\".\"notification_attempts\" >= 0 AND \"private_monitor_incidents\".\"notification_attempts\" <= 5" + }, + "private_monitor_incidents_text_nonempty": { + "name": "private_monitor_incidents_text_nonempty", + "value": "length(btrim(\"private_monitor_incidents\".\"key\")) > 0 AND length(btrim(\"private_monitor_incidents\".\"group\")) > 0 AND length(btrim(\"private_monitor_incidents\".\"summary\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.private_monitor_runs": { + "name": "private_monitor_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "private_monitor_runs_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "scheduled_for": { + "name": "scheduled_for", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "owner": { + "name": "owner", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "check_count": { + "name": "check_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "failure_count": { + "name": "failure_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "private_monitor_runs_scheduled_idx": { + "name": "private_monitor_runs_scheduled_idx", + "columns": [ + { + "expression": "scheduled_for", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "private_monitor_runs_started_idx": { + "name": "private_monitor_runs_started_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "private_monitor_runs_status_check": { + "name": "private_monitor_runs_status_check", + "value": "\"private_monitor_runs\".\"status\" IN ('running', 'completed', 'failed')" + }, + "private_monitor_runs_counts_check": { + "name": "private_monitor_runs_counts_check", + "value": "\"private_monitor_runs\".\"check_count\" >= 0 AND \"private_monitor_runs\".\"failure_count\" >= 0 AND \"private_monitor_runs\".\"failure_count\" <= \"private_monitor_runs\".\"check_count\"" + }, + "private_monitor_runs_owner_nonempty": { + "name": "private_monitor_runs_owner_nonempty", + "value": "length(btrim(\"private_monitor_runs\".\"owner\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.private_monitor_state": { + "name": "private_monitor_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true + }, + "lease_owner": { + "name": "lease_owner", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "lease_expires_at": { + "name": "lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_started_at": { + "name": "last_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_completed_at": { + "name": "last_completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "private_monitor_state_singleton_check": { + "name": "private_monitor_state_singleton_check", + "value": "\"private_monitor_state\".\"id\" = 1" + } + }, + "isRLSEnabled": false + }, + "public.private_worker_rehearsals": { + "name": "private_worker_rehearsals", + "schema": "", + "columns": { + "nonce": { + "name": "nonce", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'armed'" + }, + "operator_github_id": { + "name": "operator_github_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "job_id": { + "name": "job_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "org_slug": { + "name": "org_slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_public_id": { + "name": "review_public_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "armed_at": { + "name": "armed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "consumed_at": { + "name": "consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "interrupted_worker_instance": { + "name": "interrupted_worker_instance", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replacement_worker_instance": { + "name": "replacement_worker_instance", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replacement_observed_at": { + "name": "replacement_observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "before_review_count": { + "name": "before_review_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "before_usage_count": { + "name": "before_usage_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "before_check_count": { + "name": "before_check_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "before_publication_count": { + "name": "before_publication_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "after_review_count": { + "name": "after_review_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "after_usage_count": { + "name": "after_usage_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "after_check_count": { + "name": "after_check_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "after_publication_count": { + "name": "after_publication_count", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "private_worker_rehearsals_review_idx": { + "name": "private_worker_rehearsals_review_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "private_worker_rehearsals_job_idx": { + "name": "private_worker_rehearsals_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "private_worker_rehearsals_state_idx": { + "name": "private_worker_rehearsals_state_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "private_worker_rehearsals_org_id_organizations_id_fk": { + "name": "private_worker_rehearsals_org_id_organizations_id_fk", + "tableFrom": "private_worker_rehearsals", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "private_worker_rehearsals_repository_id_repositories_id_fk": { + "name": "private_worker_rehearsals_repository_id_repositories_id_fk", + "tableFrom": "private_worker_rehearsals", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "private_worker_rehearsals_review_id_reviews_id_fk": { + "name": "private_worker_rehearsals_review_id_reviews_id_fk", + "tableFrom": "private_worker_rehearsals", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "private_worker_rehearsals_job_id_jobs_id_fk": { + "name": "private_worker_rehearsals_job_id_jobs_id_fk", + "tableFrom": "private_worker_rehearsals", + "tableTo": "jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "private_worker_rehearsals_state_check": { + "name": "private_worker_rehearsals_state_check", + "value": "\"private_worker_rehearsals\".\"state\" IN ('armed', 'awaiting_replacement', 'replacement_verified', 'completed', 'expired', 'failed')" + }, + "private_worker_rehearsals_identity_check": { + "name": "private_worker_rehearsals_identity_check", + "value": "length(btrim(\"private_worker_rehearsals\".\"org_slug\")) > 0 AND length(btrim(\"private_worker_rehearsals\".\"repo_full_name\")) > 0 AND \"private_worker_rehearsals\".\"pr_number\" > 0 AND \"private_worker_rehearsals\".\"head_sha\" ~ '^[0-9a-f]{40}$'" + }, + "private_worker_rehearsals_arming_window_check": { + "name": "private_worker_rehearsals_arming_window_check", + "value": "\"private_worker_rehearsals\".\"expires_at\" > \"private_worker_rehearsals\".\"armed_at\" AND \"private_worker_rehearsals\".\"expires_at\" <= \"private_worker_rehearsals\".\"armed_at\" + interval '10 minutes'" + }, + "private_worker_rehearsals_before_counts_check": { + "name": "private_worker_rehearsals_before_counts_check", + "value": "(\"private_worker_rehearsals\".\"before_review_count\" IS NULL AND \"private_worker_rehearsals\".\"before_usage_count\" IS NULL AND \"private_worker_rehearsals\".\"before_check_count\" IS NULL AND \"private_worker_rehearsals\".\"before_publication_count\" IS NULL) OR (\"private_worker_rehearsals\".\"before_review_count\" >= 0 AND \"private_worker_rehearsals\".\"before_usage_count\" >= 0 AND \"private_worker_rehearsals\".\"before_check_count\" >= 0 AND \"private_worker_rehearsals\".\"before_publication_count\" >= 0)" + }, + "private_worker_rehearsals_after_counts_check": { + "name": "private_worker_rehearsals_after_counts_check", + "value": "(\"private_worker_rehearsals\".\"after_review_count\" IS NULL AND \"private_worker_rehearsals\".\"after_usage_count\" IS NULL AND \"private_worker_rehearsals\".\"after_check_count\" IS NULL AND \"private_worker_rehearsals\".\"after_publication_count\" IS NULL) OR (\"private_worker_rehearsals\".\"after_review_count\" >= 0 AND \"private_worker_rehearsals\".\"after_usage_count\" >= 0 AND \"private_worker_rehearsals\".\"after_check_count\" >= 0 AND \"private_worker_rehearsals\".\"after_publication_count\" >= 0)" + }, + "private_worker_rehearsals_replacement_pair_check": { + "name": "private_worker_rehearsals_replacement_pair_check", + "value": "(\"private_worker_rehearsals\".\"replacement_worker_instance\" IS NULL) = (\"private_worker_rehearsals\".\"replacement_observed_at\" IS NULL)" + }, + "private_worker_rehearsals_consumed_state_check": { + "name": "private_worker_rehearsals_consumed_state_check", + "value": "(\"private_worker_rehearsals\".\"state\" IN ('armed', 'expired') AND \"private_worker_rehearsals\".\"consumed_at\" IS NULL AND \"private_worker_rehearsals\".\"interrupted_worker_instance\" IS NULL AND \"private_worker_rehearsals\".\"before_review_count\" IS NULL) OR (\"private_worker_rehearsals\".\"state\" IN ('awaiting_replacement', 'replacement_verified', 'completed', 'failed') AND \"private_worker_rehearsals\".\"consumed_at\" IS NOT NULL AND \"private_worker_rehearsals\".\"interrupted_worker_instance\" IS NOT NULL AND \"private_worker_rehearsals\".\"before_review_count\" IS NOT NULL)" + }, + "private_worker_rehearsals_replacement_state_check": { + "name": "private_worker_rehearsals_replacement_state_check", + "value": "(\"private_worker_rehearsals\".\"state\" IN ('armed', 'awaiting_replacement', 'expired') AND \"private_worker_rehearsals\".\"replacement_worker_instance\" IS NULL) OR (\"private_worker_rehearsals\".\"state\" IN ('replacement_verified', 'completed') AND \"private_worker_rehearsals\".\"replacement_worker_instance\" IS NOT NULL) OR \"private_worker_rehearsals\".\"state\" = 'failed'" + }, + "private_worker_rehearsals_completion_state_check": { + "name": "private_worker_rehearsals_completion_state_check", + "value": "(\"private_worker_rehearsals\".\"state\" = 'completed' AND \"private_worker_rehearsals\".\"after_review_count\" IS NOT NULL AND \"private_worker_rehearsals\".\"completed_at\" IS NOT NULL AND \"private_worker_rehearsals\".\"failure_reason\" IS NULL) OR (\"private_worker_rehearsals\".\"state\" IN ('expired', 'failed') AND \"private_worker_rehearsals\".\"after_review_count\" IS NULL AND \"private_worker_rehearsals\".\"completed_at\" IS NOT NULL AND \"private_worker_rehearsals\".\"failure_reason\" IS NOT NULL) OR (\"private_worker_rehearsals\".\"state\" IN ('armed', 'awaiting_replacement', 'replacement_verified') AND \"private_worker_rehearsals\".\"after_review_count\" IS NULL AND \"private_worker_rehearsals\".\"completed_at\" IS NULL AND \"private_worker_rehearsals\".\"failure_reason\" IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.release_steps": { + "name": "release_steps", + "schema": "", + "columns": { + "name": { + "name": "name", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repo_config_probes": { + "name": "repo_config_probes", + "schema": "", + "columns": { + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "probed_at": { + "name": "probed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ok": { + "name": "ok", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "files": { + "name": "files", + "type": "text[]", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "repo_config_probes_repository_id_repositories_id_fk": { + "name": "repo_config_probes_repository_id_repositories_id_fk", + "tableFrom": "repo_config_probes", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repositories": { + "name": "repositories", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "repositories_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "installation_id": { + "name": "installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "github_repo_id": { + "name": "github_repo_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "full_name": { + "name": "full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private": { + "name": "private", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "repositories_installation_id_installations_id_fk": { + "name": "repositories_installation_id_installations_id_fk", + "tableFrom": "repositories", + "tableTo": "installations", + "columnsFrom": [ + "installation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "repositories_github_repo_id_unique": { + "name": "repositories_github_repo_id_unique", + "nullsNotDistinct": false, + "columns": [ + "github_repo_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.repository_enablement_events": { + "name": "repository_enablement_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "repository_enablement_events_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "github_repo_id": { + "name": "github_repo_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "repository_full_name": { + "name": "repository_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "repository_private": { + "name": "repository_private", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'dashboard'" + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "repository_enablement_events_org_time_idx": { + "name": "repository_enablement_events_org_time_idx", + "columns": [ + { + "expression": "org_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "repository_enablement_events_repo_time_idx": { + "name": "repository_enablement_events_repo_time_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "repository_enablement_events_org_id_organizations_id_fk": { + "name": "repository_enablement_events_org_id_organizations_id_fk", + "tableFrom": "repository_enablement_events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "repository_enablement_events_repository_id_repositories_id_fk": { + "name": "repository_enablement_events_repository_id_repositories_id_fk", + "tableFrom": "repository_enablement_events", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "repository_enablement_events_actor_user_id_users_id_fk": { + "name": "repository_enablement_events_actor_user_id_users_id_fk", + "tableFrom": "repository_enablement_events", + "tableTo": "users", + "columnsFrom": [ + "actor_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "repository_enablement_events_action_check": { + "name": "repository_enablement_events_action_check", + "value": "\"repository_enablement_events\".\"action\" IN ('enable', 'disable')" + }, + "repository_enablement_events_source_check": { + "name": "repository_enablement_events_source_check", + "value": "\"repository_enablement_events\".\"source\" IN ('dashboard', 'github_installation', 'github_pull_request', 'github_transfer', 'github_uninstall', 'migration_baseline')" + } + }, + "isRLSEnabled": false + }, + "public.repository_gate_enforcement": { + "name": "repository_gate_enforcement", + "schema": "", + "columns": { + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "default_branch": { + "name": "default_branch", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_protection": { + "name": "branch_protection", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "evidence": { + "name": "evidence", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "checked_at": { + "name": "checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "last_successful_at": { + "name": "last_successful_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "repository_gate_enforcement_status_checked_idx": { + "name": "repository_gate_enforcement_status_checked_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "checked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "repository_gate_enforcement_repository_id_repositories_id_fk": { + "name": "repository_gate_enforcement_repository_id_repositories_id_fk", + "tableFrom": "repository_gate_enforcement", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "repository_gate_enforcement_status_check": { + "name": "repository_gate_enforcement_status_check", + "value": "\"repository_gate_enforcement\".\"status\" IN ('required', 'not_required', 'unknown')" + }, + "repository_gate_enforcement_branch_protection_check": { + "name": "repository_gate_enforcement_branch_protection_check", + "value": "\"repository_gate_enforcement\".\"branch_protection\" IN ('protected', 'unprotected', 'unknown')" + } + }, + "isRLSEnabled": false + }, + "public.respond_deliveries": { + "name": "respond_deliveries", + "schema": "", + "columns": { + "job_id": { + "name": "job_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "reservation_id": { + "name": "reservation_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_org_id": { + "name": "source_org_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_installation_id": { + "name": "source_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_github_installation_id": { + "name": "source_github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_github_repo_id": { + "name": "source_github_repo_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "repo_full_name": { + "name": "repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "is_pr": { + "name": "is_pr", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "source_head_sha": { + "name": "source_head_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "marker_nonce": { + "name": "marker_nonce", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "reply_to_review_comment_id": { + "name": "reply_to_review_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "publication_identity_state": { + "name": "publication_identity_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'complete'" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'prepared'" + }, + "delivery_lease_expires_at": { + "name": "delivery_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "github_comment_id": { + "name": "github_comment_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "publication_lease_id": { + "name": "publication_lease_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "publication_lease_expires_at": { + "name": "publication_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "delivered_at": { + "name": "delivered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "respond_deliveries_pending_idx": { + "name": "respond_deliveries_pending_idx", + "columns": [ + { + "expression": "state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "delivery_lease_expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "respond_deliveries_job_id_jobs_id_fk": { + "name": "respond_deliveries_job_id_jobs_id_fk", + "tableFrom": "respond_deliveries", + "tableTo": "jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "respond_deliveries_repository_id_repositories_id_fk": { + "name": "respond_deliveries_repository_id_repositories_id_fk", + "tableFrom": "respond_deliveries", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "respond_deliveries_reservation_id_hosted_usage_reservations_id_fk": { + "name": "respond_deliveries_reservation_id_hosted_usage_reservations_id_fk", + "tableFrom": "respond_deliveries", + "tableTo": "hosted_usage_reservations", + "columnsFrom": [ + "reservation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "respond_deliveries_state_check": { + "name": "respond_deliveries_state_check", + "value": "\"respond_deliveries\".\"state\" IN ('prepared', 'delivering', 'delivered', 'cancelled')" + }, + "respond_deliveries_issue_number_positive": { + "name": "respond_deliveries_issue_number_positive", + "value": "\"respond_deliveries\".\"issue_number\" > 0" + }, + "respond_deliveries_body_nonempty": { + "name": "respond_deliveries_body_nonempty", + "value": "length(btrim(\"respond_deliveries\".\"body\")) > 0" + }, + "respond_deliveries_publication_identity_state_check": { + "name": "respond_deliveries_publication_identity_state_check", + "value": "\"respond_deliveries\".\"publication_identity_state\" IN ('complete', 'legacy_delivered', 'cancelled_incomplete')" + }, + "respond_deliveries_publication_identity_check": { + "name": "respond_deliveries_publication_identity_check", + "value": "(\n \"respond_deliveries\".\"source_org_id\" IS NOT NULL\n AND \"respond_deliveries\".\"source_installation_id\" IS NOT NULL\n AND \"respond_deliveries\".\"source_github_installation_id\" IS NOT NULL\n AND \"respond_deliveries\".\"source_github_repo_id\" IS NOT NULL\n AND (NOT \"respond_deliveries\".\"is_pr\" OR \"respond_deliveries\".\"source_head_sha\" IS NOT NULL)\n )" + }, + "respond_deliveries_publication_identity_state_matches_row_check": { + "name": "respond_deliveries_publication_identity_state_matches_row_check", + "value": "(\n \"respond_deliveries\".\"publication_identity_state\" = 'complete'\n OR (\"respond_deliveries\".\"publication_identity_state\" = 'legacy_delivered' AND \"respond_deliveries\".\"state\" = 'delivered')\n OR (\"respond_deliveries\".\"publication_identity_state\" = 'cancelled_incomplete' AND \"respond_deliveries\".\"state\" = 'cancelled')\n )" + } + }, + "isRLSEnabled": false + }, + "public.review_logs": { + "name": "review_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "review_logs_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "at": { + "name": "at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "line": { + "name": "line", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "review_logs_review_seq_idx": { + "name": "review_logs_review_seq_idx", + "columns": [ + { + "expression": "review_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "review_logs_review_id_reviews_id_fk": { + "name": "review_logs_review_id_reviews_id_fk", + "tableFrom": "review_logs", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.review_publication_receipts": { + "name": "review_publication_receipts", + "schema": "", + "columns": { + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "receipt_version": { + "name": "receipt_version", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "receipt_id": { + "name": "receipt_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "github_review_id": { + "name": "github_review_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "review_publication_receipts_review_id_reviews_id_fk": { + "name": "review_publication_receipts_review_id_reviews_id_fk", + "tableFrom": "review_publication_receipts", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "review_publication_receipts_identity_check": { + "name": "review_publication_receipts_identity_check", + "value": "(\"review_publication_receipts\".\"receipt_version\" IS NULL AND \"review_publication_receipts\".\"receipt_id\" IS NULL) OR (\"review_publication_receipts\".\"receipt_version\" = 1 AND length(btrim(\"review_publication_receipts\".\"receipt_id\")) BETWEEN 1 AND 200)" + }, + "review_publication_receipts_github_review_id_check": { + "name": "review_publication_receipts_github_review_id_check", + "value": "\"review_publication_receipts\".\"github_review_id\" IS NULL OR \"review_publication_receipts\".\"github_review_id\" ~ '^[1-9][0-9]{0,19}$'" + } + }, + "isRLSEnabled": false + }, + "public.reviews": { + "name": "reviews", + "schema": "", + "columns": { + "origin_claim_receipt_id": { + "name": "origin_claim_receipt_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "reviews_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "public_id": { + "name": "public_id", + "type": "uuid", + "primaryKey": false, + "notNull": true, + "default": "gen_random_uuid()" + }, + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "source_org_id": { + "name": "source_org_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_installation_id": { + "name": "source_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_github_installation_id": { + "name": "source_github_installation_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_github_repo_id": { + "name": "source_github_repo_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "source_repo_full_name": { + "name": "source_repo_full_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pr_number": { + "name": "pr_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "author_github_id": { + "name": "author_github_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "author_login": { + "name": "author_login", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "head_sha": { + "name": "head_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "base_sha": { + "name": "base_sha", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "since_sha": { + "name": "since_sha", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "trigger_context": { + "name": "trigger_context", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "review_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "envelope": { + "name": "envelope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "config_files": { + "name": "config_files", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "config_provenance": { + "name": "config_provenance", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "silent": { + "name": "silent", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "engine_gate_failing": { + "name": "engine_gate_failing", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "gate_failing": { + "name": "gate_failing", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "advisory_check_run_id": { + "name": "advisory_check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "gate_check_run_id": { + "name": "gate_check_run_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "gate_sync_lease_id": { + "name": "gate_sync_lease_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "gate_sync_lease_expires_at": { + "name": "gate_sync_lease_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "reviews_public_id_idx": { + "name": "reviews_public_id_idx", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reviews_repo_pr_idx": { + "name": "reviews_repo_pr_idx", + "columns": [ + { + "expression": "repository_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "pr_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reviews_status_idx": { + "name": "reviews_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "reviews_running_started_at_idx": { + "name": "reviews_running_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"reviews\".\"status\" = 'running'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "reviews_origin_claim_receipt_id_job_attempt_receipts_receipt_id_fk": { + "name": "reviews_origin_claim_receipt_id_job_attempt_receipts_receipt_id_fk", + "tableFrom": "reviews", + "tableTo": "job_attempt_receipts", + "columnsFrom": [ + "origin_claim_receipt_id" + ], + "columnsTo": [ + "receipt_id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "reviews_repository_id_repositories_id_fk": { + "name": "reviews_repository_id_repositories_id_fk", + "tableFrom": "reviews", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "reviews_trigger_source_check": { + "name": "reviews_trigger_source_check", + "value": "\"reviews\".\"trigger_source\" IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun')" + }, + "reviews_trigger_context_check": { + "name": "reviews_trigger_context_check", + "value": "(\"reviews\".\"trigger_source\" = 'unknown' AND (\"reviews\".\"trigger_context\" IS NULL OR \"reviews\".\"trigger_context\" = '{\"source\":\"unknown\"}'::jsonb)) OR (\"reviews\".\"trigger_source\" <> 'unknown' AND \"reviews\".\"trigger_context\" IS NOT NULL AND jsonb_typeof(\"reviews\".\"trigger_context\") = 'object' AND \"reviews\".\"trigger_context\" - ARRAY['source', 'webhookDeliveryId', 'webhookEvent', 'webhookAction', 'sourceCommentId', 'sourceUrl', 'requestedByGithubId', 'requestedByLogin', 'checkName']::text[] = '{}'::jsonb AND \"reviews\".\"trigger_context\"->>'source' = \"reviews\".\"trigger_source\" AND jsonb_typeof(\"reviews\".\"trigger_context\"->'webhookDeliveryId') = 'string' AND COALESCE(length(btrim(\"reviews\".\"trigger_context\"->>'webhookDeliveryId')), 0) > 0 AND length(\"reviews\".\"trigger_context\"->>'webhookDeliveryId') <= 200 AND ((\"reviews\".\"trigger_source\" = 'automatic_pull_request' AND \"reviews\".\"trigger_context\"->>'webhookEvent' = 'pull_request') OR (\"reviews\".\"trigger_source\" = 'requested_review' AND \"reviews\".\"trigger_context\"->>'webhookEvent' IN ('issue_comment', 'pull_request_review_comment')) OR (\"reviews\".\"trigger_source\" = 'github_check_rerun' AND \"reviews\".\"trigger_context\"->>'webhookEvent' IN ('check_run', 'check_suite'))) AND (NOT \"reviews\".\"trigger_context\" ? 'webhookAction' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'webhookAction') = 'string' AND length(\"reviews\".\"trigger_context\"->>'webhookAction') <= 100)) AND (NOT \"reviews\".\"trigger_context\" ? 'sourceCommentId' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'sourceCommentId') = 'number' AND (\"reviews\".\"trigger_context\"->>'sourceCommentId')::numeric = trunc((\"reviews\".\"trigger_context\"->>'sourceCommentId')::numeric) AND (\"reviews\".\"trigger_context\"->>'sourceCommentId')::numeric BETWEEN 1 AND 9007199254740991)) AND (NOT \"reviews\".\"trigger_context\" ? 'sourceUrl' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'sourceUrl') = 'string' AND length(\"reviews\".\"trigger_context\"->>'sourceUrl') <= 2048 AND \"reviews\".\"trigger_context\"->>'sourceUrl' ~* '^https://github[.]com([/?#]|$)')) AND (NOT \"reviews\".\"trigger_context\" ? 'requestedByGithubId' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'requestedByGithubId') = 'number' AND (\"reviews\".\"trigger_context\"->>'requestedByGithubId')::numeric = trunc((\"reviews\".\"trigger_context\"->>'requestedByGithubId')::numeric) AND (\"reviews\".\"trigger_context\"->>'requestedByGithubId')::numeric BETWEEN 1 AND 9007199254740991)) AND (NOT \"reviews\".\"trigger_context\" ? 'requestedByLogin' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'requestedByLogin') = 'string' AND length(\"reviews\".\"trigger_context\"->>'requestedByLogin') <= 100)) AND (NOT \"reviews\".\"trigger_context\" ? 'checkName' OR (jsonb_typeof(\"reviews\".\"trigger_context\"->'checkName') = 'string' AND length(\"reviews\".\"trigger_context\"->>'checkName') <= 200)))" + } + }, + "isRLSEnabled": false + }, + "public.self_service_trial_grants": { + "name": "self_service_trial_grants", + "schema": "", + "columns": { + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": true, + "notNull": true + }, + "initiated_by_github_id": { + "name": "initiated_by_github_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "requested_mode": { + "name": "requested_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "granted_mode": { + "name": "granted_mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "self_service_trial_grants_actor_created_idx": { + "name": "self_service_trial_grants_actor_created_idx", + "columns": [ + { + "expression": "initiated_by_github_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "self_service_trial_grants_requested_mode_check": { + "name": "self_service_trial_grants_requested_mode_check", + "value": "\"self_service_trial_grants\".\"requested_mode\" IN ('hosted', 'byok')" + }, + "self_service_trial_grants_granted_mode_check": { + "name": "self_service_trial_grants_granted_mode_check", + "value": "\"self_service_trial_grants\".\"granted_mode\" IN ('hosted', 'byok')" + } + }, + "isRLSEnabled": false + }, + "public.service_heartbeats": { + "name": "service_heartbeats", + "schema": "", + "columns": { + "component": { + "name": "component", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "instance_id": { + "name": "instance_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "observed_at": { + "name": "observed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "service_heartbeats_component_check": { + "name": "service_heartbeats_component_check", + "value": "\"service_heartbeats\".\"component\" IN ('worker', 'monitor')" + }, + "service_heartbeats_instance_nonempty": { + "name": "service_heartbeats_instance_nonempty", + "value": "length(btrim(\"service_heartbeats\".\"instance_id\")) > 0" + } + }, + "isRLSEnabled": false + }, + "public.sessions": { + "name": "sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "github_access_token_ciphertext": { + "name": "github_access_token_ciphertext", + "type": "bytea", + "primaryKey": false, + "notNull": false + }, + "membership_checked_at": { + "name": "membership_checked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "membership_check_available_at": { + "name": "membership_check_available_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "sessions_user_id_users_id_fk": { + "name": "sessions_user_id_users_id_fk", + "tableFrom": "sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_events": { + "name": "usage_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "usage_events_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "org_id": { + "name": "org_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "repository_id": { + "name": "repository_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "review_id": { + "name": "review_id", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "trigger_source": { + "name": "trigger_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "prompt_tokens": { + "name": "prompt_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "completion_tokens": { + "name": "completion_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "model_used": { + "name": "model_used", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cost_micros": { + "name": "cost_micros", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "billing_scope": { + "name": "billing_scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "usage_events_org_id_organizations_id_fk": { + "name": "usage_events_org_id_organizations_id_fk", + "tableFrom": "usage_events", + "tableTo": "organizations", + "columnsFrom": [ + "org_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_events_repository_id_repositories_id_fk": { + "name": "usage_events_repository_id_repositories_id_fk", + "tableFrom": "usage_events", + "tableTo": "repositories", + "columnsFrom": [ + "repository_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_events_review_id_reviews_id_fk": { + "name": "usage_events_review_id_reviews_id_fk", + "tableFrom": "usage_events", + "tableTo": "reviews", + "columnsFrom": [ + "review_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "usage_events_cost_micros_nonnegative": { + "name": "usage_events_cost_micros_nonnegative", + "value": "\"usage_events\".\"cost_micros\" IS NULL OR \"usage_events\".\"cost_micros\" >= 0" + }, + "usage_events_cost_cents_nonnegative": { + "name": "usage_events_cost_cents_nonnegative", + "value": "\"usage_events\".\"cost_cents\" IS NULL OR \"usage_events\".\"cost_cents\" >= 0" + }, + "usage_events_billing_scope_check": { + "name": "usage_events_billing_scope_check", + "value": "\"usage_events\".\"billing_scope\" IN ('analytics', 'private_hosted')" + }, + "usage_events_trigger_source_check": { + "name": "usage_events_trigger_source_check", + "value": "\"usage_events\".\"trigger_source\" IN ('unknown', 'automatic_pull_request', 'requested_review', 'github_check_rerun', 'github_mention')" + } + }, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigint", + "primaryKey": true, + "notNull": true, + "identity": { + "type": "always", + "name": "users_id_seq", + "schema": "public", + "increment": "1", + "startWith": "1", + "minValue": "1", + "maxValue": "9223372036854775807", + "cache": "1", + "cycle": false + } + }, + "github_id": { + "name": "github_id", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "login": { + "name": "login", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_github_id_unique": { + "name": "users_github_id_unique", + "nullsNotDistinct": false, + "columns": [ + "github_id" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook_deliveries": { + "name": "webhook_deliveries", + "schema": "", + "columns": { + "delivery_id": { + "name": "delivery_id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "event": { + "name": "event", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "received_at": { + "name": "received_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "webhook_deliveries_completed_at_idx": { + "name": "webhook_deliveries_completed_at_idx", + "columns": [ + { + "expression": "completed_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "where": "\"webhook_deliveries\".\"completed_at\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "webhook_deliveries_payload_completion_check": { + "name": "webhook_deliveries_payload_completion_check", + "value": "(\"webhook_deliveries\".\"payload\" IS NULL) = (\"webhook_deliveries\".\"completed_at\" IS NOT NULL)" + } + }, + "isRLSEnabled": false + } + }, + "enums": { + "public.finding_approval_role": { + "name": "finding_approval_role", + "schema": "public", + "values": [ + "member", + "admin" + ] + }, + "public.finding_approval_source": { + "name": "finding_approval_source", + "schema": "public", + "values": [ + "github", + "dashboard" + ] + }, + "public.job_status": { + "name": "job_status", + "schema": "public", + "values": [ + "queued", + "running", + "done", + "failed" + ] + }, + "public.review_status": { + "name": "review_status", + "schema": "public", + "values": [ + "queued", + "running", + "completed", + "failed", + "stale" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 9d75dfe3..f58b2c8b 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -316,6 +316,13 @@ "when": 1784616145744, "tag": "0044_customer_notification_email_outbox", "breakpoints": true + }, + { + "idx": 45, + "version": "7", + "when": 1792347173330, + "tag": "0045_job_attempt_receipts", + "breakpoints": true } ] } diff --git a/scripts/inspect-review-ledger.ts b/scripts/inspect-review-ledger.ts index cfc94cc3..5d80b7af 100644 --- a/scripts/inspect-review-ledger.ts +++ b/scripts/inspect-review-ledger.ts @@ -1,3 +1,5 @@ +import { Client } from "pg"; +import { readAttemptReceiptPageSql } from "@/lib/job-attempt-receipts"; import { closeDb, getPool } from "@/lib/db"; /** @@ -7,7 +9,25 @@ import { closeDb, getPool } from "@/lib/db"; * twelve characters, so this is the scriptable way to read the full value. * Read-only. */ +export async function readAttemptReceipts(args: string[]): Promise { + if (args.length !== 2 || args[0] !== "--attempt-receipts" || !/^(0|[1-9][0-9]{0,18})$/.test(args[1]!)) { + throw new Error("usage: --attempt-receipts "); + } + const client = new Client({ connectionString: process.env.DATABASE_URL, + connectionTimeoutMillis: 2000, query_timeout: 5000, + options: "-c default_transaction_read_only=on -c statement_timeout=5000 -c lock_timeout=250" }); + try { + await client.connect(); + const result = await client.query(readAttemptReceiptPageSql, [args[1]]); + const output = JSON.stringify({ rows: result.rows }); + if (Buffer.byteLength(output) > 65536) throw new Error("receipt page exceeds response bound"); + console.log(output); + } finally { await client.end(); } +} + async function main(): Promise { + const args = process.argv.slice(2); + if (args.length) { await readAttemptReceipts(args); return; } const pool = getPool(); const reviews = await pool.query( `SELECT r.id, r.public_id, r.status, r.queued_at, r.started_at, @@ -42,7 +62,7 @@ async function main(): Promise { await closeDb(); } -main().catch((error) => { - console.error(error); +if (import.meta.main) main().catch(() => { + console.error("review inspection unavailable"); process.exit(1); }); diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 36ccca82..7a0b8467 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -5,6 +5,8 @@ import { check, customType, index, + foreignKey, + unique, integer, jsonb, pgEnum, @@ -19,6 +21,11 @@ import type { Envelope } from "@/lib/envelope"; import type { ReviewConfigProvenance } from "@/lib/github/contents"; import type { ReviewTriggerContext } from "@/lib/review-trigger"; +/** Decimal receipt identities preserve all PostgreSQL bigint bits. */ +const receiptIdentity = customType<{ data: string; driverData: string }>({ + dataType() { return "bigint"; }, +}); + /** Raw bytes column for AES-256-GCM sealed secrets. */ const bytea = customType<{ data: Buffer; driverData: Buffer }>({ dataType() { @@ -261,6 +268,7 @@ export const repositoryEnablementEvents = pgTable( export const reviews = pgTable( "reviews", { + originClaimReceiptId: receiptIdentity("origin_claim_receipt_id").references(() => jobAttemptReceipts.receiptId), id: bigint("id", { mode: "number" }) .primaryKey() .generatedAlwaysAsIdentity(), @@ -1563,6 +1571,8 @@ export type JobPayload = Record; export const jobs = pgTable( "jobs", { + activeClaimReceiptId: receiptIdentity("active_claim_receipt_id").references(() => jobAttemptReceipts.receiptId), + stagedReviewId: receiptIdentity("staged_review_id"), id: bigint("id", { mode: "number" }) .primaryKey() .generatedAlwaysAsIdentity(), @@ -1906,3 +1916,77 @@ export const orgConfigSnapshots = pgTable("org_config_snapshots", { .notNull() .defaultNow(), }); + +/** Immutable queue claims and finite review observations. */ +export const jobAttemptReceipts = pgTable("job_attempt_receipts", { + receiptId: receiptIdentity("receipt_id").notNull().primaryKey(), + predecessorId: receiptIdentity("predecessor_id"), + sourceEpoch: uuid("source_epoch"), + schemaVersion: integer("schema_version").notNull().default(1), + claimReceiptId: receiptIdentity("claim_receipt_id"), + claimKind: text("claim_kind").notNull().default("claim"), + bindingReceiptId: receiptIdentity("binding_receipt_id"), + bindingKind: text("binding_kind").notNull().default("review_bound"), + jobId: receiptIdentity("job_id"), + ordinal: integer("ordinal"), + jobKind: text("job_kind"), + reviewId: receiptIdentity("review_id"), + reviewPublicId: uuid("review_public_id"), + eventKind: text("event_kind").notNull(), + outcome: text("outcome"), + phase: text("phase"), + category: text("category"), + recovered: boolean("recovered"), + recovery: text("recovery"), + occurredAt: timestamp("occurred_at", { withTimezone: true }).notNull().default(sql`clock_timestamp()`), +}, (t) => [ + unique().on(t.predecessorId), + unique().on(t.receiptId, t.jobId, t.ordinal, t.jobKind, t.eventKind), + unique().on(t.receiptId, t.claimReceiptId, t.jobId, t.reviewId, t.reviewPublicId, t.eventKind), + foreignKey({ columns: [t.predecessorId], foreignColumns: [t.receiptId] }), + foreignKey({ columns: [t.claimReceiptId, t.jobId, t.ordinal, t.jobKind, t.claimKind], + foreignColumns: [t.receiptId, t.jobId, t.ordinal, t.jobKind, t.eventKind] }), + foreignKey({ columns: [t.bindingReceiptId, t.claimReceiptId, t.jobId, t.reviewId, t.reviewPublicId, t.bindingKind], + foreignColumns: [t.receiptId, t.claimReceiptId, t.jobId, t.reviewId, t.reviewPublicId, t.eventKind] }), + uniqueIndex("job_attempt_one_closure").on(t.claimReceiptId).where(sql`${t.eventKind} = 'queue_closed'`), + uniqueIndex("job_attempt_one_binding").on(t.claimReceiptId, t.reviewId).where(sql`${t.eventKind} = 'review_bound'`), + uniqueIndex("job_attempt_one_observation").on(t.claimReceiptId, t.reviewId, t.outcome).where(sql`${t.eventKind} = 'review_observed'`), + check("job_attempt_receipt_check_1", sql`schema_version = 1`), + check("job_attempt_receipt_check_2", sql`claim_kind = 'claim'`), + check("job_attempt_receipt_check_3", sql`binding_kind = 'review_bound'`), + check("job_attempt_receipt_check_4", sql`job_kind IN ('review','review-feedback')`), + check("job_attempt_receipt_check_5", sql`event_kind IN ('genesis','claim','review_bound','review_observed','incident','queue_closed')`), + check("job_attempt_receipt_check_6", sql`phase IN ('planner','review','scorer','respond')`), + check("job_attempt_receipt_check_7", sql`category IN ('providerError','invalidOutput','timeout','deadline')`), + check("job_attempt_receipt_check_8", sql`recovery IN ('repair','fallback')`), + check("job_attempt_receipt_check_9", sql`isfinite(occurred_at)`), + check("job_attempt_receipt_check_10", sql` + (event_kind = 'genesis' AND receipt_id = 0 AND source_epoch IS NOT NULL + AND predecessor_id IS NULL AND claim_receipt_id IS NULL AND binding_receipt_id IS NULL + AND job_id IS NULL AND ordinal IS NULL AND job_kind IS NULL AND review_id IS NULL + AND review_public_id IS NULL AND outcome IS NULL AND phase IS NULL AND category IS NULL + AND recovered IS NULL AND recovery IS NULL) + OR + (event_kind <> 'genesis' AND receipt_id > 0 AND source_epoch IS NULL + AND predecessor_id IS NOT NULL AND predecessor_id < receipt_id + AND claim_receipt_id IS NOT NULL AND claim_receipt_id <= receipt_id + AND job_id IS NOT NULL AND job_id > 0 AND ordinal IS NOT NULL AND ordinal > 0 + AND job_kind IS NOT NULL AND outcome IS NOT NULL) + `), + check("job_attempt_receipt_check_11", sql`(review_id IS NULL) = (review_public_id IS NULL)`), + check("job_attempt_receipt_check_12", sql`review_id IS NULL OR review_id > 0`), + check("job_attempt_receipt_check_13", sql` + (event_kind = 'genesis') OR + (event_kind = 'claim' AND claim_receipt_id = receipt_id AND review_id IS NULL AND outcome = 'started') OR + (event_kind = 'review_bound' AND review_id IS NOT NULL AND outcome = 'bound') OR + (event_kind = 'review_observed' AND review_id IS NOT NULL AND outcome IN ('staged','completed','failed','interrupted','cancelled')) OR + (event_kind = 'incident' AND review_id IS NOT NULL AND outcome = 'observed') OR + (event_kind = 'queue_closed' AND review_id IS NULL AND outcome IN ('succeeded','retry','failed','continued','interrupted','cancelled','coalesced','deferred')) + `), + check("job_attempt_receipt_check_14", sql`(binding_receipt_id IS NOT NULL) = (event_kind IN ('review_observed','incident'))`), + check("job_attempt_receipt_check_15", sql` + (event_kind = 'incident' AND phase IS NOT NULL AND category IS NOT NULL AND recovered IS NOT NULL + AND recovered = (recovery IS NOT NULL)) + OR (event_kind <> 'incident' AND phase IS NULL AND category IS NULL AND recovered IS NULL AND recovery IS NULL) + `), +]); diff --git a/src/lib/github/webhook-handler.ts b/src/lib/github/webhook-handler.ts index bb0ac7b0..08486114 100644 --- a/src/lib/github/webhook-handler.ts +++ b/src/lib/github/webhook-handler.ts @@ -8,6 +8,11 @@ import { enqueueCustomerNotification, installationRemovedNotification, } from "@/lib/customer-notifications"; +import { + appendPreparedReceiptBatch, + prepareReviewObservation, + transactionReceiptClient, +} from "@/lib/job-attempt-receipts"; import { getDb, getPool, schema, type Database } from "@/lib/db"; import { requireEnv } from "@/lib/env"; import { getInstallationToken } from "@/lib/github/app-auth"; @@ -514,6 +519,7 @@ async function completeRunningReviewsForRemovedRepos( const stuck = await db .select({ id: schema.reviews.id, + originClaimReceiptId: schema.reviews.originClaimReceiptId, publicId: schema.reviews.publicId, advisoryCheckRunId: schema.reviews.advisoryCheckRunId, gateCheckRunId: schema.reviews.gateCheckRunId, @@ -549,10 +555,18 @@ async function completeRunningReviewsForRemovedRepos( return; } for (const review of stuck) { - await db - .update(schema.reviews) - .set({ status: "failed", errorMessage: message, finishedAt: new Date() }) - .where(and(eq(schema.reviews.id, review.id), eq(schema.reviews.status, "running"))); + const update = async (tx: Database) => { + const rows = await tx.update(schema.reviews) + .set({ status: "failed", errorMessage: message, finishedAt: new Date() }) + .where(and(eq(schema.reviews.id, review.id), eq(schema.reviews.status, "running"))) + .returning({ id: schema.reviews.id }); + if (rows.length && review.originClaimReceiptId) { + const client = transactionReceiptClient(tx); + await appendPreparedReceiptBatch(client, await prepareReviewObservation(client, review.id, "failed")); + } + }; + if (review.originClaimReceiptId) await db.transaction((tx) => update(tx as Database)); + else await update(db); await failCheckRuns( token, review.repoFullName, diff --git a/src/lib/hosted-review-pause.ts b/src/lib/hosted-review-pause.ts index 4f2c6c48..0fe96443 100644 --- a/src/lib/hosted-review-pause.ts +++ b/src/lib/hosted-review-pause.ts @@ -1,6 +1,14 @@ import { and, eq, inArray, sql } from "drizzle-orm"; import type { Database } from "@/lib/db"; +import { + appendPreparedReceiptBatch, + claimEvent, + lockReceiptClaim, + prepareReviewObservation, + transactionReceiptClient, + type ReviewClaimLease, +} from "@/lib/job-attempt-receipts"; import { schema } from "@/lib/db"; import { checkRunExternalId } from "@/lib/github/checks"; import { reviewDetailsUrl } from "@/lib/oauth"; @@ -33,6 +41,7 @@ export async function claimPausedHostedReview( repoFullName: string; orgSlug?: string | null; checkRunsMayExist?: boolean; + reviewClaim?: ReviewClaimLease; }, finishedAt = new Date(), ): Promise<{ @@ -42,6 +51,10 @@ export async function claimPausedHostedReview( gateCheckExternalId: string; } | null> { return db.transaction(async (tx) => { + const client = transactionReceiptClient(tx); + const lease = publication.reviewClaim; + const job = lease?.receiptClaim ? await lockReceiptClaim(client, lease) : null; + if (lease?.receiptClaim && !job?.owned) return null; const claimKey = `postil:unavailable-review:${values.repositoryId}:${values.prNumber}:${values.headSha}`; await tx.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${claimKey}, 0))`); const existing = await tx @@ -63,6 +76,7 @@ export async function claimPausedHostedReview( id: schema.reviews.id, publicId: schema.reviews.publicId, headSha: schema.reviews.headSha, + originClaimReceiptId: schema.reviews.originClaimReceiptId, advisoryCheckRunId: schema.reviews.advisoryCheckRunId, gateCheckRunId: schema.reviews.gateCheckRunId, }) @@ -107,6 +121,7 @@ export async function claimPausedHostedReview( .insert(schema.reviews) .values({ ...values, + originClaimReceiptId: lease?.receiptClaim?.receiptId ?? null, status: "failed", errorMessage: HOSTED_REVIEW_UNAVAILABLE_MESSAGE, finishedAt, @@ -136,6 +151,17 @@ export async function claimPausedHostedReview( runAfter: new Date(finishedAt.getTime() + 30_000), maxAttempts: 5, }); + const events = []; + for (const row of active) { + if (row.originClaimReceiptId) events.push(...await prepareReviewObservation(client, row.id, "cancelled")); + } + if (lease?.receiptClaim) { + events.push(claimEvent(lease.receiptClaim.jobId, Number(job!.attempts), lease.receiptClaim.receiptId, + "review_bound", "bound", String(review.id), review.publicId)); + events.push(claimEvent(lease.receiptClaim.jobId, Number(job!.attempts), lease.receiptClaim.receiptId, + "review_observed", "failed", String(review.id), review.publicId)); + } + await appendPreparedReceiptBatch(client, events); return { ...review, advisoryCheckExternalId, gateCheckExternalId }; }); } diff --git a/src/lib/job-attempt-receipts.ts b/src/lib/job-attempt-receipts.ts new file mode 100644 index 00000000..e3979478 --- /dev/null +++ b/src/lib/job-attempt-receipts.ts @@ -0,0 +1,373 @@ +import { sql, type SQL } from "drizzle-orm"; +import type { Pool, PoolClient } from "pg"; + +/** SQL transaction tails for explicitly bound review claims. */ +export interface ReceiptClient { + query(text: string, values?: unknown[]): Promise<{ rows: Record[] }>; +} + +export interface PreparedReceipt { + jobId: string; + ordinal: number; + jobKind: "review" | "review-feedback"; + claimId: string | null; + reviewId: string | null; + reviewPublicId: string | null; + eventKind: "claim" | "review_bound" | "review_observed" | "incident" | "queue_closed"; + outcome: "started" | "bound" | "staged" | "completed" | "failed" | "observed" + | "succeeded" | "retry" | "continued" | "interrupted" | "cancelled" | "coalesced" | "deferred"; + phase: "planner" | "review" | "scorer" | "respond" | null; + category: "providerError" | "invalidOutput" | "timeout" | "deadline" | null; + recovered: boolean | null; + recovery: "repair" | "fallback" | null; +} + +export const receiptAppendLockSql = "SELECT pg_advisory_xact_lock(17491, 1)"; +export const receiptHeadSql = + "SELECT r.receipt_id::text FROM job_attempt_receipts r ORDER BY r.receipt_id DESC LIMIT 1"; + +const receiptInsertSql = ` +WITH allocated AS ( + SELECT nextval('job_attempt_receipt_ids') AS id +) +INSERT INTO job_attempt_receipts ( + receipt_id, predecessor_id, claim_receipt_id, job_id, ordinal, job_kind, + review_id, review_public_id, event_kind, outcome, phase, category, recovered, + recovery, binding_receipt_id +) +SELECT id, $1::bigint, COALESCE($2::bigint, id), $3::bigint, $4::integer, $5, + $6::bigint, $7::uuid, $8, $9, $10, $11, $12::boolean, $13, + CASE WHEN $8 IN ('review_observed', 'incident') THEN ( + SELECT receipt_id FROM job_attempt_receipts + WHERE claim_receipt_id = $2::bigint AND review_id = $6::bigint + AND event_kind = 'review_bound' + ) END +FROM allocated +RETURNING receipt_id::text`; + +/** + * The caller owns a READ COMMITTED transaction and all business locks. Prepare + * and validate business state before calling; COMMIT immediately after return, + * except for assigning a new claim ID to its already locked job row. On error + * ROLLBACK the entire transaction. An uncertain COMMIT requires reconciliation. + */ +export async function appendPreparedReceiptBatch( + client: ReceiptClient, + events: readonly PreparedReceipt[], +): Promise { + const prepared = events.map((event) => { + // Reject accessors before reading any value. Only primitive domain fields + // enter the SQL arguments; an exception message or envelope is never read. + const fields = ["jobId", "ordinal", "jobKind", "claimId", "reviewId", + "reviewPublicId", "eventKind", "outcome", "phase", "category", + "recovered", "recovery"] as const; + const descriptors = Object.getOwnPropertyDescriptors(event); + if (Reflect.ownKeys(descriptors).length !== fields.length || fields.some( + (field) => !descriptors[field] || !("value" in descriptors[field]!), + )) throw new Error("invalid receipt projection"); + const value = Object.fromEntries(fields.map((field) => [field, descriptors[field]!.value])); + for (const field of fields) { + if (value[field] !== null && !["string", "number", "boolean"].includes(typeof value[field])) { + throw new Error("invalid receipt projection"); + } + } + for (const field of ["jobId", "claimId", "reviewId"]) { + if (value[field] !== null && (typeof value[field] !== "string" || !/^[1-9][0-9]{0,18}$/.test(value[field]))) { + throw new Error("invalid receipt identity"); + } + } + if (value.jobId === null || typeof value.ordinal !== "number" || !Number.isSafeInteger(value.ordinal) || value.ordinal < 1) { + throw new Error("invalid receipt identity"); + } + return fields.map((field) => value[field]); + }); + if (prepared.length === 0) return []; + const isolation = await client.query("SHOW transaction_isolation"); + if (isolation.rows[0]?.transaction_isolation !== "read committed") { + throw new Error("receipt transaction requires read committed"); + } + // Separate statements are intentional: the predecessor snapshot is acquired + // after the append lock wait, never in the statement that waits for it. + await client.query(receiptAppendLockSql); + let predecessor = (await client.query(receiptHeadSql)).rows[0]?.receipt_id; + if (typeof predecessor !== "string") throw new Error("receipt genesis unavailable"); + const ids: string[] = []; + for (const fields of prepared) { + const [job, ordinal, kind, claim, review, publicId, event, outcome, phase, category, recovered, recovery] = fields; + const result = await client.query(receiptInsertSql, [predecessor, claim, job, ordinal, + kind, review, publicId, event, outcome, phase, category, recovered, recovery]); + const id = result.rows[0]?.receipt_id; + if (typeof id !== "string") throw new Error("receipt append unavailable"); + ids.push(id); + predecessor = id; + } + return ids; +} + +/** Execute while holding the job and review locks, before the append tail. */ +export const stageOwnedReviewSql = ` +UPDATE jobs j +SET staged_review_id = r.id, + payload = j.payload || jsonb_build_object('recoveryReviewId', r.id) +FROM reviews r +WHERE j.id = $1::bigint AND j.status = 'running' + AND j.active_claim_receipt_id = $2::bigint AND j.locked_by = $3 + AND r.id = $4::bigint AND r.status = 'running' + AND EXISTS ( + SELECT 1 FROM job_attempt_receipts b + WHERE b.event_kind = 'review_bound' AND b.job_id = j.id + AND b.claim_receipt_id = j.active_claim_receipt_id + AND b.review_id = r.id AND b.review_public_id = r.public_id + ) +RETURNING j.id::text`; + +/** One snapshot, scalar projection, finite page; no ACK or application pool. */ +export const readAttemptReceiptPageSql = ` +WITH genesis AS MATERIALIZED ( + SELECT source_epoch, schema_version FROM job_attempt_receipts WHERE receipt_id = 0 +), anchor AS MATERIALIZED ( + SELECT receipt_id FROM job_attempt_receipts WHERE receipt_id = $1::bigint +), head AS MATERIALIZED ( + SELECT receipt_id FROM job_attempt_receipts ORDER BY receipt_id DESC LIMIT 1 +) +SELECT g.source_epoch::text, g.schema_version, + a.receipt_id::text AS anchor_id, h.receipt_id::text AS head_id, + p.receipt_id::text, p.predecessor_id::text, p.claim_receipt_id::text, + p.job_id::text, p.ordinal, p.job_kind, p.review_id::text, + p.review_public_id::text, p.event_kind, p.outcome, p.phase, p.category, + p.recovered, p.recovery, + to_char(p.occurred_at AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"') AS occurred_at +FROM (SELECT 1) control +LEFT JOIN genesis g ON true LEFT JOIN anchor a ON true LEFT JOIN head h ON true +LEFT JOIN LATERAL ( + SELECT receipt_id, predecessor_id, claim_receipt_id, job_id, ordinal, + job_kind, review_id, review_public_id, event_kind, outcome, phase, + category, recovered, recovery, occurred_at + FROM job_attempt_receipts WHERE receipt_id > $1::bigint + ORDER BY receipt_id LIMIT 64 +) p ON true +ORDER BY p.receipt_id`; + + +/** The expected identity is captured at claim commit, never refreshed on completion. */ +export interface ReceiptClaim { + readonly jobId: string; + readonly receiptId: string; +} +export interface ReviewClaimLease { + id: number; + lockedBy: string; + lockedAt: Date; + receiptClaim?: ReceiptClaim; +} + +/** Bind trusted domain statements to the caller's supported Drizzle executor. */ +export function transactionReceiptClient(tx: { + execute(query: SQL): Promise<{ rows: Record[] }>; +}): ReceiptClient { + return { + async query(text, values = []) { + const parts = text.split(/\$(\d+)/g).map((part, index) => index % 2 + ? sql`${values[Number(part) - 1]}` : sql.raw(part)); + return tx.execute(sql.join(parts, sql``)); + }, + }; +} + +export async function lockReceiptClaim(client: ReceiptClient, lease: ReviewClaimLease) { + const claim = lease.receiptClaim; + if (!claim) return null; + const result = await client.query(`SELECT j.id::text, j.kind, j.attempts, + j.active_claim_receipt_id::text, j.staged_review_id::text, + (j.status = 'running' AND j.active_claim_receipt_id = $2::bigint + AND j.locked_by = $3 AND j.locked_at = $4::timestamptz) AS owned + FROM jobs j WHERE j.id = $1::bigint FOR UPDATE`, + [claim.jobId, claim.receiptId, lease.lockedBy, lease.lockedAt]); + return result.rows[0] ?? null; +} + +export function claimEvent(jobId: string, ordinal: number, claimId: string | null, + eventKind: PreparedReceipt["eventKind"], outcome: PreparedReceipt["outcome"], + reviewId: string | null = null, reviewPublicId: string | null = null): PreparedReceipt { + return { jobId, ordinal, jobKind: "review", claimId, reviewId, reviewPublicId, + eventKind, outcome, phase: null, category: null, recovered: null, recovery: null }; +} + +/** All business mutations in body precede this transaction's single append tail. */ +export async function transitionReceiptClaim( + pool: Pool, lease: ReviewClaimLease, body: (client: PoolClient) => Promise, + outcome: (result: T) => PreparedReceipt["outcome"], +): Promise { + const client = await pool.connect(); + try { + await client.query("BEGIN"); + const row = await lockReceiptClaim(client, lease); + if (!row?.owned) { await client.query("ROLLBACK"); return "lost"; } + const result = await body(client); + if (result !== "lost") { + await client.query("UPDATE jobs SET active_claim_receipt_id = NULL WHERE id = $1::bigint", + [lease.receiptClaim!.jobId]); + await appendPreparedReceiptBatch(client, [claimEvent(lease.receiptClaim!.jobId, + Number(row.attempts), lease.receiptClaim!.receiptId, "queue_closed", outcome(result))]); + } + await client.query("COMMIT"); + return result; + } catch (error) { + await client.query("ROLLBACK").catch(() => undefined); + throw error; + } finally { client.release(); } +} + +/** Prepare terminal observations before the append lock; legacy rows stay unqualified. */ +export async function prepareReviewObservation(client: ReceiptClient, reviewId: number, + outcome: PreparedReceipt["outcome"], lease?: ReviewClaimLease): Promise { + const result = await client.query(`SELECT b.claim_receipt_id::text, b.job_id::text, + b.ordinal, b.review_id::text, b.review_public_id::text + FROM job_attempt_receipts b JOIN reviews r ON r.id = b.review_id + WHERE b.event_kind = 'review_bound' AND r.id = $1::bigint + AND b.claim_receipt_id = COALESCE($2::bigint, r.origin_claim_receipt_id) + AND b.review_public_id = r.public_id + AND NOT EXISTS (SELECT 1 FROM job_attempt_receipts o + WHERE o.event_kind = 'review_observed' AND o.claim_receipt_id = b.claim_receipt_id + AND o.review_id = r.id AND o.outcome = $3)`, + [String(reviewId), lease?.receiptClaim?.receiptId ?? null, outcome]); + return result.rows.map((r) => claimEvent(String(r.job_id), Number(r.ordinal), + String(r.claim_receipt_id), "review_observed", outcome, String(r.review_id), String(r.review_public_id))); +} + +export interface ReceiptSourceBinding { + /** Independently admitted resource creation/incarnation, not a database self-report. */ + target: string; + epoch: string; + schema: number; +} +export interface ReceiptAdmissionState { + /** Monotonic external revision, persisted independently of the recoverable database. */ + revision: number; + binding: ReceiptSourceBinding | null; + prefix: string; + /** Confirmed report prefix, committed by the external owner before ACK. */ + reportedThrough?: string; + /** Retired incarnations stay in the external owner's durable state across code rollback. */ + retiredTargets?: readonly string[]; +} +export interface ReceiptPageProposal { + revision: number; + binding: ReceiptSourceBinding; + after: string; + through: string; + rows: Record[]; +} +const sameBinding = (a: ReceiptSourceBinding | null, b: ReceiptSourceBinding) => + a?.target === b.target && a.epoch === b.epoch && a.schema === b.schema; + +/** A recovery cut affects correlation admission, never application work or recovery. */ +export function closeReceiptAdmission(state: ReceiptAdmissionState): ReceiptAdmissionState { + return { ...state, revision: state.revision + 1, binding: null, + reportedThrough: undefined, + retiredTargets: [...new Set([...(state.retiredTargets ?? []), + ...(state.binding ? [state.binding.target] : [])])] }; +} + +/** Called by the existing admission owner under the same durable lock as cut and ACK. + * The old source prefix remains retained in that owner's source-specific state. + */ +export function admitReceiptSource(state: ReceiptAdmissionState, binding: ReceiptSourceBinding, + expectedRevision: number, sourcePrefix: string): ReceiptAdmissionState | null { + if (sameBinding(state.binding, binding) && state.revision === expectedRevision + 1) return state; + if (state.binding || state.revision !== expectedRevision || !binding.target + || !binding.epoch || binding.schema !== 1 || state.retiredTargets?.includes(binding.target) + || !/^(0|[1-9][0-9]{0,18})$/.test(sourcePrefix)) return null; + return { ...state, revision: state.revision + 1, binding: { ...binding }, prefix: sourcePrefix, + reportedThrough: undefined }; +} + +/** The admission owner serializes this update and ACK with its durable state commit. */ +export function receiptPageAdmitted(state: ReceiptAdmissionState, page: ReceiptPageProposal): boolean { + return sameBinding(state.binding, page.binding) && state.revision === page.revision + && state.prefix === page.after; +} + +/** The existing report owner commits this only after confirmed delivery under its admission lock. */ +export function recordReceiptPageReported(state: ReceiptAdmissionState, page: ReceiptPageProposal, + through: string): ReceiptAdmissionState | null { + if (!receiptPageAdmitted(state, page) || (through !== page.after + && !page.rows.some((row) => row.receipt_id === through))) return null; + return { ...state, reportedThrough: through }; +} + +/** Check admission under the owner's lock before delivery, and again before ACK. */ +export function acknowledgeReceiptPage(state: ReceiptAdmissionState, page: ReceiptPageProposal, + reportedThrough: string): ReceiptAdmissionState | null { + if (!receiptPageAdmitted(state, page) || !/^(0|[1-9][0-9]*)$/.test(reportedThrough) + || !state.reportedThrough || BigInt(reportedThrough) > BigInt(state.reportedThrough)) return null; + // Only a genuinely reported prefix of this exact page may advance the cursor. + if (reportedThrough !== page.after && !page.rows.some((r) => r.receipt_id === reportedThrough)) return null; + return { ...state, prefix: reportedThrough, reportedThrough: undefined }; +} + +/** The target witness comes from the admitted executor, outside SQL/stdout content. */ +export async function readAdmittedReceiptPage( + state: ReceiptAdmissionState, + execute: (text: string, values: unknown[]) => Promise<{ + target: string; rows: Record[]; + }>, +): Promise<{ status: "UNKNOWN" } | { status: "READY"; page: ReceiptPageProposal }> { + if (!state.binding || !/^(0|[1-9][0-9]{0,18})$/.test(state.prefix)) return { status: "UNKNOWN" }; + const snapshot = { ...state, binding: { ...state.binding } }; + try { + const response = await execute(readAttemptReceiptPageSql, [snapshot.prefix]); + const columns = ["source_epoch", "schema_version", "anchor_id", "head_id", "receipt_id", + "predecessor_id", "claim_receipt_id", "job_id", "ordinal", "job_kind", "review_id", + "review_public_id", "event_kind", "outcome", "phase", "category", "recovered", "recovery", "occurred_at"]; + if (!Array.isArray(response.rows) || response.rows.length > 64) return { status: "UNKNOWN" }; + for (const row of response.rows) { + if (!row || ![Object.prototype, null].includes(Object.getPrototypeOf(row))) return { status: "UNKNOWN" }; + const fields = Object.getOwnPropertyDescriptors(row); + if (Reflect.ownKeys(fields).length !== columns.length || columns.some((name) => { + const field = fields[name]; + if (!field || !("value" in field)) return true; + const value = field.value; + return value !== null && !(typeof value === "string" && value.length <= 64) + && !(typeof value === "number" && Number.isSafeInteger(value)) && typeof value !== "boolean"; + })) return { status: "UNKNOWN" }; + } + const control = response.rows[0]; + if (response.target !== snapshot.binding.target || !control + || control.source_epoch !== snapshot.binding.epoch || control.schema_version !== snapshot.binding.schema + || control.anchor_id !== snapshot.prefix || typeof control.head_id !== "string" + || BigInt(control.head_id) < BigInt(snapshot.prefix) + || Buffer.byteLength(JSON.stringify(response.rows)) > 65536) { + return { status: "UNKNOWN" }; + } + let predecessor = snapshot.prefix; + const rows = response.rows.filter((r) => r.receipt_id !== null); + for (const row of rows) { + if (row.source_epoch !== control.source_epoch || row.schema_version !== control.schema_version + || row.anchor_id !== control.anchor_id || row.head_id !== control.head_id + || typeof row.receipt_id !== "string" || !/^[1-9][0-9]{0,18}$/.test(row.receipt_id) + || row.predecessor_id !== predecessor || BigInt(row.receipt_id) <= BigInt(predecessor) + || BigInt(row.receipt_id) > BigInt(control.head_id)) return { status: "UNKNOWN" }; + predecessor = row.receipt_id; + } + return { status: "READY", page: { revision: snapshot.revision, binding: snapshot.binding, + after: snapshot.prefix, through: predecessor, rows } }; + } catch { return { status: "UNKNOWN" }; } +} + + +/** Called after the caller's locked administrative mutations and before its append tail. */ +export async function prepareClosedJobReceipts(client: ReceiptClient, + jobIds: readonly string[], outcome?: PreparedReceipt["outcome"]): Promise { + if (!jobIds.length) return []; + const result = await client.query(`SELECT j.id::text, j.status, c.ordinal, c.receipt_id::text + FROM jobs j JOIN job_attempt_receipts c ON c.receipt_id = j.active_claim_receipt_id + WHERE j.id = ANY($1::bigint[]) AND j.status <> 'running' AND c.event_kind = 'claim' + AND NOT EXISTS (SELECT 1 FROM job_attempt_receipts x + WHERE x.claim_receipt_id = c.receipt_id AND x.event_kind = 'queue_closed') + ORDER BY j.id`, [jobIds]); + if (result.rows.length) await client.query(`UPDATE jobs SET active_claim_receipt_id = NULL + WHERE id = ANY($1::bigint[])`, [result.rows.map((r) => r.id)]); + return result.rows.map((r) => claimEvent(String(r.id), Number(r.ordinal), String(r.receipt_id), + "queue_closed", outcome ?? (r.status === "queued" ? "retry" : r.status === "failed" ? "failed" : "cancelled"))); +} diff --git a/src/lib/private-worker-rehearsal.ts b/src/lib/private-worker-rehearsal.ts index 380072ff..edc5870a 100644 --- a/src/lib/private-worker-rehearsal.ts +++ b/src/lib/private-worker-rehearsal.ts @@ -1,3 +1,9 @@ +import { + appendPreparedReceiptBatch, + lockReceiptClaim, + prepareClosedJobReceipts, + type ReviewClaimLease, +} from "@/lib/job-attempt-receipts"; import { randomUUID } from "node:crypto"; import type { Pool, PoolClient } from "pg"; @@ -31,6 +37,7 @@ export interface ArmPrivateWorkerRehearsalInput extends PrivateWorkerRehearsalTa export interface StagedPrivateWorkerRehearsalInput { reviewId: number; reviewJobId: number; + reviewClaim?: ReviewClaimLease; repoFullName: string; prNumber: number; headSha: string; @@ -246,6 +253,10 @@ export async function consumePrivateWorkerRehearsalAfterStaging( const client = await pool.connect(); try { await client.query("BEGIN"); + if (input.reviewClaim?.receiptClaim && !(await lockReceiptClaim(client, input.reviewClaim))?.owned) { + await client.query("ROLLBACK"); + return null; + } const request = await client.query<{ nonce: string; review_public_id: string; @@ -288,7 +299,7 @@ export async function consumePrivateWorkerRehearsalAfterStaging( FOR UPDATE OF rehearsal`, [ input.reviewId, - input.reviewJobId, + input.reviewClaim?.receiptClaim?.jobId ?? input.reviewJobId, input.repoFullName, input.prNumber, input.headSha, @@ -340,7 +351,7 @@ export async function consumePrivateWorkerRehearsalAfterStaging( AND status = 'running' AND payload->>'recoveryReviewId' = $3 AND left(locked_by, length($4) + 1) = $4 || '#'`, - [input.reviewJobId, row.nonce, String(input.reviewId), input.workerInstanceId], + [input.reviewClaim?.receiptClaim?.jobId ?? input.reviewJobId, row.nonce, String(input.reviewId), input.workerInstanceId], ); if ((marked.rowCount ?? 0) !== 1) { throw new Error("private worker rehearsal lost its exact recovery job lease"); @@ -413,6 +424,7 @@ export async function reconcilePrivateWorkerRehearsals( const client = await pool.connect(); try { await client.query("BEGIN"); + await client.query("SELECT id FROM jobs WHERE id=$1::bigint FOR UPDATE", [rehearsal.job_id]); const verified = await client.query( `UPDATE private_worker_rehearsals SET state = 'replacement_verified', @@ -477,6 +489,8 @@ export async function reconcilePrivateWorkerRehearsals( result.rehearsalsFailed += 1; } } + const events = await prepareClosedJobReceipts(client, [rehearsal.job_id], "interrupted"); + await appendPreparedReceiptBatch(client, events); await client.query("COMMIT"); } catch (error) { await client.query("ROLLBACK").catch(() => undefined); diff --git a/src/lib/queue.ts b/src/lib/queue.ts index 007cfb23..b213982c 100644 --- a/src/lib/queue.ts +++ b/src/lib/queue.ts @@ -2,6 +2,14 @@ import type { Pool, PoolClient } from "pg"; import { redactAndTruncate } from "@/lib/redact"; import type { ReviewTriggerContext } from "@/lib/review-trigger"; +import { + appendPreparedReceiptBatch, + claimEvent, + transitionReceiptClaim, + prepareClosedJobReceipts, + type ReceiptClaim, + type ReviewClaimLease, +} from "@/lib/job-attempt-receipts"; /** * Postgres-native job queue. @@ -16,6 +24,7 @@ import type { ReviewTriggerContext } from "@/lib/review-trigger"; */ export interface ClaimedJob { + receiptClaim?: ReceiptClaim; id: number; kind: string; payload: Record; @@ -121,6 +130,7 @@ export interface GithubReactionJobPayload extends Record { } export interface ExternalSideEffectLease { + receiptClaim?: ReceiptClaim; id: number; lockedBy: string; lockedAt: Date; @@ -138,8 +148,9 @@ export async function externalSideEffectLeaseActive( AND status = 'running' AND locked_by = $2 AND locked_at = $3 + AND ($4::bigint IS NULL OR active_claim_receipt_id = $4::bigint) LIMIT 1`, - [lease.id, lease.lockedBy, lease.lockedAt], + [lease.receiptClaim?.jobId ?? lease.id, lease.lockedBy, lease.lockedAt, lease.receiptClaim?.receiptId ?? null], ); return (result.rowCount ?? 0) === 1; } @@ -156,62 +167,87 @@ export async function cancelPullRequestPublication( prNumber: number; }, ): Promise { - const result = await pool.query( - `WITH matching_jobs AS MATERIALIZED ( - SELECT id - FROM jobs - WHERE kind IN ('review', 'respond', 'respond-failure-comment', 'webhook-comment') - AND status IN ('queued', 'running') - AND payload->>'installationId' = $1::text - AND payload->>'sourceInstallationId' = $2::text - AND payload->>'sourceOrgId' = $3::text - AND payload->>'githubRepoId' = $4::text - AND lower(payload->>'repoFullName') = lower($5) - AND COALESCE((payload->>'prNumber')::integer, (payload->>'number')::integer) = $6 - AND COALESCE((payload->>'isPr')::boolean, kind = 'review') - ), cancelled_deliveries AS ( - UPDATE respond_deliveries delivery - SET state = 'cancelled', - publication_lease_id = NULL, - publication_lease_expires_at = NULL, - delivery_lease_expires_at = NULL, - cancelled_at = now(), - updated_at = now() - WHERE delivery.source_github_installation_id = $1::bigint - AND delivery.source_installation_id = $2::bigint - AND delivery.source_org_id = $3::bigint - AND delivery.source_github_repo_id = $4::bigint - AND lower(delivery.repo_full_name) = lower($5) - AND delivery.issue_number = $6 - AND delivery.is_pr - AND delivery.state IN ('prepared', 'delivering') - RETURNING delivery.job_id - ), delivery_jobs AS ( - SELECT job.id - FROM jobs job - JOIN cancelled_deliveries delivery - ON job.kind = 'respond-delivery' - AND job.payload->>'respondJobId' = delivery.job_id::text - WHERE job.status IN ('queued', 'running') - ), terminal_ids AS ( - SELECT id FROM matching_jobs - UNION SELECT id FROM delivery_jobs - ) - UPDATE jobs job - SET status = 'done', locked_at = NULL, locked_by = NULL, - last_error = 'pull request closed' - WHERE job.id IN (SELECT id FROM terminal_ids) - AND job.status IN ('queued', 'running')`, - [ - String(input.installationId), - String(input.sourceInstallationId), - String(input.sourceOrgId), - String(input.githubRepoId), - input.repoFullName, - input.prNumber, - ], - ); - return result.rowCount ?? 0; + const run = async (client: Pick, qualified: boolean) => { + if (qualified) await client.query(`SELECT id FROM jobs + WHERE status IN ('queued', 'running') AND kind = 'review' + AND payload->>'installationId' = $1 AND payload->>'sourceInstallationId' = $2 + AND payload->>'sourceOrgId' = $3 AND payload->>'githubRepoId' = $4 + AND lower(payload->>'repoFullName') = lower($5) + AND (payload->>'prNumber')::integer = $6 ORDER BY id FOR UPDATE`, + [String(input.installationId), String(input.sourceInstallationId), String(input.sourceOrgId), + String(input.githubRepoId), input.repoFullName, input.prNumber]); + const result = await client.query( + `WITH matching_jobs AS MATERIALIZED ( + SELECT id + FROM jobs + WHERE kind IN ('review', 'respond', 'respond-failure-comment', 'webhook-comment') + AND status IN ('queued', 'running') + AND payload->>'installationId' = $1::text + AND payload->>'sourceInstallationId' = $2::text + AND payload->>'sourceOrgId' = $3::text + AND payload->>'githubRepoId' = $4::text + AND lower(payload->>'repoFullName') = lower($5) + AND COALESCE((payload->>'prNumber')::integer, (payload->>'number')::integer) = $6 + AND COALESCE((payload->>'isPr')::boolean, kind = 'review') + ), cancelled_deliveries AS ( + UPDATE respond_deliveries delivery + SET state = 'cancelled', + publication_lease_id = NULL, + publication_lease_expires_at = NULL, + delivery_lease_expires_at = NULL, + cancelled_at = now(), + updated_at = now() + WHERE delivery.source_github_installation_id = $1::bigint + AND delivery.source_installation_id = $2::bigint + AND delivery.source_org_id = $3::bigint + AND delivery.source_github_repo_id = $4::bigint + AND lower(delivery.repo_full_name) = lower($5) + AND delivery.issue_number = $6 + AND delivery.is_pr + AND delivery.state IN ('prepared', 'delivering') + RETURNING delivery.job_id + ), delivery_jobs AS ( + SELECT job.id + FROM jobs job + JOIN cancelled_deliveries delivery + ON job.kind = 'respond-delivery' + AND job.payload->>'respondJobId' = delivery.job_id::text + WHERE job.status IN ('queued', 'running') + ), terminal_ids AS ( + SELECT id FROM matching_jobs + UNION SELECT id FROM delivery_jobs + ) + UPDATE jobs job + SET status = 'done', locked_at = NULL, locked_by = NULL, + last_error = 'pull request closed' + WHERE job.id IN (SELECT id FROM terminal_ids) + AND job.status IN ('queued', 'running') RETURNING job.id::text`, + [ + String(input.installationId), + String(input.sourceInstallationId), + String(input.sourceOrgId), + String(input.githubRepoId), + input.repoFullName, + input.prNumber, + ], + ); + if (qualified) { + const events = await prepareClosedJobReceipts(client, result.rows.map((r) => String(r.id)), "cancelled"); + await appendPreparedReceiptBatch(client, events); + } + return result.rowCount ?? 0; + }; + if (!("connect" in pool) || !("totalCount" in pool)) return run(pool, false); + const client = await (pool as Pool).connect(); + try { + await client.query("BEGIN"); + const count = await run(client, true); + await client.query("COMMIT"); + return count; + } catch (error) { + await client.query("ROLLBACK").catch(() => undefined); + throw error; + } finally { client.release(); } } export interface RespondDeliveryJobPayload extends Record { @@ -833,6 +869,12 @@ export async function claimJob( await client.query("COMMIT"); return null; } + const receipts = row.kind === "review" && (await client.query( + "SELECT to_regclass('job_attempt_receipts') IS NOT NULL AS available", + )).rows[0]?.available === true; + if (receipts) { + await client.query("UPDATE jobs SET active_claim_receipt_id = NULL WHERE id = $1", [row.id]); + } const claimed = await client.query<{ locked_at: Date }>( `UPDATE jobs SET status = 'running', attempts = attempts + 1, @@ -843,6 +885,13 @@ export async function claimJob( ); const lockedAt = claimed.rows[0]?.locked_at; if (!lockedAt) throw new Error("claimed job returned no lock timestamp"); + let receiptClaim: ReceiptClaim | undefined; + if (receipts) { + const [receiptId] = await appendPreparedReceiptBatch(client, + [claimEvent(row.id, row.attempts + 1, null, "claim", "started")]); + await client.query("UPDATE jobs SET active_claim_receipt_id = $2::bigint WHERE id = $1::bigint", [row.id, receiptId]); + receiptClaim = Object.freeze({ jobId: row.id, receiptId: receiptId! }); + } await client.query("COMMIT"); return { id: Number(row.id), @@ -853,6 +902,7 @@ export async function claimJob( createdAt: row.created_at, lockedAt, lockedBy: workerId, + ...(receiptClaim ? { receiptClaim } : {}), }; } catch (err) { await client.query("ROLLBACK").catch(() => undefined); @@ -862,6 +912,9 @@ export async function claimJob( } } +type TransitionJob = Pick & + Partial> & { id: number | string }; + /** * Mark a job done. Scoped by `status = 'running' AND locked_by = $lockedBy` so * a worker finishing late cannot stamp `done` over a job the watchdog already @@ -870,8 +923,11 @@ export async function claimJob( */ export async function completeJob( pool: Pool, - job: Pick, + job: TransitionJob, ): Promise<"done" | "coalesced" | "lost"> { + if (job.receiptClaim) return transitionReceiptClaim(pool, job as ReviewClaimLease, + (client) => completeJob(client as unknown as Pool, { ...job, receiptClaim: undefined, id: job.receiptClaim!.jobId }), + (result) => result === "coalesced" ? "coalesced" : "succeeded"); const result = await pool.query<{ outcome: "done" | "coalesced" | "lost" }>( `WITH transitioned AS ( UPDATE jobs @@ -898,10 +954,18 @@ export async function completeJob( export async function continueClaimedJob( pool: Pool, - job: Pick, + job: TransitionJob, payload: Record, options: { runAfter?: Date } = {}, ): Promise { + if (job.receiptClaim) { + const result = await transitionReceiptClaim(pool, job as ReviewClaimLease, async (client) => { + await continueClaimedJob(client as unknown as Pool, { ...job, receiptClaim: undefined, id: job.receiptClaim!.jobId }, payload, options); + return "continued"; + }, () => "continued"); + if (result === "lost") throw new Error("job continuation lost its lease"); + return; + } const result = await pool.query( `UPDATE jobs SET payload = $3, status = 'queued', attempts = 0, @@ -921,14 +985,34 @@ export async function requeueJobsOwnedBy( lockedByPrefix: string, reason: string, kinds: readonly string[], - jobIds: readonly number[], + jobIds: readonly (number | string)[], + expectedClaims?: readonly ReviewClaimLease[], ): Promise { + if (expectedClaims) { + let count = 0; + for (const lease of expectedClaims) { + if (!jobIds.includes(lease.id) || !lease.lockedBy.startsWith(lockedByPrefix)) continue; + if (!lease.receiptClaim) { + count += await requeueJobsOwnedBy(pool, lockedByPrefix, reason, kinds, [lease.id]); + continue; + } + const result = await transitionReceiptClaim(pool, lease, async (client) => { + if (!await requeueJobsOwnedBy(client as unknown as Pool, lockedByPrefix, reason, kinds, + [lease.receiptClaim!.jobId])) return "lost"; + const row = (await client.query("SELECT status FROM jobs WHERE id = $1::bigint", + [lease.receiptClaim!.jobId])).rows[0]; + return row?.status === "done" ? "coalesced" : "requeued"; + }, (result) => result === "coalesced" ? "coalesced" : "interrupted"); + if (result !== "lost") count += 1; + } + return count; + } if (!lockedByPrefix) throw new Error("requeueJobsOwnedBy requires a lock-owner prefix"); const allowedKinds = [...new Set(kinds.filter(Boolean))]; if (allowedKinds.length === 0) { throw new Error("requeueJobsOwnedBy requires at least one safe job kind"); } - const ownedJobIds = [...new Set(jobIds.filter((id) => Number.isSafeInteger(id) && id > 0))]; + const ownedJobIds = [...new Set(jobIds.filter((id) => typeof id === "string" ? /^[1-9][0-9]*$/.test(id) : Number.isSafeInteger(id) && id > 0))]; if (ownedJobIds.length === 0) return 0; const redactedReason = redactAndTruncate(reason, 2000); const result = await pool.query<{ count: string }>( @@ -992,7 +1076,7 @@ export function backoffMs(attempts: number): number { */ export async function failJob( pool: Pool, - job: Pick, + job: TransitionJob & Pick, error: string, opts: { permanent?: boolean; @@ -1003,6 +1087,9 @@ export async function failJob( }; } = {}, ): Promise<"retried" | "failed" | "coalesced" | "lost"> { + if (job.receiptClaim) return transitionReceiptClaim(pool, job as ReviewClaimLease, + (client) => failJob(client as unknown as Pool, { ...job, receiptClaim: undefined, id: job.receiptClaim!.jobId }, error, opts), + (result) => result === "retried" ? "retry" : result === "coalesced" ? "coalesced" : "failed"); const redactedError = redactAndTruncate(error, 2000); if (!opts.permanent && job.attempts < job.maxAttempts) { const delay = backoffMs(job.attempts); @@ -1119,9 +1206,12 @@ export async function failJob( /** Requeue reconciliation work until its target state is superseded or published. */ export async function retryJobIndefinitely( pool: Pool, - job: Pick, + job: TransitionJob & Pick, error: string, ): Promise<"retried" | "lost"> { + if (job.receiptClaim) return transitionReceiptClaim(pool, job as ReviewClaimLease, + (client) => retryJobIndefinitely(client as unknown as Pool, { ...job, receiptClaim: undefined, id: job.receiptClaim!.jobId }, error), + () => "retry"); const redactedError = redactAndTruncate(error, 2000); const delay = backoffMs(job.attempts); const res = await pool.query( diff --git a/src/lib/release-job-rollout.ts b/src/lib/release-job-rollout.ts index 7916dbf8..8da4273f 100644 --- a/src/lib/release-job-rollout.ts +++ b/src/lib/release-job-rollout.ts @@ -1,5 +1,11 @@ import type { Pool } from "pg"; +import { + appendPreparedReceiptBatch, + claimEvent, + lockReceiptClaim, + type ReviewClaimLease, +} from "@/lib/job-attempt-receipts"; import { HOSTED_REVIEW_UNAVAILABLE_MESSAGE } from "@/lib/review-outcome"; export const RELEASE_V1_JOB_KINDS = [ @@ -258,7 +264,7 @@ export async function deactivateHostedInferenceRelease( /** Atomically park a claimed hosted review until a verified managed release activates. */ export async function deferHostedReviewForRelease( pool: Pool, - job: { id: number; lockedBy: string }, + job: { id: number; lockedBy: string; lockedAt?: Date; receiptClaim?: ReviewClaimLease["receiptClaim"] }, releaseSha: string, ): Promise<"deferred" | "released"> { const normalized = normalizedReleaseSha(releaseSha); @@ -269,6 +275,11 @@ export async function deferHostedReviewForRelease( "SELECT pg_advisory_xact_lock(hashtextextended($1, 0))", [HOSTED_INFERENCE_LOCK], ); + const claim = job.receiptClaim ? await lockReceiptClaim(client, job as ReviewClaimLease) : null; + if (job.receiptClaim && !claim?.owned) { + await client.query("ROLLBACK"); + throw new Error("hosted review release deferral lost its queue claim"); + } const active = await client.query<{ active: boolean }>( `SELECT EXISTS ( SELECT 1 FROM deployment_capabilities WHERE name = $1 @@ -290,11 +301,16 @@ export async function deferHostedReviewForRelease( END WHERE id = $1 AND kind = 'review' AND status = 'running' AND locked_by = $2`, - [job.id, job.lockedBy, activated, normalized], + [job.receiptClaim?.jobId ?? job.id, job.lockedBy, activated, normalized], ); if ((updated.rowCount ?? 0) !== 1) { throw new Error("hosted review release deferral lost its queue claim"); } + if (job.receiptClaim) { + await client.query("UPDATE jobs SET active_claim_receipt_id = NULL WHERE id = $1::bigint", [job.receiptClaim.jobId]); + await appendPreparedReceiptBatch(client, [claimEvent(job.receiptClaim.jobId, + Number(claim!.attempts), job.receiptClaim.receiptId, "queue_closed", "deferred")]); + } await client.query("COMMIT"); return activated ? "released" : "deferred"; } catch (error) { diff --git a/src/lib/review-completion.ts b/src/lib/review-completion.ts index b543bb63..6769749c 100644 --- a/src/lib/review-completion.ts +++ b/src/lib/review-completion.ts @@ -1,6 +1,10 @@ import { and, eq, isNotNull, sql } from "drizzle-orm"; import type { Database } from "@/lib/db"; +import { + appendPreparedReceiptBatch, claimEvent, lockReceiptClaim, prepareReviewObservation, + transactionReceiptClient, type ReviewClaimLease, +} from "@/lib/job-attempt-receipts"; import { schema } from "@/lib/db"; import { computeEffectiveGate, @@ -10,12 +14,14 @@ import { import type { ReviewConfigProvenance } from "@/lib/github/contents"; import { lockReviewApprovalState } from "@/lib/finding-approvals"; import { lockOrganizationGateMode } from "@/lib/gate-mode"; +import { reconcileHostedReviewSpendFromReceipt } from "@/lib/hosted-usage-reservations"; import { persistPublicationReceipt, type PublicationReceipt, } from "@/lib/publication-receipt"; interface ReviewCompletionAccountingInput { + reviewClaim?: ReviewClaimLease; reviewId: number; usage: Array<{ orgId: number | null; @@ -56,6 +62,9 @@ export interface StagedReviewCompletionInput extends Pick< | "publicationReceipt" > { reviewJobId?: number; + reviewClaim?: ReviewClaimLease; + /** Trusted CLI spend settles atomically when claim or terminal state rejects completion. */ + hostedSpend?: Parameters[1]; } function requireEnvelopeGateTruth( @@ -90,7 +99,8 @@ async function persistReviewCompletionAccounting( if (input.hostedUsageReservationId) { const reservation = ( await db - .select({ reservedMicros: schema.hostedUsageReservations.reservedMicros }) + .select({ reservedMicros: schema.hostedUsageReservations.reservedMicros, + status: schema.hostedUsageReservations.status, reviewId: schema.hostedUsageReservations.reviewId }) .from(schema.hostedUsageReservations) .where(eq(schema.hostedUsageReservations.id, input.hostedUsageReservationId)) .limit(1) @@ -137,10 +147,14 @@ async function persistReviewCompletionAccounting( ) .returning({ id: schema.hostedUsageReservations.id }); if (reconciled.length !== 1) { - throw new Error("hosted usage reservation is not active"); + if (reservation.status !== "reconciled" || reservation.reviewId !== input.reviewId) { + throw new Error("hosted usage reservation is not active"); + } + // A late producer can settle this same review before its recovery owner finalizes it. + persistedUsageRows.length = 0; } } - await db.insert(schema.usageEvents).values(persistedUsageRows); + if (persistedUsageRows.length > 0) await db.insert(schema.usageEvents).values(persistedUsageRows); await db.insert(schema.jobs).values({ kind: "gate-state-sync", payload: { @@ -161,8 +175,35 @@ export async function stageReviewCompletionCandidate( db: Database, input: StagedReviewCompletionInput, orgId: number | null, -): Promise { +): Promise { return db.transaction(async (tx) => { + const client = transactionReceiptClient(tx); + const lease = input.reviewClaim; + if (lease?.receiptClaim && input.reviewJobId !== undefined + && (Number.isSafeInteger(input.reviewJobId) + ? String(input.reviewJobId) !== lease.receiptClaim.jobId : input.reviewJobId !== lease.id)) { + throw new Error("staging job identity mismatch"); + } + const job = lease?.receiptClaim ? await lockReceiptClaim(client, lease) : null; + let alreadyStaged = false; + if (lease?.receiptClaim) { + const binding = await client.query(`SELECT EXISTS (SELECT 1 FROM job_attempt_receipts o + WHERE o.claim_receipt_id = b.claim_receipt_id AND o.review_id = r.id + AND o.event_kind = 'review_observed' AND o.outcome = 'staged') AS staged, + r.envelope = $4::jsonb AS same_envelope, + (SELECT p.receipt_id FROM review_publication_receipts p WHERE p.review_id = r.id) AS publication_id + FROM job_attempt_receipts b JOIN reviews r + ON r.id = b.review_id AND r.public_id = b.review_public_id + WHERE b.claim_receipt_id = $1::bigint AND b.job_id = $2::bigint + AND b.event_kind = 'review_bound' AND r.id = $3::bigint`, + [lease.receiptClaim.receiptId, lease.receiptClaim.jobId, String(input.reviewId), JSON.stringify(input.envelope)]); + if (binding.rows.length !== 1) throw new Error("staging review claim binding is missing"); + alreadyStaged = binding.rows[0]!.staged === true; + if (alreadyStaged && (binding.rows[0]!.same_envelope !== true + || (input.publicationReceipt && binding.rows[0]!.publication_id !== input.publicationReceipt.receiptId))) { + throw new Error("staged review evidence changed"); + } + } await lockReviewApprovalState(tx, input.reviewId); const gateTruth = requireEnvelopeGateTruth( input.envelope, @@ -190,7 +231,19 @@ export async function stageReviewCompletionCandidate( eq(schema.reviews.status, "running"), ), ) - .returning({ id: schema.reviews.id }); + .returning({ id: schema.reviews.id, repositoryId: schema.reviews.repositoryId, + triggerSource: schema.reviews.triggerSource }); + if (input.hostedSpend && (rows.length === 0 || (lease?.receiptClaim && !job?.owned))) { + const review = rows[0] ?? (await tx + .select({ repositoryId: schema.reviews.repositoryId, triggerSource: schema.reviews.triggerSource }) + .from(schema.reviews).where(eq(schema.reviews.id, input.reviewId)).limit(1))[0]; + if (!review || input.hostedSpend.reviewId !== input.reviewId + || input.hostedSpend.repositoryId !== review.repositoryId + || input.hostedSpend.triggerSource !== review.triggerSource) { + throw new Error("staging hosted spend identity mismatch"); + } + await reconcileHostedReviewSpendFromReceipt(tx as Database, input.hostedSpend); + } if (rows.length === 0) { return { staged: false, @@ -200,12 +253,12 @@ export async function stageReviewCompletionCandidate( }; } - await persistPublicationReceipt(tx as Database, { + if (!alreadyStaged) await persistPublicationReceipt(tx as Database, { reviewId: input.reviewId, envelope: gateTruth.envelope, receipt: input.publicationReceipt, }); - if (input.reviewJobId !== undefined) { + if (!lease?.receiptClaim && input.reviewJobId !== undefined) { await tx .update(schema.jobs) .set({ @@ -216,7 +269,28 @@ export async function stageReviewCompletionCandidate( .where(eq(schema.jobs.id, input.reviewJobId)); } + if (lease?.receiptClaim) { + if (job?.owned) { + const updated = await client.query(`UPDATE jobs SET staged_review_id = $3::bigint, + payload = payload || jsonb_build_object('recoveryReviewId', $3::bigint) + WHERE id = $1::bigint AND status = 'running' AND active_claim_receipt_id = $2::bigint + AND locked_by = $4 AND locked_at = $5::timestamptz RETURNING id`, + [lease.receiptClaim.jobId, lease.receiptClaim.receiptId, String(input.reviewId), lease.lockedBy, lease.lockedAt]); + if (updated.rows.length !== 1) throw new Error("staging lost its locked claim"); + } + const events = await prepareReviewObservation(client, input.reviewId, "staged", lease); + if (events.length) { + for (const incident of gateTruth.envelope.modelIncidents ?? []) { + events.push({ ...claimEvent(lease.receiptClaim.jobId, events[0]!.ordinal, + lease.receiptClaim.receiptId, "incident", "observed", String(input.reviewId), + events[0]!.reviewPublicId), phase: incident.phase, category: incident.category, + recovered: incident.recovered, recovery: incident.recovery ?? null }); + } + } + await appendPreparedReceiptBatch(client, events); + } return { + claimOwned: lease?.receiptClaim ? job?.owned === true : null, staged: true, completed: false, gateEnabled, @@ -232,6 +306,24 @@ export async function finalizeStagedReviewCompletionWithGateMode( orgId: number | null, ): Promise { return db.transaction(async (tx) => { + const client = transactionReceiptClient(tx); + let qualified = false; + if (input.reviewClaim?.receiptClaim && !(await lockReceiptClaim(client, input.reviewClaim))?.owned) { + return { completed: false, gateEnabled: false, gateFailing: false }; + } + if (input.reviewClaim?.receiptClaim) { + const binding = await client.query(`SELECT r.origin_claim_receipt_id::text, + EXISTS (SELECT 1 FROM job_attempt_receipts b WHERE b.review_id = r.id + AND b.review_public_id = r.public_id AND b.event_kind = 'review_bound' + AND b.claim_receipt_id = $1::bigint AND b.job_id = $2::bigint) AS bound + FROM reviews r WHERE r.id = $3::bigint`, + [input.reviewClaim.receiptClaim.receiptId, input.reviewClaim.receiptClaim.jobId, String(input.reviewId)]); + const row = binding.rows[0]; + if (row?.origin_claim_receipt_id !== null) { + if (!row?.bound) throw new Error("completion review claim binding is missing"); + qualified = true; + } + } await lockReviewApprovalState(tx, input.reviewId); const gateEnabled = orgId === null ? false : await lockOrganizationGateMode(tx, orgId); @@ -284,6 +376,10 @@ export async function finalizeStagedReviewCompletionWithGateMode( } await persistReviewCompletionAccounting(tx as Database, input, rows[0]!); + if (qualified) { + const events = await prepareReviewObservation(client, input.reviewId, "completed", input.reviewClaim); + await appendPreparedReceiptBatch(client, events); + } return { completed: true, gateEnabled, gateFailing: effectiveGateFailing }; }); } @@ -343,3 +439,52 @@ export async function persistReviewCompletionWithGateMode( return { completed: true, gateEnabled, gateFailing: effectiveGateFailing }; }); } + + +/** Create a review and its immutable claim binding in the owning transaction. */ +export async function insertReviewForClaim(db: Database, + values: Omit, lease?: ReviewClaimLease) { + if (!lease?.receiptClaim) { + return (await db.insert(schema.reviews).values(values) + .returning({ id: schema.reviews.id, publicId: schema.reviews.publicId }))[0]; + } + return db.transaction(async (tx) => { + const client = transactionReceiptClient(tx); + const job = await lockReceiptClaim(client, lease); + if (!job?.owned) return undefined; + const [review] = await tx.insert(schema.reviews) + .values({ ...values, originClaimReceiptId: lease.receiptClaim!.receiptId }) + .returning({ id: schema.reviews.id, publicId: schema.reviews.publicId, receiptReviewId: sql`${schema.reviews.id}::text` }); + if (!review) throw new Error("review insert returned no row"); + await appendPreparedReceiptBatch(client, [claimEvent(lease.receiptClaim!.jobId, + Number(job.attempts), lease.receiptClaim!.receiptId, "review_bound", "bound", + review.receiptReviewId, review.publicId)]); + return review; + }); +} + +/** Only the locked job's genuine staged pointer can authorize a recovery binding. */ +export async function bindStagedReviewClaim(db: Database, reviewId: number, lease: ReviewClaimLease) { + if (!lease.receiptClaim) return false; + return db.transaction(async (tx) => { + const client = transactionReceiptClient(tx); + const job = await lockReceiptClaim(client, lease); + if (!job?.owned || job.staged_review_id !== String(reviewId)) return false; + const result = await client.query(`SELECT r.public_id::text, r.origin_claim_receipt_id::text, + c.job_id::text FROM reviews r JOIN job_attempt_receipts c + ON c.receipt_id = r.origin_claim_receipt_id AND c.event_kind = 'claim' + WHERE r.id = $1::bigint AND c.job_id = $2::bigint FOR UPDATE OF r`, + [String(reviewId), lease.receiptClaim!.jobId]); + const review = result.rows[0]; + if (!review) return false; + const existing = await client.query(`SELECT 1 FROM job_attempt_receipts + WHERE event_kind = 'review_bound' AND claim_receipt_id = $1::bigint AND review_id = $2::bigint`, + [lease.receiptClaim!.receiptId, String(reviewId)]); + if (!existing.rows.length) { + await appendPreparedReceiptBatch(client, [claimEvent(lease.receiptClaim!.jobId, + Number(job.attempts), lease.receiptClaim!.receiptId, "review_bound", "bound", + String(reviewId), String(review.public_id))]); + } + return true; + }); +} diff --git a/src/worker/index.ts b/src/worker/index.ts index 418988cb..d08bfb3e 100644 --- a/src/worker/index.ts +++ b/src/worker/index.ts @@ -12,6 +12,7 @@ import { import { runWebhookRedeliveryPass } from "@/lib/github/webhook-redelivery"; import { claimJob, + type ClaimedJob, enqueueGateEnforcementSweepOnce, pruneCompletedWebhookDeliveries, requeueJobsOwnedBy, @@ -75,6 +76,7 @@ let wakeHeartbeat: (() => void) | undefined; let activeWebhookRedeliveryController: AbortController | undefined; const activeRuns = new Set>(); const activeControllers = new Map(); +const activeClaimLeases = new Map(); const requeueableReviewIds = new Set(); let pendingClaims = 0; @@ -141,6 +143,7 @@ async function claimLoop(slot: number): Promise { "worker stopped before starting the claim", [job.kind], [job.id], + [job], ); return; } @@ -170,11 +173,13 @@ async function claimLoop(slot: number): Promise { ); activeRuns.add(run); activeControllers.set(job.id, controller); + activeClaimLeases.set(job.receiptClaim?.jobId ?? String(job.id), job); try { await run; } finally { activeRuns.delete(run); activeControllers.delete(job.id); + activeClaimLeases.delete(job.receiptClaim?.jobId ?? String(job.id)); requeueableReviewIds.delete(job.id); } } @@ -216,6 +221,7 @@ async function shutdown(signal: string): Promise { "worker shutdown interrupted the claim", ["review"], activeReviewJobIds, + [...activeClaimLeases.values()], ).catch((error) => { console.error(`failed to requeue shutdown claims: ${redactSecrets(error)}`); return 0; diff --git a/src/worker/review.ts b/src/worker/review.ts index 4040bc91..ac1351bd 100644 --- a/src/worker/review.ts +++ b/src/worker/review.ts @@ -2,8 +2,13 @@ import { spawn } from "node:child_process"; import { mkdir, rm, writeFile } from "node:fs/promises"; import { join, resolve } from "node:path"; -import { and, desc, eq, inArray, isNotNull, ne } from "drizzle-orm"; +import { and, desc, eq, inArray, isNotNull, isNull, ne } from "drizzle-orm"; +import { + appendPreparedReceiptBatch, + prepareReviewObservation, + transactionReceiptClient, +} from "@/lib/job-attempt-receipts"; import { validateApiBase } from "@/lib/api-base"; import { calculateUsageCostMicrosForModel } from "@/lib/billing-credits"; import { @@ -27,6 +32,7 @@ import { classifyOperationalModelIncidents, gateCheckConclusionForEnvelope, ingestEnvelope, + envelopeSchema, isEnvelopeOperationallyUnavailable, type Envelope, type GateCheckConclusion, @@ -95,6 +101,8 @@ import { redactAndTruncate, redactSecrets } from "@/lib/redact"; import { finalizeStagedReviewCompletionWithGateMode, stageReviewCompletionCandidate, + insertReviewForClaim, + bindStagedReviewClaim, type ReviewCompletionInput, } from "@/lib/review-completion"; import { @@ -690,6 +698,7 @@ async function resumeStagedReviewCompletion(input: { installation: { id: number; orgId: number | null; orgSlug: string | null }; repository: { id: number; githubRepoId: number; fullName: string }; signal?: AbortSignal; + lease?: ExternalSideEffectLease; }): Promise { const { db, payload, installation, repository, signal } = input; const trigger = normalizeReviewTriggerContext(payload.trigger); @@ -697,6 +706,7 @@ async function resumeStagedReviewCompletion(input: { id: schema.reviews.id, publicId: schema.reviews.publicId, status: schema.reviews.status, + originClaimReceiptId: schema.reviews.originClaimReceiptId, repositoryId: schema.reviews.repositoryId, sourceOrgId: schema.reviews.sourceOrgId, sourceInstallationId: schema.reviews.sourceInstallationId, @@ -731,6 +741,7 @@ async function resumeStagedReviewCompletion(input: { eq(schema.reviews.triggerSource, trigger.source), eq(schema.reviews.triggerContext, trigger), eq(schema.reviews.status, "running"), + isNull(schema.reviews.originClaimReceiptId), isNotNull(schema.reviews.envelope), ), ) @@ -756,131 +767,156 @@ async function resumeStagedReviewCompletion(input: { ) { throw new PermanentJobError("review publication recovery identity changed"); } - if ( - stagedReview.status !== "running" && - stagedReview.status !== "completed" - ) { - console.warn( - `review publication recovery ${stagedReview.id} is already ${stagedReview.status}`, - ); - return true; - } - if ( - !stagedReview.envelope || - stagedReview.advisoryCheckRunId == null || - stagedReview.gateCheckRunId == null - ) { - throw new PermanentJobError("review publication recovery state is incomplete"); + if (input.lease?.receiptClaim) { + // Heuristic legacy recovery remains available but never creates a typed relationship. + if (stagedReview.originClaimReceiptId !== null) { + if (!await bindStagedReviewClaim(db, stagedReview.id, input.lease)) { + throw new ReviewPublicationReconciliationError("review recovery lost its claim binding"); + } + } } + const parsed = envelopeSchema.safeParse(stagedReview.envelope); + const reservation = parsed.success ? (await db + .select({ id: schema.hostedUsageReservations.id }) + .from(schema.hostedUsageReservations) + .where(eq(schema.hostedUsageReservations.reviewId, stagedReview.id)).limit(1))[0] : undefined; + let recoveryToken: string | undefined; + try { + if (input.lease && !await externalSideEffectLeaseActive(getPool(), input.lease)) return true; + if ( + stagedReview.status !== "running" && + stagedReview.status !== "completed" + ) { + console.warn( + `review publication recovery ${stagedReview.id} is already ${stagedReview.status}`, + ); + return true; + } + if ( + !stagedReview.envelope || + stagedReview.advisoryCheckRunId == null || + stagedReview.gateCheckRunId == null + ) { + throw new PermanentJobError("review publication recovery state is incomplete"); + } - const token = await translateWorkerAbort( - getInstallationToken(payload.installationId, signal), - signal, - ); - const currentRepository = await translateWorkerAbort( - fetchRepositorySummary(token, payload.repoFullName, signal), - signal, - ); - if ( - currentRepository.id !== payload.githubRepoId || - currentRepository.full_name !== payload.repoFullName - ) { - throw new PermanentJobError( - "review publication recovery repository identity changed", + const token = recoveryToken = await translateWorkerAbort( + getInstallationToken(payload.installationId, signal), + signal, ); - } - const operationallyUnavailable = isEnvelopeOperationallyUnavailable( - stagedReview.envelope, - ); - const detailsUrl = reviewDetailsUrl( - stagedReview.publicId, - installation.orgSlug, - ); - try { - await verifyCompletedCheckRun( - token, - payload.repoFullName, - { - id: stagedReview.advisoryCheckRunId, - name: ADVISORY_CHECK_NAME, - externalId: checkRunExternalId(stagedReview.publicId, "review"), - headSha: payload.headSha, - conclusion: operationallyUnavailable ? "neutral" : "success", - requireOutput: true, - detailsUrl, - }, + const currentRepository = await translateWorkerAbort( + fetchRepositorySummary(token, payload.repoFullName, signal), signal, ); - let gateEnabled: boolean; - if (stagedReview.status === "running") { - const reservation = ( - await db - .select({ id: schema.hostedUsageReservations.id }) - .from(schema.hostedUsageReservations) - .where(eq(schema.hostedUsageReservations.reviewId, stagedReview.id)) - .limit(1) - )[0]; - const completion = await finalizeStagedReviewCompletionWithGateMode( - db, + if ( + currentRepository.id !== payload.githubRepoId || + currentRepository.full_name !== payload.repoFullName + ) { + throw new PermanentJobError( + "review publication recovery repository identity changed", + ); + } + const operationallyUnavailable = isEnvelopeOperationallyUnavailable( + stagedReview.envelope, + ); + const detailsUrl = reviewDetailsUrl( + stagedReview.publicId, + installation.orgSlug, + ); + try { + await verifyCompletedCheckRun( + token, + payload.repoFullName, { - reviewId: stagedReview.id, - usage: reviewUsageFromEnvelope(stagedReview.envelope, { - orgId: installation.orgId, - repositoryId: repository.id, - byok: !reservation, - }), - hostedUsageReservationId: reservation?.id ?? null, - usageAccountingComplete: - stagedReview.envelope.usageAccountingComplete === true, + id: stagedReview.advisoryCheckRunId, + name: ADVISORY_CHECK_NAME, + externalId: checkRunExternalId(stagedReview.publicId, "review"), + headSha: payload.headSha, + conclusion: operationallyUnavailable ? "neutral" : "success", + requireOutput: true, + detailsUrl, }, - installation.orgId, + signal, ); - if (!completion.completed) { - throw new ReviewPublicationReconciliationError( - "review publication recovery lost its terminal-state race", + let gateEnabled: boolean; + if (stagedReview.status === "running") { + const completion = await finalizeStagedReviewCompletionWithGateMode( + db, + { + reviewId: stagedReview.id, + reviewClaim: input.lease, + usage: reviewUsageFromEnvelope(stagedReview.envelope, { + orgId: installation.orgId, + repositoryId: repository.id, + byok: !reservation, + }), + hostedUsageReservationId: reservation?.id ?? null, + usageAccountingComplete: + stagedReview.envelope.usageAccountingComplete === true, + }, + installation.orgId, ); + if (!completion.completed) { + throw new ReviewPublicationReconciliationError( + "review publication recovery lost its terminal-state race", + ); + } + gateEnabled = completion.gateEnabled; + void import("@/worker/runner").then(({ triggerQueueDrain }) => + triggerQueueDrain("gate-state-sync"), + ); + } else { + // The database transition can commit before the exact GitHub PATCH. + // Retry that final publication for completed rows without repeating + // accounting or model work. + gateEnabled = await getOrganizationGateEnabled(db, installation.orgId); } - gateEnabled = completion.gateEnabled; - void import("@/worker/runner").then(({ triggerQueueDrain }) => - triggerQueueDrain("gate-state-sync"), + const activeApprovalIds = await getActiveApprovalIds(db, stagedReview.id); + const gateConclusion = gateCheckConclusionForEnvelope( + stagedReview.envelope, + activeApprovalIds, + gateEnabled, ); - } else { - // The database transition can commit before the exact GitHub PATCH. - // Retry that final publication for completed rows without repeating - // accounting or model work. - gateEnabled = await getOrganizationGateEnabled(db, installation.orgId); + const gateOutput = gateCheckOutput( + gateConclusion, + gateEnabled, + operationallyUnavailable, + ); + await completeExpectedCheckRun( + token, + payload.repoFullName, + { + id: stagedReview.gateCheckRunId, + name: GATE_CHECK_NAME, + externalId: checkRunExternalId(stagedReview.publicId, "gate"), + headSha: payload.headSha, + conclusion: gateConclusion, + detailsUrl, + }, + gateOutput.title, + gateOutput.summary, + signal, + ); + console.log(`review publication recovery ${stagedReview.id} completed`); + return true; + } catch (error) { + if (signal?.aborted) throw new WorkerShutdownError(); + throw new ReviewPublicationReconciliationError(redactSecrets(error)); } - const activeApprovalIds = await getActiveApprovalIds(db, stagedReview.id); - const gateConclusion = gateCheckConclusionForEnvelope( - stagedReview.envelope, - activeApprovalIds, - gateEnabled, - ); - const gateOutput = gateCheckOutput( - gateConclusion, - gateEnabled, - operationallyUnavailable, - ); - await completeExpectedCheckRun( - token, - payload.repoFullName, - { - id: stagedReview.gateCheckRunId, - name: GATE_CHECK_NAME, - externalId: checkRunExternalId(stagedReview.publicId, "gate"), - headSha: payload.headSha, - conclusion: gateConclusion, - detailsUrl, - }, - gateOutput.title, - gateOutput.summary, - signal, - ); - console.log(`review publication recovery ${stagedReview.id} completed`); - return true; - } catch (error) { - if (signal?.aborted) throw new WorkerShutdownError(); - throw new ReviewPublicationReconciliationError(redactSecrets(error)); + } finally { + await finalizeReviewAttempt(db, { + reviewId: stagedReview.id, repoFullName: payload.repoFullName, + token: recoveryToken, installationId: payload.installationId, orgId: installation.orgId, + advisoryCheckRunId: stagedReview.advisoryCheckRunId, + gateCheckRunId: stagedReview.gateCheckRunId, + detailsUrl: reviewDetailsUrl(stagedReview.publicId, installation.orgSlug), + hostedSpend: reservation && parsed.success ? { + reservationId: reservation.id, repositoryId: repository.id, reviewId: stagedReview.id, + triggerSource: trigger.source, + usage: reviewUsageFromEnvelope(parsed.data, { orgId: installation.orgId, repositoryId: repository.id, byok: false }), + usageAccountingComplete: parsed.data.usageAccountingComplete === true, + } : undefined, + }); } } @@ -1002,6 +1038,7 @@ export async function runReviewJob( installation, repository, signal, + lease: timing.lease, }) ) { return; @@ -1059,6 +1096,7 @@ export async function runReviewJob( repoFullName: payload.repoFullName, orgSlug: installation.orgSlug, checkRunsMayExist: false, + reviewClaim: timing.lease, }, ); console.warn( @@ -1185,12 +1223,8 @@ export async function runReviewJob( queuedAt: timing.queuedAt, startedAt: timing.startedAt, }; - const inserted = ( - await db - .insert(schema.reviews) - .values({ ...reviewValues, status: "running" }) - .returning({ id: schema.reviews.id, publicId: schema.reviews.publicId }) - )[0]; + const inserted = await insertReviewForClaim(db, { ...reviewValues, status: "running" }, timing.lease); + if (!inserted && timing.lease?.receiptClaim) return; const reviewId = inserted?.id; const publicId = inserted?.publicId; if (reviewId === undefined || !publicId) @@ -1212,8 +1246,9 @@ export async function runReviewJob( let cliStarted = false; let publicationStarted = false; let completionStaged = false; - let receiptUsageForRace: ReviewCompletionInput["usage"] | undefined; - let usageAccountingCompleteForRace = false; + let publicationIncomplete = false; + let trustedReceiptUsage: ReviewCompletionInput["usage"] | undefined; + let trustedUsageAccountingComplete = false; let advisoryCheckRunMayExist = false; let gateCheckRunMayExist = false; let gateEnabled = false; @@ -1553,6 +1588,11 @@ export async function runReviewJob( }, maxAttempts: 5, }); + if (timing.lease?.receiptClaim) { + const client = transactionReceiptClient(tx); + const events = await prepareReviewObservation(client, reviewId, "failed", timing.lease); + await appendPreparedReceiptBatch(client, events); + } return true; }); if (settled) { @@ -1660,6 +1700,13 @@ export async function runReviewJob( stderr: result.stderr, interrupted: result.interrupted, }, sensitiveValues); + const receiptUsage = reviewUsageFromEnvelope(ingested.envelope, { + orgId: installation.orgId, + repositoryId: repository.id, + byok: llm.byok, + }); + trustedReceiptUsage = receiptUsage; + trustedUsageAccountingComplete = ingested.usageAccountingComplete; for (const incident of classifyOperationalModelIncidents( ingested.envelope, )) { @@ -1684,24 +1731,26 @@ export async function runReviewJob( const operationallyUnavailable = isEnvelopeOperationallyUnavailable( ingested.envelope, ); - const receiptUsage = reviewUsageFromEnvelope(ingested.envelope, { - orgId: installation.orgId, - repositoryId: repository.id, - byok: llm.byok, - }); - receiptUsageForRace = receiptUsage; - usageAccountingCompleteForRace = ingested.usageAccountingComplete; const staged = await stageReviewCompletionCandidate( db, { reviewId, reviewJobId: timing.lease?.id, + reviewClaim: timing.lease, envelope: ingested.envelope, configFiles, configProvenance, silent: ingested.silent, gateFailing: ingested.gateFailing, publicationReceipt, + hostedSpend: hostedUsageReservationId ? { + reservationId: hostedUsageReservationId, + repositoryId: repository.id, + reviewId, + triggerSource: reviewValues.triggerSource, + usage: receiptUsage, + usageAccountingComplete: ingested.usageAccountingComplete, + } : undefined, }, installation.orgId, ); @@ -1711,6 +1760,7 @@ export async function runReviewJob( ); } completionStaged = true; + if (staged.claimOwned === false) return; await activeLargeReviewProxy.discardCompletedRun(); reviewLog.line("review result and publication receipt staged durably"); const workerInstanceId = timing.lease?.lockedBy.match(/^(.+)#\d+$/)?.[1]; @@ -1720,6 +1770,7 @@ export async function runReviewJob( { reviewId, reviewJobId: timing.lease.id, + reviewClaim: timing.lease, repoFullName: payload.repoFullName, prNumber: payload.prNumber, headSha: payload.headSha, @@ -1765,6 +1816,7 @@ export async function runReviewJob( db, { reviewId, + reviewClaim: timing.lease, usage: receiptUsage, hostedUsageReservationId, usageAccountingComplete: ingested.usageAccountingComplete, @@ -1806,39 +1858,6 @@ export async function runReviewJob( } if (!completed) { - if (hostedUsageReservationId) { - await reconcileHostedReviewSpendFromReceipt(db, { - reservationId: hostedUsageReservationId, - repositoryId: repository.id, - reviewId, - triggerSource: reviewValues.triggerSource, - usage: receiptUsage, - usageAccountingComplete: ingested.usageAccountingComplete, - }); - } - const terminal = ( - await db - .select({ status: schema.reviews.status }) - .from(schema.reviews) - .where(eq(schema.reviews.id, reviewId)) - .limit(1) - )[0]; - if (terminal?.status === "stale") { - // A superseding webhook may have neutralized the checks while this - // CLI was still running. The CLI can PATCH them afterward, so make - // neutral the final forge state once the old process exits. - await neutralizeSupersededCheckRuns( - token, - payload.repoFullName, - advisoryCheckRunId ?? null, - gateCheckRunId ?? null, - "superseded by a newer review", - detailsUrl, - ); - reviewLog.line( - "forge check-runs restored to neutral after supersession", - ); - } console.warn( `review ${reviewId} completed after it was already superseded or failed`, ); @@ -1871,41 +1890,10 @@ export async function runReviewJob( } } } catch (err) { - if (err instanceof CheckRunPublicationError && receiptUsageForRace) { - const terminal = ( - await db - .select({ status: schema.reviews.status }) - .from(schema.reviews) - .where(eq(schema.reviews.id, reviewId)) - .limit(1) - )[0]; - if (terminal?.status === "stale") { - if (hostedUsageReservationId) { - await reconcileHostedReviewSpendFromReceipt(db, { - reservationId: hostedUsageReservationId, - repositoryId: repository.id, - reviewId, - triggerSource: reviewValues.triggerSource, - usage: receiptUsageForRace, - usageAccountingComplete: usageAccountingCompleteForRace, - }); - } - await neutralizeSupersededCheckRuns( - token, - payload.repoFullName, - advisoryCheckRunId ?? null, - gateCheckRunId ?? null, - "superseded by a newer review", - detailsUrl, - ); - reviewLog.line( - "forge check-runs restored to neutral after supersession", - ); - console.warn( - `review ${reviewId} publication verification raced with supersession`, - ); - return; - } + if (err instanceof CheckRunPublicationError && trustedReceiptUsage) { + const terminal = (await db.select({ status: schema.reviews.status }) + .from(schema.reviews).where(eq(schema.reviews.id, reviewId)).limit(1))[0]; + if (terminal?.status === "stale") return; } if (completionStaged) { if (err instanceof WorkerInterruptionRehearsalError) throw err; @@ -1919,16 +1907,23 @@ export async function runReviewJob( } if (err instanceof WorkerShutdownError && !publicationStarted) { reviewLog.line(err.message); - await db - .update(schema.reviews) - .set({ status: "stale", finishedAt: new Date() }) - .where( - and( - eq(schema.reviews.id, reviewId), - eq(schema.reviews.status, "running"), - ), - ); - await releaseHostedReviewSpend(db, hostedUsageReservationId).catch( + await db.transaction(async (tx) => { + await tx + .update(schema.reviews) + .set({ status: "stale", finishedAt: new Date() }) + .where( + and( + eq(schema.reviews.id, reviewId), + eq(schema.reviews.status, "running"), + ), + ); + if (timing.lease?.receiptClaim) { + const client = transactionReceiptClient(tx); + const events = await prepareReviewObservation(client, reviewId, "interrupted", timing.lease); + await appendPreparedReceiptBatch(client, events); + } + }); + if (!trustedReceiptUsage) await releaseHostedReviewSpend(db, hostedUsageReservationId).catch( (releaseError) => { console.error( `failed to release hosted usage reservation: ${redactSecrets(releaseError)}`, @@ -1938,7 +1933,7 @@ export async function runReviewJob( throw err; } const interruptedAfterPublication = err instanceof WorkerShutdownError; - const publicationIncomplete = err instanceof CheckRunPublicationError; + publicationIncomplete = err instanceof CheckRunPublicationError; const message = interruptedAfterPublication ? "review interrupted after GitHub publication began" : redactSecrets(err, sensitiveValues); @@ -1978,9 +1973,14 @@ export async function runReviewJob( }, maxAttempts: 5, }); + if (timing.lease?.receiptClaim) { + const client = transactionReceiptClient(tx); + const events = await prepareReviewObservation(client, reviewId, "failed", timing.lease); + await appendPreparedReceiptBatch(client, events); + } return rows; }); - if (hostedUsageReservationId && cliStarted) { + if (!trustedReceiptUsage && hostedUsageReservationId && cliStarted) { const reservationId = hostedUsageReservationId; const billingOutcome = largeReviewProxy?.billingOutcome() ?? "unused"; const reconcile = billingOutcome === "resumable" @@ -2009,7 +2009,7 @@ export async function runReviewJob( ); } }); - } else { + } else if (!trustedReceiptUsage) { await releaseHostedReviewSpend(db, hostedUsageReservationId).catch( (releaseError) => { console.error( @@ -2018,24 +2018,6 @@ export async function runReviewJob( }, ); } - // Without a token there are no check-runs to complete (creation is the - // first tokened call); with one, fail them closed - unless the watchdog - // already claimed this review and completed them itself (0 rows above). - if (token && failedRows.length > 0) { - await failCheckRuns( - token, - payload.repoFullName, - advisoryCheckRunId, - gateCheckRunId, - message, - undefined, - false, - detailsUrl, - expectedFailureCheckRuns(publicationIncomplete), - gateEnabled, - ); - reviewLog.line("forge check-runs updated for review failure"); - } // A preflight rejection becomes one durable failed review and one exact // terminal check pair. Retrying the review job would create duplicate // review/check identities for a condition that cannot change mid-job. @@ -2048,14 +2030,73 @@ export async function runReviewJob( throw interruptedAfterPublication ? new OperationalError(message) : err; } finally { if (leasePoll) clearInterval(leasePoll); - largeReviewProxy?.close(); - if (baselinePath) - await rm(baselinePath, { force: true }).catch(() => undefined); - if (workDir) - await rm(workDir, { recursive: true, force: true }).catch( - () => undefined, - ); - await reviewLog.close(); + try { + await finalizeReviewAttempt(db, { + reviewId, repoFullName: payload.repoFullName, token, gateEnabled, + advisoryCheckRunId, gateCheckRunId, detailsUrl, + expectedChecks: expectedFailureCheckRuns(publicationIncomplete), + hostedSpend: hostedUsageReservationId && trustedReceiptUsage ? { + reservationId: hostedUsageReservationId, + repositoryId: repository.id, + reviewId, + triggerSource: reviewValues.triggerSource, + usage: trustedReceiptUsage, + usageAccountingComplete: trustedUsageAccountingComplete, + } : undefined, + }); + } finally { + largeReviewProxy?.close(); + if (baselinePath) + await rm(baselinePath, { force: true }).catch(() => undefined); + if (workDir) + await rm(workDir, { recursive: true, force: true }).catch( + () => undefined, + ); + await reviewLog.close(); + } + } +} + +/** Settle trusted own-review spend and repair terminal checks on every attempt exit. */ +async function finalizeReviewAttempt(db: Database, input: { + reviewId: number; + repoFullName: string; + advisoryCheckRunId?: number | null; + gateCheckRunId?: number | null; + detailsUrl?: string; + token?: string; + installationId?: number; + orgId?: number | null; + gateEnabled?: boolean; + expectedChecks?: Parameters[8]; + hostedSpend?: Parameters[1]; +}): Promise { + try { + if (input.hostedSpend) await reconcileHostedReviewSpendFromReceipt(db, input.hostedSpend); + } finally { + if (input.advisoryCheckRunId != null || input.gateCheckRunId != null) { + const terminal = (await db + .select({ status: schema.reviews.status, errorMessage: schema.reviews.errorMessage }) + .from(schema.reviews).where(eq(schema.reviews.id, input.reviewId)).limit(1))[0]; + // A late CLI write can follow supersession or watchdog cleanup. Running + // and completed reviews retain their current publication authority. + if (terminal?.status === "stale" || terminal?.status === "failed") { + const token = input.token ?? (input.installationId === undefined ? undefined + : await getInstallationToken(input.installationId)); + if (token) { + if (terminal.status === "stale") { + await neutralizeSupersededCheckRuns(token, input.repoFullName, + input.advisoryCheckRunId ?? null, input.gateCheckRunId ?? null, + "superseded by a newer review", input.detailsUrl); + } else { + await failCheckRuns(token, input.repoFullName, input.advisoryCheckRunId, + input.gateCheckRunId, terminal.errorMessage ?? "review did not complete", + undefined, false, input.detailsUrl, input.expectedChecks, + input.gateEnabled ?? await getOrganizationGateEnabled(db, input.orgId ?? null)); + } + } + } + } } } @@ -2080,6 +2121,7 @@ export async function supersedeActiveReviews( const active = await db .select({ id: schema.reviews.id, + originClaimReceiptId: schema.reviews.originClaimReceiptId, publicId: schema.reviews.publicId, advisoryCheckRunId: schema.reviews.advisoryCheckRunId, gateCheckRunId: schema.reviews.gateCheckRunId, @@ -2119,16 +2161,25 @@ export async function supersedeActiveReviews( const message = input.message ?? `superseded by a newer review of ${input.newHeadSha}`; let superseded = 0; for (const review of active) { - const changed = await db - .update(schema.reviews) - .set({ status: "stale", finishedAt: new Date() }) - .where( - and( - eq(schema.reviews.id, review.id), - inArray(schema.reviews.status, ["queued", "running"]), - ), - ) - .returning({ id: schema.reviews.id }); + const update = async (tx: Database) => { + const rows = await tx + .update(schema.reviews) + .set({ status: "stale", finishedAt: new Date() }) + .where( + and( + eq(schema.reviews.id, review.id), + inArray(schema.reviews.status, ["queued", "running"]), + ), + ) + .returning({ id: schema.reviews.id }); + if (rows.length && review.originClaimReceiptId) { + const client = transactionReceiptClient(tx); + const events = await prepareReviewObservation(client, review.id, "cancelled"); + await appendPreparedReceiptBatch(client, events); + } + return rows; + }; + const changed = review.originClaimReceiptId ? await db.transaction(update) : await update(db); if (changed.length === 0) continue; superseded += 1; if (token) { diff --git a/src/worker/runner.ts b/src/worker/runner.ts index 9acc8335..344b33d5 100644 --- a/src/worker/runner.ts +++ b/src/worker/runner.ts @@ -260,6 +260,7 @@ export async function runClaimedJob( "worker shutdown interrupted the claim", ["review"], [job.id], + [job], ); console.warn( `[${label}] job ${job.id} requeued after worker shutdown (${requeued})`, diff --git a/src/worker/watchdog.ts b/src/worker/watchdog.ts index 083bb59b..fb94aecc 100644 --- a/src/worker/watchdog.ts +++ b/src/worker/watchdog.ts @@ -1,5 +1,11 @@ import { and, eq, lt, sql } from "drizzle-orm"; +import { + appendPreparedReceiptBatch, + prepareClosedJobReceipts, + prepareReviewObservation, + transactionReceiptClient, +} from "@/lib/job-attempt-receipts"; import { getDb, getPool, schema } from "@/lib/db"; import { scheduleCustomerNotificationEmailJobs } from "@/lib/customer-notification-email"; import { pruneExpiredCustomerNotifications } from "@/lib/customer-notifications"; @@ -34,6 +40,7 @@ export async function watchdogPass( const stuck = await db .select({ id: schema.reviews.id, + originClaimReceiptId: schema.reviews.originClaimReceiptId, publicId: schema.reviews.publicId, headSha: schema.reviews.headSha, startedAt: schema.reviews.startedAt, @@ -106,6 +113,11 @@ export async function watchdogPass( }, maxAttempts: 5, }); + if (review.originClaimReceiptId) { + const client = transactionReceiptClient(tx); + const events = await prepareReviewObservation(client, review.id, "failed"); + await appendPreparedReceiptBatch(client, events); + } return true; }); if (claimed) killed += 1; @@ -126,65 +138,78 @@ export async function watchdogPass( // an ambiguous check-run can remain absent forever, so cleanup honors its // declared retry budget in both the runner and watchdog recovery paths. const pool = getPool(); - await pool.query( - `WITH updated AS ( - UPDATE jobs - SET status = CASE - WHEN kind = 'review' - AND NOT payload ? 'recoveryReviewId' - AND jsonb_typeof(payload -> $2) = 'object' - THEN 'failed'::job_status - WHEN kind IN ('gate-state-sync', 'webhook-dispatch', 'webhook-comment', 'github-reaction') - OR (kind = 'review' AND payload ? 'recoveryReviewId') - OR attempts < max_attempts - THEN 'queued'::job_status - ELSE 'failed'::job_status - END, - locked_at = NULL, locked_by = NULL, run_after = now(), - last_error = CASE - WHEN kind = 'review' - AND NOT payload ? 'recoveryReviewId' - AND jsonb_typeof(payload -> $2) = 'object' - THEN concat_ws( - ' ', NULLIF(last_error, ''), - '[watchdog: failed stuck job and retained newer review]' - ) - ELSE COALESCE(last_error, '') || - CASE - WHEN kind IN ('gate-state-sync', 'webhook-dispatch', 'webhook-comment', 'github-reaction') - OR (kind = 'review' AND payload ? 'recoveryReviewId') - OR attempts < max_attempts - THEN ' [watchdog: requeued stuck job]' - ELSE ' [watchdog: failed stuck job after retry budget exhausted]' - END - END - WHERE status = 'running' AND locked_at < $1 - AND NOT EXISTS ( - SELECT 1 - FROM private_worker_rehearsals rehearsal - WHERE rehearsal.job_id = jobs.id - AND rehearsal.state = 'awaiting_replacement' - ) - RETURNING id, kind, status, payload, max_attempts - ) - INSERT INTO jobs (kind, payload, max_attempts) - SELECT - 'review', - payload -> $2, - max_attempts - FROM updated - WHERE kind = 'review' - AND status = 'failed' - AND jsonb_typeof(payload -> $2) = 'object' - UNION ALL - SELECT - 'respond-failure-comment', - payload || jsonb_build_object('respondJobId', id), - 5 - FROM updated - WHERE kind = 'respond' AND status = 'failed'`, - [cutoff, COALESCED_REVIEW_PAYLOAD_KEY], - ); + const client = await pool.connect(); + try { + await client.query("BEGIN"); + const locked = await client.query(`SELECT id::text FROM jobs + WHERE status = 'running' AND locked_at < $1 ORDER BY id FOR UPDATE`, [cutoff]); + await client.query( + `WITH updated AS ( + UPDATE jobs + SET status = CASE + WHEN kind = 'review' + AND NOT payload ? 'recoveryReviewId' + AND jsonb_typeof(payload -> $2) = 'object' + THEN 'failed'::job_status + WHEN kind IN ('gate-state-sync', 'webhook-dispatch', 'webhook-comment', 'github-reaction') + OR (kind = 'review' AND payload ? 'recoveryReviewId') + OR attempts < max_attempts + THEN 'queued'::job_status + ELSE 'failed'::job_status + END, + locked_at = NULL, locked_by = NULL, run_after = now(), + last_error = CASE + WHEN kind = 'review' + AND NOT payload ? 'recoveryReviewId' + AND jsonb_typeof(payload -> $2) = 'object' + THEN concat_ws( + ' ', NULLIF(last_error, ''), + '[watchdog: failed stuck job and retained newer review]' + ) + ELSE COALESCE(last_error, '') || + CASE + WHEN kind IN ('gate-state-sync', 'webhook-dispatch', 'webhook-comment', 'github-reaction') + OR (kind = 'review' AND payload ? 'recoveryReviewId') + OR attempts < max_attempts + THEN ' [watchdog: requeued stuck job]' + ELSE ' [watchdog: failed stuck job after retry budget exhausted]' + END + END + WHERE status = 'running' AND locked_at < $1 + AND NOT EXISTS ( + SELECT 1 + FROM private_worker_rehearsals rehearsal + WHERE rehearsal.job_id = jobs.id + AND rehearsal.state = 'awaiting_replacement' + ) + RETURNING id, kind, status, payload, max_attempts + ) + INSERT INTO jobs (kind, payload, max_attempts) + SELECT + 'review', + payload -> $2, + max_attempts + FROM updated + WHERE kind = 'review' + AND status = 'failed' + AND jsonb_typeof(payload -> $2) = 'object' + UNION ALL + SELECT + 'respond-failure-comment', + payload || jsonb_build_object('respondJobId', id), + 5 + FROM updated + WHERE kind = 'respond' AND status = 'failed'`, + [cutoff, COALESCED_REVIEW_PAYLOAD_KEY], + ); + + const events = await prepareClosedJobReceipts(client, locked.rows.map((r) => r.id)); + await appendPreparedReceiptBatch(client, events); + await client.query("COMMIT"); + } catch (error) { + await client.query("ROLLBACK").catch(() => undefined); + throw error; + } finally { client.release(); } await reconcileOperatorAlertDeliveries(db); const expiredTrials = await sweepExpiredSelfServiceTrials(db, now); diff --git a/tests/job-attempt-receipts.test.ts b/tests/job-attempt-receipts.test.ts new file mode 100644 index 00000000..07e7676b --- /dev/null +++ b/tests/job-attempt-receipts.test.ts @@ -0,0 +1,1054 @@ +import { afterAll, beforeAll, describe, expect, test } from "bun:test"; +import { SQL } from "bun"; +import { randomBytes, randomUUID } from "node:crypto"; +import { + appendPreparedReceiptBatch, readAttemptReceiptPageSql, receiptAppendLockSql, + receiptHeadSql, stageOwnedReviewSql, type PreparedReceipt, type ReceiptClient, +} from "../src/lib/job-attempt-receipts"; + +const port = process.env.POSTIL_RECEIPT_TEST_PORT; +type Rows = Record[]; +const connections: Array<{ close(): Promise }> = []; +async function connect() { + const pool = new SQL({ hostname: "localhost", port: Number(port), username: "postgres", + database: process.env.POSTIL_RECEIPT_TEST_DATABASE ?? "postgres", + max: 1, idleTimeout: 0, connectionTimeout: 2 }); + const reserved = await pool.reserve(); + let terminated = false; + const client = { + async query(text: string, values: unknown[] = []): Promise<{ rows: Rows }> { + return { rows: Array.from(await reserved.unsafe(text, values as never[])) }; + }, + markTerminated() { terminated = true; }, + async close() { + // A backend killed by a fault test has already closed its socket. Bun's + // release of that dead reservation raises an unrelated teardown error. + if (terminated) return; + reserved.release(); await pool.close({ timeout: 0.1 }); + }, + }; + connections.push(client); + return client; +} +let db: Awaited>; +let repository: string; +const query = async (text: string, values: unknown[] = []) => (await db.query(text, values)).rows; +async function transaction(client: ReceiptClient, body: () => Promise): Promise { + await client.query("BEGIN ISOLATION LEVEL READ COMMITTED"); + try { const result = await body(); await client.query("COMMIT"); return result; } + catch (error) { await client.query("ROLLBACK"); throw error; } +} +async function job(kind = "review") { + return (await query("INSERT INTO jobs(kind,payload) VALUES($1,'{}') RETURNING id::text", [kind]))[0]!.id as string; +} +function event(jobId: string, ordinal: number, claimId: string | null, + changes: Partial = {}): PreparedReceipt { + return { jobId, ordinal, jobKind: "review", claimId, reviewId: null, + reviewPublicId: null, eventKind: "claim", outcome: "started", phase: null, + category: null, recovered: null, recovery: null, ...changes }; +} +async function claim(jobId: string, kind: "review" | "review-feedback" = "review") { + return transaction(db, async () => { + const rows = await query(`UPDATE jobs SET status='running', attempts=attempts+1, + locked_by='fixture-worker', locked_at=now() WHERE id=$1::bigint AND status='queued' + RETURNING attempts`, [jobId]); + if (!rows.length) return null; + const ordinal = rows[0]!.attempts as number; + const [id] = await appendPreparedReceiptBatch(db, [event(jobId, ordinal, null, { jobKind: kind })]); + await query("UPDATE jobs SET active_claim_receipt_id=$2::bigint WHERE id=$1::bigint", [jobId, id]); + return { id: id!, ordinal, jobId, kind }; + }); +} +type Claim = NonNullable>>; +async function closeClaim(c: Claim, outcome: PreparedReceipt["outcome"], reset = false) { + return transaction(db, async () => { + const rows = await query(`UPDATE jobs SET status=$3::job_status, active_claim_receipt_id=NULL, + locked_by=NULL, locked_at=NULL, attempts=CASE WHEN $4 THEN 0 ELSE attempts END + WHERE id=$1::bigint AND active_claim_receipt_id=$2::bigint AND status='running' + AND locked_by='fixture-worker' RETURNING id`, + [c.jobId, c.id, ["retry", "continued", "interrupted", "deferred"].includes(outcome) ? "queued" : "done", reset]); + if (!rows.length) return false; + await appendPreparedReceiptBatch(db, [event(c.jobId, c.ordinal, c.id, + { jobKind: c.kind, eventKind: "queue_closed", outcome })]); + return true; + }); +} +async function review(c: Claim) { + return transaction(db, async () => { + await query("SELECT id FROM jobs WHERE id=$1::bigint FOR UPDATE", [c.jobId]); + const [r] = await query(`INSERT INTO reviews(repository_id,pr_number,head_sha,base_sha,status,origin_claim_receipt_id) + VALUES($1::bigint,1,$2,$3,'running',$4::bigint) RETURNING id::text,public_id::text`, + [repository, randomBytes(20).toString("hex"), randomBytes(20).toString("hex"), c.id]); + await appendPreparedReceiptBatch(db, [event(c.jobId, c.ordinal, c.id, + { jobKind: c.kind, eventKind: "review_bound", outcome: "bound", reviewId: r!.id, reviewPublicId: r!.public_id })]); + return r!; + }); +} +async function observe(c: Claim, r: Record, outcome: PreparedReceipt["outcome"]) { + return transaction(db, async () => { + const rows = await query(`UPDATE reviews SET status=$2::review_status + WHERE id=$1::bigint AND status='running' RETURNING id`, [r.id, outcome === "staged" ? "running" : outcome]); + if (!rows.length) return []; + return appendPreparedReceiptBatch(db, [event(c.jobId, c.ordinal, c.id, + { jobKind: c.kind, eventKind: "review_observed", outcome, reviewId: r.id, reviewPublicId: r.public_id })]); + }); +} +async function stage(c: Claim, r: Record) { + return transaction(db, async () => { + await query("SELECT id FROM jobs WHERE id=$1::bigint FOR UPDATE", [c.jobId]); + await query("SELECT id FROM reviews WHERE id=$1::bigint FOR UPDATE", [r.id]); + return (await query(stageOwnedReviewSql, [c.jobId, c.id, "fixture-worker", r.id])).length; + }); +} + +describe.skipIf(!port)("disposable PostgreSQL receipt SQL", () => { + beforeAll(async () => { + db = await connect(); + const [marker] = await query(`SELECT shobj_description(oid,'pg_database') AS marker + FROM pg_database WHERE datname=current_database()`); + if (marker?.marker !== "disposable receipt qualification") throw new Error("dedicated fixture required"); + const [installation] = await query(`INSERT INTO installations(github_installation_id,account_login,account_type) + VALUES($1,'receipt-test','Organization') RETURNING id::text`, [Date.now()]); + const [repo] = await query(`INSERT INTO repositories(installation_id,github_repo_id,full_name) + VALUES($1::bigint,$2,'receipt-test/fixture') RETURNING id::text`, [installation!.id, Date.now()]); + repository = repo!.id; + }); + afterAll(async () => { await Promise.all(connections.map((c) => c.close())); }); + + test("allocator is CACHE 1, positive, noncycling and rejects cache drift", async () => { + const [s] = await query("SELECT seqcache::text,seqcycle,seqincrement::text FROM pg_sequence WHERE seqrelid='job_attempt_receipt_ids'::regclass"); + expect(s).toEqual({ seqcache: "1", seqcycle: false, seqincrement: "1" }); + await query("ALTER SEQUENCE job_attempt_receipt_ids CACHE 2"); + try { await expect(claim(await job())).rejects.toThrow(); } + finally { await query("ALTER SEQUENCE job_attempt_receipt_ids CACHE 1"); } + }); + + test("same worker and reused ordinal cannot close or stage a newer claim", async () => { + const j = await job(); const a = (await claim(j))!; const ra = await review(a); + expect(await stage(a, ra)).toBe(1); + expect(await closeClaim(a, "interrupted", true)).toBe(true); + const b = (await claim(j))!; const rb = await review(b); + expect(b.ordinal).toBe(a.ordinal); expect(b.id).not.toBe(a.id); + expect(await stage(b, rb)).toBe(1); + expect(await stage(a, ra)).toBe(0); + expect(await closeClaim(a, "succeeded")).toBe(false); + expect((await query("SELECT staged_review_id::text,active_claim_receipt_id::text,payload FROM jobs WHERE id=$1::bigint", [j]))[0]) + .toEqual({ staged_review_id: rb.id, active_claim_receipt_id: b.id, payload: { recoveryReviewId: Number(rb.id) } }); + // The genuine historical review can retain its own observation under A. + expect((await observe(a, ra, "staged")).length).toBe(1); + expect(await stage(a, rb)).toBe(0); + expect(await closeClaim(b, "succeeded")).toBe(true); + expect(await closeClaim(b, "succeeded")).toBe(false); + }); + + test("failure then success retains separate claims/reviews and finite incident", async () => { + const j = await job("review-feedback"); const a = (await claim(j, "review-feedback"))!; + const ra = await review(a); + await transaction(db, async () => { + await query("UPDATE reviews SET status='failed' WHERE id=$1::bigint", [ra.id]); + await appendPreparedReceiptBatch(db, [ + event(j, a.ordinal, a.id, { jobKind: a.kind, eventKind: "incident", outcome: "observed", + reviewId: ra.id, reviewPublicId: ra.public_id, phase: "review", category: "invalidOutput", recovered: false }), + event(j, a.ordinal, a.id, { jobKind: a.kind, eventKind: "review_observed", outcome: "failed", + reviewId: ra.id, reviewPublicId: ra.public_id }), + ]); + }); + expect(await closeClaim(a, "retry")).toBe(true); + const b = (await claim(j, "review-feedback"))!; const rb = await review(b); + await observe(b, rb, "completed"); expect(await closeClaim(b, "succeeded")).toBe(true); + expect((await query("SELECT event_kind,outcome FROM job_attempt_receipts WHERE job_id=$1::bigint ORDER BY receipt_id", [j])) + .map((r) => `${r.event_kind}:${r.outcome}`)).toEqual([ + "claim:started", "review_bound:bound", "incident:observed", "review_observed:failed", "queue_closed:retry", + "claim:started", "review_bound:bound", "review_observed:completed", "queue_closed:succeeded", + ]); + expect((await observe(b, rb, "completed")).length).toBe(0); + }); + + test("append failure rolls business state back; aborted sequence gaps are safe", async () => { + const j = await job(); const c = (await claim(j))!; const before = (await query(receiptHeadSql))[0]!.receipt_id; + await expect(transaction(db, async () => { + await query("UPDATE jobs SET status='done',active_claim_receipt_id=NULL WHERE id=$1::bigint", [j]); + await appendPreparedReceiptBatch(db, [event(j, c.ordinal + 1, c.id, { eventKind: "queue_closed", outcome: "succeeded" })]); + })).rejects.toThrow(); + expect((await query("SELECT status,active_claim_receipt_id::text FROM jobs WHERE id=$1::bigint", [j]))[0]) + .toEqual({ status: "running", active_claim_receipt_id: c.id }); + expect(await closeClaim(c, "succeeded")).toBe(true); + const [last] = await query("SELECT r.receipt_id::text,r.predecessor_id::text FROM job_attempt_receipts r ORDER BY r.receipt_id DESC LIMIT 1"); + expect(last!.predecessor_id).toBe(before); + expect(BigInt(last!.receipt_id) - BigInt(before)).toBeGreaterThan(1n); + }); + + test("publication recovery binds a new claim to the genuine staged review", async () => { + const j = await job(); const a = (await claim(j))!; const r = await review(a); + expect(await stage(a, r)).toBe(1); await closeClaim(a, "interrupted", true); + const b = (await claim(j))!; + await transaction(db, async () => { + await query("SELECT id FROM jobs WHERE id=$1::bigint FOR UPDATE", [j]); + await query("SELECT id FROM reviews WHERE id=$1::bigint FOR UPDATE", [r.id]); + await appendPreparedReceiptBatch(db, [event(j, b.ordinal, b.id, { eventKind: "review_bound", outcome: "bound", + reviewId: r.id, reviewPublicId: r.public_id })]); + }); + expect(await stage(b, r)).toBe(1); + expect((await query("SELECT origin_claim_receipt_id::text FROM reviews WHERE id=$1::bigint", [r.id]))[0]!.origin_claim_receipt_id).toBe(a.id); + await observe(b, r, "completed"); await closeClaim(b, "succeeded"); + }); + + test("constraints reject wrong job, wrong claim kind, missing binding and mutable origin", async () => { + const a = (await claim(await job()))!; const b = (await claim(await job()))!; const r = await review(a); + const invalid = [ + event(b.jobId, a.ordinal, a.id, { eventKind: "queue_closed", outcome: "succeeded" }), + event(a.jobId, a.ordinal, a.id, { eventKind: "review_observed", outcome: "failed", reviewId: r.id, reviewPublicId: randomUUID() }), + event(b.jobId, b.ordinal, b.id, { eventKind: "review_bound", outcome: "bound", reviewId: r.id, reviewPublicId: r.public_id }), + ]; + for (const e of invalid) await expect(transaction(db, () => appendPreparedReceiptBatch(db, [e]))).rejects.toThrow(); + await expect(query("UPDATE reviews SET origin_claim_receipt_id=$2::bigint WHERE id=$1::bigint", [r.id, b.id])).rejects.toThrow(); + await expect(query("UPDATE job_attempt_receipts SET outcome=outcome WHERE receipt_id=$1::bigint", [a.id])).rejects.toThrow(); + await expect(query("DELETE FROM job_attempt_receipts WHERE receipt_id=$1::bigint", [a.id])).rejects.toThrow(); + await expect(query("TRUNCATE job_attempt_receipts CASCADE")).rejects.toThrow(); + const [binding] = await query("SELECT receipt_id::text FROM job_attempt_receipts WHERE claim_receipt_id=$1::bigint AND event_kind='review_bound'", [a.id]); + await expect(transaction(db, () => appendPreparedReceiptBatch(db, [event(a.jobId, a.ordinal, binding!.receipt_id, + { eventKind: "queue_closed", outcome: "succeeded" })]))).rejects.toThrow(); + expect(await closeClaim(a, "succeeded")).toBe(true); + await expect(transaction(db, () => appendPreparedReceiptBatch(db, [event(a.jobId, a.ordinal, a.id, + { eventKind: "queue_closed", outcome: "succeeded" })]))).rejects.toThrow(); + }); + + test("claim history does not cascade or restrict synthetic business row deletion", async () => { + const c = (await claim(await job()))!; const r = await review(c); + await closeClaim(c, "succeeded"); + await query("DELETE FROM reviews WHERE id=$1::bigint", [r.id]); + await query("DELETE FROM jobs WHERE id=$1::bigint", [c.jobId]); + expect((await query("SELECT receipt_id FROM job_attempt_receipts WHERE job_id=$1::bigint", [c.jobId])).length).toBe(3); + }); + + test("delayed concurrent append uses a fresh predecessor after waiting", async () => { + const a = await connect(); const b = await connect(); + const ja = await job(); const jb = await job(); + for (const [client, id] of [[a, ja], [b, jb]] as const) { + await client.query("BEGIN"); + await client.query("UPDATE jobs SET status='running',attempts=1 WHERE id=$1::bigint", [id]); + } + const oldHead = (await b.query(receiptHeadSql)).rows[0]!.receipt_id; + const [ia] = await appendPreparedReceiptBatch(a, [event(ja, 1, null)]); + const pending = appendPreparedReceiptBatch(b, [event(jb, 1, null)]); + await query("SELECT pg_sleep(0.05)"); + expect((await query(receiptHeadSql))[0]!.receipt_id).toBe(oldHead); + expect((await query("SELECT pid FROM pg_locks WHERE locktype='advisory' AND classid=17491 AND NOT granted")).length).toBe(1); + await a.query("COMMIT"); + const [ib] = await pending; await b.query("COMMIT"); + expect(BigInt(ib!)).toBeGreaterThan(BigInt(ia!)); + expect((await query("SELECT predecessor_id::text FROM job_attempt_receipts WHERE receipt_id=$1::bigint", [ib]))[0]!.predecessor_id).toBe(ia); + }); + + test("prepared multi-row batch is invisible until one commit", async () => { + const writer = await connect(); const jobs = [await job(), await job()]; + await writer.query("BEGIN"); + for (const id of jobs) await writer.query("UPDATE jobs SET status='running',attempts=1 WHERE id=$1::bigint", [id]); + const ids = await appendPreparedReceiptBatch(writer, jobs.map((id) => event(id, 1, null))); + expect((await query("SELECT receipt_id FROM job_attempt_receipts WHERE job_id=ANY($1::bigint[])", ["{" + jobs.join(",") + "}"])).length).toBe(0); + await writer.query("COMMIT"); + expect((await query("SELECT r.receipt_id::text FROM job_attempt_receipts r WHERE job_id=ANY($1::bigint[]) ORDER BY r.receipt_id", ["{" + jobs.join(",") + "}"])).map((r) => r.receipt_id)).toEqual(ids); + }); + + test("stalled writer has bounded wait; independent business work proceeds", async () => { + const holder = await connect(); const waiter = await connect(); + await holder.query("BEGIN"); await holder.query(receiptAppendLockSql); + await waiter.query("BEGIN"); await waiter.query("SET LOCAL lock_timeout='100ms'"); + const started = performance.now(); + await expect(waiter.query(receiptAppendLockSql)).rejects.toThrow(); + expect(performance.now() - started).toBeLessThan(1500); + await waiter.query("ROLLBACK"); + const j = await job(); + expect((await query("UPDATE jobs SET payload=payload || '{\"fixture\":true}'::jsonb WHERE id=$1::bigint RETURNING id", [j])).length).toBe(1); + // A finite reader does not wait for the writer's advisory lock. + expect((await query(readAttemptReceiptPageSql, ["0"])).length).toBeLessThanOrEqual(64); + await holder.query("ROLLBACK"); + expect((await claim(j))!.id).toBeString(); + }); + + test("backend death before commit rolls back state and releases append lock", async () => { + const writer = await connect(); const j = await job(); + const pid = (await writer.query("SELECT pg_backend_pid() AS pid")).rows[0]!.pid; + await writer.query("BEGIN"); + await writer.query("UPDATE jobs SET status='running',attempts=1 WHERE id=$1::bigint", [j]); + const [id] = await appendPreparedReceiptBatch(writer, [event(j, 1, null)]); + writer.markTerminated(); await query("SELECT pg_terminate_backend($1::integer, 1000)", [pid]); + expect((await query("SELECT pid FROM pg_stat_activity WHERE pid=$1::integer", [pid])).length).toBe(0); + expect((await query("SELECT status,attempts FROM jobs WHERE id=$1::bigint", [j]))[0]).toEqual({ status: "queued", attempts: 0 }); + expect((await query("SELECT receipt_id FROM job_attempt_receipts WHERE receipt_id=$1::bigint", [id])).length).toBe(0); + expect((await claim(j))!.id).toBeString(); + }); + + test("idle transaction expiry releases a stalled append tail", async () => { + const writer = await connect(); + const pid = (await writer.query("SELECT pg_backend_pid() AS pid")).rows[0]!.pid; + await writer.query("BEGIN"); await writer.query("SET LOCAL idle_in_transaction_session_timeout='100ms'"); + await writer.query(receiptAppendLockSql); + writer.markTerminated(); + await query("SELECT pg_sleep(0.2)"); + expect((await query("SELECT pid FROM pg_stat_activity WHERE pid=$1::integer", [pid])).length).toBe(0); + expect((await claim(await job()))!.id).toBeString(); + }); + + test("backend death during COMMIT rolls back the deferred transaction", async () => { + const c = (await claim(await job()))!; const writer = await connect(); + const pid = (await writer.query("SELECT pg_backend_pid() AS pid")).rows[0]!.pid; + await query(`CREATE FUNCTION receipt_fixture_commit_pause() RETURNS trigger LANGUAGE plpgsql AS + $$ BEGIN PERFORM pg_sleep(1); RETURN NEW; END $$`); + await query(`CREATE CONSTRAINT TRIGGER receipt_fixture_commit_pause AFTER INSERT ON job_attempt_receipts + DEFERRABLE INITIALLY DEFERRED FOR EACH ROW WHEN (NEW.event_kind='queue_closed') + EXECUTE FUNCTION receipt_fixture_commit_pause()`); + try { + await writer.query("BEGIN"); + await writer.query("UPDATE jobs SET status='done',active_claim_receipt_id=NULL WHERE id=$1::bigint", [c.jobId]); + await appendPreparedReceiptBatch(writer, [event(c.jobId, c.ordinal, c.id, { eventKind: "queue_closed", outcome: "succeeded" })]); + const committing = writer.query("COMMIT").then(() => true, () => false); + await query("SELECT pg_sleep(0.05)"); + expect((await query("SELECT wait_event FROM pg_stat_activity WHERE pid=$1::integer", [pid]))[0]!.wait_event).toBe("PgSleep"); + writer.markTerminated(); await query("SELECT pg_terminate_backend($1::integer)", [pid]); + expect(await committing).toBe(false); + expect((await query("SELECT status,active_claim_receipt_id::text FROM jobs WHERE id=$1::bigint", [c.jobId]))[0]) + .toEqual({ status: "running", active_claim_receipt_id: c.id }); + expect((await query("SELECT receipt_id FROM job_attempt_receipts WHERE claim_receipt_id=$1::bigint AND event_kind='queue_closed'", [c.id])).length).toBe(0); + } finally { + await query("DROP TRIGGER receipt_fixture_commit_pause ON job_attempt_receipts"); + await query("DROP FUNCTION receipt_fixture_commit_pause()"); + } + expect(await closeClaim(c, "succeeded")).toBe(true); + // Reconciliation after a committed transition returns no second occurrence. + expect(await closeClaim(c, "succeeded")).toBe(false); + }); + + test("repeatable-read and autocommit cannot append authoritative receipts", async () => { + const writer = await connect(); const j = await job(); + await writer.query("BEGIN ISOLATION LEVEL REPEATABLE READ"); + await expect(appendPreparedReceiptBatch(writer, [event(j, 1, null)])).rejects.toThrow(); + await writer.query("ROLLBACK"); + await expect(appendPreparedReceiptBatch(writer, [event(j, 1, null)])).rejects.toThrow(); + }); + + test("reader lock timeout rolls back and releases its transaction", async () => { + const holder = await connect(); const reader = await connect(); + const pid = (await reader.query("SELECT pg_backend_pid() AS pid")).rows[0]!.pid; + await holder.query("BEGIN"); await holder.query("LOCK job_attempt_receipts IN ACCESS EXCLUSIVE MODE"); + await reader.query("BEGIN READ ONLY"); await reader.query("SET LOCAL lock_timeout='100ms'"); + const start = performance.now(); + await expect(reader.query(readAttemptReceiptPageSql, ["0"])).rejects.toThrow(); + expect(performance.now() - start).toBeLessThan(1500); + await reader.query("ROLLBACK"); await holder.query("ROLLBACK"); + const [state] = await query("SELECT state,xact_start FROM pg_stat_activity WHERE pid=$1::integer", [pid]); + expect(state).toEqual({ state: "idle", xact_start: null }); + expect((await claim(await job()))!.id).toBeString(); + }); + + test("legacy writers remain available and do not manufacture receipts", async () => { + const j = await job(); + await query("UPDATE jobs SET status='running',attempts=attempts+1,locked_by='fixture-legacy' WHERE id=$1::bigint", [j]); + await query("UPDATE jobs SET status='done',locked_by=NULL,last_error=NULL WHERE id=$1::bigint", [j]); + expect((await query("SELECT receipt_id FROM job_attempt_receipts WHERE job_id=$1::bigint", [j])).length).toBe(0); + const c = (await claim(await job()))!; + // An old administrative writer can still transition a new claim; that + // transition has no receipt and fleet completeness must remain unknown. + await query("UPDATE jobs SET status='queued',locked_by=NULL WHERE id=$1::bigint", [c.jobId]); + expect(await closeClaim(c, "succeeded")).toBe(false); + expect((await query("SELECT receipt_id FROM job_attempt_receipts WHERE job_id=$1::bigint", [c.jobId])).length).toBe(1); + }); + + test("a crash between review and queue commits leaves a partial history", async () => { + const c = (await claim(await job()))!; const r = await review(c); + await observe(c, r, "failed"); + expect((await query("SELECT outcome FROM job_attempt_receipts WHERE job_id=$1::bigint AND event_kind='queue_closed'", [c.jobId])).length).toBe(0); + expect((await query("SELECT status FROM jobs WHERE id=$1::bigint", [c.jobId]))[0]!.status).toBe("running"); + await closeClaim(c, "interrupted"); + expect((await query("SELECT outcome FROM job_attempt_receipts WHERE job_id=$1::bigint ORDER BY receipt_id", [c.jobId])).map((r) => r.outcome)) + .toEqual(["started", "bound", "failed", "interrupted"]); + }); + + test("append-only guard prevents suffix loss; a privileged loss is visible only when acknowledged", async () => { + const c = (await claim(await job()))!; await closeClaim(c, "succeeded"); + const suffix = (await query(receiptHeadSql))[0]!.receipt_id; + await expect(query("DELETE FROM job_attempt_receipts WHERE receipt_id=$1::bigint", [suffix])).rejects.toThrow(); + // Fault injection is restricted to the dedicated disposable database. + await query("BEGIN"); + try { + await query("ALTER TABLE job_attempt_receipts DISABLE TRIGGER job_attempt_receipts_immutable"); + await query("DELETE FROM job_attempt_receipts WHERE receipt_id=$1::bigint", [suffix]); + const acknowledged = (await query(readAttemptReceiptPageSql, [suffix]))[0]!; + expect(acknowledged.anchor_id).toBeNull(); + expect(BigInt(acknowledged.head_id)).toBeLessThan(BigInt(suffix)); + const unseen = (await query(readAttemptReceiptPageSql, [c.id]))[0]!; + expect(unseen.anchor_id).toBe(c.id); + expect(unseen.receipt_id).toBeNull(); + // An unseen suffix loss supplies no detectable discontinuity by itself. + } finally { await query("ROLLBACK"); } + expect((await query(receiptHeadSql))[0]!.receipt_id).toBe(suffix); + }); + + test("a simultaneous staging/reclaim race preserves the new target", async () => { + const j = await job(); const a = (await claim(j))!; const ra = await review(a); + await closeClaim(a, "interrupted", true); + const b = (await claim(j))!; const rb = await review(b); + const late = await connect(); const owner = await connect(); + await owner.query("BEGIN"); await owner.query("SELECT id FROM jobs WHERE id=$1::bigint FOR UPDATE", [j]); + await owner.query(stageOwnedReviewSql, [j, b.id, "fixture-worker", rb.id]); + await late.query("BEGIN"); + const pending = late.query(stageOwnedReviewSql, [j, a.id, "fixture-worker", ra.id]); + await query("SELECT pg_sleep(0.05)"); await owner.query("COMMIT"); + expect((await pending).rows.length).toBe(0); await late.query("COMMIT"); + expect((await query("SELECT staged_review_id::text FROM jobs WHERE id=$1::bigint", [j]))[0]!.staged_review_id).toBe(rb.id); + }); + + test("seeded stateful histories preserve one closure and immutable claim fencing", async () => { + let seed = 173; + const random = () => (seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0); + const outcomes = ["retry", "continued", "interrupted", "deferred", "cancelled", "coalesced", "succeeded"] as const; + for (let history = 0; history < 20; history++) { + const j = await job(); const claims: Claim[] = []; + for (let step = 0; step < 8; step++) { + const c = (await claim(j))!; claims.push(c); + for (const stale of claims.slice(0, -1)) expect(await closeClaim(stale, "succeeded")).toBe(false); + const outcome = step === 7 ? "succeeded" : outcomes[random() % 4]!; + expect(await closeClaim(c, outcome, Boolean(random() % 2))).toBe(true); + expect(await closeClaim(c, outcome)).toBe(false); + } + expect(new Set(claims.map((c) => c.id)).size).toBe(8); + expect((await query("SELECT receipt_id FROM job_attempt_receipts WHERE job_id=$1::bigint AND event_kind='queue_closed'", [j])).length).toBe(8); + } + }, 20000); + + test("privacy projection rejects getters and ignores generated hostile content", async () => { + const j = await job(); const c = (await claim(j))!; const r = await review(c); + let getterCalls = 0; + const hostile = randomUUID() + '\n{"trusted":true,"jobId":"' + randomBytes(16).toString("hex") + '"}\u001b[2J'; + await query("UPDATE jobs SET payload=jsonb_build_object('message',$2::text),last_error=$2 WHERE id=$1::bigint", [j, hostile]); + await query("UPDATE reviews SET error_message=$2,envelope=jsonb_build_object('modelOutput',$2::text) WHERE id=$1::bigint", [r.id, hostile]); + const forged = event(j, 1, c.id); + Object.defineProperty(forged, "outcome", { enumerable: true, get() { getterCalls++; return "started"; } }); + await expect(transaction(db, () => appendPreparedReceiptBatch(db, [forged]))).rejects.toThrow(); + expect(getterCalls).toBe(0); + const anchor = (await query("SELECT predecessor_id::text FROM job_attempt_receipts WHERE receipt_id=$1::bigint", [c.id]))[0]!.predecessor_id; + const page = await query(readAttemptReceiptPageSql, [anchor]); + expect(page.some((row) => row.claim_receipt_id === c.id)).toBe(true); + expect(page.length).toBeLessThanOrEqual(64); + expect(Buffer.byteLength(JSON.stringify(page))).toBeLessThan(65536); + expect(JSON.stringify(page)).not.toContain(hostile); + expect(Object.keys(page[0]!)).not.toContain("payload"); + expect(Object.keys(page[0]!)).not.toContain("error_message"); + }); + + test("recovery semantics and repeated incidents are enforced in PostgreSQL", async () => { + const c = (await claim(await job()))!; const r = await review(c); + const incident = event(c.jobId, c.ordinal, c.id, { eventKind: "incident", outcome: "observed", + reviewId: r.id, reviewPublicId: r.public_id, phase: "review", category: "invalidOutput", recovered: false }); + await expect(transaction(db, () => appendPreparedReceiptBatch(db, [{ ...incident, recovery: "repair" }]))).rejects.toThrow(); + await expect(transaction(db, () => appendPreparedReceiptBatch(db, [{ ...incident, recovered: true }]))).rejects.toThrow(); + const ids = await transaction(db, () => appendPreparedReceiptBatch(db, [incident, incident, { ...incident, recovered: true, recovery: "repair" }])); + expect(new Set(ids).size).toBe(3); + }); + + test("large decimal identities retain numeric order and timestamps do not advance a cursor", async () => { + const [floor] = await query(`SELECT setval('job_attempt_receipt_ids', + GREATEST((SELECT receipt_id+1 FROM job_attempt_receipts ORDER BY receipt_id DESC LIMIT 1),9007199254740993),false)::text AS id`); + await query("ALTER TABLE job_attempt_receipts ALTER COLUMN occurred_at SET DEFAULT '2030-01-01T00:00:00Z'"); + const a = (await claim(await job()))!; const b = (await claim(await job()))!; + await query("ALTER TABLE job_attempt_receipts ALTER COLUMN occurred_at SET DEFAULT '2000-01-01T00:00:00Z'"); + let c: Claim; + try { c = (await claim(await job()))!; } + finally { await query("ALTER TABLE job_attempt_receipts ALTER COLUMN occurred_at SET DEFAULT clock_timestamp()"); } + expect(a.id).toBe(floor!.id); expect(BigInt(b.id)).toBe(BigInt(a.id) + 1n); + const rows = await query(readAttemptReceiptPageSql, [a.id]); + expect(rows.map((r) => r.receipt_id)).toEqual([b.id, c!.id]); + expect(rows[0]!.occurred_at.startsWith("2030-")).toBe(true); + expect(rows[1]!.occurred_at.startsWith("2000-")).toBe(true); + expect(rows[0]!.predecessor_id).toBe(a.id); + // Reading and accepting a prefix are separate operations in this harness. + const accepted = b.id; + expect((await query(readAttemptReceiptPageSql, [accepted]))[0]!.receipt_id).toBe(c!.id); + expect((await query(readAttemptReceiptPageSql, [a.id]))[0]!.receipt_id).toBe(b.id); + }); + + test("finite page has indexed range/genesis/anchor/backwards-head access", async () => { + await query("ANALYZE job_attempt_receipts"); + const explain = await query("EXPLAIN (ANALYZE, FORMAT JSON) " + readAttemptReceiptPageSql, ["9007199254740993"]); + const plan = JSON.stringify(explain); + expect(plan).toContain("job_attempt_receipts_pkey"); + expect(plan).toContain("Index Cond"); + expect(plan).toContain("Backward"); + expect(plan).not.toContain("Seq Scan"); + expect((await query(readAttemptReceiptPageSql, ["0"])).length).toBe(64); + }); +}); + +// Production driver and API checks share only the explicitly marked fixture. +import { Pool } from "pg"; +import { createServer, connect as connectSocket } from "node:net"; +import { drizzle } from "drizzle-orm/node-postgres"; +import * as schema from "../src/lib/db/schema"; +import { + claimJob, completeJob, continueClaimedJob, failJob, requeueJobsOwnedBy, + retryJobIndefinitely, externalSideEffectLeaseActive, cancelPullRequestPublication, + type ClaimedJob, +} from "../src/lib/queue"; +import { + insertReviewForClaim, bindStagedReviewClaim, stageReviewCompletionCandidate, + finalizeStagedReviewCompletionWithGateMode, +} from "../src/lib/review-completion"; +import { claimPausedHostedReview } from "../src/lib/hosted-review-pause"; +import { deferHostedReviewForRelease } from "../src/lib/release-job-rollout"; +import { consumePrivateWorkerRehearsalAfterStaging } from "../src/lib/private-worker-rehearsal"; +import { + readAdmittedReceiptPage, closeReceiptAdmission, admitReceiptSource, acknowledgeReceiptPage, + recordReceiptPageReported, receiptPageAdmitted, type ReceiptAdmissionState, +} from "../src/lib/job-attempt-receipts"; +import type { Envelope } from "../src/lib/envelope"; + +describe.skipIf(!port)("production receipt ownership APIs on PostgreSQL", () => { + let pool: Pool; + let database: ReturnType>; + const rows = async (text: string, values: unknown[] = []) => (await pool.query(text, values)).rows; + beforeAll(async () => { + pool = new Pool({ host: "127.0.0.1", port: Number(port), user: "postgres", + database: process.env.POSTIL_RECEIPT_TEST_DATABASE, max: 8, + connectionTimeoutMillis: 2000, statement_timeout: 6000, application_name: "receipt-api-fixture" }); + database = drizzle(pool, { schema }); + const [marker] = await rows(`SELECT shobj_description(oid,'pg_database') AS marker + FROM pg_database WHERE datname=current_database()`); + if (marker?.marker !== "disposable receipt qualification") throw new Error("dedicated fixture required"); + if (!repository) { + const [installation] = await rows(`INSERT INTO installations(github_installation_id,account_login,account_type) + VALUES($1,'receipt-test','Organization') RETURNING id`, [Date.now()]); + const [repo] = await rows(`INSERT INTO repositories(installation_id,github_repo_id,full_name) + VALUES($1,$2,'receipt-test/api') RETURNING id::text`, [installation.id, Date.now()]); + repository = repo.id; + } + }); + afterAll(async () => { await pool?.end(); }); + async function start(payload: Record = {}): Promise { + await rows("UPDATE jobs SET run_after='infinity' WHERE status='queued'"); + const [j] = await rows("INSERT INTO jobs(kind,payload,max_attempts) VALUES('review',$1,5) RETURNING id::text", [payload]); + const c = await claimJob(pool, "reused-worker", ["review"]); + expect(c?.receiptClaim?.jobId).toBe(j.id); + expect(Object.isFrozen(c!.receiptClaim)).toBe(true); + return c!; + } + const values = () => ({ repositoryId: Number(repository), prNumber: 1, + headSha: randomBytes(20).toString("hex"), baseSha: randomBytes(20).toString("hex"), status: "running" as const }); + function envelope(): Envelope { + return { version: 1, summary: randomUUID(), silent: true, findings: [], resolved: [], + counts: { info: 0, warn: 0, error: 0, suppressed: 0, ungrounded: 0 }, + confidenceBuckets: [0, 0, 0, 0, 0], gate: { failOn: "error", failing: false }, + modelUsed: "fixture/model", usage: { promptTokens: 3, completionTokens: 2 }, durationMs: 1, + baseSha: randomBytes(20).toString("hex"), headSha: randomBytes(20).toString("hex"), sinceSha: null }; + } + function stageInput(c: ClaimedJob, reviewId: number, e = envelope()) { + return { reviewId, reviewJobId: c.id, reviewClaim: c, envelope: e, + configFiles: [], silent: true, gateFailing: false, + publicationReceipt: { version: 1 as const, receiptId: randomUUID(), findings: [] } }; + } + async function replace(c: ClaimedJob) { + expect(await requeueJobsOwnedBy(pool, c.lockedBy, "fixture interruption", ["review"], [c.id], [c])).toBe(1); + const next = await claimJob(pool, c.lockedBy, ["review"]); + expect(next!.attempts).toBe(c.attempts); + expect(next!.receiptClaim!.receiptId).not.toBe(c.receiptClaim!.receiptId); + return next!; + } + const pointers = (c: ClaimedJob) => rows(`SELECT active_claim_receipt_id::text, staged_review_id::text, + payload, status, attempts, locked_by, locked_at FROM jobs WHERE id=$1::bigint`, [c.receiptClaim!.jobId]); + + async function losingSpend(stageFirst = false) { + const a = await start(); const review = (await insertReviewForClaim(database, values(), a))!; + const [org] = await rows("INSERT INTO organizations(name,slug) VALUES($1,$2) RETURNING id", ["Receipt fixture", randomUUID()]); + const [reservation] = await rows(`INSERT INTO hosted_usage_reservations(org_id,review_id,reserved_micros,expires_at) + VALUES($1,$2,1000,now()+interval '1 hour') RETURNING id`, [org.id, review.id]); + const spend = { reservationId: reservation.id as string, repositoryId: Number(repository), reviewId: review.id, + triggerSource: "unknown" as const, usageAccountingComplete: true, + usage: [{ orgId: Number(org.id), repositoryId: Number(repository), promptTokens: 3, completionTokens: 2, + modelUsed: "fixture/model", costMicros: 7, billingScope: "private_hosted" as const }] }; + const input = { ...stageInput(a, review.id), hostedSpend: spend }; + if (stageFirst) await stageReviewCompletionCandidate(database, input, null); + const b = await replace(a); + if (stageFirst) expect(await bindStagedReviewClaim(database, review.id, b)).toBe(true); + else { + const other = (await insertReviewForClaim(database, values(), b))!; + await stageReviewCompletionCandidate(database, stageInput(b, other.id), null); + } + return { a, b, review, input, spend }; + } + const spendState = async (reviewId: number) => (await rows(`SELECT r.status,r.actual_micros::text, + (SELECT count(*)::int FROM usage_events u WHERE u.review_id=r.review_id) AS usage_count, + (SELECT COALESCE(sum(cost_micros),0)::text FROM usage_events u WHERE u.review_id=r.review_id) AS cost + FROM hosted_usage_reservations r WHERE r.review_id=$1`, [reviewId]))[0]; + + test("losing stage settles its own receipt once and append failure rolls back the settlement", async () => { + const { b, review, input } = await losingSpend(); const before = await pointers(b); + await expect(stageReviewCompletionCandidate(database, { ...input, + hostedSpend: { ...input.hostedSpend, repositoryId: Number(repository) + 1 } }, null)).rejects.toThrow("identity mismatch"); + await rows(`CREATE FUNCTION receipt_fixture_spend_reject() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN RAISE EXCEPTION 'fixture append rejection'; END $$`); + await rows(`CREATE TRIGGER receipt_fixture_spend_reject BEFORE INSERT ON job_attempt_receipts + FOR EACH ROW EXECUTE FUNCTION receipt_fixture_spend_reject()`); + try { + await expect(stageReviewCompletionCandidate(database, input, null)).rejects.toThrow(); + expect(await spendState(review.id)).toEqual({ status: "active", actual_micros: null, usage_count: 0, cost: "0" }); + expect((await rows("SELECT envelope FROM reviews WHERE id=$1", [review.id]))[0].envelope).toBeNull(); + expect((await rows("SELECT count(*)::int AS n FROM review_publication_receipts WHERE review_id=$1", [review.id]))[0].n).toBe(0); + expect(await pointers(b)).toEqual(before); + } finally { + await rows("DROP TRIGGER receipt_fixture_spend_reject ON job_attempt_receipts"); + await rows("DROP FUNCTION receipt_fixture_spend_reject()"); + } + const results = await Promise.all([0, 1].map(() => stageReviewCompletionCandidate(database, input, null))); + expect(results.every((r) => r.staged && r.claimOwned === false)).toBe(true); + expect(await spendState(review.id)).toEqual({ status: "reconciled", actual_micros: "7", usage_count: 1, cost: "7" }); + expect(await pointers(b)).toEqual(before); + expect((await rows("SELECT count(*)::int AS n FROM job_attempt_receipts WHERE review_id=$1 AND outcome='staged'", [review.id]))[0].n).toBe(1); + expect(await completeJob(pool, b)).toBe("done"); + }); + + test("genuine recovery finalizes after the late producer already settles the same receipt", async () => { + const { b, review, input, spend } = await losingSpend(true); + expect((await stageReviewCompletionCandidate(database, input, null)).claimOwned).toBe(false); + const accounting = { reviewId: review.id, reviewClaim: b, hostedUsageReservationId: spend.reservationId, + usage: spend.usage, usageAccountingComplete: true }; + expect((await finalizeStagedReviewCompletionWithGateMode(database, accounting, null)).completed).toBe(true); + expect((await finalizeStagedReviewCompletionWithGateMode(database, accounting, null)).completed).toBe(false); + expect(await spendState(review.id)).toEqual({ status: "reconciled", actual_micros: "7", usage_count: 1, cost: "7" }); + expect(await completeJob(pool, b)).toBe("done"); + }); + + test("terminal staging settles once across current and lost claims without accepting stale evidence", async () => { + for (const status of ["stale", "failed", "completed"]) { + for (const recoveryOwns of [false, true]) { + const x = await losingSpend(recoveryOwns); + const input = recoveryOwns ? { ...x.input, reviewClaim: x.b } : x.input; + await rows("UPDATE reviews SET status=$2,finished_at=now() WHERE id=$1", [x.review.id, status]); + const before = await pointers(x.b); + const evidence = await rows("SELECT envelope,gate_failing FROM reviews WHERE id=$1", [x.review.id]); + const receipts = await rows("SELECT receipt_id::text FROM job_attempt_receipts WHERE review_id=$1 ORDER BY receipt_id", [x.review.id]); + await expect(stageReviewCompletionCandidate(database, { ...input, + hostedSpend: { ...x.spend, reviewId: x.review.id + 1 } }, null)).rejects.toThrow("identity mismatch"); + const results = await Promise.all([0, 1].map(() => stageReviewCompletionCandidate(database, input, null))); + expect(results.every((r) => r.staged === false && r.completed === false)).toBe(true); + expect(await spendState(x.review.id)).toEqual({ status: "reconciled", actual_micros: "7", usage_count: 1, cost: "7" }); + expect(await pointers(x.b)).toEqual(before); + expect((await rows("SELECT status FROM reviews WHERE id=$1", [x.review.id]))[0].status).toBe(status); + expect(await rows("SELECT envelope,gate_failing FROM reviews WHERE id=$1", [x.review.id])).toEqual(evidence); + expect(await rows("SELECT receipt_id::text FROM job_attempt_receipts WHERE review_id=$1 ORDER BY receipt_id", [x.review.id])).toEqual(receipts); + expect(await completeJob(pool, x.b)).toBe("done"); + } + } + }); + + test("terminal settlement rolls back on usage failure and backend death, then retries once", async () => { + const x = await losingSpend(); + await rows("UPDATE reviews SET status='stale',finished_at=now() WHERE id=$1", [x.review.id]); + const before = await pointers(x.b); + await rows(`CREATE FUNCTION receipt_terminal_usage_reject() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN RAISE EXCEPTION 'fixture usage rejection'; END $$`); + await rows(`CREATE TRIGGER receipt_terminal_usage_reject BEFORE INSERT ON usage_events + FOR EACH ROW EXECUTE FUNCTION receipt_terminal_usage_reject()`); + try { + await expect(stageReviewCompletionCandidate(database, x.input, null)).rejects.toThrow(); + expect(await spendState(x.review.id)).toEqual({ status: "active", actual_micros: null, usage_count: 0, cost: "0" }); + expect(await pointers(x.b)).toEqual(before); + } finally { + await rows("DROP TRIGGER receipt_terminal_usage_reject ON usage_events"); + await rows("DROP FUNCTION receipt_terminal_usage_reject()"); + } + const blocker = await pool.connect(); + const producer = new Pool({ host: "127.0.0.1", port: Number(port), user: "postgres", + database: process.env.POSTIL_RECEIPT_TEST_DATABASE, max: 1, statement_timeout: 4000 }); + let inserting!: () => void; + const reached = new Promise((resolve) => { inserting = resolve; }); + producer.on("connect", (client) => { + client.on("error", () => undefined); + const original = client.query.bind(client); + client.query = ((...args: any[]) => { + if ((typeof args[0] === "string" ? args[0] : args[0]?.text)?.startsWith('insert into "usage_events"')) inserting(); + return (original as Function)(...args); + }) as typeof client.query; + }); + try { + const pid = (await producer.query("SELECT pg_backend_pid() AS pid")).rows[0].pid; + await blocker.query("BEGIN"); await blocker.query("LOCK TABLE usage_events IN ACCESS EXCLUSIVE MODE"); + const pending = stageReviewCompletionCandidate(drizzle(producer, { schema }), x.input, null).then(() => false, () => true); + await reached; + expect((await rows("SELECT status,actual_micros FROM hosted_usage_reservations WHERE review_id=$1", [x.review.id]))[0]) + .toEqual({ status: "active", actual_micros: null }); + await rows("SELECT pg_terminate_backend($1,1000)", [pid]); + expect(await pending).toBe(true); + await blocker.query("COMMIT"); + expect(await spendState(x.review.id)).toEqual({ status: "active", actual_micros: null, usage_count: 0, cost: "0" }); + expect(await pointers(x.b)).toEqual(before); + expect((await stageReviewCompletionCandidate(database, x.input, null)).staged).toBe(false); + expect((await stageReviewCompletionCandidate(database, x.input, null)).staged).toBe(false); + expect(await spendState(x.review.id)).toEqual({ status: "reconciled", actual_micros: "7", usage_count: 1, cost: "7" }); + } finally { await blocker.query("ROLLBACK"); blocker.release(); await producer.end(); } + }); + + test("B wins staging: concurrent stale A retains only A evidence and cannot mutate B", async () => { + const a = await start(); const ra = (await insertReviewForClaim(database, values(), a))!; + const b = await replace(a); const rb = (await insertReviewForClaim(database, values(), b))!; + expect((await stageReviewCompletionCandidate(database, stageInput(b, rb.id), null)).claimOwned).toBe(true); + const before = await pointers(b); + const results = await Promise.all([ + stageReviewCompletionCandidate(database, stageInput(a, ra.id), null), + completeJob(pool, a), failJob(pool, a, "stale", { permanent: true }), + retryJobIndefinitely(pool, a, "stale"), + continueClaimedJob(pool, a, {}).then(() => false, () => true), + requeueJobsOwnedBy(pool, a.lockedBy, "stale", ["review"], [a.id], [a]), + ]); + expect(results[0]).toMatchObject({ staged: true, claimOwned: false }); + expect(results.slice(1)).toEqual(["lost", "lost", "lost", true, 0]); + expect(await pointers(b)).toEqual(before); + expect(await consumePrivateWorkerRehearsalAfterStaging(pool, { + reviewId: ra.id, reviewJobId: a.id, reviewClaim: a, + repoFullName: "fixture/rehearsal", prNumber: 1, headSha: randomBytes(20).toString("hex"), + workerInstanceId: "reused-worker", sandbox: { orgSlug: "fixture", repoFullName: "fixture/rehearsal" }, + })).toBeNull(); + expect(await pointers(b)).toEqual(before); + expect((await rows(`SELECT claim_receipt_id::text FROM job_attempt_receipts + WHERE review_id=$1 AND event_kind='review_observed'`, [ra.id]))).toEqual([{ claim_receipt_id: a.receiptClaim!.receiptId }]); + expect(await externalSideEffectLeaseActive(pool, a)).toBe(false); + expect(await externalSideEffectLeaseActive(pool, b)).toBe(true); + expect(await completeJob(pool, b)).toBe("done"); + expect(await completeJob(pool, b)).toBe("lost"); + }); + + test("A stages first: B binds the genuine target and accounts once with real reservation effects", async () => { + const a = await start(); const r = (await insertReviewForClaim(database, values(), a))!; + const e = envelope(); + e.modelIncidents = [0, 1].map(() => ({ phase: "review", category: "timeout", recovered: true, recovery: "repair" })); + const input = stageInput(a, r.id, e); + expect((await stageReviewCompletionCandidate(database, input, null)).claimOwned).toBe(true); + expect((await stageReviewCompletionCandidate(database, input, null)).staged).toBe(true); + expect((await rows("SELECT count(*)::int AS n FROM job_attempt_receipts WHERE review_id=$1 AND event_kind='incident'", [r.id]))[0].n).toBe(2); + const b = await replace(a); + expect(await bindStagedReviewClaim(database, r.id, b)).toBe(true); + expect(await bindStagedReviewClaim(database, r.id, b)).toBe(true); + const [org] = await rows("INSERT INTO organizations(name,slug) VALUES($1,$2) RETURNING id", ["Receipt fixture", randomUUID()]); + const [reservation] = await rows(`INSERT INTO hosted_usage_reservations(org_id,review_id,reserved_micros,expires_at) + VALUES($1,$2,1000,now()+interval '1 hour') RETURNING id`, [org.id, r.id]); + const completion = { reviewId: r.id, reviewClaim: b, hostedUsageReservationId: reservation.id, + usageAccountingComplete: true, usage: [{ orgId: Number(org.id), repositoryId: Number(repository), + promptTokens: 3, completionTokens: 2, modelUsed: "fixture/model", costMicros: 7, billingScope: "private_hosted" as const }] }; + expect((await finalizeStagedReviewCompletionWithGateMode(database, completion, null)).completed).toBe(true); + expect((await finalizeStagedReviewCompletionWithGateMode(database, completion, null)).completed).toBe(false); + expect((await rows("SELECT status,actual_micros::text FROM hosted_usage_reservations WHERE id=$1", [reservation.id]))[0]) + .toEqual({ status: "reconciled", actual_micros: "7" }); + expect((await rows("SELECT count(*)::int AS n FROM usage_events WHERE review_id=$1", [r.id]))[0].n).toBe(1); + expect((await rows("SELECT origin_claim_receipt_id::text FROM reviews WHERE id=$1", [r.id]))[0].origin_claim_receipt_id).toBe(a.receiptClaim!.receiptId); + expect(await completeJob(pool, b)).toBe("done"); + }); + + test("raw decimal job identity survives numeric compatibility and origin cannot be reassigned", async () => { + await rows("UPDATE jobs SET run_after='infinity' WHERE status='queued'"); + const raw = (9007199254740993n + BigInt(randomBytes(4).readUInt32BE()) * 2n).toString(); + await rows("INSERT INTO jobs(id,kind,payload) OVERRIDING SYSTEM VALUE VALUES($1,'review','{}')", [raw]); + const a = (await claimJob(pool, "raw-fixture", ["review"]))!; + expect(a.receiptClaim!.jobId).toBe(raw); + const review = (await insertReviewForClaim(database, values(), a))!; + await expect(rows("UPDATE reviews SET origin_claim_receipt_id=NULL WHERE id=$1", [review.id])).rejects.toThrow("immutable review origin"); + expect((await stageReviewCompletionCandidate(database, stageInput(a, review.id), null)).claimOwned).toBe(true); + expect(await completeJob(pool, a)).toBe("done"); + expect((await rows("SELECT job_id::text FROM job_attempt_receipts WHERE claim_receipt_id=$1 AND event_kind='queue_closed'", [a.receiptClaim!.receiptId]))[0].job_id).toBe(raw); + }); + + test("same-job history and a forged payload cannot authorize a new binding", async () => { + const a = await start(); const r = (await insertReviewForClaim(database, values(), a))!; + const b = await replace(a); + await rows("UPDATE jobs SET payload=jsonb_build_object('recoveryReviewId',$2::bigint) WHERE id=$1", [b.id, r.id]); + expect(await bindStagedReviewClaim(database, r.id, b)).toBe(false); + await expect(stageReviewCompletionCandidate(database, stageInput(b, r.id), null)).rejects.toThrow("binding is missing"); + const altered = { ...b, lockedAt: new Date(b.lockedAt.getTime() + 1) }; + expect(await completeJob(pool, altered)).toBe("lost"); + expect(await externalSideEffectLeaseActive(pool, altered)).toBe(false); + expect(await completeJob(pool, b)).toBe("done"); + }); + + test("legacy-origin recovery remains available but still honors the new owner's lease", async () => { + const c = await start(); + const r = (await insertReviewForClaim(database, values()))!; + const input = stageInput(c, r.id); + await stageReviewCompletionCandidate(database, { ...input, reviewClaim: undefined }, null); + const accounting = { reviewId: r.id, reviewClaim: c, usageAccountingComplete: true, + usage: [{ orgId: null, repositoryId: Number(repository), promptTokens: 0, completionTokens: 0, + modelUsed: "fixture/model", costMicros: 0, billingScope: "analytics" as const }] }; + expect((await finalizeStagedReviewCompletionWithGateMode(database, { ...accounting, + reviewClaim: { ...c, lockedAt: new Date(c.lockedAt.getTime() + 1) } }, null)).completed).toBe(false); + expect((await finalizeStagedReviewCompletionWithGateMode(database, accounting, null)).completed).toBe(true); + expect((await rows("SELECT origin_claim_receipt_id FROM reviews WHERE id=$1", [r.id]))[0].origin_claim_receipt_id).toBeNull(); + expect((await rows("SELECT count(*)::int AS n FROM job_attempt_receipts WHERE review_id=$1", [r.id]))[0].n).toBe(0); + expect(await completeJob(pool, c)).toBe("done"); + }); + + test("append rejection rolls back claim, staging, publication and queue effects", async () => { + const a = await start(); const r = (await insertReviewForClaim(database, values(), a))!; + const before = await pointers(a); + await rows(`CREATE FUNCTION receipt_fixture_reject() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN RAISE EXCEPTION 'fixture append rejection'; END $$`); + await rows(`CREATE TRIGGER receipt_fixture_reject BEFORE INSERT ON job_attempt_receipts + FOR EACH ROW EXECUTE FUNCTION receipt_fixture_reject()`); + try { + await expect(stageReviewCompletionCandidate(database, stageInput(a, r.id), null)).rejects.toThrow(); + expect(await pointers(a)).toEqual(before); + expect((await rows("SELECT envelope FROM reviews WHERE id=$1", [r.id]))[0].envelope).toBeNull(); + await expect(completeJob(pool, a)).rejects.toThrow(); + expect(await pointers(a)).toEqual(before); + await rows("INSERT INTO jobs(kind,payload) VALUES('review','{}')"); + await expect(claimJob(pool, "failure", ["review"])).rejects.toThrow(); + expect((await rows("SELECT count(*)::int AS n FROM jobs WHERE locked_by='failure'"))[0].n).toBe(0); + } finally { + await rows("DROP TRIGGER receipt_fixture_reject ON job_attempt_receipts"); + await rows("DROP FUNCTION receipt_fixture_reject()"); + } + expect(await completeJob(pool, a)).toBe("done"); + }); + + test("staging holds the job fence while blocked at append; backend death rolls back a claim", async () => { + const a = await start(); const r = (await insertReviewForClaim(database, values(), a))!; + const blocker = await pool.connect(); + await blocker.query("BEGIN"); await blocker.query(receiptAppendLockSql); + let reached!: () => void; + const waiting = new Promise((resolve) => { reached = resolve; }); + const producer = new Pool({ host: "127.0.0.1", port: Number(port), user: "postgres", + database: process.env.POSTIL_RECEIPT_TEST_DATABASE, max: 1, statement_timeout: 4000 }); + producer.on("connect", (client) => { + client.on("error", () => undefined); + const original = client.query.bind(client); + client.query = ((...args: any[]) => { + if ((typeof args[0] === "string" ? args[0] : args[0]?.text)?.includes("pg_advisory_xact_lock(17491")) reached(); + return (original as Function)(...args); + }) as typeof client.query; + }); + try { + const staged = stageReviewCompletionCandidate(drizzle(producer, { schema }), stageInput(a, r.id), null); + await waiting; + const contender = await pool.connect(); + await contender.query("BEGIN"); await contender.query("SET LOCAL lock_timeout='100ms'"); + await expect(contender.query("SELECT id FROM jobs WHERE id=$1 FOR UPDATE", [a.id])).rejects.toMatchObject({ code: "55P03" }); + await contender.query("ROLLBACK"); contender.release(); + await blocker.query("COMMIT"); + expect((await staged).claimOwned).toBe(true); + const b = await replace(a); + expect(await bindStagedReviewClaim(database, r.id, b)).toBe(true); + expect(await completeJob(pool, b)).toBe("done"); + await rows("UPDATE jobs SET run_after='infinity' WHERE status='queued'"); + const [j] = await rows("INSERT INTO jobs(kind,payload) VALUES('review','{}') RETURNING id::text"); + await blocker.query("BEGIN"); await blocker.query(receiptAppendLockSql); + const dying = await producer.connect(); + const pid = (await dying.query("SELECT pg_backend_pid() AS pid")).rows[0].pid; + dying.release(); + const reachedAgain = new Promise((resolve) => { reached = resolve; }); + const pending = claimJob(producer, "crash-fixture", ["review"]); + const failure = pending.then(() => false, () => true); + await reachedAgain; + await rows("SELECT pg_terminate_backend($1)", [pid]); + expect(await failure).toBe(true); + await blocker.query("COMMIT"); + expect((await rows("SELECT status,attempts,active_claim_receipt_id FROM jobs WHERE id=$1", [j.id]))[0]) + .toEqual({ status: "queued", attempts: 0, active_claim_receipt_id: null }); + await rows("UPDATE jobs SET run_after=now() WHERE id=$1", [j.id]); + const retry = (await claimJob(pool, "crash-fixture", ["review"]))!; + expect(retry.receiptClaim!.jobId).toBe(j.id); + expect(await completeJob(pool, retry)).toBe("done"); + const lost = await losingSpend(); const before = await pointers(lost.b); + const stageBackend = (await producer.query("SELECT pg_backend_pid() AS pid")).rows[0].pid; + await blocker.query("BEGIN"); await blocker.query(receiptAppendLockSql); + const settlementReached = new Promise((resolve) => { reached = resolve; }); + const pendingStage = stageReviewCompletionCandidate(drizzle(producer, { schema }), lost.input, null); + const failedStage = pendingStage.then(() => false, () => true); + await settlementReached; + expect(await spendState(lost.review.id)).toEqual({ status: "active", actual_micros: null, usage_count: 0, cost: "0" }); + await rows("SELECT pg_terminate_backend($1,1000)", [stageBackend]); + expect(await failedStage).toBe(true); await blocker.query("COMMIT"); + expect(await spendState(lost.review.id)).toEqual({ status: "active", actual_micros: null, usage_count: 0, cost: "0" }); + expect(await pointers(lost.b)).toEqual(before); + expect((await stageReviewCompletionCandidate(database, lost.input, null)).claimOwned).toBe(false); + expect(await spendState(lost.review.id)).toEqual({ status: "reconciled", actual_micros: "7", usage_count: 1, cost: "7" }); + } finally { await blocker.query("ROLLBACK"); blocker.release(); await producer.end(); } + }); + + test("wire-lost COMMIT is reconciled from one durable claim rather than replayed", async () => { + await rows("UPDATE jobs SET run_after='infinity' WHERE status='queued'"); + const [j] = await rows("INSERT INTO jobs(kind,payload) VALUES('review','{}') RETURNING id::text"); + let dropped = 0; + const sockets = new Set>(); + const proxy = createServer((front) => { + const back = connectSocket({ host: "127.0.0.1", port: Number(port) }); + sockets.add(front); sockets.add(back); + front.on("data", (bytes) => back.write(bytes)); + front.on("error", () => back.destroy()); back.on("error", () => front.destroy()); + front.on("close", () => { sockets.delete(front); back.destroy(); }); + back.on("close", () => { sockets.delete(back); front.destroy(); }); + let buffered = Buffer.alloc(0); + back.on("data", (bytes) => { + buffered = Buffer.concat([buffered, Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes)]); + while (buffered.length >= 5) { + const size = buffered.readUInt32BE(1) + 1; + if (buffered.length < size) break; + const frame = buffered.subarray(0, size); buffered = buffered.subarray(size); + if (frame[0] === 67 && frame.subarray(5).toString() === "COMMIT\0") { + dropped++; front.destroy(); back.destroy(); return; + } + front.write(frame); + } + }); + }); + await new Promise((resolve) => proxy.listen(0, "127.0.0.1", resolve)); + const address = proxy.address(); + if (!address || typeof address === "string") throw new Error("fixture proxy unavailable"); + const wire = new Pool({ host: "127.0.0.1", port: address.port, user: "postgres", + database: process.env.POSTIL_RECEIPT_TEST_DATABASE, connectionTimeoutMillis: 2000 }); + wire.on("connect", (client) => client.on("error", () => undefined)); + try { + await expect(claimJob(wire, "wire-fixture", ["review"])).rejects.toThrow(); + expect(dropped).toBe(1); + const [persisted] = await rows("SELECT status,attempts,active_claim_receipt_id::text FROM jobs WHERE id=$1", [j.id]); + expect(persisted.status).toBe("running"); expect(persisted.attempts).toBe(1); + expect((await rows("SELECT count(*)::int AS n FROM job_attempt_receipts WHERE job_id=$1 AND event_kind='claim'", [j.id]))[0].n).toBe(1); + expect(persisted.active_claim_receipt_id).not.toBeNull(); + // No retry is performed after the unknown wire result. + const lost = await losingSpend(); const before = await pointers(lost.b); + await expect(stageReviewCompletionCandidate(drizzle(wire, { schema }), lost.input, null)).rejects.toThrow(); + expect(dropped).toBe(2); + expect(await spendState(lost.review.id)).toEqual({ status: "reconciled", actual_micros: "7", usage_count: 1, cost: "7" }); + expect(await pointers(lost.b)).toEqual(before); + expect((await stageReviewCompletionCandidate(database, lost.input, null)).claimOwned).toBe(false); + expect(await spendState(lost.review.id)).toEqual({ status: "reconciled", actual_micros: "7", usage_count: 1, cost: "7" }); + const terminal = await losingSpend(); const terminalBefore = await pointers(terminal.b); + await rows("UPDATE reviews SET status='stale',finished_at=now() WHERE id=$1", [terminal.review.id]); + await expect(stageReviewCompletionCandidate(drizzle(wire, { schema }), terminal.input, null)).rejects.toThrow(); + expect(dropped).toBe(3); + expect(await spendState(terminal.review.id)).toEqual({ status: "reconciled", actual_micros: "7", usage_count: 1, cost: "7" }); + expect((await stageReviewCompletionCandidate(database, terminal.input, null)).staged).toBe(false); + expect(await spendState(terminal.review.id)).toEqual({ status: "reconciled", actual_micros: "7", usage_count: 1, cost: "7" }); + expect(await pointers(terminal.b)).toEqual(terminalBefore); + } finally { + await wire.end(); for (const socket of sockets) socket.destroy(); + await new Promise((resolve) => proxy.close(() => resolve())); + } + }); + + test("legacy callers stay available; typed cancellation and deferral close the actual claim", async () => { + const a = await start({ installationId: 101, sourceInstallationId: 102, sourceOrgId: 103, + githubRepoId: 104, repoFullName: "fixture/repository", prNumber: 1 }); + expect(await cancelPullRequestPublication(pool, { installationId: 101, sourceInstallationId: 102, + sourceOrgId: 103, githubRepoId: 104, repoFullName: "fixture/repository", prNumber: 1 })).toBe(1); + expect(await completeJob(pool, a)).toBe("lost"); + expect((await rows("SELECT outcome FROM job_attempt_receipts WHERE claim_receipt_id=$1 AND event_kind='queue_closed'", [a.receiptClaim!.receiptId]))[0].outcome).toBe("cancelled"); + const b = await start(); + await deferHostedReviewForRelease(pool, b, randomBytes(20).toString("hex")); + expect((await rows("SELECT outcome FROM job_attempt_receipts WHERE claim_receipt_id=$1 AND event_kind='queue_closed'", [b.receiptClaim!.receiptId]))[0].outcome).toBe("deferred"); + const c = await start(); + const legacy = { id: c.id, lockedBy: c.lockedBy }; + expect(await completeJob(pool, legacy)).toBe("done"); + // The old writer remains callable, and does not manufacture a qualified closure. + expect((await rows("SELECT count(*)::int AS n FROM job_attempt_receipts WHERE claim_receipt_id=$1 AND event_kind='queue_closed'", [c.receiptClaim!.receiptId]))[0].n).toBe(0); + }); + + test("generated actual queue transitions retain one closure per claim across ordinal reuse", async () => { + for (let seed = 1; seed <= 12; seed++) { + let current = await start(); + for (let step = 0; step < 4; step++) { + const operation = (seed * 17 + step * 13) % 4; + const id = current.receiptClaim!.receiptId; + let expected: string; + if (operation === 0) { await continueClaimedJob(pool, current, { fixture: randomUUID() }); expected = "continued"; } + else if (operation === 1) { expect(await retryJobIndefinitely(pool, current, randomBytes(12).toString("base64"))).toBe("retried"); expected = "retry"; } + else if (operation === 2) { expect(await failJob(pool, current, "fixture retry")).toBe("retried"); expected = "retry"; } + else { expect(await requeueJobsOwnedBy(pool, current.lockedBy, "fixture interruption", ["review"], [current.id], [current])).toBe(1); expected = "interrupted"; } + expect(await completeJob(pool, current)).toBe("lost"); + expect(await rows("SELECT outcome FROM job_attempt_receipts WHERE claim_receipt_id=$1 AND event_kind='queue_closed'", [id])).toEqual([{ outcome: expected }]); + await rows("UPDATE jobs SET run_after=now() WHERE id=$1::bigint", [current.receiptClaim!.jobId]); + const next = (await claimJob(pool, current.lockedBy, ["review"]))!; + expect(next.receiptClaim!.receiptId).not.toBe(id); + current = next; + } + expect(await completeJob(pool, current)).toBe("done"); + } + }); + + test("existing unavailable-review path atomically binds its finite failure observation", async () => { + const a = await start(); + const r = await claimPausedHostedReview(database, { ...values(), authorGithubId: null, authorLogin: null, + sinceSha: null, triggerSource: "unknown", triggerContext: { source: "unknown" }, queuedAt: new Date(), startedAt: new Date() }, + { installationId: 1, repoFullName: "fixture/repository", reviewClaim: a }); + expect(r).not.toBeNull(); + expect((await rows("SELECT event_kind,outcome FROM job_attempt_receipts WHERE review_id=$1 ORDER BY receipt_id", [r!.id])) + .map((r) => [r.event_kind, r.outcome])).toEqual([["review_bound", "bound"], ["review_observed", "failed"]]); + expect(await completeJob(pool, a)).toBe("done"); + }); + + test("actual reader admission rejects a different target and stale ACK across a cut", async () => { + const [genesis] = await rows("SELECT source_epoch::text FROM job_attempt_receipts WHERE receipt_id=0"); + let state: ReceiptAdmissionState = { revision: 1, binding: { target: randomUUID(), epoch: genesis.source_epoch, schema: 1 }, prefix: "0" }; + const target = state.binding!.target; + const read = () => readAdmittedReceiptPage(state, async (text, values) => ({ target, rows: await rows(text, values) })); + const proposal = await read(); + expect(proposal.status).toBe("READY"); + if (proposal.status !== "READY") throw new Error("fixture page unavailable"); + expect(acknowledgeReceiptPage(state, proposal.page, "9223372036854775807")).toBeNull(); + expect(acknowledgeReceiptPage(state, proposal.page, proposal.page.through)).toBeNull(); + const reported = recordReceiptPageReported(state, proposal.page, proposal.page.rows[0]!.receipt_id as string)!; + const accepted = acknowledgeReceiptPage(reported, proposal.page, reported.reportedThrough!)!; + expect(accepted.prefix).not.toBe("0"); + state = closeReceiptAdmission(accepted); + expect(state.prefix).toBe(accepted.prefix); + expect(acknowledgeReceiptPage(state, proposal.page, proposal.page.through)).toBeNull(); + expect(admitReceiptSource(state, proposal.page.binding, state.revision, "0")).toBeNull(); + const distinct = { ...proposal.page.binding, target: randomUUID() }; + state = admitReceiptSource(state, distinct, state.revision, "0")!; + expect((await read()).status).toBe("UNKNOWN"); + const admitted = await readAdmittedReceiptPage(state, async (text, values) => ({ target: distinct.target, rows: await rows(text, values) })); + expect(admitted.status).toBe("READY"); + + }); + test("generated admission/read/delivery/ACK histories never cross a binding or ACK an unreported row", async () => { + const [control] = await rows("SELECT source_epoch::text FROM job_attempt_receipts WHERE receipt_id=0"); + for (let seed = 1; seed <= 16; seed++) { + let random = seed; + const next = () => { random ^= random << 13; random ^= random >>> 17; random ^= random << 5; return random >>> 0; }; + let state: ReceiptAdmissionState = { revision: 1, binding: { target: randomUUID(), epoch: control.source_epoch, schema: 1 }, prefix: "0" }; + const pages: Array>, { status: "READY" }>["page"]> = []; + const delivered = new Map(); + for (let step = 0; step < 24; step++) { + const action = next() % 5; + if (action === 0 && state.binding) { + const target = state.binding.target; + const page = await readAdmittedReceiptPage(state, async (text, values) => ({ target, rows: await rows(text, values) })); + expect(page.status).toBe("READY"); + if (page.status === "READY") pages.push(page.page); + } else if (action === 1 && pages.length) { + const page = pages[next() % pages.length]!; + const report = recordReceiptPageReported(state, page, page.through); + if (report) { + expect(receiptPageAdmitted(state, page)).toBe(true); + delivered.set(`${page.binding.target}:${page.revision}:${page.after}`, page.through); + state = report; + } + } else if (action === 2 && pages.length) { + const page = pages[next() % pages.length]!; + const acknowledged = acknowledgeReceiptPage(state, page, page.through); + if (acknowledged) { + expect(delivered.get(`${page.binding.target}:${page.revision}:${page.after}`)).toBe(page.through); + expect(acknowledged.binding).toEqual(state.binding); + expect(BigInt(acknowledged.prefix) >= BigInt(state.prefix)).toBe(true); + state = acknowledged; + } + } else if (action === 3) { + const prefix = state.prefix; + state = closeReceiptAdmission(state); + expect(state.prefix).toBe(prefix); + for (const page of pages) expect(acknowledgeReceiptPage(state, page, page.through)).toBeNull(); + } else if (!state.binding) { + const binding = { target: randomUUID(), epoch: control.source_epoch, schema: 1 }; + const revision = state.revision; + state = admitReceiptSource(state, binding, revision, "0")!; + expect(admitReceiptSource(state, binding, revision, "0")).toEqual(state); + } + } + } + }); + +}); diff --git a/tests/worker-runner.test.ts b/tests/worker-runner.test.ts index 082c669d..4eb74672 100644 --- a/tests/worker-runner.test.ts +++ b/tests/worker-runner.test.ts @@ -870,7 +870,9 @@ describe("drainQueueOnce", () => { }); test("processes no more than maxJobs", async () => { - jobs.push(reviewJob(1), reviewJob(2)); + const first = reviewJob(1); + first.receiptClaim = Object.freeze({ jobId: "1", receiptId: String(Date.now()) }); + jobs.push(first, reviewJob(2)); const drained = await drainQueueOnce("test-drain", { maxJobs: 1, @@ -883,7 +885,7 @@ describe("drainQueueOnce", () => { expect(reviewTiming).toEqual({ queuedAt: new Date("2026-07-10T12:00:00.000Z"), startedAt: new Date("2026-07-10T12:00:05.000Z"), - lease: expect.objectContaining({ id: 1, lockedBy: "test" }), + lease: expect.objectContaining({ id: 1, lockedBy: "test", receiptClaim: first.receiptClaim }), }); expect(reviewProcessGroup).toBe("web"); }); From 0babc83be5d97f4ece33d488f88a1870c06d04e1 Mon Sep 17 00:00:00 2001 From: Postil Maintainer Date: Sun, 18 Oct 2026 21:47:11 +0000 Subject: [PATCH 3/4] Persist review attempt receipts and recovery ownership Link queue claims, review transitions and recovery work with durable attempt receipts. Reconcile trusted usage to its originating review on terminal and lost-ownership exits, and repair checks from stored state. Keep nullable links, legacy recovery payloads and job pointers compatible. Schema additions precede new writers. Mixed-version writer completeness and production rollout remain unqualified. Focused PostgreSQL, unit, workflow and TypeScript checks pass locally. The canonical local review has no verdict; these are fallback checks. --- src/lib/hosted-review-pause.ts | 4 +- src/lib/job-attempt-receipts.ts | 19 +++--- src/lib/queue.ts | 6 +- src/lib/release-job-rollout.ts | 3 +- src/lib/review-completion.ts | 6 +- tests/job-attempt-receipts.test.ts | 99 +++++++++++++++++++++++++++--- 6 files changed, 114 insertions(+), 23 deletions(-) diff --git a/src/lib/hosted-review-pause.ts b/src/lib/hosted-review-pause.ts index 0fe96443..c2377863 100644 --- a/src/lib/hosted-review-pause.ts +++ b/src/lib/hosted-review-pause.ts @@ -157,9 +157,9 @@ export async function claimPausedHostedReview( } if (lease?.receiptClaim) { events.push(claimEvent(lease.receiptClaim.jobId, Number(job!.attempts), lease.receiptClaim.receiptId, - "review_bound", "bound", String(review.id), review.publicId)); + "review_bound", "bound", String(review.id), review.publicId, String(job!.kind))); events.push(claimEvent(lease.receiptClaim.jobId, Number(job!.attempts), lease.receiptClaim.receiptId, - "review_observed", "failed", String(review.id), review.publicId)); + "review_observed", "failed", String(review.id), review.publicId, String(job!.kind))); } await appendPreparedReceiptBatch(client, events); return { ...review, advisoryCheckExternalId, gateCheckExternalId }; diff --git a/src/lib/job-attempt-receipts.ts b/src/lib/job-attempt-receipts.ts index 6b3a401e..4db325da 100644 --- a/src/lib/job-attempt-receipts.ts +++ b/src/lib/job-attempt-receipts.ts @@ -188,8 +188,10 @@ export async function lockReceiptClaim(client: ReceiptClient, lease: ReviewClaim export function claimEvent(jobId: string, ordinal: number, claimId: string | null, eventKind: PreparedReceipt["eventKind"], outcome: PreparedReceipt["outcome"], - reviewId: string | null = null, reviewPublicId: string | null = null): PreparedReceipt { - return { jobId, ordinal, jobKind: "review", claimId, reviewId, reviewPublicId, + reviewId: string | null = null, reviewPublicId: string | null = null, + jobKind: string = "review"): PreparedReceipt { + if (jobKind !== "review" && jobKind !== "review-feedback") throw new Error("invalid receipt job kind"); + return { jobId, ordinal, jobKind, claimId, reviewId, reviewPublicId, eventKind, outcome, phase: null, category: null, recovered: null, recovery: null }; } @@ -213,7 +215,8 @@ export async function transitionReceiptClaim( await client.query("UPDATE jobs SET active_claim_receipt_id = NULL WHERE id = $1::bigint", [lease.receiptClaim!.jobId]); await appendPreparedReceiptBatch(client, [claimEvent(lease.receiptClaim!.jobId, - Number(row.attempts), lease.receiptClaim!.receiptId, "queue_closed", outcome(result))]); + Number(row.attempts), lease.receiptClaim!.receiptId, "queue_closed", outcome(result), + null, null, String(row.kind))]); } if (ownsTransaction) await client.query("COMMIT"); return result; @@ -227,7 +230,7 @@ export async function transitionReceiptClaim( export async function prepareReviewObservation(client: ReceiptClient, reviewId: number, outcome: PreparedReceipt["outcome"], lease?: ReviewClaimLease): Promise { const result = await client.query(`SELECT b.claim_receipt_id::text, b.job_id::text, - b.ordinal, b.review_id::text, b.review_public_id::text + b.ordinal, b.job_kind, b.review_id::text, b.review_public_id::text FROM job_attempt_receipts b JOIN reviews r ON r.id = b.review_id WHERE b.event_kind = 'review_bound' AND r.id = $1::bigint AND b.claim_receipt_id = COALESCE($2::bigint, r.origin_claim_receipt_id) @@ -237,7 +240,8 @@ export async function prepareReviewObservation(client: ReceiptClient, reviewId: AND o.review_id = r.id AND o.outcome = $3)`, [String(reviewId), lease?.receiptClaim?.receiptId ?? null, outcome]); return result.rows.map((r) => claimEvent(String(r.job_id), Number(r.ordinal), - String(r.claim_receipt_id), "review_observed", outcome, String(r.review_id), String(r.review_public_id))); + String(r.claim_receipt_id), "review_observed", outcome, String(r.review_id), String(r.review_public_id), + String(r.job_kind))); } export interface ReceiptSourceBinding { @@ -365,7 +369,7 @@ export async function readAdmittedReceiptPage( export async function prepareClosedJobReceipts(client: ReceiptClient, jobIds: readonly string[], outcome?: PreparedReceipt["outcome"]): Promise { if (!jobIds.length) return []; - const result = await client.query(`SELECT j.id::text, j.status, c.ordinal, c.receipt_id::text + const result = await client.query(`SELECT j.id::text, j.status, c.ordinal, c.job_kind, c.receipt_id::text FROM jobs j JOIN job_attempt_receipts c ON c.receipt_id = j.active_claim_receipt_id WHERE j.id = ANY($1::bigint[]) AND j.status <> 'running' AND c.event_kind = 'claim' AND NOT EXISTS (SELECT 1 FROM job_attempt_receipts x @@ -374,5 +378,6 @@ export async function prepareClosedJobReceipts(client: ReceiptClient, if (result.rows.length) await client.query(`UPDATE jobs SET active_claim_receipt_id = NULL WHERE id = ANY($1::bigint[])`, [result.rows.map((r) => r.id)]); return result.rows.map((r) => claimEvent(String(r.id), Number(r.ordinal), String(r.receipt_id), - "queue_closed", outcome ?? (r.status === "queued" ? "retry" : r.status === "failed" ? "failed" : "cancelled"))); + "queue_closed", outcome ?? (r.status === "queued" ? "retry" : r.status === "failed" ? "failed" : "cancelled"), + null, null, String(r.job_kind))); } diff --git a/src/lib/queue.ts b/src/lib/queue.ts index 78aaac32..7998e586 100644 --- a/src/lib/queue.ts +++ b/src/lib/queue.ts @@ -240,7 +240,7 @@ export async function cancelPullRequestPublication( ): Promise { const run = async (client: Pick, qualified: boolean) => { if (qualified) await client.query(`SELECT id FROM jobs - WHERE status IN ('queued', 'running') AND kind = 'review' + WHERE status IN ('queued', 'running') AND kind IN ('review', 'review-feedback') AND payload->>'installationId' = $1 AND payload->>'sourceInstallationId' = $2 AND payload->>'sourceOrgId' = $3 AND payload->>'githubRepoId' = $4 AND lower(payload->>'repoFullName') = lower($5) @@ -1124,7 +1124,7 @@ export async function claimNextJob( ) { throw new Error("claimed job returned incomplete candidate data"); } - const receipts = row.kind === "review" && (await client.query( + const receipts = (row.kind === "review" || row.kind === FEEDBACK_REVIEW_JOB_KIND) && (await client.query( "SELECT to_regclass('job_attempt_receipts') IS NOT NULL AS available", )).rows[0]?.available === true; if (receipts) { @@ -1143,7 +1143,7 @@ export async function claimNextJob( let receiptClaim: ReceiptClaim | undefined; if (receipts) { const [receiptId] = await appendPreparedReceiptBatch(client, - [claimEvent(row.id, row.attempts + 1, null, "claim", "started")]); + [claimEvent(row.id, row.attempts + 1, null, "claim", "started", null, null, row.kind)]); await client.query("UPDATE jobs SET active_claim_receipt_id = $2::bigint WHERE id = $1::bigint", [row.id, receiptId]); receiptClaim = Object.freeze({ jobId: row.id, receiptId: receiptId! }); } diff --git a/src/lib/release-job-rollout.ts b/src/lib/release-job-rollout.ts index afefe7ee..0228e86c 100644 --- a/src/lib/release-job-rollout.ts +++ b/src/lib/release-job-rollout.ts @@ -1714,7 +1714,8 @@ async function deferHostedJobForRelease( if (job.receiptClaim) { await client.query("UPDATE jobs SET active_claim_receipt_id = NULL WHERE id = $1::bigint", [job.receiptClaim.jobId]); await appendPreparedReceiptBatch(client, [claimEvent(job.receiptClaim.jobId, - Number(claim!.attempts), job.receiptClaim.receiptId, "queue_closed", "deferred")]); + Number(claim!.attempts), job.receiptClaim.receiptId, "queue_closed", "deferred", + null, null, String(claim!.kind))]); } await client.query("COMMIT"); return activated ? "released" : "deferred"; diff --git a/src/lib/review-completion.ts b/src/lib/review-completion.ts index ef104448..d4aebd98 100644 --- a/src/lib/review-completion.ts +++ b/src/lib/review-completion.ts @@ -286,7 +286,7 @@ export async function stageReviewCompletionCandidate( for (const incident of gateTruth.envelope.modelIncidents ?? []) { events.push({ ...claimEvent(lease.receiptClaim.jobId, events[0]!.ordinal, lease.receiptClaim.receiptId, "incident", "observed", String(input.reviewId), - events[0]!.reviewPublicId), phase: incident.phase, category: incident.category, + events[0]!.reviewPublicId, events[0]!.jobKind), phase: incident.phase, category: incident.category, recovered: incident.recovered, recovery: incident.recovery ?? null }); } } @@ -498,7 +498,7 @@ export async function insertReviewForClaim(db: Database, if (!review) throw new Error("review insert returned no row"); await appendPreparedReceiptBatch(client, [claimEvent(lease.receiptClaim!.jobId, Number(job.attempts), lease.receiptClaim!.receiptId, "review_bound", "bound", - review.receiptReviewId, review.publicId)]); + review.receiptReviewId, review.publicId, String(job.kind))]); return review; }); } @@ -523,7 +523,7 @@ export async function bindStagedReviewClaim(db: Database, reviewId: number, leas if (!existing.rows.length) { await appendPreparedReceiptBatch(client, [claimEvent(lease.receiptClaim!.jobId, Number(job.attempts), lease.receiptClaim!.receiptId, "review_bound", "bound", - String(reviewId), String(review.public_id))]); + String(reviewId), String(review.public_id), String(job.kind))]); } return true; }); diff --git a/tests/job-attempt-receipts.test.ts b/tests/job-attempt-receipts.test.ts index 8f69d52d..721855e8 100644 --- a/tests/job-attempt-receipts.test.ts +++ b/tests/job-attempt-receipts.test.ts @@ -523,10 +523,10 @@ describe.skipIf(!port)("production receipt ownership APIs on PostgreSQL", () => } }); afterAll(async () => { await pool?.end(); }); - async function start(payload: Record = {}): Promise { + async function start(payload: Record = {}, kind = "review"): Promise { await rows("UPDATE jobs SET run_after='infinity' WHERE status='queued'"); - const [j] = await rows("INSERT INTO jobs(kind,payload,max_attempts) VALUES('review',$1,5) RETURNING id::text", [payload]); - const c = await claimJob(pool, "reused-worker", ["review"]); + const [j] = await rows("INSERT INTO jobs(kind,payload,max_attempts) VALUES($2,$1,5) RETURNING id::text", [payload, kind]); + const c = await claimJob(pool, "reused-worker", [kind]); expect(c?.receiptClaim?.jobId).toBe(j.id); expect(Object.isFrozen(c!.receiptClaim)).toBe(true); return c!; @@ -546,8 +546,8 @@ describe.skipIf(!port)("production receipt ownership APIs on PostgreSQL", () => publicationReceipt: { version: 1 as const, receiptId: randomUUID(), findings: [] } }; } async function replace(c: ClaimedJob) { - expect(await requeueJobsOwnedBy(pool, c.lockedBy, "fixture interruption", ["review"], [c.id], [c])).toBe(1); - const next = await claimJob(pool, c.lockedBy, ["review"]); + expect(await requeueJobsOwnedBy(pool, c.lockedBy, "fixture interruption", [c.kind], [c.id], [c])).toBe(1); + const next = await claimJob(pool, c.lockedBy, [c.kind]); expect(next!.attempts).toBe(c.attempts); expect(next!.receiptClaim!.receiptId).not.toBe(c.receiptClaim!.receiptId); return next!; @@ -555,8 +555,8 @@ describe.skipIf(!port)("production receipt ownership APIs on PostgreSQL", () => const pointers = (c: ClaimedJob) => rows(`SELECT active_claim_receipt_id::text, staged_review_id::text, payload, status, attempts, locked_by, locked_at FROM jobs WHERE id=$1::bigint`, [c.receiptClaim!.jobId]); - async function losingSpend(stageFirst = false) { - const a = await start(); const review = (await insertReviewForClaim(database, values(), a))!; + async function losingSpend(stageFirst = false, kind = "review") { + const a = await start({}, kind); const review = (await insertReviewForClaim(database, values(), a))!; const [org] = await rows("INSERT INTO organizations(name,slug) VALUES($1,$2) RETURNING id", ["Receipt fixture", randomUUID()]); const [reservation] = await rows(`INSERT INTO hosted_usage_reservations(org_id,review_id,reserved_micros,expires_at) VALUES($1,$2,1000,now()+interval '1 hour') RETURNING id`, [org.id, review.id]); @@ -1058,6 +1058,91 @@ describe.skipIf(!port)("production receipt ownership APIs on PostgreSQL", () => .toEqual([{ outcome: "coalesced" }]); }); + test("both review kinds fence generated same-label reclaims and preserve receipt kinds", async () => { + for (const kind of ["review", "review-feedback"]) { + for (let seed = 0; seed < 6; seed++) { + let a = await start({}, kind); + for (let step = 0; step < 4; step++) { + const operation = (seed * 7 + step * 3) % 4; + if (operation === 0) await continueClaimedJob(pool, a, { fixture: randomUUID() }); + else if (operation === 1) await retryJobIndefinitely(pool, a, randomUUID()); + else if (operation === 2) await failJob(pool, a, randomUUID()); + else await requeueJobsOwnedBy(pool, a.lockedBy, randomUUID(), [kind], [a.id], [a]); + await rows("UPDATE jobs SET run_after=now() WHERE id=$1", [a.id]); + const b = (await claimJob(pool, a.lockedBy, [kind]))!; + expect(b.receiptClaim!.receiptId).not.toBe(a.receiptClaim!.receiptId); + expect(b.lockedBy).toBe(a.lockedBy); + const before = await pointers(b); + expect(await completeJob(pool, a)).toBe("lost"); + expect(await pointers(b)).toEqual(before); + expect(await rows(`SELECT job_kind FROM job_attempt_receipts WHERE claim_receipt_id=$1 + AND event_kind='queue_closed'`, [a.receiptClaim!.receiptId])).toEqual([{ job_kind: kind }]); + a = b; + } + expect(await completeJob(pool, a)).toBe("done"); + expect(await rows("SELECT DISTINCT job_kind FROM job_attempt_receipts WHERE job_id=$1", [a.id])) + .toEqual([{ job_kind: kind }]); + } + } + }); + + test("both review kinds preserve losing-stage rollback, own usage once and genuine recovery", async () => { + for (const kind of ["review", "review-feedback"]) { + for (const first of [false, true]) { + const x = await losingSpend(first, kind); const before = await pointers(x.b); + if (!first) x.input.envelope.modelIncidents = [{ phase: "review", category: "providerError", recovered: true, recovery: "fallback" }]; + await rows(`CREATE FUNCTION fixture_reject_kind_stage() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN IF NEW.event_kind='review_observed' THEN RAISE EXCEPTION 'fixture kind stage rejection'; END IF; + RETURN NEW; END $$`); + await rows("CREATE TRIGGER fixture_reject_kind_stage BEFORE INSERT ON job_attempt_receipts FOR EACH ROW EXECUTE FUNCTION fixture_reject_kind_stage()"); + try { + if (!first) { + let rejection: unknown; + try { await stageReviewCompletionCandidate(database, x.input, null); } catch (error) { rejection = error; } + expect(String((rejection as { cause?: Error })?.cause)).toContain("fixture kind stage rejection"); + expect((await spendState(x.review.id)).usage_count).toBe(0); + expect((await rows("SELECT envelope FROM reviews WHERE id=$1", [x.review.id]))[0].envelope).toBeNull(); + expect(await pointers(x.b)).toEqual(before); + } + } finally { + await rows("DROP TRIGGER fixture_reject_kind_stage ON job_attempt_receipts"); + await rows("DROP FUNCTION fixture_reject_kind_stage()"); + } + const results = await Promise.all([0, 1].map(() => stageReviewCompletionCandidate(database, x.input, null))); + expect(results.every(r => r.staged && r.claimOwned === false)).toBe(true); + expect(await completeJob(pool, x.a)).toBe("lost"); + expect(await pointers(x.b)).toEqual(before); + if (first) { + expect((await finalizeStagedReviewCompletionWithGateMode(database, { reviewId: x.review.id, + reviewClaim: x.b, usage: x.spend.usage, hostedUsageReservationId: x.spend.reservationId, + usageAccountingComplete: true }, null)).completed).toBe(true); + } + expect(await spendState(x.review.id)).toEqual({ status: "reconciled", actual_micros: "7", usage_count: 1, cost: "7" }); + expect(await rows("SELECT DISTINCT job_kind FROM job_attempt_receipts WHERE job_id=$1", [x.b.id])) + .toEqual([{ job_kind: kind }]); + expect(await completeJob(pool, x.b)).toBe("done"); + } + const c = await start({}, kind); + await deferHostedReviewForRelease(pool, c, randomBytes(20).toString("hex")); + expect(await rows("SELECT job_kind,outcome FROM job_attempt_receipts WHERE claim_receipt_id=$1 AND event_kind='queue_closed'", [c.receiptClaim!.receiptId])) + .toEqual([{ job_kind: kind, outcome: "deferred" }]); + const d = await start({ installationId: 101, sourceInstallationId: 102, sourceOrgId: 103, + githubRepoId: 104, repoFullName: "fixture/repository", prNumber: 1 }, kind); + expect(await cancelPullRequestPublication(pool, { installationId: 101, sourceInstallationId: 102, + sourceOrgId: 103, githubRepoId: 104, repoFullName: "fixture/repository", prNumber: 1 })).toBe(1); + expect(await rows("SELECT job_kind,outcome FROM job_attempt_receipts WHERE claim_receipt_id=$1 AND event_kind='queue_closed'", [d.receiptClaim!.receiptId])) + .toEqual([{ job_kind: kind, outcome: "cancelled" }]); + const unavailable = await start({}, kind); + const review = await claimPausedHostedReview(database, { ...values(), authorGithubId: null, authorLogin: null, + sinceSha: null, triggerSource: "unknown", triggerContext: { source: "unknown" }, queuedAt: new Date(), startedAt: new Date() }, + { installationId: 1, repoFullName: "fixture/repository", reviewClaim: unavailable }); + expect(review).not.toBeNull(); + expect(await rows("SELECT DISTINCT job_kind FROM job_attempt_receipts WHERE review_id=$1", [review!.id])) + .toEqual([{ job_kind: kind }]); + expect(await completeJob(pool, unavailable)).toBe("done"); + } + }); + test("existing unavailable-review path atomically binds its finite failure observation", async () => { const a = await start(); const r = await claimPausedHostedReview(database, { ...values(), authorGithubId: null, authorLogin: null, From 3cd9693f8ff421105ced04bdb258c6c994255bde Mon Sep 17 00:00:00 2001 From: Postil Maintainer Date: Sun, 18 Oct 2026 22:18:31 +0000 Subject: [PATCH 4/4] Persist review attempt receipts and recovery ownership Link queue claims, review transitions and recovery work with durable attempt receipts. Reconcile trusted usage to its originating review on terminal and lost-ownership exits, and repair checks from stored state. Keep nullable links, legacy recovery payloads and job pointers compatible. Schema additions precede new writers. Mixed-version writer completeness and production rollout remain unqualified. Focused PostgreSQL, unit, workflow and TypeScript checks pass locally. The canonical local review has no verdict; these are fallback checks. --- tests/private-worker-gates.test.ts | 77 +++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/tests/private-worker-gates.test.ts b/tests/private-worker-gates.test.ts index e8d4f527..06ebeedd 100644 --- a/tests/private-worker-gates.test.ts +++ b/tests/private-worker-gates.test.ts @@ -17,9 +17,36 @@ describe("private repository worker defense in depth", () => { expect(authorLookup).toBeGreaterThan(gate); expect(source).toContain("authorGithubId: authorGithubId"); expect(source).toContain("authorLogin: authorLogin"); + const reviewInsertion = source.indexOf("await insertReviewForClaim", gate); + expect(reviewInsertion).toBeGreaterThan(gate); + expect( + source.slice(reviewInsertion, source.indexOf("const reviewId", reviewInsertion)), + ).toContain('db, { ...reviewValues, status: "running" }, timing.lease'); + const completion = readFileSync("src/lib/review-completion.ts", "utf8"); + const insertion = completion.slice( + completion.indexOf("export async function insertReviewForClaim"), + completion.indexOf("export async function bindStagedReviewClaim"), + ); + expect(insertion).toContain("if (!lease?.receiptClaim)"); + expect(insertion).toContain("db.insert(schema.reviews).values(values)"); + const transaction = insertion.indexOf("db.transaction"); + const ownership = insertion.indexOf("if (!job?.owned) return undefined;"); + const fencedInsert = insertion.indexOf("tx.insert(schema.reviews)"); + expect(transaction).toBeGreaterThan(0); + expect(ownership).toBeGreaterThan(transaction); + expect(fencedInsert).toBeGreaterThan(ownership); + expect(insertion.slice(transaction, ownership)).toContain( + "await lockReceiptClaim(client, lease)", + ); + expect(insertion.slice(fencedInsert)).toContain( + "originClaimReceiptId: lease.receiptClaim!.receiptId", + ); + expect(insertion.indexOf("await appendPreparedReceiptBatch")).toBeGreaterThan( + fencedInsert, + ); for (const sideEffect of [ - "insert(schema.reviews)", - "await getInstallationToken", + "await insertReviewForClaim", + "getInstallationToken(", "await createCheckRun", "await materializeRepoConfig", "await runCli", @@ -49,10 +76,10 @@ describe("private repository worker defense in depth", () => { source.slice(providerFailure, source.indexOf("await runCli", gate)), ).toContain("configured provider mode does not match"); expect(source.indexOf("fetchRepositorySummary", gate)).toBeLessThan( - source.indexOf("insert(schema.reviews)", gate), + reviewInsertion, ); expect(authorLookup).toBeLessThan( - source.indexOf("insert(schema.reviews)", gate), + reviewInsertion, ); expect(authorLookup).toBeLessThan( source.indexOf("await reserveHostedReviewSpend", gate), @@ -503,12 +530,25 @@ describe("private repository worker defense in depth", () => { ); expect(stagedBranch).not.toContain('kind: "check-run-cleanup"'); expect(stagedBranch).toContain("ReviewPublicationReconciliationError"); - const shutdownBranch = failureBody.slice( - failureBody.indexOf("if (err instanceof WorkerShutdownError) {"), - failureBody.indexOf("const publicationIncomplete"), + const shutdownStart = failureBody.indexOf( + "if (err instanceof WorkerShutdownError) {", ); + const failureTransition = failureBody.indexOf( + "publicationIncomplete = err instanceof CheckRunPublicationError;", + shutdownStart, + ); + expect(shutdownStart).toBeGreaterThan(0); + expect(failureTransition).toBeGreaterThan(shutdownStart); + const shutdownBranch = failureBody.slice(shutdownStart, failureTransition); expect(shutdownBranch).not.toContain('kind: "check-run-cleanup"'); expect(shutdownBranch).toContain('status: "stale"'); + expect(shutdownBranch.indexOf("await reconcileInterruptedSpend()")).toBeGreaterThan( + 0, + ); + expect(shutdownBranch.indexOf("if (!spendError)")).toBeGreaterThan( + shutdownBranch.indexOf("await reconcileInterruptedSpend()"), + ); + expect(shutdownBranch).toContain("throw err;"); expect(failureBody).toContain("db.transaction"); expect(failureBody).toContain('kind: "check-run-cleanup"'); expect(failureBody).toContain('intent: "fail"'); @@ -565,16 +605,31 @@ describe("private repository worker defense in depth", () => { source.indexOf("export async function runReviewJob"), ), ); - const failureUpdate = source.indexOf("const failedRows", catchStart); - const supersessionRace = source.slice(catchStart, failureUpdate); + const stagedCompletion = source.indexOf("if (completionStaged)", catchStart); + expect(catchStart).toBeGreaterThan(0); + expect(stagedCompletion).toBeGreaterThan(catchStart); + const supersessionRace = source.slice(catchStart, stagedCompletion); expect(supersessionRace).toContain( - "err instanceof CheckRunPublicationError && receiptUsageForRace", - ); + "err instanceof CheckRunPublicationError && trustedReceiptUsage", + ); + const ingestion = source.indexOf("const ingested = ingestCompletedHostedReview("); + const usage = source.indexOf( + "const receiptUsage = reviewUsageFromEnvelope(ingested.envelope", + ingestion, + ); + const trustedUsage = source.indexOf("trustedReceiptUsage = receiptUsage;", usage); + expect(ingestion).toBeGreaterThan(0); + expect(usage).toBeGreaterThan(ingestion); + expect(trustedUsage).toBeGreaterThan(usage); + expect(trustedUsage).toBeLessThan(catchStart); expect(supersessionRace).toContain('terminal?.status === "stale"'); expect(supersessionRace).toContain( "await reconcileHostedReviewSpendFromReceipt", ); + expect(supersessionRace).toContain("reservationId: hostedUsageReservationId"); + expect(supersessionRace).toContain("usage: trustedReceiptUsage"); + expect(supersessionRace).toContain("usageAccountingComplete: trustedUsageAccountingComplete"); expect(supersessionRace).toContain("await neutralizeSupersededCheckRuns"); expect(supersessionRace).toContain("return;"); });