Fourth commit of the Research + Loops arc. Completes the state machine
for research topics with the publish approval gate the spec asked for.
Migration 0032 — research_publish_approvals
Dedicated small table (id, workspace_id, topic_id, requested_by,
status, decided_by/at, created_at). Keeping it separate from the
existing `approvals` table (0001) because that one is tightly coupled
to gated tool calls inside an agent run — session_key + run_id +
action_type + category + payload + preview + requested_by_agent, all
NOT NULL. Forcing those nullable would ripple through cm_safety;
cleaner to give publish approvals their own two-transition state
machine.
New endpoints
POST /api/research/:id/submit-review processing → reviewing
(v1 caller-driven; the
orchestrator hook comes
when we wire actual runs)
POST /api/research/:id/request-publish creates a pending
approval. Rejects with
409 if the topic already
has one open.
GET /api/research/publish-approvals list workspace's pending
POST /api/research/publish-approvals/:id/approve flips approval to
approved + transitions
the topic
reviewing → publishing
(which stamps
published_at)
POST /api/research/publish-approvals/:id/reject stays in reviewing; new
requests allowed
The approve/reject write is an atomic UPDATE ... WHERE status = 'pending';
the decide() repo function returns whether the caller won the race so
concurrent double-approves collapse to a single topic transition.
State machine after this commit:
standby ─POST /start─▶ processing ─POST /submit-review─▶ reviewing
─POST /request-publish + approve─▶ publishing ─(future: artifact
assembly)─▶ published
42 lines
2.1 KiB
SQL
42 lines
2.1 KiB
SQL
-- Publish approval gate for research topics. Distinct from the existing
|
|
-- `approvals` table (0001), which is tightly coupled to gated tool calls
|
|
-- inside an agent run (session_key + run_id + action_type + category +
|
|
-- payload + preview + requested_by_agent, all NOT NULL). None of those apply
|
|
-- to a publish-of-a-static-topic gate; forcing them nullable would ripple
|
|
-- through cm_safety, so we give publish approvals their own small table.
|
|
--
|
|
-- Lifecycle:
|
|
-- 1. Topic reaches `reviewing` (either the orchestrator flipped it after
|
|
-- the last agent run completed, or the user hit /submit-review).
|
|
-- 2. A workspace member calls /api/research/:id/request-publish, which
|
|
-- writes a row here with status='pending'.
|
|
-- 3. Another workspace member (any user in the workspace; per-role gating
|
|
-- can layer on later) approves or rejects.
|
|
-- 4. On the first approval, the topic transitions `reviewing → publishing`
|
|
-- and (after any artifact assembly) `publishing → published`.
|
|
-- 5. On a reject, the topic stays in `reviewing` and a new request is
|
|
-- allowed. Old rejected rows are audit history.
|
|
|
|
CREATE TABLE research_publish_approvals (
|
|
id UUID PRIMARY KEY,
|
|
workspace_id UUID NOT NULL REFERENCES workspaces (id) ON DELETE CASCADE,
|
|
topic_id UUID NOT NULL REFERENCES research_topics (id) ON DELETE CASCADE,
|
|
requested_by UUID NOT NULL REFERENCES users (id) ON DELETE RESTRICT,
|
|
status TEXT NOT NULL DEFAULT 'pending'
|
|
CHECK (status IN ('pending', 'approved', 'rejected')),
|
|
decided_by UUID REFERENCES users (id) ON DELETE RESTRICT,
|
|
decided_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
-- Sidebar "pending approvals" list query.
|
|
CREATE INDEX research_publish_approvals_pending_idx
|
|
ON research_publish_approvals (workspace_id, created_at DESC)
|
|
WHERE status = 'pending';
|
|
|
|
-- Look up "does this topic already have a pending approval?" before letting
|
|
-- a caller create a second one.
|
|
CREATE INDEX research_publish_approvals_topic_pending_idx
|
|
ON research_publish_approvals (topic_id)
|
|
WHERE status = 'pending';
|