UNPKG

@vfarcic/dot-ai

Version:

Universal Kubernetes application deployment agent with CLI and MCP interfaces

748 lines (744 loc) 34.2 kB
"use strict"; /** * Documentation Testing Session Manager * * Handles creating, loading, saving, and managing documentation validation sessions. * Uses the existing session directory infrastructure from session-utils.ts. */ 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.DocTestingSessionManager = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); const session_utils_1 = require("./session-utils"); const doc_testing_types_1 = require("./doc-testing-types"); class DocTestingSessionManager { /** * Create a new validation session */ createSession(filePath, args) { const sessionDir = (0, session_utils_1.getAndValidateSessionDirectory)(args, true); // requireWrite=true const sessionId = this.generateSessionId(); const session = { sessionId, filePath, startTime: new Date().toISOString(), currentPhase: doc_testing_types_1.ValidationPhase.SCAN, status: doc_testing_types_1.SessionStatus.ACTIVE, metadata: { totalSections: 0, completedSections: 0, sectionStatus: {}, nextItemId: 1, sessionDir, lastUpdated: new Date().toISOString() } }; this.saveSession(session, args); return session; } /** * Load existing session */ loadSession(sessionId, args) { const sessionDir = (0, session_utils_1.getAndValidateSessionDirectory)(args, false); // requireWrite=false const sessionFile = path.join(sessionDir, `doc-test-${sessionId}.json`); if (!fs.existsSync(sessionFile)) { return null; } try { const sessionData = JSON.parse(fs.readFileSync(sessionFile, 'utf8')); return sessionData; } catch (error) { console.error(`Failed to load session ${sessionId}:`, error); return null; } } /** * Save session state */ saveSession(session, args) { const sessionDir = (0, session_utils_1.getAndValidateSessionDirectory)(args, true); // requireWrite=true session.metadata.lastUpdated = new Date().toISOString(); const sessionFile = path.join(sessionDir, `doc-test-${session.sessionId}.json`); fs.writeFileSync(sessionFile, JSON.stringify(session, null, 2)); } /** * Get universal agent instructions for documentation testing workflow */ getAgentInstructions() { return ` DOCUMENTATION TESTING WORKFLOW: 1. Use the provided prompt to complete the requested task (scan, test section, analyze, or fix) 2. Return results in the exact format specified in the prompt 3. Submit results by calling testDocs with these parameters: - sessionId: the session ID provided in the response - results: your formatted results (JSON for section testing, JSON array for scan results) - sectionId: (only when testing individual sections) the specific section ID RESULT SUBMISSION: - Always include the sessionId when submitting results - Include sectionId when testing individual sections - For section testing: use JSON format {"whatWasDone": "...", "issues": [...], "recommendations": [...]} - For scan results: use JSON format {"sections": ["Section 1", "Section 2", ...]} - After submitting, the system automatically provides the next step WORKFLOW PHASES: - scan: Identify testable sections → submit {"sections": [...]} JSON - test: Test individual sections → submit {"whatWasDone": "...", "issues": [...], "recommendations": [...]} JSON - analyze: Review all test results → submit analysis and recommendations - fix: Apply fixes based on analysis → submit fix results The system manages session state and workflow progression automatically.`; } /** * Get next workflow step for AI agent */ getNextStep(sessionId, args, phaseOverride) { const session = this.loadSession(sessionId, args); if (!session) { return null; } const targetPhase = phaseOverride || session.currentPhase; // Handle done phase - mark session as completed if (targetPhase === doc_testing_types_1.ValidationPhase.DONE) { return this.getDonePhaseStep(session, args); } // Handle section-by-section testing workflow if (targetPhase === doc_testing_types_1.ValidationPhase.TEST) { return this.getTestPhaseStep(session, args); } const prompt = this.loadPhasePrompt(targetPhase, session); const nextPhase = this.getNextPhase(targetPhase); return { sessionId, phase: targetPhase, prompt, nextPhase, nextAction: 'testDocs', instruction: `Complete the ${targetPhase} phase and submit your results to continue the workflow.`, agentInstructions: this.getAgentInstructions(), workflow: { completed: [], // Will be populated when we add phase tracking current: targetPhase, remaining: this.getRemainingPhases(targetPhase) }, data: { filePath: session.filePath, sessionDir: session.metadata.sessionDir } }; } /** * Handle done phase - mark session as completed and provide summary */ getDonePhaseStep(session, args) { // Update session status to completed session.status = doc_testing_types_1.SessionStatus.COMPLETED; session.currentPhase = doc_testing_types_1.ValidationPhase.DONE; session.metadata.lastUpdated = new Date().toISOString(); // Save the updated session this.saveSession(session, args); // Load and populate done phase prompt const prompt = this.loadPhasePrompt(doc_testing_types_1.ValidationPhase.DONE, session); return { sessionId: session.sessionId, phase: doc_testing_types_1.ValidationPhase.DONE, prompt, nextPhase: undefined, // No next phase - session is complete nextAction: undefined, // No next action required instruction: 'Documentation testing session completed successfully.', agentInstructions: 'This session is now complete. No further action is required.', workflow: { completed: [doc_testing_types_1.ValidationPhase.SCAN, doc_testing_types_1.ValidationPhase.TEST, doc_testing_types_1.ValidationPhase.ANALYZE, doc_testing_types_1.ValidationPhase.FIX], current: doc_testing_types_1.ValidationPhase.DONE, remaining: [] }, data: { filePath: session.filePath, sessionDir: session.metadata.sessionDir, sessionComplete: true, summary: this.generateStatusSummary(session) } }; } /** * Generate final session summary for done phase */ generateFinalSummary(session) { if (!session.sectionResults) { return "No test results available."; } // Count all items by status const allItems = []; Object.values(session.sectionResults).forEach(result => { allItems.push(...result.issues, ...result.recommendations); }); if (allItems.length === 0) { return "✅ **No issues found** - Documentation appears to be in excellent condition!"; } const statusCounts = { pending: allItems.filter(item => item.status === 'pending').length, fixed: allItems.filter(item => item.status === 'fixed').length, deferred: allItems.filter(item => item.status === 'deferred').length, failed: allItems.filter(item => item.status === 'failed').length }; const total = allItems.length; let summary = `## Testing Results\n\n`; summary += `**Total Items Identified**: ${total}\n\n`; if (statusCounts.fixed > 0) { summary += `✅ **Successfully Fixed**: ${statusCounts.fixed} items\n`; } if (statusCounts.deferred > 0) { summary += `📋 **Deferred/Ignored**: ${statusCounts.deferred} items\n`; } if (statusCounts.pending > 0) { summary += `⏳ **Remaining for Future**: ${statusCounts.pending} items\n`; } if (statusCounts.failed > 0) { summary += `❌ **Fix Attempts Failed**: ${statusCounts.failed} items\n`; } const addressedItems = statusCounts.fixed + statusCounts.deferred; const completionRate = total > 0 ? Math.round((addressedItems / total) * 100) : 100; summary += `\n**Completion Rate**: ${completionRate}% (${addressedItems}/${total} items addressed)\n`; if (statusCounts.pending > 0 || statusCounts.failed > 0) { summary += `\n💡 **Next Steps**: Start a new testing session to address the remaining ${statusCounts.pending + statusCounts.failed} items.`; } else { summary += `\n🎉 **Excellent!** All identified items have been addressed.`; } return summary; } /** * Get all active sessions */ getActiveSessions(args) { const sessionDir = (0, session_utils_1.getAndValidateSessionDirectory)(args, false); // requireWrite=false const sessions = []; if (!fs.existsSync(sessionDir)) { return sessions; } const files = fs.readdirSync(sessionDir); for (const file of files) { if (file.startsWith('doc-test-') && file.endsWith('.json')) { const sessionId = file.replace('doc-test-', '').replace('.json', ''); const session = this.loadSession(sessionId, args); if (session && session.status === doc_testing_types_1.SessionStatus.ACTIVE) { sessions.push(session); } } } return sessions; } // Private helper methods generateSessionId() { const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); const random = Math.random().toString(36).substring(2, 10); return `${timestamp}-${random}`; } /** * Load phase prompt from file (following CLAUDE.md pattern) */ loadPhasePrompt(phase, session) { const promptPath = path.join(process.cwd(), 'prompts', `doc-testing-${phase}.md`); if (!fs.existsSync(promptPath)) { // Fallback to basic prompt if file doesn't exist return `Read the file at "${session.filePath}" and process it for phase ${phase}.`; } try { const template = fs.readFileSync(promptPath, 'utf8'); // Replace all template variables with actual values let processedPrompt = template .replace(/\{filePath\}/g, session.filePath) .replace(/\{sessionId\}/g, session.sessionId) .replace(/\{phase\}/g, phase) .replace(/\{totalSections\}/g, session.metadata.totalSections.toString()) .replace(/\{completedSections\}/g, session.metadata.completedSections.toString()); // Handle fix phase specific template variables if (phase === doc_testing_types_1.ValidationPhase.FIX) { const statusSummary = this.generateStatusSummary(session); const pendingItems = this.generatePendingItemsList(session); processedPrompt = processedPrompt .replace(/\{statusSummary\}/g, statusSummary) .replace(/\{pendingItems\}/g, pendingItems); } // Handle done phase specific template variables if (phase === doc_testing_types_1.ValidationPhase.DONE) { const finalSummary = this.generateFinalSummary(session); processedPrompt = processedPrompt .replace(/\{completionTime\}/g, session.metadata.lastUpdated) .replace(/\{finalSummary\}/g, finalSummary) .replace(/\{sessionDir\}/g, session.metadata.sessionDir); } // Check for unreplaced template variables const unreplacedVars = processedPrompt.match(/\{[^}]+\}/g); if (unreplacedVars) { console.error(`Warning: Unreplaced template variables in ${phase} prompt:`, unreplacedVars); } return processedPrompt; } catch (error) { console.error(`Failed to load prompt for phase ${phase}:`, error); return `Read the file at "${session.filePath}" and process it for phase ${phase}.`; } } getNextPhase(currentPhase) { const phases = [doc_testing_types_1.ValidationPhase.SCAN, doc_testing_types_1.ValidationPhase.TEST, doc_testing_types_1.ValidationPhase.ANALYZE, doc_testing_types_1.ValidationPhase.FIX]; const currentIndex = phases.indexOf(currentPhase); return currentIndex < phases.length - 1 ? phases[currentIndex + 1] : undefined; } getRemainingPhases(currentPhase) { const phases = [doc_testing_types_1.ValidationPhase.SCAN, doc_testing_types_1.ValidationPhase.TEST, doc_testing_types_1.ValidationPhase.ANALYZE, doc_testing_types_1.ValidationPhase.FIX]; const currentIndex = phases.indexOf(currentPhase); return phases.slice(currentIndex + 1); } /** * Update the status of a specific section */ updateSectionStatus(sessionId, sectionId, status, args) { const session = this.loadSession(sessionId, args); if (!session) { throw new Error(`Session ${sessionId} not found`); } session.metadata.sectionStatus[sectionId] = status; // Update completed sections count session.metadata.completedSections = Object.values(session.metadata.sectionStatus) .filter(s => s === doc_testing_types_1.SectionStatus.COMPLETED).length; this.saveSession(session, args); } /** * Get sections for a session */ getSections(sessionId, args) { const session = this.loadSession(sessionId, args); return session?.sections || null; } /** * Get the next test phase step - handles section-by-section testing */ getTestPhaseStep(session, args) { // If no sections available, fall back to regular test phase if (!session.sections || session.sections.length === 0) { const prompt = this.loadPhasePrompt(doc_testing_types_1.ValidationPhase.TEST, session); return { sessionId: session.sessionId, phase: doc_testing_types_1.ValidationPhase.TEST, prompt, nextPhase: doc_testing_types_1.ValidationPhase.ANALYZE, nextAction: 'testDocs', instruction: 'Complete the test phase and submit your results to continue the workflow.', agentInstructions: this.getAgentInstructions(), workflow: { completed: [], current: doc_testing_types_1.ValidationPhase.TEST, remaining: [doc_testing_types_1.ValidationPhase.ANALYZE, doc_testing_types_1.ValidationPhase.FIX] }, data: { filePath: session.filePath, sessionDir: session.metadata.sessionDir } }; } // Find the next section to test const nextSection = this.getNextSectionToTest(session); if (!nextSection) { // All sections tested, move to fix phase return { sessionId: session.sessionId, phase: doc_testing_types_1.ValidationPhase.FIX, prompt: this.loadPhasePrompt(doc_testing_types_1.ValidationPhase.FIX, session), nextPhase: undefined, // FIX is the final phase nextAction: 'testDocs', instruction: 'Present the pending items to the user for selection. Ask which fixes they want to apply. DO NOT auto-select or auto-defer items.', agentInstructions: this.getAgentInstructions(), workflow: { completed: [doc_testing_types_1.ValidationPhase.SCAN, doc_testing_types_1.ValidationPhase.TEST], current: doc_testing_types_1.ValidationPhase.FIX, remaining: [] }, data: { filePath: session.filePath, sessionDir: session.metadata.sessionDir, allSectionsTested: true } }; } // Update section status to testing this.updateSectionStatus(session.sessionId, nextSection.id, doc_testing_types_1.SectionStatus.TESTING, args); const prompt = this.loadSectionTestPrompt(nextSection, session); const remainingSections = this.getRemainingTestSections(session); const nextPhase = remainingSections.length > 0 ? doc_testing_types_1.ValidationPhase.TEST : doc_testing_types_1.ValidationPhase.ANALYZE; return { sessionId: session.sessionId, phase: doc_testing_types_1.ValidationPhase.TEST, prompt, nextPhase, nextAction: 'testDocs', instruction: `Test the "${nextSection.title}" section and submit your results to continue the workflow.`, agentInstructions: this.getAgentInstructions(), workflow: { completed: [doc_testing_types_1.ValidationPhase.SCAN], current: doc_testing_types_1.ValidationPhase.TEST, remaining: remainingSections.length > 0 ? [doc_testing_types_1.ValidationPhase.TEST, doc_testing_types_1.ValidationPhase.ANALYZE, doc_testing_types_1.ValidationPhase.FIX] : [doc_testing_types_1.ValidationPhase.ANALYZE, doc_testing_types_1.ValidationPhase.FIX] }, data: { filePath: session.filePath, sessionDir: session.metadata.sessionDir, currentSection: nextSection, sectionsRemaining: remainingSections.length, totalSections: session.sections.length } }; } /** * Find the next section that needs testing */ getNextSectionToTest(session) { if (!session.sections) return null; // Find sections that are pending for (const section of session.sections) { const status = session.metadata.sectionStatus[section.id]; // Skip if already tested or currently testing if (status === doc_testing_types_1.SectionStatus.COMPLETED || status === doc_testing_types_1.SectionStatus.TESTING) { continue; } // Return first pending section (no dependencies to check) return section; } return null; } /** * Get remaining sections that need testing */ getRemainingTestSections(session) { if (!session.sections) return []; return session.sections.filter(section => { const status = session.metadata.sectionStatus[section.id]; return status === doc_testing_types_1.SectionStatus.PENDING; }); } /** * Load section-specific test prompt */ loadSectionTestPrompt(section, session) { const promptPath = path.join(process.cwd(), 'prompts', 'doc-testing-test-section.md'); if (!fs.existsSync(promptPath)) { // Fallback prompt return `Test the "${section.title}" section of ${session.filePath}.\n\nAnalyze this section and test everything you determine is testable within it.`; } try { const template = fs.readFileSync(promptPath, 'utf8'); const processedPrompt = template .replace(/\{filePath\}/g, session.filePath) .replace(/\{sessionId\}/g, session.sessionId) .replace(/\{sectionId\}/g, section.id) .replace(/\{sectionTitle\}/g, section.title) .replace(/\{totalSections\}/g, session.sections?.length.toString() || '0') .replace(/\{sectionsRemaining\}/g, this.getRemainingTestSections(session).length.toString()); // Check for unreplaced template variables const unreplacedVars = processedPrompt.match(/\{[^}]+\}/g); if (unreplacedVars) { console.error(`Warning: Unreplaced template variables in section test prompt:`, unreplacedVars); } return processedPrompt; } catch (error) { console.error(`Failed to load section test prompt:`, error); return `Test the "${section.title}" section of ${session.filePath}.`; } } /** * Convert string arrays to FixableItem arrays with generated IDs */ convertToFixableItems(items, session) { return items.map((item) => { // If already a FixableItem object, return as-is if (typeof item === 'object' && item.id !== undefined) { return item; } // Convert string to FixableItem with generated ID const fixableItem = { id: session.metadata.nextItemId++, text: item, status: 'pending' }; return fixableItem; }); } /** * Store test results for a specific section */ storeSectionTestResults(sessionId, sectionId, results, args) { const session = this.loadSession(sessionId, args); if (!session) { throw new Error(`Session ${sessionId} not found`); } // Parse and validate JSON results let parsedResults; // Use 'any' initially to handle both old and new formats try { parsedResults = JSON.parse(results); // Validate required fields if (typeof parsedResults.whatWasDone !== 'string') { throw new Error('Missing or invalid "whatWasDone" field'); } if (!Array.isArray(parsedResults.issues)) { throw new Error('Missing or invalid "issues" field - must be array'); } if (!Array.isArray(parsedResults.recommendations)) { throw new Error('Missing or invalid "recommendations" field - must be array'); } // Convert string arrays to FixableItem arrays if needed const processedResults = { whatWasDone: parsedResults.whatWasDone, issues: this.convertToFixableItems(parsedResults.issues, session), recommendations: this.convertToFixableItems(parsedResults.recommendations, session) }; parsedResults = processedResults; } catch (error) { throw new Error(`Invalid JSON results format: ${error instanceof Error ? error.message : 'Unknown error'}`); } // Initialize sectionResults if it doesn't exist if (!session.sectionResults) { session.sectionResults = {}; } // Store the parsed results session.sectionResults[sectionId] = parsedResults; // Update section status to completed if (session.metadata.sectionStatus[sectionId]) { session.metadata.sectionStatus[sectionId] = doc_testing_types_1.SectionStatus.COMPLETED; // Update completed sections count session.metadata.completedSections = Object.values(session.metadata.sectionStatus) .filter(status => status === doc_testing_types_1.SectionStatus.COMPLETED).length; } this.saveSession(session, args); } /** * Process scan results by converting section titles into DocumentSection objects */ processScanResults(sessionId, sectionTitles, args) { const session = this.loadSession(sessionId, args); if (!session) { throw new Error(`Session ${sessionId} not found`); } // Convert section titles to DocumentSection objects and initialize status const sections = sectionTitles.map((title, index) => ({ id: `section_${index + 1}`, title: title.trim() })); // Update session with sections and reset counters session.sections = sections; session.metadata.totalSections = sections.length; session.metadata.completedSections = 0; session.metadata.nextItemId = 1; // Initialize ID counter for fix tracking session.metadata.sectionStatus = sections.reduce((acc, section) => { acc[section.id] = doc_testing_types_1.SectionStatus.PENDING; return acc; }, {}); // Move to test phase after processing scan results session.currentPhase = doc_testing_types_1.ValidationPhase.TEST; this.saveSession(session, args); } /** * Generate status summary for fix phase */ generateStatusSummary(session) { if (!session.sectionResults) { return "No test results available."; } const allItems = []; // Collect all FixableItems from all sections Object.values(session.sectionResults).forEach(result => { allItems.push(...result.issues, ...result.recommendations); }); if (allItems.length === 0) { return "No issues or recommendations found during testing."; } // Count by status const statusCounts = { pending: allItems.filter(item => item.status === 'pending').length, fixed: allItems.filter(item => item.status === 'fixed').length, deferred: allItems.filter(item => item.status === 'deferred').length, failed: allItems.filter(item => item.status === 'failed').length }; const total = allItems.length; const remaining = statusCounts.pending + statusCounts.failed; let summary = `**Total Items**: ${total}\n`; if (statusCounts.fixed > 0) summary += `✅ **Fixed**: ${statusCounts.fixed}\n`; if (statusCounts.deferred > 0) summary += `📋 **Deferred**: ${statusCounts.deferred}\n`; if (remaining > 0) summary += `⏳ **Remaining**: ${remaining} (${statusCounts.pending} pending, ${statusCounts.failed} failed)\n`; if (remaining === 0) { summary += "\n🎉 All items have been addressed!"; } return summary; } /** * Generate formatted list of pending/failed items for fix phase */ generatePendingItemsList(session) { if (!session.sectionResults) { return "No test results available."; } const pendingItems = []; const issues = []; const recommendations = []; // Collect all pending/failed items from all sections Object.values(session.sectionResults).forEach(result => { const pendingIssues = result.issues.filter(item => item.status === 'pending' || item.status === 'failed'); const pendingRecs = result.recommendations.filter(item => item.status === 'pending' || item.status === 'failed'); issues.push(...pendingIssues); recommendations.push(...pendingRecs); pendingItems.push(...pendingIssues, ...pendingRecs); }); if (pendingItems.length === 0) { return "No pending items - all issues and recommendations have been addressed!"; } let output = ""; // Format issues section if (issues.length > 0) { output += "### Issues Found (Items requiring fixes)\n"; issues.forEach(item => { const statusIndicator = item.status === 'failed' ? ' ❌ [RETRY]' : ''; output += `${item.id}. ${item.text}${statusIndicator}\n`; }); output += "\n"; } // Format recommendations section if (recommendations.length > 0) { output += "### Recommendations (Items suggesting improvements)\n"; recommendations.forEach(item => { const statusIndicator = item.status === 'failed' ? ' ❌ [RETRY]' : ''; output += `${item.id}. ${item.text}${statusIndicator}\n`; }); } return output; } /** * Update the status of a specific FixableItem by ID */ updateFixableItemStatus(sessionId, itemId, status, explanation, args) { const session = this.loadSession(sessionId, args || {}); if (!session || !session.sectionResults) { throw new Error(`Session ${sessionId} not found or has no test results`); } let itemFound = false; // Search through all sections to find the item with the specified ID Object.values(session.sectionResults).forEach(result => { // Check issues const issueIndex = result.issues.findIndex(item => item.id === itemId); if (issueIndex !== -1) { result.issues[issueIndex].status = status; if (explanation) result.issues[issueIndex].explanation = explanation; itemFound = true; return; } // Check recommendations const recIndex = result.recommendations.findIndex(item => item.id === itemId); if (recIndex !== -1) { result.recommendations[recIndex].status = status; if (explanation) result.recommendations[recIndex].explanation = explanation; itemFound = true; return; } }); if (!itemFound) { throw new Error(`FixableItem with ID ${itemId} not found in session ${sessionId}`); } this.saveSession(session, args || {}); } /** * Update multiple FixableItem statuses at once */ updateMultipleFixableItemStatuses(sessionId, updates, args) { const session = this.loadSession(sessionId, args || {}); if (!session || !session.sectionResults) { throw new Error(`Session ${sessionId} not found or has no test results`); } const notFoundItems = []; // Update each item updates.forEach(update => { let itemFound = false; Object.values(session.sectionResults).forEach(result => { // Check issues const issueIndex = result.issues.findIndex(item => item.id === update.itemId); if (issueIndex !== -1) { result.issues[issueIndex].status = update.status; if (update.explanation) result.issues[issueIndex].explanation = update.explanation; itemFound = true; return; } // Check recommendations const recIndex = result.recommendations.findIndex(item => item.id === update.itemId); if (recIndex !== -1) { result.recommendations[recIndex].status = update.status; if (update.explanation) result.recommendations[recIndex].explanation = update.explanation; itemFound = true; return; } }); if (!itemFound) { notFoundItems.push(update.itemId); } }); if (notFoundItems.length > 0) { throw new Error(`FixableItems with IDs not found: ${notFoundItems.join(', ')}`); } this.saveSession(session, args || {}); } /** * Get all FixableItems with pending or failed status */ getPendingFixableItems(sessionId, args) { const session = this.loadSession(sessionId, args || {}); if (!session || !session.sectionResults) { return []; } const pendingItems = []; Object.values(session.sectionResults).forEach(result => { const pendingIssues = result.issues.filter(item => item.status === 'pending' || item.status === 'failed'); const pendingRecs = result.recommendations.filter(item => item.status === 'pending' || item.status === 'failed'); pendingItems.push(...pendingIssues, ...pendingRecs); }); return pendingItems.sort((a, b) => a.id - b.id); // Sort by ID for consistent ordering } } exports.DocTestingSessionManager = DocTestingSessionManager;