456 lines
21 KiB
PL/PgSQL
456 lines
21 KiB
PL/PgSQL
-- 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);
|