gitnexus 1.6.6-rc.65 → 1.6.6-rc.67
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/dist/cli/eval-server.js +32 -11
- package/dist/cli/help-i18n.js +3 -0
- package/dist/cli/i18n/en.d.ts +3 -0
- package/dist/cli/i18n/en.js +3 -0
- package/dist/cli/i18n/resources.d.ts +6 -0
- package/dist/cli/i18n/zh-CN.d.ts +3 -0
- package/dist/cli/i18n/zh-CN.js +3 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/tool.d.ts +3 -0
- package/dist/cli/tool.js +7 -0
- package/dist/core/group/cross-impact.js +4 -0
- package/dist/core/group/service.d.ts +1 -0
- package/dist/core/ingestion/languages/cpp/captures.js +33 -5
- package/dist/core/ingestion/languages/cpp/two-phase-lookup.d.ts +7 -3
- package/dist/core/ingestion/languages/cpp/two-phase-lookup.js +96 -54
- package/dist/mcp/local/local-backend.js +45 -2
- package/dist/mcp/tools.js +21 -1
- package/package.json +1 -1
package/dist/cli/eval-server.js
CHANGED
|
@@ -170,20 +170,41 @@ export function formatImpactResult(result) {
|
|
|
170
170
|
2: 'LIKELY AFFECTED (indirect)',
|
|
171
171
|
3: 'MAY NEED TESTING (transitive)',
|
|
172
172
|
};
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
lines.push(`
|
|
181
|
-
}
|
|
182
|
-
if (items.length > 12) {
|
|
183
|
-
lines.push(` ... and ${items.length - 12} more`);
|
|
173
|
+
if (!result.byDepth && result.byDepthCounts) {
|
|
174
|
+
lines.push('(summary only — use summaryOnly: false to see symbol lists)');
|
|
175
|
+
const depthCounts = result.byDepthCounts;
|
|
176
|
+
for (const depth of [1, 2, 3]) {
|
|
177
|
+
const count = depthCounts[depth] ?? 0;
|
|
178
|
+
if (count === 0)
|
|
179
|
+
continue;
|
|
180
|
+
lines.push(`d=${depth}: ${depthLabels[depth] || ''} (${count})`);
|
|
184
181
|
}
|
|
185
182
|
lines.push('');
|
|
186
183
|
}
|
|
184
|
+
else {
|
|
185
|
+
const depthCounts = result.byDepthCounts || {};
|
|
186
|
+
for (const depth of [1, 2, 3]) {
|
|
187
|
+
const items = byDepth[depth] || [];
|
|
188
|
+
const trueCount = depthCounts[depth] ?? items.length;
|
|
189
|
+
if (trueCount === 0)
|
|
190
|
+
continue;
|
|
191
|
+
lines.push(`d=${depth}: ${depthLabels[depth] || ''} (${trueCount})`);
|
|
192
|
+
if (items.length === 0) {
|
|
193
|
+
lines.push(` (0 items on this page — adjust offset)`);
|
|
194
|
+
}
|
|
195
|
+
else {
|
|
196
|
+
const shown = Math.min(items.length, 12);
|
|
197
|
+
for (const item of items.slice(0, shown)) {
|
|
198
|
+
const conf = item.confidence < 1 ? ` (conf: ${item.confidence})` : '';
|
|
199
|
+
lines.push(` ${item.type} ${item.name} → ${item.filePath} [${item.relationType}]${conf}`);
|
|
200
|
+
}
|
|
201
|
+
if (trueCount > shown) {
|
|
202
|
+
lines.push(` ... and ${trueCount - shown} more`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
lines.push('');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
187
208
|
return lines.join('\n').trim();
|
|
188
209
|
}
|
|
189
210
|
export function formatCypherResult(result) {
|
package/dist/cli/help-i18n.js
CHANGED
|
@@ -99,6 +99,9 @@ const OPTION_DESCRIPTION_KEYS = {
|
|
|
99
99
|
'impact|-r, --repo <name>': 'help.option.repo.target',
|
|
100
100
|
'impact|--depth <n>': 'help.option.impact.depth',
|
|
101
101
|
'impact|--include-tests': 'help.option.impact.includeTests',
|
|
102
|
+
'impact|--limit <n>': 'help.option.impact.limit',
|
|
103
|
+
'impact|--offset <n>': 'help.option.impact.offset',
|
|
104
|
+
'impact|--summary-only': 'help.option.impact.summaryOnly',
|
|
102
105
|
'cypher|-r, --repo <name>': 'help.option.repo.target',
|
|
103
106
|
'detect-changes|-s, --scope <scope>': 'help.option.detectChanges.scope',
|
|
104
107
|
'detect-changes|-b, --base-ref <ref>': 'help.option.detectChanges.baseRef',
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -180,6 +180,9 @@ export declare const en: {
|
|
|
180
180
|
readonly 'help.option.impact.direction': "upstream (dependants) or downstream (dependencies)";
|
|
181
181
|
readonly 'help.option.impact.depth': "Max relationship depth (default: 3)";
|
|
182
182
|
readonly 'help.option.impact.includeTests': "Include test files in results";
|
|
183
|
+
readonly 'help.option.impact.limit': "Max symbols per depth level (default: 100)";
|
|
184
|
+
readonly 'help.option.impact.offset': "Skip N symbols per depth level for pagination";
|
|
185
|
+
readonly 'help.option.impact.summaryOnly': "Return counts and risk only, omit symbol list";
|
|
183
186
|
readonly 'help.option.detectChanges.scope': "What to analyze: unstaged, staged, all, or compare";
|
|
184
187
|
readonly 'help.option.detectChanges.baseRef': "Branch/commit for compare scope (e.g. main)";
|
|
185
188
|
readonly 'help.option.evalServer.host': "Bind address (default: 127.0.0.1, use 0.0.0.0 to expose to all interfaces)";
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -180,6 +180,9 @@ export const en = {
|
|
|
180
180
|
'help.option.impact.direction': 'upstream (dependants) or downstream (dependencies)',
|
|
181
181
|
'help.option.impact.depth': 'Max relationship depth (default: 3)',
|
|
182
182
|
'help.option.impact.includeTests': 'Include test files in results',
|
|
183
|
+
'help.option.impact.limit': 'Max symbols per depth level (default: 100)',
|
|
184
|
+
'help.option.impact.offset': 'Skip N symbols per depth level for pagination',
|
|
185
|
+
'help.option.impact.summaryOnly': 'Return counts and risk only, omit symbol list',
|
|
183
186
|
'help.option.detectChanges.scope': 'What to analyze: unstaged, staged, all, or compare',
|
|
184
187
|
'help.option.detectChanges.baseRef': 'Branch/commit for compare scope (e.g. main)',
|
|
185
188
|
'help.option.evalServer.host': 'Bind address (default: 127.0.0.1, use 0.0.0.0 to expose to all interfaces)',
|
|
@@ -181,6 +181,9 @@ export declare const cliResources: {
|
|
|
181
181
|
readonly 'help.option.impact.direction': "upstream (dependants) or downstream (dependencies)";
|
|
182
182
|
readonly 'help.option.impact.depth': "Max relationship depth (default: 3)";
|
|
183
183
|
readonly 'help.option.impact.includeTests': "Include test files in results";
|
|
184
|
+
readonly 'help.option.impact.limit': "Max symbols per depth level (default: 100)";
|
|
185
|
+
readonly 'help.option.impact.offset': "Skip N symbols per depth level for pagination";
|
|
186
|
+
readonly 'help.option.impact.summaryOnly': "Return counts and risk only, omit symbol list";
|
|
184
187
|
readonly 'help.option.detectChanges.scope': "What to analyze: unstaged, staged, all, or compare";
|
|
185
188
|
readonly 'help.option.detectChanges.baseRef': "Branch/commit for compare scope (e.g. main)";
|
|
186
189
|
readonly 'help.option.evalServer.host': "Bind address (default: 127.0.0.1, use 0.0.0.0 to expose to all interfaces)";
|
|
@@ -387,6 +390,9 @@ export declare const cliResources: {
|
|
|
387
390
|
'help.option.impact.direction': string;
|
|
388
391
|
'help.option.impact.depth': string;
|
|
389
392
|
'help.option.impact.includeTests': string;
|
|
393
|
+
'help.option.impact.limit': string;
|
|
394
|
+
'help.option.impact.offset': string;
|
|
395
|
+
'help.option.impact.summaryOnly': string;
|
|
390
396
|
'help.option.detectChanges.scope': string;
|
|
391
397
|
'help.option.detectChanges.baseRef': string;
|
|
392
398
|
'help.option.evalServer.host': string;
|
package/dist/cli/i18n/zh-CN.d.ts
CHANGED
|
@@ -180,6 +180,9 @@ export declare const zhCN: {
|
|
|
180
180
|
'help.option.impact.direction': string;
|
|
181
181
|
'help.option.impact.depth': string;
|
|
182
182
|
'help.option.impact.includeTests': string;
|
|
183
|
+
'help.option.impact.limit': string;
|
|
184
|
+
'help.option.impact.offset': string;
|
|
185
|
+
'help.option.impact.summaryOnly': string;
|
|
183
186
|
'help.option.detectChanges.scope': string;
|
|
184
187
|
'help.option.detectChanges.baseRef': string;
|
|
185
188
|
'help.option.evalServer.host': string;
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -180,6 +180,9 @@ export const zhCN = {
|
|
|
180
180
|
'help.option.impact.direction': 'upstream(依赖它的项)或 downstream(它依赖的项)',
|
|
181
181
|
'help.option.impact.depth': '最大关系遍历深度(默认:3)',
|
|
182
182
|
'help.option.impact.includeTests': '在结果中包含测试文件',
|
|
183
|
+
'help.option.impact.limit': '每层深度最大符号数(默认:100)',
|
|
184
|
+
'help.option.impact.offset': '每层深度跳过 N 个符号(分页用)',
|
|
185
|
+
'help.option.impact.summaryOnly': '仅返回计数和风险等级,省略符号列表',
|
|
183
186
|
'help.option.detectChanges.scope': '分析范围:unstaged、staged、all 或 compare',
|
|
184
187
|
'help.option.detectChanges.baseRef': 'compare 范围的分支/提交(例如 main)',
|
|
185
188
|
'help.option.evalServer.host': '绑定地址(默认:127.0.0.1;用 0.0.0.0 暴露到所有网卡)',
|
package/dist/cli/index.js
CHANGED
|
@@ -148,6 +148,9 @@ program
|
|
|
148
148
|
.option('-r, --repo <name>', 'Target repository')
|
|
149
149
|
.option('--depth <n>', 'Max relationship depth (default: 3)')
|
|
150
150
|
.option('--include-tests', 'Include test files in results')
|
|
151
|
+
.option('--limit <n>', 'Max symbols per depth level (default: 100)')
|
|
152
|
+
.option('--offset <n>', 'Skip N symbols per depth level for pagination')
|
|
153
|
+
.option('--summary-only', 'Return counts and risk only, omit symbol list')
|
|
151
154
|
.action(createLazyAction(() => import('./tool.js'), 'impactCommand'));
|
|
152
155
|
program
|
|
153
156
|
.command('cypher <query>')
|
package/dist/cli/tool.d.ts
CHANGED
|
@@ -32,6 +32,9 @@ export declare function impactCommand(target: string, options?: {
|
|
|
32
32
|
repo?: string;
|
|
33
33
|
depth?: string;
|
|
34
34
|
includeTests?: boolean;
|
|
35
|
+
limit?: string;
|
|
36
|
+
offset?: string;
|
|
37
|
+
summaryOnly?: boolean;
|
|
35
38
|
}): Promise<void>;
|
|
36
39
|
export declare function cypherCommand(query: string, options?: {
|
|
37
40
|
repo?: string;
|
package/dist/cli/tool.js
CHANGED
|
@@ -93,12 +93,19 @@ export async function impactCommand(target, options) {
|
|
|
93
93
|
}
|
|
94
94
|
try {
|
|
95
95
|
const backend = await getBackend();
|
|
96
|
+
const rawLimit = parseInt(options?.limit ?? '', 10);
|
|
97
|
+
const rawOffset = parseInt(options?.offset ?? '', 10);
|
|
98
|
+
const parsedLimit = Number.isFinite(rawLimit) ? rawLimit : undefined;
|
|
99
|
+
const parsedOffset = Number.isFinite(rawOffset) ? rawOffset : undefined;
|
|
96
100
|
const result = await backend.callTool('impact', {
|
|
97
101
|
target,
|
|
98
102
|
direction: options?.direction || 'upstream',
|
|
99
103
|
maxDepth: options?.depth ? parseInt(options.depth, 10) : undefined,
|
|
100
104
|
includeTests: options?.includeTests ?? false,
|
|
101
105
|
repo: options?.repo,
|
|
106
|
+
limit: parsedLimit,
|
|
107
|
+
offset: parsedOffset,
|
|
108
|
+
summaryOnly: options?.summaryOnly ?? undefined,
|
|
102
109
|
});
|
|
103
110
|
output(result);
|
|
104
111
|
}
|
|
@@ -9,6 +9,9 @@ import { fileMatchesServicePrefix, normalizeServicePrefix, repoInSubgroup, } fro
|
|
|
9
9
|
import { getGroupDir } from './storage.js';
|
|
10
10
|
import { closeBridgeDb, openBridgeDbReadOnly, queryBridge, readBridgeMeta } from './bridge-db.js';
|
|
11
11
|
import { BRIDGE_SCHEMA_VERSION } from './bridge-schema.js';
|
|
12
|
+
// High limit for the local phase of group impact so collectImpactSymbolUids
|
|
13
|
+
// sees (nearly) all symbols. Bypasses the MCP-facing default of 100.
|
|
14
|
+
const GROUP_LOCAL_PHASE_LIMIT = 10000;
|
|
12
15
|
/** Cross-boundary hops beyond this value are clamped (multi-hop reserved for future work). */
|
|
13
16
|
export const MAX_SUPPORTED_CROSS_DEPTH = 1;
|
|
14
17
|
/** Default wall-clock budget for the Phase 1 `impact` leg when callers omit `timeoutMs`. */
|
|
@@ -324,6 +327,7 @@ export async function runGroupImpact(deps, params) {
|
|
|
324
327
|
relationTypes: relationTypes && relationTypes.length > 0 ? relationTypes : undefined,
|
|
325
328
|
includeTests,
|
|
326
329
|
minConfidence,
|
|
330
|
+
limit: GROUP_LOCAL_PHASE_LIMIT,
|
|
327
331
|
};
|
|
328
332
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
329
333
|
const { value: local, timedOut: localTimedOut } = await safeLocalImpact(deps.port, resolved, impactParams, timeoutMs);
|
|
@@ -373,8 +373,9 @@ function detectCppDependentBases(root, filePath) {
|
|
|
373
373
|
for (const base of iterBaseClasses(baseClause)) {
|
|
374
374
|
if (isBaseDependent(base, params)) {
|
|
375
375
|
const baseName = extractBaseLookupName(base);
|
|
376
|
+
const baseQualifier = extractBaseLookupQualifier(base);
|
|
376
377
|
if (baseName !== '') {
|
|
377
|
-
markCppDependentBase(filePath, className, baseName);
|
|
378
|
+
markCppDependentBase(filePath, className, baseName, baseQualifier);
|
|
378
379
|
}
|
|
379
380
|
}
|
|
380
381
|
}
|
|
@@ -451,10 +452,15 @@ function* iterBaseClasses(baseClause) {
|
|
|
451
452
|
*/
|
|
452
453
|
function isBaseDependent(baseNode, templateParams) {
|
|
453
454
|
if (baseNode.type !== 'template_type') {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
455
|
+
if (baseNode.type === 'qualified_identifier') {
|
|
456
|
+
// Qualified identifier bases (e.g. `detail::Inner<T>`) may contain
|
|
457
|
+
// template_type children — descend into them for template param check.
|
|
458
|
+
// Fall through to the stack walk below.
|
|
459
|
+
}
|
|
460
|
+
else {
|
|
461
|
+
// Bare `type_identifier` bases — not dependent.
|
|
462
|
+
return false;
|
|
463
|
+
}
|
|
458
464
|
}
|
|
459
465
|
// Walk all descendants of the template_argument_list looking for any
|
|
460
466
|
// type_identifier matching a template parameter, or any conservative-
|
|
@@ -525,6 +531,28 @@ function extractBaseLookupName(baseNode) {
|
|
|
525
531
|
}
|
|
526
532
|
return '';
|
|
527
533
|
}
|
|
534
|
+
/** Extract the syntactic namespace qualifier from a base class node.
|
|
535
|
+
* For `detail::Inner<T>`, returns `'detail'`.
|
|
536
|
+
* For unqualified bases (`Inner<T>`, `Base<int>`), returns `''`.
|
|
537
|
+
* Nested qualifiers (`a::b::Inner<T>`) return the full scope text.
|
|
538
|
+
*/
|
|
539
|
+
function extractBaseLookupQualifier(baseNode) {
|
|
540
|
+
if (baseNode.type === 'qualified_identifier') {
|
|
541
|
+
const scopeNode = baseNode.childForFieldName('scope');
|
|
542
|
+
if (scopeNode !== null)
|
|
543
|
+
return scopeNode.text;
|
|
544
|
+
}
|
|
545
|
+
// template_type nodes may have a qualified_identifier as their name child
|
|
546
|
+
if (baseNode.type === 'template_type') {
|
|
547
|
+
const nameNode = baseNode.childForFieldName('name');
|
|
548
|
+
if (nameNode !== null && nameNode.type === 'qualified_identifier') {
|
|
549
|
+
const scopeNode = nameNode.childForFieldName('scope');
|
|
550
|
+
if (scopeNode !== null)
|
|
551
|
+
return scopeNode.text;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
return '';
|
|
555
|
+
}
|
|
528
556
|
/**
|
|
529
557
|
* Walk parent chain from a function_definition / declaration / field_declaration
|
|
530
558
|
* to find the enclosing `template_declaration`. Returns null when the function
|
|
@@ -36,12 +36,14 @@ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexe
|
|
|
36
36
|
* Record a dependent-base relationship discovered during scope-capture
|
|
37
37
|
* emission. `className` is the simple name of the template class;
|
|
38
38
|
* `baseName` is the simple name of the dependent base class.
|
|
39
|
+
* `qualifier` is the syntactic namespace qualifier (e.g. `detail` for
|
|
40
|
+
* `detail::Inner<T>`), or '' for unqualified bases.
|
|
39
41
|
*
|
|
40
42
|
* The capture-time recorder uses simple names because the registry
|
|
41
43
|
* resolution that maps names → nodeIds runs later (in
|
|
42
44
|
* `populateCppDependentBases`).
|
|
43
45
|
*/
|
|
44
|
-
export declare function markCppDependentBase(filePath: string, className: string, baseName: string): void;
|
|
46
|
+
export declare function markCppDependentBase(filePath: string, className: string, baseName: string, qualifier?: string): void;
|
|
45
47
|
/** Clear two-phase-lookup state. Called from `clearFileLocalNames`. */
|
|
46
48
|
export declare function clearCppDependentBases(): void;
|
|
47
49
|
/**
|
|
@@ -53,8 +55,10 @@ export declare function clearCppDependentBases(): void;
|
|
|
53
55
|
* Disambiguation strategy (multiple classes sharing a simple name):
|
|
54
56
|
* 1. Prefer the candidate whose qualified-name namespace prefix matches
|
|
55
57
|
* the deriving class's namespace prefix (same-namespace bias).
|
|
56
|
-
* 2.
|
|
57
|
-
*
|
|
58
|
+
* 2. When a syntactic qualifier is available (`detail` in
|
|
59
|
+
* `detail::Inner<T>`), target the exact namespace derived from it.
|
|
60
|
+
* 3. Fall back to accepting a unique simple-name match.
|
|
61
|
+
* 4. Skip when multiple candidates exist and no namespace match is
|
|
58
62
|
* found (conservative: avoids false associations).
|
|
59
63
|
*/
|
|
60
64
|
export declare function populateCppDependentBases(parsedFiles: readonly ParsedFile[]): void;
|
|
@@ -33,10 +33,12 @@
|
|
|
33
33
|
import { findEnclosingClassDef } from '../../scope-resolution/scope/walkers.js';
|
|
34
34
|
/**
|
|
35
35
|
* Capture-time record: for each template class declaration in a file,
|
|
36
|
-
* the simple names of its dependent base classes
|
|
36
|
+
* the simple names of its dependent base classes and their syntactic
|
|
37
|
+
* qualifiers (e.g., `detail` for `detail::Inner<T>`).
|
|
37
38
|
*
|
|
38
39
|
* Key: filePath
|
|
39
|
-
* Value: Map<className,
|
|
40
|
+
* Value: Map<className, Map<baseName, qualifier>>
|
|
41
|
+
* qualifier is '' when the base was unqualified.
|
|
40
42
|
*/
|
|
41
43
|
const dependentBasesByFile = new Map();
|
|
42
44
|
/**
|
|
@@ -49,12 +51,14 @@ const dependentBaseNodeIds = new Map();
|
|
|
49
51
|
* Record a dependent-base relationship discovered during scope-capture
|
|
50
52
|
* emission. `className` is the simple name of the template class;
|
|
51
53
|
* `baseName` is the simple name of the dependent base class.
|
|
54
|
+
* `qualifier` is the syntactic namespace qualifier (e.g. `detail` for
|
|
55
|
+
* `detail::Inner<T>`), or '' for unqualified bases.
|
|
52
56
|
*
|
|
53
57
|
* The capture-time recorder uses simple names because the registry
|
|
54
58
|
* resolution that maps names → nodeIds runs later (in
|
|
55
59
|
* `populateCppDependentBases`).
|
|
56
60
|
*/
|
|
57
|
-
export function markCppDependentBase(filePath, className, baseName) {
|
|
61
|
+
export function markCppDependentBase(filePath, className, baseName, qualifier = '') {
|
|
58
62
|
let perFile = dependentBasesByFile.get(filePath);
|
|
59
63
|
if (perFile === undefined) {
|
|
60
64
|
perFile = new Map();
|
|
@@ -62,10 +66,15 @@ export function markCppDependentBase(filePath, className, baseName) {
|
|
|
62
66
|
}
|
|
63
67
|
let bases = perFile.get(className);
|
|
64
68
|
if (bases === undefined) {
|
|
65
|
-
bases = new
|
|
69
|
+
bases = new Map();
|
|
66
70
|
perFile.set(className, bases);
|
|
67
71
|
}
|
|
68
|
-
bases.
|
|
72
|
+
let quals = bases.get(baseName);
|
|
73
|
+
if (quals === undefined) {
|
|
74
|
+
quals = new Set();
|
|
75
|
+
bases.set(baseName, quals);
|
|
76
|
+
}
|
|
77
|
+
quals.add(qualifier);
|
|
69
78
|
}
|
|
70
79
|
/** Clear two-phase-lookup state. Called from `clearFileLocalNames`. */
|
|
71
80
|
export function clearCppDependentBases() {
|
|
@@ -81,8 +90,10 @@ export function clearCppDependentBases() {
|
|
|
81
90
|
* Disambiguation strategy (multiple classes sharing a simple name):
|
|
82
91
|
* 1. Prefer the candidate whose qualified-name namespace prefix matches
|
|
83
92
|
* the deriving class's namespace prefix (same-namespace bias).
|
|
84
|
-
* 2.
|
|
85
|
-
*
|
|
93
|
+
* 2. When a syntactic qualifier is available (`detail` in
|
|
94
|
+
* `detail::Inner<T>`), target the exact namespace derived from it.
|
|
95
|
+
* 3. Fall back to accepting a unique simple-name match.
|
|
96
|
+
* 4. Skip when multiple candidates exist and no namespace match is
|
|
86
97
|
* found (conservative: avoids false associations).
|
|
87
98
|
*/
|
|
88
99
|
export function populateCppDependentBases(parsedFiles) {
|
|
@@ -91,7 +102,13 @@ export function populateCppDependentBases(parsedFiles) {
|
|
|
91
102
|
// Build workspace-wide index: simpleName → {nodeId, nsPrefix}[]
|
|
92
103
|
// nsPrefix is the dot-joined namespace path (qualifiedName without the
|
|
93
104
|
// last segment). Classes at global scope have nsPrefix = ''.
|
|
105
|
+
// Dedup by nodeId, keeping the LAST occurrence: parsed.localDefs may
|
|
106
|
+
// list the same class def multiple times — the scope-extractor creates
|
|
107
|
+
// a def with simple-name qualifiedName first, then the class extractor
|
|
108
|
+
// replaces it with the correct fully-qualified qualifiedName. Keeping
|
|
109
|
+
// the later entry ensures we capture the full namespace path.
|
|
94
110
|
const classesBySimpleName = new Map();
|
|
111
|
+
const entryByNodeId = new Map();
|
|
95
112
|
for (const parsed of parsedFiles) {
|
|
96
113
|
for (const def of parsed.localDefs) {
|
|
97
114
|
if (def.type !== 'Class' && def.type !== 'Struct' && def.type !== 'Interface')
|
|
@@ -102,13 +119,16 @@ export function populateCppDependentBases(parsedFiles) {
|
|
|
102
119
|
if (simple === '')
|
|
103
120
|
continue;
|
|
104
121
|
const nsPrefix = lastDot >= 0 ? qn.slice(0, lastDot) : '';
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
entryByNodeId.set(def.nodeId, { nodeId: def.nodeId, nsPrefix, simple });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const entry of entryByNodeId.values()) {
|
|
126
|
+
let entries = classesBySimpleName.get(entry.simple);
|
|
127
|
+
if (entries === undefined) {
|
|
128
|
+
entries = [];
|
|
129
|
+
classesBySimpleName.set(entry.simple, entries);
|
|
111
130
|
}
|
|
131
|
+
entries.push({ nodeId: entry.nodeId, nsPrefix: entry.nsPrefix });
|
|
112
132
|
}
|
|
113
133
|
// Build a filePath → ParsedFile lookup for fast per-file access.
|
|
114
134
|
const parsedByFile = new Map();
|
|
@@ -133,7 +153,12 @@ export function populateCppDependentBases(parsedFiles) {
|
|
|
133
153
|
const nsPrefix = lastDot >= 0 ? qn.slice(0, lastDot) : '';
|
|
134
154
|
localClassByName.set(simple, { nodeId: def.nodeId, nsPrefix });
|
|
135
155
|
}
|
|
136
|
-
|
|
156
|
+
// V3: qualifier-based exact targeting. When the base specifier carries
|
|
157
|
+
// a syntactic qualifier (e.g., `detail` in `detail::Inner<T>`), compute
|
|
158
|
+
// the expected namespace prefix and use exact (===) match. Falls back to
|
|
159
|
+
// the V2 prefix-contains heuristic when the qualifier isn't available or
|
|
160
|
+
// the exact match fails (absolute qualifier edge cases like `::std`).
|
|
161
|
+
for (const [className, baseEntries] of perFile) {
|
|
137
162
|
const classEntry = localClassByName.get(className);
|
|
138
163
|
if (classEntry === undefined)
|
|
139
164
|
continue;
|
|
@@ -142,49 +167,66 @@ export function populateCppDependentBases(parsedFiles) {
|
|
|
142
167
|
bases = new Set();
|
|
143
168
|
dependentBaseNodeIds.set(classEntry.nodeId, bases);
|
|
144
169
|
}
|
|
145
|
-
for (const baseName of
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
170
|
+
for (const [baseName, qualsSet] of baseEntries) {
|
|
171
|
+
for (const baseQualifier of qualsSet) {
|
|
172
|
+
const candidates = classesBySimpleName.get(baseName);
|
|
173
|
+
if (candidates === undefined || candidates.length === 0)
|
|
174
|
+
continue;
|
|
175
|
+
// Compute the expected namespace prefix from the qualifier.
|
|
176
|
+
// Relative qualifier (e.g. `inner`): prepend deriving class's prefix.
|
|
177
|
+
// Absolute qualifiers (`::std`, `ns::other`) will fail the relative
|
|
178
|
+
// lookup and fall through to the prefix-heuristic below.
|
|
179
|
+
const normalizedQualifier = baseQualifier.replace(/::/g, '.');
|
|
180
|
+
const expectedNs = baseQualifier && classEntry.nsPrefix
|
|
181
|
+
? classEntry.nsPrefix + '.' + normalizedQualifier
|
|
182
|
+
: normalizedQualifier;
|
|
183
|
+
if (candidates.length === 1) {
|
|
184
|
+
// Unqualified base: accept unique match (pre-existing behavior).
|
|
185
|
+
if (!baseQualifier) {
|
|
186
|
+
bases.add(candidates[0].nodeId);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
// Qualified base: verify namespace before accepting.
|
|
190
|
+
if (candidates[0].nsPrefix === expectedNs ||
|
|
191
|
+
candidates[0].nsPrefix === normalizedQualifier) {
|
|
192
|
+
bases.add(candidates[0].nodeId);
|
|
193
|
+
}
|
|
194
|
+
// else: suppress — qualifier doesn't match. #1564 policy.
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
// V3: qualifier-based exact targeting. When the base specifier
|
|
198
|
+
// carries a syntactic qualifier, compute the expected namespace
|
|
199
|
+
// prefix and attempt an exact (===) match using the deduplicated
|
|
200
|
+
// nsPrefix. Dedup by nodeId removes broken entries from the
|
|
201
|
+
// classesBySimpleName index, making the surviving nsPrefix reliable.
|
|
202
|
+
if (baseQualifier) {
|
|
203
|
+
const qualifierMatch = candidates.find((c) => c.nsPrefix === expectedNs || c.nsPrefix === normalizedQualifier);
|
|
204
|
+
if (qualifierMatch !== undefined) {
|
|
205
|
+
bases.add(qualifierMatch.nodeId);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
continue; // qualifier was explicit but no match — suppress, don't fall through to V2
|
|
176
209
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
210
|
+
// V2 fallback: filter by prefix-match capped at one level deeper,
|
|
211
|
+
// then accept only if exactly one candidate survives.
|
|
212
|
+
const nsMatches = candidates.filter((c) => {
|
|
213
|
+
if (c.nsPrefix === classEntry.nsPrefix)
|
|
214
|
+
return true;
|
|
215
|
+
if (classEntry.nsPrefix === '') {
|
|
216
|
+
return c.nsPrefix !== '' && !c.nsPrefix.includes('.');
|
|
217
|
+
}
|
|
218
|
+
if (c.nsPrefix.startsWith(classEntry.nsPrefix + '.')) {
|
|
219
|
+
const suffix = c.nsPrefix.slice(classEntry.nsPrefix.length + 1);
|
|
220
|
+
return !suffix.includes('.');
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
});
|
|
224
|
+
const nsMatch = nsMatches.length === 1 ? nsMatches[0] : undefined;
|
|
225
|
+
if (nsMatch !== undefined) {
|
|
226
|
+
bases.add(nsMatch.nodeId);
|
|
180
227
|
}
|
|
181
|
-
|
|
182
|
-
});
|
|
183
|
-
const nsMatch = nsMatches.length === 1 ? nsMatches[0] : undefined;
|
|
184
|
-
if (nsMatch !== undefined) {
|
|
185
|
-
bases.add(nsMatch.nodeId);
|
|
228
|
+
// else: ambiguous (multiple candidates, no namespace match) → skip.
|
|
186
229
|
}
|
|
187
|
-
// else: ambiguous (multiple candidates, no namespace match) → skip.
|
|
188
230
|
}
|
|
189
231
|
}
|
|
190
232
|
}
|
|
@@ -2361,6 +2361,9 @@ export class LocalBackend {
|
|
|
2361
2361
|
relationTypes: effectiveRelationTypes,
|
|
2362
2362
|
includeTests,
|
|
2363
2363
|
minConfidence,
|
|
2364
|
+
limit: Number.isFinite(params.limit) ? params.limit : 100,
|
|
2365
|
+
offset: Number.isFinite(params.offset) ? params.offset : 0,
|
|
2366
|
+
summaryOnly: params.summaryOnly,
|
|
2364
2367
|
});
|
|
2365
2368
|
}
|
|
2366
2369
|
/**
|
|
@@ -2368,6 +2371,13 @@ export class LocalBackend {
|
|
|
2368
2371
|
*/
|
|
2369
2372
|
async _runImpactBFS(repo, sym, symType, direction, opts) {
|
|
2370
2373
|
const { maxDepth, relationTypes, includeTests, minConfidence } = opts;
|
|
2374
|
+
const hasExplicitLimit = typeof opts.limit === 'number' && Number.isFinite(opts.limit);
|
|
2375
|
+
const paginationLimit = hasExplicitLimit
|
|
2376
|
+
? Math.max(1, Math.min(Math.trunc(opts.limit), 10000))
|
|
2377
|
+
: Infinity;
|
|
2378
|
+
const rawOffset = typeof opts.offset === 'number' && Number.isFinite(opts.offset) ? opts.offset : 0;
|
|
2379
|
+
const paginationOffset = Math.max(0, Math.trunc(rawOffset));
|
|
2380
|
+
const summaryOnly = opts.summaryOnly ?? false;
|
|
2371
2381
|
const relTypeFilter = relationTypes.map((t) => `'${t}'`).join(', ');
|
|
2372
2382
|
const confidenceFilter = minConfidence > 0 ? ` AND r.confidence >= ${minConfidence}` : '';
|
|
2373
2383
|
const symId = sym.id || sym[0];
|
|
@@ -2708,7 +2718,12 @@ export class LocalBackend {
|
|
|
2708
2718
|
else if (directCount >= 5 || impacted.length >= 30) {
|
|
2709
2719
|
risk = 'MEDIUM';
|
|
2710
2720
|
}
|
|
2711
|
-
|
|
2721
|
+
// Build per-depth counts (always included, even in summaryOnly mode)
|
|
2722
|
+
const byDepthCounts = {};
|
|
2723
|
+
for (const [depth, items] of Object.entries(grouped)) {
|
|
2724
|
+
byDepthCounts[Number(depth)] = items.length;
|
|
2725
|
+
}
|
|
2726
|
+
const base = {
|
|
2712
2727
|
target: {
|
|
2713
2728
|
id: symId,
|
|
2714
2729
|
name: sym.name || sym[1],
|
|
@@ -2724,9 +2739,34 @@ export class LocalBackend {
|
|
|
2724
2739
|
processes_affected: processCount,
|
|
2725
2740
|
modules_affected: moduleCount,
|
|
2726
2741
|
},
|
|
2742
|
+
byDepthCounts,
|
|
2727
2743
|
affected_processes: affectedProcesses,
|
|
2728
2744
|
affected_modules: affectedModules,
|
|
2729
|
-
|
|
2745
|
+
};
|
|
2746
|
+
if (summaryOnly) {
|
|
2747
|
+
return base;
|
|
2748
|
+
}
|
|
2749
|
+
// Apply limit/offset pagination per depth level
|
|
2750
|
+
const paginatedGrouped = {};
|
|
2751
|
+
let anyTruncated = false;
|
|
2752
|
+
for (const [depth, items] of Object.entries(grouped)) {
|
|
2753
|
+
const total = items.length;
|
|
2754
|
+
const sliced = items.slice(paginationOffset, paginationOffset + paginationLimit);
|
|
2755
|
+
paginatedGrouped[Number(depth)] = sliced;
|
|
2756
|
+
if (paginationOffset > 0 || paginationOffset + paginationLimit < total) {
|
|
2757
|
+
anyTruncated = true;
|
|
2758
|
+
}
|
|
2759
|
+
}
|
|
2760
|
+
return {
|
|
2761
|
+
...base,
|
|
2762
|
+
...(anyTruncated && {
|
|
2763
|
+
pagination: {
|
|
2764
|
+
...(Number.isFinite(paginationLimit) && { limit: paginationLimit }),
|
|
2765
|
+
offset: paginationOffset,
|
|
2766
|
+
truncated: true,
|
|
2767
|
+
},
|
|
2768
|
+
}),
|
|
2769
|
+
byDepth: paginatedGrouped,
|
|
2730
2770
|
};
|
|
2731
2771
|
}
|
|
2732
2772
|
/**
|
|
@@ -2855,6 +2895,9 @@ export class LocalBackend {
|
|
|
2855
2895
|
impactArgs.timeoutMs = params.timeoutMs;
|
|
2856
2896
|
if (params.timeout !== undefined)
|
|
2857
2897
|
impactArgs.timeout = params.timeout;
|
|
2898
|
+
// limit/offset/summaryOnly are not forwarded to group-mode impact:
|
|
2899
|
+
// runGroupImpact uses GROUP_LOCAL_PHASE_LIMIT internally for UID
|
|
2900
|
+
// collection and does not re-paginate the local result yet.
|
|
2858
2901
|
return svc.groupImpact(impactArgs);
|
|
2859
2902
|
}
|
|
2860
2903
|
if (method === 'query') {
|
package/dist/mcp/tools.js
CHANGED
|
@@ -299,13 +299,15 @@ Output includes:
|
|
|
299
299
|
- summary: direct callers, processes affected, modules affected
|
|
300
300
|
- affected_processes: which execution flows break and at which step
|
|
301
301
|
- affected_modules: which functional areas are hit (direct vs indirect)
|
|
302
|
-
- byDepth:
|
|
302
|
+
- byDepth: affected symbols grouped by traversal depth (paginated by limit/offset; omitted when summaryOnly:true — use byDepthCounts for totals per depth, pagination object when truncated)
|
|
303
303
|
|
|
304
304
|
Depth groups:
|
|
305
305
|
- d=1: WILL BREAK (direct callers/importers)
|
|
306
306
|
- d=2: LIKELY AFFECTED (indirect)
|
|
307
307
|
- d=3: MAY NEED TESTING (transitive)
|
|
308
308
|
|
|
309
|
+
TIP: For hub symbols (base error classes, shared utilities) with many direct callers, use summaryOnly: true first to see counts and risk, then drill into specific depths with limit/offset. maxDepth alone does not bound output size when most dependents are at depth 1. limit and offset apply independently to each depth level, not to the total result set — use byDepthCounts to see totals per depth.
|
|
310
|
+
|
|
309
311
|
TIP: Default traversal uses CALLS/IMPORTS/EXTENDS/IMPLEMENTS. For class members, include HAS_METHOD and HAS_PROPERTY in relationTypes. For field access analysis, include ACCESSES in relationTypes.
|
|
310
312
|
|
|
311
313
|
Handles disambiguation: when multiple symbols share the target name, returns ranked candidates (each with a relevance score) instead of silently picking one. Use target_uid for zero-ambiguity lookup, or narrow with file_path and/or kind hints.
|
|
@@ -377,6 +379,24 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
|
|
|
377
379
|
type: 'string',
|
|
378
380
|
description: 'Optional group subgroup prefix (member repo paths) limiting which repos participate in cross fan-out.',
|
|
379
381
|
},
|
|
382
|
+
limit: {
|
|
383
|
+
type: 'integer',
|
|
384
|
+
description: 'Max symbols returned in byDepth per depth level (default: 100). Single-repo only; ignored in group mode (@groupName). Use small values for hub symbols to avoid output truncation.',
|
|
385
|
+
default: 100,
|
|
386
|
+
minimum: 1,
|
|
387
|
+
maximum: 10000,
|
|
388
|
+
},
|
|
389
|
+
offset: {
|
|
390
|
+
type: 'integer',
|
|
391
|
+
description: 'Skip this many symbols per depth level before applying limit. Single-repo only; ignored in group mode (@groupName). Use with limit for pagination.',
|
|
392
|
+
default: 0,
|
|
393
|
+
minimum: 0,
|
|
394
|
+
},
|
|
395
|
+
summaryOnly: {
|
|
396
|
+
type: 'boolean',
|
|
397
|
+
description: 'When true, returns target, summary, risk, byDepthCounts, affected_processes, and affected_modules — omits byDepth. Single-repo only; ignored in group mode (@groupName). Use for hub symbols to get actionable signal without output explosion.',
|
|
398
|
+
default: false,
|
|
399
|
+
},
|
|
380
400
|
timeoutMs: {
|
|
381
401
|
type: 'number',
|
|
382
402
|
description: 'Wall-clock budget in milliseconds for the Phase-1 local impact leg (default 30000)',
|
package/package.json
CHANGED