autosnippet 3.0.11 → 3.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/bin/cli.js +64 -1
  2. package/config/default.json +9 -0
  3. package/dashboard/dist/assets/{index-I2ySoCmF.js → index-Bnm26ulL.js} +47 -47
  4. package/dashboard/dist/index.html +1 -1
  5. package/lib/cli/SetupService.js +92 -5
  6. package/lib/cli/UpgradeService.js +14 -5
  7. package/lib/core/discovery/GenericDiscoverer.js +4 -28
  8. package/lib/external/mcp/handlers/bootstrap/base-dimensions.js +246 -0
  9. package/lib/external/mcp/handlers/bootstrap/pipeline/checkpoint.js +80 -0
  10. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +275 -0
  11. package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +600 -0
  12. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +125 -342
  13. package/lib/external/mcp/handlers/bootstrap/refine.js +362 -0
  14. package/lib/external/mcp/handlers/bootstrap.js +6 -590
  15. package/lib/external/mcp/handlers/browse.js +119 -9
  16. package/lib/external/mcp/handlers/guard.js +25 -6
  17. package/lib/external/mcp/handlers/search.js +56 -24
  18. package/lib/http/routes/guardRules.js +9 -17
  19. package/lib/injection/ServiceContainer.js +12 -3
  20. package/lib/platform/ios/xcode/XcodeImportResolver.js +434 -0
  21. package/lib/platform/ios/xcode/XcodeIntegration.js +40 -659
  22. package/lib/platform/ios/xcode/XcodeWriteUtils.js +220 -0
  23. package/lib/service/chat/ChatAgent.js +39 -418
  24. package/lib/service/chat/ChatAgentPrompts.js +149 -0
  25. package/lib/service/chat/ChatAgentTasks.js +297 -0
  26. package/lib/service/chat/tools/_shared.js +61 -0
  27. package/lib/service/chat/tools/ai-analysis.js +284 -0
  28. package/lib/service/chat/tools/ast-graph.js +681 -0
  29. package/lib/service/chat/tools/composite.js +496 -0
  30. package/lib/service/chat/tools/guard.js +265 -0
  31. package/lib/service/chat/tools/index.js +250 -0
  32. package/lib/service/chat/tools/infrastructure.js +222 -0
  33. package/lib/service/chat/tools/knowledge-graph.js +234 -0
  34. package/lib/service/chat/tools/lifecycle.js +469 -0
  35. package/lib/service/chat/tools/project-access.js +923 -0
  36. package/lib/service/chat/tools/query.js +264 -0
  37. package/lib/service/chat/tools.js +14 -3994
  38. package/lib/service/cursor/AgentInstructionsGenerator.js +395 -0
  39. package/lib/service/cursor/CursorDeliveryPipeline.js +70 -11
  40. package/lib/service/cursor/FileProtection.js +116 -0
  41. package/lib/service/cursor/KnowledgeCompressor.js +61 -11
  42. package/lib/service/cursor/SkillsSyncer.js +5 -3
  43. package/lib/service/cursor/TopicClassifier.js +19 -3
  44. package/lib/service/guard/ExclusionManager.js +26 -2
  45. package/lib/service/guard/GuardCheckEngine.js +38 -370
  46. package/lib/service/guard/GuardCodeChecks.js +362 -0
  47. package/lib/service/guard/GuardCrossFileChecks.js +307 -0
  48. package/lib/service/guard/GuardPatternUtils.js +180 -0
  49. package/lib/service/guard/GuardService.js +80 -38
  50. package/lib/service/module/ModuleService.js +1 -0
  51. package/lib/service/search/SearchEngine.js +10 -2
  52. package/lib/service/wiki/WikiGenerator.js +226 -1532
  53. package/lib/service/wiki/WikiRenderers.js +1878 -0
  54. package/lib/service/wiki/WikiUtils.js +907 -0
  55. package/lib/shared/LanguageService.js +299 -0
  56. package/package.json +1 -1
