This commit is contained in:
@@ -0,0 +1,455 @@
|
||||
-- DevRunbook v1.1 relational reference. Migration code may differ syntactically
|
||||
-- but must preserve the ownership, immutability and uniqueness contracts.
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
CREATE TABLE users (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email text NOT NULL,
|
||||
display_name text NOT NULL,
|
||||
password_hash text NOT NULL,
|
||||
instance_role text NOT NULL CHECK (instance_role IN ('instance_owner','instance_admin','user')),
|
||||
status text NOT NULL DEFAULT 'active' CHECK (status IN ('active','disabled','pending_deletion')),
|
||||
password_changed_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz
|
||||
);
|
||||
CREATE UNIQUE INDEX users_email_ci_uq ON users (lower(email)) WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE TABLE auth_sessions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash text NOT NULL UNIQUE,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
last_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||
idle_expires_at timestamptz NOT NULL,
|
||||
absolute_expires_at timestamptz NOT NULL,
|
||||
revoked_at timestamptz,
|
||||
source_ip_hash text,
|
||||
user_agent_summary text
|
||||
);
|
||||
CREATE INDEX auth_sessions_user_active_idx ON auth_sessions(user_id, absolute_expires_at) WHERE revoked_at IS NULL;
|
||||
|
||||
CREATE TABLE invitations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email text NOT NULL,
|
||||
token_hash text NOT NULL UNIQUE,
|
||||
instance_role text NOT NULL CHECK (instance_role IN ('instance_admin','user')),
|
||||
workspace_id uuid,
|
||||
workspace_role text CHECK (workspace_role IN ('owner','editor','viewer')),
|
||||
expires_at timestamptz NOT NULL,
|
||||
accepted_at timestamptz,
|
||||
created_by uuid NOT NULL REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE password_reset_tokens (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
token_hash text NOT NULL UNIQUE,
|
||||
expires_at timestamptz NOT NULL,
|
||||
used_at timestamptz,
|
||||
created_by uuid REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE workspaces (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name text NOT NULL,
|
||||
type text NOT NULL DEFAULT 'personal' CHECK (type IN ('personal','team')),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
deleted_at timestamptz
|
||||
);
|
||||
ALTER TABLE invitations ADD CONSTRAINT invitations_workspace_fk FOREIGN KEY (workspace_id) REFERENCES workspaces(id) ON DELETE CASCADE;
|
||||
|
||||
CREATE TABLE workspace_memberships (
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role text NOT NULL CHECK (role IN ('owner','editor','viewer')),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (workspace_id, user_id)
|
||||
);
|
||||
CREATE INDEX workspace_memberships_user_idx ON workspace_memberships(user_id);
|
||||
|
||||
CREATE TABLE instance_settings (
|
||||
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
|
||||
setup_completed_at timestamptz,
|
||||
owner_user_id uuid REFERENCES users(id),
|
||||
config_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
config_digest text,
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE playbooks (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
logical_id text NOT NULL,
|
||||
slug text NOT NULL,
|
||||
namespace text NOT NULL,
|
||||
source_type text NOT NULL CHECK (source_type IN ('built_in','private','imported','remote_registry')),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(namespace, logical_id),
|
||||
UNIQUE(namespace, slug)
|
||||
);
|
||||
CREATE INDEX playbooks_workspace_idx ON playbooks(workspace_id);
|
||||
|
||||
CREATE TABLE playbook_versions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
playbook_id uuid NOT NULL REFERENCES playbooks(id) ON DELETE CASCADE,
|
||||
semantic_version text NOT NULL,
|
||||
lifecycle text NOT NULL CHECK (lifecycle IN ('draft','reviewed','validated','battle-tested','deprecated')),
|
||||
package_api_version text NOT NULL,
|
||||
title text NOT NULL,
|
||||
summary text NOT NULL,
|
||||
category text NOT NULL,
|
||||
risk_tier text NOT NULL CHECK (risk_tier IN ('low','moderate','high','critical')),
|
||||
package_json jsonb NOT NULL,
|
||||
template_text text NOT NULL,
|
||||
content_digest text NOT NULL,
|
||||
draft_revision integer NOT NULL DEFAULT 1 CHECK (draft_revision > 0),
|
||||
draft_digest text NOT NULL DEFAULT repeat('0', 64) CHECK (draft_digest ~ '^[0-9a-f]{64}$'),
|
||||
draft_validation_json jsonb NOT NULL DEFAULT '{"valid":true,"issues":[]}'::jsonb CHECK (jsonb_typeof(draft_validation_json) = 'object'),
|
||||
search_document tsvector,
|
||||
published_at timestamptz,
|
||||
supersedes_version_id uuid REFERENCES playbook_versions(id),
|
||||
created_by uuid REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(playbook_id, semantic_version)
|
||||
);
|
||||
CREATE INDEX playbook_versions_search_idx ON playbook_versions USING gin(search_document);
|
||||
CREATE INDEX playbook_versions_filters_idx ON playbook_versions(category, risk_tier, lifecycle, published_at DESC);
|
||||
|
||||
CREATE FUNCTION normalize_playbook_version_draft_digest() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
IF NEW.draft_digest = repeat('0', 64) THEN
|
||||
NEW.draft_digest := NEW.content_digest;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
CREATE TRIGGER playbook_versions_normalize_draft_digest
|
||||
BEFORE INSERT ON playbook_versions
|
||||
FOR EACH ROW EXECUTE FUNCTION normalize_playbook_version_draft_digest();
|
||||
|
||||
CREATE TABLE playbook_package_files (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
playbook_version_id uuid NOT NULL REFERENCES playbook_versions(id) ON DELETE CASCADE,
|
||||
path text NOT NULL CHECK (length(path) BETWEEN 1 AND 512 AND path = btrim(path) AND path !~ '(^/|\\|//|(^|/)\.\.?(/|$))'),
|
||||
role text NOT NULL CHECK (role IN ('manifest','template','partial','documentation','changelog','example','evaluation','resource','run-pack-resource')),
|
||||
content bytea NOT NULL,
|
||||
size_bytes bigint NOT NULL CHECK (size_bytes BETWEEN 0 AND 5242880 AND octet_length(content) = size_bytes),
|
||||
sha256 text NOT NULL CHECK (sha256 ~ '^[0-9a-f]{64}$' AND encode(digest(content, 'sha256'), 'hex') = sha256),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(playbook_version_id, path)
|
||||
);
|
||||
CREATE INDEX playbook_package_files_version_idx ON playbook_package_files(playbook_version_id);
|
||||
|
||||
CREATE FUNCTION reject_published_playbook_file_mutation() RETURNS trigger
|
||||
LANGUAGE plpgsql AS $$
|
||||
DECLARE
|
||||
target_version_id uuid;
|
||||
target_published_at timestamptz;
|
||||
BEGIN
|
||||
target_version_id := CASE WHEN TG_OP = 'DELETE' THEN OLD.playbook_version_id ELSE NEW.playbook_version_id END;
|
||||
SELECT published_at INTO target_published_at FROM playbook_versions WHERE id = target_version_id FOR SHARE;
|
||||
IF target_published_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'published playbook package files are immutable' USING ERRCODE = '55000';
|
||||
END IF;
|
||||
IF TG_OP = 'UPDATE' AND OLD.playbook_version_id <> NEW.playbook_version_id THEN
|
||||
SELECT published_at INTO target_published_at FROM playbook_versions WHERE id = OLD.playbook_version_id FOR SHARE;
|
||||
IF target_published_at IS NOT NULL THEN
|
||||
RAISE EXCEPTION 'published playbook package files are immutable' USING ERRCODE = '55000';
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
|
||||
END;
|
||||
$$;
|
||||
CREATE TRIGGER playbook_package_files_immutable_when_published
|
||||
BEFORE INSERT OR UPDATE OR DELETE ON playbook_package_files
|
||||
FOR EACH ROW EXECUTE FUNCTION reject_published_playbook_file_mutation();
|
||||
|
||||
CREATE TABLE favorites (
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
playbook_id uuid NOT NULL REFERENCES playbooks(id) ON DELETE CASCADE,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY(workspace_id, user_id, playbook_id)
|
||||
);
|
||||
|
||||
CREATE TABLE collections (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
name text NOT NULL,
|
||||
description text NOT NULL DEFAULT '',
|
||||
created_by uuid NOT NULL REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
CONSTRAINT collections_owner_name_uq UNIQUE(workspace_id, created_by, name),
|
||||
CONSTRAINT collections_name_check CHECK (
|
||||
char_length(name) BETWEEN 1 AND 80 AND name = btrim(name) AND
|
||||
name !~ '[[:cntrl:]]'
|
||||
),
|
||||
CONSTRAINT collections_description_check CHECK (
|
||||
char_length(description) <= 500
|
||||
)
|
||||
);
|
||||
CREATE TABLE collection_items (
|
||||
collection_id uuid NOT NULL REFERENCES collections(id) ON DELETE CASCADE,
|
||||
playbook_id uuid NOT NULL REFERENCES playbooks(id) ON DELETE CASCADE,
|
||||
position integer NOT NULL DEFAULT 0,
|
||||
added_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY(collection_id, playbook_id),
|
||||
CONSTRAINT collection_items_position_check CHECK (position >= 0)
|
||||
);
|
||||
|
||||
CREATE TABLE integrations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
type text NOT NULL CHECK (type IN ('gitea')),
|
||||
display_name text NOT NULL,
|
||||
base_url text NOT NULL,
|
||||
allow_private_http boolean NOT NULL DEFAULT false,
|
||||
request_timeout_ms integer NOT NULL DEFAULT 15000 CHECK (request_timeout_ms BETWEEN 1000 AND 60000),
|
||||
status text NOT NULL DEFAULT 'configured' CHECK (status IN ('configured','healthy','degraded','disabled')),
|
||||
capabilities_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
server_version text,
|
||||
remote_identity_id text,
|
||||
remote_identity_login text,
|
||||
health_code text CHECK (health_code IS NULL OR health_code IN ('AUTH_INVALID','PERMISSION_MISSING','CAPABILITY_UNSUPPORTED','RATE_LIMITED','NETWORK_BLOCKED','TLS_ERROR','REMOTE_UNAVAILABLE','CONTENT_TOO_LARGE')),
|
||||
last_checked_at timestamptz,
|
||||
created_by uuid NOT NULL REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(workspace_id, type, base_url),
|
||||
CHECK ((remote_identity_id IS NULL) = (remote_identity_login IS NULL))
|
||||
);
|
||||
CREATE INDEX integrations_workspace_status_idx ON integrations(workspace_id, status, updated_at DESC);
|
||||
|
||||
CREATE TABLE integration_secrets (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
integration_id uuid NOT NULL REFERENCES integrations(id) ON DELETE CASCADE,
|
||||
secret_kind text NOT NULL CHECK (secret_kind = 'access_token'),
|
||||
envelope_version integer NOT NULL CHECK (envelope_version = 1),
|
||||
key_version text NOT NULL CHECK (length(btrim(key_version)) BETWEEN 1 AND 64),
|
||||
nonce bytea NOT NULL CHECK (octet_length(nonce) = 12),
|
||||
ciphertext bytea NOT NULL,
|
||||
auth_tag bytea NOT NULL CHECK (octet_length(auth_tag) = 16),
|
||||
last_four text CHECK (last_four IS NULL OR length(last_four) = 4),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
rotated_at timestamptz,
|
||||
UNIQUE(integration_id, secret_kind)
|
||||
);
|
||||
|
||||
CREATE TABLE repositories (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
display_name text NOT NULL,
|
||||
source_type text NOT NULL CHECK (source_type IN ('manual','gitea')),
|
||||
external_owner text,
|
||||
external_name text,
|
||||
external_id text,
|
||||
integration_id uuid REFERENCES integrations(id) ON DELETE SET NULL,
|
||||
default_branch text,
|
||||
archived boolean NOT NULL DEFAULT false,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE UNIQUE INDEX repositories_external_uq ON repositories(workspace_id, integration_id, external_id) WHERE external_id IS NOT NULL;
|
||||
|
||||
CREATE TABLE repository_snapshots (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
repository_id uuid NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
|
||||
integration_id uuid REFERENCES integrations(id) ON DELETE SET NULL,
|
||||
state text NOT NULL CHECK (state IN ('collecting','complete','failed','cancelled')),
|
||||
captured_at timestamptz,
|
||||
capability_snapshot_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
evidence_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
evidence_digest text,
|
||||
sync_job_id uuid,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
CHECK (state <> 'complete' OR (captured_at IS NOT NULL AND evidence_digest ~ '^[0-9a-f]{64}$'))
|
||||
);
|
||||
CREATE INDEX repository_snapshots_repo_time_idx ON repository_snapshots(repository_id, captured_at DESC);
|
||||
CREATE UNIQUE INDEX repository_snapshots_sync_job_uq ON repository_snapshots(sync_job_id) WHERE sync_job_id IS NOT NULL;
|
||||
CREATE INDEX repository_snapshots_integration_state_idx ON repository_snapshots(integration_id, state, created_at DESC);
|
||||
|
||||
CREATE TABLE repository_profile_revisions (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
repository_id uuid NOT NULL REFERENCES repositories(id) ON DELETE CASCADE,
|
||||
revision_number integer NOT NULL,
|
||||
profile_json jsonb NOT NULL,
|
||||
source_snapshot_id uuid REFERENCES repository_snapshots(id) ON DELETE SET NULL,
|
||||
content_digest text NOT NULL,
|
||||
created_by uuid NOT NULL REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(repository_id, revision_number),
|
||||
UNIQUE(repository_id, content_digest)
|
||||
);
|
||||
|
||||
CREATE TABLE repository_findings (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
snapshot_id uuid NOT NULL REFERENCES repository_snapshots(id) ON DELETE CASCADE,
|
||||
rule_id text NOT NULL,
|
||||
severity text NOT NULL CHECK (severity IN ('info','low','medium','high','critical')),
|
||||
title text NOT NULL,
|
||||
rationale text NOT NULL,
|
||||
evidence_pointer text NOT NULL,
|
||||
recommended_playbook_slug text,
|
||||
status text NOT NULL DEFAULT 'open' CHECK (status IN ('open','dismissed','resolved')),
|
||||
resolution_note text,
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(snapshot_id, rule_id, evidence_pointer)
|
||||
);
|
||||
|
||||
CREATE TABLE composition_drafts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
playbook_version_id uuid NOT NULL REFERENCES playbook_versions(id),
|
||||
repository_profile_revision_id uuid REFERENCES repository_profile_revisions(id),
|
||||
input_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
scope_override_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
policy_override_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
autonomy_level text NOT NULL CHECK (autonomy_level IN ('observe','diagnose','plan','implement','verify','repair')),
|
||||
work_mode text NOT NULL CHECK (work_mode IN ('inspect','plan','guided','execute','recovery')),
|
||||
output_format text NOT NULL DEFAULT 'prompt' CHECK (output_format IN ('prompt','markdown','run-pack')),
|
||||
last_render_digest text CHECK (last_render_digest IS NULL OR last_render_digest ~ '^[0-9a-f]{64}$'),
|
||||
revision integer NOT NULL DEFAULT 1 CHECK (revision > 0),
|
||||
created_by uuid NOT NULL REFERENCES users(id),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX composition_drafts_workspace_updated_idx ON composition_drafts(workspace_id, updated_at DESC);
|
||||
|
||||
CREATE TABLE generated_runs (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid NOT NULL REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
source_draft_id uuid REFERENCES composition_drafts(id) ON DELETE SET NULL,
|
||||
playbook_version_id uuid NOT NULL REFERENCES playbook_versions(id),
|
||||
playbook_snapshot_json jsonb NOT NULL,
|
||||
repository_profile_snapshot_json jsonb,
|
||||
normalized_input_json jsonb NOT NULL,
|
||||
policy_snapshot_json jsonb NOT NULL,
|
||||
provenance_json jsonb NOT NULL,
|
||||
lint_result_json jsonb NOT NULL,
|
||||
rendered_prompt text NOT NULL,
|
||||
render_digest text NOT NULL CHECK (render_digest ~ '^[0-9a-f]{64}$'),
|
||||
idempotency_key text NOT NULL CHECK (length(idempotency_key) BETWEEN 1 AND 255 AND btrim(idempotency_key) = idempotency_key),
|
||||
generated_by uuid NOT NULL REFERENCES users(id),
|
||||
generated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(workspace_id, idempotency_key)
|
||||
);
|
||||
CREATE INDEX generated_runs_workspace_time_idx ON generated_runs(workspace_id, generated_at DESC);
|
||||
CREATE UNIQUE INDEX generated_runs_digest_actor_uq ON generated_runs(workspace_id, generated_by, render_digest, generated_at);
|
||||
|
||||
CREATE TABLE generated_artifacts (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
run_id uuid NOT NULL REFERENCES generated_runs(id) ON DELETE CASCADE,
|
||||
artifact_type text NOT NULL CHECK (artifact_type IN ('prompt_text','markdown','run_pack_zip','agents_suggestion','support_bundle')),
|
||||
storage_key text NOT NULL UNIQUE,
|
||||
filename text NOT NULL,
|
||||
media_type text NOT NULL,
|
||||
size_bytes bigint NOT NULL CHECK (size_bytes >= 0),
|
||||
sha256 text NOT NULL,
|
||||
expires_at timestamptz,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE run_feedback (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
run_id uuid NOT NULL REFERENCES generated_runs(id) ON DELETE CASCADE,
|
||||
user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
rating text CHECK (rating IN ('helpful','mixed','unhelpful')),
|
||||
notes text NOT NULL DEFAULT '',
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(run_id, user_id)
|
||||
);
|
||||
|
||||
CREATE TABLE evaluation_cases (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
playbook_version_id uuid NOT NULL REFERENCES playbook_versions(id) ON DELETE CASCADE,
|
||||
logical_case_id text NOT NULL,
|
||||
case_version text,
|
||||
fixture_version text NOT NULL,
|
||||
target_digest text CHECK (target_digest IS NULL OR target_digest ~ '^[0-9a-f]{64}$'),
|
||||
fixture_id text,
|
||||
fixture_digest text CHECK (fixture_digest IS NULL OR fixture_digest ~ '^[0-9a-f]{64}$'),
|
||||
environment_digest text CHECK (environment_digest IS NULL OR environment_digest ~ '^[0-9a-f]{64}$'),
|
||||
case_json jsonb NOT NULL,
|
||||
case_digest text NOT NULL,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(playbook_version_id, logical_case_id, fixture_version)
|
||||
);
|
||||
|
||||
CREATE TABLE evaluation_results (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
evaluation_case_id uuid NOT NULL REFERENCES evaluation_cases(id) ON DELETE CASCADE,
|
||||
environment_json jsonb NOT NULL,
|
||||
target_digest text CHECK (target_digest IS NULL OR target_digest ~ '^[0-9a-f]{64}$'),
|
||||
fixture_digest text CHECK (fixture_digest IS NULL OR fixture_digest ~ '^[0-9a-f]{64}$'),
|
||||
environment_digest text CHECK (environment_digest IS NULL OR environment_digest ~ '^[0-9a-f]{64}$'),
|
||||
result_json jsonb,
|
||||
status text NOT NULL CHECK (status IN ('passed','failed','error','skipped','stale')),
|
||||
dimension_scores_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
evidence_artifact_id uuid REFERENCES generated_artifacts(id) ON DELETE SET NULL,
|
||||
executed_by uuid REFERENCES users(id),
|
||||
executed_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE playbook_review_attestations (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
playbook_version_id uuid NOT NULL REFERENCES playbook_versions(id) ON DELETE CASCADE,
|
||||
reviewed_by uuid NOT NULL REFERENCES users(id),
|
||||
attested_digest text NOT NULL CHECK (attested_digest ~ '^[0-9a-f]{64}$'),
|
||||
schema_and_semantic_validation_passed boolean NOT NULL,
|
||||
blocking_lint_finding_count integer NOT NULL CHECK (blocking_lint_finding_count >= 0),
|
||||
limitations_documented boolean NOT NULL,
|
||||
unresolved_safety_regression boolean NOT NULL,
|
||||
review_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
reviewed_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX playbook_review_attestations_version_time_idx
|
||||
ON playbook_review_attestations(playbook_version_id, reviewed_at DESC);
|
||||
|
||||
CREATE TABLE jobs (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
workspace_id uuid REFERENCES workspaces(id) ON DELETE CASCADE,
|
||||
type text NOT NULL,
|
||||
state text NOT NULL CHECK (state IN ('queued','running','succeeded','failed','cancelled')),
|
||||
idempotency_key text,
|
||||
payload_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
progress_json jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
attempt_count integer NOT NULL DEFAULT 0,
|
||||
max_attempts integer NOT NULL DEFAULT 3,
|
||||
lease_owner text,
|
||||
lease_expires_at timestamptz,
|
||||
available_at timestamptz NOT NULL DEFAULT now(),
|
||||
started_at timestamptz,
|
||||
finished_at timestamptz,
|
||||
error_code text,
|
||||
error_detail_redacted text,
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
UNIQUE(workspace_id, type, idempotency_key)
|
||||
);
|
||||
CREATE INDEX jobs_claim_idx ON jobs(state, available_at, created_at) WHERE state = 'queued';
|
||||
CREATE INDEX jobs_lease_idx ON jobs(state, lease_expires_at) WHERE state = 'running';
|
||||
ALTER TABLE repository_snapshots ADD CONSTRAINT repository_snapshots_job_fk FOREIGN KEY (sync_job_id) REFERENCES jobs(id) ON DELETE SET NULL;
|
||||
|
||||
CREATE TABLE audit_events (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
occurred_at timestamptz NOT NULL DEFAULT now(),
|
||||
actor_user_id uuid REFERENCES users(id) ON DELETE SET NULL,
|
||||
workspace_id uuid REFERENCES workspaces(id) ON DELETE SET NULL,
|
||||
action text NOT NULL,
|
||||
resource_type text NOT NULL,
|
||||
resource_id text,
|
||||
request_id text,
|
||||
outcome text NOT NULL CHECK (outcome IN ('success','denied','failed')),
|
||||
metadata_json jsonb NOT NULL DEFAULT '{}'::jsonb
|
||||
);
|
||||
CREATE INDEX audit_events_workspace_time_idx ON audit_events(workspace_id, occurred_at DESC);
|
||||
CREATE INDEX audit_events_action_time_idx ON audit_events(action, occurred_at DESC);
|
||||
Reference in New Issue
Block a user