nexus/.planning/phases/25-file-system/25-00-PLAN.md
Nexus Dev d72c065fc7 docs(25-file-system): create phase plan — 4 plans in 3 waves
Plans cover FILE-01 through FILE-06: DB schema + shared types (wave 1),
server file service + routes and UI file upload (wave 2, parallel),
file preview components + full wiring (wave 3).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 03:55:48 +00:00

13 KiB

phase plan type wave depends_on files_modified autonomous requirements must_haves
25-file-system 00 execute 1
packages/db/src/schema/chat_files.ts
packages/db/src/schema/chat_file_references.ts
packages/db/src/schema/index.ts
packages/db/src/migrations/0053_create_chat_files.sql
packages/db/src/migrations/0054_create_chat_file_references.sql
packages/shared/src/types/chat.ts
packages/shared/src/validators/chat.ts
packages/shared/src/index.ts
server/src/__tests__/chat-file-service.test.ts
server/src/__tests__/chat-file-routes.test.ts
true
FILE-01
FILE-02
FILE-03
truths artifacts key_links
chat_files table exists in Postgres with all required metadata columns
chat_file_references table exists enabling cross-conversation file references
Shared types and validators for file operations are exported from @paperclipai/shared
path provides contains
packages/db/src/schema/chat_files.ts chat_files Drizzle schema table pgTable
path provides contains
packages/db/src/schema/chat_file_references.ts chat_file_references Drizzle schema table pgTable
path provides contains
packages/shared/src/types/chat.ts ChatFile and ChatFileReference types ChatFile
from to via pattern
packages/db/src/schema/chat_files.ts packages/db/src/schema/chat_conversations.ts FK reference conversationId chatConversations
from to via pattern
packages/db/src/schema/chat_file_references.ts packages/db/src/schema/chat_files.ts FK reference fileId chatFiles
Create the database schema, shared types, validators, and test stubs for the chat file system.

Purpose: Establish the data layer contracts that all subsequent plans (upload routes, UI previews) build against. Output: Two new DB tables (chat_files, chat_file_references), shared TypeScript types, Zod validators, and test scaffolds.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/25-file-system/25-RESEARCH.md

@packages/db/src/schema/chat_messages.ts @packages/db/src/schema/chat_conversations.ts @packages/db/src/schema/assets.ts @packages/db/src/schema/index.ts @packages/shared/src/types/chat.ts @packages/shared/src/validators/chat.ts @server/src/tests/chat-routes.test.ts

Task 1: Create chat_files and chat_file_references DB schema + migrations packages/db/src/schema/chat_files.ts, packages/db/src/schema/chat_file_references.ts, packages/db/src/schema/index.ts, packages/db/src/migrations/0053_create_chat_files.sql, packages/db/src/migrations/0054_create_chat_file_references.sql packages/db/src/schema/chat_messages.ts, packages/db/src/schema/chat_conversations.ts, packages/db/src/schema/assets.ts, packages/db/src/schema/index.ts, packages/db/src/migrations/meta/_journal.json Create `packages/db/src/schema/chat_files.ts` with a `chatFiles` pgTable containing: - id: uuid PK defaultRandom - companyId: uuid NOT NULL FK to companies.id - conversationId: uuid FK to chatConversations.id (nullable — file may exist without conversation) - messageId: uuid FK to chatMessages.id ON DELETE SET NULL (nullable — file may be uploaded before message is sent) - filename: text NOT NULL (display filename, may differ from original) - originalFilename: text NOT NULL (as uploaded) - mimeType: text NOT NULL - sizeBytes: integer NOT NULL - objectKey: text NOT NULL (StorageService object key) - sha256: text NOT NULL - source: text NOT NULL (values: 'user_upload', 'agent_generated') - category: text (values: 'image', 'document', 'code', 'other' — nullable, derived from mimeType) - projectId: uuid FK to projects.id ON DELETE SET NULL (nullable — for dual scoping FILE-04) - createdAt: timestamp with timezone NOT NULL defaultNow - updatedAt: timestamp with timezone NOT NULL defaultNow
Indexes (use object-syntax callback like existing chat schemas):
- chat_files_conversation_idx on (conversationId)
- chat_files_message_idx on (messageId)
- chat_files_company_created_idx on (companyId, createdAt)
- chat_files_project_idx on (projectId)

