feat: plane-lift schema (states/modules/cycles/links)

Phase 1 of the Plane feature lift into Project E.

Schema changes:
- Add stateGroupEnum, moduleStatusEnum, linkTypeEnum
- Add states table (per-project workflow states with group enum)
- Add modules table (project-scoped planning buckets)
- Add cycles table (time-boxed sprints)
- Add links table (canonical cross-entity mesh)
- Drop taskStatusEnum and tasks.status column
- Add stateId, moduleId, cycleId FKs to tasks
- Drop taskDependencies, noteLinks, noteEntityLinks tables

Project creation bootstrap:
- Seed 5 default states (Backlog/Todo/In Progress/Done/Cancelled) on new project

Minimal API fixes for typecheck:
- Remove references to dropped tables/columns
- Replace status-based queries with completedAt checks
- Stub deprecated dependency/status endpoints for Phase 2

Drizzle migration: 0008_plane-lift-schema.sql (custom, big-bang)
This commit is contained in:
2026-09-07 17:02:04 +00:00
parent 264f65955b
commit 639306a26f
11 changed files with 5064 additions and 448 deletions
+95 -56
View File
@@ -23,7 +23,6 @@ export const tsvector = customType<{ data: string }>({
// ── Enums ──────────────────────────────────────────────────────────────────────
export const taskStatusEnum = pgEnum('task_status', ['todo', 'in_progress', 'done', 'cancelled']);
export const taskPriorityEnum = pgEnum('task_priority', ['low', 'medium', 'high', 'urgent']);
export const habitFrequencyEnum = pgEnum('habit_frequency', ['daily', 'weekly', 'custom']);
export const habitDifficultyEnum = pgEnum('habit_difficulty', ['easy', 'medium', 'hard']);
@@ -32,6 +31,9 @@ export const sectionKindEnum = pgEnum('section_kind', ['section', 'milestone']);
export const sectionStatusEnum = pgEnum('section_status', ['planned', 'in_progress', 'complete']);
export const tagScopeEnum = pgEnum('tag_scope', ['global', 'tasks', 'habits', 'projects', 'notes']);
export const jobStatusEnum = pgEnum('job_status', ['pending', 'processing', 'completed', 'failed']);
export const stateGroupEnum = pgEnum('state_group', ['backlog', 'unstarted', 'started', 'completed', 'cancelled']);
export const moduleStatusEnum = pgEnum('module_status', ['planned', 'in_progress', 'completed', 'cancelled']);
export const linkTypeEnum = pgEnum('link_type', ['relates', 'blocks', 'parent-child', 'created-from']);
// ── Users ──────────────────────────────────────────────────────────────────────
@@ -143,6 +145,72 @@ export const sections = pgTable(
]
);
// ── States (per-project workflow states) ────────────────────────────────────────
export const states = pgTable(
'states',
{
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
color: text('color'),
group: stateGroupEnum('group').notNull().default('unstarted'),
projectId: uuid('project_id')
.notNull()
.references((): any => projects.id, { onDelete: 'cascade' }),
sortOrder: integer('sort_order').default(0),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index('states_project_id_idx').on(table.projectId),
index('states_sort_order_idx').on(table.projectId, table.sortOrder),
]
);
// ── Modules (project-scoped planning buckets) ──────────────────────────────────
export const modules = pgTable(
'modules',
{
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
description: text('description'),
projectId: uuid('project_id')
.notNull()
.references((): any => projects.id, { onDelete: 'cascade' }),
status: moduleStatusEnum('status').notNull().default('planned'),
startDate: timestamp('start_date', { withTimezone: true }),
targetDate: timestamp('target_date', { withTimezone: true }),
sortOrder: integer('sort_order').default(0),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index('modules_project_id_idx').on(table.projectId),
]
);
// ── Cycles (time-boxed sprints) ────────────────────────────────────────────────
export const cycles = pgTable(
'cycles',
{
id: uuid('id').defaultRandom().primaryKey(),
name: text('name').notNull(),
projectId: uuid('project_id')
.notNull()
.references((): any => projects.id, { onDelete: 'cascade' }),
startDate: timestamp('start_date', { withTimezone: true }),
endDate: timestamp('end_date', { withTimezone: true }),
active: boolean('active').default(false),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index('cycles_project_id_idx').on(table.projectId),
]
);
// ── Tasks ───────────────────────────────────────────────────────────────────────
export const tasks = pgTable(
@@ -151,13 +219,15 @@ export const tasks = pgTable(
id: uuid('id').defaultRandom().primaryKey(),
title: text('title').notNull(),
description: text('description'),
status: taskStatusEnum('status').notNull().default('todo'),
priority: taskPriorityEnum('priority').notNull().default('medium'),
domainId: uuid('domain_id')
.notNull()
.references((): any => domains.id, { onDelete: 'cascade' }),
projectId: uuid('project_id').references((): any => projects.id, { onDelete: 'set null' }),
sectionId: uuid('section_id').references((): any => sections.id, { onDelete: 'set null' }),
stateId: uuid('state_id').references((): any => states.id, { onDelete: 'set null' }),
moduleId: uuid('module_id').references((): any => modules.id, { onDelete: 'set null' }),
cycleId: uuid('cycle_id').references((): any => cycles.id, { onDelete: 'set null' }),
parentId: uuid('parent_id').references((): any => tasks.id, { onDelete: 'set null' }),
dueDate: timestamp('due_date', { withTimezone: true }),
completedAt: timestamp('completed_at', { withTimezone: true }),
@@ -175,8 +245,10 @@ export const tasks = pgTable(
index('tasks_domain_id_idx').on(table.domainId),
index('tasks_project_id_idx').on(table.projectId),
index('tasks_section_id_idx').on(table.sectionId),
index('tasks_state_id_idx').on(table.stateId),
index('tasks_module_id_idx').on(table.moduleId),
index('tasks_cycle_id_idx').on(table.cycleId),
index('tasks_parent_id_idx').on(table.parentId),
index('tasks_status_idx').on(table.status),
index('tasks_priority_idx').on(table.priority),
index('tasks_due_date_idx').on(table.dueDate),
index('tasks_order_idx').on(table.order),
@@ -203,24 +275,6 @@ export const taskTags = pgTable(
]
);
// ── Task Dependencies (junction) ───────────────────────────────────────────────
export const taskDependencies = pgTable(
'task_dependencies',
{
taskId: uuid('task_id')
.notNull()
.references((): any => tasks.id, { onDelete: 'cascade' }),
dependsOnTaskId: uuid('depends_on_task_id')
.notNull()
.references((): any => tasks.id, { onDelete: 'cascade' }),
},
(table) => [
primaryKey({ columns: [table.taskId, table.dependsOnTaskId] }),
index('task_dependencies_depends_on_idx').on(table.dependsOnTaskId),
]
);
// ── Habits ──────────────────────────────────────────────────────────────────────
export const habits = pgTable(
@@ -321,41 +375,6 @@ export const notes = pgTable(
]
);
// ── Note Links (wikilinks / backlinks) ─────────────────────────────────────────
export const noteLinks = pgTable(
'note_links',
{
sourceNoteId: uuid('source_note_id')
.notNull()
.references((): any => notes.id, { onDelete: 'cascade' }),
targetNoteId: uuid('target_note_id')
.notNull()
.references((): any => notes.id, { onDelete: 'cascade' }),
},
(table) => [
primaryKey({ columns: [table.sourceNoteId, table.targetNoteId] }),
index('note_links_target_note_id_idx').on(table.targetNoteId),
]
);
// ── Note Entity Links (cross-entity linking) ────────────────────────────────────
export const noteEntityLinks = pgTable(
'note_entity_links',
{
noteId: uuid('note_id')
.notNull()
.references((): any => notes.id, { onDelete: 'cascade' }),
entityType: text('entity_type').notNull(),
entityId: uuid('entity_id').notNull(),
},
(table) => [
index('note_entity_links_entity_idx').on(table.entityType, table.entityId),
index('note_entity_links_note_id_idx').on(table.noteId),
]
);
// ── Note Tags (junction) ────────────────────────────────────────────────────────
export const noteTags = pgTable(
@@ -374,6 +393,26 @@ export const noteTags = pgTable(
]
);
// ── Links (canonical cross-entity mesh) ────────────────────────────────────────
export const links = pgTable(
'links',
{
id: uuid('id').defaultRandom().primaryKey(),
sourceType: text('source_type').notNull(),
sourceId: uuid('source_id').notNull(),
targetType: text('target_type').notNull(),
targetId: uuid('target_id').notNull(),
linkType: linkTypeEnum('link_type').notNull(),
direction: text('direction'),
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
},
(table) => [
index('links_source_idx').on(table.sourceType, table.sourceId),
index('links_target_idx').on(table.targetType, table.targetId),
]
);
// ── Project Tags (junction) ─────────────────────────────────────────────────────
export const projectTags = pgTable(