diff --git a/tools/analysis/test/analyze-test-coverage.ts b/tools/analysis/test/analyze-test-coverage.ts index cff92f47a..375f11dcf 100644 --- a/tools/analysis/test/analyze-test-coverage.ts +++ b/tools/analysis/test/analyze-test-coverage.ts @@ -1,332 +1,17 @@ #!/usr/bin/env node -/** - * Function-to-Test Coverage Analyzer - * - * This script: - * 1. Scans all TypeScript/TSX source files for exported functions - * 2. Scans all test files for test cases - * 3. Maps functions to tests - * 4. Reports functions without tests - * 5. Generates a coverage report - */ +import { analyzeCoverage } from './analyze-test-coverage/coverage-runner' +import { printReport, writeJsonReport } from './analyze-test-coverage/reporter' -import * as fs from "fs"; -import * as path from "path"; -import { glob } from "glob"; - -interface FunctionDef { - name: string; - file: string; - type: "named" | "default" | "class-method"; - line: number; -} - -interface TestCase { - name: string; - file: string; - functions: string[]; - line: number; -} - -interface CoverageReport { - totalFunctions: number; - testedFunctions: number; - untested: FunctionDef[]; - tested: Map; - coverage: number; -} - -// Configuration -const CONFIG = { - srcPatterns: [ - "src/**/*.ts", - "src/**/*.tsx", - "packages/**/src/**/*.ts", - "packages/**/src/**/*.tsx", - "dbal/development/**/*.ts", - ], - testPatterns: [ - "src/**/*.test.ts", - "src/**/*.test.tsx", - "packages/**/tests/**/*.test.ts", - "packages/**/tests/**/*.test.tsx", - "dbal/**/*.test.ts", - ], - ignorePatterns: [ - "**/node_modules/**", - "**/.next/**", - "**/build/**", - "**/*.d.ts", - "**/dist/**", - ], -}; - -// Extract function names from source code -function extractFunctions(content: string, file: string): FunctionDef[] { - const functions: FunctionDef[] = []; - const lines = content.split("\n"); - - lines.forEach((line, index) => { - const lineNum = index + 1; - - // Named exports: export function name() or export const name = () => - const namedFuncMatch = line.match( - /export\s+(?:function|const)\s+(\w+)\s*(?:\(|=)/ - ); - if (namedFuncMatch) { - functions.push({ - name: namedFuncMatch[1], - file, - type: "named", - line: lineNum, - }); - } - - // Class methods: methodName() { or methodName = () => { - const classMethodMatch = line.match(/^\s+(\w+)\s*\(.*\)\s*[:{]/); - if (classMethodMatch && line.includes("class")) { - functions.push({ - name: classMethodMatch[1], - file, - type: "class-method", - line: lineNum, - }); - } - - // Default exports - if (line.includes("export default") && line.includes("function")) { - const defaultMatch = line.match(/export\s+default\s+function\s+(\w+)/); - if (defaultMatch) { - functions.push({ - name: defaultMatch[1], - file, - type: "default", - line: lineNum, - }); - } - } - }); - - return functions; -} - -// Extract test cases and mentioned functions -function extractTestCases( - content: string, - file: string -): Map { - const testMap = new Map(); - const lines = content.split("\n"); - let currentTestName = ""; - - lines.forEach((line) => { - // Detect test blocks: it(), test(), describe() - const testMatch = line.match( - /(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/ - ); - if (testMatch) { - currentTestName = testMatch[1]; - testMap.set(currentTestName, []); - } - - if (currentTestName) { - // Look for function calls within the test - const funcCalls = line.match(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g); - if (funcCalls) { - funcCalls.forEach((call) => { - const funcName = call.replace(/\s*\($/, ""); - if (funcName && !isCommonTestHelper(funcName)) { - testMap.get(currentTestName)!.push(funcName); - } - }); - } - } - }); - - return testMap; -} - -// Filter out common test helpers -function isCommonTestHelper(name: string): boolean { - const helpers = [ - "it", - "test", - "describe", - "expect", - "beforeEach", - "afterEach", - "beforeAll", - "afterAll", - "jest", - "vi", - "assert", - "eq", - "ok", - "throws", - "doesNotThrow", - "async", - "render", - "screen", - "fireEvent", - "userEvent", - "waitFor", - "within", - ]; - return helpers.includes(name); -} - -// Main analysis function -async function analyze(): Promise { - const cwd = process.cwd(); - - // Get all source files - const srcFiles = await glob(CONFIG.srcPatterns, { - cwd, - ignore: CONFIG.ignorePatterns, - }); - - // Get all test files - const testFiles = await glob(CONFIG.testPatterns, { - cwd, - ignore: CONFIG.ignorePatterns, - }); - - // Extract functions from source files - const allFunctions: FunctionDef[] = []; - for (const file of srcFiles) { - try { - const content = fs.readFileSync(path.join(cwd, file), "utf-8"); - const funcs = extractFunctions(content, file); - allFunctions.push(...funcs); - } catch (e) { - console.warn(`Error reading ${file}:`, (e as Error).message); - } - } - - // Extract test cases and create mapping - const testMapping = new Map(); - const testedFunctionNames = new Set(); - - for (const file of testFiles) { - try { - const content = fs.readFileSync(path.join(cwd, file), "utf-8"); - const testCases = extractTestCases(content, file); - - testCases.forEach((functionNames, testName) => { - functionNames.forEach((funcName) => { - testedFunctionNames.add(funcName); - - if (!testMapping.has(funcName)) { - testMapping.set(funcName, []); - } - testMapping.get(funcName)!.push({ - name: testName, - file, - functions: functionNames, - line: 0, - }); - }); - }); - } catch (e) { - console.warn(`Error reading ${file}:`, (e as Error).message); - } - } - - // Identify untested functions - const untested = allFunctions.filter((f) => !testedFunctionNames.has(f.name)); - - // Calculate coverage - const coverage = ( - ((allFunctions.length - untested.length) / allFunctions.length) * - 100 - ).toFixed(2); - - return { - totalFunctions: allFunctions.length, - testedFunctions: allFunctions.length - untested.length, - untested, - tested: testMapping, - coverage: parseFloat(coverage), - }; -} - -// Generate report -async function generateReport() { +const run = async () => { try { - const report = await analyze(); - - console.log("\n" + "=".repeat(70)); - console.log("FUNCTION-TO-TEST COVERAGE ANALYSIS"); - console.log("=".repeat(70)); - - console.log(`\nSummary:`); - console.log(` Total Functions: ${report.totalFunctions}`); - console.log(` Tested Functions: ${report.testedFunctions}`); - console.log(` Untested Functions: ${report.untested.length}`); - console.log(` Coverage: ${report.coverage}%`); - - if (report.untested.length > 0) { - console.log(`\n${"─".repeat(70)}`); - console.log("UNTESTED FUNCTIONS:"); - console.log(`${"─".repeat(70)}`); - - // Group by file - const grouped = new Map(); - report.untested.forEach((fn) => { - if (!grouped.has(fn.file)) { - grouped.set(fn.file, []); - } - grouped.get(fn.file)!.push(fn); - }); - - grouped.forEach((fns, file) => { - console.log(`\nšŸ“„ ${file}`); - fns.forEach((fn) => { - console.log( - ` └─ ${fn.name} (${fn.type}) [line ${fn.line}]` - ); - }); - }); - - // Generate TODO items - console.log(`\n${"─".repeat(70)}`); - console.log("TODO - CREATE TESTS FOR:"); - console.log(`${"─".repeat(70)}`); - - report.untested.forEach((fn) => { - console.log(`- [ ] Write test for \`${fn.name}\` in ${fn.file}`); - }); - } - - console.log("\n" + "=".repeat(70) + "\n"); - - // Generate JSON report - const reportPath = path.join(process.cwd(), "coverage-report.json"); - fs.writeFileSync( - reportPath, - JSON.stringify( - { - timestamp: new Date().toISOString(), - summary: { - totalFunctions: report.totalFunctions, - testedFunctions: report.testedFunctions, - untestedFunctions: report.untested.length, - coverage: report.coverage, - }, - untested: report.untested, - }, - null, - 2 - ) - ); - - console.log(`āœ… Report saved to: coverage-report.json`); + const report = await analyzeCoverage() + printReport(report) + writeJsonReport(report) } catch (e) { - console.error("Error analyzing coverage:", e); - process.exit(1); + console.error('Error analyzing coverage:', e) + process.exit(1) } } -// Run the analysis -generateReport(); +run() diff --git a/tools/analysis/test/analyze-test-coverage/config.ts b/tools/analysis/test/analyze-test-coverage/config.ts new file mode 100644 index 000000000..aaf28885e --- /dev/null +++ b/tools/analysis/test/analyze-test-coverage/config.ts @@ -0,0 +1,17 @@ +export const COVERAGE_CONFIG = { + srcPatterns: [ + 'src/**/*.ts', + 'src/**/*.tsx', + 'packages/**/src/**/*.ts', + 'packages/**/src/**/*.tsx', + 'dbal/development/**/*.ts' + ], + testPatterns: [ + 'src/**/*.test.ts', + 'src/**/*.test.tsx', + 'packages/**/tests/**/*.test.ts', + 'packages/**/tests/**/*.test.tsx', + 'dbal/**/*.test.ts' + ], + ignorePatterns: ['**/node_modules/**', '**/.next/**', '**/build/**', '**/*.d.ts', '**/dist/**'] +} diff --git a/tools/analysis/test/analyze-test-coverage/coverage-runner.ts b/tools/analysis/test/analyze-test-coverage/coverage-runner.ts new file mode 100644 index 000000000..02394c7b7 --- /dev/null +++ b/tools/analysis/test/analyze-test-coverage/coverage-runner.ts @@ -0,0 +1,65 @@ +import * as fs from 'fs' +import * as path from 'path' +import { glob } from 'glob' +import { COVERAGE_CONFIG } from './config' +import { extractFunctions, FunctionDef } from './function-extractor' +import { extractTestCases, TestCase } from './test-extractor' + +export interface CoverageReport { + totalFunctions: number + testedFunctions: number + untested: FunctionDef[] + tested: Map + coverage: number +} + +export const analyzeCoverage = async (): Promise => { + const cwd = process.cwd() + const srcFiles = await glob(COVERAGE_CONFIG.srcPatterns, { cwd, ignore: COVERAGE_CONFIG.ignorePatterns }) + const testFiles = await glob(COVERAGE_CONFIG.testPatterns, { cwd, ignore: COVERAGE_CONFIG.ignorePatterns }) + + const allFunctions: FunctionDef[] = [] + for (const file of srcFiles) { + try { + const content = fs.readFileSync(path.join(cwd, file), 'utf-8') + const funcs = extractFunctions(content, file) + allFunctions.push(...funcs) + } catch (e) { + console.warn(`Error reading ${file}:`, (e as Error).message) + } + } + + const testMapping = new Map() + const testedFunctionNames = new Set() + + for (const file of testFiles) { + try { + const content = fs.readFileSync(path.join(cwd, file), 'utf-8') + const testCases = extractTestCases(content, file) + + testCases.forEach((functionNames, testName) => { + functionNames.forEach(funcName => { + testedFunctionNames.add(funcName) + + if (!testMapping.has(funcName)) { + testMapping.set(funcName, []) + } + testMapping.get(funcName)!.push({ name: testName, file, functions: functionNames, line: 0 }) + }) + }) + } catch (e) { + console.warn(`Error reading ${file}:`, (e as Error).message) + } + } + + const untested = allFunctions.filter(f => !testedFunctionNames.has(f.name)) + const coverage = (((allFunctions.length - untested.length) / allFunctions.length) * 100).toFixed(2) + + return { + totalFunctions: allFunctions.length, + testedFunctions: allFunctions.length - untested.length, + untested, + tested: testMapping, + coverage: parseFloat(coverage) + } +} diff --git a/tools/analysis/test/analyze-test-coverage/function-extractor.ts b/tools/analysis/test/analyze-test-coverage/function-extractor.ts new file mode 100644 index 000000000..8d64b4fd6 --- /dev/null +++ b/tools/analysis/test/analyze-test-coverage/function-extractor.ts @@ -0,0 +1,49 @@ +export interface FunctionDef { + name: string + file: string + type: 'named' | 'default' | 'class-method' + line: number +} + +export const extractFunctions = (content: string, file: string): FunctionDef[] => { + const functions: FunctionDef[] = [] + const lines = content.split('\n') + + lines.forEach((line, index) => { + const lineNum = index + 1 + + const namedFuncMatch = line.match(/export\s+(?:function|const)\s+(\w+)\s*(?:\(|=)/) + if (namedFuncMatch) { + functions.push({ + name: namedFuncMatch[1], + file, + type: 'named', + line: lineNum + }) + } + + const classMethodMatch = line.match(/^\s+(\w+)\s*\(.*\)\s*[:{]/) + if (classMethodMatch && line.includes('class')) { + functions.push({ + name: classMethodMatch[1], + file, + type: 'class-method', + line: lineNum + }) + } + + if (line.includes('export default') && line.includes('function')) { + const defaultMatch = line.match(/export\s+default\s+function\s+(\w+)/) + if (defaultMatch) { + functions.push({ + name: defaultMatch[1], + file, + type: 'default', + line: lineNum + }) + } + } + }) + + return functions +} diff --git a/tools/analysis/test/analyze-test-coverage/reporter.ts b/tools/analysis/test/analyze-test-coverage/reporter.ts new file mode 100644 index 000000000..a3389f763 --- /dev/null +++ b/tools/analysis/test/analyze-test-coverage/reporter.ts @@ -0,0 +1,68 @@ +import * as fs from 'fs' +import * as path from 'path' +import { CoverageReport, FunctionDef } from './coverage-runner' + +export const printReport = (report: CoverageReport) => { + console.log('\n' + '='.repeat(70)) + console.log('FUNCTION-TO-TEST COVERAGE ANALYSIS') + console.log('='.repeat(70)) + + console.log(`\nSummary:`) + console.log(` Total Functions: ${report.totalFunctions}`) + console.log(` Tested Functions: ${report.testedFunctions}`) + console.log(` Untested Functions: ${report.untested.length}`) + console.log(` Coverage: ${report.coverage}%`) + + if (report.untested.length > 0) { + console.log(`\n${'─'.repeat(70)}`) + console.log('UNTESTED FUNCTIONS:') + console.log(`${'─'.repeat(70)}`) + + const grouped = new Map() + report.untested.forEach(fn => { + if (!grouped.has(fn.file)) { + grouped.set(fn.file, []) + } + grouped.get(fn.file)!.push(fn) + }) + + grouped.forEach((fns, file) => { + console.log(`\nšŸ“„ ${file}`) + fns.forEach(fn => { + console.log(` └─ ${fn.name} (${fn.type}) [line ${fn.line}]`) + }) + }) + + console.log(`\n${'─'.repeat(70)}`) + console.log('TODO - CREATE TESTS FOR:') + console.log(`${'─'.repeat(70)}`) + + report.untested.forEach(fn => { + console.log(`- [ ] Write test for \`${fn.name}\` in ${fn.file}`) + }) + } + + console.log('\n' + '='.repeat(70) + '\n') +} + +export const writeJsonReport = (report: CoverageReport) => { + const reportPath = path.join(process.cwd(), 'coverage-report.json') + fs.writeFileSync( + reportPath, + JSON.stringify( + { + timestamp: new Date().toISOString(), + summary: { + totalFunctions: report.totalFunctions, + testedFunctions: report.testedFunctions, + untestedFunctions: report.untested.length, + coverage: report.coverage + }, + untested: report.untested + }, + null, + 2 + ) + ) + console.log(`āœ… Report saved to: coverage-report.json`) +} diff --git a/tools/analysis/test/analyze-test-coverage/test-extractor.ts b/tools/analysis/test/analyze-test-coverage/test-extractor.ts new file mode 100644 index 000000000..bcdd273e8 --- /dev/null +++ b/tools/analysis/test/analyze-test-coverage/test-extractor.ts @@ -0,0 +1,62 @@ +export interface TestCase { + name: string + file: string + functions: string[] + line: number +} + +export const extractTestCases = (content: string, file: string): Map => { + const testMap = new Map() + const lines = content.split('\n') + let currentTestName = '' + + lines.forEach(line => { + const testMatch = line.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/) + if (testMatch) { + currentTestName = testMatch[1] + testMap.set(currentTestName, []) + } + + if (currentTestName) { + const funcCalls = line.match(/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\(/g) + if (funcCalls) { + funcCalls.forEach(call => { + const funcName = call.replace(/\s*\($/, '') + if (funcName && !isCommonTestHelper(funcName)) { + testMap.get(currentTestName)!.push(funcName) + } + }) + } + } + }) + + return testMap +} + +const isCommonTestHelper = (name: string): boolean => { + const helpers = [ + 'it', + 'test', + 'describe', + 'expect', + 'beforeEach', + 'afterEach', + 'beforeAll', + 'afterAll', + 'jest', + 'vi', + 'assert', + 'eq', + 'ok', + 'throws', + 'doesNotThrow', + 'async', + 'render', + 'screen', + 'fireEvent', + 'userEvent', + 'waitFor', + 'within' + ] + return helpers.includes(name) +} diff --git a/tools/detection/detect-stub-implementations.ts b/tools/detection/detect-stub-implementations.ts index f1782347d..0d9680694 100644 --- a/tools/detection/detect-stub-implementations.ts +++ b/tools/detection/detect-stub-implementations.ts @@ -1,215 +1,14 @@ #!/usr/bin/env tsx -import { readdirSync, readFileSync, statSync, writeFileSync } from 'fs' -import { join, extname } from 'path' +import { collectStubs } from './stub-lambdas/directory-walker' +import { summarizeStubs, writeSummary } from './stub-lambdas/reporter' -interface StubLocation { - file: string - line: number - type: 'placeholder-return' | 'not-implemented' | 'empty-body' | 'todo-comment' | 'console-log-only' | 'placeholder-render' | 'mock-data' | 'stub-component' - name: string - severity: 'high' | 'medium' | 'low' - code: string -} - -const STUB_PATTERNS = [ - { - name: 'Not implemented error', - pattern: /throw\s+new\s+Error\s*\(\s*['"]not\s+implemented/i, - type: 'not-implemented' as const, - severity: 'high' as const, - description: 'Function throws "not implemented"' - }, - { - name: 'TODO comment in function', - pattern: /\/\/\s*TODO|\/\/\s*FIXME|\/\/\s*XXX|\/\/\s*HACK/i, - type: 'todo-comment' as const, - severity: 'medium' as const, - description: 'Function has TODO/FIXME comment' - }, - { - name: 'Console.log only', - pattern: /function\s+\w+[^{]*{\s*console\.(log|debug)\s*\([^)]*\)\s*}|const\s+\w+\s*=\s*[^=>\s]*=>\s*console\.(log|debug)/, - type: 'console-log-only' as const, - severity: 'high' as const, - description: 'Function only logs to console' - }, - { - name: 'Return null/undefined stub', - pattern: /return\s+(null|undefined)|return\s*;(?=\s*})/, - type: 'placeholder-return' as const, - severity: 'low' as const, - description: 'Function only returns null/undefined' - }, - { - name: 'Return mock data', - pattern: /return\s+(\{[^}]*\}|\[[^\]]*\])\s*\/\/\s*(mock|stub|todo|placeholder|example)/i, - type: 'mock-data' as const, - severity: 'medium' as const, - description: 'Function returns hardcoded mock data' - }, - { - name: 'Placeholder text in JSX', - pattern: /<[A-Z]\w*[^>]*>\s*(placeholder|TODO|FIXME|stub|mock|example|not implemented)/i, - type: 'placeholder-render' as const, - severity: 'medium' as const, - description: 'Component renders placeholder text' - }, - { - name: 'Empty component body', - pattern: /export\s+(?:default\s+)?(?:function|const)\s+(\w+).*?\{[\s\n]*return\s+<[^>]+>\s*<\/[^>]+>\s*;?[\s\n]*\}/, - type: 'stub-component' as const, - severity: 'high' as const, - description: 'Component has empty/minimal body' - } -] - -function findStubs(): StubLocation[] { - const results: StubLocation[] = [] - const srcDir = 'src' - - function walkDir(dir: string) { - try { - const files = readdirSync(dir) - - for (const file of files) { - const fullPath = join(dir, file) - const stat = statSync(fullPath) - - if (stat.isDirectory() && !['node_modules', '.next', 'dist', 'build', '.git'].includes(file)) { - walkDir(fullPath) - } else if (['.ts', '.tsx', '.js', '.jsx'].includes(extname(file))) { - scanFile(fullPath, results) - } - } - } catch (e) { - // Skip inaccessible directories - } - } - - walkDir(srcDir) - return results -} - -function scanFile(filePath: string, results: StubLocation[]): void { - try { - const content = readFileSync(filePath, 'utf8') - const lines = content.split('\n') - - // Find function/component boundaries - const functionPattern = /(?:export\s+)?(?:async\s+)?(?:function|const)\s+(\w+)/g - let match - - while ((match = functionPattern.exec(content)) !== null) { - const functionName = match[1] - const startIndex = match.index - const lineNumber = content.substring(0, startIndex).split('\n').length - - // Extract function body - const bodyStart = content.indexOf('{', startIndex) - let braceCount = 0 - let bodyEnd = bodyStart - - for (let i = bodyStart; i < content.length; i++) { - if (content[i] === '{') braceCount++ - if (content[i] === '}') braceCount-- - if (braceCount === 0) { - bodyEnd = i - break - } - } - - const functionBody = content.substring(bodyStart, bodyEnd + 1) - - // Check against stub patterns - checkPatterns(functionBody, filePath, lineNumber, functionName, results) - } - - // Check for stub comments in file - lines.forEach((line, idx) => { - if (line.match(/stub|placeholder|mock|not implemented|TODO.*implementation/i)) { - results.push({ - file: filePath, - line: idx + 1, - type: 'todo-comment', - name: 'Stub indicator', - severity: 'low', - code: line.trim() - }) - } - }) - } catch (e) { - // Skip files that can't be analyzed - } -} - -function checkPatterns(body: string, filePath: string, lineNumber: number, name: string, results: StubLocation[]): void { - for (const pattern of STUB_PATTERNS) { - const regex = new RegExp(pattern.pattern.source, 'i') - if (regex.test(body)) { - const bodyLineNum = body.split('\n')[0]?.length > 0 ? - lineNumber : lineNumber + 1 - - results.push({ - file: filePath, - line: bodyLineNum, - type: pattern.type, - name: name, - severity: pattern.severity, - code: body.split('\n').slice(0, 3).join('\n').substring(0, 80) - }) - } - } -} - -// Main execution -const stubs = findStubs() - -// Categorize by severity -const bySeverity = { - high: stubs.filter(s => s.severity === 'high'), - medium: stubs.filter(s => s.severity === 'medium'), - low: stubs.filter(s => s.severity === 'low') -} - -const summary = { - totalStubsFound: stubs.length, - bySeverity: { - high: bySeverity.high.length, - medium: bySeverity.medium.length, - low: bySeverity.low.length - }, - byType: { - 'not-implemented': stubs.filter(s => s.type === 'not-implemented').length, - 'todo-comment': stubs.filter(s => s.type === 'todo-comment').length, - 'console-log-only': stubs.filter(s => s.type === 'console-log-only').length, - 'placeholder-return': stubs.filter(s => s.type === 'placeholder-return').length, - 'mock-data': stubs.filter(s => s.type === 'mock-data').length, - 'placeholder-render': stubs.filter(s => s.type === 'placeholder-render').length, - 'stub-component': stubs.filter(s => s.type === 'stub-component').length, - 'empty-body': stubs.filter(s => s.type === 'empty-body').length - }, - criticalIssues: bySeverity.high.map(s => ({ - file: s.file, - line: s.line, - function: s.name, - type: s.type - })), - details: stubs.sort((a, b) => { - const severityOrder = { high: 0, medium: 1, low: 2 } - return severityOrder[a.severity] - severityOrder[b.severity] - }), - timestamp: new Date().toISOString() -} - -const serialized = JSON.stringify(summary, null, 2) +const rootDir = 'src' const outputPath = process.argv[2] || 'stub-patterns.json' -try { - writeFileSync(outputPath, serialized) - console.error(`Stub summary written to ${outputPath}`) -} catch (error) { - console.error(`Failed to write stub summary to ${outputPath}:`, error) -} +const stubs = collectStubs(rootDir) +const summary = summarizeStubs(stubs) -console.log(serialized) +writeSummary(summary, outputPath) + +console.log(JSON.stringify(summary, null, 2)) diff --git a/tools/detection/stub-lambdas/directory-walker.ts b/tools/detection/stub-lambdas/directory-walker.ts new file mode 100644 index 000000000..5db0d2841 --- /dev/null +++ b/tools/detection/stub-lambdas/directory-walker.ts @@ -0,0 +1,31 @@ +import { readdirSync, statSync } from 'fs' +import { join } from 'path' +import { StubLocation } from './patterns' +import { scanFile } from './file-scanner' +import { isCodeFile } from './patterns' + +export const collectStubs = (rootDir: string): StubLocation[] => { + const results: StubLocation[] = [] + + const walkDir = (dir: string) => { + try { + const files = readdirSync(dir) + + for (const file of files) { + const fullPath = join(dir, file) + const stat = statSync(fullPath) + + if (stat.isDirectory() && !['node_modules', '.next', 'dist', 'build', '.git'].includes(file)) { + walkDir(fullPath) + } else if (isCodeFile(file)) { + scanFile(fullPath, results) + } + } + } catch { + // Skip inaccessible directories + } + } + + walkDir(rootDir) + return results +} diff --git a/tools/detection/stub-lambdas/file-scanner.ts b/tools/detection/stub-lambdas/file-scanner.ts new file mode 100644 index 000000000..f4dcf8e28 --- /dev/null +++ b/tools/detection/stub-lambdas/file-scanner.ts @@ -0,0 +1,72 @@ +import { readFileSync } from 'fs' +import { StubLocation, STUB_PATTERNS } from './patterns' + +export const scanFile = (filePath: string, results: StubLocation[]): void => { + try { + const content = readFileSync(filePath, 'utf8') + const lines = content.split('\n') + const functionPattern = /(?:export\s+)?(?:async\s+)?(?:function|const)\s+(\w+)/g + let match + + while ((match = functionPattern.exec(content)) !== null) { + const functionName = match[1] + const startIndex = match.index + const lineNumber = content.substring(0, startIndex).split('\n').length + + const bodyStart = content.indexOf('{', startIndex) + let braceCount = 0 + let bodyEnd = bodyStart + + for (let i = bodyStart; i < content.length; i++) { + if (content[i] === '{') braceCount++ + if (content[i] === '}') braceCount-- + if (braceCount === 0) { + bodyEnd = i + break + } + } + + const functionBody = content.substring(bodyStart, bodyEnd + 1) + checkPatterns(functionBody, filePath, lineNumber, functionName, results) + } + + lines.forEach((line, idx) => { + if (line.match(/stub|placeholder|mock|not implemented|TODO.*implementation/i)) { + results.push({ + file: filePath, + line: idx + 1, + type: 'todo-comment', + name: 'Stub indicator', + severity: 'low', + code: line.trim() + }) + } + }) + } catch { + // Skip files that can't be analyzed + } +} + +const checkPatterns = ( + body: string, + filePath: string, + lineNumber: number, + name: string, + results: StubLocation[] +): void => { + for (const pattern of STUB_PATTERNS) { + const regex = new RegExp(pattern.pattern.source, 'i') + if (regex.test(body)) { + const bodyLineNum = body.split('\n')[0]?.length > 0 ? lineNumber : lineNumber + 1 + + results.push({ + file: filePath, + line: bodyLineNum, + type: pattern.type, + name, + severity: pattern.severity, + code: body.split('\n').slice(0, 3).join('\n').substring(0, 80) + }) + } + } +} diff --git a/tools/detection/stub-lambdas/patterns.ts b/tools/detection/stub-lambdas/patterns.ts new file mode 100644 index 000000000..a0f314d44 --- /dev/null +++ b/tools/detection/stub-lambdas/patterns.ts @@ -0,0 +1,81 @@ +import { extname } from 'path' + +export interface StubLocation { + file: string + line: number + type: + | 'placeholder-return' + | 'not-implemented' + | 'empty-body' + | 'todo-comment' + | 'console-log-only' + | 'placeholder-render' + | 'mock-data' + | 'stub-component' + name: string + severity: 'high' | 'medium' | 'low' + code: string +} + +export const STUB_PATTERNS = [ + { + name: 'Not implemented error', + pattern: /throw\s+new\s+Error\s*\(\s*['"]not\s+implemented/i, + type: 'not-implemented' as const, + severity: 'high' as const, + description: 'Function throws "not implemented"' + }, + { + name: 'TODO comment in function', + pattern: /\/\/\s*TODO|\/\/\s*FIXME|\/\/\s*XXX|\/\/\s*HACK/i, + type: 'todo-comment' as const, + severity: 'medium' as const, + description: 'Function has TODO/FIXME comment' + }, + { + name: 'Console.log only', + pattern: + /function\s+\w+[^{]*{\s*console\.(log|debug)\s*\([^)]*\)\s*}|const\s+\w+\s*=\s*[^=>\s]*=>\s*console\.(log|debug)/, + type: 'console-log-only' as const, + severity: 'high' as const, + description: 'Function only logs to console' + }, + { + name: 'Return null/undefined stub', + pattern: /return\s+(null|undefined)|return\s*;(?=\s*})/, + type: 'placeholder-return' as const, + severity: 'low' as const, + description: 'Function only returns null/undefined' + }, + { + name: 'Return mock data', + pattern: /return\s+(\{[^}]*\}|\[[^\]]*\])\s*\/\/\s*(mock|stub|placeholder|example)/i, + type: 'mock-data' as const, + severity: 'medium' as const, + description: 'Function returns hardcoded mock data' + }, + { + name: 'Placeholder text in JSX', + pattern: /<[A-Z]\w*[^>]*>\s*(placeholder|TODO|FIXME|stub|mock|example|not implemented)/i, + type: 'placeholder-render' as const, + severity: 'medium' as const, + description: 'Component renders placeholder text' + }, + { + name: 'Empty component body', + pattern: + /export\s+(?:default\s+)?(?:function|const)\s+(\w+).*?\{[\s\n]*return\s+<[^>]+>\s*<\/[^>]+>\s*;?[\s\n]*\}/, + type: 'stub-component' as const, + severity: 'high' as const, + description: 'Component has empty/minimal body' + } +] + +export const isCodeFile = (file: string): boolean => + ['.ts', '.tsx', '.js', '.jsx'].includes(extname(file)) + +export const severityOrder: Record<'high' | 'medium' | 'low', number> = { + high: 0, + medium: 1, + low: 2 +} diff --git a/tools/detection/stub-lambdas/reporter.ts b/tools/detection/stub-lambdas/reporter.ts new file mode 100644 index 000000000..ecdabd7ec --- /dev/null +++ b/tools/detection/stub-lambdas/reporter.ts @@ -0,0 +1,57 @@ +import { writeFileSync } from 'fs' +import { StubLocation, severityOrder } from './patterns' + +export interface StubSummary { + totalStubsFound: number + bySeverity: Record<'high' | 'medium' | 'low', number> + byType: Record + criticalIssues: Array<{ file: string; line: number; function: string; type: StubLocation['type'] }> + details: StubLocation[] + timestamp: string +} + +export const summarizeStubs = (stubs: StubLocation[]): StubSummary => { + const bySeverity = { + high: stubs.filter(s => s.severity === 'high'), + medium: stubs.filter(s => s.severity === 'medium'), + low: stubs.filter(s => s.severity === 'low') + } + + return { + totalStubsFound: stubs.length, + bySeverity: { + high: bySeverity.high.length, + medium: bySeverity.medium.length, + low: bySeverity.low.length + }, + byType: { + 'not-implemented': stubs.filter(s => s.type === 'not-implemented').length, + 'todo-comment': stubs.filter(s => s.type === 'todo-comment').length, + 'console-log-only': stubs.filter(s => s.type === 'console-log-only').length, + 'placeholder-return': stubs.filter(s => s.type === 'placeholder-return').length, + 'mock-data': stubs.filter(s => s.type === 'mock-data').length, + 'placeholder-render': stubs.filter(s => s.type === 'placeholder-render').length, + 'stub-component': stubs.filter(s => s.type === 'stub-component').length, + 'empty-body': stubs.filter(s => s.type === 'empty-body').length + }, + criticalIssues: bySeverity.high.map(s => ({ + file: s.file, + line: s.line, + function: s.name, + type: s.type + })), + details: stubs.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]), + timestamp: new Date().toISOString() + } +} + +export const writeSummary = (summary: StubSummary, outputPath: string): void => { + const serialized = JSON.stringify(summary, null, 2) + + try { + writeFileSync(outputPath, serialized) + console.error(`Stub summary written to ${outputPath}`) + } catch (error) { + console.error(`Failed to write stub summary to ${outputPath}:`, error) + } +} diff --git a/tools/generation/generate-stub-report.ts b/tools/generation/generate-stub-report.ts index 1daeaa99c..958305091 100644 --- a/tools/generation/generate-stub-report.ts +++ b/tools/generation/generate-stub-report.ts @@ -1,204 +1,10 @@ #!/usr/bin/env tsx -import { existsSync, readFileSync } from 'fs' +import { generateStubReport } from './stub-report/report-builder' -function generateStubReport(): string { - let report = '# Stub Implementation Detection Report\n\n' - - report += '## Overview\n\n' - report += 'This report identifies incomplete, placeholder, or stubbed implementations in the codebase.\n' - report += 'Stubs should be replaced with real implementations before production use.\n\n' - - // Load pattern detection results - if (existsSync('stub-patterns.json')) { - try { - const patterns = JSON.parse(readFileSync('stub-patterns.json', 'utf8')) - - report += '## Pattern-Based Detection Results\n\n' - report += `**Total Stubs Found**: ${patterns.totalStubsFound}\n\n` - - // Severity - report += '### By Severity\n\n' - report += `- šŸ”“ **Critical**: ${patterns.bySeverity.high} (blocks production)\n` - report += `- 🟠 **Medium**: ${patterns.bySeverity.medium} (should be fixed)\n` - report += `- 🟔 **Low**: ${patterns.bySeverity.low} (nice to fix)\n\n` - - // Types - report += '### By Type\n\n' - for (const [type, count] of Object.entries(patterns.byType)) { - if (count > 0) { - report += `- **${type}**: ${count}\n` - } - } - report += '\n' - - // Critical issues - if (patterns.criticalIssues && patterns.criticalIssues.length > 0) { - report += '### šŸ”“ Critical Stubs\n\n' - report += 'These must be implemented before production:\n\n' - report += '| File | Line | Function | Type |\n' - report += '|------|------|----------|------|\n' - patterns.criticalIssues.slice(0, 20).forEach(issue => { - report += `| \`${issue.file}\` | ${issue.line} | \`${issue.function}\` | ${issue.type} |\n` - }) - report += '\n' - } - - // Top findings - if (patterns.details && patterns.details.length > 0) { - report += '### Detailed Findings\n\n' - report += '
Click to expand (showing first 15)\n\n' - report += '| File | Line | Function | Type | Code Preview |\n' - report += '|------|------|----------|------|---------------|\n' - patterns.details.slice(0, 15).forEach(item => { - const preview = item.code?.substring(0, 50)?.replace(/\n/g, ' ') || 'N/A' - report += `| ${item.file} | ${item.line} | ${item.name} | ${item.type} | \`${preview}...\` |\n` - }) - report += '\n
\n\n' - } - } catch (e) { - report += 'āš ļø Could not parse pattern detection results.\n\n' - } - } - - // Load completeness analysis - if (existsSync('implementation-analysis.json')) { - try { - const completeness = JSON.parse(readFileSync('implementation-analysis.json', 'utf8')) - - report += '## Implementation Completeness Analysis\n\n' - report += `**Average Completeness Score**: ${completeness.averageCompleteness}%\n\n` - - // Breakdown - report += '### Completeness Levels\n\n' - report += `- **Critical** (0% complete): ${completeness.bySeverity.critical}\n` - report += `- **High** (10-30% complete): ${completeness.bySeverity.high}\n` - report += `- **Medium** (40-70% complete): ${completeness.bySeverity.medium}\n` - report += `- **Low** (80-99% complete): ${completeness.bySeverity.low}\n\n` - - // Flag types - if (Object.values(completeness.flagTypes).some(v => v > 0)) { - report += '### Common Stub Indicators\n\n' - for (const [flag, count] of Object.entries(completeness.flagTypes)) { - if (count > 0) { - report += `- **${flag}**: ${count} instances\n` - } - } - report += '\n' - } - - // Critical stubs - if (completeness.criticalStubs && completeness.criticalStubs.length > 0) { - report += '### šŸ”“ Incomplete Implementations (0% Completeness)\n\n' - report += '
Click to expand\n\n' - completeness.criticalStubs.forEach(stub => { - report += `#### \`${stub.name}\` in \`${stub.file}:${stub.line}\`\n` - report += `**Type**: ${stub.type}\n` - report += `**Flags**: ${stub.flags.join(', ')}\n` - report += `**Summary**: ${stub.summary}\n\n` - }) - report += '
\n\n' - } - } catch (e) { - report += 'āš ļø Could not parse completeness analysis.\n\n' - } - } - - // Recommendations - report += '## How to Fix Stub Implementations\n\n' - - report += '### Pattern: "Not Implemented" Errors\n\n' - report += '```typescript\n' - report += '// āŒ Stub\n' - report += 'export function processData(data) {\n' - report += ' throw new Error("not implemented")\n' - report += '}\n\n' - report += '// āœ… Real implementation\n' - report += 'export function processData(data) {\n' - report += ' return data.map(item => transform(item))\n' - report += '}\n' - report += '```\n\n' - - report += '### Pattern: Console.log Only\n\n' - report += '```typescript\n' - report += '// āŒ Stub\n' - report += 'export function validateEmail(email) {\n' - report += ' console.log("validating:", email)\n' - report += '}\n\n' - report += '// āœ… Real implementation\n' - report += 'export function validateEmail(email: string): boolean {\n' - report += ' return /^[^@]+@[^@]+\\.\\w+$/.test(email)\n' - report += '}\n' - report += '```\n\n' - - report += '### Pattern: Return null/undefined\n\n' - report += '```typescript\n' - report += '// āŒ Stub\n' - report += 'export function fetchUserData(id: string) {\n' - report += ' return null // TODO: implement API call\n' - report += '}\n\n' - report += '// āœ… Real implementation\n' - report += 'export async function fetchUserData(id: string): Promise {\n' - report += ' const response = await fetch(`/api/users/${id}`)\n' - report += ' return response.json()\n' - report += '}\n' - report += '```\n\n' - - report += '### Pattern: Placeholder Component\n\n' - report += '```typescript\n' - report += '// āŒ Stub\n' - report += 'export function Dashboard() {\n' - report += ' return
TODO: Build dashboard
\n' - report += '}\n\n' - report += '// āœ… Real implementation\n' - report += 'export function Dashboard() {\n' - report += ' return (\n' - report += '
\n' - report += '
\n' - report += ' \n' - report += ' \n' - report += '
\n' - report += ' )\n' - report += '}\n' - report += '```\n\n' - - report += '### Pattern: Mock Data\n\n' - report += '```typescript\n' - report += '// āŒ Stub\n' - report += 'export function getUsers() {\n' - report += ' return [ // stub data\n' - report += ' { id: 1, name: "John" },\n' - report += ' { id: 2, name: "Jane" }\n' - report += ' ]\n' - report += '}\n\n' - report += '// āœ… Real implementation\n' - report += 'export async function getUsers(): Promise {\n' - report += ' const response = await Database.query("SELECT * FROM users")\n' - report += ' return response.map(row => new User(row))\n' - report += '}\n' - report += '```\n\n' - - report += '## Checklist for Implementation\n\n' - report += '- [ ] All critical stubs have been implemented\n' - report += '- [ ] Functions have proper type signatures\n' - report += '- [ ] Components render actual content (not placeholders)\n' - report += '- [ ] All TODO/FIXME comments reference GitHub issues\n' - report += '- [ ] Mock data is replaced with real data sources\n' - report += '- [ ] Error handling is in place\n' - report += '- [ ] Functions are tested with realistic inputs\n' - report += '- [ ] Documentation comments are added (JSDoc)\n\n' - - report += '## Best Practices\n\n' - report += '1. **Never commit stubs to main** - Use feature branches and require reviews\n' - report += '2. **Use TypeScript types** - Force implementations by using specific return types\n' - report += '3. **Convert stubs to issues** - Don\'t use TODO in code, create GitHub issues\n' - report += '4. **Test from the start** - Write tests before implementing\n' - report += '5. **Use linting rules** - Configure ESLint to catch console.log and TODO\n\n' - - report += `---\n\n` - report += `**Generated**: ${new Date().toISOString()}\n` - - return report +function main() { + const report = generateStubReport() + console.log(report) } -console.log(generateStubReport()) +main() diff --git a/tools/generation/stub-report/completeness-section.ts b/tools/generation/stub-report/completeness-section.ts new file mode 100644 index 000000000..047b12691 --- /dev/null +++ b/tools/generation/stub-report/completeness-section.ts @@ -0,0 +1,43 @@ +import { existsSync, readFileSync } from 'fs' + +export const buildCompletenessSection = (): string => { + if (!existsSync('implementation-analysis.json')) return '' + + try { + const completeness = JSON.parse(readFileSync('implementation-analysis.json', 'utf8')) + let section = '## Implementation Completeness Analysis\n\n' + section += `**Average Completeness Score**: ${completeness.averageCompleteness}%\n\n` + + section += '### Completeness Levels\n\n' + section += `- **Critical** (0% complete): ${completeness.bySeverity.critical}\n` + section += `- **High** (10-30% complete): ${completeness.bySeverity.high}\n` + section += `- **Medium** (40-70% complete): ${completeness.bySeverity.medium}\n` + section += `- **Low** (80-99% complete): ${completeness.bySeverity.low}\n\n` + + if (Object.values(completeness.flagTypes).some((v: number) => v > 0)) { + section += '### Common Stub Indicators\n\n' + for (const [flag, count] of Object.entries(completeness.flagTypes)) { + if (count > 0) { + section += `- **${flag}**: ${count} instances\n` + } + } + section += '\n' + } + + if (completeness.criticalStubs && completeness.criticalStubs.length > 0) { + section += '### šŸ”“ Incomplete Implementations (0% Completeness)\n\n' + section += '
Click to expand\n\n' + completeness.criticalStubs.forEach((stub: any) => { + section += `#### \`${stub.name}\` in \`${stub.file}:${stub.line}\`\n` + section += `**Type**: ${stub.type}\n` + section += `**Flags**: ${stub.flags.join(', ')}\n` + section += `**Summary**: ${stub.summary}\n\n` + }) + section += '
\n\n' + } + + return section + } catch { + return 'āš ļø Could not parse completeness analysis.\n\n' + } +} diff --git a/tools/generation/stub-report/guidance-sections.ts b/tools/generation/stub-report/guidance-sections.ts new file mode 100644 index 000000000..5a2cdff5f --- /dev/null +++ b/tools/generation/stub-report/guidance-sections.ts @@ -0,0 +1,101 @@ +const recommendations = () => { + let report = '## How to Fix Stub Implementations\n\n' + + report += '### Pattern: "Not Implemented" Errors\n\n' + report += '```typescript\n' + report += '// āŒ Stub\n' + report += 'export function processData(data) {\n' + report += ' throw new Error("not implemented")\n' + report += '}\n\n' + report += '// āœ… Real implementation\n' + report += 'export function processData(data) {\n' + report += ' return data.map(item => transform(item))\n' + report += '}\n' + report += '```\n\n' + + report += '### Pattern: Console.log Only\n\n' + report += '```typescript\n' + report += '// āŒ Stub\n' + report += 'export function validateEmail(email) {\n' + report += ' console.log("validating:", email)\n' + report += '}\n\n' + report += '// āœ… Real implementation\n' + report += 'export function validateEmail(email: string): boolean {\n' + report += ' return /^[^@]+@[^@]+\\.\\w+$/.test(email)\n' + report += '}\n' + report += '```\n\n' + + report += '### Pattern: Return null/undefined\n\n' + report += '```typescript\n' + report += '// āŒ Stub\n' + report += 'export function fetchUserData(id: string) {\n' + report += ' return null // TODO: implement API call\n' + report += '}\n\n' + report += '// āœ… Real implementation\n' + report += 'export async function fetchUserData(id: string): Promise {\n' + report += ' const response = await fetch(`/api/users/${id}`)\n' + report += ' return response.json()\n' + report += '}\n' + report += '```\n\n' + + report += '### Pattern: Placeholder Component\n\n' + report += '```typescript\n' + report += '// āŒ Stub\n' + report += 'export function Dashboard() {\n' + report += ' return
TODO: Build dashboard
\n' + report += '}\n\n' + report += '// āœ… Real implementation\n' + report += 'export function Dashboard() {\n' + report += ' return (\n' + report += '
\n' + report += '
\n' + report += ' \n' + report += ' \n' + report += '
\n' + report += ' )\n' + report += '}\n' + report += '```\n\n' + + report += '### Pattern: Mock Data\n\n' + report += '```typescript\n' + report += '// āŒ Stub\n' + report += 'export function getUsers() {\n' + report += ' return [ // stub data\n' + report += ' { id: 1, name: "John" },\n' + report += ' { id: 2, name: "Jane" }\n' + report += ' ]\n' + report += '}\n\n' + report += '// āœ… Real implementation\n' + report += 'export async function getUsers(): Promise {\n' + report += ' const response = await Database.query("SELECT * FROM users")\n' + report += ' return response.map(row => new User(row))\n' + report += '}\n' + report += '```\n\n' + + return report +} + +const checklist = () => { + let report = '## Checklist for Implementation\n\n' + report += '- [ ] All critical stubs have been implemented\n' + report += '- [ ] Functions have proper type signatures\n' + report += '- [ ] Components render actual content (not placeholders)\n' + report += '- [ ] All TODO/FIXME comments reference GitHub issues\n' + report += '- [ ] Mock data is replaced with real data sources\n' + report += '- [ ] Error handling is in place\n' + report += '- [ ] Functions are tested with realistic inputs\n' + report += '- [ ] Documentation comments are added (JSDoc)\n\n' + return report +} + +const bestPractices = () => { + let report = '## Best Practices\n\n' + report += '1. **Never commit stubs to main** - Use feature branches and require reviews\n' + report += '2. **Use TypeScript types** - Force implementations by using specific return types\n' + report += "3. **Convert stubs to issues** - Don't use TODO in code, create GitHub issues\n" + report += '4. **Test from the start** - Write tests before implementing\n' + report += '5. **Use linting rules** - Configure ESLint to catch console.log and TODO\n\n' + return report +} + +export const buildGuidanceSections = (): string => recommendations() + checklist() + bestPractices() diff --git a/tools/generation/stub-report/pattern-section.ts b/tools/generation/stub-report/pattern-section.ts new file mode 100644 index 000000000..af9819b5e --- /dev/null +++ b/tools/generation/stub-report/pattern-section.ts @@ -0,0 +1,50 @@ +import { existsSync, readFileSync } from 'fs' + +export const buildPatternSection = (): string => { + if (!existsSync('stub-patterns.json')) return '' + + try { + const patterns = JSON.parse(readFileSync('stub-patterns.json', 'utf8')) + let section = '## Pattern-Based Detection Results\n\n' + section += `**Total Stubs Found**: ${patterns.totalStubsFound}\n\n` + section += '### By Severity\n\n' + section += `- šŸ”“ **Critical**: ${patterns.bySeverity.high} (blocks production)\n` + section += `- 🟠 **Medium**: ${patterns.bySeverity.medium} (should be fixed)\n` + section += `- 🟔 **Low**: ${patterns.bySeverity.low} (nice to fix)\n\n` + + section += '### By Type\n\n' + for (const [type, count] of Object.entries(patterns.byType)) { + if (count > 0) { + section += `- **${type}**: ${count}\n` + } + } + section += '\n' + + if (patterns.criticalIssues && patterns.criticalIssues.length > 0) { + section += '### šŸ”“ Critical Stubs\n\n' + section += 'These must be implemented before production:\n\n' + section += '| File | Line | Function | Type |\n' + section += '|------|------|----------|------|\n' + patterns.criticalIssues.slice(0, 20).forEach(issue => { + section += `| \`${issue.file}\` | ${issue.line} | \`${issue.function}\` | ${issue.type} |\n` + }) + section += '\n' + } + + if (patterns.details && patterns.details.length > 0) { + section += '### Detailed Findings\n\n' + section += '
Click to expand (showing first 15)\n\n' + section += '| File | Line | Function | Type | Code Preview |\n' + section += '|------|------|----------|------|---------------|\n' + patterns.details.slice(0, 15).forEach(item => { + const preview = item.code?.substring(0, 50)?.replace(/\n/g, ' ') || 'N/A' + section += `| ${item.file} | ${item.line} | ${item.name} | ${item.type} | \`${preview}...\` |\n` + }) + section += '\n
\n\n' + } + + return section + } catch { + return 'āš ļø Could not parse pattern detection results.\n\n' + } +} diff --git a/tools/generation/stub-report/report-builder.ts b/tools/generation/stub-report/report-builder.ts new file mode 100644 index 000000000..5687a0b6e --- /dev/null +++ b/tools/generation/stub-report/report-builder.ts @@ -0,0 +1,18 @@ +import { buildPatternSection } from './pattern-section' +import { buildCompletenessSection } from './completeness-section' +import { buildGuidanceSections } from './guidance-sections' + +export const generateStubReport = (): string => { + let report = '# Stub Implementation Detection Report\n\n' + report += '## Overview\n\n' + report += 'This report identifies incomplete, placeholder, or stubbed implementations in the codebase.\n' + report += 'Stubs should be replaced with real implementations before production use.\n\n' + + report += buildPatternSection() + report += buildCompletenessSection() + report += buildGuidanceSections() + + report += `---\n\n` + report += `**Generated**: ${new Date().toISOString()}\n` + return report +}