Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,43 @@ jobs:
STRIPE_WEBHOOK_SECRET: ""
run: cd apps/web && npx vitest run --reporter=default

# Proves the delivery CHECK constraints *behave*, against a real Postgres.
# The hermetic `test-frontend` job runs without a database, so a constraint
# whose predicate is wrong (e.g. accepting `https://localhost` as a shipped
# URL, which happened for weeks) is invisible to it. This job applies the
# shipped migrations to a throwaway Postgres and attempts every forbidden
# write; it fails if the engine accepts one.
db-constraints:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: eventrelay_test
ports:
- 5432:5432
# Don't start the proof until the container answers, or the first
# connection races startup and the job flakes red.
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 5s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: "npm"
- run: npm install --legacy-peer-deps
- name: Apply migrations and verify constraint behavior
env:
# localhost triggers the no-TLS branch in the script; the container
# is a sibling process on the runner with no credential to protect.
TEST_DATABASE_URL: postgres://postgres:postgres@localhost:5432/eventrelay_test
run: cd apps/web && node scripts/verify-constraints.mjs --apply-migrations

test:
runs-on: ubuntu-latest
steps:
Expand Down
177 changes: 177 additions & 0 deletions apps/web/drizzle/0000_delivery.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
-- Delivery pipeline schema.
--
-- Idempotent: safe to re-run. Every statement guards on existence so this can
-- be applied to a database that is already partially migrated.
--
-- The CHECK constraints at the bottom are the point of this file. They encode
-- "delivered means proven" in the database itself, so no application bug,
-- rewrite, or manual UPDATE can record a delivery that never happened.

-- ── Enums ──

DO $$ BEGIN
CREATE TYPE run_status AS ENUM (
'sourcing', 'requirements', 'planning', 'awaiting_approval',
'building', 'verifying', 'deploying',
'delivered', 'blocked', 'failed', 'cancelled'
);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

DO $$ BEGIN
CREATE TYPE gate_kind AS ENUM (
'source_evidence', 'requirements_complete', 'plan_executable',
'human_approved', 'build_succeeded', 'tests_passed', 'deployment_live'
);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

DO $$ BEGIN
CREATE TYPE gate_result AS ENUM ('pass', 'fail', 'skipped');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

DO $$ BEGIN
CREATE TYPE artifact_kind AS ENUM (
'repository', 'deployment', 'test_report', 'build_log', 'transcript'
);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

-- ── Tables ──