@@ -0,0 +1,907 @@
1
+ /**
2
+ * WikiUtils.js — Wiki 生成器工具函数
3
+ *
4
+ * 从 WikiGenerator.js 中提取的纯工具/辅助函数,无 class 依赖。
5
+ *
6
+ * @module WikiUtils
7
+ */
8
+
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { LanguageService } from '../../shared/LanguageService.js';
12
+ import Logger from '../../infrastructure/logging/Logger.js';
13
+
14
+ const logger = Logger.getInstance();
15
+
16
+ // ─── 工具函数 ────────────────────────────────────────────────
17
+
18
+ /**
19
+ * 文本 slug 化
20
+ */
21
+ export function slug(name) {
22
+ return name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase();
23
+ }
24
+
25
+ /**
26
+ * Mermaid 安全 ID
27
+ */
28
+ export function mermaidId(name) {
29
+ return name.replace(/[^a-zA-Z0-9]/g, '_');
30
+ }
31
+
32
+ /**
33
+ * 遍历目录(排除 build/Pods/DerivedData 等)
34
+ */
35
+ export function walkDir(dir, callback, maxFiles = 500) {
36
+ const excludeNames = new Set([
37
+ 'Pods',
38
+ 'Carthage',
39
+ 'node_modules',
40
+ '.build',
41
+ 'build',
42
+ 'DerivedData',
43
+ 'vendor',
44
+ '.git',
45
+ '__tests__',
46
+ 'Tests',
47
+ 'AutoSnippet',
48
+ '.cursor',
49
+ ]);
50
+ let count = 0;
51
+
52
+ const walk = (d) => {
53
+ if (count >= maxFiles) {
54
+ return;
55
+ }
56
+ let entries;
57
+ try {
58
+ entries = fs.readdirSync(d, { withFileTypes: true });
59
+ } catch {
60
+ return;
61
+ }
62
+
63
+ for (const entry of entries) {
64
+ if (count >= maxFiles) {
65
+ return;
66
+ }
67
+ if (excludeNames.has(entry.name)) {
68
+ continue;
69
+ }
70
+ if (entry.name.startsWith('.')) {
71
+ continue;
72
+ }
73
+
74
+ const fullPath = path.join(d, entry.name);
75
+ if (entry.isDirectory()) {
76
+ walk(fullPath);
77
+ } else if (entry.isFile()) {
78
+ callback(fullPath);
79
+ count++;
80
+ }
81
+ }
82
+ };
83
+
84
+ walk(dir);
85
+ }
86
+
87
+ /**
88
+ * 从文件相对路径推断所属模块名
89
+ * 支持多种项目结构约定:
90
+ * SPM: Sources/{ModuleName}/...
91
+ * Node.js: packages/{name}/... | src/{name}/... | lib/{name}/...
92
+ * Go: pkg/{name}/... | internal/{name}/... | cmd/{name}/...
93
+ * Rust: crates/{name}/... | src/ (单 crate)
94
+ * Python: src/{name}/... | {name}/ (顶层包)
95
+ * Java/Kt: src/main/java/{pkg}/... (取第一个包段)
96
+ * Dart: lib/{name}/...
97
+ *
98
+ * 兜底: 取第一级目录名
99
+ */
100
+ export function inferModuleFromPath(filePath) {
101
+ const parts = filePath.split('/');
102
+
103
+ // SPM: Sources/{Module}/...
104
+ const sourcesIdx = parts.indexOf('Sources');
105
+ if (sourcesIdx >= 0 && sourcesIdx + 1 < parts.length) {
106
+ return parts[sourcesIdx + 1];
107
+ }
108
+
109
+ // Node.js monorepo: packages/{name}/... | apps/{name}/...
110
+ for (const dir of ['packages', 'apps', 'modules']) {
111
+ const idx = parts.indexOf(dir);
112
+ if (idx >= 0 && idx + 1 < parts.length) {
113
+ return parts[idx + 1];
114
+ }
115
+ }
116
+
117
+ // Go: pkg/{name}/... | internal/{name}/... | cmd/{name}/...
118
+ for (const dir of ['pkg', 'internal', 'cmd']) {
119
+ const idx = parts.indexOf(dir);
120
+ if (idx >= 0 && idx + 1 < parts.length) {
121
+ return parts[idx + 1];
122
+ }
123
+ }
124
+
125
+ // Rust: crates/{name}/...
126
+ const cratesIdx = parts.indexOf('crates');
127
+ if (cratesIdx >= 0 && cratesIdx + 1 < parts.length) {
128
+ return parts[cratesIdx + 1];
129
+ }
130
+
131
+ // Java/Kotlin: src/main/java/{pkg}/... → 跳过域名前缀,取最后一个有意义的包目录
132
+ // 例: src/main/java/org/springframework/samples/petclinic/vet/Vet.java → "vet"
133
+ // 例: src/main/java/com/example/demo/DemoApp.java → "demo"
134
+ const JAVA_DOMAIN_PREFIXES = new Set(['com', 'org', 'net', 'io', 'de', 'fr', 'jp', 'cn', 'uk', 'us', 'edu', 'gov']);
135
+ for (const langDir of ['java', 'kotlin']) {
136
+ const langIdx = parts.indexOf(langDir);
137
+ if (langIdx >= 0 && langIdx + 1 < parts.length) {
138
+ // 文件名所在目录(倒数第二个 part)才是 "模块"
139
+ const fileName = parts[parts.length - 1];
140
+ const pkgParts = parts.slice(langIdx + 1, parts.length - 1); // 包路径(不含文件名)
141
+ if (pkgParts.length >= 2) {
142
+ // 从尾部取: 最后一个包段即为功能模块
143
+ return pkgParts[pkgParts.length - 1];
144
+ }
145
+ if (pkgParts.length === 1) {
146
+ return pkgParts[0];
147
+ }
148
+ // 只有文件直接在 java/ 下
149
+ return parts[langIdx + 1];
150
+ }
151
+ }
152
+
153
+ // Generic: src/{name}/... | lib/{name}/... (至少 3 层深时)
154
+ for (const dir of ['src', 'lib']) {
155
+ const idx = parts.indexOf(dir);
156
+ if (idx >= 0 && idx + 1 < parts.length && parts.length > idx + 2) {
157
+ return parts[idx + 1];
158
+ }
159
+ }
160
+
161
+ // 兜底: 取第一级目录名
162
+ return parts.length > 1 ? parts[0] : null;
163
+ }
164
+
165
+ /**
166
+ * 获取某个 Target 对应的源文件列表
167
+ * 按优先级匹配: target.path → target.info.path → sourceFilesByModule[name]
168
+ */
169
+ export function getModuleSourceFiles(target, projectInfo) {
170
+ const sfm = projectInfo.sourceFilesByModule || {};
171
+ const name = target.name;
172
+
173
+ // 1. 按模块名直接匹配(最常见: Sources/{name}/ 解析出的 key)
174
+ if (sfm[name]?.length > 0) {
175
+ return sfm[name];
176
+ }
177
+
178
+ // 2. 通过 target.path 或 target.info.path 匹配
179
+ const targetPath = target.path || target.info?.path;
180
+ if (targetPath) {
181
+ const matched = (projectInfo.sourceFiles || []).filter(
182
+ (f) => f.startsWith(`${targetPath}/`) || f.startsWith(targetPath + path.sep)
183
+ );
184
+ if (matched.length > 0) {
185
+ return matched;
186
+ }
187
+ }
188
+
189
+ // 3. 大小写不敏感模糊匹配
190
+ const lower = name.toLowerCase();
191
+ for (const [key, files] of Object.entries(sfm)) {
192
+ if (key.toLowerCase() === lower) {
193
+ return files;
194
+ }
195
+ }
196
+
197
+ return [];
198
+ }
199
+
200
+ /**
201
+ * 基于模块名称和内容推断模块功能
202
+ * 对常见命名模式做智能推断
203
+ */
204
+ export function inferModulePurpose(name, classes, protocols, files) {
205
+ const lower = name.toLowerCase();
206
+ const _fileNames = files.map((f) => path.basename(f).toLowerCase());
207
+
208
+ // 常见模块功能推断规则
209
+ const rules = [
210
+ {
211
+ match: /network|http|api|client|request|fetch/i,
212
+ zh: '负责网络通信和 API 调用',
213
+ en: 'handles network communication and API calls',
214
+ },
215
+ {
216
+ match: /ui|view|component|widget|screen|page/i,
217
+ zh: '提供用户界面组件',
218
+ en: 'provides user interface components',
219
+ },
220
+ {
221
+ match: /model|entity|domain|data/i,
222
+ zh: '定义数据模型和领域实体',
223
+ en: 'defines data models and domain entities',
224
+ },
225
+ {
226
+ match: /storage|database|cache|persist|core\s*data|realm/i,
227
+ zh: '负责数据持久化和存储',
228
+ en: 'manages data persistence and storage',
229
+ },
230
+ {
231
+ match: /auth|login|session|token|credential/i,
232
+ zh: '处理认证授权和会话管理',
233
+ en: 'handles authentication and session management',
234
+ },
235
+ {
236
+ match: /util|helper|extension|common|shared|foundation/i,
237
+ zh: '提供公共工具类和扩展方法',
238
+ en: 'provides common utilities and extensions',
239
+ },
240
+ { match: /test|spec|mock/i, zh: '包含单元测试和 Mock', en: 'contains unit tests and mocks' },
241
+ {
242
+ match: /router|navigation|coordinator|flow/i,
243
+ zh: '管理页面路由和导航流',
244
+ en: 'manages page routing and navigation flow',
245
+ },
246
+ {
247
+ match: /config|setting|preference|env/i,
248
+ zh: '管理应用配置和环境设置',
249
+ en: 'manages app configuration and environment settings',
250
+ },
251
+ {
252
+ match: /log|analytics|track|monitor/i,
253
+ zh: '提供日志记录和数据分析能力',
254
+ en: 'provides logging and analytics capabilities',
255
+ },
256
+ {
257
+ match: /media|image|video|audio|player/i,
258
+ zh: '处理多媒体资源',
259
+ en: 'handles multimedia resources',
260
+ },
261
+ {
262
+ match: /service|manager|provider/i,
263
+ zh: '提供核心业务服务',
264
+ en: 'provides core business services',
265
+ },
266
+ ];
267
+
268
+ // 先按模块名匹配
269
+ for (const rule of rules) {
270
+ if (rule.match.test(lower)) {
271
+ return rule;
272
+ }
273
+ }
274
+
275
+ // 再按类名匹配
276
+ const classStr = classes.join(' ');
277
+ for (const rule of rules) {
278
+ if (rule.match.test(classStr)) {
279
+ return rule;
280
+ }
281
+ }
282
+
283
+ return null;
284
+ }
285
+
286
+ /**
287
+ * 从 CodeEntityGraph 提取继承根节点
288
+ * @param {object|null} codeEntityGraph
289
+ * @returns {Array<{name: string, children: string[]}>}
290
+ */
291
+ export function getInheritanceRoots(codeEntityGraph) {
292
+ if (!codeEntityGraph) {
293
+ return [];
294
+ }
295
+ try {
296
+ // 尝试查询继承关系
297
+ const entities =
298
+ codeEntityGraph.queryEntities?.({ entityType: 'class', limit: 50 }) || [];
299
+ const roots = [];
300
+ for (const e of entities) {
301
+ const _parents =
302
+ codeEntityGraph.queryEdges?.({ toId: e.entityId, relation: 'inherits' }) || [];
303
+ const children =
304
+ codeEntityGraph.queryEdges?.({ fromId: e.entityId, relation: 'inherits' }) || [];
305
+ if (children.length > 0) {
306
+ roots.push({ name: e.name, children: children.map((c) => c.toId || c.to_id) });
307
+ }
308
+ }
309
+ return roots.sort((a, b) => (b.children?.length || 0) - (a.children?.length || 0));
310
+ } catch {
311
+ return [];
312
+ }
313
+ }
314
+
315
+ /**
316
+ * 两层去重
317
+ *
318
+ * Layer 1: Title slug 碰撞 — 同名文件不同目录 → hash 相同则删除副本
319
+ * Layer 2: Content hash — 跨文件内容完全相同 → 仅保留第一个
320
+ *
321
+ * @param {Array} files
322
+ * @param {string} wikiDir
323
+ * @param {(phase: string, progress: number, message: string) => void} emit
324
+ * @returns {{ removed: string[], kept: number }}
325
+ */
326
+ export function dedup(files, wikiDir, emit) {
327
+ const removed = [];
328
+
329
+ // Layer 1: slug 碰撞(同名文件跨目录)
330
+ const slugMap = new Map(); // slug → first file
331
+ for (const file of files) {
332
+ const s = path.basename(file.path, path.extname(file.path)).toLowerCase();
333
+ if (slugMap.has(s)) {
334
+ const existing = slugMap.get(s);
335
+ // 完全相同 hash → 移除后来的
336
+ if (existing.hash === file.hash) {
337
+ const fullPath = path.join(wikiDir, file.path);
338
+ try {
339
+ fs.unlinkSync(fullPath);
340
+ } catch {
341
+ /* skip */
342
+ }
343
+ removed.push(file.path);
344
+ logger.info(
345
+ `[WikiGenerator] Dedup: removed ${file.path} (same hash as ${existing.path})`
346
+ );
347
+ }
348
+ // hash 不同 → 保留两个(不同目录允许同名)
349
+ } else {
350
+ slugMap.set(s, file);
351
+ }
352
+ }
353
+
354
+ // Layer 2: content hash 碰撞(不同文件名但内容相同)
355
+ const hashMap = new Map(); // hash → first file path
356
+ for (const file of files) {
357
+ if (removed.includes(file.path)) {
358
+ continue;
359
+ }
360
+ if (hashMap.has(file.hash)) {
361
+ const firstPath = hashMap.get(file.hash);
362
+ // 优先保留代码生成的(非 synced)
363
+ const isFirstSynced = firstPath.startsWith('documents/') || firstPath.startsWith('skills/');
364
+ const isCurrentSynced =
365
+ file.path.startsWith('documents/') || file.path.startsWith('skills/');
366
+
367
+ if (isCurrentSynced && !isFirstSynced) {
368
+ // 当前是 synced,first 是 codegen → 删除 synced
369
+ const fullPath = path.join(wikiDir, file.path);
370
+ try {
371
+ fs.unlinkSync(fullPath);
372
+ } catch {
373
+ /* skip */
374
+ }
375
+ removed.push(file.path);
376
+ logger.info(
377
+ `[WikiGenerator] Dedup: removed synced ${file.path} (same content as ${firstPath})`
378
+ );
379
+ }
380
+ // 其他情况保留两个
381
+ } else {
382
+ hashMap.set(file.hash, file.path);
383
+ }
384
+ }
385
+
386
+ // 从 files 数组中移除已删除的
387
+ for (let i = files.length - 1; i >= 0; i--) {
388
+ if (removed.includes(files[i].path)) {
389
+ files.splice(i, 1);
390
+ }
391
+ }
392
+
393
+ if (removed.length > 0) {
394
+ emit('dedup', 93, `去重: 移除 ${removed.length} 个重复文件`);
395
+ } else {
396
+ emit('dedup', 93, '无重复文件');
397
+ }
398
+
399
+ return { removed, kept: files.length };
400
+ }
401
+
402
+ // ─── 多语言支持 ──────────────────────────────────────────────
403
+
404
+ /**
405
+ * 按主语言返回 AST 术语(中英文)
406
+ *
407
+ * 不同语言对"类"和"接口"有不同称谓,Wiki 文档应使用合适的措辞。
408
+ *
409
+ * @param {string} langId - LanguageService langId,如 'swift', 'python', 'go'
410
+ * @returns {{ typeLabel: {zh: string, en: string}, interfaceLabel: {zh: string, en: string}, moduleMetric: {zh: string, en: string} }}
411
+ */
412
+ export function getLangTerms(langId) {
413
+ const TERMS = {
414
+ swift: { typeLabel: { zh: '类/结构体', en: 'Classes/Structs' }, interfaceLabel: { zh: '协议', en: 'Protocols' }, moduleMetric: { zh: 'SPM Targets', en: 'SPM Targets' } },
415
+ objectivec: { typeLabel: { zh: '类', en: 'Classes' }, interfaceLabel: { zh: '协议', en: 'Protocols' }, moduleMetric: { zh: 'Targets', en: 'Targets' } },
416
+ typescript: { typeLabel: { zh: '类', en: 'Classes' }, interfaceLabel: { zh: '接口', en: 'Interfaces' }, moduleMetric: { zh: 'Packages', en: 'Packages' } },
417
+ javascript: { typeLabel: { zh: '类/模块', en: 'Classes/Modules' }, interfaceLabel: { zh: '接口', en: 'Interfaces' }, moduleMetric: { zh: 'Packages', en: 'Packages' } },
418
+ python: { typeLabel: { zh: '类', en: 'Classes' }, interfaceLabel: { zh: '抽象基类', en: 'Abstract Base' }, moduleMetric: { zh: 'Packages', en: 'Packages' } },
419
+ go: { typeLabel: { zh: '结构体', en: 'Structs' }, interfaceLabel: { zh: '接口', en: 'Interfaces' }, moduleMetric: { zh: 'Go Modules', en: 'Go Modules' } },
420
+ rust: { typeLabel: { zh: '结构体/枚举', en: 'Structs/Enums' }, interfaceLabel: { zh: 'Trait', en: 'Traits' }, moduleMetric: { zh: 'Crates', en: 'Crates' } },
421
+ java: { typeLabel: { zh: '类', en: 'Classes' }, interfaceLabel: { zh: '接口', en: 'Interfaces' }, moduleMetric: { zh: 'Modules', en: 'Modules' } },
422
+ kotlin: { typeLabel: { zh: '类', en: 'Classes' }, interfaceLabel: { zh: '接口', en: 'Interfaces' }, moduleMetric: { zh: 'Modules', en: 'Modules' } },
423
+ dart: { typeLabel: { zh: '类', en: 'Classes' }, interfaceLabel: { zh: '抽象类', en: 'Abstract Classes' }, moduleMetric: { zh: 'Packages', en: 'Packages' } },
424
+ csharp: { typeLabel: { zh: '类', en: 'Classes' }, interfaceLabel: { zh: '接口', en: 'Interfaces' }, moduleMetric: { zh: 'Projects', en: 'Projects' } },
425
+ };
426
+ return TERMS[langId] || { typeLabel: { zh: '类型', en: 'Types' }, interfaceLabel: { zh: '接口', en: 'Interfaces' }, moduleMetric: { zh: 'Modules', en: 'Modules' } };
427
+ }
428
+
429
+ /**
430
+ * 已知的构建系统标志文件 → 生态类型映射
431
+ *
432
+ * @deprecated 请使用 LanguageService.buildSystemMarkers。此处保留为只读引用以保持向后兼容。
433
+ * @type {ReadonlyArray<{file: string, eco: string, buildTool: string}>}
434
+ */
435
+ export const BUILD_SYSTEM_MARKERS = LanguageService.buildSystemMarkers;
436
+
437
+ /**
438
+ * 检测项目根目录中存在的构建系统标志
439
+ *
440
+ * 两级检测:
441
+ * 1. 先检查根目录的一级文件
442
+ * 2. 如果根目录未找到,检查一级子目录(支持 monorepo 如 AppFlowy/frontend/...)
443
+ *
444
+ * @param {string[]} rootEntryNames - 项目根目录一级文件/目录名列表
445
+ * @param {string} [projectRoot] - 可选的项目根路径,用于二级检测
446
+ * @returns {Array<{eco: string, buildTool: string}>} 匹配到的构建系统
447
+ */
448
+ export function detectBuildSystems(rootEntryNames, projectRoot) {
449
+ // 委托给 LanguageService 做一级匹配
450
+ const results = LanguageService.matchBuildMarkers(rootEntryNames);
451
+ const seenEco = new Set(results.map((r) => r.eco));
452
+
453
+ // 二级检测: monorepo / 嵌套项目 — 检查一级子目录
454
+ if (projectRoot && results.length === 0) {
455
+ const skipDirs = LanguageService.scanSkipDirs;
456
+ try {
457
+ const entries = fs.readdirSync(projectRoot, { withFileTypes: true });
458
+ for (const dir of entries) {
459
+ if (!dir.isDirectory() || dir.name.startsWith('.') || skipDirs.has(dir.name)) continue;
460
+ try {
461
+ const subEntries = fs.readdirSync(path.join(projectRoot, dir.name)).filter(
462
+ (n) => !n.startsWith('.')
463
+ );
464
+ const subResults = LanguageService.matchBuildMarkers(subEntries);
465
+ for (const r of subResults) {
466
+ if (!seenEco.has(r.eco)) {
467
+ results.push(r);
468
+ seenEco.add(r.eco);
469
+ }
470
+ }
471
+ } catch { /* skip unreadable subdirs */ }
472
+ }
473
+ } catch { /* skip */ }
474
+ }
475
+
476
+ return results;
477
+ }
478
+
479
+ // ─── Folder Profile 分析 (AST 不可用时的降级策略) ─────────
480
+
481
+ /**
482
+ * @typedef {object} FolderProfile
483
+ * @property {string} name — 文件夹名
484
+ * @property {string} relPath — 相对于项目根的路径
485
+ * @property {number} fileCount — 源文件数 (递归)
486
+ * @property {number} totalSize — 总字节数
487
+ * @property {number} depth — 距项目根的深度
488
+ * @property {Record<string, number>} langBreakdown — {语言 → 文件数}
489
+ * @property {string[]} keyFiles — 重要文件 (入口、配置、大文件)
490
+ * @property {string[]} fileNames — 所有源文件 basename (排序)
491
+ * @property {string|null} readme — 文件夹内 README 内容摘要
492
+ * @property {string|null} purpose — 从命名推断的功能描述
493
+ * @property {string[]} imports — 通过 import/require regex 提取的外部依赖目标 (其他文件夹)
494
+ * @property {string[]} entryPoints — 检测到的入口文件 (index.*, main.*, __init__.py, mod.rs)
495
+ * @property {string[]} namingPatterns — 检测到的命名约定
496
+ * @property {string[]} headerComments — 从关键文件提取的首段注释
497
+ */
498
+
499
+ /** 入口文件名模式 */
500
+ const ENTRY_POINT_NAMES = new Set([
501
+ 'index.js', 'index.ts', 'index.tsx', 'index.jsx', 'index.mjs',
502
+ 'main.js', 'main.ts', 'main.go', 'main.py', 'main.rs', 'main.dart', 'main.c', 'main.cpp',
503
+ 'mod.rs', 'lib.rs',
504
+ '__init__.py',
505
+ 'app.js', 'app.ts', 'app.py', 'app.rb',
506
+ 'server.js', 'server.ts', 'server.py',
507
+ ]);
508
+
509
+ /** 多语言 import/require 正则 (轻量级, 不依赖 AST) */
510
+ const IMPORT_PATTERNS = [
511
+ // JS/TS: import ... from '...' or require('...')
512
+ /(?:import\s+.*?\s+from\s+['"]([^'"]+)['"]|require\s*\(\s*['"]([^'"]+)['"]\s*\))/,
513
+ // Python: import xxx / from xxx import yyy
514
+ /(?:^from\s+([.\w]+)\s+import|^import\s+([.\w]+))/,
515
+ // Go: import "path/to/pkg"
516
+ /import\s+(?:\w+\s+)?["']([^"']+)["']/,
517
+ // Rust: use crate::xxx / use super::xxx / mod xxx
518
+ /(?:use\s+(?:crate|super)::(\w+)|mod\s+(\w+)\s*;)/,
519
+ // Java/Kotlin: import com.xxx.yyy
520
+ /import\s+([\w.]+)/,
521
+ // Ruby: require 'xxx' / require_relative 'xxx'
522
+ /require(?:_relative)?\s+['"]([^'"]+)['"]/,
523
+ // C/C++: #include "xxx"
524
+ /#include\s+"([^"]+)"/,
525
+ // Dart: import 'package:xxx/yyy.dart'
526
+ /import\s+['"](?:package:)?([^'"]+)['"]/,
527
+ ];
528
+
529
+ /**
530
+ * 分析项目中重要文件夹,生成 FolderProfile 列表
531
+ *
532
+ * 适用场景: AST 无法提取 target(类/函数/协议)的语言,
533
+ * 通过文件夹结构、文件命名、轻量 import 分析来产出有意义的 wiki 内容。
534
+ *
535
+ * @param {object} projectInfo - WikiGenerator._scanProject() 的输出
536
+ * @param {object} [options]
537
+ * @param {number} [options.minFiles=3] — 文件夹最少文件数阈值
538
+ * @param {number} [options.maxFolders=20] — 最多分析的文件夹数
539
+ * @param {number} [options.sampleLines=40] — 每个文件采样行数 (用于 import 提取)
540
+ * @returns {FolderProfile[]}
541
+ */
542
+ export function profileFolders(projectInfo, options = {}) {
543
+ const {
544
+ minFiles = 3,
545
+ maxFolders = 20,
546
+ sampleLines = 40,
547
+ } = options;
548
+
549
+ const root = projectInfo.root;
550
+ const sourceFiles = projectInfo.sourceFiles || [];
551
+
552
+ // ── 1. 按文件夹分组源文件 ──
553
+ /** @type {Map<string, string[]>} relDir → [relFilePath, ...] */
554
+ const folderFiles = new Map();
555
+ for (const relFile of sourceFiles) {
556
+ const dir = path.dirname(relFile);
557
+ if (!folderFiles.has(dir)) {
558
+ folderFiles.set(dir, []);
559
+ }
560
+ folderFiles.get(dir).push(relFile);
561
+ }
562
+
563
+ // ── 2. 聚合: 将子文件夹的文件计入父文件夹 (递归) ──
564
+ /** @type {Map<string, string[]>} relDir → 所有递归子文件 */
565
+ const folderRecursive = new Map();
566
+ for (const [dir, files] of folderFiles) {
567
+ // 把文件计入 dir 本身及所有祖先
568
+ const parts = dir.split('/');
569
+ for (let depth = 1; depth <= parts.length; depth++) {
570
+ const ancestor = parts.slice(0, depth).join('/');
571
+ if (!folderRecursive.has(ancestor)) {
572
+ folderRecursive.set(ancestor, []);
573
+ }
574
+ folderRecursive.get(ancestor).push(...files);
575
+ }
576
+ }
577
+
578
+ // ── 3. 筛选重要文件夹 ──
579
+ const candidates = [];
580
+ for (const [dir, files] of folderRecursive) {
581
+ if (files.length < minFiles) continue;
582
+ // 排除根目录 '.'
583
+ if (dir === '.') continue;
584
+ // 排除太深的目录 (depth > 4), 这些通常是叶子目录, 信息量低
585
+ const depth = dir.split('/').length;
586
+ if (depth > 4) continue;
587
+
588
+ candidates.push({ dir, files, depth });
589
+ }
590
+
591
+ // 按文件数降序, 优先保留文件多的大模块
592
+ candidates.sort((a, b) => b.files.length - a.files.length);
593
+
594
+ // 去除被父级包含且文件完全是父级子集的冗余候选
595
+ // (保留层次分明的目录: 如果父子文件数差异不大, 去掉子)
596
+ const selected = _pruneRedundantFolders(candidates.slice(0, maxFolders * 2), maxFolders);
597
+
598
+ // ── 4. 为每个选中的文件夹生成 Profile ──
599
+ const profiles = [];
600
+
601
+ for (const { dir, files, depth } of selected) {
602
+ const profile = _buildFolderProfile(dir, files, depth, root, sampleLines);
603
+ if (profile) {
604
+ profiles.push(profile);
605
+ }
606
+ }
607
+
608
+ // 按 fileCount 降序 + depth 升序 排序
609
+ profiles.sort((a, b) => {
610
+ if (b.fileCount !== a.fileCount) return b.fileCount - a.fileCount;
611
+ return a.depth - b.depth;
612
+ });
613
+
614
+ return profiles.slice(0, maxFolders);
615
+ }
616
+
617
+ /**
618
+ * 修剪冗余文件夹: 如果子目录文件数与父目录接近 (>80%), 仅保留父目录
619
+ * @private
620
+ */
621
+ function _pruneRedundantFolders(candidates, maxFolders) {
622
+ const kept = [];
623
+ const removedDirs = new Set();
624
+
625
+ for (const c of candidates) {
626
+ if (removedDirs.has(c.dir)) continue;
627
+
628
+ // 检查是否有已 kept 的父目录, 且文件比率 > 80%
629
+ let isRedundant = false;
630
+ for (const k of kept) {
631
+ if (c.dir.startsWith(k.dir + '/')) {
632
+ // c 是 k 的子目录
633
+ if (c.files.length / k.files.length > 0.8) {
634
+ isRedundant = true;
635
+ break;
636
+ }
637
+ } else if (k.dir.startsWith(c.dir + '/')) {
638
+ // c 是 k 的父目录, k 覆盖了 c 大部分 → 保留 c (更高层), 移除 k
639
+ if (k.files.length / c.files.length > 0.8) {
640
+ removedDirs.add(k.dir);
641
+ }
642
+ }
643
+ }
644
+
645
+ if (!isRedundant) {
646
+ kept.push(c);
647
+ }
648
+
649
+ if (kept.length >= maxFolders) break;
650
+ }
651
+
652
+ return kept.filter(c => !removedDirs.has(c.dir));
653
+ }
654
+
655
+ /**
656
+ * 为单个文件夹构建 FolderProfile
657
+ * @private
658
+ */
659
+ function _buildFolderProfile(relDir, files, depth, projectRoot, sampleLines) {
660
+ const fullDir = path.join(projectRoot, relDir);
661
+ const folderName = path.basename(relDir);
662
+
663
+ // ── 语言分布 ──
664
+ const langBreakdown = {};
665
+ let totalSize = 0;
666
+ for (const f of files) {
667
+ const ext = path.extname(f);
668
+ const lang = LanguageService.displayNameFromExt(ext) || ext;
669
+ langBreakdown[lang] = (langBreakdown[lang] || 0) + 1;
670
+ try {
671
+ const stat = fs.statSync(path.join(projectRoot, f));
672
+ totalSize += stat.size;
673
+ } catch { /* skip */ }
674
+ }
675
+
676
+ // ── 文件名列表 ──
677
+ const fileNames = files.map(f => path.basename(f)).sort();
678
+
679
+ // ── 入口点检测 ──
680
+ const entryPoints = files.filter(f => ENTRY_POINT_NAMES.has(path.basename(f).toLowerCase()));
681
+
682
+ // ── 重要文件 (大文件 top5 + 入口文件) ──
683
+ const fileSizes = [];
684
+ for (const f of files) {
685
+ try {
686
+ const stat = fs.statSync(path.join(projectRoot, f));
687
+ fileSizes.push({ file: f, size: stat.size });
688
+ } catch { /* skip */ }
689
+ }
690
+ fileSizes.sort((a, b) => b.size - a.size);
691
+ const keyFiles = [
692
+ ...new Set([
693
+ ...entryPoints,
694
+ ...fileSizes.slice(0, 5).map(fs => fs.file),
695
+ ]),
696
+ ];
697
+
698
+ // ── README 检测 ──
699
+ let readme = null;
700
+ const readmeNames = ['README.md', 'readme.md', 'README.txt', 'README', 'readme.markdown'];
701
+ for (const rn of readmeNames) {
702
+ const rPath = path.join(fullDir, rn);
703
+ try {
704
+ if (fs.existsSync(rPath)) {
705
+ const content = fs.readFileSync(rPath, 'utf-8');
706
+ readme = content.slice(0, 1000); // 只取前 1000 字符
707
+ break;
708
+ }
709
+ } catch { /* skip */ }
710
+ }
711
+
712
+ // ── 命名模式检测 ──
713
+ const namingPatterns = _detectNamingPatterns(fileNames);
714
+
715
+ // ── 轻量 Import 分析 ──
716
+ const imports = _extractImports(keyFiles.slice(0, 10), projectRoot, sampleLines, relDir);
717
+
718
+ // ── 头部注释提取 (从关键文件提取首段注释) ──
719
+ const headerComments = [];
720
+ for (const f of keyFiles.slice(0, 3)) {
721
+ const comment = _extractHeaderComment(path.join(projectRoot, f));
722
+ if (comment) {
723
+ headerComments.push(`${path.basename(f)}: ${comment}`);
724
+ }
725
+ }
726
+
727
+ // ── 功能推断 (复用已有 inferModulePurpose + 增强) ──
728
+ const purpose = inferModulePurpose(folderName, [], [], files);
729
+
730
+ return {
731
+ name: folderName,
732
+ relPath: relDir,
733
+ fileCount: files.length,
734
+ totalSize,
735
+ depth,
736
+ langBreakdown,
737
+ keyFiles,
738
+ fileNames,
739
+ readme,
740
+ purpose: purpose ? purpose : null,
741
+ imports,
742
+ entryPoints: [...new Set(entryPoints.map(f => path.basename(f)))],
743
+ namingPatterns,
744
+ headerComments,
745
+ };
746
+ }
747
+
748
+ /**
749
+ * 从文件名列表检测命名约定
750
+ * @private
751
+ * @param {string[]} fileNames - basename 列表
752
+ * @returns {string[]}
753
+ */
754
+ function _detectNamingPatterns(fileNames) {
755
+ const patterns = [];
756
+ const lower = fileNames.map(n => n.toLowerCase());
757
+
758
+ // 测试文件
759
+ const testFiles = lower.filter(n =>
760
+ n.startsWith('test_') || n.startsWith('test.') ||
761
+ n.endsWith('_test.go') || n.endsWith('.test.js') || n.endsWith('.test.ts') ||
762
+ n.endsWith('.spec.js') || n.endsWith('.spec.ts') || n.endsWith('_spec.rb') ||
763
+ n.startsWith('test') && n.includes('.')
764
+ );
765
+ if (testFiles.length > 0) {
766
+ patterns.push(`test files: ${testFiles.length}`);
767
+ }
768
+
769
+ // 常见后缀模式
770
+ const suffixes = {};
771
+ for (const name of fileNames) {
772
+ const base = path.basename(name, path.extname(name));
773
+ // 检测 CamelCase 后缀: UserController → Controller
774
+ const camelMatch = base.match(/([A-Z][a-z]+)$/);
775
+ if (camelMatch) {
776
+ const suffix = camelMatch[1];
777
+ suffixes[suffix] = (suffixes[suffix] || 0) + 1;
778
+ }
779
+ // 检测 snake_case 后缀: user_controller → controller
780
+ const snakeMatch = base.match(/_([a-z]+)$/);
781
+ if (snakeMatch) {
782
+ const suffix = snakeMatch[1];
783
+ suffixes[suffix] = (suffixes[suffix] || 0) + 1;
784
+ }
785
+ }
786
+
787
+ // 出现 ≥2 次的后缀视为命名约定
788
+ for (const [suffix, count] of Object.entries(suffixes).sort((a, b) => b[1] - a[1])) {
789
+ if (count >= 2) {
790
+ patterns.push(`*${suffix}: ${count}`);
791
+ }
792
+ }
793
+
794
+ return patterns.slice(0, 8);
795
+ }
796
+
797
+ /**
798
+ * 从文件顶部提取 import/require 语句,推断文件夹级依赖
799
+ * @private
800
+ */
801
+ function _extractImports(keyFiles, projectRoot, sampleLines, currentDir) {
802
+ const importTargets = new Set();
803
+
804
+ // Node.js / 常见运行时内置模块 — 不应计入项目文件夹依赖
805
+ const BUILTIN_MODULES = new Set([
806
+ 'fs', 'path', 'os', 'http', 'https', 'url', 'util', 'crypto', 'stream',
807
+ 'events', 'child_process', 'cluster', 'net', 'dns', 'tls', 'zlib',
808
+ 'readline', 'assert', 'buffer', 'querystring', 'string_decoder',
809
+ 'timers', 'tty', 'dgram', 'vm', 'worker_threads', 'perf_hooks',
810
+ 'async_hooks', 'v8', 'inspector', 'console', 'process', 'module',
811
+ // node: prefix 会被 firstSeg 拆出 "node" — 直接排除
812
+ 'node',
813
+ // 常见第三方包 (非项目目录)
814
+ 'react', 'vue', 'express', 'lodash', 'axios', 'moment', 'dayjs',
815
+ 'webpack', 'vite', 'jest', 'mocha', 'chai',
816
+ ]);
817
+
818
+ for (const relFile of keyFiles) {
819
+ try {
820
+ const fullPath = path.join(projectRoot, relFile);
821
+ const content = fs.readFileSync(fullPath, 'utf-8');
822
+ const lines = content.split('\n').slice(0, sampleLines);
823
+
824
+ for (const line of lines) {
825
+ for (const pattern of IMPORT_PATTERNS) {
826
+ const match = line.match(pattern);
827
+ if (match) {
828
+ // 取第一个非 undefined 捕获组
829
+ const target = match[1] || match[2];
830
+ if (target) {
831
+ // 跳过 node: 协议前缀 (Node.js 内置模块)
832
+ if (target.startsWith('node:')) continue;
833
+
834
+ // 解析相对路径 import → 文件夹名
835
+ if (target.startsWith('.') || target.startsWith('/')) {
836
+ const resolved = path.normalize(path.join(currentDir, target));
837
+ const topDir = resolved.split('/')[0];
838
+ if (topDir && topDir !== '.' && topDir !== '..' && topDir !== currentDir.split('/')[0]) {
839
+ importTargets.add(topDir);
840
+ }
841
+ } else {
842
+ // 绝对 import → 取第一段作为模块名
843
+ const firstSeg = target.split(/[/.]/)[0];
844
+ if (firstSeg && firstSeg.length > 1 && !BUILTIN_MODULES.has(firstSeg)) {
845
+ importTargets.add(firstSeg);
846
+ }
847
+ }
848
+ }
849
+ }
850
+ }
851
+ }
852
+ } catch { /* skip unreadable files */ }
853
+ }
854
+
855
+ return [...importTargets].slice(0, 20);
856
+ }
857
+
858
+ /**
859
+ * 提取文件头部注释 (第一个注释块)
860
+ * @private
861
+ */
862
+ function _extractHeaderComment(fullPath) {
863
+ try {
864
+ const content = fs.readFileSync(fullPath, 'utf-8');
865
+ const lines = content.split('\n').slice(0, 30);
866
+
867
+ // 尝试匹配多行注释 /** ... */ 或 /* ... */
868
+ const joined = lines.join('\n');
869
+ const blockMatch = joined.match(/\/\*\*?([\s\S]*?)\*\//);
870
+ if (blockMatch) {
871
+ const comment = blockMatch[1]
872
+ .split('\n')
873
+ .map(l => l.replace(/^\s*\*\s?/, '').trim())
874
+ .filter(l => l && !l.startsWith('@'))
875
+ .join(' ')
876
+ .slice(0, 200);
877
+ if (comment.length > 10) return comment;
878
+ }
879
+
880
+ // 尝试匹配 # 或 // 开头的连续行注释
881
+ const lineComments = [];
882
+ for (const line of lines) {
883
+ const stripped = line.trim();
884
+ if (stripped.startsWith('#') && !stripped.startsWith('#!') && !stripped.startsWith('#include')) {
885
+ lineComments.push(stripped.replace(/^#+\s*/, ''));
886
+ } else if (stripped.startsWith('//')) {
887
+ lineComments.push(stripped.replace(/^\/\/\s*/, ''));
888
+ } else if (stripped.startsWith('"""') || stripped.startsWith("'''")) {
889
+ // Python docstring
890
+ const docMatch = joined.match(/(?:"""|''')([\s\S]*?)(?:"""|''')/);
891
+ if (docMatch) {
892
+ return docMatch[1].trim().slice(0, 200);
893
+ }
894
+ } else if (lineComments.length > 0) {
895
+ break; // 注释块结束
896
+ }
897
+ }
898
+ if (lineComments.length > 0) {
899
+ const comment = lineComments.join(' ').slice(0, 200);
900
+ if (comment.length > 10) return comment;
901
+ }
902
+
903
+ return null;
904
+ } catch {
905
+ return null;
906
+ }
907
+ }