made tiny updates
This commit is contained in:
@@ -160,16 +160,16 @@ describe('IntentDiffSchema', () => {
|
||||
id: UUID,
|
||||
artboard_id: UUID,
|
||||
author_id: 'user_xyz',
|
||||
changes_jsonb: { propChanges: [], styleChanges: [] },
|
||||
summary: 'Updated button variant',
|
||||
status: 'DRAFT',
|
||||
changes: { propChanges: [], styleChanges: [] },
|
||||
aggregate_summary: 'Updated button variant',
|
||||
status: 'draft',
|
||||
created_at: NOW,
|
||||
updated_at: NOW,
|
||||
};
|
||||
|
||||
it('parses a valid diff', () => {
|
||||
const d = IntentDiffSchema.parse(valid);
|
||||
expect(d.status).toBe('DRAFT');
|
||||
expect(d.status).toBe('draft');
|
||||
expect(d.notes).toBeUndefined();
|
||||
});
|
||||
|
||||
|
||||
@@ -12,11 +12,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@originmain/diff-engine": "workspace:*",
|
||||
"@supabase/supabase-js": "^2.104.1",
|
||||
"zod": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^2.1.9",
|
||||
"typescript": "^5.5.0",
|
||||
"vitest": "^2.0.0",
|
||||
"@vitest/coverage-v8": "^2.0.0"
|
||||
"vitest": "^2.1.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
// ── Origin Graph client (spec Layer 4.2) ─────────────────────────────────────
|
||||
// Thin typed wrapper around @supabase/supabase-js for the Origin Graph schema.
|
||||
// The spec requires this file as the primary entry point for Supabase access
|
||||
// in the origin-graph package.
|
||||
//
|
||||
// Usage in the app layer:
|
||||
// import { createOriginGraphClient } from '@originmain/origin-graph';
|
||||
// const db = createOriginGraphClient(supabaseUrl, supabaseKey);
|
||||
//
|
||||
// The returned client satisfies the `DbClient` interface used by all
|
||||
// query functions in queries.ts — no type casting needed.
|
||||
|
||||
import { createClient } from '@supabase/supabase-js';
|
||||
import type { DbClient } from './queries.js';
|
||||
|
||||
/**
|
||||
* Creates a typed Supabase client configured for the Origin Graph schema.
|
||||
* Pass `supabaseUrl` + `anonKey` for client-side usage (RLS enforced),
|
||||
* or `supabaseUrl` + `serviceRoleKey` for server-side usage (bypasses RLS).
|
||||
*/
|
||||
export function createOriginGraphClient(
|
||||
supabaseUrl: string,
|
||||
supabaseKey: string,
|
||||
options?: { auth?: { autoRefreshToken?: boolean; persistSession?: boolean } },
|
||||
): DbClient {
|
||||
return createClient(supabaseUrl, supabaseKey, options) as unknown as DbClient;
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './types.js';
|
||||
export * from './queries.js';
|
||||
export * from './client.js';
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
InsertAgentSession,
|
||||
InsertTeamMember,
|
||||
InsertProject,
|
||||
InsertWorkspace,
|
||||
DiffStatus,
|
||||
ArtboardAncestry,
|
||||
} from './types.js';
|
||||
@@ -73,6 +74,17 @@ export async function getArtboards(
|
||||
return data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Spec Layer 4.2 canonical function name — delegates to getArtboards.
|
||||
* Callers that use the spec-mandated name get the same result.
|
||||
*/
|
||||
export async function getArtboardsByWorkspace(
|
||||
db: DbClient,
|
||||
workspaceId: string,
|
||||
): Promise<Artboard[]> {
|
||||
return getArtboards(db, workspaceId);
|
||||
}
|
||||
|
||||
export async function getArtboard(db: DbClient, id: string): Promise<Artboard> {
|
||||
const { data, error } = await (db
|
||||
.from('artboards')
|
||||
@@ -126,6 +138,31 @@ export async function getArtboardAncestors(db: DbClient, artboardId: string): Pr
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Full-text artboard search (migration 006) ─────────────────────────────────
|
||||
// Calls the search_artboards RPC which ranks artboards by relevance across name,
|
||||
// metadata_jsonb, linked intent diff summaries, and origin source_ref.
|
||||
// Returns up to `limit` results ordered by rank DESC.
|
||||
|
||||
export interface ArtboardSearchResult extends Artboard {
|
||||
rank: number;
|
||||
}
|
||||
|
||||
export async function searchArtboards(
|
||||
db: DbClient,
|
||||
workspaceId: string,
|
||||
query: string,
|
||||
limit = 20,
|
||||
): Promise<ArtboardSearchResult[]> {
|
||||
if (!query.trim()) return [];
|
||||
const { data, error } = await (db.rpc('search_artboards', {
|
||||
p_workspace_id: workspaceId,
|
||||
p_query: query.trim(),
|
||||
p_limit: limit,
|
||||
}) as Promise<{ data: ArtboardSearchResult[]; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data ?? [];
|
||||
}
|
||||
|
||||
// ── Intent diff queries ───────────────────────────────────────────────────────
|
||||
|
||||
export async function getDiffs(db: DbClient, artboardId: string): Promise<IntentDiff[]> {
|
||||
@@ -201,6 +238,16 @@ export async function getActiveDesignLanguageFile(
|
||||
|
||||
// ── Origin queries ────────────────────────────────────────────────────────────
|
||||
|
||||
export async function getOrigin(db: DbClient, id: string): Promise<Origin> {
|
||||
const { data, error } = await (db
|
||||
.from('origins')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single() as Promise<{ data: Origin; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function createOrigin(db: DbClient, row: InsertOrigin): Promise<Origin> {
|
||||
const { data, error } = await (db
|
||||
.from('origins')
|
||||
@@ -211,6 +258,34 @@ export async function createOrigin(db: DbClient, row: InsertOrigin): Promise<Ori
|
||||
return data;
|
||||
}
|
||||
|
||||
// ── Origin Graph aggregate query ─────────────────────────────────────────────
|
||||
// Returns the complete provenance picture for a single artboard: the artboard
|
||||
// row, its linked origin (if any), and all intent diffs ever recorded against
|
||||
// it. This is the primary read path described in Layer 4.2 of the spec.
|
||||
|
||||
export async function getOriginGraph(
|
||||
db: DbClient,
|
||||
artboardId: string,
|
||||
): Promise<{ artboard: Artboard; origin: Origin | null; diffs: IntentDiff[] }> {
|
||||
// Fetch artboard and diffs in parallel for minimum latency.
|
||||
const [artboard, diffs] = await Promise.all([
|
||||
getArtboard(db, artboardId),
|
||||
getDiffs(db, artboardId),
|
||||
]);
|
||||
|
||||
let origin: Origin | null = null;
|
||||
if (artboard.origin_id) {
|
||||
try {
|
||||
origin = await getOrigin(db, artboard.origin_id);
|
||||
} catch {
|
||||
// Origin may have been deleted (ON DELETE SET NULL on the FK) — treat as
|
||||
// missing rather than throwing, since the artboard itself is valid.
|
||||
}
|
||||
}
|
||||
|
||||
return { artboard, origin, diffs };
|
||||
}
|
||||
|
||||
// ── Agent session queries ─────────────────────────────────────────────────────
|
||||
|
||||
export async function createAgentSession(db: DbClient, row: InsertAgentSession): Promise<AgentSession> {
|
||||
@@ -225,6 +300,16 @@ export async function createAgentSession(db: DbClient, row: InsertAgentSession):
|
||||
|
||||
// ── Workspace queries ─────────────────────────────────────────────────────────
|
||||
|
||||
export async function createWorkspace(db: DbClient, row: InsertWorkspace): Promise<Workspace> {
|
||||
const { data, error } = await (db
|
||||
.from('workspaces')
|
||||
.insert(row)
|
||||
.select()
|
||||
.single() as Promise<{ data: Workspace; error: DbError | null }>);
|
||||
if (error) throw new Error(error.message);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getWorkspace(db: DbClient, id: string): Promise<Workspace> {
|
||||
const { data, error } = await (db
|
||||
.from('workspaces')
|
||||
|
||||
@@ -3,122 +3,143 @@ import { z } from 'zod';
|
||||
// ── Enums ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export const OriginTypeSchema = z.enum([
|
||||
'GIT_COMMIT',
|
||||
'LINEAR_ISSUE',
|
||||
'SLACK_MESSAGE',
|
||||
'URL',
|
||||
'FORK',
|
||||
// Spec Layer 4 lowercase set (canonical)
|
||||
'route', 'linear', 'git', 'slack', 'feedback', 'fork', 'manual',
|
||||
// Legacy uppercase set (migration 001) — kept for backward compat
|
||||
'GIT_COMMIT', 'LINEAR_ISSUE', 'SLACK_MESSAGE', 'URL', 'FORK',
|
||||
// Canonical uppercase aliases (migration 007)
|
||||
'ROUTE', 'FEEDBACK', 'MANUAL',
|
||||
]);
|
||||
export type OriginType = z.infer<typeof OriginTypeSchema>;
|
||||
|
||||
export const DiffStatusSchema = z.enum([
|
||||
'DRAFT',
|
||||
'EXPORTED',
|
||||
'IMPLEMENTED',
|
||||
'BLOCKED',
|
||||
// Spec Layer 4 lowercase set (canonical)
|
||||
'draft', 'exported', 'acknowledged', 'implemented', 'rejected',
|
||||
// Legacy uppercase set — kept for backward compat
|
||||
'DRAFT', 'EXPORTED', 'IMPLEMENTED', 'BLOCKED',
|
||||
// UI-driven additions
|
||||
'ACKNOWLEDGED', 'REJECTED', 'REVIEWED', 'APPLIED',
|
||||
]);
|
||||
export type DiffStatus = z.infer<typeof DiffStatusSchema>;
|
||||
|
||||
export const TeamRoleSchema = z.enum([
|
||||
'OWNER',
|
||||
'DESIGNER',
|
||||
'ENGINEER',
|
||||
'PM',
|
||||
'VIEWER',
|
||||
]);
|
||||
export const TeamRoleSchema = z.enum(['OWNER', 'DESIGNER', 'ENGINEER', 'PM', 'VIEWER']);
|
||||
export type TeamRole = z.infer<typeof TeamRoleSchema>;
|
||||
|
||||
export const AgentTypeSchema = z.enum(['CURSOR', 'CLAUDE_CODE', 'GENERIC']);
|
||||
export const AgentTypeSchema = z.enum([
|
||||
// Spec lowercase (canonical)
|
||||
'cursor', 'claude-code', 'generic',
|
||||
// Legacy uppercase
|
||||
'CURSOR', 'CLAUDE_CODE', 'GENERIC',
|
||||
]);
|
||||
export type AgentType = z.infer<typeof AgentTypeSchema>;
|
||||
|
||||
export const WorkspacePlanSchema = z.enum(['FREE', 'TEAM', 'ENTERPRISE']);
|
||||
export type WorkspacePlan = z.infer<typeof WorkspacePlanSchema>;
|
||||
|
||||
// ── Row types (match PostgreSQL columns 1:1) ──────────────────────────────────
|
||||
// ── Row types (match PostgreSQL columns 1:1) ─────────────────────────────��────
|
||||
|
||||
export const WorkspaceSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
owner_id: z.string(),
|
||||
plan: WorkspacePlanSchema,
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
owner_id: z.string(),
|
||||
plan: WorkspacePlanSchema,
|
||||
settings_jsonb: z.record(z.unknown()),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type Workspace = z.infer<typeof WorkspaceSchema>;
|
||||
|
||||
export const ArtboardSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
workspace_id: z.string().uuid(),
|
||||
project_id: z.string().uuid().nullable(),
|
||||
name: z.string(),
|
||||
origin_id: z.string().uuid().nullable(),
|
||||
id: z.string().uuid(),
|
||||
workspace_id: z.string().uuid(),
|
||||
project_id: z.string().uuid().nullable(),
|
||||
name: z.string(),
|
||||
origin_id: z.string().uuid().nullable(),
|
||||
parent_artboard_id: z.string().uuid().nullable(),
|
||||
metadata_jsonb: z.record(z.unknown()),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
/** Spec alias added migration 008 — mirrors parent_artboard_id */
|
||||
parent_id: z.string().uuid().nullable().optional(),
|
||||
metadata_jsonb: z.record(z.unknown()),
|
||||
route: z.string().nullable().optional(),
|
||||
remote_url: z.string().nullable().optional(),
|
||||
/** Spec: NOT NULL DEFAULT 1440 (migration 008) */
|
||||
width: z.number().int().default(1440),
|
||||
/** Spec: NOT NULL DEFAULT 900 (migration 008) */
|
||||
height: z.number().int().default(900),
|
||||
created_by: z.string().optional(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type Artboard = z.infer<typeof ArtboardSchema>;
|
||||
|
||||
export const OriginSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
type: OriginTypeSchema,
|
||||
source_ref: z.string(),
|
||||
id: z.string().uuid(),
|
||||
type: OriginTypeSchema,
|
||||
source_ref: z.string(),
|
||||
source_metadata_jsonb: z.record(z.unknown()),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
/** Spec Layer 4 FK: origins.artboard_id (added migration 008) */
|
||||
artboard_id: z.string().uuid().nullable().optional(),
|
||||
source_id: z.string().nullable().optional(),
|
||||
source_url: z.string().nullable().optional(),
|
||||
screenshot_url: z.string().nullable().optional(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type Origin = z.infer<typeof OriginSchema>;
|
||||
|
||||
export const IntentDiffSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
artboard_id: z.string().uuid(),
|
||||
author_id: z.string(),
|
||||
changes_jsonb: z.record(z.unknown()),
|
||||
summary: z.string(),
|
||||
status: DiffStatusSchema,
|
||||
/** Free-text notes from the coding agent (e.g. why it was blocked) */
|
||||
notes: z.string().optional(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
id: z.string().uuid(),
|
||||
artboard_id: z.string().uuid(),
|
||||
author_id: z.string(),
|
||||
/** Spec column name: changes (renamed from changes_jsonb in migration 008) */
|
||||
changes: z.record(z.unknown()),
|
||||
/** Spec column name: aggregate_summary (renamed from summary in migration 008) */
|
||||
aggregate_summary: z.string(),
|
||||
status: DiffStatusSchema,
|
||||
notes: z.string().optional(),
|
||||
session_id: z.string().nullable().optional(),
|
||||
before_screenshot: z.string().nullable().optional(),
|
||||
after_screenshot: z.string().nullable().optional(),
|
||||
exported_code: z.string().nullable().optional(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type IntentDiff = z.infer<typeof IntentDiffSchema>;
|
||||
|
||||
export const AgentSessionSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
artboard_id: z.string().uuid(),
|
||||
diff_id: z.string().uuid().nullable(),
|
||||
agent_type: AgentTypeSchema,
|
||||
id: z.string().uuid(),
|
||||
artboard_id: z.string().uuid(),
|
||||
diff_id: z.string().uuid().nullable(),
|
||||
agent_type: AgentTypeSchema,
|
||||
messages_jsonb: z.array(z.record(z.unknown())),
|
||||
status: z.enum(['ACTIVE', 'COMPLETED', 'FAILED']),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
status: z.enum(['ACTIVE', 'COMPLETED', 'FAILED']),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type AgentSession = z.infer<typeof AgentSessionSchema>;
|
||||
|
||||
export const DesignLanguageFileSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
id: z.string().uuid(),
|
||||
workspace_id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
name: z.string(),
|
||||
schema_jsonb: z.record(z.unknown()),
|
||||
version: z.number().int(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
version: z.number().int(),
|
||||
is_active: z.boolean().optional(),
|
||||
created_by: z.string().nullable().optional(),
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type DesignLanguageFile = z.infer<typeof DesignLanguageFileSchema>;
|
||||
|
||||
export const TeamMemberSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
id: z.string().uuid(),
|
||||
workspace_id: z.string().uuid(),
|
||||
user_id: z.string(),
|
||||
role: TeamRoleSchema,
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
user_id: z.string(),
|
||||
role: TeamRoleSchema,
|
||||
created_at: z.string().datetime(),
|
||||
updated_at: z.string().datetime(),
|
||||
});
|
||||
export type TeamMember = z.infer<typeof TeamMemberSchema>;
|
||||
|
||||
// ── Project ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const ProjectSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
workspace_id: z.string().uuid(),
|
||||
@@ -131,7 +152,7 @@ export const ProjectSchema = z.object({
|
||||
});
|
||||
export type Project = z.infer<typeof ProjectSchema>;
|
||||
|
||||
// ── Ancestry (from materialized view) ────────────────────────────────────────
|
||||
// ── Ancestry (from closure table) ────────────────────────────────────────────
|
||||
|
||||
export interface ArtboardAncestry {
|
||||
artboard_id: string;
|
||||
@@ -139,13 +160,18 @@ export interface ArtboardAncestry {
|
||||
depth: number;
|
||||
}
|
||||
|
||||
// ── Insert types (omit server-set fields) ────────────────────────────────────
|
||||
// ── Insert types (omit server-set fields) ───────────────────��─────────────────
|
||||
|
||||
export type InsertWorkspace = Omit<Workspace, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertArtboard = Omit<Artboard, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertOrigin = Omit<Origin, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertIntentDiff = Omit<IntentDiff, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertAgentSession = Omit<AgentSession, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertDesignLanguageFile = Omit<DesignLanguageFile, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertTeamMember = Omit<TeamMember, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertProject = Omit<Project, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertWorkspace = Omit<Workspace, 'id' | 'created_at' | 'updated_at'>;
|
||||
/**
|
||||
* width/height are optional on insert because the DB column defaults are
|
||||
* 1440 and 900 respectively (migration 008). Callers may omit them.
|
||||
*/
|
||||
export type InsertArtboard = Omit<Artboard, 'id' | 'created_at' | 'updated_at' | 'width' | 'height'>
|
||||
& { width?: number; height?: number };
|
||||
export type InsertOrigin = Omit<Origin, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertIntentDiff = Omit<IntentDiff, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertAgentSession = Omit<AgentSession, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertDesignLanguageFile = Omit<DesignLanguageFile, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertTeamMember = Omit<TeamMember, 'id' | 'created_at' | 'updated_at'>;
|
||||
export type InsertProject = Omit<Project, 'id' | 'created_at' | 'updated_at'>;
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['src/**/*.ts'],
|
||||
exclude: ['src/index.ts', 'src/queries.ts'],
|
||||
exclude: ['src/index.ts', 'src/queries.ts', 'src/client.ts'],
|
||||
thresholds: { lines: 80, functions: 80, branches: 75 },
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user