1295 lines
52 KiB
TypeScript
1295 lines
52 KiB
TypeScript
/**
|
|
* Mosaic Native Kanban — frozen Drizzle schema contract v1.
|
|
*
|
|
* This is the canonical target/compatibility declaration for integration into
|
|
* the ONE current-main packages/db/src/schema.ts. It MUST NOT be imported as a
|
|
* competing schema module. KBN-100 applies the field-by-field expand/backfill/
|
|
* switch/contract map in SHARED-CONTRACT.md; legacy fields marked below remain
|
|
* declared throughout the expand and N-1 window.
|
|
*
|
|
* Existing Better Auth users.id is the identity parent. User references require
|
|
* active workspace membership checks in the same authoritative transaction.
|
|
*/
|
|
|
|
import { sql } from 'drizzle-orm';
|
|
import {
|
|
bigint,
|
|
boolean,
|
|
check,
|
|
foreignKey,
|
|
index,
|
|
integer,
|
|
jsonb,
|
|
numeric,
|
|
pgEnum,
|
|
pgTable,
|
|
primaryKey,
|
|
text,
|
|
timestamp,
|
|
uniqueIndex,
|
|
uuid,
|
|
} from 'drizzle-orm/pg-core';
|
|
|
|
// ─── Frozen vocabularies ─────────────────────────────────────────────────────
|
|
|
|
export const workspaceLifecycleEnum = pgEnum('workspace_lifecycle', [
|
|
'active',
|
|
'suspended',
|
|
'archived',
|
|
]);
|
|
export const workspaceMemberRoleEnum = pgEnum('workspace_member_role', [
|
|
'owner',
|
|
'admin',
|
|
'member',
|
|
'auditor',
|
|
'service',
|
|
]);
|
|
export const teamMemberRoleEnum = pgEnum('team_member_role', ['manager', 'member']);
|
|
export const agentLifecycleEnum = pgEnum('agent_lifecycle', ['enabled', 'disabled']);
|
|
export const agentSessionStateEnum = pgEnum('agent_session_state', [
|
|
'starting',
|
|
'available',
|
|
'busy',
|
|
'degraded',
|
|
'offline',
|
|
'ended',
|
|
]);
|
|
export const projectStatusEnum = pgEnum('project_status_v1', [
|
|
'planning',
|
|
'active',
|
|
'paused',
|
|
'completed',
|
|
'archived',
|
|
]);
|
|
export const missionStatusEnum = pgEnum('mission_status_v1', [
|
|
'draft',
|
|
'awaiting_approval',
|
|
'active',
|
|
'paused',
|
|
'certifying',
|
|
'completed',
|
|
'failed',
|
|
'cancelled',
|
|
]);
|
|
export const milestoneStatusEnum = pgEnum('milestone_status_v1', [
|
|
'planned',
|
|
'active',
|
|
'at_risk',
|
|
'completed',
|
|
'cancelled',
|
|
]);
|
|
export const taskStatusEnum = pgEnum('task_status_v1', [
|
|
'backlog',
|
|
'ready',
|
|
'in_progress',
|
|
'blocked',
|
|
'in_review',
|
|
'done',
|
|
'cancelled',
|
|
]);
|
|
export const priorityEnum = pgEnum('work_priority_v1', ['critical', 'high', 'medium', 'low']);
|
|
export const specialistRoleEnum = pgEnum('specialist_role_v1', [
|
|
'planning',
|
|
'enhance',
|
|
'coder',
|
|
'review',
|
|
'security-review',
|
|
'pr-monitor',
|
|
'certifier',
|
|
]);
|
|
export const dependencyTypeEnum = pgEnum('task_dependency_type', [
|
|
'blocks',
|
|
'review_gate',
|
|
'certification_gate',
|
|
]);
|
|
/** Must match assignmentStates in mechanical-coordinator.v1.ts exactly. */
|
|
export const assignmentStateEnum = pgEnum('task_assignment_state_v1', [
|
|
'awaiting_approval',
|
|
'policy_pre_authorized',
|
|
'approved',
|
|
'rejected',
|
|
'leased',
|
|
'released',
|
|
'expired',
|
|
'superseded',
|
|
]);
|
|
export const leaseStateEnum = pgEnum('task_lease_state', [
|
|
'pending_ack',
|
|
'active',
|
|
'released',
|
|
'expired',
|
|
'revoked',
|
|
]);
|
|
export const actorKindEnum = pgEnum('actor_kind_v1', [
|
|
'user',
|
|
'agent',
|
|
'session',
|
|
'service',
|
|
'policy',
|
|
'system',
|
|
]);
|
|
export const approvalDecisionEnum = pgEnum('approval_decision_v1', [
|
|
'requested',
|
|
'approved',
|
|
'rejected',
|
|
'escalated',
|
|
]);
|
|
export const executionDispositionEnum = pgEnum('task_execution_disposition_v1', [
|
|
'available',
|
|
'retry_delayed',
|
|
'quarantined',
|
|
'exhausted',
|
|
]);
|
|
export const changeProposalStateEnum = pgEnum('change_proposal_state_v1', [
|
|
'pending',
|
|
'accepted',
|
|
'rejected',
|
|
]);
|
|
export const outboxStateEnum = pgEnum('outbox_state_v1', [
|
|
'pending',
|
|
'publishing',
|
|
'published',
|
|
'failed',
|
|
]);
|
|
|
|
// ─── Tenant and identity ─────────────────────────────────────────────────────
|
|
|
|
export const workspacesV1 = pgTable(
|
|
'workspaces',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
name: text('name').notNull(),
|
|
slug: text('slug').notNull(),
|
|
settings: jsonb('settings').notNull().$type<Record<string, unknown>>().default({}),
|
|
lifecycle: workspaceLifecycleEnum('lifecycle').notNull().default('active'),
|
|
ownerId: text('owner_id').notNull(), // FK to existing users.id at integration
|
|
version: integer('version').notNull().default(1),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('workspaces_slug_uidx').on(t.slug),
|
|
check('workspaces_version_positive_chk', sql`${t.version} > 0`),
|
|
],
|
|
);
|
|
|
|
export const workspaceMembersV1 = pgTable(
|
|
'workspace_members',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
userId: text('user_id').notNull(), // FK to existing users.id at integration
|
|
role: workspaceMemberRoleEnum('role').notNull().default('member'),
|
|
active: boolean('active').notNull().default(true),
|
|
joinedAt: timestamp('joined_at', { withTimezone: true }).notNull().defaultNow(),
|
|
revokedAt: timestamp('revoked_at', { withTimezone: true }),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('workspace_members_workspace_user_uidx').on(t.workspaceId, t.userId),
|
|
index('workspace_members_user_active_idx').on(t.userId, t.active),
|
|
],
|
|
);
|
|
|
|
export const teamsV1 = pgTable(
|
|
'teams',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
name: text('name').notNull(),
|
|
slug: text('slug').notNull(),
|
|
ownerId: text('owner_id').notNull(), // legacy/current users.id field retained
|
|
managerId: text('manager_id').notNull(), // legacy/current users.id field retained
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('teams_workspace_slug_uidx').on(t.workspaceId, t.slug),
|
|
uniqueIndex('teams_workspace_id_uidx').on(t.workspaceId, t.id),
|
|
],
|
|
);
|
|
|
|
export const teamMembersV1 = pgTable(
|
|
'team_members',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
teamId: uuid('team_id').notNull(),
|
|
userId: text('user_id').notNull(),
|
|
role: teamMemberRoleEnum('role').notNull().default('member'),
|
|
invitedBy: text('invited_by'),
|
|
joinedAt: timestamp('joined_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'team_members_workspace_team_fk',
|
|
columns: [t.workspaceId, t.teamId],
|
|
foreignColumns: [teamsV1.workspaceId, teamsV1.id],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('team_members_workspace_team_user_uidx').on(t.workspaceId, t.teamId, t.userId),
|
|
],
|
|
);
|
|
|
|
export const agentsV1 = pgTable(
|
|
'agents',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
name: text('name').notNull(),
|
|
provider: text('provider').notNull(), // legacy retained
|
|
model: text('model').notNull(), // legacy retained
|
|
legacyStatus: text('status', {
|
|
enum: ['idle', 'active', 'error', 'offline'],
|
|
})
|
|
.notNull()
|
|
.default('idle'),
|
|
projectId: uuid('project_id'), // legacy retained through N-1
|
|
ownerId: text('owner_id'), // legacy retained; active membership required
|
|
systemPrompt: text('system_prompt'), // legacy retained
|
|
allowedTools: jsonb('allowed_tools').$type<string[]>(), // legacy retained
|
|
skills: jsonb('skills').$type<string[]>(), // legacy retained
|
|
isSystem: boolean('is_system').notNull().default(false), // legacy retained
|
|
config: jsonb('config'), // legacy retained
|
|
runtime: text('runtime').notNull(),
|
|
roles: jsonb('roles').notNull().$type<string[]>().default([]),
|
|
capabilities: jsonb('capabilities').notNull().$type<string[]>().default([]),
|
|
lifecycle: agentLifecycleEnum('lifecycle').notNull().default('enabled'),
|
|
metadata: jsonb('metadata').notNull().$type<Record<string, unknown>>().default({}),
|
|
version: integer('version').notNull().default(1),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('agents_workspace_id_uidx').on(t.workspaceId, t.id),
|
|
uniqueIndex('agents_workspace_name_uidx').on(t.workspaceId, t.name),
|
|
index('agents_workspace_lifecycle_idx').on(t.workspaceId, t.lifecycle),
|
|
check('agents_version_positive_chk', sql`${t.version} > 0`),
|
|
],
|
|
);
|
|
|
|
export const agentSessionsV1 = pgTable(
|
|
'agent_sessions',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
agentId: uuid('agent_id').notNull(),
|
|
harnessSessionKey: text('harness_session_key').notNull(),
|
|
host: text('host').notNull(),
|
|
state: agentSessionStateEnum('state').notNull().default('starting'),
|
|
declaredRoles: specialistRoleEnum('declared_primary_role'),
|
|
roleSet: jsonb('role_set').notNull().$type<string[]>().default([]),
|
|
capabilities: jsonb('capabilities').notNull().$type<string[]>().default([]),
|
|
capacity: integer('capacity').notNull().default(1),
|
|
contextUsagePercent: integer('context_usage_percent'),
|
|
startedAt: timestamp('started_at', { withTimezone: true }).notNull().defaultNow(),
|
|
lastHeartbeatAt: timestamp('last_heartbeat_at', { withTimezone: true }),
|
|
endedAt: timestamp('ended_at', { withTimezone: true }),
|
|
metadata: jsonb('metadata').notNull().$type<Record<string, unknown>>().default({}),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'agent_sessions_workspace_agent_fk',
|
|
columns: [t.workspaceId, t.agentId],
|
|
foreignColumns: [agentsV1.workspaceId, agentsV1.id],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('agent_sessions_workspace_harness_key_uidx').on(t.workspaceId, t.harnessSessionKey),
|
|
uniqueIndex('agent_sessions_workspace_id_uidx').on(t.workspaceId, t.id),
|
|
uniqueIndex('agent_sessions_workspace_agent_id_uidx').on(t.workspaceId, t.agentId, t.id),
|
|
index('agent_sessions_workspace_state_heartbeat_idx').on(
|
|
t.workspaceId,
|
|
t.state,
|
|
t.lastHeartbeatAt,
|
|
),
|
|
check('agent_sessions_capacity_positive_chk', sql`${t.capacity} > 0`),
|
|
check(
|
|
'agent_sessions_context_percent_chk',
|
|
sql`${t.contextUsagePercent} is null or (${t.contextUsagePercent} >= 0 and ${t.contextUsagePercent} <= 100)`,
|
|
),
|
|
],
|
|
);
|
|
|
|
// ─── Planning hierarchy ──────────────────────────────────────────────────────
|
|
|
|
export const projectsV1 = pgTable(
|
|
'projects',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
name: text('name').notNull(),
|
|
description: text('description'),
|
|
/** Legacy status remains declared/readable through the N-1 window. */
|
|
legacyStatus: text('status', {
|
|
enum: ['active', 'paused', 'completed', 'archived'],
|
|
})
|
|
.notNull()
|
|
.default('active'),
|
|
canonicalStatus: projectStatusEnum('canonical_status').notNull().default('planning'),
|
|
/** Legacy ownership fields retained until contract release. */
|
|
legacyOwnerId: text('owner_id'),
|
|
legacyTeamId: uuid('team_id'),
|
|
legacyOwnerType: text('owner_type', { enum: ['user', 'team'] })
|
|
.notNull()
|
|
.default('user'),
|
|
accountableUserId: text('accountable_user_id'),
|
|
accountableTeamId: uuid('accountable_team_id'),
|
|
priority: priorityEnum('priority').notNull().default('medium'),
|
|
repositoryUrl: text('repository_url'),
|
|
repositoryProvider: text('repository_provider'),
|
|
defaultBranch: text('default_branch'),
|
|
domain: text('domain'),
|
|
startDate: timestamp('start_date', { withTimezone: true }),
|
|
targetDate: timestamp('target_date', { withTimezone: true }),
|
|
blockerSummary: text('blocker_summary'),
|
|
progressPolicy: jsonb('progress_policy').notNull().$type<Record<string, unknown>>().default({}),
|
|
metadata: jsonb('metadata').notNull().$type<Record<string, unknown>>().default({}),
|
|
version: integer('version').notNull().default(1),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'projects_workspace_accountable_user_fk',
|
|
columns: [t.workspaceId, t.accountableUserId],
|
|
foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'projects_workspace_accountable_team_fk',
|
|
columns: [t.workspaceId, t.accountableTeamId],
|
|
foreignColumns: [teamsV1.workspaceId, teamsV1.id],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('projects_workspace_id_uidx').on(t.workspaceId, t.id),
|
|
index('projects_workspace_status_idx').on(t.workspaceId, t.canonicalStatus),
|
|
check(
|
|
'projects_exactly_one_accountable_owner_chk',
|
|
sql`num_nonnulls(${t.accountableUserId}, ${t.accountableTeamId}) = 1`,
|
|
),
|
|
check('projects_version_positive_chk', sql`${t.version} > 0`),
|
|
],
|
|
);
|
|
|
|
export const milestonesV1 = pgTable(
|
|
'milestones',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
projectId: uuid('project_id').notNull(),
|
|
name: text('name').notNull(),
|
|
description: text('description'),
|
|
status: milestoneStatusEnum('status').notNull().default('planned'),
|
|
sequence: integer('sequence').notNull(),
|
|
targetDate: timestamp('target_date', { withTimezone: true }),
|
|
completedAt: timestamp('completed_at', { withTimezone: true }),
|
|
providerMilestoneRef: text('provider_milestone_ref'),
|
|
acceptanceSummary: text('acceptance_summary'),
|
|
version: integer('version').notNull().default(1),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'milestones_workspace_project_fk',
|
|
columns: [t.workspaceId, t.projectId],
|
|
foreignColumns: [projectsV1.workspaceId, projectsV1.id],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('milestones_workspace_project_id_uidx').on(t.workspaceId, t.projectId, t.id),
|
|
uniqueIndex('milestones_project_sequence_uidx').on(t.workspaceId, t.projectId, t.sequence),
|
|
index('milestones_workspace_project_status_idx').on(t.workspaceId, t.projectId, t.status),
|
|
check('milestones_sequence_positive_chk', sql`${t.sequence} > 0`),
|
|
check('milestones_version_positive_chk', sql`${t.version} > 0`),
|
|
],
|
|
);
|
|
|
|
/** Avoids an unsafe circular projects.current_milestone FK during expand. */
|
|
export const projectCurrentMilestonesV1 = pgTable(
|
|
'project_current_milestones',
|
|
{
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
projectId: uuid('project_id').notNull(),
|
|
milestoneId: uuid('milestone_id').notNull(),
|
|
setByUserId: text('set_by_user_id').notNull(),
|
|
setAt: timestamp('set_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
primaryKey({ name: 'project_current_milestones_pk', columns: [t.workspaceId, t.projectId] }),
|
|
foreignKey({
|
|
name: 'project_current_milestones_project_fk',
|
|
columns: [t.workspaceId, t.projectId],
|
|
foreignColumns: [projectsV1.workspaceId, projectsV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'project_current_milestones_milestone_fk',
|
|
columns: [t.workspaceId, t.projectId, t.milestoneId],
|
|
foreignColumns: [milestonesV1.workspaceId, milestonesV1.projectId, milestonesV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'project_current_milestones_set_by_user_fk',
|
|
columns: [t.workspaceId, t.setByUserId],
|
|
foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId],
|
|
}).onDelete('restrict'),
|
|
],
|
|
);
|
|
|
|
export const missionsV1 = pgTable(
|
|
'missions',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
projectId: uuid('project_id').notNull(),
|
|
name: text('name').notNull(),
|
|
/** Legacy fields remain through N-1 and are mapped, never dropped on expand. */
|
|
legacyDescription: text('description'),
|
|
legacyStatus: text('status', {
|
|
enum: ['planning', 'active', 'paused', 'completed', 'failed'],
|
|
})
|
|
.notNull()
|
|
.default('planning'),
|
|
legacyUserId: text('user_id'),
|
|
legacyMilestones: jsonb('milestones').$type<Record<string, unknown>[]>(),
|
|
legacyConfig: jsonb('config'),
|
|
objective: text('objective').notNull(),
|
|
canonicalStatus: missionStatusEnum('canonical_status').notNull().default('draft'),
|
|
phase: text('phase'),
|
|
prdArtifactUri: text('prd_artifact_uri'),
|
|
prdRevision: text('prd_revision'),
|
|
portfolioOrchestratorId: text('portfolio_orchestrator_id'),
|
|
projectSubOrchestratorId: text('project_sub_orchestrator_id'),
|
|
approvalPolicy: jsonb('approval_policy').notNull().$type<Record<string, unknown>>().default({}),
|
|
startedAt: timestamp('started_at', { withTimezone: true }),
|
|
completedAt: timestamp('completed_at', { withTimezone: true }),
|
|
metadata: jsonb('metadata').notNull().$type<Record<string, unknown>>().default({}),
|
|
version: integer('version').notNull().default(1),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'missions_workspace_project_fk',
|
|
columns: [t.workspaceId, t.projectId],
|
|
foreignColumns: [projectsV1.workspaceId, projectsV1.id],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('missions_workspace_project_id_uidx').on(t.workspaceId, t.projectId, t.id),
|
|
index('missions_workspace_project_status_idx').on(
|
|
t.workspaceId,
|
|
t.projectId,
|
|
t.canonicalStatus,
|
|
),
|
|
check('missions_version_positive_chk', sql`${t.version} > 0`),
|
|
],
|
|
);
|
|
|
|
export const missionMilestonesV1 = pgTable(
|
|
'mission_milestones',
|
|
{
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
projectId: uuid('project_id').notNull(),
|
|
missionId: uuid('mission_id').notNull(),
|
|
milestoneId: uuid('milestone_id').notNull(),
|
|
ordering: integer('ordering').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
primaryKey({
|
|
name: 'mission_milestones_pk',
|
|
columns: [t.workspaceId, t.projectId, t.missionId, t.milestoneId],
|
|
}),
|
|
foreignKey({
|
|
name: 'mission_milestones_project_mission_fk',
|
|
columns: [t.workspaceId, t.projectId, t.missionId],
|
|
foreignColumns: [missionsV1.workspaceId, missionsV1.projectId, missionsV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'mission_milestones_project_milestone_fk',
|
|
columns: [t.workspaceId, t.projectId, t.milestoneId],
|
|
foreignColumns: [milestonesV1.workspaceId, milestonesV1.projectId, milestonesV1.id],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('mission_milestones_order_uidx').on(
|
|
t.workspaceId,
|
|
t.projectId,
|
|
t.missionId,
|
|
t.ordering,
|
|
),
|
|
check('mission_milestones_order_positive_chk', sql`${t.ordering} > 0`),
|
|
],
|
|
);
|
|
|
|
// ─── Tasks, tags, dependencies ───────────────────────────────────────────────
|
|
|
|
export const tasksV1 = pgTable(
|
|
'tasks',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
projectId: uuid('project_id').notNull(),
|
|
missionId: uuid('mission_id'),
|
|
milestoneId: uuid('milestone_id'),
|
|
parentTaskId: uuid('parent_task_id'),
|
|
title: text('title').notNull(),
|
|
description: text('description'),
|
|
/** Legacy/current-main fields retained throughout expand/N-1. */
|
|
legacyStatus: text('status', {
|
|
enum: ['not-started', 'in-progress', 'blocked', 'done', 'cancelled'],
|
|
})
|
|
.notNull()
|
|
.default('not-started'),
|
|
legacyAssignee: text('assignee'),
|
|
legacyTags: jsonb('tags').$type<string[]>(),
|
|
legacyDueDate: timestamp('due_date', { withTimezone: true }),
|
|
acceptanceCriteria: jsonb('acceptance_criteria')
|
|
.notNull()
|
|
.$type<Record<string, unknown> | string[]>()
|
|
.default([]),
|
|
canonicalStatus: taskStatusEnum('canonical_status').notNull().default('backlog'),
|
|
priority: priorityEnum('priority').notNull().default('medium'),
|
|
boardRank: numeric('board_rank', { precision: 30, scale: 15 }).notNull().default('1000'),
|
|
accountableUserId: text('accountable_user_id'),
|
|
accountableTeamId: uuid('accountable_team_id'),
|
|
assignedSpecialistRole: specialistRoleEnum('assigned_specialist_role'),
|
|
dueAt: timestamp('due_at', { withTimezone: true }),
|
|
notBeforeAt: timestamp('not_before_at', { withTimezone: true }),
|
|
estimateMinutes: integer('estimate_minutes'),
|
|
progressPercent: integer('progress_percent').notNull().default(0),
|
|
blocker: text('blocker'),
|
|
retryPolicy: jsonb('retry_policy').notNull().$type<Record<string, unknown>>().default({}),
|
|
/** Atomically incremented under this task row lock for every new lease. */
|
|
fencingCounter: bigint('fencing_counter', { mode: 'bigint' })
|
|
.notNull()
|
|
.default(sql`0`),
|
|
archivedAt: timestamp('archived_at', { withTimezone: true }),
|
|
archivedByUserId: text('archived_by_user_id'),
|
|
archiveReason: text('archive_reason'),
|
|
metadata: jsonb('metadata').notNull().$type<Record<string, unknown>>().default({}),
|
|
version: integer('version').notNull().default(1),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
completedAt: timestamp('completed_at', { withTimezone: true }),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'tasks_workspace_project_fk',
|
|
columns: [t.workspaceId, t.projectId],
|
|
foreignColumns: [projectsV1.workspaceId, projectsV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'tasks_project_mission_fk',
|
|
columns: [t.workspaceId, t.projectId, t.missionId],
|
|
foreignColumns: [missionsV1.workspaceId, missionsV1.projectId, missionsV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'tasks_project_milestone_fk',
|
|
columns: [t.workspaceId, t.projectId, t.milestoneId],
|
|
foreignColumns: [milestonesV1.workspaceId, milestonesV1.projectId, milestonesV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'tasks_project_parent_fk',
|
|
columns: [t.workspaceId, t.projectId, t.parentTaskId],
|
|
foreignColumns: [t.workspaceId, t.projectId, t.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'tasks_workspace_accountable_user_fk',
|
|
columns: [t.workspaceId, t.accountableUserId],
|
|
foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'tasks_workspace_archived_by_user_fk',
|
|
columns: [t.workspaceId, t.archivedByUserId],
|
|
foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'tasks_workspace_accountable_team_fk',
|
|
columns: [t.workspaceId, t.accountableTeamId],
|
|
foreignColumns: [teamsV1.workspaceId, teamsV1.id],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('tasks_workspace_id_uidx').on(t.workspaceId, t.id),
|
|
uniqueIndex('tasks_workspace_project_id_uidx').on(t.workspaceId, t.projectId, t.id),
|
|
index('tasks_workspace_project_status_rank_idx').on(
|
|
t.workspaceId,
|
|
t.projectId,
|
|
t.canonicalStatus,
|
|
t.boardRank,
|
|
),
|
|
index('tasks_workspace_due_idx').on(t.workspaceId, t.dueAt),
|
|
check(
|
|
'tasks_exactly_one_accountable_owner_chk',
|
|
sql`num_nonnulls(${t.accountableUserId}, ${t.accountableTeamId}) = 1`,
|
|
),
|
|
check('tasks_version_positive_chk', sql`${t.version} > 0`),
|
|
check('tasks_fencing_counter_nonnegative_chk', sql`${t.fencingCounter} >= 0`),
|
|
check(
|
|
'tasks_progress_percent_chk',
|
|
sql`${t.progressPercent} >= 0 and ${t.progressPercent} <= 100`,
|
|
),
|
|
check(
|
|
'tasks_archive_fields_chk',
|
|
sql`(${t.archivedAt} is null and ${t.archivedByUserId} is null and ${t.archiveReason} is null) or (${t.archivedAt} is not null and ${t.archivedByUserId} is not null and ${t.archiveReason} is not null)`,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const tagsV1 = pgTable(
|
|
'tags',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
name: text('name').notNull(),
|
|
normalizedName: text('normalized_name').notNull(),
|
|
color: text('color'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('tags_workspace_id_uidx').on(t.workspaceId, t.id),
|
|
uniqueIndex('tags_workspace_normalized_name_uidx').on(t.workspaceId, t.normalizedName),
|
|
],
|
|
);
|
|
|
|
export const taskTagsV1 = pgTable(
|
|
'task_tags',
|
|
{
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
taskId: uuid('task_id').notNull(),
|
|
tagId: uuid('tag_id').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
primaryKey({ name: 'task_tags_pk', columns: [t.workspaceId, t.taskId, t.tagId] }),
|
|
foreignKey({
|
|
name: 'task_tags_workspace_task_fk',
|
|
columns: [t.workspaceId, t.taskId],
|
|
foreignColumns: [tasksV1.workspaceId, tasksV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'task_tags_workspace_tag_fk',
|
|
columns: [t.workspaceId, t.tagId],
|
|
foreignColumns: [tagsV1.workspaceId, tagsV1.id],
|
|
}).onDelete('restrict'),
|
|
],
|
|
);
|
|
|
|
export const taskDependenciesV1 = pgTable(
|
|
'task_dependencies',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
predecessorTaskId: uuid('predecessor_task_id').notNull(),
|
|
successorTaskId: uuid('successor_task_id').notNull(),
|
|
dependencyType: dependencyTypeEnum('dependency_type').notNull().default('blocks'),
|
|
completionCondition: jsonb('completion_condition').$type<Record<string, unknown>>(),
|
|
createdByActorKind: actorKindEnum('created_by_actor_kind').notNull(),
|
|
createdByActorId: text('created_by_actor_id').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'task_dependencies_predecessor_fk',
|
|
columns: [t.workspaceId, t.predecessorTaskId],
|
|
foreignColumns: [tasksV1.workspaceId, tasksV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'task_dependencies_successor_fk',
|
|
columns: [t.workspaceId, t.successorTaskId],
|
|
foreignColumns: [tasksV1.workspaceId, tasksV1.id],
|
|
}).onDelete('restrict'),
|
|
/** One directed pair only; dependency type is an attribute, not a second edge. */
|
|
uniqueIndex('task_dependencies_directed_edge_uidx').on(
|
|
t.workspaceId,
|
|
t.predecessorTaskId,
|
|
t.successorTaskId,
|
|
),
|
|
index('task_dependencies_successor_idx').on(t.workspaceId, t.successorTaskId),
|
|
check(
|
|
'task_dependencies_no_self_edge_chk',
|
|
sql`${t.predecessorTaskId} <> ${t.successorTaskId}`,
|
|
),
|
|
],
|
|
);
|
|
|
|
// ─── Links, immutable artifacts, audit events, outage proposals ──────────────
|
|
|
|
export const externalLinksV1 = pgTable(
|
|
'external_links',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
entityType: text('entity_type', {
|
|
enum: ['project', 'mission', 'milestone', 'task'],
|
|
}).notNull(),
|
|
entityId: uuid('entity_id').notNull(),
|
|
provider: text('provider').notNull(),
|
|
linkType: text('link_type', {
|
|
enum: ['issue', 'pr', 'ci', 'document', 'release', 'deployment'],
|
|
}).notNull(),
|
|
externalId: text('external_id').notNull(),
|
|
url: text('url').notNull(),
|
|
repository: text('repository'),
|
|
syncMetadata: jsonb('sync_metadata').notNull().$type<Record<string, unknown>>().default({}),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('external_links_entity_provider_type_external_uidx').on(
|
|
t.workspaceId,
|
|
t.entityType,
|
|
t.entityId,
|
|
t.provider,
|
|
t.linkType,
|
|
t.externalId,
|
|
),
|
|
index('external_links_entity_idx').on(t.workspaceId, t.entityType, t.entityId),
|
|
],
|
|
);
|
|
|
|
export const artifactsV1 = pgTable(
|
|
'artifacts',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
taskId: uuid('task_id'),
|
|
missionId: uuid('mission_id'),
|
|
type: text('type').notNull(),
|
|
uri: text('uri').notNull(),
|
|
immutableRevision: text('immutable_revision').notNull(),
|
|
digest: text('digest').notNull(),
|
|
producerActorKind: actorKindEnum('producer_actor_kind').notNull(),
|
|
producerActorId: text('producer_actor_id').notNull(),
|
|
evidenceClassification: text('evidence_classification').notNull(),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'artifacts_workspace_task_fk',
|
|
columns: [t.workspaceId, t.taskId],
|
|
foreignColumns: [tasksV1.workspaceId, tasksV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'artifacts_workspace_mission_fk',
|
|
columns: [t.workspaceId, t.missionId],
|
|
foreignColumns: [missionsV1.workspaceId, missionsV1.id],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('artifacts_workspace_id_uidx').on(t.workspaceId, t.id),
|
|
uniqueIndex('artifacts_workspace_digest_uidx').on(t.workspaceId, t.digest),
|
|
check('artifacts_exactly_one_owner_chk', sql`num_nonnulls(${t.taskId}, ${t.missionId}) = 1`),
|
|
],
|
|
);
|
|
|
|
export const taskEventsV1 = pgTable(
|
|
'task_events',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
aggregateType: text('aggregate_type', {
|
|
enum: [
|
|
'workspace',
|
|
'project',
|
|
'mission',
|
|
'milestone',
|
|
'task',
|
|
'assignment',
|
|
'lease',
|
|
'change_proposal',
|
|
],
|
|
}).notNull(),
|
|
aggregateId: uuid('aggregate_id').notNull(),
|
|
eventType: text('event_type').notNull(),
|
|
actorKind: actorKindEnum('actor_kind').notNull(),
|
|
actorId: text('actor_id').notNull(),
|
|
correlationId: uuid('correlation_id').notNull(),
|
|
causationId: uuid('causation_id'),
|
|
idempotencyKey: text('idempotency_key').notNull(),
|
|
previousVersion: integer('previous_version'),
|
|
newVersion: integer('new_version'),
|
|
payload: jsonb('payload').notNull().$type<Record<string, unknown>>().default({}),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('task_events_workspace_id_uidx').on(t.workspaceId, t.id),
|
|
uniqueIndex('task_events_workspace_idempotency_uidx').on(t.workspaceId, t.idempotencyKey),
|
|
index('task_events_aggregate_created_idx').on(
|
|
t.workspaceId,
|
|
t.aggregateType,
|
|
t.aggregateId,
|
|
t.createdAt,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const changeProposalsV1 = pgTable(
|
|
'change_proposals',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
proposerUserId: text('proposer_user_id').notNull(),
|
|
sourceNoteDigest: text('source_note_digest').notNull(),
|
|
targetAggregateType: text('target_aggregate_type', {
|
|
enum: ['project', 'mission', 'milestone', 'task'],
|
|
}).notNull(),
|
|
targetAggregateId: uuid('target_aggregate_id').notNull(),
|
|
expectedAggregateVersion: integer('expected_aggregate_version').notNull(),
|
|
proposedCommand: text('proposed_command').notNull(),
|
|
proposedPayload: jsonb('proposed_payload').notNull().$type<Record<string, unknown>>(),
|
|
state: changeProposalStateEnum('state').notNull().default('pending'),
|
|
idempotencyKey: text('idempotency_key').notNull(),
|
|
submittedAuditEventId: uuid('submitted_audit_event_id').notNull(),
|
|
decisionActorUserId: text('decision_actor_user_id'),
|
|
decisionReason: text('decision_reason'),
|
|
decidedAt: timestamp('decided_at', { withTimezone: true }),
|
|
acceptedCommandAuditEventId: uuid('accepted_command_audit_event_id'),
|
|
version: integer('version').notNull().default(1),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'change_proposals_workspace_proposer_user_fk',
|
|
columns: [t.workspaceId, t.proposerUserId],
|
|
foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'change_proposals_workspace_decision_actor_user_fk',
|
|
columns: [t.workspaceId, t.decisionActorUserId],
|
|
foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'change_proposals_workspace_submitted_event_fk',
|
|
columns: [t.workspaceId, t.submittedAuditEventId],
|
|
foreignColumns: [taskEventsV1.workspaceId, taskEventsV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'change_proposals_workspace_accepted_command_event_fk',
|
|
columns: [t.workspaceId, t.acceptedCommandAuditEventId],
|
|
foreignColumns: [taskEventsV1.workspaceId, taskEventsV1.id],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('change_proposals_workspace_id_uidx').on(t.workspaceId, t.id),
|
|
uniqueIndex('change_proposals_workspace_idempotency_uidx').on(t.workspaceId, t.idempotencyKey),
|
|
index('change_proposals_workspace_target_state_idx').on(
|
|
t.workspaceId,
|
|
t.targetAggregateType,
|
|
t.targetAggregateId,
|
|
t.state,
|
|
),
|
|
check('change_proposals_expected_version_positive_chk', sql`${t.expectedAggregateVersion} > 0`),
|
|
check('change_proposals_version_positive_chk', sql`${t.version} > 0`),
|
|
check(
|
|
'change_proposals_decision_fields_chk',
|
|
sql`(${t.state} = 'pending' and ${t.decisionActorUserId} is null and ${t.decisionReason} is null and ${t.decidedAt} is null and ${t.acceptedCommandAuditEventId} is null) or (${t.state} = 'rejected' and ${t.decisionActorUserId} is not null and ${t.decisionReason} is not null and ${t.decidedAt} is not null and ${t.acceptedCommandAuditEventId} is null) or (${t.state} = 'accepted' and ${t.decisionActorUserId} is not null and ${t.decisionReason} is not null and ${t.decidedAt} is not null and ${t.acceptedCommandAuditEventId} is not null)`,
|
|
),
|
|
],
|
|
);
|
|
|
|
// ─── Assignments, execution, fencing, checkpoints ────────────────────────────
|
|
|
|
export const taskAssignmentsV1 = pgTable(
|
|
'task_assignments',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
taskId: uuid('task_id').notNull(),
|
|
taskVersion: integer('task_version').notNull(),
|
|
targetUserId: text('target_user_id'),
|
|
targetTeamId: uuid('target_team_id'),
|
|
targetAgentId: uuid('target_agent_id'),
|
|
targetSessionId: uuid('target_session_id'),
|
|
specialistRole: specialistRoleEnum('specialist_role').notNull(),
|
|
state: assignmentStateEnum('state').notNull().default('awaiting_approval'),
|
|
policyRevision: text('policy_revision').notNull(),
|
|
proposedByUserId: text('proposed_by_user_id'),
|
|
proposedByAgentId: uuid('proposed_by_agent_id'),
|
|
reason: text('reason').notNull(),
|
|
proposedAt: timestamp('proposed_at', { withTimezone: true }).notNull().defaultNow(),
|
|
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
|
approvedAt: timestamp('approved_at', { withTimezone: true }),
|
|
endedAt: timestamp('ended_at', { withTimezone: true }),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'task_assignments_workspace_task_fk',
|
|
columns: [t.workspaceId, t.taskId],
|
|
foreignColumns: [tasksV1.workspaceId, tasksV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'task_assignments_workspace_user_fk',
|
|
columns: [t.workspaceId, t.targetUserId],
|
|
foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'task_assignments_workspace_proposer_user_fk',
|
|
columns: [t.workspaceId, t.proposedByUserId],
|
|
foreignColumns: [workspaceMembersV1.workspaceId, workspaceMembersV1.userId],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'task_assignments_workspace_proposer_agent_fk',
|
|
columns: [t.workspaceId, t.proposedByAgentId],
|
|
foreignColumns: [agentsV1.workspaceId, agentsV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'task_assignments_workspace_team_fk',
|
|
columns: [t.workspaceId, t.targetTeamId],
|
|
foreignColumns: [teamsV1.workspaceId, teamsV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'task_assignments_workspace_agent_fk',
|
|
columns: [t.workspaceId, t.targetAgentId],
|
|
foreignColumns: [agentsV1.workspaceId, agentsV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'task_assignments_workspace_agent_session_fk',
|
|
columns: [t.workspaceId, t.targetAgentId, t.targetSessionId],
|
|
foreignColumns: [agentSessionsV1.workspaceId, agentSessionsV1.agentId, agentSessionsV1.id],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('task_assignments_workspace_id_uidx').on(t.workspaceId, t.id),
|
|
uniqueIndex('task_assignments_exact_target_uidx').on(
|
|
t.workspaceId,
|
|
t.taskId,
|
|
t.id,
|
|
t.targetAgentId,
|
|
t.targetSessionId,
|
|
),
|
|
index('task_assignments_workspace_task_state_idx').on(t.workspaceId, t.taskId, t.state),
|
|
check(
|
|
'task_assignments_exactly_one_principal_chk',
|
|
sql`num_nonnulls(${t.targetUserId}, ${t.targetTeamId}, ${t.targetAgentId}) = 1`,
|
|
),
|
|
check(
|
|
'task_assignments_exactly_one_proposer_chk',
|
|
sql`num_nonnulls(${t.proposedByUserId}, ${t.proposedByAgentId}) = 1`,
|
|
),
|
|
check(
|
|
'task_assignments_agent_session_pair_chk',
|
|
sql`(${t.targetAgentId} is null and ${t.targetSessionId} is null) or (${t.targetAgentId} is not null and ${t.targetSessionId} is not null)`,
|
|
),
|
|
check('task_assignments_task_version_positive_chk', sql`${t.taskVersion} > 0`),
|
|
],
|
|
);
|
|
|
|
export const taskExecutionStatesV1 = pgTable(
|
|
'task_execution_states',
|
|
{
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
taskId: uuid('task_id').notNull(),
|
|
disposition: executionDispositionEnum('disposition').notNull().default('available'),
|
|
attemptCount: integer('attempt_count').notNull().default(0),
|
|
maxAttempts: integer('max_attempts').notNull(),
|
|
nextEligibleAt: timestamp('next_eligible_at', { withTimezone: true }),
|
|
terminalReason: text('terminal_reason'),
|
|
updatedByActorKind: actorKindEnum('updated_by_actor_kind').notNull(),
|
|
updatedByActorId: text('updated_by_actor_id').notNull(),
|
|
policyRevision: text('policy_revision').notNull(),
|
|
version: integer('version').notNull().default(1),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
primaryKey({ name: 'task_execution_states_pk', columns: [t.workspaceId, t.taskId] }),
|
|
foreignKey({
|
|
name: 'task_execution_states_workspace_task_fk',
|
|
columns: [t.workspaceId, t.taskId],
|
|
foreignColumns: [tasksV1.workspaceId, tasksV1.id],
|
|
}).onDelete('restrict'),
|
|
index('task_execution_states_disposition_next_idx').on(
|
|
t.workspaceId,
|
|
t.disposition,
|
|
t.nextEligibleAt,
|
|
),
|
|
check('task_execution_states_attempt_nonnegative_chk', sql`${t.attemptCount} >= 0`),
|
|
check('task_execution_states_max_positive_chk', sql`${t.maxAttempts} > 0`),
|
|
check('task_execution_states_attempt_bound_chk', sql`${t.attemptCount} <= ${t.maxAttempts}`),
|
|
check('task_execution_states_version_positive_chk', sql`${t.version} > 0`),
|
|
],
|
|
);
|
|
|
|
export const taskLeasesV1 = pgTable(
|
|
'task_leases',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
taskId: uuid('task_id').notNull(),
|
|
assignmentId: uuid('assignment_id').notNull(),
|
|
agentId: uuid('agent_id').notNull(),
|
|
agentSessionId: uuid('agent_session_id').notNull(),
|
|
state: leaseStateEnum('state').notNull().default('pending_ack'),
|
|
acquiredAt: timestamp('acquired_at', { withTimezone: true }).notNull().defaultNow(),
|
|
acknowledgedAt: timestamp('acknowledged_at', { withTimezone: true }),
|
|
acknowledgeBy: timestamp('acknowledge_by', { withTimezone: true }).notNull(),
|
|
lastHeartbeatAt: timestamp('last_heartbeat_at', { withTimezone: true }),
|
|
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
|
/** Exact value atomically returned from tasks.fencing_counter. */
|
|
fencingToken: bigint('fencing_token', { mode: 'bigint' }).notNull(),
|
|
attemptNumber: integer('attempt_number').notNull(),
|
|
releaseReason: text('release_reason'),
|
|
releasedAt: timestamp('released_at', { withTimezone: true }),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'task_leases_exact_assignment_target_fk',
|
|
columns: [t.workspaceId, t.taskId, t.assignmentId, t.agentId, t.agentSessionId],
|
|
foreignColumns: [
|
|
taskAssignmentsV1.workspaceId,
|
|
taskAssignmentsV1.taskId,
|
|
taskAssignmentsV1.id,
|
|
taskAssignmentsV1.targetAgentId,
|
|
taskAssignmentsV1.targetSessionId,
|
|
],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('task_leases_active_task_uidx')
|
|
.on(t.workspaceId, t.taskId)
|
|
.where(sql`${t.state} in ('pending_ack', 'active')`),
|
|
uniqueIndex('task_leases_exact_fence_uidx').on(t.workspaceId, t.taskId, t.id, t.fencingToken),
|
|
uniqueIndex('task_leases_task_fence_uidx').on(t.workspaceId, t.taskId, t.fencingToken),
|
|
index('task_leases_workspace_state_expiry_idx').on(t.workspaceId, t.state, t.expiresAt),
|
|
check('task_leases_fence_positive_chk', sql`${t.fencingToken} > 0`),
|
|
check('task_leases_attempt_positive_chk', sql`${t.attemptNumber} > 0`),
|
|
],
|
|
);
|
|
|
|
export const taskCheckpointsV1 = pgTable(
|
|
'task_checkpoints',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
taskId: uuid('task_id').notNull(),
|
|
leaseId: uuid('lease_id').notNull(),
|
|
fencingToken: bigint('fencing_token', { mode: 'bigint' }).notNull(),
|
|
sequence: integer('sequence').notNull(),
|
|
resumableSummary: text('resumable_summary').notNull(),
|
|
contextUsagePercent: integer('context_usage_percent'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'task_checkpoints_exact_lease_fence_fk',
|
|
columns: [t.workspaceId, t.taskId, t.leaseId, t.fencingToken],
|
|
foreignColumns: [
|
|
taskLeasesV1.workspaceId,
|
|
taskLeasesV1.taskId,
|
|
taskLeasesV1.id,
|
|
taskLeasesV1.fencingToken,
|
|
],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('task_checkpoints_workspace_task_id_uidx').on(t.workspaceId, t.taskId, t.id),
|
|
uniqueIndex('task_checkpoints_lease_sequence_uidx').on(t.workspaceId, t.leaseId, t.sequence),
|
|
index('task_checkpoints_task_created_idx').on(t.workspaceId, t.taskId, t.createdAt),
|
|
check('task_checkpoints_sequence_positive_chk', sql`${t.sequence} > 0`),
|
|
check('task_checkpoints_fence_positive_chk', sql`${t.fencingToken} > 0`),
|
|
check(
|
|
'task_checkpoints_context_percent_chk',
|
|
sql`${t.contextUsagePercent} is null or (${t.contextUsagePercent} >= 0 and ${t.contextUsagePercent} <= 100)`,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const checkpointArtifactsV1 = pgTable(
|
|
'task_checkpoint_artifacts',
|
|
{
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
taskId: uuid('task_id').notNull(),
|
|
checkpointId: uuid('checkpoint_id').notNull(),
|
|
artifactId: uuid('artifact_id').notNull(),
|
|
},
|
|
(t) => [
|
|
primaryKey({
|
|
name: 'task_checkpoint_artifacts_pk',
|
|
columns: [t.workspaceId, t.checkpointId, t.artifactId],
|
|
}),
|
|
foreignKey({
|
|
name: 'task_checkpoint_artifacts_checkpoint_fk',
|
|
columns: [t.workspaceId, t.taskId, t.checkpointId],
|
|
foreignColumns: [
|
|
taskCheckpointsV1.workspaceId,
|
|
taskCheckpointsV1.taskId,
|
|
taskCheckpointsV1.id,
|
|
],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'task_checkpoint_artifacts_artifact_fk',
|
|
columns: [t.workspaceId, t.artifactId],
|
|
foreignColumns: [artifactsV1.workspaceId, artifactsV1.id],
|
|
}).onDelete('restrict'),
|
|
],
|
|
);
|
|
|
|
// ─── Approvals, evidence, outbox ─────────────────────────────────────────────
|
|
|
|
export const approvalDecisionsV1 = pgTable(
|
|
'approval_decisions',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
taskId: uuid('task_id'),
|
|
missionId: uuid('mission_id'),
|
|
assignmentId: uuid('assignment_id'),
|
|
gateType: text('gate_type').notNull(),
|
|
requestedFromRole: specialistRoleEnum('requested_from_role'),
|
|
decision: approvalDecisionEnum('decision').notNull().default('requested'),
|
|
conditions: jsonb('conditions').notNull().$type<Record<string, unknown>>().default({}),
|
|
actorKind: actorKindEnum('actor_kind'),
|
|
actorId: text('actor_id'),
|
|
policyRevision: text('policy_revision').notNull(),
|
|
reason: text('reason'),
|
|
requestedAt: timestamp('requested_at', { withTimezone: true }).notNull().defaultNow(),
|
|
decidedAt: timestamp('decided_at', { withTimezone: true }),
|
|
},
|
|
(t) => [
|
|
foreignKey({
|
|
name: 'approval_decisions_workspace_task_fk',
|
|
columns: [t.workspaceId, t.taskId],
|
|
foreignColumns: [tasksV1.workspaceId, tasksV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'approval_decisions_workspace_mission_fk',
|
|
columns: [t.workspaceId, t.missionId],
|
|
foreignColumns: [missionsV1.workspaceId, missionsV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'approval_decisions_workspace_assignment_fk',
|
|
columns: [t.workspaceId, t.assignmentId],
|
|
foreignColumns: [taskAssignmentsV1.workspaceId, taskAssignmentsV1.id],
|
|
}).onDelete('restrict'),
|
|
uniqueIndex('approval_decisions_workspace_id_uidx').on(t.workspaceId, t.id),
|
|
index('approval_decisions_assignment_idx').on(t.workspaceId, t.assignmentId, t.decision),
|
|
check(
|
|
'approval_decisions_one_target_chk',
|
|
sql`num_nonnulls(${t.taskId}, ${t.missionId}, ${t.assignmentId}) = 1`,
|
|
),
|
|
],
|
|
);
|
|
|
|
export const approvalDecisionArtifactsV1 = pgTable(
|
|
'approval_decision_artifacts',
|
|
{
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
approvalDecisionId: uuid('approval_decision_id').notNull(),
|
|
artifactId: uuid('artifact_id').notNull(),
|
|
},
|
|
(t) => [
|
|
primaryKey({
|
|
name: 'approval_decision_artifacts_pk',
|
|
columns: [t.workspaceId, t.approvalDecisionId, t.artifactId],
|
|
}),
|
|
foreignKey({
|
|
name: 'approval_decision_artifacts_decision_fk',
|
|
columns: [t.workspaceId, t.approvalDecisionId],
|
|
foreignColumns: [approvalDecisionsV1.workspaceId, approvalDecisionsV1.id],
|
|
}).onDelete('restrict'),
|
|
foreignKey({
|
|
name: 'approval_decision_artifacts_artifact_fk',
|
|
columns: [t.workspaceId, t.artifactId],
|
|
foreignColumns: [artifactsV1.workspaceId, artifactsV1.id],
|
|
}).onDelete('restrict'),
|
|
],
|
|
);
|
|
|
|
export const outboxEventsV1 = pgTable(
|
|
'outbox_events',
|
|
{
|
|
id: uuid('id').primaryKey().defaultRandom(),
|
|
workspaceId: uuid('workspace_id')
|
|
.notNull()
|
|
.references(() => workspacesV1.id, { onDelete: 'restrict' }),
|
|
aggregateType: text('aggregate_type').notNull(),
|
|
aggregateId: uuid('aggregate_id').notNull(),
|
|
aggregateRevision: integer('aggregate_revision').notNull(),
|
|
eventType: text('event_type').notNull(),
|
|
payload: jsonb('payload').notNull().$type<Record<string, unknown>>(),
|
|
state: outboxStateEnum('state').notNull().default('pending'),
|
|
attempts: integer('attempts').notNull().default(0),
|
|
nextAttemptAt: timestamp('next_attempt_at', { withTimezone: true }),
|
|
publishedAt: timestamp('published_at', { withTimezone: true }),
|
|
lastError: text('last_error'),
|
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
},
|
|
(t) => [
|
|
uniqueIndex('outbox_events_aggregate_revision_type_uidx').on(
|
|
t.workspaceId,
|
|
t.aggregateType,
|
|
t.aggregateId,
|
|
t.aggregateRevision,
|
|
t.eventType,
|
|
),
|
|
index('outbox_events_state_next_attempt_idx').on(t.state, t.nextAttemptAt),
|
|
check('outbox_events_revision_positive_chk', sql`${t.aggregateRevision} > 0`),
|
|
check('outbox_events_attempts_nonnegative_chk', sql`${t.attempts} >= 0`),
|
|
],
|
|
);
|
|
|
|
/**
|
|
* Mandatory transaction/privilege rules frozen with this schema:
|
|
*
|
|
* 1. Every user principal/owner/proposer/decision actor must have ACTIVE
|
|
* workspace_members membership; denials expose no foreign-ID existence.
|
|
* 2. roleSet/roles JSON values are validated against specialist_role_v1 until
|
|
* normalized role joins are introduced; the primary/assigned role is enum.
|
|
* 3. Dependency cycles are rejected under a serialized recursive check.
|
|
* 4. task_events is declared before change_proposals so migration DDL creates
|
|
* task_events_workspace_id_uidx before both workspace-aware proposal event
|
|
* FKs. Submission preallocates the proposal ID and, in one transaction,
|
|
* inserts `change_proposal.submitted` with aggregate_type=change_proposal,
|
|
* aggregate_id=proposal.id, previous_version=NULL, new_version=1, then the
|
|
* proposal referencing that event. Missing or foreign-workspace events fail.
|
|
* 5. change_proposals never mutate targets directly. Acceptance locks proposal
|
|
* and target, proves fresh PG write health, checks expected version, invokes
|
|
* the NORMAL typed command, and in that same transaction binds its emitted
|
|
* event. The event workspace/aggregate type/aggregate ID must equal the
|
|
* proposal workspace/target, causation_id must equal submittedAuditEventId,
|
|
* and payload.changeProposalId must equal proposal.id; unrelated events fail.
|
|
* 6. Lease acquisition locks task+assignment+approval+session, increments
|
|
* tasks.fencing_counter atomically, and uses RETURNING bigint as the token.
|
|
* 7. task_events, task_checkpoints, checkpoint/artifact evidence, approval
|
|
* evidence, and immutable artifacts grant application roles INSERT/SELECT
|
|
* only. Canonical parents are archived, not hard-deleted; all parent FKs use
|
|
* RESTRICT. Purge requires an audited retention/break-glass procedure.
|
|
* 8. Polymorphic external_links and change_proposals targets are validated in a
|
|
* workspace-scoped transaction before insert; no existence oracle.
|
|
* 9. Legacy fields remain in the unified Drizzle declaration throughout expand
|
|
* and N-1. New Gateway commands never accept mission_tasks.status as a write
|
|
* source; its concrete retirement is specified in SHARED-CONTRACT.md.
|
|
*/
|