UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

325 lines 12.2 kB
/** * UseCaseParser - Parse use case documents to extract testable scenarios * * Parses use case markdown documents following the standard template format * and extracts structured test scenarios for test generation. * * @module src/testing/generators/use-case-parser */ import * as fs from 'fs/promises'; import * as path from 'path'; // =========================== // UseCaseParser Class // =========================== export class UseCaseParser { MAIN_SCENARIO_PATTERN = /^#+\s*(Main\s+)?Success\s+Scenario/im; EXTENSION_PATTERN = /^#+\s*Extensions?/im; EXCEPTION_PATTERN = /^#+\s*Exception(s|\s+Scenario)/im; STEP_PATTERN = /^\s*(\d+)\.\s+(?:\*\*(\w+)\*\*:?\s+)?(.+)/; PRECONDITION_PATTERN = /^#+\s*Pre-?conditions?/im; POSTCONDITION_PATTERN = /^#+\s*Post-?conditions?/im; NFR_REF_PATTERN = /NFR-[A-Z]+-\d+/g; UC_REF_PATTERN = /UC-\d+/g; /** * Parse a use case document from file * * @param filePath - Path to use case markdown file * @returns Parse result with structured document */ async parseFile(filePath) { try { const content = await fs.readFile(filePath, 'utf-8'); return this.parseContent(content, path.basename(filePath)); } catch (error) { return { success: false, errors: [`Failed to read file: ${error.message}`], warnings: [] }; } } /** * Parse use case content from string * * @param content - Markdown content * @param sourceName - Source identifier for error messages * @returns Parse result with structured document */ parseContent(content, sourceName = 'input') { const errors = []; const warnings = []; // Extract use case ID from content const idMatch = content.match(/UC-(\d+)/); const id = idMatch ? `UC-${idMatch[1]}` : this.extractIdFromFilename(sourceName); if (!id) { errors.push('Could not determine use case ID'); } // Extract title from first heading const titleMatch = content.match(/^#\s+(.+)/m); const title = titleMatch ? titleMatch[1].trim() : 'Unknown Use Case'; // Extract actor const actorMatch = content.match(/\*\*(?:Primary\s+)?Actor\*\*:\s*(.+)/i) || content.match(/Actor:\s*(.+)/i); const actor = actorMatch ? actorMatch[1].trim() : 'User'; // Extract description const descMatch = content.match(/\*\*Description\*\*:\s*(.+)/i) || content.match(/^>\s*(.+)/m); const description = descMatch ? descMatch[1].trim() : ''; // Extract priority const priorityMatch = content.match(/\*\*Priority\*\*:\s*(\w+)/i); const priority = this.normalizePriority(priorityMatch?.[1] || 'medium'); // Parse preconditions const preconditions = this.parseBulletList(content, this.PRECONDITION_PATTERN); // Parse postconditions const postconditions = this.parseBulletList(content, this.POSTCONDITION_PATTERN); // Parse main scenario const mainScenario = this.parseMainScenario(content, id || 'UC-000'); if (!mainScenario) { warnings.push('No main success scenario found'); } // Parse extensions const extensions = this.parseExtensions(content, id || 'UC-000'); // Parse exceptions const exceptions = this.parseExceptions(content, id || 'UC-000'); // Extract NFR references const nfrs = this.extractReferences(content, this.NFR_REF_PATTERN); // Extract related use cases const relatedUseCases = this.extractReferences(content, this.UC_REF_PATTERN) .filter(ref => ref !== id); if (errors.length > 0) { return { success: false, errors, warnings }; } return { success: true, document: { id: id, title, actor, description, preconditions, postconditions, mainScenario: mainScenario || this.createEmptyScenario(id, 'main'), extensions, exceptions, nfrs, relatedUseCases, priority }, errors: [], warnings }; } /** * Parse multiple use case files from a directory * * @param dirPath - Directory containing use case files * @returns Array of parse results */ async parseDirectory(dirPath) { const results = new Map(); try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { if (entry.isFile() && entry.name.match(/UC-\d+.*\.md$/i)) { const filePath = path.join(dirPath, entry.name); const result = await this.parseFile(filePath); results.set(entry.name, result); } } } catch (error) { // Return empty map if directory doesn't exist } return results; } /** * Extract testable scenarios from a use case document * * @param document - Parsed use case document * @returns Array of all testable scenarios */ extractTestableScenarios(document) { const scenarios = []; // Add main scenario scenarios.push(document.mainScenario); // Add extensions scenarios.push(...document.extensions); // Add exceptions scenarios.push(...document.exceptions); return scenarios; } // =========================== // Private Parsing Methods // =========================== parseMainScenario(content, useCaseId) { const match = content.match(this.MAIN_SCENARIO_PATTERN); if (!match) return null; const sectionStart = match.index + match[0].length; const sectionContent = this.extractSectionContent(content, sectionStart); const steps = this.parseSteps(sectionContent); return { id: `${useCaseId}-MSS`, name: 'Main Success Scenario', type: 'main', preconditions: [], steps, postconditions: [] }; } parseExtensions(content, useCaseId) { const extensions = []; const match = content.match(this.EXTENSION_PATTERN); if (!match) return extensions; const sectionStart = match.index + match[0].length; const sectionContent = this.extractSectionContent(content, sectionStart); // Parse extension scenarios (e.g., "2a. User cancels...") const extensionPattern = /(\d+)([a-z])\.\s+(.+?)(?=\n\d+[a-z]\.|\n#+|\n\n\n|$)/gs; let extMatch; while ((extMatch = extensionPattern.exec(sectionContent)) !== null) { const stepNum = parseInt(extMatch[1]); const extId = extMatch[2]; const extContent = extMatch[3].trim(); // Parse extension steps const extSteps = this.parseExtensionSteps(extContent); extensions.push({ id: `${useCaseId}-EXT-${stepNum}${extId.toUpperCase()}`, name: `Extension at step ${stepNum}`, type: 'extension', preconditions: [], steps: extSteps, postconditions: [], extensionOf: `${useCaseId}-MSS`, triggeredAt: stepNum }); } return extensions; } parseExceptions(content, useCaseId) { const exceptions = []; const match = content.match(this.EXCEPTION_PATTERN); if (!match) return exceptions; const sectionStart = match.index + match[0].length; const sectionContent = this.extractSectionContent(content, sectionStart); // Parse exception scenarios const exceptionPattern = /\*\*([^*]+)\*\*:\s*(.+?)(?=\n\*\*|\n#+|\n\n\n|$)/gs; let exMatch; let counter = 1; while ((exMatch = exceptionPattern.exec(sectionContent)) !== null) { const name = exMatch[1].trim(); const description = exMatch[2].trim(); exceptions.push({ id: `${useCaseId}-EXC-${String(counter).padStart(2, '0')}`, name, type: 'exception', preconditions: [], steps: [{ number: 1, actor: 'system', action: description }], postconditions: [] }); counter++; } return exceptions; } parseSteps(content) { const steps = []; const lines = content.split('\n'); for (const line of lines) { const match = line.match(this.STEP_PATTERN); if (match) { const number = parseInt(match[1]); const actorHint = match[2]?.toLowerCase() || ''; const action = match[3].trim(); let actor = 'system'; if (actorHint.includes('user') || action.toLowerCase().startsWith('user')) { actor = 'user'; } else if (actorHint.includes('external') || action.toLowerCase().includes('external')) { actor = 'external'; } steps.push({ number, actor, action }); } } return steps; } parseExtensionSteps(content) { const steps = []; const lines = content.split('\n'); let stepNum = 1; for (const line of lines) { const trimmed = line.trim(); if (trimmed && !trimmed.startsWith('#')) { // Determine actor from content let actor = 'system'; if (trimmed.toLowerCase().includes('user')) { actor = 'user'; } steps.push({ number: stepNum++, actor, action: trimmed }); } } return steps; } parseBulletList(content, sectionPattern) { const items = []; const match = content.match(sectionPattern); if (!match) return items; const sectionStart = match.index + match[0].length; const sectionContent = this.extractSectionContent(content, sectionStart); // Parse bullet points const bulletPattern = /^\s*[-*]\s+(.+)/gm; let bulletMatch; while ((bulletMatch = bulletPattern.exec(sectionContent)) !== null) { items.push(bulletMatch[1].trim()); } return items; } extractSectionContent(content, startIndex) { // Find next heading or end of content const remaining = content.substring(startIndex); const nextHeading = remaining.search(/\n#+\s/); if (nextHeading === -1) { return remaining; } return remaining.substring(0, nextHeading); } extractReferences(content, pattern) { const matches = content.match(pattern) || []; return [...new Set(matches)]; // Deduplicate } extractIdFromFilename(filename) { const match = filename.match(/UC-(\d+)/i); return match ? `UC-${match[1]}` : null; } normalizePriority(priority) { const lower = priority.toLowerCase(); if (lower.includes('critical') || lower === 'p0') return 'critical'; if (lower.includes('high') || lower === 'p1') return 'high'; if (lower.includes('low') || lower === 'p3') return 'low'; return 'medium'; } createEmptyScenario(useCaseId, type) { return { id: `${useCaseId}-${type.toUpperCase()}`, name: `Empty ${type} scenario`, type, preconditions: [], steps: [], postconditions: [] }; } } //# sourceMappingURL=use-case-parser.js.map