146 lines
5.6 KiB
PL/PgSQL
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'); |