Create `packages/db/src/schema/chat_file_references.ts` with a `chatFileReferences` pgTable:
- id: uuid PK defaultRandom
- fileId: uuid NOT NULL FK to chatFiles.id ON DELETE CASCADE
- conversationId: uuid NOT NULL FK to chatConversations.id ON DELETE CASCADE
- messageId: uuid FK to chatMessages.id ON DELETE SET NULL (nullable)
- createdAt: timestamp with timezone NOT NULL defaultNow

Indexes:
- chat_file_refs_file_idx on (fileId)
- chat_file_refs_conversation_idx on (conversationId)
- chat_file_refs_message_idx on (messageId)

Add exports to `packages/db/src/schema/index.ts`:
- export { chatFiles } from "./chat_files.js";
- export { chatFileReferences } from "./chat_file_references.js";

Create migration SQL files:
- 0053_create_chat_files.sql: CREATE TABLE chat_files with all columns and indexes
- 0054_create_chat_file_references.sql: CREATE TABLE chat_file_references with all columns and indexes

Update `packages/db/src/migrations/meta/_journal.json` to add entries for idx 53 and 54.
Copy the latest snapshot JSON file as a template for the new snapshots (0053_snapshot.json, 0054_snapshot.json) — but note that the migration system only strictly requires the SQL files and journal entries; skip snapshot creation if too complex.

IMPORTANT: Follow existing index callback pattern from chat_messages.ts — use `(table) => ({})` object syntax, not array syntax.
cd /opt/nexus && grep -q "chatFiles" packages/db/src/schema/index.ts && grep -q "chatFileReferences" packages/db/src/schema/index.ts && grep -q "chat_files" packages/db/src/migrations/0053_create_chat_files.sql && grep -q "chat_file_references" packages/db/src/migrations/0054_create_chat_file_references.sql && echo "PASS" - grep "chatFiles" packages/db/src/schema/index.ts returns a match - grep "chatFileReferences" packages/db/src/schema/index.ts returns a match - grep "pgTable" packages/db/src/schema/chat_files.ts returns a match - grep "pgTable" packages/db/src/schema/chat_file_references.ts returns a match - grep "CREATE TABLE" packages/db/src/migrations/0053_create_chat_files.sql returns a match - grep "CREATE TABLE" packages/db/src/migrations/0054_create_chat_file_references.sql returns a match - grep "company_id" packages/db/src/schema/chat_files.ts returns a match (dual scoping support) - grep "project_id" packages/db/src/schema/chat_files.ts returns a match (FILE-04 support) - grep "source" packages/db/src/schema/chat_files.ts returns a match chat_files and chat_file_references Drizzle schemas exist with all columns, indexes, FKs. Migration SQL files exist. Schema index re-exports both tables. Task 2: Add shared types, validators, and test stubs packages/shared/src/types/chat.ts, packages/shared/src/validators/chat.ts, packages/shared/src/index.ts, server/src/__tests__/chat-file-service.test.ts, server/src/__tests__/chat-file-routes.test.ts packages/shared/src/types/chat.ts, packages/shared/src/validators/chat.ts, packages/shared/src/index.ts, server/src/__tests__/chat-routes.test.ts Extend `packages/shared/src/types/chat.ts` — add these interfaces (append, do NOT modify existing interfaces):
```typescript
export interface ChatFile {
  id: string;
  companyId: string;
  conversationId: string | null;
  messageId: string | null;
  filename: string;
  originalFilename: string;
  mimeType: string;
  sizeBytes: number;
  objectKey: string;
  sha256: string;
  source: "user_upload" | "agent_generated";
  category: "image" | "document" | "code" | "other" | null;
  projectId: string | null;
  createdAt: string;
  updatedAt: string;
}

export interface ChatFileReference {
  id: string;
  fileId: string;
  conversationId: string;
  messageId: string | null;
  createdAt: string;
}

export interface ChatFileUploadResponse {
  file: ChatFile;
  contentPath: string;
}

export interface ChatFileListResponse {
  items: ChatFile[];
}
```

