Files
stack/docs/native-kanban-sot/contracts/kanban-schema.v1.ts
jason.woltje 49e8a54105
All checks were successful
ci/woodpecker/push/publish Pipeline was successful
ci/woodpecker/push/ci Pipeline was successful
docs(#751): Publish native Kanban/SOT canon (#752)
2026-07-14 17:08:09 +00:00

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.
*/