Files
clawmates/migrations/0032_research_publish_approvals.sql
T
Omar Sobh 973eeb272e
ci / gates (push) Successful in 6s
ci / frontend (push) Successful in 24s
ci / rust (push) Successful in 3m44s
ci / publish (push) Successful in 2m14s
ci / e2e (push) Failing after 29m59s
research: publish approval gate + explicit state transitions
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
2026-07-06 06:21:29 -07:00

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';