feat: prepare ARR for controlled public deployment
This commit is contained in:
329
database/010_mcp_result_ingestion.sql
Normal file
329
database/010_mcp_result_ingestion.sql
Normal file
@@ -0,0 +1,329 @@
|
||||
-- ARR MCP direct structured-result ingestion ledger.
|
||||
-- PostgreSQL 15+. Additive only; migrations 008 and 009 remain immutable.
|
||||
|
||||
BEGIN;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR MCP result ingestion is allowed only in booking_test';
|
||||
END IF;
|
||||
IF to_regclass('ingestion.processing_runs') IS NULL
|
||||
OR to_regclass('ingestion.processing_attempts') IS NULL
|
||||
OR to_regclass('finance.daily_versions') IS NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR MVP v1 migration 008 must be applied first';
|
||||
END IF;
|
||||
IF to_regclass('ingestion.result_submission_grants') IS NOT NULL
|
||||
OR to_regclass('ingestion.result_submissions') IS NOT NULL THEN
|
||||
RAISE EXCEPTION
|
||||
'ARR MCP result ingestion migration 010 is already applied';
|
||||
END IF;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Existing rows retain the artifact callback contract. New MCP runs opt in
|
||||
-- explicitly and may finish without Agent-produced result objects in OSS.
|
||||
ALTER TABLE ingestion.processing_runs
|
||||
ADD COLUMN result_delivery_mode text NOT NULL
|
||||
DEFAULT 'artifact_callback';
|
||||
|
||||
ALTER TABLE ingestion.processing_runs
|
||||
ADD CONSTRAINT processing_runs_result_delivery_mode_check CHECK (
|
||||
result_delivery_mode IN ('artifact_callback', 'direct_mcp')
|
||||
),
|
||||
ADD CONSTRAINT processing_runs_result_delivery_pipeline_check CHECK (
|
||||
pipeline_type = 'opera_daily'
|
||||
OR result_delivery_mode = 'artifact_callback'
|
||||
);
|
||||
|
||||
ALTER TABLE ingestion.processing_runs
|
||||
DROP CONSTRAINT processing_runs_terminal_shape;
|
||||
|
||||
ALTER TABLE ingestion.processing_runs
|
||||
ADD CONSTRAINT processing_runs_terminal_shape CHECK (
|
||||
(
|
||||
run_status = 'accepted'
|
||||
AND failure_code IS NULL
|
||||
AND validated_at IS NOT NULL
|
||||
AND finished_at IS NOT NULL
|
||||
AND (
|
||||
pipeline_type <> 'opera_daily'
|
||||
OR (
|
||||
business_date IS NOT NULL
|
||||
AND delivered_processor_version IS NOT NULL
|
||||
AND delivered_rule_set_sha256 IS NOT NULL
|
||||
AND result_schema_version IS NOT NULL
|
||||
AND delivery_sha256 IS NOT NULL
|
||||
AND (
|
||||
(
|
||||
result_delivery_mode = 'artifact_callback'
|
||||
AND result_artifact_id IS NOT NULL
|
||||
)
|
||||
OR (
|
||||
result_delivery_mode = 'direct_mcp'
|
||||
AND result_artifact_id IS NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
OR (
|
||||
run_status IN ('rejected', 'failed')
|
||||
AND failure_code IS NOT NULL
|
||||
AND finished_at IS NOT NULL
|
||||
)
|
||||
OR (
|
||||
run_status = 'cancelled'
|
||||
AND finished_at IS NOT NULL
|
||||
)
|
||||
OR run_status IN ('received', 'queued', 'running', 'validating')
|
||||
);
|
||||
|
||||
ALTER TABLE finance.daily_versions
|
||||
ADD COLUMN result_delivery_mode text NOT NULL
|
||||
DEFAULT 'artifact_callback';
|
||||
|
||||
ALTER TABLE finance.daily_versions
|
||||
ADD CONSTRAINT daily_versions_result_delivery_mode_check CHECK (
|
||||
result_delivery_mode IN ('artifact_callback', 'direct_mcp')
|
||||
),
|
||||
ADD CONSTRAINT daily_versions_result_delivery_shape CHECK (
|
||||
result_delivery_mode <> 'direct_mcp'
|
||||
OR (
|
||||
daily_report_artifact_id IS NULL
|
||||
AND result_json_artifact_id IS NULL
|
||||
AND structured_result_artifact_id IS NULL
|
||||
AND exception_report_artifact_id IS NULL
|
||||
)
|
||||
);
|
||||
|
||||
ALTER TABLE finance.daily_versions
|
||||
DROP CONSTRAINT daily_versions_status_shape;
|
||||
|
||||
ALTER TABLE finance.daily_versions
|
||||
ADD CONSTRAINT daily_versions_status_shape CHECK (
|
||||
(
|
||||
version_status IN ('validated', 'active', 'superseded')
|
||||
AND business_date IS NOT NULL
|
||||
AND exception_report_artifact_id IS NULL
|
||||
AND failure_code IS NULL
|
||||
AND validated_at IS NOT NULL
|
||||
AND (
|
||||
(
|
||||
result_delivery_mode = 'artifact_callback'
|
||||
AND daily_report_artifact_id IS NOT NULL
|
||||
AND result_json_artifact_id IS NOT NULL
|
||||
AND structured_result_artifact_id IS NOT NULL
|
||||
)
|
||||
OR (
|
||||
result_delivery_mode = 'direct_mcp'
|
||||
AND daily_report_artifact_id IS NULL
|
||||
AND result_json_artifact_id IS NULL
|
||||
AND structured_result_artifact_id IS NULL
|
||||
)
|
||||
)
|
||||
)
|
||||
OR (
|
||||
version_status = 'rejected'
|
||||
AND failure_code IS NOT NULL
|
||||
)
|
||||
);
|
||||
|
||||
CREATE TABLE ingestion.result_submission_grants (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
grant_sha256 character(64) NOT NULL UNIQUE CHECK (
|
||||
grant_sha256 ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
processing_run_id bigint NOT NULL
|
||||
REFERENCES ingestion.processing_runs(id),
|
||||
attempt_id bigint NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
consumed_at timestamptz,
|
||||
revoked_at timestamptz,
|
||||
CONSTRAINT result_submission_grants_attempt_unique
|
||||
UNIQUE (attempt_id),
|
||||
CONSTRAINT result_submission_grants_id_run_attempt_unique
|
||||
UNIQUE (id, processing_run_id, attempt_id),
|
||||
CONSTRAINT result_submission_grants_attempt_run_fk
|
||||
FOREIGN KEY (attempt_id, processing_run_id)
|
||||
REFERENCES ingestion.processing_attempts(id, processing_run_id),
|
||||
CONSTRAINT result_submission_grants_lifetime CHECK (
|
||||
expires_at > created_at
|
||||
AND expires_at <= created_at + interval '30 minutes'
|
||||
),
|
||||
CONSTRAINT result_submission_grants_usage_shape CHECK (
|
||||
NOT (consumed_at IS NOT NULL AND revoked_at IS NOT NULL)
|
||||
AND (
|
||||
consumed_at IS NULL
|
||||
OR consumed_at BETWEEN created_at AND expires_at
|
||||
)
|
||||
AND (revoked_at IS NULL OR revoked_at >= created_at)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX result_submission_grants_expiry_idx
|
||||
ON ingestion.result_submission_grants (expires_at)
|
||||
WHERE consumed_at IS NULL AND revoked_at IS NULL;
|
||||
|
||||
CREATE TABLE ingestion.result_submissions (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
submission_key text NOT NULL UNIQUE CHECK (
|
||||
submission_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
||||
),
|
||||
grant_id bigint NOT NULL UNIQUE,
|
||||
processing_run_id bigint NOT NULL,
|
||||
attempt_id bigint NOT NULL UNIQUE,
|
||||
submission_status text NOT NULL CHECK (submission_status IN (
|
||||
'received',
|
||||
'validating',
|
||||
'committed',
|
||||
'already_committed',
|
||||
'rejected',
|
||||
'expired'
|
||||
)),
|
||||
contract_version text NOT NULL CHECK (
|
||||
contract_version = 'arr-direct-ingestion-1'
|
||||
),
|
||||
business_date date NOT NULL,
|
||||
processor_version text NOT NULL CHECK (
|
||||
processor_version ~ '^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$'
|
||||
),
|
||||
rule_set_sha256 character(64) NOT NULL CHECK (
|
||||
rule_set_sha256 ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
result_schema_version text NOT NULL CHECK (
|
||||
btrim(result_schema_version) <> ''
|
||||
),
|
||||
payload_json jsonb CHECK (
|
||||
payload_json IS NULL
|
||||
OR jsonb_typeof(payload_json) = 'object'
|
||||
),
|
||||
payload_sha256 character(64) NOT NULL CHECK (
|
||||
payload_sha256 ~ '^[0-9a-f]{64}$'
|
||||
),
|
||||
payload_byte_size integer NOT NULL CHECK (
|
||||
payload_byte_size BETWEEN 2 AND 3145728
|
||||
),
|
||||
record_count integer NOT NULL CHECK (
|
||||
record_count >= 0
|
||||
),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
expires_at timestamptz NOT NULL,
|
||||
validation_started_at timestamptz,
|
||||
finished_at timestamptz,
|
||||
payload_purged_at timestamptz,
|
||||
daily_version_id bigint
|
||||
REFERENCES finance.daily_versions(id),
|
||||
failure_code text,
|
||||
failure_message text,
|
||||
receipt_json jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (
|
||||
jsonb_typeof(receipt_json) = 'object'
|
||||
),
|
||||
CONSTRAINT result_submissions_grant_run_attempt_fk
|
||||
FOREIGN KEY (grant_id, processing_run_id, attempt_id)
|
||||
REFERENCES ingestion.result_submission_grants(
|
||||
id,
|
||||
processing_run_id,
|
||||
attempt_id
|
||||
),
|
||||
CONSTRAINT result_submissions_lifetime CHECK (
|
||||
expires_at > created_at
|
||||
AND expires_at <= created_at + interval '30 minutes'
|
||||
),
|
||||
CONSTRAINT result_submissions_payload_shape CHECK (
|
||||
payload_json IS NULL
|
||||
OR (
|
||||
payload_json ? 'records'
|
||||
AND jsonb_typeof(payload_json -> 'records') = 'array'
|
||||
AND jsonb_array_length(payload_json -> 'records') = record_count
|
||||
)
|
||||
),
|
||||
CONSTRAINT result_submissions_timestamps_valid CHECK (
|
||||
(
|
||||
validation_started_at IS NULL
|
||||
OR validation_started_at >= created_at
|
||||
)
|
||||
AND (finished_at IS NULL OR finished_at >= created_at)
|
||||
AND (
|
||||
payload_purged_at IS NULL
|
||||
OR (
|
||||
finished_at IS NOT NULL
|
||||
AND payload_purged_at >= finished_at
|
||||
)
|
||||
)
|
||||
AND (
|
||||
submission_status <> 'expired'
|
||||
OR finished_at >= expires_at
|
||||
)
|
||||
),
|
||||
CONSTRAINT result_submissions_status_shape CHECK (
|
||||
(
|
||||
submission_status = 'received'
|
||||
AND validation_started_at IS NULL
|
||||
AND finished_at IS NULL
|
||||
AND daily_version_id IS NULL
|
||||
AND failure_code IS NULL
|
||||
AND receipt_json = '{}'::jsonb
|
||||
AND payload_json IS NOT NULL
|
||||
AND payload_purged_at IS NULL
|
||||
)
|
||||
OR (
|
||||
submission_status = 'validating'
|
||||
AND validation_started_at IS NOT NULL
|
||||
AND finished_at IS NULL
|
||||
AND daily_version_id IS NULL
|
||||
AND failure_code IS NULL
|
||||
AND receipt_json = '{}'::jsonb
|
||||
AND payload_json IS NOT NULL
|
||||
AND payload_purged_at IS NULL
|
||||
)
|
||||
OR (
|
||||
submission_status IN ('committed', 'already_committed')
|
||||
AND validation_started_at IS NOT NULL
|
||||
AND finished_at IS NOT NULL
|
||||
AND daily_version_id IS NOT NULL
|
||||
AND failure_code IS NULL
|
||||
AND receipt_json <> '{}'::jsonb
|
||||
AND payload_json IS NULL
|
||||
AND payload_purged_at IS NOT NULL
|
||||
)
|
||||
OR (
|
||||
submission_status IN ('rejected', 'expired')
|
||||
AND finished_at IS NOT NULL
|
||||
AND daily_version_id IS NULL
|
||||
AND failure_code IS NOT NULL
|
||||
AND btrim(failure_code) <> ''
|
||||
AND payload_json IS NULL
|
||||
AND payload_purged_at IS NOT NULL
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX result_submissions_status_expiry_idx
|
||||
ON ingestion.result_submissions (submission_status, expires_at);
|
||||
|
||||
-- Deliberately non-unique: the same XML/result may be processed repeatedly by
|
||||
-- different test jobs while final Finance facts remain idempotent.
|
||||
CREATE INDEX result_submissions_payload_sha256_idx
|
||||
ON ingestion.result_submissions (payload_sha256);
|
||||
|
||||
CREATE INDEX result_submissions_daily_version_idx
|
||||
ON ingestion.result_submissions (daily_version_id)
|
||||
WHERE daily_version_id IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN ingestion.processing_runs.result_delivery_mode IS
|
||||
'artifact_callback preserves the v3 OSS result contract; direct_mcp commits validated structured records without Agent-produced output objects.';
|
||||
|
||||
COMMENT ON COLUMN finance.daily_versions.result_delivery_mode IS
|
||||
'Identifies whether the validated Finance version arrived through the legacy artifact callback or the direct MCP ingestion path.';
|
||||
|
||||
COMMENT ON TABLE ingestion.result_submission_grants IS
|
||||
'Hashed, attempt-bound grants for starting one direct MCP result submission. Plaintext grants and database credentials are never stored.';
|
||||
|
||||
COMMENT ON TABLE ingestion.result_submissions IS
|
||||
'Durable, idempotent single-call result ledger. Payload hashes are intentionally non-unique so the same XML can be rerun under a new processing attempt; guest-bearing payload JSON is purged at terminal status.';
|
||||
|
||||
COMMIT;
|
||||
Reference in New Issue
Block a user