UNPKG

@vfarcic/dot-ai

Version:

Universal Kubernetes application deployment agent with CLI and MCP interfaces

350 lines (347 loc) 18.8 kB
"use strict"; /** * Test Docs Tool - Documentation testing workflow orchestrator */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.TESTDOCS_TOOL_INPUT_SCHEMA = exports.TESTDOCS_TOOL_DESCRIPTION = exports.TESTDOCS_TOOL_NAME = void 0; exports.handleTestDocsTool = handleTestDocsTool; const zod_1 = require("zod"); const error_handling_1 = require("../core/error-handling"); const fs = __importStar(require("fs")); const doc_testing_session_1 = require("../core/doc-testing-session"); const doc_discovery_1 = require("../core/doc-discovery"); // Tool metadata for direct MCP registration exports.TESTDOCS_TOOL_NAME = 'testDocs'; exports.TESTDOCS_TOOL_DESCRIPTION = 'Test, validate, check, scan, verify, analyze, or review documentation files for accuracy, functionality, broken examples, outdated commands, invalid links, and overall quality. Use this tool whenever the user wants to test docs, validate documentation, check if examples work, scan for issues, verify commands, analyze doc quality, or review documentation content. IMPORTANT: This tool returns a workflow step with a "prompt" field - you must execute that prompt immediately to perform the actual documentation analysis.'; // Zod schema for MCP registration exports.TESTDOCS_TOOL_INPUT_SCHEMA = { filePath: zod_1.z.string().min(1).optional().describe('Path to documentation file to test (optional - if not provided, will discover available files)'), sessionId: zod_1.z.string().optional().describe('Existing session ID to continue (optional)'), phase: zod_1.z.enum(['scan', 'test', 'analyze', 'fix', 'done']).optional().describe('Specific phase to run (defaults to scan)'), sectionId: zod_1.z.string().optional().describe('Section ID when submitting test results'), results: zod_1.z.string().optional().describe('Test results to store (for client agent reporting back)'), filePattern: zod_1.z.string().optional().describe('File pattern for discovery (e.g., "**/*.md", "*.rst")') }; /** * Handle test-docs tool request */ async function handleTestDocsTool(args, _dotAI, logger, requestId) { try { logger.info('Processing test-docs tool request', { requestId, filePath: args.filePath, sessionId: args.sessionId, phase: args.phase }); // Check if we're in discovery mode (no filePath and no sessionId provided) if (!args.filePath && !args.sessionId) { logger.info('Running in discovery mode - scanning for documentation files', { requestId }); const discovery = new doc_discovery_1.DocDiscovery(); const pattern = discovery.getFilePattern(args); const discoveredFiles = await discovery.discoverFiles(process.cwd(), pattern); if (discoveredFiles.length === 0) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, `No documentation files found matching pattern: ${pattern}`, { operation: 'file_discovery', component: 'TestDocsTool', requestId, input: { pattern } }); } // Return discovery results const displayText = discovery.formatForDisplay(discoveredFiles); const defaultFile = discoveredFiles[0]; logger.info('Discovery completed', { requestId, filesFound: discoveredFiles.length, pattern, defaultFile: defaultFile.relativePath }); return { content: [{ type: 'text', text: JSON.stringify({ mode: 'discovery', pattern, filesFound: discoveredFiles.length, defaultFile: defaultFile.relativePath, files: discoveredFiles.map(f => ({ path: f.relativePath, category: f.category, priority: f.priority })), displayText, instruction: `I found ${discoveredFiles.length} documentation file${discoveredFiles.length === 1 ? '' : 's'} matching "${pattern}". You must ask the user which file they want to test. Do not choose automatically - wait for the user to specify which file they prefer. The recommended option is "${defaultFile.relativePath}".` }, null, 2) }] }; } // If we have sessionId but no filePath, load session to get filePath if (args.sessionId && !args.filePath) { const sessionManager = new doc_testing_session_1.DocTestingSessionManager(); const existingSession = sessionManager.loadSession(args.sessionId, args); if (existingSession) { args.filePath = existingSession.filePath; } } // Validate file exists (testing mode) if (!fs.existsSync(args.filePath)) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, `Documentation file not found: ${args.filePath}`, { operation: 'file_validation', component: 'TestDocsTool', requestId, input: { filePath: args.filePath } }); } // Initialize session manager const sessionManager = new doc_testing_session_1.DocTestingSessionManager(); let session; if (args.sessionId) { // Load existing session session = sessionManager.loadSession(args.sessionId, args); if (!session) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.STORAGE, error_handling_1.ErrorSeverity.HIGH, `Session not found: ${args.sessionId}`, { operation: 'session_load', component: 'TestDocsTool', requestId, input: { sessionId: args.sessionId } }); } logger.info('Loaded existing session', { requestId, sessionId: args.sessionId }); } else { // Create new session session = sessionManager.createSession(args.filePath, args); logger.info('Created new session', { requestId, sessionId: session.sessionId }); } // Handle results submission if provided if (args.results && args.sessionId) { if (args.sectionId) { // Section-specific results logger.info('Storing section test results', { requestId, sessionId: args.sessionId, sectionId: args.sectionId }); sessionManager.storeSectionTestResults(args.sessionId, args.sectionId, args.results, args); // After storing section results, get the next workflow step automatically const nextWorkflowStep = sessionManager.getNextStep(args.sessionId, args); if (nextWorkflowStep) { return { content: [ { type: 'text', text: JSON.stringify({ success: true, data: nextWorkflowStep }, null, 2) } ] }; } } else { // Scan results - process JSON array of section titles logger.info('Processing scan results', { requestId, sessionId: args.sessionId }); try { const resultsData = JSON.parse(args.results); // Handle scan results if (resultsData.sections && Array.isArray(resultsData.sections)) { sessionManager.processScanResults(args.sessionId, resultsData.sections, args); logger.info('Scan results processed successfully', { requestId, sessionId: args.sessionId, sectionsCount: resultsData.sections.length }); // After processing scan results, get the next workflow step based on updated session state const nextWorkflowStep = sessionManager.getNextStep(args.sessionId, args); if (nextWorkflowStep) { return { content: [ { type: 'text', text: JSON.stringify({ success: true, data: nextWorkflowStep }, null, 2) } ] }; } } // Handle fix phase results - array of item status updates else if (Array.isArray(resultsData)) { logger.info('Processing fix phase results', { requestId, sessionId: args.sessionId, itemUpdates: resultsData.length }); // Update status for each item const statusUpdates = []; for (const itemUpdate of resultsData) { if (itemUpdate.id && itemUpdate.status) { // Convert string ID to number if needed const itemId = typeof itemUpdate.id === 'string' ? parseInt(itemUpdate.id, 10) : itemUpdate.id; sessionManager.updateFixableItemStatus(args.sessionId, itemId, itemUpdate.status, itemUpdate.explanation, args); statusUpdates.push({ id: itemId, status: itemUpdate.status, explanation: itemUpdate.explanation }); } } logger.info('Fix phase results processed successfully', { requestId, sessionId: args.sessionId, updatedItems: statusUpdates.length }); // After processing fix results, get the next workflow step const nextWorkflowStep = sessionManager.getNextStep(args.sessionId, args); if (nextWorkflowStep) { return { content: [ { type: 'text', text: JSON.stringify({ success: true, data: nextWorkflowStep }, null, 2) } ] }; } } else { // Provide specific error message based on what we received if (Array.isArray(resultsData)) { // Fix results format - check if items have correct structure const firstItem = resultsData[0]; if (!firstItem || typeof firstItem !== 'object') { throw new Error(`Invalid fix results format. Expected array of objects like: [{"id": 1, "status": "fixed", "explanation": "..."}]. Got array with: ${typeof firstItem}`); } if (!firstItem.id || !firstItem.status) { throw new Error(`Invalid fix result item. Each item must have 'id' and 'status' fields. Expected: [{"id": 1, "status": "fixed", "explanation": "..."}]. Missing fields in: ${JSON.stringify(firstItem)}`); } // If we get here, it's properly formatted but might have failed in the update process throw new Error(`Fix results format is correct but processing failed. Array format: [{"id": number, "status": "fixed|deferred|failed", "explanation": "optional"}]`); } else { // Not an array and not scan results throw new Error(`Invalid results format. Expected either: - Scan results: {"sections": ["Section 1", "Section 2", ...]} - Fix results: [{"id": 1, "status": "fixed", "explanation": "..."}, {"id": 2, "status": "deferred", "explanation": "..."}] Got: ${JSON.stringify(resultsData).substring(0, 200)}`); } } } catch (parseError) { const errorMessage = parseError instanceof Error ? parseError.message : 'Unknown error'; // Provide helpful JSON parsing guidance if (errorMessage.includes('Unexpected token')) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, `Invalid JSON format in results parameter. ${errorMessage} Expected formats: - Scan results: {"sections": ["Section 1", "Section 2"]} - Fix results: [{"id": 1, "status": "fixed", "explanation": "description"}] Your input: "${args.results?.substring(0, 200)}..."`, { operation: 'results_parsing', component: 'TestDocsTool', requestId, input: { sessionId: args.sessionId, results: args.results } }); } throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, `Failed to process results: ${errorMessage}`, { operation: 'results_processing', component: 'TestDocsTool', requestId, input: { sessionId: args.sessionId, results: args.results } }); } } } // Determine phase to run - only override if explicitly provided const phaseOverride = args.phase ? args.phase : undefined; // Get workflow step const workflowStep = sessionManager.getNextStep(session.sessionId, args, phaseOverride); if (!workflowStep) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.OPERATION, error_handling_1.ErrorSeverity.HIGH, `Failed to get workflow step for session: ${session.sessionId}`, { operation: 'workflow_step_generation', component: 'TestDocsTool', requestId, input: { sessionId: session.sessionId, phaseOverride } }); } logger.info('Generated workflow step', { requestId, sessionId: session.sessionId, phase: workflowStep.phase, nextPhase: workflowStep.nextPhase }); // Return successful response with all WorkflowStep fields return { content: [{ type: 'text', text: JSON.stringify({ sessionId: session.sessionId, phase: workflowStep.phase, filePath: session.filePath, prompt: workflowStep.prompt, nextPhase: workflowStep.nextPhase, nextAction: workflowStep.nextAction, instruction: workflowStep.instruction, agentInstructions: workflowStep.agentInstructions, workflow: workflowStep.workflow, data: workflowStep.data }, null, 2) }] }; } catch (error) { logger.error('Test-docs tool failed', error); // Handle errors consistently if (error instanceof Error && 'category' in error) { // Already an AppError, just return it throw error; } throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.OPERATION, error_handling_1.ErrorSeverity.HIGH, error instanceof Error ? error.message : 'Unknown error in test-docs tool', { operation: 'test_docs_tool', component: 'TestDocsTool', requestId, input: args }); } }