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.
@@ -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?: number;
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;AA0CD,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,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,oBAAoB,EAAE,CAiCzB;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,eAAe,GAAG,gBAAgB,CAWxE"}
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
- function countsWithUnusedAxesZeroed(input) {
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 && (input.impactedCount ?? 1) > 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: score(countsWithUnusedAxesZeroed(input)),
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":"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"}
1
+ {"version":3,"file":"impact-risk.js","sourceRoot":"","sources":["../src/impact-risk.ts"],"names":[],"mappings":"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"}
@@ -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 analysis before editing.** Use \`impact({target: "symbolName", direction: "upstream"})\` (MCP) or \`${runner} impact "symbolName" --direction upstream --repo .\` (CLI fallback); report callers, processes, and risk. Never substitute grep for graph analysis.${hasPdg
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
- - **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits.
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
- - When exploring unfamiliar code, use \`query({search_query: "concept"})\` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance.
149
- - When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use \`context({name: "symbolName"})\`.
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
  : ''}
@@ -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
- const lines = [
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
- return `${target?.name || '?'}: No ${direction} dependencies found. This symbol appears isolated.`;
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: string, cross: CrossRepoImpact[]): string;
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: String(local.risk ?? 'LOW'),
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 = String(local.risk ?? 'LOW');
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: string;
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: directModuleNameSet.has(name) ? 'direct' : 'indirect',
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
- let risk;
6349
- if (direction === 'upstream' && impacted.length === 0) {
6350
- // An upstream walk that resolved NO callers cannot support `LOW`. "Safe
6351
- // to change" is a claim ABOUT callers, and this walk found none to reason
6352
- // about: the symbol may be genuinely unused, or reached only through a
6353
- // reference class this index does not record — a property access on a
6354
- // plain object, or a bare-identifier read of a module-scope `Const`,
6355
- // neither of which mints a reference site today. Seeding `LOW` from an
6356
- // empty result is the same false-safe signal `anyKnownRisk` refuses to
6357
- // emit on the ambiguous-candidate path, and that #2687 removed by making
6358
- // an undetermined `impactedCount` `null` instead of `0`.
6359
- //
6360
- // Downstream is deliberately untouched: an empty downstream walk reports
6361
- // that this symbol resolved no callees, which is not a safety verdict.
6362
- risk = 'UNKNOWN';
6363
- }
6364
- else if (directCount >= 30 ||
6365
- processCount >= 5 ||
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: 'No callers resolved. Absence of edges is not evidence the symbol is unused: ' +
6409
- 'a caller reaching it through a reference class this index does not record — ' +
6410
- 'plain-object property access, a bare-identifier read of a module-scope const — ' +
6411
- 'produces no edge to find. Confirm with a text search before treating the ' +
6412
- 'change as safe.',
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.11-rc.6",
3
+ "version": "1.6.11-rc.7",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",
@@ -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: