made tiny updates
This commit is contained in:
@@ -1,46 +1,59 @@
|
||||
-- Origin Graph — Artboard Ancestry Materialized View
|
||||
-- Migration: 002
|
||||
-- Pre-computes all ancestor/descendant relationships so the app never needs
|
||||
-- recursive CTEs at query time. Updated automatically on artboards INSERT.
|
||||
-- ── Migration 002: Artboard Ancestry Guard ───────────────────────────────────
|
||||
-- The artboard_ancestry closure table and its maintenance trigger are already
|
||||
-- created in migration 001 (initial schema). This migration is a safe, idempotent
|
||||
-- guard that ensures the trigger function and trigger exist as expected.
|
||||
--
|
||||
-- IMPORTANT: An earlier draft of this file attempted to create a MATERIALIZED
|
||||
-- VIEW named artboard_ancestry, which would conflict with the table created in
|
||||
-- 001. That approach has been superseded: the trigger-based closure table from
|
||||
-- 001 is maintained incrementally (O(depth) per INSERT) which is far cheaper
|
||||
-- than a full REFRESH MATERIALIZED VIEW CONCURRENTLY on every artboard insert.
|
||||
-- Do NOT re-introduce the materialized view approach.
|
||||
|
||||
CREATE MATERIALIZED VIEW artboard_ancestry AS
|
||||
WITH RECURSIVE ancestry(artboard_id, ancestor_id, depth) AS (
|
||||
-- Base: each artboard is at depth 0 relative to itself
|
||||
SELECT id AS artboard_id, id AS ancestor_id, 0 AS depth
|
||||
FROM artboards
|
||||
-- ── Ensure the ancestry maintenance trigger function exists ───────────────────
|
||||
-- Uses CREATE OR REPLACE so the migration is idempotent; safe to re-run.
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Recurse: walk up the parent chain
|
||||
SELECT a.id AS artboard_id, anc.ancestor_id, anc.depth + 1
|
||||
FROM artboards a
|
||||
JOIN ancestry anc ON a.parent_artboard_id = anc.artboard_id
|
||||
)
|
||||
SELECT artboard_id, ancestor_id, depth
|
||||
FROM ancestry
|
||||
WHERE artboard_id <> ancestor_id -- exclude self-reference
|
||||
ORDER BY artboard_id, depth;
|
||||
|
||||
CREATE UNIQUE INDEX artboard_ancestry_pk
|
||||
ON artboard_ancestry(artboard_id, ancestor_id);
|
||||
|
||||
CREATE INDEX artboard_ancestry_ancestor_idx
|
||||
ON artboard_ancestry(ancestor_id);
|
||||
|
||||
-- ── Refresh trigger ───────────────────────────────────────────────────────────
|
||||
-- Refreshes the materialized view concurrently whenever an artboard is
|
||||
-- inserted. CONCURRENTLY requires the unique index above — it allows reads
|
||||
-- to continue during refresh.
|
||||
|
||||
CREATE OR REPLACE FUNCTION refresh_artboard_ancestry()
|
||||
CREATE OR REPLACE FUNCTION maintain_artboard_ancestry()
|
||||
RETURNS TRIGGER LANGUAGE plpgsql AS $$
|
||||
BEGIN
|
||||
REFRESH MATERIALIZED VIEW CONCURRENTLY artboard_ancestry;
|
||||
RETURN NULL;
|
||||
-- Self-row: every artboard is its own ancestor at depth 0.
|
||||
INSERT INTO artboard_ancestry (artboard_id, ancestor_id, depth)
|
||||
VALUES (NEW.id, NEW.id, 0)
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Inherit all ancestors of the parent at depth + 1.
|
||||
IF NEW.parent_artboard_id IS NOT NULL THEN
|
||||
INSERT INTO artboard_ancestry (artboard_id, ancestor_id, depth)
|
||||
SELECT NEW.id, ancestor_id, depth + 1
|
||||
FROM artboard_ancestry
|
||||
WHERE artboard_id = NEW.parent_artboard_id
|
||||
ON CONFLICT DO NOTHING;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TRIGGER trg_artboard_ancestry_refresh
|
||||
AFTER INSERT OR UPDATE OF parent_artboard_id ON artboards
|
||||
FOR EACH STATEMENT
|
||||
EXECUTE FUNCTION refresh_artboard_ancestry();
|
||||
-- ── Ensure the trigger is attached ───────────────────────────────────────────
|
||||
-- DROP + recreate is the idempotent way to ensure the trigger is attached
|
||||
-- exactly once; IF NOT EXISTS for triggers requires PG 17+ so use this pattern.
|
||||
|
||||
DROP TRIGGER IF EXISTS artboards_ancestry_insert ON artboards;
|
||||
|
||||
CREATE TRIGGER artboards_ancestry_insert
|
||||
AFTER INSERT ON artboards
|
||||
FOR EACH ROW EXECUTE FUNCTION maintain_artboard_ancestry();
|
||||
|
||||
-- ── RPC helper: get all descendants of an artboard ───────────────────────────
|
||||
-- Complements getArtboardAncestors() in origin-graph/queries.ts.
|
||||
-- Returns direct + transitive descendants, ordered nearest-first.
|
||||
|
||||
CREATE OR REPLACE FUNCTION get_artboard_descendants(p_artboard_id UUID)
|
||||
RETURNS TABLE (artboard_id UUID, depth INTEGER)
|
||||
LANGUAGE SQL STABLE AS $$
|
||||
SELECT artboard_id, depth
|
||||
FROM artboard_ancestry
|
||||
WHERE ancestor_id = p_artboard_id
|
||||
AND artboard_id <> p_artboard_id -- exclude self
|
||||
ORDER BY depth, artboard_id;
|
||||
$$;
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
-- ── Migration 006: Full-Text Search ──────────────────────────────────────────
|
||||
-- Adds tsvector generated columns and GIN indexes to artboards and origins so
|
||||
-- the natural-language cross-artboard query feature (Layer 10 / Phase 3) can do
|
||||
-- weighted full-text search without re-scanning jsonb at query time.
|
||||
--
|
||||
-- search_artboards(query, workspace_id) is the primary search entry point.
|
||||
-- It returns artboard rows ranked by text relevance using ts_rank_cd.
|
||||
|
||||
-- ── 1. Generated tsvector column on artboards ─────────────────────────────────
|
||||
-- Covers: artboard name (weight A), route / renderUrl from metadata (weight B),
|
||||
-- and the raw metadata_jsonb text (weight C).
|
||||
|
||||
ALTER TABLE artboards
|
||||
ADD COLUMN IF NOT EXISTS search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('english', coalesce(name, '')), 'A') ||
|
||||
setweight(to_tsvector('english', coalesce(metadata_jsonb::text, '')), 'C')
|
||||
) STORED;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS artboards_search_vector_idx
|
||||
ON artboards USING GIN (search_vector);
|
||||
|
||||
-- ── 2. Generated tsvector column on origins ───────────────────────────────────
|
||||
-- Covers: source_ref (weight A), source_metadata_jsonb text (weight B).
|
||||
|
||||
ALTER TABLE origins
|
||||
ADD COLUMN IF NOT EXISTS search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('english', coalesce(source_ref, '')), 'A') ||
|
||||
setweight(to_tsvector('english', coalesce(source_metadata_jsonb::text, '')), 'B')
|
||||
) STORED;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS origins_search_vector_idx
|
||||
ON origins USING GIN (search_vector);
|
||||
|
||||
-- ── 3. Generated tsvector on intent_diffs ────────────────────────────────────
|
||||
-- Covers: summary (weight A), changes_jsonb text (weight C).
|
||||
-- Allows searching "show me every diff about the navigation bar" style queries.
|
||||
|
||||
ALTER TABLE intent_diffs
|
||||
ADD COLUMN IF NOT EXISTS search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('english', coalesce(summary, '')), 'A') ||
|
||||
setweight(to_tsvector('english', coalesce(changes_jsonb::text, '')), 'C')
|
||||
) STORED;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS intent_diffs_search_vector_idx
|
||||
ON intent_diffs USING GIN (search_vector);
|
||||
|
||||
-- ── 4. search_artboards RPC ───────────────────────────────────────────────────
|
||||
-- Parameters:
|
||||
-- p_workspace_id — scope results to a single workspace
|
||||
-- p_query — plain-text search string (converted to tsquery internally)
|
||||
-- p_limit — max rows to return (default 20)
|
||||
--
|
||||
-- Returns artboards ranked by text relevance. Joins to origins and intent_diffs
|
||||
-- to boost matches found in provenance or diff history.
|
||||
--
|
||||
-- Plain-to-tsquery converts arbitrary user text into a safe tsquery, allowing
|
||||
-- partial words and multi-word phrases without syntax errors.
|
||||
|
||||
CREATE OR REPLACE FUNCTION search_artboards(
|
||||
p_workspace_id UUID,
|
||||
p_query TEXT,
|
||||
p_limit INTEGER DEFAULT 20
|
||||
)
|
||||
RETURNS TABLE (
|
||||
id UUID,
|
||||
workspace_id UUID,
|
||||
project_id UUID,
|
||||
name TEXT,
|
||||
origin_id UUID,
|
||||
parent_artboard_id UUID,
|
||||
metadata_jsonb JSONB,
|
||||
created_at TIMESTAMPTZ,
|
||||
updated_at TIMESTAMPTZ,
|
||||
rank FLOAT4
|
||||
)
|
||||
LANGUAGE SQL STABLE AS $$
|
||||
WITH q AS (
|
||||
SELECT plainto_tsquery('english', p_query) AS tsq
|
||||
)
|
||||
SELECT
|
||||
a.id,
|
||||
a.workspace_id,
|
||||
a.project_id,
|
||||
a.name,
|
||||
a.origin_id,
|
||||
a.parent_artboard_id,
|
||||
a.metadata_jsonb,
|
||||
a.created_at,
|
||||
a.updated_at,
|
||||
-- Combine artboard rank with a bonus for matching origins or diffs
|
||||
(
|
||||
ts_rank_cd(a.search_vector, q.tsq, 32) * 1.5 +
|
||||
coalesce((
|
||||
SELECT max(ts_rank_cd(d.search_vector, q.tsq, 32))
|
||||
FROM intent_diffs d
|
||||
WHERE d.artboard_id = a.id
|
||||
AND d.search_vector @@ q.tsq
|
||||
), 0) +
|
||||
coalesce((
|
||||
SELECT ts_rank_cd(o.search_vector, q.tsq, 32)
|
||||
FROM origins o
|
||||
WHERE o.id = a.origin_id
|
||||
AND o.search_vector @@ q.tsq
|
||||
), 0)
|
||||
) AS rank
|
||||
FROM artboards a, q
|
||||
WHERE
|
||||
a.workspace_id = p_workspace_id
|
||||
AND (
|
||||
a.search_vector @@ q.tsq
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM intent_diffs d
|
||||
WHERE d.artboard_id = a.id
|
||||
AND d.search_vector @@ q.tsq
|
||||
)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM origins o
|
||||
WHERE o.id = a.origin_id
|
||||
AND o.search_vector @@ q.tsq
|
||||
)
|
||||
)
|
||||
ORDER BY rank DESC, a.updated_at DESC
|
||||
LIMIT p_limit;
|
||||
$$;
|
||||
@@ -0,0 +1,111 @@
|
||||
-- ── Migration 007: Spec Compliance ───────────────────────────────────────────
|
||||
-- Adds the columns and constraint values that were specified in the Layer 4-5
|
||||
-- design but omitted from migration 001. All additions are additive (nullable
|
||||
-- columns, expanded CHECK constraint value sets) so existing rows remain valid.
|
||||
--
|
||||
-- Tables touched:
|
||||
-- origins — source_id, source_url, screenshot_url
|
||||
-- intent_diffs — session_id, before_screenshot, after_screenshot,
|
||||
-- exported_code + expanded status CHECK
|
||||
-- design_language_files — is_active, created_by
|
||||
-- artboards — route, remote_url, width, height, created_by
|
||||
-- + expanded origin type CHECK via origins table
|
||||
|
||||
-- ── 1. origins — provenance fields ───────────────────────────────────────────
|
||||
-- The spec required source_id (e.g. Linear issue ID, Git SHA, Slack message TS),
|
||||
-- a canonical source_url, and a screenshot_url for the origin artifact.
|
||||
-- All nullable — not every origin type has all three fields.
|
||||
|
||||
ALTER TABLE origins
|
||||
ADD COLUMN IF NOT EXISTS source_id text,
|
||||
ADD COLUMN IF NOT EXISTS source_url text,
|
||||
ADD COLUMN IF NOT EXISTS screenshot_url text;
|
||||
|
||||
-- Expand the origin type CHECK to include the spec's lowercase values alongside
|
||||
-- the existing uppercase ones. PostgreSQL CHECK is re-evaluated on INSERT/UPDATE
|
||||
-- only, so existing rows are unaffected.
|
||||
ALTER TABLE origins
|
||||
DROP CONSTRAINT IF EXISTS origins_type_check;
|
||||
|
||||
ALTER TABLE origins
|
||||
ADD CONSTRAINT origins_type_check
|
||||
CHECK (type IN (
|
||||
-- Original uppercase set
|
||||
'GIT_COMMIT', 'LINEAR_ISSUE', 'SLACK_MESSAGE', 'URL', 'FORK',
|
||||
-- Spec Layer 4 lowercase set
|
||||
'route', 'linear', 'git', 'slack', 'feedback', 'fork', 'manual',
|
||||
-- Canonical aliases
|
||||
'ROUTE', 'FEEDBACK', 'MANUAL'
|
||||
));
|
||||
|
||||
-- ── 2. intent_diffs — lifecycle fields ───────────────────────────────────────
|
||||
-- session_id links a diff back to the agent session that produced it.
|
||||
-- before/after screenshots enable the visual diff view in the export panel.
|
||||
-- exported_code stores the generated TypeScript/JSX expressing the changes.
|
||||
|
||||
ALTER TABLE intent_diffs
|
||||
ADD COLUMN IF NOT EXISTS session_id text,
|
||||
ADD COLUMN IF NOT EXISTS before_screenshot text,
|
||||
ADD COLUMN IF NOT EXISTS after_screenshot text,
|
||||
ADD COLUMN IF NOT EXISTS exported_code text;
|
||||
|
||||
-- Expand the status CHECK to include all values used by the spec and the UI.
|
||||
-- Original: DRAFT, EXPORTED, IMPLEMENTED, BLOCKED
|
||||
-- Spec adds: acknowledged, rejected (lowercase in spec, uppercase in app)
|
||||
-- App uses: REVIEWED, APPLIED (from Inspector STATUS_COLOR map)
|
||||
ALTER TABLE intent_diffs
|
||||
DROP CONSTRAINT IF EXISTS intent_diffs_status_check;
|
||||
|
||||
ALTER TABLE intent_diffs
|
||||
ADD CONSTRAINT intent_diffs_status_check
|
||||
CHECK (status IN (
|
||||
'DRAFT', 'EXPORTED', 'IMPLEMENTED', 'BLOCKED',
|
||||
'ACKNOWLEDGED', 'REJECTED',
|
||||
'REVIEWED', 'APPLIED'
|
||||
));
|
||||
|
||||
-- Update the get_diffs_by_status RPC to accept the full value set.
|
||||
-- The function body is unchanged; only the type comment is refreshed.
|
||||
CREATE OR REPLACE FUNCTION get_diffs_by_status(p_workspace_id uuid, p_status text)
|
||||
RETURNS SETOF intent_diffs
|
||||
LANGUAGE SQL STABLE AS $$
|
||||
SELECT d.*
|
||||
FROM intent_diffs d
|
||||
JOIN artboards a ON a.id = d.artboard_id
|
||||
WHERE a.workspace_id = p_workspace_id
|
||||
AND d.status = p_status
|
||||
ORDER BY d.created_at DESC;
|
||||
$$;
|
||||
|
||||
-- ── 3. design_language_files — activation + ownership ────────────────────────
|
||||
-- is_active provides the spec's boolean gate for "is this DLF in effect?"
|
||||
-- The existing schema uses version + max(version) query for the active file;
|
||||
-- is_active supplements that pattern and also enables instant deactivation
|
||||
-- without incrementing the version.
|
||||
-- created_by stores the Clerk user ID of the uploader (audit trail).
|
||||
|
||||
ALTER TABLE design_language_files
|
||||
ADD COLUMN IF NOT EXISTS is_active boolean NOT NULL DEFAULT true,
|
||||
ADD COLUMN IF NOT EXISTS created_by text;
|
||||
|
||||
-- Index for the "get active DLF for workspace" query pattern.
|
||||
CREATE INDEX IF NOT EXISTS design_language_files_is_active_idx
|
||||
ON design_language_files (workspace_id, is_active, version DESC);
|
||||
|
||||
-- ── 4. artboards — explicit dimensional + provenance columns ─────────────────
|
||||
-- The spec modelled these as first-class columns. The existing schema stores
|
||||
-- them in metadata_jsonb instead. Adding as nullable explicit columns lets both
|
||||
-- patterns coexist: old rows keep metadata_jsonb, new rows can set either/both.
|
||||
-- No NOT NULL constraints so zero migration cost for existing rows.
|
||||
|
||||
ALTER TABLE artboards
|
||||
ADD COLUMN IF NOT EXISTS route text,
|
||||
ADD COLUMN IF NOT EXISTS remote_url text,
|
||||
ADD COLUMN IF NOT EXISTS width integer,
|
||||
ADD COLUMN IF NOT EXISTS height integer,
|
||||
ADD COLUMN IF NOT EXISTS created_by text;
|
||||
|
||||
-- Partial index: quickly find all artboards with an explicit route set.
|
||||
CREATE INDEX IF NOT EXISTS artboards_route_idx
|
||||
ON artboards (workspace_id, route)
|
||||
WHERE route IS NOT NULL;
|
||||
@@ -0,0 +1,197 @@
|
||||
-- ── Migration 008: Spec schema alignment ──────────────────────────────────────
|
||||
-- Aligns the database schema with the Layer 4 spec requirements.
|
||||
-- All changes are additive or rename-only; no data is destroyed.
|
||||
--
|
||||
-- Changes:
|
||||
-- 1. artboards.width / height — add NOT NULL DEFAULT per spec (1440 / 900)
|
||||
-- 2. artboards.parent_id — spec names this column parent_id (not parent_artboard_id);
|
||||
-- add parent_id as a generated column alias
|
||||
-- 3. origins.artboard_id — spec requires the FK to live on origins (not artboards)
|
||||
-- 4. intent_diffs: rename summary → aggregate_summary + rename changes_jsonb → changes
|
||||
-- 5. intent_diffs.session_id — make NOT NULL per spec (was nullable in migration 007)
|
||||
-- 6. agent_sessions.agent_type — add lowercase check values per spec
|
||||
-- 7. RLS: add permissive workspace-membership allow policies
|
||||
|
||||
-- ── 1. artboards width/height defaults ───────────────────────────────────────
|
||||
-- Backfill nulls introduced by migration 007, then add NOT NULL + DEFAULT.
|
||||
|
||||
update artboards set width = 1440 where width is null;
|
||||
update artboards set height = 900 where height is null;
|
||||
|
||||
alter table artboards
|
||||
alter column width set not null,
|
||||
alter column width set default 1440,
|
||||
alter column height set not null,
|
||||
alter column height set default 900;
|
||||
|
||||
-- created_by: spec says NOT NULL; backfill with '' then constrain
|
||||
update artboards set created_by = '' where created_by is null;
|
||||
alter table artboards alter column created_by set not null;
|
||||
alter table artboards alter column created_by set default '';
|
||||
|
||||
-- ── 2. artboards.parent_id alias ─────────────────────────────────────────────
|
||||
-- Spec uses parent_id; existing code uses parent_artboard_id. Add parent_id as
|
||||
-- a nullable FK that mirrors parent_artboard_id for spec-compliant consumers.
|
||||
-- Both columns will co-exist during the transition.
|
||||
do $$ begin
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_name = 'artboards' and column_name = 'parent_id'
|
||||
) then
|
||||
alter table artboards add column parent_id uuid references artboards(id);
|
||||
-- Backfill from the existing column
|
||||
update artboards set parent_id = parent_artboard_id where parent_artboard_id is not null;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- ── 3. origins.artboard_id (spec FK direction) ───────────────────────────────
|
||||
-- Spec: origins.artboard_id uuid not null references artboards(id) on delete cascade
|
||||
-- Existing: artboards.origin_id (reverse FK).
|
||||
-- We add the spec-required column as nullable (existing origin rows have no artboard_id
|
||||
-- until they are re-linked). New origins created via spec-compliant code must set it.
|
||||
do $$ begin
|
||||
if not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_name = 'origins' and column_name = 'artboard_id'
|
||||
) then
|
||||
alter table origins add column artboard_id uuid references artboards(id) on delete cascade;
|
||||
-- Best-effort backfill: find artboards pointing to each origin
|
||||
update origins o
|
||||
set artboard_id = a.id
|
||||
from artboards a
|
||||
where a.origin_id = o.id;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- ── 4. intent_diffs: rename columns to spec names ────────────────────────────
|
||||
|
||||
-- Rename changes_jsonb → changes (spec column name)
|
||||
do $$ begin
|
||||
if exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_name = 'intent_diffs' and column_name = 'changes_jsonb'
|
||||
) and not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_name = 'intent_diffs' and column_name = 'changes'
|
||||
) then
|
||||
alter table intent_diffs rename column changes_jsonb to changes;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- Rename summary → aggregate_summary (spec column name)
|
||||
do $$ begin
|
||||
if exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_name = 'intent_diffs' and column_name = 'summary'
|
||||
) and not exists (
|
||||
select 1 from information_schema.columns
|
||||
where table_name = 'intent_diffs' and column_name = 'aggregate_summary'
|
||||
) then
|
||||
alter table intent_diffs rename column summary to aggregate_summary;
|
||||
end if;
|
||||
end $$;
|
||||
|
||||
-- ── 5. intent_diffs.session_id NOT NULL ──────────────────────────────────────
|
||||
-- Spec: session_id text not null. Backfill existing nulls, then constrain.
|
||||
update intent_diffs set session_id = '' where session_id is null;
|
||||
alter table intent_diffs alter column session_id set not null;
|
||||
alter table intent_diffs alter column session_id set default '';
|
||||
|
||||
-- Also: author_id was NOT NULL in the spec from migration 001 — ensure created_by
|
||||
-- exists on design_language_files as NOT NULL (backfill then constrain)
|
||||
update design_language_files set created_by = '' where created_by is null;
|
||||
alter table design_language_files alter column created_by set not null;
|
||||
alter table design_language_files alter column created_by set default '';
|
||||
|
||||
-- ── 6. agent_sessions: expand agent_type to include spec lowercase values ─────
|
||||
-- Spec: 'cursor', 'claude-code', 'generic' (lowercase, hyphenated)
|
||||
-- Existing: uppercase 'CURSOR', 'CLAUDE_CODE', 'GENERIC' + original set
|
||||
-- Drop and re-create the check constraint to include both sets.
|
||||
alter table agent_sessions drop constraint if exists agent_sessions_agent_type_check;
|
||||
alter table agent_sessions add constraint agent_sessions_agent_type_check
|
||||
check (agent_type in (
|
||||
'cursor', 'claude-code', 'generic', -- spec lowercase
|
||||
'CURSOR', 'CLAUDE_CODE', 'GENERIC' -- existing uppercase
|
||||
));
|
||||
|
||||
-- ── 7. RLS workspace-membership allow policies ────────────────────────────────
|
||||
-- Spec: "workspace members can access" via auth.uid()::text = owner_id
|
||||
-- These are the permissive policies required by Layer 4. The deny-all-anon
|
||||
-- policies from migration 003 remain and complement these.
|
||||
|
||||
-- workspaces: owner can access their own workspace
|
||||
drop policy if exists "workspace owner access" on workspaces;
|
||||
create policy "workspace owner access" on workspaces
|
||||
for all
|
||||
using (owner_id = auth.uid()::text);
|
||||
|
||||
-- artboards: members of the workspace can access
|
||||
drop policy if exists "workspace member artboard access" on artboards;
|
||||
create policy "workspace member artboard access" on artboards
|
||||
for all
|
||||
using (
|
||||
workspace_id in (
|
||||
select id from workspaces where owner_id = auth.uid()::text
|
||||
)
|
||||
or workspace_id in (
|
||||
select workspace_id from team_members where user_id = auth.uid()::text
|
||||
)
|
||||
);
|
||||
|
||||
-- origins: accessible when the linked artboard is accessible
|
||||
drop policy if exists "workspace member origin access" on origins;
|
||||
create policy "workspace member origin access" on origins
|
||||
for all
|
||||
using (
|
||||
id in (
|
||||
select origin_id from artboards
|
||||
where workspace_id in (
|
||||
select id from workspaces where owner_id = auth.uid()::text
|
||||
union
|
||||
select workspace_id from team_members where user_id = auth.uid()::text
|
||||
)
|
||||
and origin_id is not null
|
||||
)
|
||||
);
|
||||
|
||||
-- intent_diffs: accessible when the linked artboard is accessible
|
||||
drop policy if exists "workspace member diff access" on intent_diffs;
|
||||
create policy "workspace member diff access" on intent_diffs
|
||||
for all
|
||||
using (
|
||||
artboard_id in (
|
||||
select id from artboards
|
||||
where workspace_id in (
|
||||
select id from workspaces where owner_id = auth.uid()::text
|
||||
union
|
||||
select workspace_id from team_members where user_id = auth.uid()::text
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
-- design_language_files: accessible within the workspace
|
||||
drop policy if exists "workspace member dlf access" on design_language_files;
|
||||
create policy "workspace member dlf access" on design_language_files
|
||||
for all
|
||||
using (
|
||||
workspace_id in (
|
||||
select id from workspaces where owner_id = auth.uid()::text
|
||||
union
|
||||
select workspace_id from team_members where user_id = auth.uid()::text
|
||||
)
|
||||
);
|
||||
|
||||
-- agent_sessions: accessible when the linked artboard is accessible
|
||||
drop policy if exists "workspace member session access" on agent_sessions;
|
||||
create policy "workspace member session access" on agent_sessions
|
||||
for all
|
||||
using (
|
||||
artboard_id in (
|
||||
select id from artboards
|
||||
where workspace_id in (
|
||||
select id from workspaces where owner_id = auth.uid()::text
|
||||
union
|
||||
select workspace_id from team_members where user_id = auth.uid()::text
|
||||
)
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,22 @@
|
||||
-- migration 009 — Add spec-canonical lowercase status values to intent_diffs CHECK
|
||||
-- Spec Layer 4: canonical status values are lowercase:
|
||||
-- 'draft', 'exported', 'acknowledged', 'implemented', 'rejected'
|
||||
-- The current CHECK (from migration 007) only allows uppercase values.
|
||||
-- New code (Inspector.tsx, CompletionZone.tsx) inserts lowercase 'draft'.
|
||||
-- This migration expands the CHECK to accept both cases so both old and new
|
||||
-- code work without a breaking schema change.
|
||||
|
||||
ALTER TABLE intent_diffs
|
||||
DROP CONSTRAINT IF EXISTS intent_diffs_status_check;
|
||||
|
||||
ALTER TABLE intent_diffs
|
||||
ADD CONSTRAINT intent_diffs_status_check
|
||||
CHECK (status IN (
|
||||
-- Spec-canonical lowercase (Layer 4)
|
||||
'draft', 'exported', 'acknowledged', 'implemented', 'rejected',
|
||||
-- Legacy uppercase (migration 001, migration 007) — kept for compat
|
||||
'DRAFT', 'EXPORTED', 'IMPLEMENTED', 'BLOCKED',
|
||||
'ACKNOWLEDGED', 'REJECTED',
|
||||
-- UI-driven additions (Inspector STATUS_COLOR map)
|
||||
'REVIEWED', 'APPLIED'
|
||||
));
|
||||
@@ -0,0 +1,53 @@
|
||||
-- ── Migration 010: Fix origins RLS policy ────────────────────────────────────
|
||||
--
|
||||
-- Problem (introduced in migration 008):
|
||||
-- The "workspace member origin access" policy only checks the OLD FK direction
|
||||
-- (artboards.origin_id → origins.id). Migration 008 added the SPEC-required FK
|
||||
-- column origins.artboard_id, but the policy was not updated. Any origin created
|
||||
-- via spec-compliant code (setting origins.artboard_id) is therefore invisible
|
||||
-- to authenticated users — the SELECT returns zero rows even though the insert
|
||||
-- succeeds.
|
||||
--
|
||||
-- Fix:
|
||||
-- Rewrite the policy to accept either FK direction:
|
||||
-- 1. NEW (spec): origins.artboard_id is in the user's accessible artboard set.
|
||||
-- 2. OLD (legacy): origins.id appears in artboards.origin_id for accessible boards.
|
||||
-- The OR allows both legacy data and spec-compliant data to be readable without
|
||||
-- requiring a destructive data migration.
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- Helper CTE: IDs of all artboards accessible to the calling user.
|
||||
-- Referenced by both branches of the OR below.
|
||||
drop policy if exists "workspace member origin access" on origins;
|
||||
|
||||
create policy "workspace member origin access" on origins
|
||||
for all
|
||||
using (
|
||||
-- ── Branch 1: spec-compliant origins (origins.artboard_id FK) ───────────
|
||||
-- New origins set origins.artboard_id = <artboard uuid>; the origin is
|
||||
-- accessible iff that artboard is in the user's workspace.
|
||||
artboard_id in (
|
||||
select id from artboards
|
||||
where workspace_id in (
|
||||
select id from workspaces where owner_id = auth.uid()::text
|
||||
union
|
||||
select workspace_id from team_members where user_id = auth.uid()::text
|
||||
)
|
||||
)
|
||||
|
||||
or
|
||||
|
||||
-- ── Branch 2: legacy origins (artboards.origin_id FK) ───────────────────
|
||||
-- Old origins are referenced from artboards via artboards.origin_id.
|
||||
-- Still need to work so existing data stays accessible while backfill
|
||||
-- populates origins.artboard_id (migration 008 best-effort backfill).
|
||||
id in (
|
||||
select origin_id from artboards
|
||||
where workspace_id in (
|
||||
select id from workspaces where owner_id = auth.uid()::text
|
||||
union
|
||||
select workspace_id from team_members where user_id = auth.uid()::text
|
||||
)
|
||||
and origin_id is not null
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,37 @@
|
||||
-- ── Migration 011: Add route column to artboards FTS tsvector ────────────────
|
||||
--
|
||||
-- Problem:
|
||||
-- Migration 006 created artboards.search_vector with only:
|
||||
-- name (weight A) || metadata_jsonb::text (weight C)
|
||||
-- The comment in 006 says route should be at weight B, but route didn't exist
|
||||
-- until migration 007. Generated column expressions cannot be altered in-place
|
||||
-- (pre-PG17) — the column must be dropped and re-added.
|
||||
--
|
||||
-- Fix:
|
||||
-- Drop and recreate artboards.search_vector to include:
|
||||
-- name (weight A) — exact component/screen name matches rank highest
|
||||
-- route (weight B) — URL route matches rank highly (added by migration 007)
|
||||
-- metadata_jsonb (weight C) — catch-all metadata text
|
||||
--
|
||||
-- Drop/re-add regenerates values for all existing rows automatically since
|
||||
-- the column is GENERATED ALWAYS AS (stored computed column).
|
||||
-- ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
-- Drop the existing generated column (and its GIN index, which depends on it)
|
||||
DROP INDEX IF EXISTS artboards_search_vector_idx;
|
||||
|
||||
ALTER TABLE artboards
|
||||
DROP COLUMN IF EXISTS search_vector;
|
||||
|
||||
-- Re-add with route at weight B
|
||||
ALTER TABLE artboards
|
||||
ADD COLUMN search_vector tsvector
|
||||
GENERATED ALWAYS AS (
|
||||
setweight(to_tsvector('english', coalesce(name, '')), 'A') ||
|
||||
setweight(to_tsvector('english', coalesce(route, '')), 'B') ||
|
||||
setweight(to_tsvector('english', coalesce(metadata_jsonb::text, '')), 'C')
|
||||
) STORED;
|
||||
|
||||
-- Rebuild the GIN index on the updated column
|
||||
CREATE INDEX artboards_search_vector_idx
|
||||
ON artboards USING GIN (search_vector);
|
||||
Reference in New Issue
Block a user