UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

851 lines (850 loc) 32.9 kB
/** * MCP Handlers — Guard 审计 & 项目扫描 * * 统一入口:autosnippet_guard * 无参数 → review 模式(自动 git diff 增量文件 + inline recipe) * files: string[] → 指定文件检查(+ inline recipe) * code: string → 单文件内联检查 */ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { LanguageService } from '#shared/LanguageService.js'; import { resolveProjectRoot } from '#shared/resolveProjectRoot.js'; import { envelope } from '../envelope.js'; // ═══ Review 轮次追踪(模块私有) ═══════════════════ const _reviewRounds = new Map(); // projectRoot → round count const _lastReviewPassed = new Map(); // projectRoot → boolean const MAX_REVIEW_ROUNDS = 5; export async function guardCheck(ctx, args) { const { GuardCheckEngine, detectLanguage } = await import('#service/guard/GuardCheckEngine.js'); // 输入校验:空代码直接返回 if (!args.code || !args.code.trim()) { return envelope({ success: true, data: { language: args.language || 'unknown', violations: [], summary: { total: 0, errors: 0, warnings: 0 }, }, meta: { tool: 'autosnippet_guard', note: 'Empty code — skipped' }, }); } const engine = _getOrCreateEngine(ctx, GuardCheckEngine); // 注入 Enhancement Pack Guard 规则 await _injectEnhancementGuardRules(engine, ctx); const language = args.language || detectLanguage(args.filePath || ''); const violations = engine.checkCode(args.code, language); // ── SkillHooks: onGuardCheck — 允许 hooks 修改 violations ── try { const skillHooks = ctx.container.get('skillHooks'); if (skillHooks.has('onGuardCheck')) { for (let i = 0; i < violations.length; i++) { const modified = await skillHooks.run('onGuardCheck', violations[i], { language }); if (modified && typeof modified === 'object') { violations[i] = modified; } } } } catch { /* skillHooks not available */ } const warnings = []; if (language === 'unknown') { warnings.push('未能识别语言,部分语言相关规则可能未执行。建议提供 language 或 filePath 参数。'); } return envelope({ success: true, data: { language, violations, summary: { total: violations.length, errors: violations.filter((v) => v.severity === 'error').length, warnings: violations.filter((v) => v.severity === 'warning').length, }, ...(warnings.length ? { warnings } : {}), }, meta: { tool: 'autosnippet_guard' }, }); } export async function guardAuditFiles(ctx, args) { if (!Array.isArray(args.files) || args.files.length === 0) { throw new Error('files array is required and must not be empty'); } const scope = args.scope || 'project'; const { GuardCheckEngine } = await import('#service/guard/GuardCheckEngine.js'); const engine = _getOrCreateEngine(ctx, GuardCheckEngine); // 注入 Enhancement Pack Guard 规则 await _injectEnhancementGuardRules(engine, ctx); // 解析项目根路径(用于相对路径转绝对路径) const projectRoot = resolveProjectRoot(ctx.container); // 补充缺失的 content(从磁盘读取) // 相对路径自动转绝对路径,避免 MCP 进程 cwd 不在项目目录时读不到文件 const filesToAudit = await Promise.all(args.files.map(async (f) => { const absPath = path.isAbsolute(f.path) ? f.path : path.resolve(projectRoot, f.path); let content = f.content; if (!content) { try { content = await readFile(absPath, 'utf8'); } catch { content = ''; } } return { path: absPath, content, isTest: LanguageService.isTestFile(absPath) }; })); const result = engine.auditFiles(filesToAudit, { scope }); // 写入 ViolationsStore + GuardFeedbackLoop try { const violationsStore = ctx.container.get('violationsStore'); for (const fileResult of result.files || []) { if (fileResult.violations.length > 0) { violationsStore.appendRun({ filePath: fileResult.filePath, violations: fileResult.violations, summary: `MCP audit (${scope}): ${fileResult.summary.errors}E ${fileResult.summary.warnings}W`, }); } // Guard ↔ Recipe 闭环:检测修复并自动确认使用 try { const feedbackLoop = ctx.container.get('guardFeedbackLoop'); feedbackLoop.processFixDetection(fileResult, fileResult.filePath); } catch { /* guardFeedbackLoop not available */ } } } catch { /* ViolationsStore not available */ } return envelope({ success: true, data: { summary: result.summary, files: result.files.map((f) => ({ filePath: f.filePath, language: f.language, violations: f.violations, summary: f.summary, })), ...(result.crossFileViolations?.length ? { crossFileViolations: result.crossFileViolations } : {}), // uncertain 消费链路 — 结构化上抛给 Agent ...(result.capabilityReport ? { capabilityReport: result.capabilityReport, uncertainSummary: { total: result.capabilityReport.uncertainResults.length, byLayer: _groupBy(result.capabilityReport.uncertainResults, 'layer'), byReason: _groupBy(result.capabilityReport.uncertainResults, 'reason'), }, boundaries: result.capabilityReport.boundaries, } : {}), }, meta: { tool: 'autosnippet_guard' }, }); } // ═══ Review 模式 — 编码后质量门禁(无参数 = 自动检测) ═══ /** * Guard Review — 编码后的代码质量检查 * * 设计要点: * 1. 无参数 → 自动从 git diff 检测增量文件(staged + unstaged + untracked) * 2. files: string[] → 指定文件路径(简化,不再要求对象数组) * 3. violations 内联 recipe 修复指南(doClause + coreCode) * 4. 防无限循环:reviewRound 计数 + MAX_REVIEW_ROUNDS 限制 * 5. 不绑定 task ID — 代码检查独立于任务系统 * * @param ctx MCP context with container * @param args { files?: string[] } */ export async function guardReview(ctx, args) { const { GuardCheckEngine } = await import('#service/guard/GuardCheckEngine.js'); const projectRoot = resolveProjectRoot(ctx.container); // 轮次追踪(基于 projectRoot,不绑定 task) const round = (_reviewRounds.get(projectRoot) || 0) + 1; _reviewRounds.set(projectRoot, round); if (round > MAX_REVIEW_ROUNDS) { _reviewRounds.delete(projectRoot); _lastReviewPassed.set(projectRoot, true); // 强制通过 return envelope({ success: true, data: { passed: true, files: [], totalViolations: 0, reviewRound: round, maxRoundsReached: true, }, message: `⚠️ Guard review round ${round} exceeds max ${MAX_REVIEW_ROUNDS}. Force-passing. Remaining issues should be tracked as follow-up.`, meta: { tool: 'autosnippet_guard', mode: 'review' }, }); } // 1. 确定待检查文件 let filePaths = []; let fileSource = 'git-diff'; if (args.files && Array.isArray(args.files) && args.files.length > 0) { // files 参数: string[] — 简化版,自动读取文件内容 filePaths = args.files .map((f) => typeof f === 'string' ? f : f.path || String(f)) .map((f) => (path.isAbsolute(f) ? f : path.resolve(projectRoot, f))) .filter((f) => fs.existsSync(f)); fileSource = 'explicit'; } else { // 无参数 → 自动检测 git 变更文件 filePaths = _detectChangedFiles(projectRoot); } if (!filePaths.length) { _reviewRounds.delete(projectRoot); _lastReviewPassed.set(projectRoot, true); return envelope({ success: true, data: { passed: true, files: [], totalViolations: 0, reviewRound: round, fileSource }, message: '✅ No changed source files detected. Guard review passed.', meta: { tool: 'autosnippet_guard', mode: 'review' }, }); } // 2. 预加载 rule recipe 缓存 const recipeMap = await _loadRuleRecipes(ctx); // 3. 创建引擎,注入 Enhancement Pack const engine = _getOrCreateEngine(ctx, GuardCheckEngine); await _injectEnhancementGuardRules(engine, ctx); // 4. 逐文件检查(使用 auditFile 以捕获 uncertain) const results = []; let totalViolations = 0; let totalErrors = 0; let totalWarnings = 0; const allUncertainResults = []; for (const fp of filePaths) { try { const code = await readFile(fp, 'utf8'); const auditResult = engine.auditFile(fp, code, { isTest: LanguageService.isTestFile(fp) }); const violations = auditResult.violations; // 收集 uncertain if (auditResult.uncertainResults?.length) { allUncertainResults.push(...auditResult.uncertainResults); } const fileSummary = { total: violations.length, errors: violations.filter((v) => v.severity === 'error').length, warnings: violations.filter((v) => v.severity === 'warning').length, }; totalViolations += violations.length; totalErrors += fileSummary.errors; totalWarnings += fileSummary.warnings; // 内联 recipe 修复指南 const enriched = violations.map((v) => { const base = { ruleId: v.ruleId, message: v.message, severity: v.severity, line: v.line, snippet: v.snippet, fixSuggestion: v.fixSuggestion || null, }; const recipe = recipeMap.get(v.ruleId); if (recipe) { base.recipe = { title: recipe.title, doClause: recipe.doClause || null, dontClause: recipe.dontClause || null, coreCode: recipe.coreCode || null, }; } return base; }); results.push({ filePath: fp, language: auditResult.language, violations: enriched, summary: fileSummary, }); } catch (err) { results.push({ filePath: fp, error: `Cannot read: ${err instanceof Error ? err.message : String(err)}`, violations: [], summary: { total: 0, errors: 0, warnings: 0 }, }); } } const passed = totalViolations === 0; // 5. 更新共享状态 if (passed) { _reviewRounds.delete(projectRoot); _lastReviewPassed.set(projectRoot, true); } else { _lastReviewPassed.set(projectRoot, false); } // 6. 写入 ViolationsStore try { const violationsStore = ctx.container.get('violationsStore'); for (const r of results) { if (r.violations.length > 0) { violationsStore.appendRun({ filePath: r.filePath, violations: r.violations, summary: `guard review round ${round}: ${r.summary.errors}E ${r.summary.warnings}W`, }); } } } catch { /* optional */ } // 7. 构造消息 let message; if (passed) { message = `✅ Guard review passed (round ${round}). ${filePaths.length} file(s) checked, 0 violations.`; } else { const violatingFiles = results.filter((r) => r.violations.length > 0); const details = violatingFiles .map((f) => ` ${path.basename(f.filePath)}: ${f.violations.map((v) => `L${v.line} ${v.ruleId}`).join(', ')}`) .join('\n'); message = [ `⚠️ Guard review round ${round}: ${totalViolations} violation(s) in ${violatingFiles.length} file(s).`, details, '', 'Each violation includes inline `recipe` with doClause + coreCode — apply fixes directly.', round >= MAX_REVIEW_ROUNDS - 1 ? `⚠️ Next round is the last (max ${MAX_REVIEW_ROUNDS}). Unresolved issues will be force-passed.` : `Fix and call autosnippet_guard again (round ${round + 1}).`, ].join('\n'); } return envelope({ success: true, data: { passed, reviewRound: round, fileSource, files: results, totalViolations, summary: { total: totalViolations, errors: totalErrors, warnings: totalWarnings, filesChecked: filePaths.length, }, // uncertain 消费链路 — 结构化上抛给 Agent ...(allUncertainResults.length > 0 ? { uncertainSummary: { total: allUncertainResults.length, byLayer: _groupBy(allUncertainResults, 'layer'), byReason: _groupBy(allUncertainResults, 'reason'), }, uncertainResults: allUncertainResults, } : {}), }, message, meta: { tool: 'autosnippet_guard', mode: 'review' }, }); } // ═══ Recipe 缓存 ═════════════════════════════════════════ /** * 预加载所有 rule 类型 recipe 的修复字段 * 构建 guardId → recipe 映射 */ async function _loadRuleRecipes(ctx) { const map = new Map(); try { const knowledgeRepo = ctx.container.get('knowledgeRepository'); const entries = await knowledgeRepo.findActiveGuardRecipes(); for (const row of entries) { try { const constraints = typeof row.constraints === 'object' && row.constraints ? row.constraints : JSON.parse(row.constraints || '{}'); const guards = (constraints.guards || []); for (const g of guards) { if (g.id) { map.set(g.id, { title: row.title, doClause: row.doClause, dontClause: row.dontClause, coreCode: row.coreCode, }); } } } catch { /* skip */ } map.set(row.id, { title: row.title, doClause: row.doClause, dontClause: row.dontClause, coreCode: row.coreCode, }); } } catch { /* DB not available */ } return map; } // ═══ Git Diff 检测 ═══════════════════════════════════════ const SOURCE_EXTS = new Set([ '.m', '.mm', '.h', '.swift', '.js', '.ts', '.jsx', '.tsx', '.py', '.rb', '.java', '.kt', '.go', '.rs', '.c', '.cpp', '.cc', '.cs', '.vue', '.svelte', ]); function _detectChangedFiles(projectRoot) { const root = projectRoot; try { const diffOutput = execSync('git diff --name-only HEAD 2>/dev/null; git diff --staged --name-only 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null', { cwd: root, encoding: 'utf8', timeout: 5000 }); const files = [ ...new Set(diffOutput .split('\n') .map((f) => f.trim()) .filter((f) => f && SOURCE_EXTS.has(path.extname(f).toLowerCase()))), ]; return files .map((f) => (path.isAbsolute(f) ? f : path.resolve(root, f))) .filter((f) => fs.existsSync(f)); } catch { return []; } } // ═══ 项目扫描 ════════════════════════════════════════════ export async function scanProject(ctx, args) { const maxFiles = args.maxFiles || 200; const includeContent = args.includeContent || false; const contentMaxLines = args.contentMaxLines || 100; const projectRoot = resolveProjectRoot(ctx.container); // 使用 ModuleService(多语言统一入口) let service; try { const { ModuleService } = await import('#service/module/ModuleService.js'); service = new ModuleService(projectRoot); } catch { return envelope({ success: false, data: { targets: [], files: [], guardAudit: null, message: 'ModuleService not available' }, meta: { tool: 'autosnippet_bootstrap' }, }); } await service.load(); const allTargets = await service.listTargets(); if (!allTargets || allTargets.length === 0) { return envelope({ success: true, data: { targets: [], files: [], guardAudit: null, message: 'No module targets found' }, meta: { tool: 'autosnippet_bootstrap' }, }); } // 收集所有文件(去重) const seenPaths = new Set(); const allFiles = []; for (const t of allTargets) { try { const fileList = await service.getTargetFiles(t); for (const f of fileList) { const fp = typeof f === 'string' ? f : f.path; if (seenPaths.has(fp)) { continue; } seenPaths.add(fp); const entry = { name: f.name || path.basename(fp), path: fp, relativePath: f.relativePath || path.basename(fp), targetName: t.name, }; if (includeContent) { try { const raw = await readFile(fp, 'utf8'); const lines = raw.split('\n'); entry.content = lines.slice(0, contentMaxLines).join('\n'); entry.totalLines = lines.length; entry.truncated = lines.length > contentMaxLines; } catch { entry.content = ''; entry.totalLines = 0; } } allFiles.push(entry); if (allFiles.length >= maxFiles) { break; } } } catch { /* skip target */ } if (allFiles.length >= maxFiles) { break; } } // Guard 审计 let guardAudit = null; try { const { GuardCheckEngine } = await import('#service/guard/GuardCheckEngine.js'); const engine = _getOrCreateEngine(ctx, GuardCheckEngine); // 注入 Enhancement Pack Guard 规则 await _injectEnhancementGuardRules(engine, ctx); const filesToAudit = await Promise.all(allFiles.map(async (f) => { let content = f.content; if (!content) { try { content = await readFile(f.path, 'utf8'); } catch { content = ''; } } return { path: f.path, content, isTest: LanguageService.isTestFile(f.path) }; })); guardAudit = engine.auditFiles(filesToAudit, { scope: 'project' }); // 写入 ViolationsStore try { const violationsStore = ctx.container.get('violationsStore'); for (const fileResult of guardAudit.files || []) { if (fileResult.violations.length > 0) { violationsStore.appendRun({ filePath: fileResult.filePath, violations: fileResult.violations, summary: `MCP project scan: ${fileResult.summary.errors}E ${fileResult.summary.warnings}W`, }); } } } catch { /* store not available */ } } catch (e) { const logger = ctx.logger; logger?.warn?.(`[MCP] Guard audit in scanProject failed: ${e instanceof Error ? e.message : String(e)}`); } // 构建文件列表摘要 const fileSummary = allFiles.map((f) => { const base = { name: f.name, path: f.relativePath, targetName: f.targetName }; if (includeContent) { base.content = f.content; base.totalLines = f.totalLines; base.truncated = f.truncated; } return base; }); return envelope({ success: true, data: { targets: allTargets.map((t) => ({ name: t.name, type: t.type, packageName: t.packageName, })), files: fileSummary, fileCount: allFiles.length, guardAudit: guardAudit ? { summary: guardAudit.summary, filesWithViolations: (guardAudit.files || []) .filter((f) => f.violations.length > 0) .map((f) => ({ filePath: f.filePath, language: f.language, violations: f.violations, summary: f.summary, })), ...(guardAudit.crossFileViolations?.length ? { crossFileViolations: guardAudit.crossFileViolations } : {}), } : null, }, meta: { tool: 'autosnippet_bootstrap' }, }); } // ─── 内部辅助 ───────────────────────────────────────────── /** 按字段值分组计数 */ function _groupBy(arr, key) { const counts = {}; for (const item of arr) { const k = String(item[key] ?? 'unknown'); counts[k] = (counts[k] || 0) + 1; } return counts; } /** * 获取 DI 容器中的 GuardCheckEngine 单例,回退到新建实例 * 优先复用 DI 单例以保持 externalRules / cache 的跨调用一致性 * @param ctx MCP context with container * @param GuardCheckEngine 引擎构造函数(用于回退) */ function _getOrCreateEngine(ctx, GuardCheckEngineCtor) { try { const engine = ctx.container.get('guardCheckEngine'); if (engine) { return engine; } } catch { /* DI not registered — fall back to new instance */ } const db = ctx.container.get('database'); return new GuardCheckEngineCtor(db); } /** * 将 Enhancement Pack 的 Guard 规则注入 GuardCheckEngine * 幂等 — 已注入的引擎直接跳过,避免每次请求重复加载 EnhancementRegistry * 静默失败 — Enhancement Pack 不可用不应阻断 Guard 审计 */ async function _injectEnhancementGuardRules(engine, ctx) { // 幂等保护: 已注入则跳过 if (engine.isEpInjected?.()) { return; } try { const { initEnhancementRegistry } = await import('#core/enhancement/index.js'); const enhReg = await initEnhancementRegistry(); // 仅注入无框架条件的通用 Pack 规则(如 go-web 无 frameworks 条件) // 有框架条件的 Pack(如 go-grpc 需要 frameworks: ['grpc'])由 Bootstrap Phase 4 // 通过 resolve(lang, detectedFrameworks) 精确注入,避免非 gRPC 项目出现误报 const allPacks = enhReg.all().filter((pack) => { const cond = pack.conditions; return !cond?.frameworks?.length; }); const allGuardRules = []; for (const pack of allPacks) { try { const rules = pack.getGuardRules(); if (rules.length > 0) { allGuardRules.push(...rules); } } catch { /* graceful degradation per pack */ } } if (allGuardRules.length > 0) { engine.injectExternalRules(allGuardRules); } engine.markEpInjected?.(); } catch { /* Enhancement registry not available — non-critical */ } } /** * 对所有 active rule Recipe 执行反向验证: * - 检查 coreCode 引用的符号是否还存在 * - 检查 guard pattern 匹配率是否骤降 */ export async function guardReverseAudit(ctx, args) { const { ReverseGuard } = await import('#service/guard/ReverseGuard.js'); const { collectSourceFilesWithContent } = await import('#service/guard/SourceFileCollector.js'); const projectRoot = resolveProjectRoot(ctx.container); // 尝试从 DI 获取,回退到新建 let reverseGuard; try { reverseGuard = ctx.container.get('reverseGuard'); } catch { reverseGuard = new ReverseGuard(ctx.container.get('knowledgeRepository'), ctx.container.get('codeEntityRepository'), ctx.container.get('recipeSourceRefRepository')); } const maxFiles = args.maxFiles || 200; const projectFiles = await collectSourceFilesWithContent(projectRoot, { maxFiles }); const results = reverseGuard.auditAllRules(projectFiles); const drifts = reverseGuard.getDriftResults(results); return envelope({ success: true, data: { totalRecipes: results.length, healthy: results.filter((r) => r.recommendation === 'healthy').length, investigate: results.filter((r) => r.recommendation === 'investigate').length, decay: results.filter((r) => r.recommendation === 'decay').length, drifts: drifts.map((d) => ({ recipeId: d.recipeId, title: d.title, recommendation: d.recommendation, signals: d.signals, })), allResults: results.map((r) => ({ recipeId: r.recipeId, title: r.title, recommendation: r.recommendation, signalCount: r.signals.length, })), }, meta: { tool: 'autosnippet_guard', operation: 'reverse_audit' }, }); } /** * 计算模块级 Guard 规则覆盖率矩阵 */ export async function guardCoverageMatrix(ctx, _args) { const { CoverageAnalyzer } = await import('#service/guard/CoverageAnalyzer.js'); const projectRoot = resolveProjectRoot(ctx.container); // 尝试从 DI 获取,回退到新建 let analyzer; try { analyzer = ctx.container.get('coverageAnalyzer'); } catch { analyzer = new CoverageAnalyzer(ctx.container.get('knowledgeRepository'), ctx.container.get('guardViolationRepository')); } // 构建 moduleFiles 映射 — 从 Panorama 或目录结构推断 const moduleFiles = await _buildModuleFiles(ctx, projectRoot); const matrix = analyzer.analyze(moduleFiles); return envelope({ success: true, data: { overallCoverage: matrix.overallCoverage, zeroModules: matrix.zeroModules, lowModules: matrix.lowModules, modules: matrix.modules, }, meta: { tool: 'autosnippet_guard', operation: 'coverage_matrix' }, }); } /** * 生成 3D 合规报告(compliance + coverage + confidence) * 包含完整 uncertain 消费数据 */ export async function guardComplianceReport(ctx, _args) { const { ComplianceReporter } = await import('#service/guard/ComplianceReporter.js'); const projectRoot = resolveProjectRoot(ctx.container); // 尝试从 DI 获取,回退到新建 let reporter; try { reporter = ctx.container.get('complianceReporter'); } catch { const { GuardCheckEngine } = await import('#service/guard/GuardCheckEngine.js'); const engine = _getOrCreateEngine(ctx, GuardCheckEngine); await _injectEnhancementGuardRules(engine, ctx); // ComplianceReporter(engine, violationsStore, ruleLearner, exclusionManager, config) let violationsStore = null; let ruleLearner = null; let exclusionManager = null; try { violationsStore = ctx.container.get('violationsStore'); } catch { /* optional */ } try { ruleLearner = ctx.container.get('ruleLearner'); } catch { /* optional */ } try { exclusionManager = ctx.container.get('exclusionManager'); } catch { /* optional */ } reporter = new ComplianceReporter(engine, violationsStore, ruleLearner, exclusionManager); } const report = await reporter.generate(projectRoot); return envelope({ success: true, data: { scores: { compliance: report.complianceScore, coverage: report.coverageScore, confidence: report.confidenceScore, }, qualityGate: report.qualityGate, summary: report.summary, uncertainSummary: report.uncertainSummary || null, boundaries: report.boundaries || [], topViolations: (report.topViolations || []).slice(0, 10), trend: report.trend || null, }, meta: { tool: 'autosnippet_guard', operation: 'compliance_report' }, }); } /** 从 Panorama 或目录结构构建模块→文件映射 */ async function _buildModuleFiles(ctx, projectRoot) { const moduleFiles = new Map(); try { const panorama = ctx.container.get('panoramaService'); const result = await panorama.getResult(); if (result?.modules) { for (const [name, mod] of result.modules) { if (mod.files?.length > 0) { moduleFiles.set(name, mod.files); } } } } catch { /* PanoramaService not available */ } if (moduleFiles.size === 0) { const { readdirSync, existsSync } = await import('node:fs'); const srcDirs = ['Sources', 'BiliDili/Modules', 'src', 'lib']; for (const dir of srcDirs) { const fullDir = path.join(projectRoot, dir); if (existsSync(fullDir)) { for (const entry of readdirSync(fullDir, { withFileTypes: true })) { if (entry.isDirectory() && !entry.name.startsWith('.')) { const files = _walkSourceFiles(path.join(fullDir, entry.name)); if (files.length > 0) { moduleFiles.set(entry.name, files); } } } } } } return moduleFiles; } function _walkSourceFiles(dir) { const files = []; try { const { readdirSync } = require('node:fs'); const walk = (d) => { for (const e of readdirSync(d, { withFileTypes: true })) { const fp = path.join(d, e.name); if (e.isDirectory() && !e.name.startsWith('.')) { walk(fp); } else if (e.isFile() && /\.(m|h|swift|mm|ts|js|py|java|kt|dart|rs|go|cs|rb)$/.test(e.name)) { files.push(fp); } } }; walk(dir); } catch { /* directory read error */ } return files; }