Files
wursor/Originmain-ClaudeCode-Build-Prompt.md
T
2026-04-23 04:20:14 +01:00

1103 lines
48 KiB
Markdown

# Originmain — Claude Code Build Prompt
## Layer-by-Layer Engineering Guide for AI Coding Agents
**Product:** Originmain — AI-Native Design Engineering Platform
**Stack:** React 19 · Next.js 15 · Fluent UI v9 · TypeScript 5 · PostgreSQL · Supabase · Webpack Module Federation · MCP · Claude Sonnet 4
**Renderer:** @pierre/diffs (code diff display) · @pierre/trees (codebase file browser)
**Tooling:** pnpm workspaces · Vitest · Playwright · ESLint strict · Zod
**Date:** April 2026
---
## How to Read This Document
This prompt is structured as a strict sequence of layers. Each layer has a **goal**, a set of **files to create or modify**, a **verification gate** you must pass before proceeding, and **critical constraints** that must not be violated. Do not begin a layer until the previous layer's gate passes. Do not skip gates under any circumstances.
When you see `[FILE]`, create or modify that file. When you see `[VERIFY]`, run the specified command and confirm it passes before continuing. When you see `[CRITICAL]`, treat that constraint as a hard requirement — violations will cause downstream failure.
---
## Repository Bootstrap (Before Layer 0)
```bash
# Initialise the pnpm monorepo
mkdir originmain && cd originmain
git init
pnpm init
echo "packages:\n - 'packages/*'" > pnpm-workspace.yaml
# Create all package directories
mkdir -p packages/{app,renderer,diff-engine,origin-graph,ai-layer,agent-bridge,ui,integrations}
mkdir -p .github/workflows
```
The root `package.json` sets `"type": "module"` and declares the pnpm workspace. All packages share TypeScript 5 strict config via a root `tsconfig.base.json`. All packages run tests via a root `vitest.config.ts` with `projects` pointing to each package.
---
## Layer 0 — Infrastructure & DevOps
**Goal:** A working monorepo skeleton that CI can lint, type-check, build, and test. No product logic yet.
### 0.1 Root Configuration
**[FILE] `tsconfig.base.json`**
```json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"jsx": "react-jsx",
"paths": {
"@originmain/ui": ["./packages/ui/src/index.ts"],
"@originmain/diff-engine": ["./packages/diff-engine/src/index.ts"],
"@originmain/origin-graph": ["./packages/origin-graph/src/index.ts"],
"@originmain/agent-bridge": ["./packages/agent-bridge/src/index.ts"],
"@originmain/ai-layer": ["./packages/ai-layer/src/index.ts"],
"@originmain/renderer": ["./packages/renderer/src/index.ts"],
"@originmain/integrations": ["./packages/integrations/src/index.ts"]
}
}
}
```
**[FILE] `.github/workflows/ci.yml`**
Define a GitHub Actions workflow named `CI` that runs on every pull request to `main`. It must execute these steps in strict sequence:
1. `pnpm install --frozen-lockfile`
2. `pnpm run typecheck` (runs `tsc --noEmit` in every package)
3. `pnpm run lint` (ESLint with `@typescript-eslint/strict` ruleset, zero warnings policy)
4. `pnpm run test` (Vitest across all packages, minimum 80% coverage on `diff-engine` and `origin-graph`)
5. `pnpm run build` (Next.js production build for `packages/app`)
6. `pnpm run migration:dry-run` (Supabase migration dry-run against production schema snapshot)
No step may be skipped or made non-blocking. The CI pipeline is the product's quality gate.
**[FILE] `.github/workflows/preview.yml`**
A separate workflow that deploys a Vercel preview URL on every PR and posts it as a PR comment.
### 0.2 Supabase Project Setup
Create a `supabase/` directory at the monorepo root. Initialise it with `supabase init`. The `supabase/config.toml` sets:
- `project_id = "originmain-local"`
- `db.port = 54322`
- `studio.port = 54323`
- `api.port = 54321`
The local Supabase instance is started with `supabase start` and used by all local development. Staging and production use separate Supabase cloud projects.
### 0.3 Environment Variables
**[FILE] `.env.example`** — document every required variable:
```
NEXT_PUBLIC_SUPABASE_URL=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
SUPABASE_SERVICE_ROLE_KEY=
ANTHROPIC_API_KEY=
CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
LIVEBLOCKS_SECRET_KEY=
LINEAR_WEBHOOK_SECRET=
SLACK_BOT_TOKEN=
SLACK_SIGNING_SECRET=
AGENT_BRIDGE_PORT=3001
NEXT_PUBLIC_APP_URL=http://localhost:3000
```
Never commit `.env.local`. Add it to `.gitignore`. The CI pipeline uses GitHub Actions secrets injected as environment variables.
**[VERIFY]** Run `pnpm install` and `pnpm run typecheck`. Both must exit 0.
---
## Layer 1 — Canvas UI Shell
**Goal:** A navigable Next.js app with the Fluent 2 chrome, a working infinite canvas viewport, and the two-surface navigation architecture (artboard navigator + codebase file browser). No artboard content yet — placeholder divs only.
### 1.1 Package Setup (`packages/app`)
Install core dependencies:
```bash
cd packages/app
pnpm add next@15 react@19 react-dom@19
pnpm add @fluentui/react-components @fluentui/react-icons
pnpm add @pierre/trees
pnpm add zustand immer
pnpm add @tanstack/react-query
pnpm add -D typescript @types/react @types/node
```
The Next.js app uses the App Router (`app/` directory). No Pages Router.
### 1.2 Root Layout and Fluent Provider
**[FILE] `packages/app/src/app/layout.tsx`**
```tsx
import { FluentProvider } from '@fluentui/react-components';
import { originmainTheme } from '@originmain/ui';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<FluentProvider theme={originmainTheme}>
{children}
</FluentProvider>
</body>
</html>
);
}
```
**[FILE] `packages/ui/src/themes/originmain-theme.ts`**
Create the Originmain brand theme using `createLightTheme` from `@fluentui/react-components`. The brand palette anchors at `#0F52BA`. Map this colour to all 16 `BrandVariants` slots (10 through 160 in increments of 10), interpolating from a near-white tint at the 10 end to a near-black shade at the 160 end. Export as `originmainTheme` and also export `originmainDarkTheme` using `createDarkTheme` with the same brand variants.
**[CRITICAL]** The `FluentProvider` wraps the entire application. No component outside it uses Fluent 2 hooks or tokens. This is a hard constraint — Fluent 2's context system requires it.
### 1.3 Canvas Viewport
**[FILE] `packages/app/src/store/viewport.store.ts`**
A Zustand store that manages:
```typescript
interface ViewportState {
panX: number; // pixels
panY: number; // pixels
zoom: number; // 0.1 to 4.0
setPan: (x: number, y: number) => void;
setZoom: (level: number, originX: number, originY: number) => void;
resetViewport: () => void;
}
```
The `setZoom` function adjusts `panX` and `panY` to keep the zoom origin stationary on screen (the same point under the cursor before and after zooming).
**[FILE] `packages/app/src/components/canvas/Canvas.tsx`**
A full-viewport div with `overflow: hidden` and a pointer-event handler that:
- On mouse drag (middle button or space+left): updates `setPan`
- On scroll wheel: calls `setZoom` with the wheel delta and current cursor position
- On pinch gesture (via `onPointerDown` multi-touch detection): calls `setZoom`
Inside, render a transform div:
```tsx
<div
style={{
transform: `matrix(${zoom}, 0, 0, ${zoom}, ${panX}, ${panY})`,
transformOrigin: '0 0',
willChange: 'transform',
}}
>
{children}
</div>
```
**[CRITICAL]** The canvas transform is applied via a CSS matrix to a single container. Never apply individual transforms to artboard elements. This is the only way to achieve 60fps pan/zoom without re-rendering artboard content.
### 1.4 Navigation Architecture
**[FILE] `packages/app/src/components/navigator/ArtboardNavigator.tsx`**
Uses Fluent 2's `Tree` and `TreeItem` components to render the workspace hierarchy:
- Workspace (root)
- Project folders
- Artboard groups
- Individual artboards
Each `TreeItem` renders the artboard's origin badge (Linear issue, Git commit, User feedback, Manual) alongside its name. Clicking selects the artboard and centres the canvas viewport on it.
```typescript
// Data shape this component expects:
interface NavigatorNode {
id: string;
label: string;
type: 'workspace' | 'folder' | 'group' | 'artboard';
originType?: 'linear' | 'git' | 'feedback' | 'manual';
children?: NavigatorNode[];
}
```
**[FILE] `packages/app/src/components/codebase/CodebaseFileTree.tsx`**
Uses `@pierre/trees` to render the connected application's repository file tree. This component is entirely separate from the artboard navigator — it lives in a collapsible side panel on the opposite side of the canvas.
```tsx
import { FileTree } from '@pierre/trees';
interface CodebaseFileTreeProps {
nodes: FileTreeNode[]; // from the renderer's component tree extraction
onFileSelect: (path: string) => void;
}
export function CodebaseFileTree({ nodes, onFileSelect }: CodebaseFileTreeProps) {
return (
<div
className="codebase-file-tree"
style={{
// Map Fluent 2 tokens to @pierre/trees CSS custom properties
'--tree-background': 'var(--colorNeutralBackground2)',
'--tree-item-hover': 'var(--colorNeutralBackground3)',
'--tree-item-selected': 'var(--colorBrandBackground2)',
'--tree-text-color': 'var(--colorNeutralForeground1)',
'--tree-secondary-text': 'var(--colorNeutralForeground3)',
'--tree-git-added': 'var(--colorPaletteGreenForeground1)',
'--tree-git-modified': 'var(--colorPaletteYellowForeground1)',
'--tree-git-deleted': 'var(--colorPaletteRedForeground1)',
} as React.CSSProperties}
>
<FileTree
nodes={nodes}
gitStatus={true}
flattenEmptyDirectories={true}
fileTreeSearchMode="filter"
onSelect={(node) => node.type === 'file' && onFileSelect(node.path)}
/>
</div>
);
}
```
**[CRITICAL]** The CSS custom property names above match the ones @pierre/trees exposes for theming. Verify against the @pierre/trees documentation before shipping. Never hardcode colour values — always reference Fluent 2 tokens.
### 1.5 Chrome Layout
**[FILE] `packages/app/src/app/(workspace)/layout.tsx`**
A three-column layout: left panel (ArtboardNavigator, 240px), centre (Canvas, flex 1), right panel (Inspector + CodebaseFileTree, 320px). All panels use Fluent 2's `makeStyles` from Griffel for styling. No external CSS files.
Fluent 2 Toolbar across the top with these tool groups (left to right): Workspace name, Select tool, Pan tool, Artboard tools (new, duplicate, fork), AI tools (Completion Zone trigger), Export, Settings.
**[VERIFY]** `pnpm run build` passes. `pnpm run dev` opens the canvas in a browser with visible toolbar, left panel, and right panel. Panning and zooming work.
---
## Layer 2 — Live Rendering Engine
**Goal:** Originmain can connect to a running Next.js application, render one of its routes as a Live Artboard in the canvas, and extract the component tree from the rendered iframe.
### 2.1 Package Setup (`packages/renderer`)
```bash
cd packages/renderer
pnpm add react@19 react-dom@19
pnpm add -D webpack@5 @module-federation/enhanced
pnpm add -D typescript zod
```
### 2.2 The Renderer Protocol
**[FILE] `packages/renderer/src/protocol.ts`**
Define the message contract between the iframe and the host application:
```typescript
export type RendererMessage =
| { type: 'COMPONENT_TREE_READY'; payload: ComponentTreeNode }
| { type: 'COMPONENT_SELECTED'; payload: { componentId: string; rect: DOMRect } }
| { type: 'THEME_INJECTED'; payload: { success: boolean } }
| { type: 'ROUTE_CHANGED'; payload: { path: string } }
| { type: 'RENDER_ERROR'; payload: { message: string; stack?: string } };
export type HostMessage =
| { type: 'SELECT_COMPONENT'; payload: { componentId: string } }
| { type: 'INJECT_THEME'; payload: { tokens: Record<string, string> } }
| { type: 'NAVIGATE'; payload: { path: string } }
| { type: 'REQUEST_TREE' };
export interface ComponentTreeNode {
id: string; // stable ID derived from fiber key + display name
displayName: string; // React component display name
filePath?: string; // source file path (from sourcemaps)
props: Record<string, unknown>; // serialisable props only
rect: DOMRect; // bounding rect at render time
designTokens?: Record<string, string>; // resolved Fluent 2 token values
children: ComponentTreeNode[];
}
```
All messages are validated with Zod schemas before processing. Invalid messages are silently dropped.
### 2.3 The Fiber Tree Injector
**[FILE] `packages/renderer/src/fiber-injector.ts`**
A script injected into the iframe before the remote application initialises. It hooks into React's DevTools global hook (`__REACT_DEVTOOLS_GLOBAL_HOOK__`) to intercept the Fiber tree after each render commit. From the Fiber tree, it extracts `ComponentTreeNode` records by walking the `child` / `sibling` / `return` fiber links. It posts the extracted tree to the parent frame via `window.parent.postMessage`.
**[CRITICAL]** This injector must never import React or any application code — it runs in the iframe's global scope and must be a plain IIFE. It reads from the global hook only, produces a serialisable data structure, and posts it. Do not access `fiber.stateNode` beyond reading its bounding rect.
### 2.4 The Artboard Iframe Wrapper
**[FILE] `packages/app/src/components/artboard/ArtboardFrame.tsx`**
```tsx
interface ArtboardFrameProps {
artboardId: string;
remoteUrl: string; // URL of the connected app's Module Federation entry
route: string; // path to render (e.g., '/dashboard')
width: number;
height: number;
}
```
The component renders an `<iframe>` with:
```
sandbox="allow-scripts allow-same-origin allow-forms"
```
Before setting `src`, it injects the fiber injector script via the `srcdoc` attribute (a minimal HTML shell that loads the injector, then redirects to the remote app). The component listens for `RendererMessage` via `window.addEventListener('message', ...)`, validates each message against the Zod schema, and dispatches the payload to the artboard Zustand store.
An overlay `<div>` positioned absolutely over the iframe at the same dimensions intercepts pointer events for the visual editing system (Layer 3).
### 2.5 Module Federation Configuration
**[FILE] `packages/renderer/webpack.config.ts`**
Configure Webpack 5 with `@module-federation/enhanced`'s `ModuleFederationPlugin`. Originmain acts as the host (`name: 'originmain'`). Connected applications register as remotes. The remote URL is set dynamically at runtime from the artboard's `remoteUrl` prop.
Document the configuration that connected apps must add to their own webpack config to expose themselves as a Module Federation remote. This configuration lives at `packages/renderer/docs/connected-app-setup.md`.
### 2.6 Rendering Triggers
**[FILE] `packages/app/src/components/artboard/ArtboardIngestionPanel.tsx`**
A Fluent 2 `Drawer` component that presents four artboard creation paths:
1. **App Route**: Input a URL path from the connected application. The renderer loads that route in the iframe.
2. **Linear Issue**: Input a Linear issue ID. The ingestion connector fetches the issue, extracts the linked screenshot or route, and pre-populates the artboard with issue metadata.
3. **Git Commit**: Input a commit SHA. The ingestion connector checks out that revision of the connected app and renders the specified route at that point in history.
4. **Manual / Blank**: Creates an empty artboard with a placeholder iframe and no origin.
Each path calls `POST /api/artboards` with the appropriate origin payload. The API validates the payload with Zod, creates the artboard record in the Origin Graph, and returns the artboard ID. The canvas then renders a new `ArtboardFrame` at a default position.
**[VERIFY]** With a local Next.js test app running (any basic Next.js starter) configured as a Module Federation remote, Originmain can render its `/` route as a Live Artboard. The component tree is extracted and visible in the browser console. No CSP errors.
---
## Layer 3 — Visual Editing & Diff Engine
**Goal:** A user can click a component in a rendered artboard, modify its visual properties (colour, padding, component swap), and the system produces a correctly typed `IntentDiff` for every change. The code-level diff renders in the export panel using @pierre/diffs.
### 3.1 Package Setup (`packages/diff-engine`)
```bash
cd packages/diff-engine
pnpm add typescript-estree @babel/parser
pnpm add -D vitest typescript
```
No @pierre/diffs here — the diff engine is the computation layer only. @pierre/diffs is installed in `packages/app` for rendering.
```bash
cd packages/app
pnpm add @pierre/diffs
```
### 3.2 The Intent Diff Schema
**[FILE] `packages/diff-engine/src/types.ts`**
```typescript
import { z } from 'zod';
export const ComponentChangeSchema = z.object({
componentId: z.string(),
displayName: z.string(),
filePath: z.string().optional(),
changeType: z.enum(['prop_change', 'component_swap', 'layout_change', 'token_change', 'removal', 'insertion']),
before: z.record(z.unknown()),
after: z.record(z.unknown()),
humanSummary: z.string(), // AI-generated: "Replace secondary button with primary button"
});
export const IntentDiffSchema = z.object({
id: z.string().uuid(),
artboardId: z.string().uuid(),
timestamp: z.string().datetime(),
authorId: z.string(),
sessionId: z.string(),
changes: z.array(ComponentChangeSchema).min(1),
aggregateSummary: z.string(), // AI-generated summary of all changes combined
beforeScreenshot: z.string().url().optional(),
afterScreenshot: z.string().url().optional(),
exportedCode: z.string().optional(), // generated TypeScript/JSX expressing the changes
status: z.enum(['draft', 'exported', 'acknowledged', 'implemented', 'rejected']),
});
export type IntentDiff = z.infer<typeof IntentDiffSchema>;
export type ComponentChange = z.infer<typeof ComponentChangeSchema>;
```
### 3.3 The AST Differ
**[FILE] `packages/diff-engine/src/ast-differ.ts`**
The core computation engine. Takes two `ComponentTreeNode` snapshots (before and after a visual edit) and produces an array of `ComponentChange` records.
```typescript
export function computeIntentDiff(
before: ComponentTreeNode,
after: ComponentTreeNode,
sessionId: string,
authorId: string
): IntentDiff
```
The differ performs a depth-first tree walk, matching nodes by their stable `id`. For each matched pair:
- **Prop change**: any prop value differs → `changeType: 'prop_change'`, `before: { [propName]: oldValue }`, `after: { [propName]: newValue }`
- **Token change**: a design token reference in a prop changes → `changeType: 'token_change'`
- **Layout change**: position or size changes (derived from `rect` delta) → `changeType: 'layout_change'`
- **Component swap**: `displayName` differs for same `id``changeType: 'component_swap'`
- **Insertion**: node exists in `after` but not `before``changeType: 'insertion'`
- **Removal**: node exists in `before` but not `after``changeType: 'removal'`
The `humanSummary` field for each change is initially generated by a simple rule-based string template (e.g., `"Changed ${propName} on ${displayName} from ${oldValue} to ${newValue}"`). The AI layer (Layer 6) upgrades these summaries to natural language. Until Layer 6 is complete, the template strings are the source of truth.
**[CRITICAL]** The AST differ has zero dependencies on React, the browser, or any UI library. It is a pure function: `(before, after, meta) => IntentDiff`. This must be enforced with an ESLint no-restricted-imports rule in `packages/diff-engine/.eslintrc.json`.
### 3.4 Visual Editing Overlay
**[FILE] `packages/app/src/components/artboard/EditingOverlay.tsx`**
An absolutely positioned transparent div rendered over the artboard iframe. It receives the `ComponentTreeNode` from the artboard store and renders:
- A selection indicator (Fluent 2 border-style outline at the selected component's rect)
- Eight resize handles at the corners and midpoints
- A component picker popover (Fluent 2 `Popover` with `Combobox`) for component swapping
When the user drags a resize handle, the overlay translates the pixel delta into a prop change on the target component. It then takes a new snapshot of the iframe's component tree and calls `computeIntentDiff` with the before and after snapshots.
Every completed edit appends a new `ComponentChange` to the session's running `IntentDiff` in the artboard Zustand store.
### 3.5 Diff Inspector Sidebar
**[FILE] `packages/app/src/components/inspector/DiffInspector.tsx`**
Renders the current session's `IntentDiff` as a structured panel in the right column using Fluent 2 components:
- `Card` + `CardHeader`: artboard name, author, timestamp
- `Accordion` + `AccordionItem`: one item per `ComponentChange`, expanded by default
- Each `AccordionItem` body shows: `displayName`, `changeType` badge, before/after prop values in a two-column layout
- `aggregateSummary` in a `MessageBar` at the top of the panel
- "Export Diff" `Button` (primary) at the bottom
**[FILE] `packages/app/src/components/diff/CodeDiffPanel.tsx`**
The code-level diff view shown when the user clicks "Export Diff". Rendered inside a Fluent 2 `Drawer` (large, from the right edge).
```tsx
import { FileDiff, MultiFileDiff } from '@pierre/diffs/react';
interface CodeDiffPanelProps {
diff: IntentDiff;
isOpen: boolean;
onClose: () => void;
}
export function CodeDiffPanel({ diff, isOpen, onClose }: CodeDiffPanelProps) {
// Map IntentDiff changes to file-based diff format expected by @pierre/diffs
const fileDiffs = mapIntentDiffToFileDiffs(diff);
return (
<Drawer type="overlay" position="end" size="large" open={isOpen} onOpenChange={onClose}>
<DrawerHeader>
<DrawerHeaderTitle>Code Changes</DrawerHeaderTitle>
</DrawerHeader>
<DrawerBody>
<div
style={{
'--diff-background': 'var(--colorNeutralBackground1)',
'--diff-gutter-background': 'var(--colorNeutralBackground2)',
'--diff-added-line': 'var(--colorPaletteGreenBackground1)',
'--diff-removed-line': 'var(--colorPaletteRedBackground1)',
'--diff-annotation-color': 'var(--colorNeutralForeground3)',
} as React.CSSProperties}
>
<MultiFileDiff
diffs={fileDiffs}
mode="split" // side-by-side: before | after
theme="github-light" // matches webLightTheme; dark variant uses github-dark-dimmed
annotations={buildAnnotations(diff)} // line-anchored Intent Diff summaries
/>
</div>
</DrawerBody>
<DrawerFooter>
<Button appearance="primary" onClick={() => exportToAgent(diff)}>
Send to Coding Agent
</Button>
</DrawerFooter>
</Drawer>
);
}
```
**[FILE] `packages/app/src/components/diff/diff-mappers.ts`**
Implement `mapIntentDiffToFileDiffs(diff: IntentDiff)` and `buildAnnotations(diff: IntentDiff)`.
`mapIntentDiffToFileDiffs` converts `ComponentChange` records to the file diff format @pierre/diffs expects. Each change that has a known `filePath` becomes a separate file in the `MultiFileDiff`. Changes without a `filePath` are grouped under a synthetic file called `intent-diff.tsx` that shows the structural prop-level changes.
`buildAnnotations` anchors each `humanSummary` string to the line number of its corresponding change in the rendered diff. This produces the connected "design intent → code line" view that is Originmain's signature export experience.
**[CRITICAL]** The mode (`"split"` vs `"stacked"`) must respond to the drawer width. Use a `ResizeObserver` on the drawer body: if width < 640px, switch to `"stacked"`. This is not optional — split mode is unreadable at narrow widths.
**[VERIFY]** Make a visual change (e.g., change a button's `appearance` prop from `"secondary"` to `"primary"` by clicking it in the editing overlay). Confirm:
1. The DiffInspector shows the change with correct `displayName` and prop values
2. "Export Diff" opens the Drawer with a rendered code diff
3. The `humanSummary` text is visible as an annotation anchored to the relevant line
4. The diff correctly reflects only the changed prop, not unrelated code
---
## Layer 4 — Origin Graph & Data Store
**Goal:** All artboard metadata, provenance, and intent diffs are persisted in PostgreSQL and queryable via the GraphQL API.
### 4.1 Database Schema
**[FILE] `supabase/migrations/001_origin_graph.sql`**
```sql
-- Workspaces
create table workspaces (
id uuid primary key default gen_random_uuid(),
name text not null,
owner_id text not null, -- Clerk user ID
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Artboards
create table artboards (
id uuid primary key default gen_random_uuid(),
workspace_id uuid not null references workspaces(id) on delete cascade,
name text not null,
parent_id uuid references artboards(id), -- null for root artboards
route text, -- the app route this artboard renders
remote_url text, -- the Module Federation remote URL
width integer not null default 1440,
height integer not null default 900,
created_by text not null,
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Origins — the provenance record for every artboard
create table origins (
id uuid primary key default gen_random_uuid(),
artboard_id uuid not null references artboards(id) on delete cascade,
origin_type text not null check (origin_type in ('route', 'linear', 'git', 'slack', 'feedback', 'fork', 'manual')),
source_id text, -- Linear issue ID, Git SHA, Slack message TS, etc.
source_url text,
source_metadata jsonb, -- arbitrary origin-specific context
screenshot_url text,
created_at timestamptz default now()
);
-- Intent Diffs
create table intent_diffs (
id uuid primary key default gen_random_uuid(),
artboard_id uuid not null references artboards(id) on delete cascade,
author_id text not null,
session_id text not null,
changes jsonb not null, -- ComponentChange[]
aggregate_summary text not null,
before_screenshot text,
after_screenshot text,
exported_code text,
status text not null default 'draft'
check (status in ('draft','exported','acknowledged','implemented','rejected')),
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Agent sessions (for the Agent Bridge)
create table agent_sessions (
id uuid primary key default gen_random_uuid(),
artboard_id uuid not null references artboards(id),
diff_id uuid references intent_diffs(id),
agent_type text not null, -- 'cursor', 'claude-code', 'generic'
status text not null default 'pending',
messages jsonb not null default '[]',
created_at timestamptz default now(),
updated_at timestamptz default now()
);
-- Design Language Files
create table design_language_files (
id uuid primary key default gen_random_uuid(),
workspace_id uuid not null references workspaces(id) on delete cascade,
name text not null,
content jsonb not null,
is_active boolean not null default true,
created_by text not null,
created_at timestamptz default now()
);
-- Row-level security (enable on all tables)
alter table workspaces enable row level security;
alter table artboards enable row level security;
alter table origins enable row level security;
alter table intent_diffs enable row level security;
alter table agent_sessions enable row level security;
alter table design_language_files enable row level security;
-- RLS policies (workspace membership check via Clerk JWT claim)
create policy "workspace members can access" on workspaces
for all using (owner_id = auth.uid()::text);
-- ... (create equivalent policies for all other tables referencing workspace_id)
```
### 4.2 TypeScript Client (`packages/origin-graph`)
**[FILE] `packages/origin-graph/src/client.ts`**
A thin typed wrapper around `@supabase/supabase-js` that exposes CRUD functions for each table. All functions are fully typed against the Zod schemas from `packages/diff-engine`. No raw SQL in application code — all queries go through this client.
**[FILE] `packages/origin-graph/src/queries.ts`**
Implement these queries:
- `getArtboardsByWorkspace(workspaceId: string): Promise<Artboard[]>`
- `getOriginGraph(artboardId: string): Promise<{ artboard: Artboard; origin: Origin; diffs: IntentDiff[] }>`
- `getDiffsByStatus(workspaceId: string, status: IntentDiff['status']): Promise<IntentDiff[]>`
- `searchArtboards(workspaceId: string, query: string): Promise<Artboard[]>` — uses Supabase full-text search on `name`, `route`, and `jsonb` metadata fields
**[VERIFY]** `pnpm run migration:apply` on a fresh local Supabase instance completes without errors. `supabase db diff` shows zero unexpected schema changes. All TypeScript types resolve without errors.
---
## Layer 5 — Design Language Runtime
**Goal:** Teams can upload a Design Language File. The runtime loads it and uses it to validate visual edits and AI completions in real time.
### 5.1 Design Language File Schema
**[FILE] `packages/diff-engine/src/design-language.schema.ts`**
```typescript
export const DesignLanguageFileSchema = z.object({
version: z.literal('1.0'),
tokens: z.object({
colors: z.record(z.string()), // name → hex or token reference
typography: z.record(z.string()),
spacing: z.record(z.string()),
motion: z.record(z.string()).optional(),
}),
components: z.record(z.object({
allowedProps: z.record(z.array(z.string())).optional(), // prop → allowed values
forbiddenVariants: z.array(z.string()).optional(),
requiredAria: z.array(z.string()).optional(),
})),
screens: z.record(z.object({
allowedComponents: z.array(z.string()).optional(),
forbiddenComponents: z.array(z.string()).optional(),
requiredSections: z.array(z.string()).optional(),
})).optional(),
voice: z.object({
tone: z.array(z.string()),
avoidWords: z.array(z.string()).optional(),
}).optional(),
accessibility: z.object({
minContrastRatio: z.number().default(4.5),
minTouchTargetPx: z.number().default(44),
requireAltText: z.boolean().default(true),
}).optional(),
});
```
### 5.2 Validation Engine
**[FILE] `packages/diff-engine/src/design-language-validator.ts`**
```typescript
export interface ValidationResult {
valid: boolean;
violations: DesignViolation[];
}
export interface DesignViolation {
componentId: string;
rule: string;
severity: 'error' | 'warning';
message: string;
}
export function validateChange(
change: ComponentChange,
dlf: DesignLanguageFile,
): ValidationResult
```
The validator checks each `ComponentChange` against the active Design Language File. Violations are returned as an array — they do not block the edit but are shown as inline annotations on the artboard (Fluent 2 `Badge` with `appearance="filled"` and `color="warning"` or `color="danger"`).
**[VERIFY]** Upload a Design Language File via the settings panel. Make a visual edit that violates a constraint (e.g., set a component's colour to a value not in the token palette). Confirm a warning annotation appears on the artboard.
---
## Layer 6 — AI Completion Layer
**Goal:** AI Completion Zones work end-to-end. Visual edit summaries are upgraded to natural language. The Claude Sonnet 4 API is the only AI provider.
### 6.1 Package Setup (`packages/ai-layer`)
```bash
cd packages/ai-layer
pnpm add @anthropic-ai/sdk
pnpm add -D typescript zod
```
**[CRITICAL]** No AI calls are made from the frontend. All Claude API calls go through `packages/ai-layer`. The Next.js app calls `packages/ai-layer` via tRPC server-side routes only. The Anthropic API key is never exposed to the browser.
### 6.2 The AI Gateway
**[FILE] `packages/ai-layer/src/gateway.ts`**
A class `AIGateway` that wraps the Anthropic SDK with:
- API key validation on construction
- Request ID tracking for cost attribution per workspace
- Rate limit handling with exponential backoff (max 3 retries)
- Structured logging of every request (model, token count, cost estimate, latency)
- A prompt version registry: all prompts are versioned strings imported from `packages/ai-layer/src/prompts/`
```typescript
class AIGateway {
async generateDiffSummary(changes: ComponentChange[], dlf?: DesignLanguageFile): Promise<string>
async fillCompletionZone(zone: CompletionZoneSpec, dlf: DesignLanguageFile): Promise<CompletionZoneResult>
async queryArtboards(query: string, workspaceContext: WorkspaceContext): Promise<string>
async answerAgentQuery(question: string, diff: IntentDiff, dlf: DesignLanguageFile): Promise<string>
}
```
### 6.3 Prompt Files
**[FILE] `packages/ai-layer/src/prompts/diff-summary.prompt.ts`**
The system prompt for generating `humanSummary` strings for `ComponentChange` records and the `aggregateSummary` for full `IntentDiff` records.
Structure: system message that includes the Design Language File (if available) and instructs the model to describe changes in terms of user-visible impact, not technical prop names. Temperature 0.2. Max tokens 300 per change, 500 for aggregate.
**[FILE] `packages/ai-layer/src/prompts/completion-zone.prompt.ts`**
The system prompt for filling Completion Zones. Includes: the team's DLF as a hard constraint block, the component tree context of the surrounding region, any user-provided reference screenshot, and the zone intent string. Temperature 0.3. Output is a structured JSON response validated against a Zod schema before being accepted.
**[FILE] `packages/ai-layer/src/prompts/agent-query.prompt.ts`**
The system prompt for answering coding agent questions through the Agent Bridge. Includes: the full IntentDiff as context, before/after screenshots, and the DLF. Temperature 0.1 (factual, not creative). The model's role is "design agent" — it speaks with authority about design intent and can answer questions like "Is 16px or 24px padding intended here?" directly from the IntentDiff data.
### 6.4 Completion Zone UI
**[FILE] `packages/app/src/components/canvas/CompletionZone.tsx`**
A React component rendered in the UI chrome layer (above the canvas, pointer-events enabled) as a dashed-border rectangle that the designer draws over a region of the artboard. Internally it uses:
- `useRef` to track the drawing gesture
- A `Popover` (Fluent 2) for the intent input form
- An `Input` + `Textarea` for the intent string and optional reference description
- A `Button` (primary) "Generate" that calls the tRPC mutation `completionZone.generate`
- A preview overlay showing the AI-generated completion on the artboard
- `Button` pair: "Accept" (commits to IntentDiff) and "Reject" (discards)
**[VERIFY]** Mark a region of an artboard as a Completion Zone, enter an intent like "Fill this table with 5 rows of realistic customer data matching the existing column structure", and click Generate. The AI response should appear as a preview overlay within 8 seconds. Accepting it should add a `ComponentChange` with `changeType: 'insertion'` to the session's IntentDiff.
---
## Layer 7 — Agent Bridge (MCP)
**Goal:** A running MCP server that a Cursor or Claude Code instance can connect to, receive Intent Diffs, and send implementation status back to Originmain.
### 7.1 Package Setup (`packages/agent-bridge`)
```bash
cd packages/agent-bridge
pnpm add @modelcontextprotocol/sdk
pnpm add ws zod
pnpm add -D typescript
```
### 7.2 MCP Server Tools
**[FILE] `packages/agent-bridge/src/server.ts`**
An MCP server built with `@modelcontextprotocol/sdk`. Register these tools:
```typescript
// Returns the latest exported IntentDiff for a connected artboard
tool: 'get_intent_diff'
input: { artboard_id: string }
output: IntentDiff
// Returns component tree for a specific artboard
tool: 'get_component_tree'
input: { artboard_id: string }
output: ComponentTreeNode
// Returns the active Design Language File for the connected workspace
tool: 'get_design_language'
input: { workspace_id: string }
output: DesignLanguageFile
// The coding agent reports its implementation status
tool: 'report_status'
input: { diff_id: string; status: 'implemented' | 'blocked' | 'needs_clarification'; message?: string }
output: { acknowledged: boolean }
// The coding agent asks the design agent a clarifying question
tool: 'ask_design_agent'
input: { diff_id: string; question: string }
output: { answer: string } // AI-generated via packages/ai-layer
```
The `ask_design_agent` tool calls `AIGateway.answerAgentQuery` with the diff's full context. The answer is returned synchronously to the coding agent.
### 7.3 Cursor Adapter
**[FILE] `packages/agent-bridge/src/adapters/cursor.ts`**
Generates a `.cursorrules` file and `cursor_settings.json` MCP server configuration for a connected workspace. The `.cursorrules` content instructs Cursor to query Originmain's MCP server before making any UI-related code change and to report back when each change is applied.
### 7.4 Claude Code Adapter
**[FILE] `packages/agent-bridge/src/adapters/claude-code.ts`**
Generates a `CLAUDE.md` preamble and an MCP server declaration for Claude Code. The `CLAUDE.md` preamble instructs Claude Code to treat the `get_intent_diff` and `get_design_language` tools as primary sources of truth for all UI work.
### 7.5 Status Sync
When `report_status` is called, the Agent Bridge:
1. Updates `intent_diffs.status` in the Origin Graph via `packages/origin-graph`
2. Broadcasts a real-time update to the connected Originmain canvas via Supabase Realtime
3. The canvas updates the corresponding artboard's component badges (Fluent 2 `PresenceBadge`) to reflect the new status
**[VERIFY]** Start the MCP server with `node packages/agent-bridge/dist/server.js`. Configure a local Claude Code session to connect to it. Run `get_intent_diff` from the Claude Code session and confirm the correct IntentDiff is returned. Call `report_status` with `status: 'implemented'` and confirm the artboard badge updates in the Originmain canvas.
---
## Layer 8 — Integrations (Multi-Origin Ingestion)
**Goal:** Artboards can be created from Linear issues, Slack screenshots, and Git commit renders. Each integration is an isolated connector.
### 8.1 Shared Interface (`packages/integrations`)
**[FILE] `packages/integrations/src/types.ts`**
```typescript
export interface OriginIngester<TSource> {
validate(source: TSource): Promise<boolean>;
ingest(source: TSource): Promise<Origin>;
}
export interface Origin {
type: 'linear' | 'slack' | 'git' | 'feedback';
sourceId: string;
sourceUrl: string;
metadata: Record<string, unknown>;
screenshotUrl?: string;
suggestedRoute?: string; // the app route this origin most likely relates to
}
```
### 8.2 Linear Connector
**[FILE] `packages/integrations/src/linear/index.ts`**
Uses `@linear/sdk` to fetch the issue by ID. Extracts: title, description, priority, assignee, linked screenshots or attachments, labels, and team. Returns an `Origin` with all this data in `metadata`. If the issue has an attached screenshot, store it and return the URL as `screenshotUrl`. If the issue body contains a route reference (e.g., `/settings/billing`), extract it as `suggestedRoute`.
### 8.3 Slack Connector
**[FILE] `packages/integrations/src/slack/index.ts`**
Uses `@slack/bolt` to handle webhook events. On a message containing a screenshot or URL, downloads the attachment and stores it. Extracts the message text and sender identity into the origin metadata. The Slack connector runs as a serverless function behind a webhook endpoint.
### 8.4 Git Connector
**[FILE] `packages/integrations/src/git/index.ts`**
Uses `@octokit/rest` to fetch the diff between a given commit SHA and the previous commit. Identifies changed files that are React components (`.tsx`, `.jsx`). Returns an `Origin` with the commit message, author, timestamp, and changed file paths. The `suggestedRoute` is derived from the route mapping in the connected app's Next.js config if available.
**[VERIFY]** Create an artboard from a real Linear issue in a test Linear workspace. Confirm the artboard's metadata panel shows the issue title, description, and a link to the original issue.
---
## Layer 9 — Multiplayer & Presence (Phase 3)
**Goal:** Multiple users can work on the same workspace simultaneously. Cursors, selections, and artboard edits are synchronised in real time without conflicts.
### 9.1 Liveblocks Setup
```bash
cd packages/app
pnpm add @liveblocks/client @liveblocks/react @liveblocks/react-ui
```
**[FILE] `packages/app/src/liveblocks.config.ts`**
Define the Liveblocks type system:
```typescript
import { createClient } from '@liveblocks/client';
import { createRoomContext } from '@liveblocks/react';
type Presence = {
cursor: { x: number; y: number } | null;
activeArtboardId: string | null;
selectedComponentId: string | null;
userInfo: { name: string; avatarUrl: string; color: string };
};
type Storage = {
artboards: LiveMap<string, LiveObject<ArtboardState>>;
};
```
One Liveblocks room per workspace. The room name is `workspace-${workspaceId}`.
### 9.2 Presence Rendering
**[FILE] `packages/app/src/components/canvas/PresenceLayer.tsx`**
Renders other users' cursors as Fluent 2 `Avatar` components with a pointer indicator, positioned absolutely at their reported canvas coordinates. Updates at a 50ms throttle (Liveblocks' recommended rate). Each cursor fades out after 3 seconds of no movement.
**[VERIFY]** Open two browser sessions in the same workspace. Confirm cursor positions update in both sessions. Confirm making an artboard edit in session A is reflected in session B within 100ms.
---
## Layer 10 — Cross-Artboard Querying & Drift Detection (Phase 3)
**Goal:** Natural-language queries across the artboard workspace return relevant results. Design system drift is automatically detected and corrective diffs are generated.
### 10.1 Search Infrastructure
**[FILE] `supabase/migrations/008_search.sql`**
Add a generated `tsvector` column to `artboards` and `origins` for full-text search. Create a GIN index on both. Add a PostgreSQL function `search_artboards(query text, workspace_id uuid)` that performs weighted full-text search across names, route paths, origin metadata, and diff summaries.
### 10.2 Natural Language Query
**[FILE] `packages/ai-layer/src/prompts/workspace-query.prompt.ts`**
The system prompt for natural-language artboard queries. The model receives: the user's query, the workspace's artboard list (names + routes + origin types + creation dates), and the Design Language File. It returns a structured filter object that maps to the PostgreSQL search query. Temperature 0.1.
Example: "Show me every artboard linked to a user complaint this sprint" → `{ originType: 'feedback', dateAfter: '<sprint start>', dateRange: '<sprint end>' }`
### 10.3 Drift Detection
**[FILE] `packages/diff-engine/src/drift-detector.ts`**
```typescript
export function detectDrift(
liveComponentTree: ComponentTreeNode,
dlf: DesignLanguageFile,
): DriftReport
```
Walks the live component tree and compares each component's resolved design token values against the DLF's token definitions. Identifies:
- Components using deprecated tokens (tokens that existed in an older DLF version)
- Components using non-DLF colours (hardcoded hex or RGB values)
- Components using forbidden variants
- Layout violations (spacing values not in the DLF spacing scale)
The `DriftReport` lists each violation with enough context (componentId, filePath, offending value, correct value) to generate a corrective `IntentDiff` automatically.
---
## Layer 11 — Testing Strategy
**Goal:** The 10 critical E2E journeys all pass. Coverage gates are met.
### 11.1 Unit Tests (Vitest)
All unit tests live in `__tests__/` directories alongside the code they test. Required coverage:
- `packages/diff-engine`: 90% line coverage. The AST differ, validator, and drift detector are pure functions and are fully testable without a browser.
- `packages/origin-graph`: 85% coverage on query functions (mock Supabase client).
- `packages/ai-layer`: 80% coverage on gateway and prompt functions (mock Anthropic SDK).
- `packages/agent-bridge`: 85% coverage on MCP tool handlers.
### 11.2 E2E Tests (Playwright)
**[FILE] `e2e/critical-journeys.spec.ts`**
Implement Playwright tests for all 10 critical journeys documented in the Implementation Guide. Each test uses a dedicated Supabase project (provisioned per test run in CI) and a Module Federation test app (`e2e/test-app/`).
The test app is a minimal Next.js app with 3 routes (`/`, `/dashboard`, `/settings`) and 5 components. It is deterministic and produces known component trees — this makes diff assertions exact rather than approximate.
**[VERIFY]** `pnpm run test:e2e` passes all 10 journeys in under 5 minutes.
---
## Performance Targets (Verify Before Each Phase Ships)
| Metric | Target | Measurement |
|---|---|---|
| Canvas 60fps pan/zoom | < 2ms frame budget for transform application | Chrome DevTools Performance panel |
| Artboard first render | < 3s from iframe src set to component tree extraction | `performance.mark()` in the renderer protocol |
| Intent Diff computation | < 100ms for trees up to 500 nodes | Vitest benchmark in diff-engine |
| @pierre/diffs first paint | < 200ms for diffs up to 50 files | Playwright `page.coverage()` |
| @pierre/trees 5,000 nodes | < 50ms initial render | Playwright performance trace |
| AI Completion Zone (p50) | < 6s wall clock | Gateway request log |
| MCP tool response | < 200ms for read tools, < 500ms for write tools | Agent Bridge integration test |
---
## Security Checklist (Review Before Each Production Deployment)
- [ ] Iframe sandbox attribute contains exactly `allow-scripts allow-same-origin allow-forms` — no additions without security review
- [ ] All postMessage handlers validate origin against an allowlist of known remote URLs
- [ ] All postMessage payloads are validated with Zod before processing
- [ ] The Anthropic API key is never in any client bundle — verify with `pnpm run build && grep -r "sk-ant" .next/` (must return zero results)
- [ ] RLS policies prevent cross-workspace data access — verify with a Supabase policy test that a workspace A user cannot read workspace B data
- [ ] The MCP server validates the `workspace_id` in every tool call against the authenticated session
- [ ] Design Language Files are validated against the JSON Schema before storage — malformed files are rejected with a user-facing error, not silently stored
- [ ] All external URLs (Linear, Slack, Git) fetched via integrations are proxied server-side — never fetched client-side
---
## Appendix: Package Dependency Map
```
packages/app
├── @originmain/ui
├── @originmain/diff-engine
├── @originmain/origin-graph
├── @originmain/ai-layer (server-side only, via tRPC)
├── @originmain/agent-bridge (MCP client config only)
├── @originmain/renderer
├── @originmain/integrations (server-side only)
├── @fluentui/react-components
├── @pierre/diffs
└── @pierre/trees
packages/diff-engine
└── zod (no UI dependencies — enforced by ESLint)
packages/origin-graph
├── @supabase/supabase-js
├── @originmain/diff-engine
└── zod
packages/ai-layer
├── @anthropic-ai/sdk
├── @originmain/diff-engine
└── zod
packages/agent-bridge
├── @modelcontextprotocol/sdk
├── @originmain/diff-engine
├── @originmain/origin-graph
└── @originmain/ai-layer
packages/renderer
├── @module-federation/enhanced
└── zod
packages/integrations
├── @linear/sdk
├── @slack/bolt
├── @octokit/rest
└── @originmain/origin-graph
packages/ui
└── @fluentui/react-components
```
---
*End of Originmain Claude Code Build Prompt — v1.0 — April 2026*