CREATE TABLE IF NOT EXISTS delivery_runs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id text NOT NULL,
title text NOT NULL,
status run_status NOT NULL DEFAULT 'sourcing',
source_kind text NOT NULL,
source_url text,
workflow_run_id text,
repo_url text,
tests_passed_at timestamptz,
deployment_url text,
delivered_at timestamptz,
blocked_reason text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS delivery_runs_user_created_idx
ON delivery_runs (user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS delivery_runs_status_idx ON delivery_runs (status);

CREATE TABLE IF NOT EXISTS run_specs (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
run_id uuid NOT NULL REFERENCES delivery_runs (id) ON DELETE CASCADE,
version integer NOT NULL DEFAULT 1,
requirements jsonb NOT NULL,
plan jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);

CREATE UNIQUE INDEX IF NOT EXISTS run_specs_run_version_idx
ON run_specs (run_id, version);

CREATE TABLE IF NOT EXISTS run_steps (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
run_id uuid NOT NULL REFERENCES delivery_runs (id) ON DELETE CASCADE,
seq integer NOT NULL,
phase run_status NOT NULL,
name text NOT NULL,
status text NOT NULL DEFAULT 'running',
detail jsonb,
started_at timestamptz NOT NULL DEFAULT now(),
finished_at timestamptz
);

-- Doubles as the idempotency guard for retried durable steps.
CREATE UNIQUE INDEX IF NOT EXISTS run_steps_run_seq_idx ON run_steps (run_id, seq);

CREATE TABLE IF NOT EXISTS run_gates (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
run_id uuid NOT NULL REFERENCES delivery_runs (id) ON DELETE CASCADE,
kind gate_kind NOT NULL,
result gate_result NOT NULL,
evidence jsonb NOT NULL,
evaluated_at timestamptz NOT NULL DEFAULT now()
);

CREATE UNIQUE INDEX IF NOT EXISTS run_gates_run_kind_idx ON run_gates (run_id, kind);

CREATE TABLE IF NOT EXISTS run_artifacts (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
run_id uuid NOT NULL REFERENCES delivery_runs (id) ON DELETE CASCADE,
kind artifact_kind NOT NULL,
uri text NOT NULL,
meta jsonb,
created_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS run_artifacts_run_idx ON run_artifacts (run_id);

CREATE TABLE IF NOT EXISTS run_approvals (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
run_id uuid NOT NULL REFERENCES delivery_runs (id) ON DELETE CASCADE,
spec_id uuid NOT NULL REFERENCES run_specs (id) ON DELETE CASCADE,
decision text NOT NULL,
decided_by text NOT NULL,
note text,
decided_at timestamptz NOT NULL DEFAULT now()
);

CREATE INDEX IF NOT EXISTS run_approvals_run_idx ON run_approvals (run_id);

-- ── Preventative constraints ──
--
-- Added with a guarded DO block rather than plain ALTER so re-running is safe.

-- A run may only be 'delivered' with all three pieces of delivery evidence
-- present. This is the structural answer to "the system reported success it
-- could not prove": there is no code path, including a manual UPDATE, that can
-- store a delivered run without a repository, a passing test run, and a live
-- deployment URL.
DO $$ BEGIN
ALTER TABLE delivery_runs ADD CONSTRAINT delivery_runs_delivered_requires_evidence
CHECK (
status <> 'delivered'
OR (
repo_url IS NOT NULL
AND tests_passed_at IS NOT NULL
AND deployment_url IS NOT NULL
AND delivered_at IS NOT NULL
)
);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

-- 'blocked' must always say which gate refused. A blocked run with no reason is
-- indistinguishable from a crash, which defeats the purpose of separating the
-- two states.
DO $$ BEGIN
ALTER TABLE delivery_runs ADD CONSTRAINT delivery_runs_blocked_requires_reason
CHECK (status <> 'blocked' OR blocked_reason IS NOT NULL);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

-- A deployment URL must be a real https origin. Guards against storing a
-- placeholder like 'pending' or 'localhost' and treating it as a live delivery.
DO $$ BEGIN
ALTER TABLE delivery_runs ADD CONSTRAINT delivery_runs_deployment_url_is_https
CHECK (deployment_url IS NULL OR deployment_url ~ '^https://[^/]+');
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

-- A video run needs a source URL; an idea run must not carry one.
DO $$ BEGIN
ALTER TABLE delivery_runs ADD CONSTRAINT delivery_runs_source_shape
CHECK (
(source_kind = 'video' AND source_url IS NOT NULL)
OR (source_kind = 'idea' AND source_url IS NULL)
);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

-- A passing gate must carry non-empty evidence. A pass with `{}` is the same
-- unfalsifiable claim this schema exists to prevent.
DO $$ BEGIN
ALTER TABLE run_gates ADD CONSTRAINT run_gates_pass_requires_evidence
CHECK (result <> 'pass' OR (evidence IS NOT NULL AND evidence <> '{}'::jsonb));
EXCEPTION WHEN duplicate_object THEN NULL; END $$;

DO $$ BEGIN
ALTER TABLE run_approvals ADD CONSTRAINT run_approvals_decision_valid
CHECK (decision IN ('approved', 'rejected', 'changes_requested'));
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
45 changes: 45 additions & 0 deletions apps/web/drizzle/0001_training.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-- Training dataset storage.
--
-- Replaces `data/training/video-analysis.jsonl` + `metadata.json`, which were
-- written under `process.cwd()`. On Vercel that path is a read-only bundle, so
-- every write threw EROFS (audit finding F4). Worse, `getMetadata()` called
-- `ensureDir()` before reading, so even the *read* path threw — meaning the
-- training status endpoint failed rather than reporting an empty dataset.
--
-- Dedup moves from an application-level `videosProcessed.includes(url)` array
-- scan to a UNIQUE constraint. The array check was also a race: two concurrent
-- pipeline runs for the same video could both observe "not present" and both
-- append, silently corrupting the fine-tuning dataset with duplicates. A UNIQUE
-- index makes that outcome impossible regardless of concurrency.

CREATE TABLE IF NOT EXISTS training_examples (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
video_url text NOT NULL,
video_title text NOT NULL DEFAULT 'Unknown',
-- Vertex AI SFT-formatted example, ready to serialise as JSONL.
example jsonb NOT NULL,
-- Raw analysis output retained so the example can be regenerated if the
-- prompt format changes, without re-running the (paid) analysis.
analysis jsonb NOT NULL,
exported_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now()
);

-- The dedup guarantee.
CREATE UNIQUE INDEX IF NOT EXISTS training_examples_video_url_idx
ON training_examples (video_url);

-- Ordering for JSONL export.
CREATE INDEX IF NOT EXISTS training_examples_created_idx
ON training_examples (created_at);

-- Singleton row tracking fine-tuning job state. `id` is pinned to a constant so
-- a second row cannot be created.
CREATE TABLE IF NOT EXISTS training_runs (
id integer PRIMARY KEY DEFAULT 1,
tuning_triggered_at timestamptz,
tuning_job_id text,
CONSTRAINT training_runs_singleton CHECK (id = 1)
);

INSERT INTO training_runs (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
45 changes: 45 additions & 0 deletions apps/web/drizzle/0002_deployment_url_placeholders.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
-- Reject placeholder hosts in `deployment_url`.
--
-- The original constraint (`delivery_runs_deployment_url_is_https`) only
-- required the URL to start with `https://`. That was too weak to mean
-- "shipped": `https://localhost`, `https://example.com/app`, and
-- `https://example.org` all satisfied it, so a run pointing at a local dev
-- server or a documentation placeholder could still be stored as `delivered`.
--
-- The application-side guard (`isRealDeploymentUrl` in
-- `src/lib/delivery-lifecycle.ts`) already rejected those hosts, so the two
-- layers disagreed. The cross-layer parity suite
-- (`src/lib/__tests__/delivery-guard-parity.integration.test.ts`) exists to
-- fail CI when that happens, and this migration is the database half of the fix.
--
-- Keep the host list here in lockstep with `PLACEHOLDER_HOSTS` in
-- `src/lib/delivery-lifecycle.ts`. The parity suite enforces that.

ALTER TABLE delivery_runs
DROP CONSTRAINT IF EXISTS delivery_runs_deployment_url_is_https;

ALTER TABLE delivery_runs
DROP CONSTRAINT IF EXISTS delivery_runs_deployment_url_real;

-- A real deployment URL must:
-- 1. be absolute https with a non-empty host
-- 2. not be a loopback / unspecified address
-- 3. not be a reserved documentation domain (example.com/org/net),
-- including any subdomain of one
--
-- The host is the run of characters after `https://` up to the first `/`, `?`,
-- or `#`, and may carry a `:port` suffix.
ALTER TABLE delivery_runs
ADD CONSTRAINT delivery_runs_deployment_url_real CHECK (
deployment_url IS NULL
OR (
deployment_url ~ '^https://[^/?#]+'
-- Loopback and unspecified addresses, plus bare/subdomain `localhost`.
AND deployment_url !~* '^https://([^/?#@]*\.)?localhost(:[0-9]+)?([/?#]|$)'
AND deployment_url !~* '^https://127\.0\.0\.1(:[0-9]+)?([/?#]|$)'
AND deployment_url !~* '^https://0\.0\.0\.0(:[0-9]+)?([/?#]|$)'
AND deployment_url !~* '^https://\[::1\](:[0-9]+)?([/?#]|$)'
-- RFC 2606 reserved documentation domains.
AND deployment_url !~* '^https://([^/?#@]*\.)?example\.(com|org|net)(:[0-9]+)?([/?#]|$)'
)
);
52 changes: 52 additions & 0 deletions apps/web/drizzle/0003_run_blocked_from.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
-- Resumption metadata for blocked and failed runs.
--
-- `delivery_runs` recorded *that* a run blocked (`blocked_reason`) but not
-- *where* it stopped, so a blocked run could not be resumed from the failing
-- phase — the operator had to guess, or restart the whole pipeline and redo
-- work that had already succeeded. The lifecycle model in
-- `src/lib/delivery-lifecycle.ts` has always carried `blockedFrom` and `error`;
-- this migration makes the table match the contract the code already assumes.
--
-- Both columns are nullable with no default: they are meaningful only for
-- blocked/failed runs, and back-filling a value for historical rows would
-- invent a phase that was never observed.

ALTER TABLE delivery_runs
ADD COLUMN IF NOT EXISTS blocked_from text;

ALTER TABLE delivery_runs
ADD COLUMN IF NOT EXISTS error text;

-- A blocked run must say where it stopped, mirroring the existing
-- `delivery_runs_blocked_requires_reason` constraint. Together they guarantee a
-- blocked run is always fully diagnosable: it has both a reason and an origin.
--
-- Scoped to rows created from here on: pre-existing blocked rows predate the
-- column and cannot retroactively know their origin phase, so validating them
-- would fail on data the application never had the chance to populate.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'delivery_runs_blocked_requires_origin'
) THEN
ALTER TABLE delivery_runs ADD CONSTRAINT delivery_runs_blocked_requires_origin
CHECK (status <> 'blocked' OR blocked_from IS NOT NULL) NOT VALID;
END IF;
END $$;

-- `blocked_from` must name a real phase; a typo would silently break resumption.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'delivery_runs_blocked_from_valid'
) THEN
ALTER TABLE delivery_runs ADD CONSTRAINT delivery_runs_blocked_from_valid
CHECK (
blocked_from IS NULL
OR blocked_from IN (
'sourcing', 'requirements', 'planning', 'awaiting_approval',
'building', 'verifying', 'deploying'
)
) NOT VALID;
END IF;
END $$;
32 changes: 32 additions & 0 deletions apps/web/drizzle/0004_embeddings.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
-- Transcript embedding storage.
--
-- Replaces `data/embeddings/<videoId>.json` under `process.cwd()` — the last
-- remaining instance of audit finding F4. On Vercel that path is a read-only
-- bundle, so `saveEmbeddings()` threw EROFS on every pipeline run, and
-- `loadEmbeddings()` caught the resulting ENOENT and returned `null`. The
-- semantic search endpoint then reported "no embeddings for this video"
-- instead of "embeddings could never be stored": a silent, permanent
-- degradation that looked like an empty index.
--
-- One row per video rather than one per chunk. Chunks are always read and
-- written as a complete set for a single video (the search endpoint scores all
-- of them in process), so per-chunk rows would add join cost and a partial-write
-- failure mode for no gain. If similarity search moves into the database with
-- pgvector, that is a per-chunk table and a deliberate migration, not a
-- silent reshaping of this one.

CREATE TABLE IF NOT EXISTS video_embeddings (
video_id text PRIMARY KEY,
-- ChunkEmbedding[]: { start, duration, text, embedding[] }
chunks jsonb NOT NULL,
chunk_count integer NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);

-- An empty chunk set is not a saved index. Storing one would recreate the
-- exact ambiguity this table exists to remove: a present-but-useless row
-- reading as a successful save.
DO $$ BEGIN
ALTER TABLE video_embeddings ADD CONSTRAINT video_embeddings_non_empty
CHECK (chunk_count > 0 AND jsonb_array_length(chunks) = chunk_count);
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
Loading
Loading