gitnexus 1.6.11-rc.6 → 1.6.11-rc.7
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/_shared/impact-risk.d.ts +4 -2
- package/dist/_shared/impact-risk.d.ts.map +1 -1
- package/dist/_shared/impact-risk.js +35 -21
- package/dist/_shared/impact-risk.js.map +1 -1
- package/dist/cli/ai-context.js +4 -4
- package/dist/cli/eval-server.js +22 -5
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +23 -2
- package/dist/core/group/types.d.ts +12 -1
- package/dist/mcp/local/local-backend.js +78 -46
- package/dist/mcp/tools.js +4 -2
- package/package.json +1 -1
- package/skills/gitnexus-impact-analysis.md +9 -0
- package/web/assets/{agent-Dr4l5EOp.js → agent-BWAXPjC5.js} +106 -104
- package/web/assets/{index-2zdvEdzg.js → index-DDRie6_8.js} +3 -3
- package/web/index.html +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type ImpactRisk = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' | 'UNKNOWN';
|
|
2
2
|
export type ImpactRiskAxis = 'processes' | 'modules';
|
|
3
|
-
export type UnusedImpactRiskReason = 'file-nodes-have-no-process-or-community-membership' | 'enrichment-skipped' | 'enrichment-budget-exhausted' | 'enrichment-query-failed';
|
|
3
|
+
export type UnusedImpactRiskReason = 'file-nodes-have-no-process-or-community-membership' | 'enrichment-skipped' | 'enrichment-budget-exhausted' | 'enrichment-truncated' | 'enrichment-query-failed';
|
|
4
4
|
export interface UnusedImpactRiskAxis {
|
|
5
5
|
axis: ImpactRiskAxis;
|
|
6
6
|
reason: UnusedImpactRiskReason;
|
|
@@ -29,7 +29,9 @@ export declare function unusedAxesForImpactWalk(input: {
|
|
|
29
29
|
processQueryFailed: boolean;
|
|
30
30
|
moduleQueryFailed: boolean;
|
|
31
31
|
/** When 0, a zero chunk budget is not an unused-axis event — there was nothing to enrich. */
|
|
32
|
-
impactedCount
|
|
32
|
+
impactedCount: number;
|
|
33
|
+
/** True when process/module queries ran on a strict subset of impacted symbols. */
|
|
34
|
+
enrichmentTruncated?: boolean;
|
|
33
35
|
}): UnusedImpactRiskAxis[];
|
|
34
36
|
export declare function scoreImpactRisk(input: ImpactRiskInput): ImpactRiskResult;
|
|
35
37
|
//# sourceMappingURL=impact-risk.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impact-risk.d.ts","sourceRoot":"","sources":["../src/impact-risk.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;AAE5E,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,SAAS,CAAC;AAErD,MAAM,MAAM,sBAAsB,GAC9B,oDAAoD,GACpD,oBAAoB,GACpB,6BAA6B,GAC7B,yBAAyB,CAAC;AAE9B,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,sBAAsB,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,GAAG,YAAY,CAAC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,cAAc,EAAE,UAAU,CAAC;IAC3B,SAAS,EAAE;QACT,qBAAqB,EAAE,OAAO,CAAC;QAC/B,UAAU,EAAE,SAAS,oBAAoB,EAAE,CAAC;KAC7C,CAAC;CACH;
|
|
1
|
+
{"version":3,"file":"impact-risk.d.ts","sourceRoot":"","sources":["../src/impact-risk.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;AAE5E,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,SAAS,CAAC;AAErD,MAAM,MAAM,sBAAsB,GAC9B,oDAAoD,GACpD,oBAAoB,GACpB,6BAA6B,GAC7B,sBAAsB,GACtB,yBAAyB,CAAC;AAE9B,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,MAAM,EAAE,sBAAsB,CAAC;CAChC;AAED,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,UAAU,GAAG,YAAY,CAAC;IACrC,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,UAAU,CAAC;IACjB,cAAc,EAAE,UAAU,CAAC;IAC3B,SAAS,EAAE;QACT,qBAAqB,EAAE,OAAO,CAAC;QAC/B,UAAU,EAAE,SAAS,oBAAoB,EAAE,CAAC;KAC7C,CAAC;CACH;AAwDD,iGAAiG;AACjG,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,YAAY,EAAE,OAAO,CAAC;IACtB,cAAc,EAAE,OAAO,CAAC;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,iBAAiB,EAAE,OAAO,CAAC;IAC3B,6FAA6F;IAC7F,aAAa,EAAE,MAAM,CAAC;IACtB,mFAAmF;IACnF,mBAAmB,CAAC,EAAE,OAAO,CAAC;CAC/B,GAAG,oBAAoB,EAAE,CAqBzB;AAOD,wBAAgB,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAuBxE"}
|
|
@@ -12,10 +12,23 @@ function score(input) {
|
|
|
12
12
|
return 'MEDIUM';
|
|
13
13
|
return 'LOW';
|
|
14
14
|
}
|
|
15
|
-
|
|
15
|
+
const UNMEASURED_REASONS = new Set([
|
|
16
|
+
'file-nodes-have-no-process-or-community-membership',
|
|
17
|
+
'enrichment-skipped',
|
|
18
|
+
'enrichment-budget-exhausted',
|
|
19
|
+
]);
|
|
20
|
+
function unusedPair(reason) {
|
|
21
|
+
return [
|
|
22
|
+
{ axis: 'processes', reason },
|
|
23
|
+
{ axis: 'modules', reason },
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
function countsWithUnmeasuredAxesZeroed(input) {
|
|
16
27
|
let processCount = input.processCount;
|
|
17
28
|
let moduleCount = input.moduleCount;
|
|
18
29
|
for (const unused of input.unusedAxes ?? []) {
|
|
30
|
+
if (!UNMEASURED_REASONS.has(unused.reason))
|
|
31
|
+
continue;
|
|
19
32
|
if (unused.axis === 'processes')
|
|
20
33
|
processCount = 0;
|
|
21
34
|
if (unused.axis === 'modules')
|
|
@@ -32,30 +45,18 @@ function countsWithUnusedAxesZeroed(input) {
|
|
|
32
45
|
/** Map walk outcomes to unused process/module axes so comparability matches what was sampled. */
|
|
33
46
|
export function unusedAxesForImpactWalk(input) {
|
|
34
47
|
if (input.isFileTarget) {
|
|
35
|
-
return
|
|
36
|
-
{
|
|
37
|
-
axis: 'processes',
|
|
38
|
-
reason: 'file-nodes-have-no-process-or-community-membership',
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
axis: 'modules',
|
|
42
|
-
reason: 'file-nodes-have-no-process-or-community-membership',
|
|
43
|
-
},
|
|
44
|
-
];
|
|
48
|
+
return unusedPair('file-nodes-have-no-process-or-community-membership');
|
|
45
49
|
}
|
|
46
50
|
if (input.skipEnrichment) {
|
|
47
|
-
return
|
|
48
|
-
{ axis: 'processes', reason: 'enrichment-skipped' },
|
|
49
|
-
{ axis: 'modules', reason: 'enrichment-skipped' },
|
|
50
|
-
];
|
|
51
|
+
return unusedPair('enrichment-skipped');
|
|
51
52
|
}
|
|
52
|
-
if (input.maxChunks === 0 &&
|
|
53
|
-
return
|
|
54
|
-
{ axis: 'processes', reason: 'enrichment-budget-exhausted' },
|
|
55
|
-
{ axis: 'modules', reason: 'enrichment-budget-exhausted' },
|
|
56
|
-
];
|
|
53
|
+
if (input.maxChunks === 0 && input.impactedCount > 0) {
|
|
54
|
+
return unusedPair('enrichment-budget-exhausted');
|
|
57
55
|
}
|
|
58
56
|
const unused = [];
|
|
57
|
+
if (input.enrichmentTruncated) {
|
|
58
|
+
unused.push(...unusedPair('enrichment-truncated'));
|
|
59
|
+
}
|
|
59
60
|
if (input.processQueryFailed) {
|
|
60
61
|
unused.push({ axis: 'processes', reason: 'enrichment-query-failed' });
|
|
61
62
|
}
|
|
@@ -64,10 +65,23 @@ export function unusedAxesForImpactWalk(input) {
|
|
|
64
65
|
}
|
|
65
66
|
return unused;
|
|
66
67
|
}
|
|
68
|
+
const INCOMPLETE_SAMPLE_REASONS = new Set([
|
|
69
|
+
'enrichment-query-failed',
|
|
70
|
+
'enrichment-truncated',
|
|
71
|
+
]);
|
|
67
72
|
export function scoreImpactRisk(input) {
|
|
68
73
|
const unusedAxes = input.unusedAxes ?? [];
|
|
74
|
+
const observedRisk = score(countsWithUnmeasuredAxesZeroed(input));
|
|
75
|
+
const incompleteSample = unusedAxes.some((unused) => INCOMPLETE_SAMPLE_REASONS.has(unused.reason));
|
|
76
|
+
// Failed queries and truncated samples make observed process/module counts
|
|
77
|
+
// lower bounds. Preserve any HIGH/CRITICAL warning already proved by those
|
|
78
|
+
// counts, but never emit a confident LOW/MEDIUM edit gate from an incomplete
|
|
79
|
+
// enrichment pass.
|
|
80
|
+
const risk = incompleteSample && (observedRisk === 'LOW' || observedRisk === 'MEDIUM')
|
|
81
|
+
? 'UNKNOWN'
|
|
82
|
+
: observedRisk;
|
|
69
83
|
return {
|
|
70
|
-
risk
|
|
84
|
+
risk,
|
|
71
85
|
riskSharedAxes: score({ ...input, processCount: 0, moduleCount: 0 }),
|
|
72
86
|
riskScale: {
|
|
73
87
|
comparableAcrossKinds: unusedAxes.length === 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"impact-risk.js","sourceRoot":"","sources":["../src/impact-risk.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"impact-risk.js","sourceRoot":"","sources":["../src/impact-risk.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/ai-context.js
CHANGED
|
@@ -139,14 +139,14 @@ This project is indexed by GitNexus as **${projectName}**${noStats ? '' : ` (${s
|
|
|
139
139
|
|
|
140
140
|
## Always Do
|
|
141
141
|
|
|
142
|
-
- **MUST run impact
|
|
142
|
+
- **MUST run impact before editing.** Use \`impact({target: "symbolName", direction: "upstream"})\` or \`${runner} impact "symbolName" --direction upstream --repo .\`; report callers, processes, and risk. Never substitute grep for graph analysis.${hasPdg
|
|
143
143
|
? ` For unified PDG impact, add \`mode: "pdg"\` with optional \`line: <N>\` — it returns statement-level \`affectedStatements\` over CDG + REACHING_DEF and inter-procedural symbols in \`interproceduralByDepth\`/\`byDepth\`; no-layer/degraded PDG results are UNKNOWN-risk notes (\`--pdg\` layer). CLI equivalent: \`${runner} impact "symbolName" --direction upstream --mode pdg --line <N> --repo .\`.`
|
|
144
144
|
: ''}
|
|
145
145
|
- **MUST analyze graph changes before committing.** Use \`detect_changes({scope: "all"})\` (MCP) or \`${runner} detect-changes --scope all --repo .\` (CLI fallback). \`partial: true\` or \`truncated: true\` is not a clean check — a zero means unseen, not unaffected; re-run it. For regression review: \`detect_changes({scope: "compare", base_ref: ${JSON.stringify(markdownSafeBranch(defaultBranch))}})\` or \`${runner} detect-changes --scope compare --base-ref ${JSON.stringify(markdownSafeBranch(defaultBranch))} --repo .\`.
|
|
146
|
-
-
|
|
146
|
+
- MUST warn on HIGH/CRITICAL \`risk\` pre-edit; never use \`riskSharedAxes\` to waive a HIGH/CRITICAL \`risk\` warning. Compare File/symbol: MCP File omits axes; Graph-RAG expands File.
|
|
147
147
|
- **MUST treat \`risk: UNKNOWN\` as unresolved, not as low.** An empty caller set is not evidence the symbol is unused — it can also mean the callers are not resolvable by the index (plain-object property access, dynamic dispatch, cross-language calls). \`impact\` pairs \`UNKNOWN\` with a \`riskNote\` saying so. Confirm with a text search before treating the symbol as safe to change or delete; do not proceed on the strength of a zero.
|
|
148
|
-
-
|
|
149
|
-
-
|
|
148
|
+
- Explore with \`query({search_query: "concept"})\` for process-grouped flows.
|
|
149
|
+
- Use \`context({name: "symbolName"})\` for callers, callees, and flows.
|
|
150
150
|
- For security review, \`explain({target: "fileOrSymbol"})\` lists taint findings (source→sink flows; needs \`analyze --pdg\`).${hasPdg
|
|
151
151
|
? `\n- For control/data dependence, \`pdg_query({mode: "controls", target: "fileOrSymbol"})\` answers "under what condition does X run?" (CDG, incl. guard clauses) and \`pdg_query({mode: "flows", target, variable})\` traces "where does variable Y flow?" (REACHING_DEF). \`--pdg\` layer.`
|
|
152
152
|
: ''}
|
package/dist/cli/eval-server.js
CHANGED
|
@@ -240,6 +240,17 @@ function formatTruncationSuffix(result) {
|
|
|
240
240
|
: '';
|
|
241
241
|
return label ? ` (by ${label})` : '';
|
|
242
242
|
}
|
|
243
|
+
function pushCallgraphRiskLines(lines, result) {
|
|
244
|
+
if (result.risk) {
|
|
245
|
+
lines.push(`Risk: ${result.risk}`);
|
|
246
|
+
}
|
|
247
|
+
if (result.riskNote) {
|
|
248
|
+
lines.push(String(result.riskNote));
|
|
249
|
+
}
|
|
250
|
+
if (result.riskScale?.comparableAcrossKinds === false && result.riskSharedAxes) {
|
|
251
|
+
lines.push(`Shared-axes risk: ${result.riskSharedAxes} (process/module axes are unavailable — compare File vs symbol only; do not use this to waive a HIGH/CRITICAL risk warning)`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
243
254
|
export function formatImpactResult(result) {
|
|
244
255
|
if (result.error) {
|
|
245
256
|
const suggestion = result.suggestion ? `\nSuggestion: ${result.suggestion}` : '';
|
|
@@ -467,15 +478,20 @@ export function formatImpactResult(result) {
|
|
|
467
478
|
// #1858 — "isolated" is a confident claim. If an interface / indirection
|
|
468
479
|
// boundary is on the path, the true count is a lower bound, not zero;
|
|
469
480
|
// callers binding via DI / dynamic dispatch were not traced. Say so instead.
|
|
481
|
+
const lines = [];
|
|
470
482
|
if (result.epistemic === 'lower-bound') {
|
|
471
|
-
|
|
472
|
-
`${target?.name || '?'}: no direct ${direction} dependencies traced, but this is a LOWER BOUND — unresolved indirection on the path (actual impact may be higher):`,
|
|
473
|
-
];
|
|
483
|
+
lines.push(`${target?.name || '?'}: no direct ${direction} dependencies traced, but this is a LOWER BOUND — unresolved indirection on the path (actual impact may be higher):`);
|
|
474
484
|
for (const b of result.boundaries || [])
|
|
475
485
|
lines.push(` • ${b}`);
|
|
476
|
-
return lines.join('\n');
|
|
477
486
|
}
|
|
478
|
-
|
|
487
|
+
else if (direction === 'upstream') {
|
|
488
|
+
lines.push(`${target?.name || '?'}: No ${direction} callers resolved. This is not evidence the symbol is unused or isolated.`);
|
|
489
|
+
}
|
|
490
|
+
else {
|
|
491
|
+
lines.push(`${target?.name || '?'}: No ${direction} dependencies found.`);
|
|
492
|
+
}
|
|
493
|
+
pushCallgraphRiskLines(lines, result);
|
|
494
|
+
return lines.join('\n');
|
|
479
495
|
}
|
|
480
496
|
const lines = [];
|
|
481
497
|
const dirLabel = direction === 'upstream' ? 'depends on this (will break if changed)' : 'this depends on';
|
|
@@ -490,6 +506,7 @@ export function formatImpactResult(result) {
|
|
|
490
506
|
for (const b of result.boundaries || [])
|
|
491
507
|
lines.push(` • ${b}`);
|
|
492
508
|
}
|
|
509
|
+
pushCallgraphRiskLines(lines, result);
|
|
493
510
|
lines.push('');
|
|
494
511
|
const depthLabels = {
|
|
495
512
|
1: 'WILL BREAK (direct)',
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Cross-repo impact (Phase 1 local walk + Phase 2 bridge fan-out).
|
|
3
3
|
* All bridge Cypher for this feature lives in this module.
|
|
4
4
|
*/
|
|
5
|
+
import type { ImpactRisk } from '../../_shared/index.js';
|
|
5
6
|
import type { BridgeHandle, BridgeMeta, CrossRepoImpact, GroupImpactResult } from './types.js';
|
|
6
7
|
import type { GroupToolPort } from './service.js';
|
|
7
8
|
export { truncationFields, crossRepoCompleteness, type TruncationFields, type CrossRepoCompleteness, type CrossRepoCompletenessInput, } from './completeness.js';
|
|
@@ -109,7 +110,7 @@ export declare function collectImpactSymbolUids(local: unknown, servicePrefix: s
|
|
|
109
110
|
uids: string[];
|
|
110
111
|
targetFilePath?: string;
|
|
111
112
|
};
|
|
112
|
-
export declare function mergeRisk(localRisk:
|
|
113
|
+
export declare function mergeRisk(localRisk: ImpactRisk, cross: CrossRepoImpact[]): ImpactRisk;
|
|
113
114
|
/**
|
|
114
115
|
* Is this bridge's metadata unable to say where its contents came from?
|
|
115
116
|
*
|
|
@@ -279,6 +279,15 @@ function extractProcessNames(impact) {
|
|
|
279
279
|
// permanently that a PDG `risk:'UNKNOWN'` never coalesces to a confident `LOW`.
|
|
280
280
|
// No behavior change — `'UNKNOWN'` was already handled correctly at the
|
|
281
281
|
// `(localRisk === 'LOW' || localRisk === 'UNKNOWN')` branch below.
|
|
282
|
+
function asImpactRisk(value, fallback = 'LOW') {
|
|
283
|
+
return value === 'LOW' ||
|
|
284
|
+
value === 'MEDIUM' ||
|
|
285
|
+
value === 'HIGH' ||
|
|
286
|
+
value === 'CRITICAL' ||
|
|
287
|
+
value === 'UNKNOWN'
|
|
288
|
+
? value
|
|
289
|
+
: fallback;
|
|
290
|
+
}
|
|
282
291
|
export function mergeRisk(localRisk, cross) {
|
|
283
292
|
const traversed = cross.filter((c) => c.fanout_status !== 'not_attempted');
|
|
284
293
|
const highConf = traversed.some((c) => c.contract.confidence >= 0.85);
|
|
@@ -292,6 +301,15 @@ export function mergeRisk(localRisk, cross) {
|
|
|
292
301
|
return 'MEDIUM';
|
|
293
302
|
return localRisk;
|
|
294
303
|
}
|
|
304
|
+
function liftLocalRiskMeta(local, cross) {
|
|
305
|
+
const { riskSharedAxes, riskScale } = local;
|
|
306
|
+
return {
|
|
307
|
+
...(riskSharedAxes !== undefined
|
|
308
|
+
? { riskSharedAxes: mergeRisk(asImpactRisk(riskSharedAxes), cross) }
|
|
309
|
+
: {}),
|
|
310
|
+
...(riskScale !== undefined ? { riskScale } : {}),
|
|
311
|
+
};
|
|
312
|
+
}
|
|
295
313
|
/**
|
|
296
314
|
* Is this bridge's metadata unable to say where its contents came from?
|
|
297
315
|
*
|
|
@@ -461,6 +479,7 @@ export async function runGroupImpact(deps, params) {
|
|
|
461
479
|
cross_repo_hits: 0,
|
|
462
480
|
},
|
|
463
481
|
risk: 'UNKNOWN',
|
|
482
|
+
...liftLocalRiskMeta(local, []),
|
|
464
483
|
timeoutMs,
|
|
465
484
|
crossDepthWarning,
|
|
466
485
|
};
|
|
@@ -513,7 +532,8 @@ export async function runGroupImpact(deps, params) {
|
|
|
513
532
|
modules_affected: s.modules_affected ?? 0,
|
|
514
533
|
cross_repo_hits: 0,
|
|
515
534
|
},
|
|
516
|
-
risk:
|
|
535
|
+
risk: asImpactRisk(local.risk),
|
|
536
|
+
...liftLocalRiskMeta(local, []),
|
|
517
537
|
timeoutMs,
|
|
518
538
|
crossDepthWarning,
|
|
519
539
|
};
|
|
@@ -666,7 +686,7 @@ export async function runGroupImpact(deps, params) {
|
|
|
666
686
|
await closeBridgeDb(handle);
|
|
667
687
|
}
|
|
668
688
|
const localSum = local?.summary || {};
|
|
669
|
-
const localRisk =
|
|
689
|
+
const localRisk = asImpactRisk(local.risk);
|
|
670
690
|
const localPartial = Boolean(local.partial);
|
|
671
691
|
// The bridge's own incompleteness, in the shared vocabulary, read through
|
|
672
692
|
// what this query DECLARED. The fan-out above already drops every neighbour
|
|
@@ -740,6 +760,7 @@ export async function runGroupImpact(deps, params) {
|
|
|
740
760
|
cross_repo_hits: cross.length,
|
|
741
761
|
},
|
|
742
762
|
risk: mergeRisk(localRisk, cross),
|
|
763
|
+
...liftLocalRiskMeta(local, cross),
|
|
743
764
|
timeoutMs,
|
|
744
765
|
crossDepthWarning,
|
|
745
766
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ImpactRisk, ImpactRiskResult } from '../../_shared/index.js';
|
|
1
2
|
export type ContractType = 'http' | 'graphql' | 'grpc' | 'thrift' | 'topic' | 'lib' | 'custom' | 'include';
|
|
2
3
|
export type ManifestContractType = Exclude<ContractType, 'graphql'>;
|
|
3
4
|
export type MatchType = 'exact' | 'manifest' | 'wildcard';
|
|
@@ -166,7 +167,17 @@ export interface GroupImpactResult {
|
|
|
166
167
|
modules_affected: number;
|
|
167
168
|
cross_repo_hits: number;
|
|
168
169
|
};
|
|
169
|
-
risk:
|
|
170
|
+
risk: ImpactRisk;
|
|
171
|
+
/**
|
|
172
|
+
* Two-axis (direct + total) risk from the local leg, then `mergeRisk` with
|
|
173
|
+
* crossings — compare File vs symbol here, not via top-level `risk`.
|
|
174
|
+
*/
|
|
175
|
+
riskSharedAxes?: ImpactRisk;
|
|
176
|
+
/**
|
|
177
|
+
* Local-leg scale metadata (File / skipped enrichment). Crossings do not
|
|
178
|
+
* invent process/module membership for File nodes.
|
|
179
|
+
*/
|
|
180
|
+
riskScale?: ImpactRiskResult['riskScale'];
|
|
170
181
|
/**
|
|
171
182
|
* `'lower-bound'` when the fan-out was cut short, so `risk` is a FLOOR, not a
|
|
172
183
|
* verdict. Same vocabulary as single-repo `impact`'s `epistemic` field.
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import fs from 'fs/promises';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { createHash } from 'crypto';
|
|
11
|
+
import { scoreImpactRisk, unusedAxesForImpactWalk } from '../../_shared/index.js';
|
|
11
12
|
import { initLbug, executeQuery, executeParameterized, ensureVectorExtension, closeLbug, isLbugReady, statDbIdentity, dbIdentityChanged, } from '../../core/lbug/pool-adapter.js';
|
|
12
13
|
import { queryClassBeanMetadata } from './bean-metadata.js';
|
|
13
14
|
import { querySpringAopMetadata } from './aop-metadata.js';
|
|
@@ -5230,6 +5231,10 @@ export class LocalBackend {
|
|
|
5230
5231
|
score: Number(c.score.toFixed(2)),
|
|
5231
5232
|
impactedCount: summary?.impactedCount ?? 0,
|
|
5232
5233
|
risk: summary?.risk ?? 'UNKNOWN',
|
|
5234
|
+
...(summary?.riskSharedAxes !== undefined
|
|
5235
|
+
? { riskSharedAxes: summary.riskSharedAxes }
|
|
5236
|
+
: {}),
|
|
5237
|
+
...(summary?.riskScale !== undefined ? { riskScale: summary.riskScale } : {}),
|
|
5233
5238
|
direct: summary?.summary?.direct ?? 0,
|
|
5234
5239
|
...(summary?.riskNote !== undefined ? { riskNote: summary.riskNote } : {}),
|
|
5235
5240
|
// Carry the explanation with the verdict. The single-symbol path
|
|
@@ -6122,6 +6127,10 @@ export class LocalBackend {
|
|
|
6122
6127
|
const rawMaxChunks = process.env.IMPACT_MAX_CHUNKS?.trim();
|
|
6123
6128
|
const parsedMaxChunks = rawMaxChunks ? Number(rawMaxChunks) : Number.NaN;
|
|
6124
6129
|
const MAX_CHUNKS = Number.isInteger(parsedMaxChunks) && parsedMaxChunks >= 0 ? parsedMaxChunks : 10;
|
|
6130
|
+
let processQueryFailed = false;
|
|
6131
|
+
let moduleQueryFailed = false;
|
|
6132
|
+
let enrichmentDegraded = false;
|
|
6133
|
+
let moduleClassificationFailed = false;
|
|
6125
6134
|
// `skipEnrichment` (ambiguous #2129 per-candidate probes) bypasses the
|
|
6126
6135
|
// process/module aggregation passes entirely — those probes need only the
|
|
6127
6136
|
// count + a count-based risk, so paying the bounded-but-real enrichment cost
|
|
@@ -6155,7 +6164,12 @@ export class LocalBackend {
|
|
|
6155
6164
|
p.entryPointId AS entryPointId, hits, minStep, p.stepCount AS stepCount,
|
|
6156
6165
|
ep.name AS epName, labels(ep)[0] AS epType, ep.filePath AS epFilePath
|
|
6157
6166
|
ORDER BY pId
|
|
6158
|
-
`, { ids }).catch(() =>
|
|
6167
|
+
`, { ids }).catch((err) => {
|
|
6168
|
+
processQueryFailed = true;
|
|
6169
|
+
enrichmentDegraded = true;
|
|
6170
|
+
logQueryError('impact:process-chunk', err);
|
|
6171
|
+
return [];
|
|
6172
|
+
});
|
|
6159
6173
|
for (const row of rows) {
|
|
6160
6174
|
const pId = row.pId ?? row[0];
|
|
6161
6175
|
const epId = row.entryPointId ?? row[3] ?? row.pId ?? row[0];
|
|
@@ -6203,6 +6217,8 @@ export class LocalBackend {
|
|
|
6203
6217
|
}
|
|
6204
6218
|
}
|
|
6205
6219
|
catch (e) {
|
|
6220
|
+
processQueryFailed = true;
|
|
6221
|
+
enrichmentDegraded = true;
|
|
6206
6222
|
logQueryError('impact:process-chunk', e);
|
|
6207
6223
|
}
|
|
6208
6224
|
}
|
|
@@ -6217,7 +6233,11 @@ export class LocalBackend {
|
|
|
6217
6233
|
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
6218
6234
|
WHERE p.id IN $pIds AND s.id IN $ids
|
|
6219
6235
|
RETURN p.id AS pid, MIN(r.step) AS minStep
|
|
6220
|
-
`, { pIds, ids: allImpactedIds }).catch(() =>
|
|
6236
|
+
`, { pIds, ids: allImpactedIds }).catch((err) => {
|
|
6237
|
+
enrichmentDegraded = true;
|
|
6238
|
+
logQueryError('impact:process-chunk-backfill', err);
|
|
6239
|
+
return [];
|
|
6240
|
+
});
|
|
6221
6241
|
for (const mr of missingRows) {
|
|
6222
6242
|
const pid = mr.pid ?? mr[0];
|
|
6223
6243
|
const minStep = mr.minStep ?? mr[1];
|
|
@@ -6233,6 +6253,7 @@ export class LocalBackend {
|
|
|
6233
6253
|
}
|
|
6234
6254
|
}
|
|
6235
6255
|
catch (e) {
|
|
6256
|
+
enrichmentDegraded = true;
|
|
6236
6257
|
logQueryError('impact:process-chunk-backfill', e);
|
|
6237
6258
|
}
|
|
6238
6259
|
}
|
|
@@ -6280,7 +6301,12 @@ export class LocalBackend {
|
|
|
6280
6301
|
RETURN c.heuristicLabel AS name, COUNT(DISTINCT s.id) AS hits
|
|
6281
6302
|
ORDER BY hits DESC, name
|
|
6282
6303
|
LIMIT 20
|
|
6283
|
-
`, { ids: idsChunk }).catch(() =>
|
|
6304
|
+
`, { ids: idsChunk }).catch((err) => {
|
|
6305
|
+
moduleQueryFailed = true;
|
|
6306
|
+
enrichmentDegraded = true;
|
|
6307
|
+
logQueryError('impact:module-chunk', err);
|
|
6308
|
+
return [];
|
|
6309
|
+
});
|
|
6284
6310
|
for (const r of rows) {
|
|
6285
6311
|
const name = r.name ?? r[0] ?? null;
|
|
6286
6312
|
const hits = (r.hits ?? r[1]) || 0;
|
|
@@ -6290,6 +6316,8 @@ export class LocalBackend {
|
|
|
6290
6316
|
}
|
|
6291
6317
|
}
|
|
6292
6318
|
catch (e) {
|
|
6319
|
+
moduleQueryFailed = true;
|
|
6320
|
+
enrichmentDegraded = true;
|
|
6293
6321
|
logQueryError('impact:module-chunk', e);
|
|
6294
6322
|
}
|
|
6295
6323
|
};
|
|
@@ -6310,7 +6338,12 @@ export class LocalBackend {
|
|
|
6310
6338
|
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
6311
6339
|
WHERE s.id IN $ids
|
|
6312
6340
|
RETURN DISTINCT c.heuristicLabel AS name
|
|
6313
|
-
`, { ids: idsChunk }).catch(() =>
|
|
6341
|
+
`, { ids: idsChunk }).catch((err) => {
|
|
6342
|
+
enrichmentDegraded = true;
|
|
6343
|
+
moduleClassificationFailed = true;
|
|
6344
|
+
logQueryError('impact:direct-module-chunk', err);
|
|
6345
|
+
return [];
|
|
6346
|
+
});
|
|
6314
6347
|
for (const r of rows) {
|
|
6315
6348
|
const name = r.name ?? r[0] ?? null;
|
|
6316
6349
|
if (name)
|
|
@@ -6318,6 +6351,8 @@ export class LocalBackend {
|
|
|
6318
6351
|
}
|
|
6319
6352
|
}
|
|
6320
6353
|
catch (e) {
|
|
6354
|
+
enrichmentDegraded = true;
|
|
6355
|
+
moduleClassificationFailed = true;
|
|
6321
6356
|
logQueryError('impact:direct-module-chunk', e);
|
|
6322
6357
|
}
|
|
6323
6358
|
};
|
|
@@ -6338,47 +6373,35 @@ export class LocalBackend {
|
|
|
6338
6373
|
return {
|
|
6339
6374
|
name,
|
|
6340
6375
|
hits,
|
|
6341
|
-
impact:
|
|
6376
|
+
impact: moduleClassificationFailed
|
|
6377
|
+
? 'classification-unavailable'
|
|
6378
|
+
: directModuleNameSet.has(name)
|
|
6379
|
+
? 'direct'
|
|
6380
|
+
: 'indirect',
|
|
6342
6381
|
};
|
|
6343
6382
|
});
|
|
6344
6383
|
}
|
|
6345
6384
|
// Risk scoring
|
|
6346
6385
|
const processCount = affectedProcesses.length;
|
|
6347
6386
|
const moduleCount = affectedModules.length;
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6355
|
-
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
|
|
6364
|
-
|
|
6365
|
-
|
|
6366
|
-
moduleCount >= 5 ||
|
|
6367
|
-
impacted.length >= 200) {
|
|
6368
|
-
risk = 'CRITICAL';
|
|
6369
|
-
}
|
|
6370
|
-
else if (directCount >= 15 ||
|
|
6371
|
-
processCount >= 3 ||
|
|
6372
|
-
moduleCount >= 3 ||
|
|
6373
|
-
impacted.length >= 100) {
|
|
6374
|
-
risk = 'HIGH';
|
|
6375
|
-
}
|
|
6376
|
-
else if (directCount >= 5 || impacted.length >= 30) {
|
|
6377
|
-
risk = 'MEDIUM';
|
|
6378
|
-
}
|
|
6379
|
-
else {
|
|
6380
|
-
risk = 'LOW';
|
|
6381
|
-
}
|
|
6387
|
+
const isFileTarget = symType === 'File' || String(symId).startsWith('File:');
|
|
6388
|
+
const unusedAxes = unusedAxesForImpactWalk({
|
|
6389
|
+
isFileTarget,
|
|
6390
|
+
skipEnrichment,
|
|
6391
|
+
maxChunks: MAX_CHUNKS,
|
|
6392
|
+
processQueryFailed,
|
|
6393
|
+
moduleQueryFailed,
|
|
6394
|
+
impactedCount: impacted.length,
|
|
6395
|
+
enrichmentTruncated: !skipEnrichment && MAX_CHUNKS > 0 && impacted.length > MAX_CHUNKS * CHUNK_SIZE,
|
|
6396
|
+
});
|
|
6397
|
+
const { risk, riskSharedAxes, riskScale } = scoreImpactRisk({
|
|
6398
|
+
direction,
|
|
6399
|
+
directCount,
|
|
6400
|
+
processCount,
|
|
6401
|
+
moduleCount,
|
|
6402
|
+
impactedCount: impacted.length,
|
|
6403
|
+
unusedAxes,
|
|
6404
|
+
});
|
|
6382
6405
|
// Build per-depth counts (always included, even in summaryOnly mode)
|
|
6383
6406
|
const byDepthCounts = {};
|
|
6384
6407
|
for (const [depth, items] of Object.entries(grouped)) {
|
|
@@ -6395,7 +6418,7 @@ export class LocalBackend {
|
|
|
6395
6418
|
target: {
|
|
6396
6419
|
id: symId,
|
|
6397
6420
|
name: sym.name || sym[1],
|
|
6398
|
-
type: symType,
|
|
6421
|
+
type: isFileTarget ? symType || 'File' : symType,
|
|
6399
6422
|
filePath: sym.filePath || sym[2],
|
|
6400
6423
|
...(beanMetadata ? { bean: beanMetadata } : {}),
|
|
6401
6424
|
...(aopMetadata ? { aop: aopMetadata } : {}),
|
|
@@ -6403,17 +6426,26 @@ export class LocalBackend {
|
|
|
6403
6426
|
direction,
|
|
6404
6427
|
impactedCount: impacted.length,
|
|
6405
6428
|
risk,
|
|
6429
|
+
riskSharedAxes,
|
|
6430
|
+
riskScale,
|
|
6406
6431
|
...(risk === 'UNKNOWN'
|
|
6407
6432
|
? {
|
|
6408
|
-
riskNote:
|
|
6409
|
-
'
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6433
|
+
riskNote: processQueryFailed || moduleQueryFailed
|
|
6434
|
+
? 'Risk is unresolved because process/module enrichment failed. Observed counts ' +
|
|
6435
|
+
'are lower bounds; retry impact before treating the change as safe.'
|
|
6436
|
+
: unusedAxes.some((axis) => axis.reason === 'enrichment-truncated')
|
|
6437
|
+
? 'Risk is unresolved because process/module enrichment was truncated. Observed ' +
|
|
6438
|
+
'counts are lower bounds; retry with a higher IMPACT_MAX_CHUNKS before ' +
|
|
6439
|
+
'treating the change as safe.'
|
|
6440
|
+
: 'No callers resolved. Absence of edges is not evidence the symbol is unused: ' +
|
|
6441
|
+
'a caller reaching it through a reference class this index does not record — ' +
|
|
6442
|
+
'plain-object property access, a bare-identifier read of a module-scope const — ' +
|
|
6443
|
+
'produces no edge to find. Confirm with a text search before treating the ' +
|
|
6444
|
+
'change as safe.',
|
|
6413
6445
|
}
|
|
6414
6446
|
: {}),
|
|
6415
6447
|
...epistemic,
|
|
6416
|
-
...(!traversalComplete && { partial: true }),
|
|
6448
|
+
...((!traversalComplete || enrichmentDegraded) && { partial: true }),
|
|
6417
6449
|
summary: {
|
|
6418
6450
|
direct: directCount,
|
|
6419
6451
|
processes_affected: processCount,
|
package/dist/mcp/tools.js
CHANGED
|
@@ -431,11 +431,13 @@ WHEN TO USE: Before making code changes — especially refactoring, renaming, or
|
|
|
431
431
|
AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols.
|
|
432
432
|
|
|
433
433
|
Output includes:
|
|
434
|
-
- risk: LOW / MEDIUM / HIGH / CRITICAL / UNKNOWN. An upstream walk that resolved ZERO callers reports UNKNOWN, never LOW, and carries riskNote: "safe to change" is a claim about callers and there were none to reason about, so the symbol is either genuinely unused OR reached only through a reference class the index does not record (plain-object property access, a bare-identifier read of a module-scope const). Confirm with a text search before acting on it. Downstream walks are unaffected — an empty downstream result reports resolved callees, not safety.
|
|
434
|
+
- risk: LOW / MEDIUM / HIGH / CRITICAL / UNKNOWN. This is the HIGH/CRITICAL edit-gate field. File targets lack process/community membership, so their risk is not directly comparable with symbol risk; use riskSharedAxes to compare the direct/total axes common to both. Group-mode (\`repo: "@…"\`) results lift the same fields to the top-level envelope. The web Graph-RAG impact tool expands File targets to in-file symbols before enrichment, so process/cluster axes remain comparable there. An upstream walk that resolved ZERO callers reports UNKNOWN, never LOW, and carries riskNote: "safe to change" is a claim about callers and there were none to reason about, so the symbol is either genuinely unused OR reached only through a reference class the index does not record (plain-object property access, a bare-identifier read of a module-scope const). Confirm with a text search before acting on it. Downstream walks are unaffected — an empty downstream result reports resolved callees, not safety.
|
|
435
|
+
- riskSharedAxes: single-repo risk computed only from direct and total impact. Group mode then applies the cross-repo crossing overlay to that local value. Suitable for comparing File and symbol targets within the same mode. Never substitute it for \`risk\` when deciding whether to warn before edits.
|
|
436
|
+
- riskScale: { comparableAcrossKinds, unusedAxes } — names process/module axes that were structurally unavailable, skipped, budget-exhausted (\`IMPACT_MAX_CHUNKS=0\`), truncated (sampled a subset of impacted symbols), or failed at query time. Failed-query and truncated-sample counts are lower bounds: known HIGH/CRITICAL warnings survive, otherwise risk is UNKNOWN. Group impact copies this metadata from the local leg.
|
|
435
437
|
- riskNote: string — present only when risk is UNKNOWN; states why the verdict is withheld.
|
|
436
438
|
- summary: direct callers, processes affected, modules affected
|
|
437
439
|
- affected_processes: which execution flows break and at which step
|
|
438
|
-
- affected_modules: which functional areas are hit (direct vs indirect)
|
|
440
|
+
- affected_modules: which functional areas are hit (direct vs indirect; classification-unavailable when that secondary query fails)
|
|
439
441
|
- 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). Each item includes a processes:[{id,label,processType,step}] field listing the execution flows that symbol participates in. Empty when the symbol has no process membership. Can ALSO be empty when partial:true is set — either the process-aggregation pass hit its cap before detecting affected processes, or per-symbol enrichment was capped on a very large page. When partial:true, do NOT treat processes:[] as proof of no participation; cross-check the top-level affected_processes list.
|
|
440
442
|
- epistemic: 'exact' | 'lower-bound' — whether impactedCount is the whole story. 'lower-bound' means the walk provably missed callers, so the count is a floor. Absent only on skipped probes (ambiguous-candidate lists, group fan-out).
|
|
441
443
|
- boundaries: string[] — one plain-language sentence per reason the count is short. Prose for humans; branch on causes instead.
|
package/package.json
CHANGED
|
@@ -93,6 +93,15 @@ dispatch, cross-language calls), so few-callers ⇒ LOW does **not** apply. The
|
|
|
93
93
|
result carries a `riskNote` saying so. Confirm with a text search before
|
|
94
94
|
treating the symbol as safe to change or delete.
|
|
95
95
|
|
|
96
|
+
`risk` is the edit gate: warn on HIGH/CRITICAL and stop on UNKNOWN until the
|
|
97
|
+
uncertainty is resolved. Within single-repo mode, compare File and symbol
|
|
98
|
+
targets with local `riskSharedAxes` (direct/total only). Within group mode,
|
|
99
|
+
compare only group results: their `riskSharedAxes` overlays resolved
|
|
100
|
+
cross-repo crossings on that local value. Never use either field to waive the
|
|
101
|
+
edit gate. Check `riskScale.unusedAxes` before comparing kinds: MCP File walks
|
|
102
|
+
omit process/module axes, while web Graph-RAG expands File targets to in-file
|
|
103
|
+
symbols before enrichment.
|
|
104
|
+
|
|
96
105
|
## Tools
|
|
97
106
|
|
|
98
107
|
**impact** — the primary tool for symbol blast radius. If MCP is unavailable, use `node .gitnexus/run.cjs impact <symbol> --direction upstream --repo .` instead:
|