UNPKG

@boundless-oss/atlas

Version:

Atlas - MCP Server for comprehensive startup project management

1,177 lines (1,176 loc) 63.9 kB
import { randomUUID } from 'crypto'; import { createTool, createSuccessResult, createErrorResult } from '../../core/tool-framework.js'; /** * Create a new product requirement */ const createRequirementTool = createTool({ name: 'create_requirement', description: 'Create a new product requirement', category: 'product-requirements', inputSchema: { type: 'object', properties: { name: { type: 'string', description: 'Name of the requirement', minLength: 1, maxLength: 200 }, type: { type: 'string', enum: ['functional', 'non-functional', 'business', 'technical', 'user-interface', 'security', 'performance'], description: 'Type of requirement' }, description: { type: 'string', description: 'Detailed description of the requirement', minLength: 1, maxLength: 5000 }, acceptanceCriteria: { type: 'array', items: { type: 'string', maxLength: 1000 }, description: 'List of acceptance criteria', maxItems: 20 }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], description: 'Priority level', default: 'medium' }, status: { type: 'string', enum: ['draft', 'approved', 'in_development', 'implemented', 'deprecated'], description: 'Current status', default: 'draft' }, repositories: { type: 'array', items: { type: 'object', properties: { name: { type: 'string', maxLength: 100 }, implementationStatus: { type: 'string', enum: ['not_started', 'in_progress', 'completed', 'blocked'] }, branch: { type: 'string', maxLength: 100 }, prUrl: { type: 'string', maxLength: 500 }, notes: { type: 'string', maxLength: 1000 } }, required: ['name', 'implementationStatus'], additionalProperties: false }, description: 'Repositories where this requirement is implemented', maxItems: 10 }, relatedStories: { type: 'array', items: { type: 'string' }, description: 'IDs of related user stories', maxItems: 50 }, tags: { type: 'array', items: { type: 'string', maxLength: 50 }, description: 'Tags for categorization', maxItems: 20 }, businessValue: { type: 'string', description: 'Business value statement', maxLength: 2000 }, technicalNotes: { type: 'string', description: 'Technical implementation notes', maxLength: 5000 }, testCriteria: { type: 'array', items: { type: 'string', maxLength: 1000 }, description: 'Testing criteria', maxItems: 20 }, complianceRequirements: { type: 'array', items: { type: 'string', maxLength: 500 }, description: 'Compliance and regulatory requirements', maxItems: 10 }, estimatedEffort: { type: 'string', description: 'Estimated effort (e.g., "2 weeks", "5 story points")', maxLength: 100 }, targetRelease: { type: 'string', description: 'Target release version', maxLength: 50 }, parentRequirementId: { type: 'string', description: 'ID of parent requirement for hierarchical organization', pattern: '^req-[a-f0-9-]+$' } }, required: ['name', 'type', 'description'], additionalProperties: false }, async execute(input, context) { try { const requirementId = `req-${randomUUID()}`; const now = Date.now(); // Validate parent requirement if provided if (input.parentRequirementId) { const parentCheck = await context.db.get('SELECT id FROM product_requirements WHERE id = ? AND project_id = ?', [input.parentRequirementId, context.projectId || 'default']); if (!parentCheck.success || !parentCheck.data) { return createErrorResult({ code: 'VALIDATION_ERROR', message: 'Parent requirement not found', details: { parentRequirementId: input.parentRequirementId }, category: 'validation' }); } } // Insert requirement const result = await context.db.run(`INSERT INTO product_requirements (id, project_id, name, type, description, acceptance_criteria, priority, status, related_stories, tags, created_at, updated_at, created_by, version, parent_requirement_id, business_value, technical_notes, test_criteria, compliance_requirements, estimated_effort, target_release) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ requirementId, context.projectId || 'default', input.name, input.type, input.description, JSON.stringify(input.acceptanceCriteria || []), input.priority || 'medium', input.status || 'draft', JSON.stringify(input.relatedStories || []), JSON.stringify(input.tags || []), now, now, context.userId || 'system', 1, input.parentRequirementId || null, input.businessValue || null, input.technicalNotes || null, JSON.stringify(input.testCriteria || []), JSON.stringify(input.complianceRequirements || []), input.estimatedEffort || null, input.targetRelease || null ]); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to create requirement', details: { error: result.error }, category: 'system' }); } // Insert repositories if provided if (input.repositories && input.repositories.length > 0) { for (const repo of input.repositories) { const repoId = `repo-${randomUUID()}`; await context.db.run(`INSERT INTO requirement_repositories (id, requirement_id, project_id, name, implementation_status, branch, pr_url, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ repoId, requirementId, context.projectId || 'default', repo.name, repo.implementationStatus, repo.branch || null, repo.prUrl || null, repo.notes || null, now, now ]); } } // Update parent requirement's child_requirement_ids if applicable if (input.parentRequirementId) { const parent = await context.db.get('SELECT child_requirement_ids FROM product_requirements WHERE id = ?', [input.parentRequirementId]); if (parent.success && parent.data) { const childIds = JSON.parse(parent.data.child_requirement_ids || '[]'); childIds.push(requirementId); await context.db.run('UPDATE product_requirements SET child_requirement_ids = ?, updated_at = ? WHERE id = ?', [JSON.stringify(childIds), now, input.parentRequirementId]); } } return createSuccessResult({ requirement: { id: requirementId, name: input.name, type: input.type, description: input.description, status: input.status || 'draft', priority: input.priority || 'medium', version: 1 }, message: `Created requirement "${input.name}" with ID ${requirementId}`, nextSteps: [ 'Add acceptance criteria if not already provided', 'Link to user stories using link_requirement_to_story', 'Update implementation status as development progresses', 'Review and approve the requirement when ready' ] }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to create requirement: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * List all product requirements with optional filtering */ const listRequirementsTool = createTool({ name: 'list_requirements', description: 'List all product requirements with optional filtering', category: 'product-requirements', inputSchema: { type: 'object', properties: { filterByType: { type: 'string', enum: ['functional', 'non-functional', 'business', 'technical', 'user-interface', 'security', 'performance'], description: 'Filter by requirement type' }, filterByStatus: { type: 'string', enum: ['draft', 'approved', 'in_development', 'implemented', 'deprecated'], description: 'Filter by status' }, filterByPriority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], description: 'Filter by priority' }, filterByRepository: { type: 'string', description: 'Filter by repository name', maxLength: 100 }, filterByTag: { type: 'string', description: 'Filter by tag', maxLength: 50 }, filterByParent: { type: 'string', description: 'Filter by parent requirement ID', pattern: '^req-[a-f0-9-]+$' }, hasStories: { type: 'boolean', description: 'Filter requirements with/without related stories' }, implementationStatus: { type: 'string', enum: ['not_started', 'in_progress', 'completed', 'blocked'], description: 'Filter by implementation status in any repository' } }, additionalProperties: false }, async execute(input, context) { try { let query = ` SELECT DISTINCT r.*, (SELECT COUNT(*) FROM requirement_repositories WHERE requirement_id = r.id) as repo_count, (SELECT COUNT(*) FROM requirement_story_links WHERE requirement_id = r.id) as story_count FROM product_requirements r WHERE r.project_id = ? `; const params = [context.projectId || 'default']; // Apply filters if (input.filterByType) { query += ' AND r.type = ?'; params.push(input.filterByType); } if (input.filterByStatus) { query += ' AND r.status = ?'; params.push(input.filterByStatus); } if (input.filterByPriority) { query += ' AND r.priority = ?'; params.push(input.filterByPriority); } if (input.filterByTag) { query += ' AND r.tags LIKE ?'; params.push(`%"${input.filterByTag}"%`); } if (input.filterByParent !== undefined) { if (input.filterByParent === null) { query += ' AND r.parent_requirement_id IS NULL'; } else { query += ' AND r.parent_requirement_id = ?'; params.push(input.filterByParent); } } if (input.hasStories !== undefined) { if (input.hasStories) { query += ' AND EXISTS (SELECT 1 FROM requirement_story_links WHERE requirement_id = r.id)'; } else { query += ' AND NOT EXISTS (SELECT 1 FROM requirement_story_links WHERE requirement_id = r.id)'; } } if (input.filterByRepository || input.implementationStatus) { query += ' AND EXISTS (SELECT 1 FROM requirement_repositories rr WHERE rr.requirement_id = r.id'; if (input.filterByRepository) { query += ' AND rr.name = ?'; params.push(input.filterByRepository); } if (input.implementationStatus) { query += ' AND rr.implementation_status = ?'; params.push(input.implementationStatus); } query += ')'; } query += ' ORDER BY r.priority DESC, r.updated_at DESC'; const requirements = await context.db.all(query, params); if (!requirements.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to fetch requirements', details: { error: requirements.error }, category: 'system' }); } if (requirements.data.length === 0) { return createSuccessResult({ requirements: [], totalCount: 0, message: 'No requirements found matching the criteria' }); } // Get repository details for each requirement const requirementsWithRepos = await Promise.all(requirements.data.map(async (req) => { const repos = await context.db.all('SELECT * FROM requirement_repositories WHERE requirement_id = ?', [req.id]); return { id: req.id, name: req.name, type: req.type, description: req.description, priority: req.priority, status: req.status, tags: JSON.parse(req.tags || '[]'), repositories: repos.success ? repos.data.map((r) => ({ name: r.name, implementationStatus: r.implementation_status, branch: r.branch, prUrl: r.pr_url })) : [], storyCount: req.story_count, createdAt: new Date(req.created_at).toISOString(), updatedAt: new Date(req.updated_at).toISOString() }; })); // Generate summary statistics const stats = { byType: {}, byStatus: {}, byPriority: {} }; requirementsWithRepos.forEach(req => { stats.byType[req.type] = (stats.byType[req.type] || 0) + 1; stats.byStatus[req.status] = (stats.byStatus[req.status] || 0) + 1; stats.byPriority[req.priority] = (stats.byPriority[req.priority] || 0) + 1; }); return createSuccessResult({ requirements: requirementsWithRepos, totalCount: requirementsWithRepos.length, statistics: stats, message: `Found ${requirementsWithRepos.length} requirement(s)` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to list requirements: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Get a specific product requirement by ID */ const getRequirementTool = createTool({ name: 'get_requirement', description: 'Get a specific product requirement by ID', category: 'product-requirements', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Requirement ID', pattern: '^req-[a-f0-9-]+$' } }, required: ['id'], additionalProperties: false }, async execute(input, context) { try { const requirement = await context.db.get('SELECT * FROM product_requirements WHERE id = ? AND project_id = ?', [input.id, context.projectId || 'default']); if (!requirement.success || !requirement.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: `Requirement ${input.id} not found`, details: { requirementId: input.id }, category: 'validation' }); } // Get repositories const repos = await context.db.all('SELECT * FROM requirement_repositories WHERE requirement_id = ?', [input.id]); // Get story links const storyLinks = await context.db.all('SELECT * FROM requirement_story_links WHERE requirement_id = ?', [input.id]); // Get child requirements if any let childRequirements = []; if (requirement.data.child_requirement_ids) { const childIds = JSON.parse(requirement.data.child_requirement_ids); if (childIds.length > 0) { const placeholders = childIds.map(() => '?').join(','); const children = await context.db.all(`SELECT id, name, type, status FROM product_requirements WHERE id IN (${placeholders})`, childIds); childRequirements = children.success ? children.data : []; } } const requirementData = { id: requirement.data.id, name: requirement.data.name, type: requirement.data.type, description: requirement.data.description, acceptanceCriteria: JSON.parse(requirement.data.acceptance_criteria || '[]'), priority: requirement.data.priority, status: requirement.data.status, repositories: repos.success ? repos.data.map((r) => ({ name: r.name, implementationStatus: r.implementation_status, branch: r.branch, prUrl: r.pr_url, notes: r.notes })) : [], relatedStories: JSON.parse(requirement.data.related_stories || '[]'), storyLinks: storyLinks.success ? storyLinks.data.map((l) => ({ storyId: l.story_id, linkType: l.link_type, notes: l.notes })) : [], tags: JSON.parse(requirement.data.tags || '[]'), createdAt: new Date(requirement.data.created_at).toISOString(), updatedAt: new Date(requirement.data.updated_at).toISOString(), createdBy: requirement.data.created_by, approvedBy: requirement.data.approved_by, approvedAt: requirement.data.approved_at ? new Date(requirement.data.approved_at).toISOString() : null, version: requirement.data.version, parentRequirementId: requirement.data.parent_requirement_id, childRequirements, dependencies: JSON.parse(requirement.data.dependencies || '[]'), businessValue: requirement.data.business_value, technicalNotes: requirement.data.technical_notes, testCriteria: JSON.parse(requirement.data.test_criteria || '[]'), complianceRequirements: JSON.parse(requirement.data.compliance_requirements || '[]'), estimatedEffort: requirement.data.estimated_effort, actualEffort: requirement.data.actual_effort, targetRelease: requirement.data.target_release, documentationLinks: JSON.parse(requirement.data.documentation_links || '[]') }; return createSuccessResult({ requirement: requirementData, message: `Retrieved requirement "${requirementData.name}"` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to get requirement: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Update an existing product requirement */ const updateRequirementTool = createTool({ name: 'update_requirement', description: 'Update an existing product requirement', category: 'product-requirements', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Requirement ID to update', pattern: '^req-[a-f0-9-]+$' }, name: { type: 'string', description: 'Updated name', minLength: 1, maxLength: 200 }, type: { type: 'string', enum: ['functional', 'non-functional', 'business', 'technical', 'user-interface', 'security', 'performance'], description: 'Updated type' }, description: { type: 'string', description: 'Updated description', minLength: 1, maxLength: 5000 }, acceptanceCriteria: { type: 'array', items: { type: 'string', maxLength: 1000 }, description: 'Updated acceptance criteria', maxItems: 20 }, priority: { type: 'string', enum: ['low', 'medium', 'high', 'critical'], description: 'Updated priority' }, status: { type: 'string', enum: ['draft', 'approved', 'in_development', 'implemented', 'deprecated'], description: 'Updated status' }, repositories: { type: 'array', items: { type: 'object', properties: { name: { type: 'string', maxLength: 100 }, implementationStatus: { type: 'string', enum: ['not_started', 'in_progress', 'completed', 'blocked'] }, branch: { type: 'string', maxLength: 100 }, prUrl: { type: 'string', maxLength: 500 }, notes: { type: 'string', maxLength: 1000 } }, required: ['name', 'implementationStatus'], additionalProperties: false }, description: 'Updated repository information', maxItems: 10 }, tags: { type: 'array', items: { type: 'string', maxLength: 50 }, description: 'Updated tags', maxItems: 20 }, businessValue: { type: 'string', description: 'Updated business value', maxLength: 2000 }, technicalNotes: { type: 'string', description: 'Updated technical notes', maxLength: 5000 }, testCriteria: { type: 'array', items: { type: 'string', maxLength: 1000 }, description: 'Updated test criteria', maxItems: 20 }, complianceRequirements: { type: 'array', items: { type: 'string', maxLength: 500 }, description: 'Updated compliance requirements', maxItems: 10 }, estimatedEffort: { type: 'string', description: 'Updated estimated effort', maxLength: 100 }, actualEffort: { type: 'string', description: 'Actual effort spent', maxLength: 100 }, targetRelease: { type: 'string', description: 'Updated target release', maxLength: 50 }, documentationLinks: { type: 'array', items: { type: 'string', maxLength: 500 }, description: 'Links to documentation', maxItems: 10 } }, required: ['id'], additionalProperties: false }, async execute(input, context) { try { // Check if requirement exists const existing = await context.db.get('SELECT * FROM product_requirements WHERE id = ? AND project_id = ?', [input.id, context.projectId || 'default']); if (!existing.success || !existing.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: `Requirement ${input.id} not found`, details: { requirementId: input.id }, category: 'validation' }); } const now = Date.now(); const updates = ['updated_at = ?']; const values = [now]; // Build update query dynamically if (input.name !== undefined) { updates.push('name = ?'); values.push(input.name); } if (input.type !== undefined) { updates.push('type = ?'); values.push(input.type); } if (input.description !== undefined) { updates.push('description = ?'); values.push(input.description); } if (input.acceptanceCriteria !== undefined) { updates.push('acceptance_criteria = ?'); values.push(JSON.stringify(input.acceptanceCriteria)); } if (input.priority !== undefined) { updates.push('priority = ?'); values.push(input.priority); } if (input.status !== undefined) { updates.push('status = ?'); values.push(input.status); // Set approval info if moving to approved status if (input.status === 'approved' && existing.data.status !== 'approved') { updates.push('approved_by = ?', 'approved_at = ?'); values.push(context.userId || 'system', now); } } if (input.tags !== undefined) { updates.push('tags = ?'); values.push(JSON.stringify(input.tags)); } if (input.businessValue !== undefined) { updates.push('business_value = ?'); values.push(input.businessValue); } if (input.technicalNotes !== undefined) { updates.push('technical_notes = ?'); values.push(input.technicalNotes); } if (input.testCriteria !== undefined) { updates.push('test_criteria = ?'); values.push(JSON.stringify(input.testCriteria)); } if (input.complianceRequirements !== undefined) { updates.push('compliance_requirements = ?'); values.push(JSON.stringify(input.complianceRequirements)); } if (input.estimatedEffort !== undefined) { updates.push('estimated_effort = ?'); values.push(input.estimatedEffort); } if (input.actualEffort !== undefined) { updates.push('actual_effort = ?'); values.push(input.actualEffort); } if (input.targetRelease !== undefined) { updates.push('target_release = ?'); values.push(input.targetRelease); } if (input.documentationLinks !== undefined) { updates.push('documentation_links = ?'); values.push(JSON.stringify(input.documentationLinks)); } // Increment version updates.push('version = version + 1'); values.push(input.id, context.projectId || 'default'); const updateQuery = `UPDATE product_requirements SET ${updates.join(', ')} WHERE id = ? AND project_id = ?`; const result = await context.db.run(updateQuery, values); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to update requirement', details: { error: result.error }, category: 'system' }); } // Update repositories if provided if (input.repositories !== undefined) { // Delete existing repositories await context.db.run('DELETE FROM requirement_repositories WHERE requirement_id = ?', [input.id]); // Insert new repositories for (const repo of input.repositories) { const repoId = `repo-${randomUUID()}`; await context.db.run(`INSERT INTO requirement_repositories (id, requirement_id, project_id, name, implementation_status, branch, pr_url, notes, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ repoId, input.id, context.projectId || 'default', repo.name, repo.implementationStatus, repo.branch || null, repo.prUrl || null, repo.notes || null, now, now ]); } } // Track changes const changedFields = Object.keys(input).filter(k => k !== 'id'); for (const field of changedFields) { const changeId = `change-${randomUUID()}`; await context.db.run(`INSERT INTO requirement_changes (id, requirement_id, project_id, field, old_value, new_value, changed_by, changed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [ changeId, input.id, context.projectId || 'default', field, JSON.stringify(existing.data[field]), JSON.stringify(input[field]), context.userId || 'system', now ]); } // Get updated requirement const updated = await context.db.get('SELECT * FROM product_requirements WHERE id = ?', [input.id]); return createSuccessResult({ requirement: { id: updated.data.id, name: updated.data.name, type: updated.data.type, status: updated.data.status, priority: updated.data.priority, version: updated.data.version }, message: `Updated requirement "${updated.data.name}"`, fieldsUpdated: changedFields }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to update requirement: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Delete a product requirement */ const deleteRequirementTool = createTool({ name: 'delete_requirement', description: 'Delete a product requirement', category: 'product-requirements', inputSchema: { type: 'object', properties: { id: { type: 'string', description: 'Requirement ID to delete', pattern: '^req-[a-f0-9-]+$' } }, required: ['id'], additionalProperties: false }, async execute(input, context) { try { // Check if requirement exists const existing = await context.db.get('SELECT * FROM product_requirements WHERE id = ? AND project_id = ?', [input.id, context.projectId || 'default']); if (!existing.success || !existing.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: `Requirement ${input.id} not found`, details: { requirementId: input.id }, category: 'validation' }); } // Check for child requirements const childIds = JSON.parse(existing.data.child_requirement_ids || '[]'); if (childIds.length > 0) { return createErrorResult({ code: 'VALIDATION_ERROR', message: 'Cannot delete requirement with child requirements', details: { childCount: childIds.length }, category: 'validation' }); } // Remove from parent's child list if applicable if (existing.data.parent_requirement_id) { const parent = await context.db.get('SELECT child_requirement_ids FROM product_requirements WHERE id = ?', [existing.data.parent_requirement_id]); if (parent.success && parent.data) { const parentChildIds = JSON.parse(parent.data.child_requirement_ids || '[]'); const updatedChildIds = parentChildIds.filter((id) => id !== input.id); await context.db.run('UPDATE product_requirements SET child_requirement_ids = ?, updated_at = ? WHERE id = ?', [JSON.stringify(updatedChildIds), Date.now(), existing.data.parent_requirement_id]); } } // Delete requirement (cascades to repositories, story links, and changes) const result = await context.db.run('DELETE FROM product_requirements WHERE id = ? AND project_id = ?', [input.id, context.projectId || 'default']); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to delete requirement', details: { error: result.error }, category: 'system' }); } return createSuccessResult({ deletedRequirement: { id: input.id, name: existing.data.name }, message: `Deleted requirement "${existing.data.name}"` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to delete requirement: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Search product requirements by text query */ const searchRequirementsTool = createTool({ name: 'search_requirements', description: 'Search product requirements by text query', category: 'product-requirements', inputSchema: { type: 'object', properties: { query: { type: 'string', description: 'Search query', minLength: 2, maxLength: 200 }, searchIn: { type: 'array', items: { type: 'string', enum: ['name', 'description', 'acceptance_criteria', 'tags'] }, description: 'Fields to search in (default: name, description)', maxItems: 4 }, limit: { type: 'number', description: 'Maximum results to return', minimum: 1, maximum: 100, default: 20 }, offset: { type: 'number', description: 'Offset for pagination', minimum: 0, default: 0 } }, required: ['query'], additionalProperties: false }, async execute(input, context) { try { const searchFields = input.searchIn || ['name', 'description']; const searchConditions = []; const params = []; // Build search conditions if (searchFields.includes('name')) { searchConditions.push('r.name LIKE ?'); params.push(`%${input.query}%`); } if (searchFields.includes('description')) { searchConditions.push('r.description LIKE ?'); params.push(`%${input.query}%`); } if (searchFields.includes('acceptance_criteria')) { searchConditions.push('r.acceptance_criteria LIKE ?'); params.push(`%${input.query}%`); } if (searchFields.includes('tags')) { searchConditions.push('r.tags LIKE ?'); params.push(`%${input.query}%`); } const query = ` SELECT r.*, (SELECT COUNT(*) FROM requirement_repositories WHERE requirement_id = r.id) as repo_count, (SELECT COUNT(*) FROM requirement_story_links WHERE requirement_id = r.id) as story_count FROM product_requirements r WHERE r.project_id = ? AND (${searchConditions.join(' OR ')}) ORDER BY CASE WHEN r.name LIKE ? THEN 1 WHEN r.description LIKE ? THEN 2 ELSE 3 END, r.updated_at DESC LIMIT ? OFFSET ? `; const allParams = [ context.projectId || 'default', ...params, `${input.query}%`, // For relevance sorting `${input.query}%`, input.limit || 20, input.offset || 0 ]; const results = await context.db.all(query, allParams); if (!results.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to search requirements', details: { error: results.error }, category: 'system' }); } if (results.data.length === 0) { return createSuccessResult({ requirements: [], totalFound: 0, message: `No requirements found matching "${input.query}"` }); } const requirements = results.data.map((req) => ({ id: req.id, name: req.name, type: req.type, description: req.description.substring(0, 200) + (req.description.length > 200 ? '...' : ''), priority: req.priority, status: req.status, tags: JSON.parse(req.tags || '[]'), repositoryCount: req.repo_count, storyCount: req.story_count, relevanceScore: req.name.toLowerCase().includes(input.query.toLowerCase()) ? 100 : 50 })); return createSuccessResult({ requirements, totalFound: requirements.length, query: input.query, searchedIn: searchFields, message: `Found ${requirements.length} requirement(s) matching "${input.query}"` }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to search requirements: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Link a product requirement to a user story */ const linkRequirementToStoryTool = createTool({ name: 'link_requirement_to_story', description: 'Link a product requirement to a user story', category: 'product-requirements', inputSchema: { type: 'object', properties: { requirementId: { type: 'string', description: 'Product requirement ID', pattern: '^req-[a-f0-9-]+$' }, storyId: { type: 'string', description: 'User story ID' }, linkType: { type: 'string', enum: ['implements', 'relates_to', 'depends_on'], description: 'Type of relationship', default: 'implements' }, notes: { type: 'string', description: 'Additional notes about the link', maxLength: 1000 } }, required: ['requirementId', 'storyId'], additionalProperties: false }, async execute(input, context) { try { // Verify requirement exists const reqCheck = await context.db.get('SELECT id, related_stories FROM product_requirements WHERE id = ? AND project_id = ?', [input.requirementId, context.projectId || 'default']); if (!reqCheck.success || !reqCheck.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: 'Requirement not found', details: { requirementId: input.requirementId }, category: 'validation' }); } // Verify story exists const storyCheck = await context.db.get('SELECT id FROM agile_stories WHERE id = ? AND project_id = ?', [input.storyId, context.projectId || 'default']); if (!storyCheck.success || !storyCheck.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: 'Story not found', details: { storyId: input.storyId }, category: 'validation' }); } // Check if link already exists const existingLink = await context.db.get('SELECT id FROM requirement_story_links WHERE requirement_id = ? AND story_id = ?', [input.requirementId, input.storyId]); if (existingLink.success && existingLink.data) { return createErrorResult({ code: 'DUPLICATE_RESOURCE', message: 'Link already exists between this requirement and story', category: 'validation' }); } const linkId = `link-${randomUUID()}`; const now = Date.now(); // Create link const result = await context.db.run(`INSERT INTO requirement_story_links (id, requirement_id, story_id, project_id, link_type, notes, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [ linkId, input.requirementId, input.storyId, context.projectId || 'default', input.linkType || 'implements', input.notes || null, now ]); if (!result.success) { return createErrorResult({ code: 'DATABASE_ERROR', message: 'Failed to create link', details: { error: result.error }, category: 'system' }); } // Update requirement's related_stories array const relatedStories = JSON.parse(reqCheck.data.related_stories || '[]'); if (!relatedStories.includes(input.storyId)) { relatedStories.push(input.storyId); await context.db.run('UPDATE product_requirements SET related_stories = ?, updated_at = ? WHERE id = ?', [JSON.stringify(relatedStories), now, input.requirementId]); } return createSuccessResult({ link: { id: linkId, requirementId: input.requirementId, storyId: input.storyId, linkType: input.linkType || 'implements', notes: input.notes }, message: `Linked requirement ${input.requirementId} to story ${input.storyId}`, nextSteps: [ 'Update story to reflect requirement implementation', 'Track progress through story status updates' ] }); } catch (error) { return createErrorResult({ code: 'EXECUTION_ERROR', message: `Failed to link requirement to story: ${error instanceof Error ? error.message : 'Unknown error'}`, category: 'execution' }); } } }); /** * Update implementation status for a requirement in a specific repository */ const updateImplementationStatusTool = createTool({ name: 'update_implementation_status', description: 'Update implementation status for a requirement in a specific repository', category: 'product-requirements', inputSchema: { type: 'object', properties: { requirementId: { type: 'string', description: 'Product requirement ID', pattern: '^req-[a-f0-9-]+$' }, repositoryName: { type: 'string', description: 'Repository name', minLength: 1, maxLength: 100 }, status: { type: 'string', enum: ['not_started', 'in_progress', 'completed', 'blocked'], description: 'New implementation status' }, branch: { type: 'string', description: 'Branch name where implementation is happening', maxLength: 100 }, prUrl: { type: 'string', description: 'Pull request URL', maxLength: 500 }, notes: { type: 'string', description: 'Implementation notes', maxLength: 1000 } }, required: ['requirementId', 'repositoryName', 'status'], additionalProperties: false }, async execute(input, context) { try { // Verify requirement exists const reqCheck = await context.db.get('SELECT id FROM product_requirements WHERE id = ? AND project_id = ?', [input.requirementId, context.projectId || 'default']); if (!reqCheck.success || !reqCheck.data) { return createErrorResult({ code: 'RESOURCE_NOT_FOUND', message: 'Requirement not found', details: { requirementId: input.requirementId }, category: 'validation' }); } // Check if repository exists for this requirement const repoCheck = await context.db.get('SELECT id FROM requirement_repositories WHERE requirement_id = ? AND name = ?', [input.requirementId, input.repositoryName]); const now = Date.now(); if (repoCheck.success && repoCheck.data) { // Update existing repository const result = await context.db.run(`UPDATE requirement_repositories SET implementation_status = ?, branch = ?, pr_url = ?, notes = ?, updated_at = ? WHERE id = ?`, [ input.status, input.branch || null, input.prUrl || null, input.notes || null,