import type { BrainTask, BrainProject, BrainEvent, BrainAgent, BrainTicket, BrainAppreciation, BrainMission, BrainMissionTask, BrainTaskFilters, BrainProjectFilters, BrainEventFilters, BrainMissionFilters, BrainMissionTaskFilters, BrainMissionSummary, BrainTodaySummary, BrainStats, BrainStaleReport, BrainAuditResult, BrainSearchResult, TaskPriority, } from '@mosaic/types'; import { JsonStore } from './json-store.js'; function today(): string { return new Date().toISOString().slice(0, 10); } interface TasksFile { version: string; tasks: BrainTask[] } interface ProjectsFile { version: string; projects: BrainProject[] } interface EventsFile { version: string; events: BrainEvent[] } interface AgentsFile { version: string; agents: BrainAgent[] } interface TicketsFile { version: string; tickets: BrainTicket[] } interface AppreciationsFile { version: string; appreciations: BrainAppreciation[] } interface MissionsFile { version: string; missions: BrainMission[] } interface MissionTasksFile { version: string; mission_tasks: BrainMissionTask[] } const DEFAULT_TASKS: TasksFile = { version: '2.0', tasks: [] }; const DEFAULT_PROJECTS: ProjectsFile = { version: '2.0', projects: [] }; const DEFAULT_EVENTS: EventsFile = { version: '2.0', events: [] }; const DEFAULT_AGENTS: AgentsFile = { version: '2.0', agents: [] }; const DEFAULT_TICKETS: TicketsFile = { version: '2.0', tickets: [] }; const DEFAULT_APPRECIATIONS: AppreciationsFile = { version: '2.0', appreciations: [] }; const DEFAULT_MISSIONS: MissionsFile = { version: '2.0', missions: [] }; const DEFAULT_MISSION_TASKS: MissionTasksFile = { version: '2.0', mission_tasks: [] }; const PRIORITY_ORDER: Record = { critical: 0, high: 1, medium: 2, low: 3 }; function matchesFilter(item: T, filters: object): boolean { for (const [key, value] of Object.entries(filters as Record)) { if (value === undefined || value === null) continue; if (key === 'limit') continue; if (key === 'due_before') { if (!(item as Record)['due'] || (item as Record)['due']! > value) return false; continue; } if (key === 'due_after') { if (!(item as Record)['due'] || (item as Record)['due']! < value) return false; continue; } if (key === 'date_from') { if ((item as Record)['date']! < value) return false; continue; } if (key === 'date_to') { if ((item as Record)['date']! > value) return false; continue; } if (key === 'priority_min') { if (((item as Record)['priority'] as number) < (value as number)) return false; continue; } if (key === 'priority_max') { if (((item as Record)['priority'] as number) > (value as number)) return false; continue; } if ((item as Record)[key] !== value) return false; } return true; } function applyLimit(items: T[], limit?: number): T[] { return limit != null && limit > 0 ? items.slice(0, limit) : items; } export class Collections { constructor(private readonly store: JsonStore) {} // === Tasks === async getTasks(filters: BrainTaskFilters = {}): Promise { const file = await this.store.read('tasks', DEFAULT_TASKS); const filtered = file.tasks.filter(t => matchesFilter(t, filters)); filtered.sort((a, b) => (PRIORITY_ORDER[a.priority] ?? 9) - (PRIORITY_ORDER[b.priority] ?? 9) || (a.due ?? '9999').localeCompare(b.due ?? '9999'), ); return applyLimit(filtered, filters.limit); } async getTask(id: string): Promise { const file = await this.store.read('tasks', DEFAULT_TASKS); return file.tasks.find(t => t.id === id) ?? null; } async addTask(task: BrainTask): Promise { const result = await this.store.modify('tasks', DEFAULT_TASKS, (file) => { if (file.tasks.some(t => t.id === task.id)) { throw new Error(`Task '${task.id}' already exists`); } return { ...file, tasks: [...file.tasks, task] }; }); return result.tasks.find(t => t.id === task.id)!; } async updateTask(id: string, updates: Partial): Promise { let updated: BrainTask | undefined; await this.store.modify('tasks', DEFAULT_TASKS, (file) => { const idx = file.tasks.findIndex(t => t.id === id); if (idx === -1) throw new Error(`Task '${id}' not found`); const tasks = [...file.tasks]; updated = { ...tasks[idx]!, ...updates, id, updated: today() } as BrainTask; tasks[idx] = updated; return { ...file, tasks }; }); return updated!; } // === Projects === async getProjects(filters: BrainProjectFilters = {}): Promise { const file = await this.store.read('projects', DEFAULT_PROJECTS); const filtered = file.projects.filter(p => matchesFilter(p, filters)); filtered.sort((a, b) => a.priority - b.priority); return applyLimit(filtered, filters.limit); } async getProject(id: string): Promise { const file = await this.store.read('projects', DEFAULT_PROJECTS); return file.projects.find(p => p.id === id) ?? null; } async addProject(project: BrainProject): Promise { const result = await this.store.modify('projects', DEFAULT_PROJECTS, (file) => { if (file.projects.some(p => p.id === project.id)) { throw new Error(`Project '${project.id}' already exists`); } return { ...file, projects: [...file.projects, project] }; }); return result.projects.find(p => p.id === project.id)!; } async updateProject(id: string, updates: Partial): Promise { let updated: BrainProject | undefined; await this.store.modify('projects', DEFAULT_PROJECTS, (file) => { const idx = file.projects.findIndex(p => p.id === id); if (idx === -1) throw new Error(`Project '${id}' not found`); const projects = [...file.projects]; updated = { ...projects[idx]!, ...updates, id, updated: today() } as BrainProject; projects[idx] = updated; return { ...file, projects }; }); return updated!; } // === Events === async getEvents(filters: BrainEventFilters = {}): Promise { const file = await this.store.read('events', DEFAULT_EVENTS); const filtered = file.events.filter(e => matchesFilter(e, filters)); filtered.sort((a, b) => a.date.localeCompare(b.date) || (a.time ?? '').localeCompare(b.time ?? '')); return applyLimit(filtered, filters.limit); } async getEvent(id: string): Promise { const file = await this.store.read('events', DEFAULT_EVENTS); return file.events.find(e => e.id === id) ?? null; } async addEvent(event: BrainEvent): Promise { const result = await this.store.modify('events', DEFAULT_EVENTS, (file) => { if (file.events.some(e => e.id === event.id)) { throw new Error(`Event '${event.id}' already exists`); } return { ...file, events: [...file.events, event] }; }); return result.events.find(e => e.id === event.id)!; } async updateEvent(id: string, updates: Partial): Promise { let updated: BrainEvent | undefined; await this.store.modify('events', DEFAULT_EVENTS, (file) => { const idx = file.events.findIndex(e => e.id === id); if (idx === -1) throw new Error(`Event '${id}' not found`); const events = [...file.events]; updated = { ...events[idx]!, ...updates, id } as BrainEvent; events[idx] = updated; return { ...file, events }; }); return updated!; } // === Agents === async getAgents(filters: { status?: string; project?: string } = {}): Promise { const file = await this.store.read('agents', DEFAULT_AGENTS); return file.agents.filter(a => matchesFilter(a, filters)); } async updateAgent(id: string, updates: Partial): Promise { let updated: BrainAgent | undefined; await this.store.modify('agents', DEFAULT_AGENTS, (file) => { const idx = file.agents.findIndex(a => a.id === id); if (idx === -1) { // Auto-create agent entries updated = { id, project: '', status: 'active', updated: new Date().toISOString(), ...updates } as BrainAgent; return { ...file, agents: [...file.agents, updated] }; } const agents = [...file.agents]; updated = { ...agents[idx]!, ...updates, id, updated: new Date().toISOString() } as BrainAgent; agents[idx] = updated; return { ...file, agents }; }); return updated!; } // === Tickets === async getTickets(filters: { status?: number; priority?: number; limit?: number } = {}): Promise { const file = await this.store.read('tickets', DEFAULT_TICKETS); let filtered = file.tickets; if (filters.status !== undefined) filtered = filtered.filter(t => t.status === filters.status); if (filters.priority !== undefined) filtered = filtered.filter(t => t.priority === filters.priority); return applyLimit(filtered, filters.limit); } // === Appreciations === async getAppreciations(): Promise { const file = await this.store.read('appreciations', DEFAULT_APPRECIATIONS); return file.appreciations; } async addAppreciation(appreciation: BrainAppreciation): Promise { await this.store.modify('appreciations', DEFAULT_APPRECIATIONS, (file) => ({ ...file, appreciations: [...file.appreciations, appreciation], })); return appreciation; } // === Missions === async getMissions(filters: BrainMissionFilters = {}): Promise { const file = await this.store.read('missions', DEFAULT_MISSIONS); const filtered = file.missions.filter(m => matchesFilter(m, filters)); return applyLimit(filtered, filters.limit); } async getMission(id: string): Promise { const file = await this.store.read('missions', DEFAULT_MISSIONS); return file.missions.find(m => m.id === id) ?? null; } async addMission(mission: BrainMission): Promise { const result = await this.store.modify('missions', DEFAULT_MISSIONS, (file) => { if (file.missions.some(m => m.id === mission.id)) { throw new Error(`Mission '${mission.id}' already exists`); } return { ...file, missions: [...file.missions, mission] }; }); return result.missions.find(m => m.id === mission.id)!; } async updateMission(id: string, updates: Partial): Promise { let updated: BrainMission | undefined; await this.store.modify('missions', DEFAULT_MISSIONS, (file) => { const idx = file.missions.findIndex(m => m.id === id); if (idx === -1) throw new Error(`Mission '${id}' not found`); const missions = [...file.missions]; updated = { ...missions[idx]!, ...updates, id, updated: today() } as BrainMission; missions[idx] = updated; return { ...file, missions }; }); return updated!; } async getMissionSummary(id: string): Promise { const mission = await this.getMission(id); if (!mission) return null; const tasks = await this.getMissionTasks({ mission_id: id }); const completed = tasks.filter(t => t.status === 'done'); const completedIds = new Set(completed.map(t => t.id)); const nextAvailable = tasks.filter(t => t.status !== 'done' && t.status !== 'cancelled' && t.status !== 'blocked' && t.dependencies.every(dep => completedIds.has(dep)) && !t.assigned_to, ); const blockedTasks = tasks.filter(t => t.status === 'blocked' || (t.status !== 'done' && t.status !== 'cancelled' && !t.dependencies.every(dep => completedIds.has(dep))), ); return { ...mission, task_count: tasks.length, completed_count: completed.length, progress: tasks.length > 0 ? Math.round((completed.length / tasks.length) * 100) : 0, next_available: nextAvailable, blocked_tasks: blockedTasks, }; } // === Mission Tasks === async getMissionTasks(filters: BrainMissionTaskFilters): Promise { const file = await this.store.read('mission_tasks', DEFAULT_MISSION_TASKS); const filtered = file.mission_tasks.filter(t => matchesFilter(t, filters)); filtered.sort((a, b) => a.order - b.order); return filtered; } async addMissionTask(task: BrainMissionTask): Promise { const result = await this.store.modify('mission_tasks', DEFAULT_MISSION_TASKS, (file) => { if (file.mission_tasks.some(t => t.id === task.id && t.mission_id === task.mission_id)) { throw new Error(`Mission task '${task.id}' already exists in mission '${task.mission_id}'`); } return { ...file, mission_tasks: [...file.mission_tasks, task] }; }); return result.mission_tasks.find(t => t.id === task.id && t.mission_id === task.mission_id)!; } async updateMissionTask(missionId: string, taskId: string, updates: Partial): Promise { let updated: BrainMissionTask | undefined; await this.store.modify('mission_tasks', DEFAULT_MISSION_TASKS, (file) => { const idx = file.mission_tasks.findIndex(t => t.id === taskId && t.mission_id === missionId); if (idx === -1) throw new Error(`Mission task '${taskId}' not found in mission '${missionId}'`); const tasks = [...file.mission_tasks]; updated = { ...tasks[idx]!, ...updates, id: taskId, mission_id: missionId, updated: today() } as BrainMissionTask; if (updates.status === 'done' && !updated.completed_at) { updated = { ...updated, completed_at: new Date().toISOString() }; } tasks[idx] = updated; return { ...file, mission_tasks: tasks }; }); return updated!; } // === Computed: Today === async getToday(date?: string): Promise { const d = date ?? today(); const weekFromNow = new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 10); const staleCutoff = new Date(Date.now() - 7 * 86400000).toISOString().slice(0, 10); const [tasks, projects, events, agents, missions] = await Promise.all([ this.getTasks(), this.getProjects(), this.getEvents(), this.getAgents(), this.getMissions({ status: 'active' }), ]); const activeTasks = tasks.filter(t => t.status !== 'done' && t.status !== 'cancelled'); const eventsToday = events.filter(e => e.date === d); const eventsUpcoming = events.filter(e => e.date > d && e.date <= weekFromNow); const tasksNearTerm = activeTasks.filter(t => t.due && t.due >= d && t.due <= weekFromNow); const tasksBlocked = activeTasks.filter(t => t.status === 'blocked'); const tasksStale = activeTasks.filter(t => t.status === 'in-progress' && t.updated < staleCutoff); const tasksAlmostDone = activeTasks.filter(t => t.progress != null && t.progress >= 80); const missionSummaries: BrainMissionSummary[] = []; for (const m of missions) { const summary = await this.getMissionSummary(m.id); if (summary) missionSummaries.push(summary); } const tasksByStatus: Record = {}; const tasksByDomain: Record = {}; for (const t of tasks) { tasksByStatus[t.status] = (tasksByStatus[t.status] ?? 0) + 1; tasksByDomain[t.domain] = (tasksByDomain[t.domain] ?? 0) + 1; } const projectsByStatus: Record = {}; for (const p of projects) { projectsByStatus[p.status] = (projectsByStatus[p.status] ?? 0) + 1; } return { date: d, events_today: eventsToday, events_upcoming: eventsUpcoming, tasks_near_term: tasksNearTerm, tasks_blocked: tasksBlocked, tasks_stale: tasksStale, tasks_almost_done: tasksAlmostDone, active_missions: missionSummaries, stats: { tasks: tasks.length, projects: projects.length, events: events.length, agents: agents.length, tickets: (await this.getTickets()).length, missions: (await this.getMissions()).length, tasks_by_status: tasksByStatus, tasks_by_domain: tasksByDomain, projects_by_status: projectsByStatus, }, }; } // === Computed: Stale === async getStale(days: number = 7): Promise { const cutoff = new Date(Date.now() - days * 86400000).toISOString().slice(0, 10); const tasks = await this.getTasks(); const projects = await this.getProjects(); return { days, tasks: tasks.filter(t => t.status === 'in-progress' && t.updated < cutoff), projects: projects.filter(p => p.status === 'active' && p.updated < cutoff), }; } // === Computed: Stats === async getStats(): Promise { const [tasks, projects, events, agents, tickets, missions] = await Promise.all([ this.getTasks(), this.getProjects(), this.getEvents(), this.getAgents(), this.getTickets(), this.getMissions(), ]); const tasksByStatus: Record = {}; const tasksByDomain: Record = {}; for (const t of tasks) { tasksByStatus[t.status] = (tasksByStatus[t.status] ?? 0) + 1; tasksByDomain[t.domain] = (tasksByDomain[t.domain] ?? 0) + 1; } const projectsByStatus: Record = {}; for (const p of projects) { projectsByStatus[p.status] = (projectsByStatus[p.status] ?? 0) + 1; } return { tasks: tasks.length, projects: projects.length, events: events.length, agents: agents.length, tickets: tickets.length, missions: missions.length, tasks_by_status: tasksByStatus, tasks_by_domain: tasksByDomain, projects_by_status: projectsByStatus, }; } // === Computed: Search === async search(query: string, collection?: string): Promise { const q = query.toLowerCase(); const results: BrainSearchResult[] = []; const searchIn = (items: Array<{ id: string; title: string; notes?: string | null }>, collectionName: string) => { for (const item of items) { const titleMatch = item.title.toLowerCase().includes(q); const notesMatch = item.notes?.toLowerCase().includes(q); if (titleMatch || notesMatch) { results.push({ collection: collectionName, id: item.id, title: item.title, match_context: titleMatch ? item.title : (item.notes?.substring(0, 200) ?? ''), score: titleMatch ? 1.0 : 0.5, }); } } }; if (!collection || collection === 'tasks') searchIn(await this.getTasks(), 'tasks'); if (!collection || collection === 'projects') { const projects = await this.getProjects(); for (const p of projects) { const titleMatch = p.name.toLowerCase().includes(q); const notesMatch = p.notes?.toLowerCase().includes(q); if (titleMatch || notesMatch) { results.push({ collection: 'projects', id: p.id, title: p.name, match_context: titleMatch ? p.name : (p.notes?.substring(0, 200) ?? ''), score: titleMatch ? 1.0 : 0.5, }); } } } if (!collection || collection === 'events') searchIn(await this.getEvents(), 'events'); if (!collection || collection === 'missions') searchIn(await this.getMissions(), 'missions'); results.sort((a, b) => b.score - a.score); return results; } // === Computed: Audit === async audit(): Promise { const tasks = await this.getTasks(); const projects = await this.getProjects(); const taskIds = new Set(tasks.map(t => t.id)); const projectIds = new Set(projects.map(p => p.id)); const allIds = new Set([...taskIds, ...projectIds]); const orphanRefs: string[] = []; const brokenDependencies: string[] = []; const missingRequiredFields: string[] = []; const duplicateIds: string[] = []; // Check for duplicate task IDs const seenTaskIds = new Set(); for (const t of tasks) { if (seenTaskIds.has(t.id)) duplicateIds.push(`task:${t.id}`); seenTaskIds.add(t.id); } // Check task references for (const t of tasks) { if (t.project && !projectIds.has(t.project)) { orphanRefs.push(`task:${t.id} -> project:${t.project}`); } for (const dep of t.blocked_by ?? []) { if (!allIds.has(dep)) brokenDependencies.push(`task:${t.id} blocked_by:${dep}`); } for (const dep of t.blocks ?? []) { if (!allIds.has(dep)) brokenDependencies.push(`task:${t.id} blocks:${dep}`); } if (!t.title) missingRequiredFields.push(`task:${t.id} missing title`); } return { orphan_refs: orphanRefs, broken_dependencies: brokenDependencies, missing_required_fields: missingRequiredFields, duplicate_ids: duplicateIds }; } }