UNPKG

autosnippet

Version:

Extract code patterns into a knowledge base for AI coding assistants

414 lines (413 loc) 22 kB
/** * mock-pipeline.ts — Mock AI Bootstrap 轻量管线 * * 当 AI Provider 为 mock 时,利用 Phase 1-4 已收集的真实数据 * (AST、依赖图、文件列表、Panorama) 为每个维度生成模板化候选知识。 * * 不调用 AI,但走完完整的 submit → dimension_complete 流程, * 使 Dashboard 能正常展示知识库、健康雷达等 UI。 * * @module pipeline/mock-pipeline */ import path from 'node:path'; import Logger from '#infra/logging/Logger.js'; import { BootstrapEventEmitter } from '#service/bootstrap/BootstrapEventEmitter.js'; const logger = Logger.getInstance(); // ── 模板生成器 ──────────────────────────────────────────── /** 从 AST 指标提取代码统计 */ function extractAstStats(ast) { if (!ast) { return { classes: 0, protocols: 0, functions: 0 }; } const pm = (ast.projectMetrics ?? ast); return { classes: pm.totalClasses ?? 0, protocols: pm.totalProtocols ?? 0, functions: pm.totalFunctions ?? 0, }; } /** 从文件列表中按 target 分组并提取代表性文件 */ function getRepresentativeFiles(files, targetFileMap) { const result = new Map(); if (targetFileMap) { for (const [target, paths] of Object.entries(targetFileMap)) { result.set(target, paths.slice(0, 5)); } } else if (files) { const grouped = new Map(); for (const f of files) { const target = f.targetName || 'default'; const list = grouped.get(target) || []; list.push(f.relativePath || f.name); grouped.set(target, list); } for (const [target, paths] of grouped) { result.set(target, paths.slice(0, 5)); } } return result; } /** 从文件内容中提取 import 语句(作为 headers)*/ function extractImports(content, lang) { const imports = []; const lines = content.split('\n').slice(0, 30); for (const line of lines) { const trimmed = line.trim(); if (lang === 'swift' && trimmed.startsWith('import ')) { imports.push(trimmed); } else if ((lang === 'typescript' || lang === 'javascript') && trimmed.startsWith('import ')) { imports.push(trimmed); } } return [...new Set(imports)].slice(0, 5); } /** 从文件内容中提取类/结构体/协议名 */ function extractTypes(content) { const types = []; for (const m of content.matchAll(/(?:class|struct|protocol|enum|interface|type)\s+(\w+)/g)) { types.push(m[1]); } return [...new Set(types)].slice(0, 10); } /** 生成 kebab-case trigger */ function toKebab(text) { return text .replace(/([a-z])([A-Z])/g, '$1-$2') .replace(/[\s_]+/g, '-') .toLowerCase() .replace(/[^a-z0-9-]/g, '') .slice(0, 40); } // ── 维度级候选生成 ──────────────────────────────────────── /** 为单个维度生成 Mock 候选 */ function generateDimensionCandidates(dimId, dimLabel, files, lang, projectName, astStats, targetFiles) { const candidates = []; // 从文件中收集有代表性的类型和源文件 const allTypes = []; const allSources = []; const allImports = []; const dimFiles = files?.filter((f) => { const p = f.relativePath?.toLowerCase() || f.name.toLowerCase(); // 根据维度 ID 做简单相关性匹配 switch (dimId) { case 'architecture': return (p.includes('coordinator') || p.includes('delegate') || p.includes('service') || p.includes('manager')); case 'code-pattern': return p.includes('extension') || p.includes('helper') || p.includes('util'); case 'naming-style': return true; // 全局适用 case 'error-handling': return p.includes('error') || p.includes('result') || p.includes('handler'); case 'networking': return (p.includes('network') || p.includes('api') || p.includes('request') || p.includes('http')); case 'data-model': return p.includes('model') || p.includes('entity') || p.includes('dto'); case 'testing-quality': return p.includes('test') || p.includes('spec') || p.includes('mock'); case 'ui-patterns': return (p.includes('view') || p.includes('controller') || p.includes('cell') || p.includes('component')); case 'concurrency': return (p.includes('actor') || p.includes('async') || p.includes('queue') || p.includes('dispatch')); case 'storage': return (p.includes('store') || p.includes('cache') || p.includes('database') || p.includes('persist')); default: return true; } }) || []; // 提取文件中的类型和导入 for (const f of dimFiles.slice(0, 10)) { allTypes.push(...extractTypes(f.content || '')); allSources.push(f.relativePath || f.name); allImports.push(...extractImports(f.content || '', lang)); } // 如果没找到相关文件,用全部文件兜底 if (allTypes.length === 0 && files) { for (const f of files.slice(0, 5)) { allTypes.push(...extractTypes(f.content || '')); allSources.push(f.relativePath || f.name); } } const uniqueTypes = [...new Set(allTypes)].slice(0, 8); const uniqueSources = [...new Set(allSources)].slice(0, 5); const uniqueImports = [...new Set(allImports)].slice(0, 5); // 为每个维度生成 3 个候选 const templates = getDimensionTemplates(dimId, dimLabel, uniqueTypes, uniqueSources, lang, projectName, astStats); for (const tpl of templates.slice(0, 3)) { candidates.push({ ...tpl, headers: uniqueImports, sourceRefs: uniqueSources.slice(0, 3), reasoning: { whyStandard: `基于 ${projectName} 项目 ${dimFiles.length} 个相关文件的代码分析(Mock 模式自动生成)`, sources: uniqueSources.slice(0, 3), confidence: 0.7, }, tags: ['mock-generated', dimId, lang], }); } return candidates; } /** 根据维度 ID 返回候选模板 */ function getDimensionTemplates(dimId, dimLabel, types, sources, lang, projectName, ast) { const typeList = types.length > 0 ? types.slice(0, 3).join('、') : '核心模块'; const typeFirst = types[0] || 'AppModule'; const srcFirst = sources[0] || 'Sources/Main.swift'; const base = { language: lang, knowledgeType: dimId, }; switch (dimId) { case 'architecture': return [ { ...base, title: `${projectName} 分层架构模式`, description: `项目采用分层架构,包含 ${ast.classes} 个类、${ast.protocols} 个协议`, trigger: `@${toKebab(projectName)}-layered-arch`, kind: 'pattern', category: 'Architecture', topicHint: 'architecture', content: { markdown: `### ${projectName} 分层架构\n\n项目基于分层架构设计,核心类型包括 ${typeList}。\n\n**层次结构**:\n- 表现层: View/Controller\n- 业务层: Service/Manager\n- 数据层: Repository/Store\n\n**来源**: \`${srcFirst}\`\n\n> ⚠️ 此条目由 Mock AI 基于代码结构自动生成`, rationale: `分层架构是 ${projectName} 的核心设计决策,确保模块间关注点分离`, }, coreCode: `// ${lang === 'swift' ? 'protocol' : 'interface'} ${typeFirst}Service { ... }`, doClause: `Follow the layered architecture with ${typeFirst} as the core service boundary`, dontClause: 'Do not bypass service layer to access data layer directly', whenClause: 'When adding new features or modules to the project', usageGuide: `### 使用指南\n\n遵循 ${projectName} 的分层架构进行模块设计。`, }, { ...base, title: `${typeFirst} 依赖注入约定`, description: `${typeFirst} 等核心类型通过依赖注入管理生命周期`, trigger: `@${toKebab(typeFirst)}-di-pattern`, kind: 'pattern', category: 'Architecture', topicHint: 'architecture', content: { markdown: `### 依赖注入约定\n\n${projectName} 使用依赖注入管理 ${typeList} 等组件的生命周期。\n\n**来源**: \`${srcFirst}\`\n\n> ⚠️ Mock AI 自动生成`, rationale: '依赖注入提高可测试性和模块解耦', }, coreCode: `${lang === 'swift' ? `class ${typeFirst} {\n init(service: Service) { }\n}` : `class ${typeFirst} {\n constructor(private service: Service) {}\n}`}`, doClause: `Use constructor injection for ${typeFirst} dependencies`, dontClause: 'Do not use service locator or global singletons', whenClause: `When creating or modifying ${typeFirst} and its dependencies`, usageGuide: `### 使用指南\n\n通过构造器注入依赖项。`, }, { ...base, title: `${projectName} 模块通信约定`, description: `模块间通过协议/接口进行通信,避免直接耦合`, trigger: `@${toKebab(projectName)}-module-comm`, kind: 'rule', category: 'Architecture', topicHint: 'architecture', content: { markdown: `### 模块通信\n\n${projectName} 的模块通信遵循接口隔离原则,${ast.protocols} 个协议定义了清晰的边界。\n\n> ⚠️ Mock AI 自动生成`, rationale: '接口隔离确保模块可独立演进和测试', }, coreCode: `${lang === 'swift' ? `protocol ${typeFirst}Protocol {\n func execute() async\n}` : `interface I${typeFirst} {\n execute(): Promise<void>\n}`}`, doClause: 'Define protocols/interfaces for all cross-module communication', dontClause: 'Do not import concrete implementations across module boundaries', whenClause: 'When modules need to communicate or share data', usageGuide: `### 使用指南\n\n定义协议作为模块间通信契约。`, }, ]; case 'code-pattern': return [ { ...base, title: `${typeFirst} 扩展方法约定`, description: `使用扩展方法为 ${typeFirst} 添加功能,保持核心类型精简`, trigger: `@${toKebab(typeFirst)}-extension-pattern`, kind: 'pattern', category: 'Tool', topicHint: 'conventions', content: { markdown: `### 扩展方法约定\n\n${projectName} 广泛使用扩展方法组织代码,${typeList} 等类型均采用此模式。\n\n**来源**: \`${srcFirst}\`\n\n> ⚠️ Mock AI 自动生成`, rationale: '扩展方法使核心类型保持精简,增强可读性', }, coreCode: `${lang === 'swift' ? `extension ${typeFirst} {\n func helper() { }\n}` : `// ${typeFirst}.extensions.ts`}`, doClause: `Group related functionality in ${lang === 'swift' ? 'extensions' : 'utility modules'}`, dontClause: 'Do not bloat core types with unrelated methods', whenClause: `When adding utility or convenience methods to ${typeFirst}`, usageGuide: `### 使用指南\n\n按功能分组创建扩展文件。`, }, { ...base, title: `${projectName} 类型安全惯例`, description: `项目统一使用强类型和类型守卫确保安全`, trigger: `@${toKebab(projectName)}-type-safety`, kind: 'rule', category: 'Tool', topicHint: 'conventions', content: { markdown: `### 类型安全\n\n${projectName} 强制类型安全,包含 ${ast.classes} 个类和 ${ast.protocols} 个协议定义。\n\n> ⚠️ Mock AI 自动生成`, rationale: '类型安全减少运行时错误', }, coreCode: `${lang === 'swift' ? 'guard let value = optional else { return }' : 'if (value === undefined) { return; }'}`, doClause: 'Use type guards and optional binding for safe access', dontClause: 'Do not use force unwrapping or type casting without checks', whenClause: 'When handling optional values or type conversions', usageGuide: `### 使用指南\n\n始终使用安全的类型转换。`, }, { ...base, title: `${projectName} 命名约定`, description: `文件和类型命名遵循一致的约定`, trigger: `@${toKebab(projectName)}-naming-conventions`, kind: 'rule', category: 'Tool', topicHint: 'conventions', content: { markdown: `### 命名约定\n\n${projectName} 使用一致的命名风格。类型: PascalCase, 方法/属性: camelCase。\n\n> ⚠️ Mock AI 自动生成`, rationale: '统一命名提高代码可读性', }, coreCode: `// ${typeFirst}.swift / ${typeFirst}.ts`, doClause: 'Follow PascalCase for types and camelCase for members', dontClause: 'Do not use abbreviations or inconsistent casing', whenClause: 'When naming new types, methods, or properties', usageGuide: `### 使用指南\n\n遵循项目既有命名风格。`, }, ]; default: return [ { ...base, title: `${projectName} ${dimLabel}实践`, description: `${dimLabel}维度的最佳实践,基于 ${typeList} 等代码分析`, trigger: `@${toKebab(projectName)}-${toKebab(dimLabel)}-practice`, kind: 'pattern', category: 'Tool', topicHint: dimId.includes('network') ? 'networking' : dimId.includes('ui') ? 'ui' : 'conventions', content: { markdown: `### ${dimLabel}\n\n${projectName}${dimLabel} 方面的实践,涉及 ${typeList} 等核心类型。\n\n**文件统计**: ${ast.classes} 类, ${ast.protocols} 协议, ${ast.functions} 函数\n\n**来源**: \`${srcFirst}\`\n\n> ⚠️ Mock AI 自动生成`, rationale: `${dimLabel}是项目质量的重要维度`, }, coreCode: `// See ${srcFirst}`, doClause: `Follow established ${dimLabel} patterns in the project`, dontClause: `Do not deviate from the project's ${dimLabel} conventions`, whenClause: `When working on ${dimLabel}-related code`, usageGuide: `### 使用指南\n\n遵循项目的 ${dimLabel} 约定。`, }, { ...base, title: `${typeFirst} ${dimLabel}约定`, description: `${typeFirst} 相关的 ${dimLabel} 约定`, trigger: `@${toKebab(typeFirst)}-${toKebab(dimLabel)}`, kind: 'rule', category: 'Tool', topicHint: dimId.includes('network') ? 'networking' : 'conventions', content: { markdown: `### ${typeFirst} ${dimLabel}\n\n${typeFirst} 遵循的 ${dimLabel} 约定。\n\n> ⚠️ Mock AI 自动生成`, rationale: `基于 ${typeFirst} 的实际代码模式`, }, coreCode: `// ${typeFirst}${dimLabel}`, doClause: `Apply ${dimLabel} patterns consistent with ${typeFirst}`, dontClause: `Do not introduce inconsistent ${dimLabel} approaches`, whenClause: `When modifying ${typeFirst} or related components`, usageGuide: `### 使用指南\n\n参考 ${typeFirst} 的既有实现。`, }, { ...base, title: `${projectName} ${dimLabel}检查清单`, description: `${dimLabel}方面的代码审查清单`, trigger: `@${toKebab(projectName)}-${toKebab(dimLabel)}-checklist`, kind: 'fact', category: 'Tool', topicHint: 'conventions', content: { markdown: `### ${dimLabel} 检查清单\n\n1. 遵循项目既有模式\n2. 确保类型安全\n3. 添加适当的错误处理\n4. 保持代码一致性\n\n> ⚠️ Mock AI 自动生成`, rationale: `${dimLabel}检查清单帮助保持代码质量`, }, coreCode: `// ${dimLabel} checklist applied`, doClause: `Review code against the ${dimLabel} checklist before committing`, dontClause: `Do not skip ${dimLabel} review for shortcuts`, whenClause: `During code review for ${dimLabel}-related changes`, usageGuide: `### 使用指南\n\n在代码审查时参考此清单。`, }, ]; } } // ── Mock Pipeline 入口 ──────────────────────────────────────── /** * fillDimensionsMock — Mock AI 轻量管线 * * 利用 Phase 1-4 的真实数据(AST、文件列表、Panorama)自动生成候选知识, * 不调用任何 AI API,但走完 submit → dimension_complete 的完整流程。 */ export async function fillDimensionsMock(view, dimensions) { const { snapshot, projectRoot } = view; const ctx = view.ctx; const emitter = new BootstrapEventEmitter(ctx.container); const projectName = path.basename(projectRoot); const primaryLang = snapshot.language.primaryLang ?? 'unknown'; const astStats = extractAstStats(snapshot.ast); const allFiles = snapshot.allFiles; const targetFileMap = view.targetFileMap; const repFiles = getRepresentativeFiles(allFiles, targetFileMap); logger.info(`[MockPipeline] ═══ Starting Mock bootstrap — ${dimensions.length} dimensions, ` + `${allFiles?.length || 0} files, lang=${primaryLang}`); emitter.emitProgress('bootstrap:mock-mode', { message: '🧪 Mock AI 模式 — 基于代码结构自动生成知识候选(非 AI 深度分析)', mockMode: true, }); let totalGenerated = 0; for (const dim of dimensions) { const dimStartTime = Date.now(); const dimLabel = dim.label ?? dim.id; emitter.emitProgress('bootstrap:dimension-start', { dimensionId: dim.id, dimensionLabel: dimLabel, mockMode: true, }); logger.info(`[MockPipeline] ── Dimension "${dim.id}" (${dimLabel}) ──`); // 生成候选(仅在内存中,不写入数据库) const candidates = generateDimensionCandidates(dim.id, dimLabel, allFiles, primaryLang, projectName, astStats, repFiles); totalGenerated += candidates.length; for (const candidate of candidates) { logger.info(`[MockPipeline] 📝 Generated (not persisted): "${candidate.title}"`); } const durationMs = Date.now() - dimStartTime; emitter.emitDimensionComplete(dim.id, { type: 'candidate', extracted: candidates.length, created: 0, status: 'mock-pipeline-complete', degraded: false, durationMs, toolCallCount: 0, source: 'mock-pipeline', }); logger.info(`[MockPipeline] ✅ "${dim.id}": ${candidates.length} candidates generated (mock-only, not persisted), ${durationMs}ms`); } emitter.emitProgress('bootstrap:mock-complete', { message: `🧪 Mock Bootstrap 完成: ${totalGenerated} 个候选知识已生成(仅预览,未写入数据库)`, totalCreated: 0, totalGenerated, mockMode: true, }); logger.info(`[MockPipeline] ═══ Mock bootstrap complete — ${totalGenerated} candidates generated (not persisted) from ${dimensions.length} dimensions`); }