Files
damascus-orchestrator/schema.sql
2026-06-27 16:38:32 +00:00

146 lines
5.6 KiB
PL/PgSQL

-- Damascus orchestrator — PostgreSQL 16 schema.
-- Atomic claim is implemented with `SELECT ... FOR UPDATE SKIP LOCKED`
-- on `work_items`, gated by a per-row `phase` enum.
--
-- Postgres is the authoritative scheduler (design doc §3). This file is
-- applied by `damascus init`, which creates the `damascus` database first
-- (CREATE DATABASE cannot run inside a transaction/DO block) and then
-- executes this whole file in a single execute() call.
-- --- enum types (idempotent; CREATE TYPE has no IF NOT EXISTS) ------------
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'work_item_phase') THEN
CREATE TYPE work_item_phase AS ENUM
('spec','build','review','merged','blocked','awaiting_human');
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'gate_kind') THEN
CREATE TYPE gate_kind AS ENUM ('and','or','first');
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'issue_status') THEN
CREATE TYPE issue_status AS ENUM ('open','answered','resolved');
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'verdict_kind') THEN
-- design doc §5: pass | tests_failed | rebase_conflict |
-- spec_ambiguous | spec_wrong | no_pr
CREATE TYPE verdict_kind AS ENUM
('pass','tests_failed','rebase_conflict','spec_ambiguous','spec_wrong','no_pr');
END IF;
END $$;
-- --- shared trigger function: keep updated_at honest (no ON UPDATE in PG) --
CREATE OR REPLACE FUNCTION fn_touch_updated_at() RETURNS trigger AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- --- tables ---------------------------------------------------------------
-- work_items: one row per story, across all projects.
CREATE TABLE IF NOT EXISTS work_items (
id CHAR(36) NOT NULL PRIMARY KEY,
project VARCHAR(64) NOT NULL,
story_id VARCHAR(128) NOT NULL,
title VARCHAR(255) NOT NULL DEFAULT '',
phase work_item_phase NOT NULL DEFAULT 'spec',
file_scope JSONB NOT NULL DEFAULT '[]'::jsonb,
attempts INT NOT NULL DEFAULT 0,
budget_cycles INT NOT NULL DEFAULT 3, -- amendment §4: default N=3 (was 5). Per-row configurable; this is just the default for new rows.
priority INT NOT NULL DEFAULT 100,
base_commit VARCHAR(64) DEFAULT NULL,
branch VARCHAR(128) DEFAULT NULL,
pr_url VARCHAR(512) DEFAULT NULL,
last_verdict verdict_kind DEFAULT NULL,
last_feedback JSONB DEFAULT NULL,
spec_path VARCHAR(512) DEFAULT NULL,
wiki_pin VARCHAR(64) DEFAULT NULL,
claimed_by VARCHAR(64) DEFAULT NULL,
claimed_at TIMESTAMPTZ DEFAULT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
merged_at TIMESTAMPTZ DEFAULT NULL,
-- ADR-005: set by claim_for_* on first claim; used by cycle.py to escalate
-- persistent transient retries to blocked after 24h.
first_attempted_at TIMESTAMPTZ DEFAULT NULL,
UNIQUE (project, story_id)
);
DROP TRIGGER IF EXISTS trg_work_items_touch ON work_items;
CREATE TRIGGER trg_work_items_touch BEFORE UPDATE ON work_items
FOR EACH ROW EXECUTE FUNCTION fn_touch_updated_at();
CREATE INDEX IF NOT EXISTS idx_work_items_phase_priority
ON work_items (phase, priority, updated_at);
-- coordination_gates: AND/OR/FIRST dependency edges between stories.
CREATE TABLE IF NOT EXISTS coordination_gates (
parent_id CHAR(36) NOT NULL,
child_id CHAR(36) NOT NULL,
kind gate_kind NOT NULL DEFAULT 'and',
PRIMARY KEY (parent_id, child_id)
);
CREATE INDEX IF NOT EXISTS idx_coord_gates_child ON coordination_gates (child_id);
-- human_issues: async channel for open questions / approvals.
CREATE TABLE IF NOT EXISTS human_issues (
id CHAR(36) NOT NULL PRIMARY KEY,
work_item_id CHAR(36) NOT NULL,
question TEXT NOT NULL,
answer TEXT DEFAULT NULL,
status issue_status NOT NULL DEFAULT 'open',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
answered_at TIMESTAMPTZ DEFAULT NULL
);
CREATE INDEX IF NOT EXISTS idx_human_issues_status ON human_issues (status, created_at);
CREATE INDEX IF NOT EXISTS idx_human_issues_item ON human_issues (work_item_id);
-- cost_ledger: per-call token + dollar spend.
CREATE TABLE IF NOT EXISTS cost_ledger (
id BIGSERIAL PRIMARY KEY,
work_item_id CHAR(36) DEFAULT NULL,
project VARCHAR(64) DEFAULT NULL,
phase VARCHAR(32) DEFAULT NULL,
model VARCHAR(128) DEFAULT NULL,
input_tokens INT DEFAULT NULL,
output_tokens INT DEFAULT NULL,
usd DECIMAL(10,6) DEFAULT NULL,
recorded_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_cost_ledger_item ON cost_ledger (work_item_id);
CREATE INDEX IF NOT EXISTS idx_cost_ledger_recorded ON cost_ledger (recorded_at);
-- events_outbox: per-cycle transition log. Written in the same transaction
-- as the state update (transactional outbox, design doc §8). A drainer
-- delivers these to the overseer with idempotency keys (Phase 3).
CREATE TABLE IF NOT EXISTS events_outbox (
id BIGSERIAL PRIMARY KEY,
work_item_id CHAR(36) DEFAULT NULL,
kind VARCHAR(64) NOT NULL,
payload JSONB NOT NULL,
delivered BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_events_outbox_delivered
ON events_outbox (delivered, created_at);
-- A handy view for the external concurrency tracker.
CREATE OR REPLACE VIEW v_active_claims AS
SELECT id, project, story_id, phase, claimed_by, claimed_at, updated_at
FROM work_items
WHERE phase IN ('spec','build','review');