autosnippet 3.2.7 → 3.2.8

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 (60) hide show
  1. package/bin/cli.js +7 -0
  2. package/dashboard/dist/assets/index-D5jiDBQG.css +1 -0
  3. package/dashboard/dist/assets/{index-DfHY_3ln.js → index-e5OKj-Ni.js} +38 -38
  4. package/dashboard/dist/index.html +2 -2
  5. package/lib/cli/AiScanService.js +3 -3
  6. package/lib/core/AstAnalyzer.js +26 -4
  7. package/lib/core/analysis/CallEdgeResolver.js +402 -0
  8. package/lib/core/analysis/CallGraphAnalyzer.js +367 -0
  9. package/lib/core/analysis/CallSiteExtractor.js +629 -0
  10. package/lib/core/analysis/DataFlowInferrer.js +57 -0
  11. package/lib/core/analysis/ImportPathResolver.js +189 -0
  12. package/lib/core/analysis/ImportRecord.js +105 -0
  13. package/lib/core/analysis/SymbolTableBuilder.js +211 -0
  14. package/lib/core/ast/ProjectGraph.js +8 -0
  15. package/lib/core/ast/lang-dart.js +352 -5
  16. package/lib/core/ast/lang-go.js +212 -10
  17. package/lib/core/ast/lang-java.js +205 -1
  18. package/lib/core/ast/lang-kotlin.js +330 -1
  19. package/lib/core/ast/lang-python.js +31 -2
  20. package/lib/core/ast/lang-rust.js +284 -3
  21. package/lib/core/ast/lang-swift.js +180 -1
  22. package/lib/core/ast/lang-typescript.js +290 -1
  23. package/lib/external/mcp/McpServer.js +1 -0
  24. package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +21 -0
  25. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +5 -4
  26. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -1
  27. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +70 -4
  28. package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +95 -1
  29. package/lib/external/mcp/handlers/bootstrap-external.js +9 -2
  30. package/lib/external/mcp/handlers/bootstrap-internal.js +17 -6
  31. package/lib/external/mcp/handlers/consolidated.js +9 -0
  32. package/lib/external/mcp/handlers/guard.js +3 -3
  33. package/lib/external/mcp/handlers/structure.js +62 -0
  34. package/lib/external/mcp/handlers/wiki-external.js +66 -3
  35. package/lib/external/mcp/tools.js +36 -1
  36. package/lib/http/routes/remote.js +15 -15
  37. package/lib/injection/ServiceContainer.js +6 -11
  38. package/lib/platform/ios/index.js +2 -2
  39. package/lib/platform/ios/spm/PackageSwiftParser.js +14 -3
  40. package/lib/platform/ios/spm/SpmDiscoverer.js +123 -17
  41. package/lib/platform/ios/spm/{SpmService.js → SpmHelper.js} +43 -675
  42. package/lib/platform/ios/xcode/XcodeWriteUtils.js +1 -1
  43. package/lib/service/chat/ChatAgent.js +1 -1
  44. package/lib/service/chat/ChatAgentPrompts.js +13 -1
  45. package/lib/service/chat/ExplorationTracker.js +52 -8
  46. package/lib/service/chat/HandoffProtocol.js +19 -1
  47. package/lib/service/chat/WorkingMemory.js +3 -1
  48. package/lib/service/chat/memory/ActiveContext.js +3 -1
  49. package/lib/service/chat/memory/SessionStore.js +4 -3
  50. package/lib/service/chat/tools/ast-graph.js +229 -32
  51. package/lib/service/chat/tools/index.js +6 -1
  52. package/lib/service/chat/tools/infrastructure.js +5 -0
  53. package/lib/service/cursor/CursorDeliveryPipeline.js +167 -1
  54. package/lib/service/knowledge/CodeEntityGraph.js +327 -2
  55. package/lib/service/knowledge/KnowledgeService.js +5 -1
  56. package/lib/service/module/ModuleService.js +9 -0
  57. package/lib/service/wiki/WikiGenerator.js +1 -1
  58. package/lib/shared/PathGuard.js +1 -1
  59. package/package.json +1 -1
  60. package/dashboard/dist/assets/index-BaGY7kJI.css +0 -1
@@ -4,8 +4,13 @@
4
4
  *
5
5
  * 提取: class, interface, type alias, enum, function, method, property, import, export
6
6
  * 模式检测: Singleton, Factory, Observer, React Hook/Component, Middleware, Decorator
7
+ *
8
+ * Phase 5: 新增 ImportRecord 结构化导入 + extractCallSites 调用点提取
7
9
  */
8
10
 
