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.
- package/bin/cli.js +7 -0
- package/dashboard/dist/assets/index-D5jiDBQG.css +1 -0
- package/dashboard/dist/assets/{index-DfHY_3ln.js → index-e5OKj-Ni.js} +38 -38
- package/dashboard/dist/index.html +2 -2
- package/lib/cli/AiScanService.js +3 -3
- package/lib/core/AstAnalyzer.js +26 -4
- package/lib/core/analysis/CallEdgeResolver.js +402 -0
- package/lib/core/analysis/CallGraphAnalyzer.js +367 -0
- package/lib/core/analysis/CallSiteExtractor.js +629 -0
- package/lib/core/analysis/DataFlowInferrer.js +57 -0
- package/lib/core/analysis/ImportPathResolver.js +189 -0
- package/lib/core/analysis/ImportRecord.js +105 -0
- package/lib/core/analysis/SymbolTableBuilder.js +211 -0
- package/lib/core/ast/ProjectGraph.js +8 -0
- package/lib/core/ast/lang-dart.js +352 -5
- package/lib/core/ast/lang-go.js +212 -10
- package/lib/core/ast/lang-java.js +205 -1
- package/lib/core/ast/lang-kotlin.js +330 -1
- package/lib/core/ast/lang-python.js +31 -2
- package/lib/core/ast/lang-rust.js +284 -3
- package/lib/core/ast/lang-swift.js +180 -1
- package/lib/core/ast/lang-typescript.js +290 -1
- package/lib/external/mcp/McpServer.js +1 -0
- package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +21 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +5 -4
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +70 -4
- package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +95 -1
- package/lib/external/mcp/handlers/bootstrap-external.js +9 -2
- package/lib/external/mcp/handlers/bootstrap-internal.js +17 -6
- package/lib/external/mcp/handlers/consolidated.js +9 -0
- package/lib/external/mcp/handlers/guard.js +3 -3
- package/lib/external/mcp/handlers/structure.js +62 -0
- package/lib/external/mcp/handlers/wiki-external.js +66 -3
- package/lib/external/mcp/tools.js +36 -1
- package/lib/http/routes/remote.js +15 -15
- package/lib/injection/ServiceContainer.js +6 -11
- package/lib/platform/ios/index.js +2 -2
- package/lib/platform/ios/spm/PackageSwiftParser.js +14 -3
- package/lib/platform/ios/spm/SpmDiscoverer.js +123 -17
- package/lib/platform/ios/spm/{SpmService.js → SpmHelper.js} +43 -675
- package/lib/platform/ios/xcode/XcodeWriteUtils.js +1 -1
- package/lib/service/chat/ChatAgent.js +1 -1
- package/lib/service/chat/ChatAgentPrompts.js +13 -1
- package/lib/service/chat/ExplorationTracker.js +52 -8
- package/lib/service/chat/HandoffProtocol.js +19 -1
- package/lib/service/chat/WorkingMemory.js +3 -1
- package/lib/service/chat/memory/ActiveContext.js +3 -1
- package/lib/service/chat/memory/SessionStore.js +4 -3
- package/lib/service/chat/tools/ast-graph.js +229 -32
- package/lib/service/chat/tools/index.js +6 -1
- package/lib/service/chat/tools/infrastructure.js +5 -0
- package/lib/service/cursor/CursorDeliveryPipeline.js +167 -1
- package/lib/service/knowledge/CodeEntityGraph.js +327 -2
- package/lib/service/knowledge/KnowledgeService.js +5 -1
- package/lib/service/module/ModuleService.js +9 -0
- package/lib/service/wiki/WikiGenerator.js +1 -1
- package/lib/shared/PathGuard.js +1 -1
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
-
|
|
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?.
|
|
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,
|