aiwg
Version:
Cognitive architecture for AI-augmented software development with structured memory, ensemble validation, and closed-loop correction. FAIR-aligned artifacts, 84% cost reduction via human-in-the-loop, standards adopted by 100+ organizations.
203 lines (167 loc) • 5.36 kB
text/typescript
/**
* ID Extractor - Extract requirement IDs from various file types
* Supports: Use Cases (UC-xxx), NFRs (NFR-XXX-xxx), User Stories (US-xxx), Features (F-xxx)
*/
export interface RequirementId {
id: string;
type: 'use-case' | 'nfr' | 'user-story' | 'feature' | 'acceptance-criteria';
lineNumber?: number;
context?: string;
}
export interface ExtractionResult {
filePath: string;
ids: RequirementId[];
extractionTime: number;
}
/**
* IDExtractor - Extract requirement IDs from code, tests, and documentation
*/
export class IDExtractor {
// Regex patterns for different ID types
private readonly patterns = {
useCase: /\bUC-\d{3}\b/g,
nfr: /\bNFR-[A-Z]{3,10}-\d{3}\b/g,
userStory: /\bUS-\d{3}\b/g,
feature: /\bF-\d{3}\b/g,
acceptanceCriteria: /\bAC-\d{3}\b/g
};
// Combined pattern for all ID types
private readonly combinedPattern = /\b(?:UC-\d{3}|NFR-[A-Z]{3,10}-\d{3}|US-\d{3}|F-\d{3}|AC-\d{3})\b/g;
/**
* Extract IDs from a single line of text
*/
extractFromLine(line: string, lineNumber?: number): RequirementId[] {
const ids: RequirementId[] = [];
const matches = line.match(this.combinedPattern);
if (!matches) {
return ids;
}
// Remove duplicates
const uniqueMatches = Array.from(new Set(matches));
for (const id of uniqueMatches) {
ids.push({
id,
type: this.determineType(id),
lineNumber,
context: this.extractContext(line, id)
});
}
return ids;
}
/**
* Extract IDs from file content
*/
extractFromContent(content: string, _filePath?: string): RequirementId[] {
const lines = content.split('\n');
const allIds: RequirementId[] = [];
for (let i = 0; i < lines.length; i++) {
const lineIds = this.extractFromLine(lines[i], i + 1);
allIds.push(...lineIds);
}
// Remove duplicate IDs (keep first occurrence)
const seen = new Set<string>();
const uniqueIds: RequirementId[] = [];
for (const reqId of allIds) {
if (!seen.has(reqId.id)) {
seen.add(reqId.id);
uniqueIds.push(reqId);
}
}
return uniqueIds;
}
/**
* Extract IDs from multiple files (parallel processing)
*/
async extractFromFiles(files: Map<string, string>): Promise<Map<string, ExtractionResult>> {
const results = new Map<string, ExtractionResult>();
// Process all files in parallel
const promises = Array.from(files.entries()).map(async ([filePath, content]) => {
const startTime = performance.now();
const ids = this.extractFromContent(content, filePath);
const extractionTime = performance.now() - startTime;
return {
filePath,
result: {
filePath,
ids,
extractionTime
}
};
});
const completed = await Promise.all(promises);
for (const { filePath, result } of completed) {
results.set(filePath, result);
}
return results;
}
/**
* Determine requirement type from ID pattern
*/
private determineType(id: string): RequirementId['type'] {
if (id.startsWith('UC-')) return 'use-case';
if (id.startsWith('NFR-')) return 'nfr';
if (id.startsWith('US-')) return 'user-story';
if (id.startsWith('F-')) return 'feature';
if (id.startsWith('AC-')) return 'acceptance-criteria';
// Should never happen due to regex, but TypeScript needs it
return 'use-case';
}
/**
* Extract context around the ID (up to 50 characters before/after)
*/
private extractContext(line: string, id: string): string {
const index = line.indexOf(id);
if (index === -1) return line.trim();
const start = Math.max(0, index - 50);
const end = Math.min(line.length, index + id.length + 50);
let context = line.substring(start, end).trim();
// Add ellipsis if truncated
if (start > 0) context = '...' + context;
if (end < line.length) context = context + '...';
return context;
}
/**
* Validate ID format
*/
isValidId(id: string): boolean {
const pattern = /\b(?:UC-\d{3}|NFR-[A-Z]{3,10}-\d{3}|US-\d{3}|F-\d{3}|AC-\d{3})\b/;
return pattern.test(id);
}
/**
* Parse ID to extract components (e.g., NFR-PERF-001 -> {prefix: 'NFR', category: 'PERF', number: '001'})
*/
parseId(id: string): { prefix: string; category?: string; number: string } | null {
// Use case: UC-001
const ucMatch = id.match(/^(UC)-(\d{3})$/);
if (ucMatch) {
return { prefix: ucMatch[1], number: ucMatch[2] };
}
// NFR: NFR-PERF-001
const nfrMatch = id.match(/^(NFR)-([A-Z]{3,6})-(\d{3})$/);
if (nfrMatch) {
return { prefix: nfrMatch[1], category: nfrMatch[2], number: nfrMatch[3] };
}
// User Story: US-001
const usMatch = id.match(/^(US)-(\d{3})$/);
if (usMatch) {
return { prefix: usMatch[1], number: usMatch[2] };
}
// Feature: F-001
const fMatch = id.match(/^(F)-(\d{3})$/);
if (fMatch) {
return { prefix: fMatch[1], number: fMatch[2] };
}
// Acceptance Criteria: AC-001
const acMatch = id.match(/^(AC)-(\d{3})$/);
if (acMatch) {
return { prefix: acMatch[1], number: acMatch[2] };
}
return null;
}
/**
* Get all patterns for testing/validation
*/
getPatterns() {
return { ...this.patterns };
}
}