11
+ import { ImportRecord } from '../analysis/ImportRecord.js';
12
+ import { extractCallSitesTS } from '../analysis/CallSiteExtractor.js';
13
+
9
14
  function walkTypeScript(root, ctx) {
10
15
  _walkTSNode(root, ctx, null);
11
16
  }
@@ -20,7 +25,9 @@ function _walkTSNode(node, ctx, parentClassName) {
20
25
  (c) => c.type === 'string' || c.type === 'string_fragment'
21
26
  );
22
27
  if (source) {
23
- ctx.imports.push(source.text.replace(/^['"]|['"]$/g, ''));
28
+ const importPath = source.text.replace(/^['"]|['"]$/g, '');
29
+ const importMeta = _parseImportClause(child);
30
+ ctx.imports.push(new ImportRecord(importPath, importMeta));
24
31
  }
25
32
  break;
26
33
  }
@@ -123,6 +130,12 @@ function _walkTSClassBody(body, ctx, className) {
123
130
  case 'method_definition': {
124
131
  const m = _parseTSMethod(child, className);
125
132
  ctx.methods.push(m);
133
+
134
+ // Phase 5.3: Extract constructor parameter properties (DI pattern)
135
+ if (m.name === 'constructor') {
136
+ const constructorProps = _extractTSConstructorProperties(child, className);
137
+ ctx.properties.push(...constructorProps);
138
+ }
126
139
  break;
127
140
  }
128
141
 
@@ -287,15 +300,111 @@ function _parseTSProperty(node, className) {
287
300
  const isStatic = node.text.trimStart().startsWith('static');
288
301
  const isReadonly = node.text.includes('readonly');
289
302
 
303
+ // Phase 5.3: Extract type annotation for DI/RTA resolution
304
+ const typeAnnotation = _extractTypeAnnotation(node);
305
+
306
+ // Phase 5.3: Extract decorators on properties (e.g. @Inject)
307
+ const decorators = [];
308
+ for (const child of node.namedChildren) {
309
+ if (child.type === 'decorator') {
310
+ decorators.push(child.text);
311
+ }
312
+ }
313
+
290
314
  return {
291
315
  name,
292
316
  className,
293
317
  isStatic,
294
318
  isReadonly,
319
+ typeAnnotation,
320
+ decorators: decorators.length > 0 ? decorators : undefined,
295
321
  line: node.startPosition.row + 1,
296
322
  };
297
323
  }
298
324
 
325
+ /**
326
+ * Phase 5.3: Extract type name from a type_annotation node
327
+ * Handles: type_identifier, generic_type, nested_type_identifier, union_type
328
+ * Strips generics: UserRepo<T> → UserRepo
329
+ *
330
+ * @param {object} parentNode — AST node that may contain a type_annotation child
331
+ * @returns {string|null}
332
+ */
333
+ function _extractTypeAnnotation(parentNode) {
334
+ const typeAnnotNode = parentNode.namedChildren.find((c) => c.type === 'type_annotation');
335
+ if (!typeAnnotNode) return null;
336
+
337
+ const typeNode = typeAnnotNode.namedChildren.find(
338
+ (c) =>
339
+ c.type === 'type_identifier' ||
340
+ c.type === 'generic_type' ||
341
+ c.type === 'nested_type_identifier'
342
+ );
343
+ if (!typeNode) return null;
344
+
345
+ // Strip generics: Repository<User> → Repository
346
+ const text = typeNode.text;
347
+ const bracketIdx = text.indexOf('<');
348
+ return bracketIdx > 0 ? text.slice(0, bracketIdx) : text;
349
+ }
350
+
351
+ /**
352
+ * Phase 5.3: Extract constructor parameter properties (TypeScript DI pattern)
353
+ *
354
+ * TypeScript shorthand: constructor(private userRepo: UserRepo)
355
+ * creates a class property `userRepo` with type `UserRepo`
356
+ *
357
+ * @param {object} constructorNode — method_definition node with name "constructor"
358
+ * @param {string} className
359
+ * @returns {Array} — property objects
360
+ */
361
+ function _extractTSConstructorProperties(constructorNode, className) {
362
+ const properties = [];
363
+ const params = constructorNode.namedChildren.find((c) => c.type === 'formal_parameters');
364
+ if (!params) return properties;
365
+
366
+ for (const param of params.namedChildren) {
367
+ if (param.type !== 'required_parameter' && param.type !== 'optional_parameter') continue;
368
+
369
+ // Check for accessibility modifier (public, private, protected) or readonly
370
+ // These turn constructor params into class properties
371
+ const hasAccessibility = param.namedChildren.some(
372
+ (c) => c.type === 'accessibility_modifier' || c.type === 'override_modifier'
373
+ );
374
+ const hasReadonly = param.text.includes('readonly');
375
+
376
+ if (!hasAccessibility && !hasReadonly) continue; // Not a property declaration
377
+
378
+ const nameNode = param.namedChildren.find((c) => c.type === 'identifier');
379
+ const name = nameNode?.text;
380
+ if (!name) continue;
381
+
382
+ // Extract type annotation
383
+ const typeAnnotation = _extractTypeAnnotation(param);
384
+
385
+ // Extract decorators on parameter (e.g. @Inject)
386
+ const decorators = [];
387
+ for (const child of param.namedChildren) {
388
+ if (child.type === 'decorator') {
389
+ decorators.push(child.text);
390
+ }
391
+ }
392
+
393
+ properties.push({
394
+ name,
395
+ className,
396
+ isStatic: false,
397
+ isReadonly: hasReadonly,
398
+ typeAnnotation,
399
+ isConstructorParam: true,
400
+ decorators: decorators.length > 0 ? decorators : undefined,
401
+ line: param.startPosition.row + 1,
402
+ });
403
+ }
404
+
405
+ return properties;
406
+ }
407
+
299
408
  function _parseTSVariableDecl(node, ctx, parentClassName) {
300
409
  for (const child of node.namedChildren) {
301
410
  if (child.type === 'variable_declarator') {
@@ -315,9 +424,187 @@ function _parseTSVariableDecl(node, ctx, parentClassName) {
315
424
  line: child.startPosition.row + 1,
316
425
  kind: 'definition',
317
426
  });
427
+ continue;
428
+ }
429
+
430
+ // CJS require(): const x = require('path') / const { a, b } = require('path')
431
+ // Dynamic import(): const mod = await import('./module')
432
+ const callNode = child.namedChildren.find((c) => c.type === 'call_expression');
433
+ if (callNode) {
434
+ const requireImport = _parseCJSRequire(callNode, child);
435
+ if (requireImport) {
436
+ ctx.imports.push(requireImport);
437
+ continue;
438
+ }
439
+ const dynamicImport = _parseDynamicImport(callNode, child);
440
+ if (dynamicImport) {
441
+ ctx.imports.push(dynamicImport);
442
+ continue;
443
+ }
444
+ }
445
+ // await import() — await wraps the call_expression
446
+ const awaitNode = child.namedChildren.find((c) => c.type === 'await_expression');
447
+ if (awaitNode) {
448
+ const awaitedCall = awaitNode.namedChildren.find((c) => c.type === 'call_expression');
449
+ if (awaitedCall) {
450
+ const dynamicImport = _parseDynamicImport(awaitedCall, child);
451
+ if (dynamicImport) {
452
+ ctx.imports.push(dynamicImport);
453
+ }
454
+ }
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ // ── TS/JS Import 解析 ──
461
+
462
+ /**
463
+ * 从 import_statement 节点解析导入子句的结构化信息
464
+ *
465
+ * @param {TreeSitterNode} importNode — import_statement 节点
466
+ * @returns {{ symbols: string[], kind: string, alias: string|null, isTypeOnly: boolean }}
467
+ */
468
+ function _parseImportClause(importNode) {
469
+ const symbols = [];
470
+ let kind = 'side-effect';
471
+ let alias = null;
472
+ let isTypeOnly = false;
473
+
474
+ // 检查 type-only import: import type { ... }
475
+ if (importNode.text.trimStart().startsWith('import type')) {
476
+ isTypeOnly = true;
477
+ }
478
+
479
+ for (const child of importNode.namedChildren) {
480
+ if (child.type === 'import_clause') {
481
+ for (const clauseChild of child.namedChildren) {
482
+ if (clauseChild.type === 'identifier') {
483
+ // default import: import Foo from '...'
484
+ symbols.push(clauseChild.text);
485
+ kind = 'default';
486
+ } else if (clauseChild.type === 'named_imports') {
487
+ // named imports: import { A, B as C } from '...'
488
+ kind = 'named';
489
+ for (const specifier of clauseChild.namedChildren) {
490
+ if (specifier.type === 'import_specifier') {
491
+ // 收集 specifier 中的所有 identifier / type_identifier
492
+ const identifiers = specifier.namedChildren.filter(
493
+ (c) => c.type === 'identifier' || c.type === 'type_identifier'
494
+ );
495
+ // import { A as B } → identifiers = [A, B] → push B (本地名)
496
+ // import { A } → identifiers = [A] → push A
497
+ if (identifiers.length > 0) {
498
+ symbols.push(identifiers[identifiers.length - 1].text);
499
+ }
500
+ }
501
+ }
502
+ } else if (clauseChild.type === 'namespace_import') {
503
+ // namespace import: import * as M from '...'
504
+ kind = 'namespace';
505
+ symbols.push('*');
506
+ const aliasNode = clauseChild.namedChildren.find((c) => c.type === 'identifier');
507
+ if (aliasNode) {
508
+ alias = aliasNode.text;
509
+ }
510
+ }
511
+ }
512
+ }
513
+ }
514
+
515
+ return { symbols, kind, alias, isTypeOnly };
516
+ }
517
+
518
+ /**
519
+ * 解析 CJS require() 调用: const x = require('path') / const { a, b } = require('path')
520
+ *
521
+ * @param {TreeSitterNode} callNode — call_expression 节点
522
+ * @param {TreeSitterNode} declaratorNode — variable_declarator 节点 (包含 lhs 绑定)
523
+ * @returns {ImportRecord|null}
524
+ */
525
+ function _parseCJSRequire(callNode, declaratorNode) {
526
+ // 检查 callee 是否为 'require'
527
+ const callee = callNode.namedChildren[0];
528
+ if (!callee || callee.type !== 'identifier' || callee.text !== 'require') return null;
529
+
530
+ // 提取 require 参数中的路径字符串
531
+ const args = callNode.namedChildren.find((c) => c.type === 'arguments');
532
+ if (!args || args.namedChildCount === 0) return null;
533
+
534
+ const firstArg = args.namedChildren[0];
535
+ if (!firstArg || (firstArg.type !== 'string' && firstArg.type !== 'template_string')) return null;
536
+
537
+ const importPath = firstArg.text.replace(/^['"`]|['"`]$/g, '');
538
+ if (!importPath) return null;
539
+
540
+ // 解析 lhs 绑定模式
541
+ const lhs = declaratorNode.namedChildren[0]; // identifier or object_pattern
542
+ if (!lhs) return new ImportRecord(importPath, { symbols: [], kind: 'side-effect' });
543
+
544
+ if (lhs.type === 'identifier') {
545
+ // const express = require('express') → namespace import
546
+ return new ImportRecord(importPath, {
547
+ symbols: ['*'],
548
+ kind: 'namespace',
549
+ alias: lhs.text,
550
+ });
551
+ }
552
+
553
+ if (lhs.type === 'object_pattern') {
554
+ // const { readFile, writeFile } = require('fs')
555
+ const symbols = [];
556
+ for (const prop of lhs.namedChildren) {
557
+ if (prop.type === 'shorthand_property_identifier_pattern' || prop.type === 'shorthand_property_identifier') {
558
+ symbols.push(prop.text);
559
+ } else if (prop.type === 'pair_pattern' || prop.type === 'pair') {
560
+ // { readFile: rf } → 使用本地名 rf
561
+ const identifiers = prop.namedChildren.filter((c) => c.type === 'identifier');
562
+ if (identifiers.length > 0) {
563
+ symbols.push(identifiers[identifiers.length - 1].text);
564
+ }
318
565
  }
319
566
  }
567
+ return new ImportRecord(importPath, {
568
+ symbols: symbols.length > 0 ? symbols : ['*'],
569
+ kind: symbols.length > 0 ? 'named' : 'namespace',
570
+ });
320
571
  }
572
+
573
+ // 其他 lhs 模式 (如数组解构), 作为 side-effect 处理
574
+ return new ImportRecord(importPath, { symbols: [], kind: 'side-effect' });
575
+ }
576
+
577
+ /**
578
+ * 解析动态 import() 表达式: const mod = await import('./module')
579
+ *
580
+ * @param {TreeSitterNode} callNode — call_expression 节点
581
+ * @param {TreeSitterNode} declaratorNode — variable_declarator 节点
582
+ * @returns {ImportRecord|null}
583
+ */
584
+ function _parseDynamicImport(callNode, declaratorNode) {
585
+ // 动态 import() 在 tree-sitter 中解析为 call_expression, callee 是 'import'
586
+ const callee = callNode.namedChildren[0];
587
+ if (!callee) return null;
588
+ // tree-sitter 可能将 import() 解析为 identifier('import') 或 special node
589
+ if (callee.text !== 'import') return null;
590
+
591
+ const args = callNode.namedChildren.find((c) => c.type === 'arguments');
592
+ if (!args || args.namedChildCount === 0) return null;
593
+
594
+ const firstArg = args.namedChildren[0];
595
+ if (!firstArg || (firstArg.type !== 'string' && firstArg.type !== 'template_string')) return null;
596
+
597
+ const importPath = firstArg.text.replace(/^['"`]|['"`]$/g, '');
598
+ if (!importPath) return null;
599
+
600
+ const lhs = declaratorNode?.namedChildren?.[0];
601
+ const alias = lhs?.type === 'identifier' ? lhs.text : null;
602
+
603
+ return new ImportRecord(importPath, {
604
+ symbols: ['*'],
605
+ kind: 'dynamic',
606
+ alias,
607
+ });
321
608
  }
322
609
 
323
610
  // ── TS/JS 模式检测 ──
@@ -477,6 +764,7 @@ export const plugin = {
477
764
  getGrammar,
478
765
  walk: walkTypeScript,
479
766
  detectPatterns: detectTSPatterns,
767
+ extractCallSites: extractCallSitesTS,
480
768
  extensions: ['.ts'],
481
769
  };
482
770
 
@@ -493,5 +781,6 @@ export const tsxPlugin = {
493
781
  getGrammar: getTsxGrammar,
494
782
  walk: walkTypeScript,
495
783
  detectPatterns: detectTSPatterns,
784
+ extractCallSites: extractCallSitesTS,
496
785
  extensions: ['.tsx'],
497
786
  };
@@ -353,6 +353,7 @@ export class McpServer {
353
353
  autosnippet_search: (ctx, args) => consolidated.consolidatedSearch(ctx, args),
354
354
  autosnippet_knowledge: (ctx, args) => consolidated.consolidatedKnowledge(ctx, args),
355
355
  autosnippet_structure: (ctx, args) => consolidated.consolidatedStructure(ctx, args),
356
+ autosnippet_call_context: (ctx, args) => consolidated.consolidatedCallContext(ctx, args),
356
357
  autosnippet_graph: (ctx, args) => consolidated.consolidatedGraph(ctx, args),
357
358
  autosnippet_guard: (ctx, args) => consolidated.consolidatedGuard(ctx, args),
358
359
  autosnippet_submit_knowledge: (ctx, args) => consolidated.enhancedSubmitKnowledge(ctx, args),
@@ -551,6 +551,20 @@ function summarizeEntityGraph(codeEntityResult) {
551
551
  };
552
552
  }
553
553
 
554
+ /**
555
+ * 压缩 Call Graph 结果
556
+ * @param {object|null} callGraphResult — CodeEntityGraph.populateCallGraph() 返回值
557
+ * @returns {object|null}
558
+ */
559
+ function summarizeCallGraph(callGraphResult) {
560
+ if (!callGraphResult) return null;
561
+ return {
562
+ methodEntities: callGraphResult.entitiesUpserted || 0,
563
+ callEdges: callGraphResult.edgesCreated || 0,
564
+ durationMs: callGraphResult.durationMs || 0,
565
+ };
566
+ }
567
+
554
568
  /**
555
569
  * 压缩 Guard 审计结果
556
570
  */
@@ -673,6 +687,7 @@ export function buildMissionBriefing({
673
687
  projectMeta,
674
688
  astData,
675
689
  codeEntityResult,
690
+ callGraphResult,
676
691
  depGraphData,
677
692
  guardAudit,
678
693
  targets,
@@ -680,6 +695,7 @@ export function buildMissionBriefing({
680
695
  session,
681
696
  languageExtension, // §7.1: 语言扩展(反模式、Guard 规则、Agent 注意事项)
682
697
  incrementalPlan, // §7.3: 增量 Bootstrap 评估结果
698
+ languageStats, // §7.4: 完整语言分布统计
683
699
  }) {
684
700
  const scheduler = new TierScheduler();
685
701
 
@@ -720,6 +736,8 @@ export function buildMissionBriefing({
720
736
 
721
737
  codeEntityGraph: summarizeEntityGraph(codeEntityResult),
722
738
 
739
+ callGraph: summarizeCallGraph(callGraphResult),
740
+
723
741
  dependencyGraph: depGraphData
724
742
  ? {
725
743
  nodes: (depGraphData.nodes || []).map((n) => ({
@@ -750,6 +768,9 @@ export function buildMissionBriefing({
750
768
  example,
751
769
  },
752
770
 
771
+ // 完整语言统计(按文件扩展名计数)
772
+ languageStats: languageStats || null,
773
+
753
774
  executionPlan: buildExecutionPlan(activeDimensions),
754
775
 
755
776
  session: session.toJSON(),
@@ -133,10 +133,10 @@ export class EpisodicMemory {
133
133
  */
134
134
  storeDimensionReport(dimId, report) {
135
135
  // findings 统一形状: { finding: string, evidence: string, importance: number }
136
- // 源头 buildAnalysisArtifact() ActiveContext.distill() 已保证一致
136
+ // P0 Fix: evidence 可能是 array/object,强制 string
137
137
  const findings = (report.findings || []).map((f) => ({
138
138
  finding: f.finding || '',
139
- evidence: f.evidence || '',
139
+ evidence: typeof f.evidence === 'string' ? f.evidence : Array.isArray(f.evidence) ? f.evidence.join(', ') : f.evidence ? String(f.evidence) : '',
140
140
  importance: f.importance || 5,
141
141
  }));
142
142
 
@@ -154,7 +154,8 @@ export class EpisodicMemory {
154
154
  // 自动提取文件级 Evidence
155
155
  for (const f of findings) {
156
156
  if (f.evidence) {
157
- const filePath = f.evidence.split(':')[0]; // "file.m:123" → "file.m"
157
+ const ev = typeof f.evidence === 'string' ? f.evidence : String(f.evidence);
158
+ const filePath = ev.split(':')[0]; // "file.m:123" → "file.m"
158
159
  this.addEvidence(filePath, {
159
160
  dimId,
160
161
  finding: f.finding,
@@ -418,7 +419,7 @@ export class EpisodicMemory {
418
419
  if ((!findings || findings.length === 0) && report.workingMemoryDistilled?.keyFindings) {
419
420
  findings = report.workingMemoryDistilled.keyFindings.map((f) => ({
420
421
  finding: f.finding || '',
421
- evidence: f.evidence || '',
422
+ evidence: typeof f.evidence === 'string' ? f.evidence : Array.isArray(f.evidence) ? f.evidence.join(', ') : f.evidence ? String(f.evidence) : '',
422
423
  importance: f.importance || 5,
423
424
  }));
424
425
  }
@@ -115,7 +115,8 @@ export function buildTierReflection(tierIndex, tierResults, sessionStore) {
115
115
  for (const f of allFindings) {
116
116
  // 统计文件引用频率
117
117
  if (f.evidence) {
118
- const file = f.evidence.split(':')[0];
118
+ const ev = typeof f.evidence === 'string' ? f.evidence : String(f.evidence);
119
+ const file = ev.split(':')[0];
119
120
  if (file) {
120
121
  fileMentions[file] = (fileMentions[file] || 0) + 1;
121
122
  }
@@ -594,17 +594,55 @@ export async function fillDimensionsV3(fillContext) {
594
594
  producerResult,
595
595
  };
596
596
 
597
+ // v5.1: 当 analysisText 过短(force-exit 时 AI 仅输出 digest 被清洗后仅剩 50-80 chars)
598
+ // 但有足够的结构化发现时,从 findings 合成补充文本,避免 Producer 被 100 char 门控拦截
599
+ if (needsCandidates && analysisReport.analysisText.length < 100) {
600
+ const findings = analysisReport.findings || [];
601
+ if (findings.length >= 3) {
602
+ const dimLabel = dimConfig.label || dimId;
603
+ const synthesized = [
604
+ `## ${dimLabel}`,
605
+ '',
606
+ analysisReport.analysisText.trim(),
607
+ '',
608
+ '### 关键发现',
609
+ '',
610
+ ...findings.slice(0, 10).map((f, i) => {
611
+ const text = typeof f === 'string' ? f : f.finding;
612
+ return `${i + 1}. ${text}`;
613
+ }),
614
+ ];
615
+ // 追加探索记录 (如果有)
616
+ const dimReport = sessionStore.getDimensionReport(dimId);
617
+ const memDistilled = dimReport?.workingMemoryDistilled;
618
+ if (memDistilled?.toolCallSummary?.length > 0) {
619
+ synthesized.push('', '### 探索记录', '');
620
+ for (const s of memDistilled.toolCallSummary.slice(0, 10)) {
621
+ synthesized.push(`- ${s}`);
622
+ }
623
+ }
624
+ const originalLen = analysisReport.analysisText.length;
625
+ analysisReport.analysisText = synthesized.join('\n');
626
+ logger.info(
627
+ `[Bootstrap-v3] analysisText 补强 "${dimId}": ${originalLen} → ${analysisReport.analysisText.length} chars ` +
628
+ `(from ${findings.length} findings)`
629
+ );
630
+ }
631
+ }
632
+
597
633
  if (needsCandidates && analysisReport.analysisText.length >= 100) {
598
634
  try {
599
635
  // v5.0: 为 Producer 创建独立作用域
600
636
  const producerScopeId = `${dimId}:producer`;
601
637
  memoryCoordinator.createDimensionScope(producerScopeId);
602
638
 
639
+ const producerPromise = producerAgent.produce(analysisReport, dimConfig, projectInfo, {
640
+ sessionId,
641
+ memoryCoordinator,
642
+ });
643
+
603
644
  producerResult = await Promise.race([
604
- producerAgent.produce(analysisReport, dimConfig, projectInfo, {
605
- sessionId,
606
- memoryCoordinator,
607
- }),
645
+ producerPromise,
608
646
  new Promise((_, reject) =>
609
647
  setTimeout(() => reject(new Error(`Producer timeout for "${dimId}"`)), 180_000)
610
648
  ),
@@ -621,6 +659,32 @@ export async function fillDimensionsV3(fillContext) {
621
659
  `[Bootstrap-v3] Producer "${dimId}" failed: ${producerErr.message} — Analyst result preserved for Skill generation`
622
660
  );
623
661
  candidateResults.errors.push({ dimId, error: `Producer: ${producerErr.message}` });
662
+
663
+ // v5.1: 超时后异步监听实际结果,避免 ghost candidates 永远不被计数
664
+ if (producerErr.message.includes('timeout')) {
665
+ const dimIdRef = dimId;
666
+ // producerPromise 仍在后台执行 — 监听完成后更新统计
667
+ // biome-ignore lint: 故意 fire-and-forget
668
+ producerPromise
669
+ ?.then((actualResult) => {
670
+ const count = actualResult?.candidateCount || 0;
671
+ if (count > 0) {
672
+ logger.info(
673
+ `[Bootstrap-v3] Producer "${dimIdRef}" completed post-timeout: ${count} candidates (ghost → reconciled)`
674
+ );
675
+ if (dimensionStats[dimIdRef]) {
676
+ dimensionStats[dimIdRef].candidateCount = count;
677
+ }
678
+ candidateResults.created += count;
679
+ dimensionCandidates[dimIdRef].producerResult = actualResult;
680
+ }
681
+ })
682
+ .catch((finalErr) => {
683
+ logger.warn(
684
+ `[Bootstrap-v3] Producer "${dimIdRef}" also failed post-timeout: ${finalErr.message}`
685
+ );
686
+ });
687
+ }
624
688
  }
625
689
  }
626
690
 
@@ -656,6 +720,8 @@ export async function fillDimensionsV3(fillContext) {
656
720
  type: needsCandidates ? 'candidate' : 'skill',
657
721
  extracted: producerResult.candidateCount,
658
722
  created: producerResult.candidateCount,
723
+ // v5.1: 标记 Skill 待生成,Dashboard 可据此避免显示 "无匹配内容"
724
+ skillPending: dimConfig.skillWorthy && producerResult.candidateCount === 0,
659
725
  status: 'v3-complete',
660
726
  durationMs: Date.now() - dimStartTime,
661
727
  toolCallCount:
@@ -271,6 +271,91 @@ export async function runPhase1_6_EntityGraph(astProjectSummary, projectRoot, co
271
271
 
272
272
  // ── Phase 2: 依赖关系 ──────────────────────────────────────
273
273
 
274
+ /**
275
+ * Phase 1.7: 跨文件调用图分析 (Phase 5)
276
+ *
277
+ * 从 AST 的 callSites 构建全局调用图并写入 CodeEntityGraph。
278
+ *
279
+ * @param {object|null} astProjectSummary — AST 分析结果 (含 fileSummaries[].callSites)
280
+ * @param {string} projectRoot
281
+ * @param {object} container — ServiceContainer
282
+ * @param {object} logger
283
+ * @param {object} [incrementalOpts] — 增量分析选项
284
+ * @param {string[]} [incrementalOpts.changedFiles] — 变更文件的相对路径
285
+ * @returns {Promise<{ callGraphResult: object|null, warnings: string[] }>}
286
+ */
287
+ export async function runPhase1_7_CallGraph(astProjectSummary, projectRoot, container, logger, incrementalOpts = null) {
288
+ const warnings = [];
289
+ let callGraphResult = null;
290
+
291
+ if (!astProjectSummary?.fileSummaries?.length) {
292
+ return { callGraphResult, warnings };
293
+ }
294
+
295
+ // 检查是否有 callSites 数据 (Phase 5 提取)
296
+ const hasCallSites = astProjectSummary.fileSummaries.some(
297
+ (f) => f.callSites && f.callSites.length > 0
298
+ );
299
+ if (!hasCallSites) {
300
+ logger.info('[Bootstrap] Call Graph skipped: no call sites extracted');
301
+ return { callGraphResult, warnings };
302
+ }
303
+
304
+ try {
305
+ const { CallGraphAnalyzer } = await import('../../../../../core/analysis/CallGraphAnalyzer.js');
306
+ const { CodeEntityGraph } = await import('../../../../../service/knowledge/CodeEntityGraph.js');
307
+
308
+ const analyzer = new CallGraphAnalyzer(projectRoot);
309
+ const changedFiles = incrementalOpts?.changedFiles;
310
+ const isIncremental = changedFiles?.length > 0 && changedFiles.length <= 10;
311
+
312
+ // Phase 5 分析 (带超时保护 + 渐进式 partial result)
313
+ const result = isIncremental
314
+ ? await analyzer.analyzeIncremental(astProjectSummary, changedFiles, {
315
+ timeout: 15_000,
316
+ maxCallSitesPerFile: 500,
317
+ minConfidence: 0.5,
318
+ })
319
+ : await analyzer.analyze(astProjectSummary, {
320
+ timeout: 15_000,
321
+ maxCallSitesPerFile: 500,
322
+ minConfidence: 0.5,
323
+ });
324
+
325
+ // 写入 CodeEntityGraph
326
+ const db = container.get('database');
327
+ if (db && result && result.callEdges.length > 0) {
328
+ const ceg = new CodeEntityGraph(db, { projectRoot });
329
+
330
+ // 增量模式: 先删除变更文件的旧边
331
+ if (isIncremental) {
332
+ ceg.clearCallGraphForFiles(changedFiles);
333
+ }
334
+
335
+ callGraphResult = ceg.populateCallGraph(result.callEdges, result.dataFlowEdges);
336
+
337
+ const partialTag = result.stats.partial ? ' [partial]' : '';
338
+ const incrTag = isIncremental ? ' [incremental]' : '';
339
+ logger.info(
340
+ `[Bootstrap] Call Graph${incrTag}${partialTag}: ${result.callEdges.length} call edges, ` +
341
+ `${result.dataFlowEdges.length} data flow edges, ` +
342
+ `resolution rate: ${(result.stats.resolvedRate * 100).toFixed(1)}%`
343
+ );
344
+ } else if (result) {
345
+ logger.info(
346
+ `[Bootstrap] Call Graph: ${result.stats.totalCallSites} call sites, 0 resolved edges`
347
+ );
348
+ }
349
+ } catch (e) {
350
+ logger.warn(`[Bootstrap] Call Graph failed (degraded): ${e.message}`);
351
+ warnings.push(`Call Graph failed: ${e.message}`);
352
+ }
353
+
354
+ return { callGraphResult, warnings };
355
+ }
356
+
357
+ // ── Phase 2: 依赖关系 ──────────────────────────────────────
358
+
274
359
  /**
275
360
  * Phase 2: 获取依赖图并写入 knowledge_edges
276
361
  *
@@ -598,7 +683,15 @@ export async function runAllPhases(projectRoot, ctx, options = {}) {
598
683
  phase1_5.astProjectSummary, projectRoot, ctx.container, ctx.logger
599
684
  );
600
685
  warnings.push(...phase1_6.warnings);
601
- if (report) report.phases.entityGraph = { entityCount: phase1_6.codeEntityResult?.entities?.length || 0, ms: Date.now() - p16Start };
686
+ if (report) report.phases.entityGraph = { entityCount: phase1_6.codeEntityResult?.entitiesUpserted || 0, edgeCount: phase1_6.codeEntityResult?.edgesCreated || 0, ms: Date.now() - p16Start };
687
+
688
+ // ── Phase 1.7: Call Graph (Phase 5) ──
689
+ const p17Start = Date.now();
690
+ const phase1_7 = await runPhase1_7_CallGraph(
691
+ phase1_5.astProjectSummary, projectRoot, ctx.container, ctx.logger
692
+ );
693
+ warnings.push(...phase1_7.warnings);
694
+ if (report) report.phases.callGraph = { result: phase1_7.callGraphResult, ms: Date.now() - p17Start };
602
695
 
603
696
  // ── Phase 2: 依赖图 ──
604
697
  const p2Start = Date.now();
@@ -659,6 +752,7 @@ export async function runAllPhases(projectRoot, ctx, options = {}) {
659
752
  astProjectSummary: phase1_5.astProjectSummary,
660
753
  astContext: phase1_5.astContext,
661
754
  codeEntityResult: phase1_6.codeEntityResult,
755
+ callGraphResult: phase1_7.callGraphResult,
662
756
  depGraphData: phase2.depGraphData,
663
757
  depEdgesWritten: phase2.depEdgesWritten,
664
758
  guardAudit: finalGuardAudit,