UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

382 lines 15.6 kB
import { promises as fs } from 'fs'; import { join } from 'path'; export class ProductRequirementsStore { requirementsPath; linksPath; changesPath; constructor(dataDir = '.atlas') { this.requirementsPath = join(dataDir, 'requirements.json'); this.linksPath = join(dataDir, 'requirement-links.json'); this.changesPath = join(dataDir, 'requirement-changes.json'); } async ensureDataDir() { try { await fs.mkdir('.atlas', { recursive: true }); } catch (error) { // Directory might already exist } } async loadRequirements() { try { const data = await fs.readFile(this.requirementsPath, 'utf-8'); return JSON.parse(data); } catch (error) { return []; } } async saveRequirements(requirements) { await this.ensureDataDir(); await fs.writeFile(this.requirementsPath, JSON.stringify(requirements, null, 2)); } async loadLinks() { try { const data = await fs.readFile(this.linksPath, 'utf-8'); return JSON.parse(data); } catch (error) { return []; } } async saveLinks(links) { await this.ensureDataDir(); await fs.writeFile(this.linksPath, JSON.stringify(links, null, 2)); } async loadChanges() { try { const data = await fs.readFile(this.changesPath, 'utf-8'); return JSON.parse(data); } catch (error) { return []; } } async saveChanges(changes) { await this.ensureDataDir(); await fs.writeFile(this.changesPath, JSON.stringify(changes, null, 2)); } async recordChange(requirementId, field, oldValue, newValue) { const changes = await this.loadChanges(); changes.push({ requirement_id: requirementId, field, old_value: oldValue, new_value: newValue, changed_by: 'system', changed_at: new Date().toISOString() }); await this.saveChanges(changes); } async createRequirement(options) { const requirements = await this.loadRequirements(); const requirement = { id: `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, name: options.name, type: options.type, description: options.description, acceptance_criteria: options.acceptance_criteria || [], priority: options.priority || 'medium', status: options.status || 'draft', repositories: options.repositories || [], related_stories: options.related_stories || [], tags: options.tags || [], created_at: new Date().toISOString(), updated_at: new Date().toISOString(), version: 1, business_value: options.business_value, technical_notes: options.technical_notes, test_criteria: options.test_criteria || [], compliance_requirements: options.compliance_requirements || [], estimated_effort: options.estimated_effort, target_release: options.target_release, parent_requirement_id: options.parent_requirement_id, child_requirement_ids: [] }; // Update parent's child list if this is a child requirement if (options.parent_requirement_id) { const parentIndex = requirements.findIndex(r => r.id === options.parent_requirement_id); if (parentIndex !== -1) { requirements[parentIndex].child_requirement_ids = requirements[parentIndex].child_requirement_ids || []; requirements[parentIndex].child_requirement_ids.push(requirement.id); } } requirements.push(requirement); await this.saveRequirements(requirements); return requirement; } async getRequirement(id) { const requirements = await this.loadRequirements(); return requirements.find(r => r.id === id) || null; } async updateRequirement(options) { const requirements = await this.loadRequirements(); const index = requirements.findIndex(r => r.id === options.id); if (index === -1) { return null; } const oldRequirement = { ...requirements[index] }; const updatedRequirement = { ...requirements[index] }; // Track changes for each modified field const fieldsToUpdate = Object.keys(options).filter(key => key !== 'id'); for (const field of fieldsToUpdate) { const oldValue = oldRequirement[field]; const newValue = options[field]; if (JSON.stringify(oldValue) !== JSON.stringify(newValue)) { updatedRequirement[field] = newValue; await this.recordChange(options.id, field, oldValue, newValue); } } updatedRequirement.updated_at = new Date().toISOString(); updatedRequirement.version = (updatedRequirement.version || 1) + 1; requirements[index] = updatedRequirement; await this.saveRequirements(requirements); return updatedRequirement; } async deleteRequirement(id) { const requirements = await this.loadRequirements(); const index = requirements.findIndex(r => r.id === id); if (index === -1) { return false; } const requirement = requirements[index]; // Remove from parent's child list if this is a child requirement if (requirement.parent_requirement_id) { const parentIndex = requirements.findIndex(r => r.id === requirement.parent_requirement_id); if (parentIndex !== -1 && requirements[parentIndex].child_requirement_ids) { requirements[parentIndex].child_requirement_ids = requirements[parentIndex].child_requirement_ids.filter(childId => childId !== id); } } // Remove all links for this requirement const links = await this.loadLinks(); const updatedLinks = links.filter(l => l.requirement_id !== id); await this.saveLinks(updatedLinks); requirements.splice(index, 1); await this.saveRequirements(requirements); return true; } async listRequirements(filter) { const requirements = await this.loadRequirements(); if (!filter) { return requirements; } return requirements.filter(req => { if (filter.type && req.type !== filter.type) return false; if (filter.status && req.status !== filter.status) return false; if (filter.priority && req.priority !== filter.priority) return false; if (filter.repository) { const hasRepo = req.repositories?.some(r => r.name === filter.repository); if (!hasRepo) return false; } if (filter.tag) { const hasTag = req.tags?.includes(filter.tag); if (!hasTag) return false; } if (filter.parent_id && req.parent_requirement_id !== filter.parent_id) return false; if (filter.has_stories !== undefined) { const hasStories = req.related_stories && req.related_stories.length > 0; if (filter.has_stories !== hasStories) return false; } if (filter.implementation_status) { const hasStatus = req.repositories?.some(r => r.implementation_status === filter.implementation_status); if (!hasStatus) return false; } return true; }); } async searchRequirements(options) { const requirements = await this.loadRequirements(); const query = options.query.toLowerCase(); const searchFields = options.search_in || ['name', 'description']; let results = requirements.filter(req => { for (const field of searchFields) { switch (field) { case 'name': if (req.name.toLowerCase().includes(query)) return true; break; case 'description': if (req.description.toLowerCase().includes(query)) return true; break; case 'acceptance_criteria': if (req.acceptance_criteria?.some(ac => ac.toLowerCase().includes(query))) return true; break; case 'tags': if (req.tags?.some(tag => tag.toLowerCase().includes(query))) return true; break; } } return false; }); // Apply additional filters if provided if (options.filter) { results = results.filter(req => { const filter = options.filter; if (filter.type && req.type !== filter.type) return false; if (filter.status && req.status !== filter.status) return false; if (filter.priority && req.priority !== filter.priority) return false; return true; }); } // Apply pagination const offset = options.offset || 0; const limit = options.limit || results.length; return results.slice(offset, offset + limit); } async linkRequirementToStory(requirementId, storyId, linkType, notes) { const requirements = await this.loadRequirements(); const requirement = requirements.find(r => r.id === requirementId); if (!requirement) { throw new Error(`Requirement ${requirementId} not found`); } // Update requirement's related stories if (!requirement.related_stories) { requirement.related_stories = []; } if (!requirement.related_stories.includes(storyId)) { requirement.related_stories.push(storyId); await this.saveRequirements(requirements); } // Create link record const links = await this.loadLinks(); const link = { requirement_id: requirementId, story_id: storyId, link_type: linkType || 'implements', notes, created_at: new Date().toISOString() }; links.push(link); await this.saveLinks(links); return link; } async getRequirementsByStory(storyId) { const links = await this.loadLinks(); const requirementIds = links .filter(l => l.story_id === storyId) .map(l => l.requirement_id); const requirements = await this.loadRequirements(); return requirements.filter(r => requirementIds.includes(r.id)); } async getStoriesByRequirement(requirementId) { const requirement = await this.getRequirement(requirementId); return requirement?.related_stories || []; } async updateImplementationStatus(update) { const requirements = await this.loadRequirements(); const requirement = requirements.find(r => r.id === update.requirement_id); if (!requirement) { return null; } if (!requirement.repositories) { requirement.repositories = []; } const repoIndex = requirement.repositories.findIndex(r => r.name === update.repository_name); if (repoIndex === -1) { // Add new repository requirement.repositories.push({ name: update.repository_name, implementation_status: update.status, branch: update.branch, pr_url: update.pr_url, notes: update.notes }); } else { // Update existing repository requirement.repositories[repoIndex] = { ...requirement.repositories[repoIndex], implementation_status: update.status, branch: update.branch || requirement.repositories[repoIndex].branch, pr_url: update.pr_url || requirement.repositories[repoIndex].pr_url, notes: update.notes || requirement.repositories[repoIndex].notes }; } requirement.updated_at = new Date().toISOString(); await this.saveRequirements(requirements); return requirement; } async getRequirementsByStatus(status) { const requirements = await this.loadRequirements(); return requirements.filter(r => r.status === status); } async generateRequirementsReport(options) { const requirements = options?.filter ? await this.listRequirements(options.filter) : await this.loadRequirements(); const summary = { total_requirements: requirements.length, by_type: {}, by_status: {}, by_priority: {}, implementation_progress: { not_started: 0, in_progress: 0, completed: 0, blocked: 0 } }; // Calculate summary statistics for (const req of requirements) { // By type summary.by_type[req.type] = (summary.by_type[req.type] || 0) + 1; // By status summary.by_status[req.status] = (summary.by_status[req.status] || 0) + 1; // By priority summary.by_priority[req.priority] = (summary.by_priority[req.priority] || 0) + 1; // Implementation progress if (req.repositories && req.repositories.length > 0) { // Check if all repos are completed const allCompleted = req.repositories.every(r => r.implementation_status === 'completed'); const anyInProgress = req.repositories.some(r => r.implementation_status === 'in_progress'); const anyBlocked = req.repositories.some(r => r.implementation_status === 'blocked'); if (allCompleted) { summary.implementation_progress.completed++; } else if (anyBlocked) { summary.implementation_progress.blocked++; } else if (anyInProgress) { summary.implementation_progress.in_progress++; } else { summary.implementation_progress.not_started++; } } else { summary.implementation_progress.not_started++; } } // Sort requirements based on group_by option let sortedRequirements = [...requirements]; if (options?.group_by) { sortedRequirements.sort((a, b) => { const aValue = a[options.group_by]; const bValue = b[options.group_by]; return aValue.localeCompare(bValue); }); } return { summary, requirements: sortedRequirements, generated_at: new Date().toISOString(), format: options?.format || 'json' }; } } //# sourceMappingURL=store.js.map