gitnexus 1.6.11-rc.3 → 1.6.11-rc.4

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.
@@ -3000,7 +3000,22 @@ export class LocalBackend {
3000
3000
  else if (isQualified) {
3001
3001
  // Parenthesised because the kind filter below is appended with AND, which
3002
3002
  // binds tighter than OR.
3003
- whereClause = `WHERE (n.id = $symName OR n.name = $symName)`;
3003
+ // #3074: a repo-relative file path (e.g. "supabase/functions/_shared/crypto.ts")
3004
+ // is the most natural way to name a File and is exactly what `target.filePath`
3005
+ // reports, but the old clause only matched `n.id` (= "File:<path>") or basename
3006
+ // `n.name`, so the same path the graph stores never resolved. Also match the
3007
+ // repo-relative `n.filePath` exactly and via an anchored suffix (segment-boundary
3008
+ // "ENDS WITH $suffix" where suffix is "/"+path) so "a.ts" does not spuriously
3009
+ // match "mylib/a.ts" — same anchoring used in detect_changes (#2915).
3010
+ const suffix = pathSuffixOf(name);
3011
+ // File-path terms must be scoped to File nodes — n.filePath is shared by
3012
+ // every symbol in the file, so an unlabeled predicate would turn
3013
+ // "src/actions.ts" into every symbol in that file (bot review #3084 P1).
3014
+ // LadybugDB does not allow label tests in WHERE (n:File), so scope via
3015
+ // id prefix — File nodes are `File:<path>`.
3016
+ whereClause = `WHERE (n.id = $symName OR n.name = $symName OR (n.id STARTS WITH $filePrefix AND (n.filePath = $symName OR n.filePath ENDS WITH $suffix)))`;
3017
+ queryParams.suffix = suffix;
3018
+ queryParams.filePrefix = 'File:';
3004
3019
  }
3005
3020
  else {
3006
3021
  whereClause = `WHERE n.name = $symName`;
@@ -3079,7 +3094,7 @@ export class LocalBackend {
3079
3094
  if (rows.length === 0)
3080
3095
  return { kind: 'not_found' };
3081
3096
  // Normalise row shape across object / tuple returns from LadybugDB.
3082
- const normalized = rows.map((r) => ({
3097
+ let normalized = rows.map((r) => ({
3083
3098
  id: (r.id ?? r[0]),
3084
3099
  name: (r.name ?? r[1]),
3085
3100
  type: (r.type ?? r[2] ?? ''),
@@ -3088,6 +3103,14 @@ export class LocalBackend {
3088
3103
  endLine: (r.endLine ?? r[5]),
3089
3104
  ...(include_content ? { content: (r.content ?? r[6]) } : {}),
3090
3105
  }));
3106
+ // An exact File path wins over anchored suffix candidates. Without this,
3107
+ // `lib/a.ts` and `src/lib/a.ts` both score as File candidates and turn an
3108
+ // otherwise unambiguous exact target into `ambiguous` (#3084 review P2).
3109
+ if (isQualified) {
3110
+ const exactFiles = normalized.filter((candidate) => candidate.id.startsWith('File:') && candidate.filePath === name);
3111
+ if (exactFiles.length > 0)
3112
+ normalized = exactFiles;
3113
+ }
3091
3114
  // The COUNT can never legitimately be below the page it accompanies, so a
3092
3115
  // value under `normalized.length` means the count leg failed or returned an
3093
3116
  // unreadable shape. Keep the window size as the floor — reporting zero would
@@ -4942,6 +4965,7 @@ export class LocalBackend {
4942
4965
  direction: params.direction,
4943
4966
  suggestion,
4944
4967
  recoverySuggestion,
4968
+ undetermined: true,
4945
4969
  });
4946
4970
  return pdgErr;
4947
4971
  }
@@ -4949,7 +4973,7 @@ export class LocalBackend {
4949
4973
  error: message,
4950
4974
  target: { name: params.target },
4951
4975
  direction: params.direction,
4952
- impactedCount: 0,
4976
+ impactedCount: null,
4953
4977
  risk: 'UNKNOWN',
4954
4978
  suggestion,
4955
4979
  ...(recoverySuggestion ? { recoverySuggestion } : {}),
@@ -5036,6 +5060,7 @@ export class LocalBackend {
5036
5060
  `(single-repo PDG impact). Remove them or use mode:'callgraph' for cross-repo fan-out.`,
5037
5061
  target: crossDepthTarget,
5038
5062
  direction,
5063
+ undetermined: true,
5039
5064
  });
5040
5065
  return pdgErr;
5041
5066
  }
@@ -5091,12 +5116,17 @@ export class LocalBackend {
5091
5116
  error: `Target '${missing}' not found`,
5092
5117
  target: notFoundTarget,
5093
5118
  direction,
5119
+ undetermined: true,
5094
5120
  })
5095
5121
  : {
5096
5122
  error: `Target '${missing}' not found`,
5097
5123
  target: { name: target },
5098
5124
  direction,
5099
- impactedCount: 0,
5125
+ // #3074 follow-up: do not ship a normal-shaped 0/UNKNOWN blast radius
5126
+ // alongside the error — it reads as a real "nothing depends on this"
5127
+ // answer. Null marks UNDETERMINED (same as the ambiguous path) so a
5128
+ // consumer testing `impactedCount === 0` cannot misread a miss as safe.
5129
+ impactedCount: null,
5100
5130
  risk: 'UNKNOWN',
5101
5131
  };
5102
5132
  }
@@ -31,8 +31,10 @@ export declare function splitCalleeIds(raw: unknown): string[];
31
31
  * Bump on any breaking change to the PDG result fields.
32
32
  * v2: `startLine` in the result is now 1-based display (#2380), matching the
33
33
  * context/query/impact tools (was 0-based).
34
+ * v3: error envelopes use `impactedCount: null` when a target cannot be resolved
35
+ * (#3074), so consumers cannot read a miss as a measured zero.
34
36
  */
35
- export declare const PDG_RESULT_VERSION: 2;
37
+ export declare const PDG_RESULT_VERSION: 3;
36
38
  /** A reachable dependence block resolved to its source statement. */
37
39
  export interface PdgStatement {
38
40
  /** 1-based source line where the statement's block starts. */
@@ -229,7 +231,7 @@ export interface PdgInterproceduralImpact {
229
231
  export interface PdgImpactBaseResult extends PdgImpactParityFields {
230
232
  mode: 'pdg';
231
233
  /** Contract version of the mode:'pdg' impact result shape; bump on any breaking change to the PDG result fields. */
232
- pdgResultVersion: 2;
234
+ pdgResultVersion: 3;
233
235
  target: PdgImpactTarget;
234
236
  direction: 'upstream' | 'downstream';
235
237
  impactedCount: number;
@@ -296,11 +298,11 @@ export interface PdgImpactDegradedResult extends PdgImpactBaseResult {
296
298
  export interface PdgImpactErrorResult {
297
299
  mode?: 'pdg';
298
300
  /** Contract version of the mode:'pdg' impact result shape; bump on any breaking change to the PDG result fields. */
299
- pdgResultVersion: 2;
301
+ pdgResultVersion: 3;
300
302
  error: string;
301
303
  target: PdgImpactTarget;
302
304
  direction: 'upstream' | 'downstream';
303
- impactedCount: 0;
305
+ impactedCount: number | null;
304
306
  risk: 'UNKNOWN';
305
307
  suggestion?: string;
306
308
  recoverySuggestion?: string;
@@ -313,6 +315,8 @@ export declare function makePdgImpactErrorResult(input: {
313
315
  mode?: 'pdg';
314
316
  suggestion?: string;
315
317
  recoverySuggestion?: string;
318
+ /** True when analysis did not obtain a measured impact count. */
319
+ undetermined?: boolean;
316
320
  }): PdgImpactErrorResult;
317
321
  export declare function isPdgDegradedLayerStatus(layer: PdgLayerStatus): layer is PdgDegradedLayerStatus;
318
322
  export declare function makePdgLayerDegradedResult(input: {
@@ -109,8 +109,10 @@ export function splitCalleeIds(raw) {
109
109
  * Bump on any breaking change to the PDG result fields.
110
110
  * v2: `startLine` in the result is now 1-based display (#2380), matching the
111
111
  * context/query/impact tools (was 0-based).
112
+ * v3: error envelopes use `impactedCount: null` when a target cannot be resolved
113
+ * (#3074), so consumers cannot read a miss as a measured zero.
112
114
  */
113
- export const PDG_RESULT_VERSION = 2;
115
+ export const PDG_RESULT_VERSION = 3;
114
116
  /**
115
117
  * FU-B-2 intra-block def→use line walk. Given a block's self REACHING_DEF
116
118
  * def→use line PAIRS (every `defLine → useLine` step decoded from the edge
@@ -469,7 +471,10 @@ export function makePdgImpactErrorResult(input) {
469
471
  error: input.error,
470
472
  target: input.target,
471
473
  direction: input.direction,
472
- impactedCount: 0,
474
+ // #3074 follow-up + P2 review: an unmeasured PDG result must not ship a
475
+ // confident-looking 0 blast radius. null marks UNDETERMINED, so a consumer
476
+ // testing === 0 cannot misread a miss or failed query as safe.
477
+ impactedCount: input.undetermined ? null : 0,
473
478
  risk: 'UNKNOWN',
474
479
  ...(input.suggestion ? { suggestion: input.suggestion } : {}),
475
480
  ...(input.recoverySuggestion ? { recoverySuggestion: input.recoverySuggestion } : {}),
package/dist/mcp/tools.js CHANGED
@@ -425,7 +425,7 @@ MODE (opt-in): "callgraph" (default) walks symbol→symbol edges (CALLS/IMPORTS/
425
425
 
426
426
  STATEMENT-ANCHORED PDG SLICE: with mode:'pdg', pass "line" (1-based source line within the target symbol) to seed the dependence slice on the statement at that line and return what depends on it in affectedStatements (line + text). Inter-procedural symbols are still reported through interproceduralByDepth/pdgInterprocedural and the compatibility byDepth bucket. Without "line", pdg returns whole-symbol inter-procedural reach plus local whole-symbol PDG diagnostics.
427
427
 
428
- PDG OUTPUT CONTRACT: every mode:'pdg' result (success, empty, degraded, or error) carries pdgResultVersion:2 — a stable discriminator for external consumers that bumps on any breaking change to the PDG result shape (distinct from the DB schema version). Successful PDG results include mode:'pdg', a full target envelope (id/name/type/filePath), affectedStatements, affectedStatementCount, interproceduralByDepth/pdgInterprocedural for cross-function reach, compatibility byDepth/byDepthCounts, risk:'UNKNOWN', and a note describing the unified contract. Degraded PDG results (no-layer, sub-layer-missing, unknown) keep mode:'pdg', pdgResultVersion:2, target metadata when the target resolves, risk:'UNKNOWN', note/remediation, and empty byDepth parity fields — never a false-safe zero. If depth and limit both bound the slice, truncatedByReasons reports both causes while truncatedBy remains scalar. Return-value-ascent coverage is published structurally at pdgEvidence.ascent — present iff the inter-procedural descent ran, including on an empty slice — with referencesScanned (DISTINCT callees scanned for a CALL_SUMMARY: a distinct-id tally, not a call-site count — two call sites to the same callee count once), returnFlowFound (whether the ascent fired anywhere in the slice), undecodableSummaryCount, examinedComplete (whether that scan covered every callee the index recorded a resolved id for on the visited blocks), incompleteReasons ('traversal-truncated' | 'callee-list-capped' | 'callee-ids-unrecorded'), and callSummaryLayerPresent. Read callSummaryLayerPresent FIRST: false ⇒ a pre-CALL_SUMMARY index, so {referencesScanned:N>0, returnFlowFound:false} is self-consistent and says nothing about the callees — the scan ran, but no layer existed in which a return-flow could be recorded (remedy: re-run gitnexus analyze --pdg). Branch on those fields; the note narrates the same facts in prose for humans and is not a stable contract.
428
+ PDG OUTPUT CONTRACT: every mode:'pdg' result (success, empty, degraded, or error) carries pdgResultVersion:3 — a stable discriminator for external consumers that bumps on any breaking change to the PDG result shape (distinct from the DB schema version). Successful PDG results include mode:'pdg', a full target envelope (id/name/type/filePath), affectedStatements, affectedStatementCount, interproceduralByDepth/pdgInterprocedural for cross-function reach, compatibility byDepth/byDepthCounts, risk:'UNKNOWN', and a note describing the unified contract. Degraded PDG results (no-layer, sub-layer-missing, unknown) keep mode:'pdg', pdgResultVersion:3, target metadata when the target resolves, risk:'UNKNOWN', note/remediation, and empty byDepth parity fields — never a false-safe zero. If depth and limit both bound the slice, truncatedByReasons reports both causes while truncatedBy remains scalar. Return-value-ascent coverage is published structurally at pdgEvidence.ascent — present iff the inter-procedural descent ran, including on an empty slice — with referencesScanned (DISTINCT callees scanned for a CALL_SUMMARY: a distinct-id tally, not a call-site count — two call sites to the same callee count once), returnFlowFound (whether the ascent fired anywhere in the slice), undecodableSummaryCount, examinedComplete (whether that scan covered every callee the index recorded a resolved id for on the visited blocks), incompleteReasons ('traversal-truncated' | 'callee-list-capped' | 'callee-ids-unrecorded'), and callSummaryLayerPresent. Read callSummaryLayerPresent FIRST: false ⇒ a pre-CALL_SUMMARY index, so {referencesScanned:N>0, returnFlowFound:false} is self-consistent and says nothing about the callees — the scan ran, but no layer existed in which a return-flow could be recorded (remedy: re-run gitnexus analyze --pdg). Branch on those fields; the note narrates the same facts in prose for humans and is not a stable contract.
429
429
 
430
430
  WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break.
431
431
  AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.11-rc.3",
3
+ "version": "1.6.11-rc.4",
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",