Files
mosaic/packages/brain/src/schemas.ts
Jason Woltje 1aa11c4ee8
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/pr/woodpecker Pipeline failed
ci/woodpecker/pull_request_closed/woodpecker Pipeline failed
feat(brain): @mosaic/brain structured data service (#10)
Implement @mosaic/brain — typed structured data service with MCP + REST API,
JSON file backend, and schema validation via Zod.

Collections: tasks, projects, events, agents, tickets, appreciations,
missions, mission_tasks.

MCP tools: brain_tasks, brain_projects, brain_events, brain_agents,
brain_tickets, brain_today, brain_stale, brain_stats, brain_search,
brain_audit, brain_missions, brain_mission, brain_mission_tasks,
plus mutation tools for all collections.

REST API mirrors MCP 1:1 at /v1/*.
Bearer token auth with timing-safe comparison.
Fastify server with per-request MCP instances (stateless HTTP transport).
JSON file storage with proper-lockfile for concurrent access.

Also adds Brain* types to @mosaic/types.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 20:10:12 -05:00

223 lines
6.9 KiB
TypeScript

import { z } from 'zod';
// === Enums ===
export const BrainDomainSchema = z.enum([
'work', 'software-dev', 'homelab', 'family', 'marriage',
'finances', 'fitness', 'music', 'home-improvement', 'woodworking',
'home', 'consulting', 'personal',
]);
export const TaskPrioritySchema = z.enum(['critical', 'high', 'medium', 'low']);
export const BrainTaskStatusSchema = z.enum([
'backlog', 'scheduled', 'in-progress', 'blocked', 'done', 'cancelled',
]);
export const BrainProjectStatusSchema = z.enum([
'planning', 'active', 'paused', 'blocked', 'completed', 'archived',
]);
export const BrainAgentStatusSchema = z.enum(['active', 'idle', 'blocked', 'completed']);
export const BrainEventTypeSchema = z.enum([
'meeting', 'deadline', 'maintenance', 'event', 'recurring',
'milestone', 'task', 'constraint', 'client-work', 'appointment',
'reminder', 'conflict', 'time-off',
]);
export const BrainEventStatusSchema = z.enum([
'scheduled', 'confirmed', 'tentative', 'completed', 'cancelled',
'done', 'blocked', 'postponed', 'deferred', 'in-progress',
'pending-approval', 'canceled', 'needs-resolution',
]);
export const BrainMissionStatusSchema = z.enum([
'planning', 'active', 'blocked', 'completed', 'cancelled',
]);
// === Record Schemas ===
export const BrainTaskSchema = z.object({
id: z.string().regex(/^[a-z0-9-]+$/),
title: z.string().min(1),
domain: BrainDomainSchema,
project: z.string().nullish(),
priority: TaskPrioritySchema,
status: BrainTaskStatusSchema,
progress: z.number().int().min(0).max(100).nullish(),
due: z.string().nullish(),
blocks: z.array(z.string()).optional().default([]),
blocked_by: z.array(z.string()).optional().default([]),
related: z.array(z.string()).optional().default([]),
canonical_source: z.string().nullish(),
assignee: z.string().nullish(),
created: z.string(),
updated: z.string(),
notes: z.string().nullish(),
notes_nontechnical: z.string().nullish(),
});
export const BrainProjectSchema = z.object({
id: z.string().regex(/^[a-z0-9-]+$/),
name: z.string().min(1),
description: z.string().nullish(),
domain: BrainDomainSchema,
status: BrainProjectStatusSchema,
priority: z.number().int().min(1).max(10),
progress: z.number().int().min(0).max(100).nullish(),
repo: z.string().nullish(),
branch: z.string().nullish(),
current_milestone: z.string().nullish(),
next_milestone: z.string().nullish(),
blocker: z.string().nullish(),
owner: z.string().nullish(),
docs_path: z.string().nullish(),
created: z.string(),
updated: z.string(),
notes: z.string().nullish(),
notes_nontechnical: z.string().nullish(),
});
export const BrainEventSchema = z.object({
id: z.string().regex(/^[a-z0-9-]+$/),
title: z.string().min(1),
date: z.string(),
end_date: z.string().nullish(),
time: z.string().nullish(),
end_time: z.string().nullish(),
domain: BrainDomainSchema,
type: BrainEventTypeSchema,
status: BrainEventStatusSchema.optional().default('scheduled'),
priority: TaskPrioritySchema.nullish(),
recur: z.boolean().nullish(),
recur_rate: z.string().nullish(),
recur_start: z.string().nullish(),
recur_end: z.string().nullish(),
location: z.string().nullish(),
project: z.string().nullish(),
related_task: z.string().nullish(),
related_tasks: z.array(z.string()).optional().default([]),
notes: z.string().nullish(),
gcal_id: z.string().nullish(),
ics_uid: z.string().nullish(),
});
export const BrainAgentSchema = z.object({
id: z.string(),
project: z.string(),
focus: z.string().nullish(),
repo: z.string().nullish(),
branch: z.string().nullish(),
status: BrainAgentStatusSchema,
workload: z.enum(['light', 'medium', 'heavy']).nullish(),
next_step: z.string().nullish(),
blocker: z.string().nullish(),
updated: z.string(),
});
export const BrainTicketSchema = z.object({
id: z.string(),
title: z.string().min(1),
status: z.number().int(),
priority: z.number().int(),
urgency: z.number().int(),
impact: z.number().int(),
date_creation: z.string(),
date_mod: z.string(),
content: z.string().nullish(),
assigned_to: z.string().nullish(),
});
export const BrainAppreciationSchema = z.object({
date: z.string(),
from: z.enum(['jason', 'melanie']),
to: z.enum(['jason', 'melanie']),
text: z.string().min(1),
});
export const BrainMissionSchema = z.object({
id: z.string(),
title: z.string().min(1),
project: z.string(),
prd_path: z.string().nullish(),
status: BrainMissionStatusSchema,
created: z.string(),
updated: z.string(),
notes: z.string().nullish(),
});
export const BrainMissionTaskSchema = z.object({
id: z.string(),
mission_id: z.string(),
title: z.string().min(1),
phase: z.string().nullish(),
status: BrainTaskStatusSchema,
priority: TaskPrioritySchema,
dependencies: z.array(z.string()).default([]),
assigned_to: z.string().nullish(),
pr: z.string().nullish(),
order: z.number().int(),
created: z.string(),
updated: z.string(),
completed_at: z.string().nullish(),
notes: z.string().nullish(),
});
// === Partial schemas for updates ===
export const BrainTaskUpdateSchema = BrainTaskSchema.partial().omit({ id: true });
export const BrainProjectUpdateSchema = BrainProjectSchema.partial().omit({ id: true });
export const BrainEventUpdateSchema = BrainEventSchema.partial().omit({ id: true });
export const BrainAgentUpdateSchema = BrainAgentSchema.partial().omit({ id: true });
export const BrainMissionUpdateSchema = BrainMissionSchema.partial().omit({ id: true });
export const BrainMissionTaskUpdateSchema = BrainMissionTaskSchema.partial().omit({ id: true, mission_id: true });
// === Collection file schemas ===
export const TasksFileSchema = z.object({
version: z.string(),
tasks: z.array(BrainTaskSchema),
});
export const ProjectsFileSchema = z.object({
version: z.string(),
projects: z.array(BrainProjectSchema),
});
export const EventsFileSchema = z.object({
version: z.string(),
domain: BrainDomainSchema,
events: z.array(BrainEventSchema),
overdue: z.array(z.object({
id: z.string(),
title: z.string(),
original_due: z.string().optional(),
domain: z.string(),
notes: z.string().nullish(),
})).default([]),
});
export const AgentsFileSchema = z.object({
version: z.string(),
agents: z.array(BrainAgentSchema),
});
export const MissionsFileSchema = z.object({
version: z.string(),
missions: z.array(BrainMissionSchema),
});
export const MissionTasksFileSchema = z.object({
version: z.string(),
mission_tasks: z.array(BrainMissionTaskSchema),
});
// Inferred types for convenience
export type BrainTaskInput = z.input<typeof BrainTaskSchema>;
export type BrainProjectInput = z.input<typeof BrainProjectSchema>;
export type BrainEventInput = z.input<typeof BrainEventSchema>;
export type BrainAgentInput = z.input<typeof BrainAgentSchema>;
export type BrainMissionInput = z.input<typeof BrainMissionSchema>;
export type BrainMissionTaskInput = z.input<typeof BrainMissionTaskSchema>;