Also extend `ChatMessage` by adding an optional `files` field:
```typescript
// In ChatMessage interface, add:
files?: ChatFile[];
```

Extend `packages/shared/src/validators/chat.ts` — add Zod schemas:
```typescript
export const uploadChatFileSchema = z.object({
  conversationId: z.string().uuid().optional(),
  messageId: z.string().uuid().optional(),
  source: z.enum(["user_upload", "agent_generated"]).default("user_upload"),
  projectId: z.string().uuid().optional(),
});

export const createFileReferenceSchema = z.object({
  fileId: z.string().uuid(),
  messageId: z.string().uuid().optional(),
});
```

Ensure new schemas and types are re-exported from `packages/shared/src/index.ts` — check existing export pattern in that file and add:
- The new type exports (ChatFile, ChatFileReference, ChatFileUploadResponse, ChatFileListResponse)
- The new validator exports (uploadChatFileSchema, createFileReferenceSchema)

Create test stubs:

`server/src/__tests__/chat-file-service.test.ts`:
```typescript
import { describe, it } from "vitest";

describe("chatFileService", () => {
  it.todo("creates a file record after upload");
  it.todo("lists files for a conversation");
  it.todo("lists files for a message");
  it.todo("creates a file reference in another conversation");
  it.todo("returns file with contentPath");
});
```

`server/src/__tests__/chat-file-routes.test.ts`:
```typescript
import { describe, it } from "vitest";

describe("chatFileRoutes", () => {
  it.todo("POST /conversations/:id/files uploads a file and returns 201");
  it.todo("GET /conversations/:id/files lists files for conversation");
  it.todo("GET /files/:fileId/content serves file content");
  it.todo("POST /files/:fileId/references creates a cross-conversation reference");
  it.todo("rejects upload when file exceeds size limit");
  it.todo("rejects upload when content type is not allowed");
});
```
cd /opt/nexus && npx tsc --noEmit -p packages/shared/tsconfig.json 2>&1 | tail -5 && grep -q "ChatFile" packages/shared/src/types/chat.ts && grep -q "uploadChatFileSchema" packages/shared/src/validators/chat.ts && echo "PASS" - grep "ChatFile" packages/shared/src/types/chat.ts returns a match - grep "ChatFileReference" packages/shared/src/types/chat.ts returns a match - grep "ChatFileUploadResponse" packages/shared/src/types/chat.ts returns a match - grep "files?" packages/shared/src/types/chat.ts returns a match (optional files on ChatMessage) - grep "uploadChatFileSchema" packages/shared/src/validators/chat.ts returns a match - grep "createFileReferenceSchema" packages/shared/src/validators/chat.ts returns a match - grep "it.todo" server/src/__tests__/chat-file-service.test.ts returns matches - grep "it.todo" server/src/__tests__/chat-file-routes.test.ts returns matches - TypeScript compilation of shared package succeeds ChatFile and related types exported from shared. Zod validators for file upload and reference creation exported. ChatMessage type has optional files array. Test stubs exist for service and routes. - `packages/db/src/schema/chat_files.ts` exists with pgTable definition - `packages/db/src/schema/chat_file_references.ts` exists with pgTable definition - Both tables exported from schema index - SQL migration files exist for both tables - `ChatFile` type exported from shared - `uploadChatFileSchema` validator exported from shared - Test stubs have `.todo()` tests for service and routes - TypeScript compiles without errors in shared package

<success_criteria> Database schema for file tracking is defined with all columns needed for FILE-01 (directory/storage), FILE-02 (metadata), FILE-03 (cross-references), and FILE-04 (dual scoping via projectId). Shared types provide the contract for Plans 01-03. </success_criteria>

After completion, create `.planning/phases/25-file-system/25-00-SUMMARY.md`