gitnexus 1.6.10-rc.4 → 1.6.10-rc.5

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.
@@ -7,6 +7,10 @@ import { logger } from '../../logger.js';
7
7
  // reserved-keyword labels `Macro` and `Union`, and LadybugDB's parser rejects
8
8
  // a disjunction that names a reserved keyword (#2325) — which the resolver's
9
9
  // try/catch then swallowed. `labels(n) IN` has no such collision.
10
+ // This list overlaps `ingestion/utils/symbol-labels.ts` (SYMBOL_NODE_LABELS) but
11
+ // is a deliberate SUBSET — it omits `Namespace`/`Variable`/`Module`. Unifying the
12
+ // two would widen which nodes resolve as contract symbols and must update the
13
+ // #2325 test, so they are intentionally kept separate for now.
10
14
  export const CUSTOM_CONTRACT_RESOLVE_QUERY = `MATCH (n)
11
15
  WHERE labels(n) IN ['Function','Method','Class','Interface','Struct','Enum','Trait','Constructor','TypeAlias','Impl','Macro','Union','Typedef','Property','Record','Delegate','Annotation','Template','Const','Static','CodeElement']
12
16
  AND n.name = $symbolName
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { parseJcl } from './jcl-parser.js';
19
19
  import { generateId } from '../../../lib/utils.js';
20
+ import { toZeroBasedLine } from '../utils/line-base.js';
20
21
  /**
21
22
  * Process JCL files and integrate into the knowledge graph.
22
23
  *
@@ -71,8 +72,8 @@ function integrateJclResults(graph, parsed, filePath, moduleNames) {
71
72
  properties: {
72
73
  name: job.name,
73
74
  filePath,
74
- startLine: job.line,
75
- endLine: job.line,
75
+ startLine: toZeroBasedLine(job.line),
76
+ endLine: toZeroBasedLine(job.line),
76
77
  description: `jcl-job${classPart}${msgPart}`,
77
78
  },
78
79
  });
@@ -105,8 +106,8 @@ function integrateJclResults(graph, parsed, filePath, moduleNames) {
105
106
  properties: {
106
107
  name: step.name,
107
108
  filePath,
108
- startLine: step.line,
109
- endLine: step.line,
109
+ startLine: toZeroBasedLine(step.line),
110
+ endLine: toZeroBasedLine(step.line),
110
111
  description: `jcl-step${pgmPart}${procPart}`,
111
112
  },
112
113
  });
@@ -170,8 +171,8 @@ function integrateJclResults(graph, parsed, filePath, moduleNames) {
170
171
  properties: {
171
172
  name: dd.dataset,
172
173
  filePath,
173
- startLine: dd.line,
174
- endLine: dd.line,
174
+ startLine: toZeroBasedLine(dd.line),
175
+ endLine: toZeroBasedLine(dd.line),
175
176
  description: `jcl-dataset${dispPart}`,
176
177
  },
177
178
  });
@@ -202,8 +203,8 @@ function integrateJclResults(graph, parsed, filePath, moduleNames) {
202
203
  properties: {
203
204
  name: proc.name,
204
205
  filePath,
205
- startLine: proc.line,
206
- endLine: proc.line,
206
+ startLine: toZeroBasedLine(proc.line),
207
+ endLine: toZeroBasedLine(proc.line),
207
208
  description: 'jcl-proc-instream',
208
209
  },
209
210
  });
@@ -14,6 +14,7 @@
14
14
  */
15
15
  import path from 'node:path';
16
16
  import { generateId } from '../../lib/utils.js';
17
+ import { toZeroBasedLine } from './utils/line-base.js';
17
18
  import { SupportedLanguages } from '../../_shared/index.js';
18
19
  import { preprocessCobolSource, extractCobolSymbolsWithRegex, } from './cobol/cobol-preprocessor.js';
19
20
  import { expandCopies } from './cobol/cobol-copy-expander.js';
@@ -275,8 +276,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
275
276
  properties: {
276
277
  name: extracted.programName,
277
278
  filePath,
278
- startLine: 1,
279
- endLine: lines.length,
279
+ startLine: toZeroBasedLine(1),
280
+ endLine: toZeroBasedLine(lines.length),
280
281
  language: SupportedLanguages.Cobol,
281
282
  isExported: true,
282
283
  description: metaDesc || undefined,
@@ -310,8 +311,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
310
311
  properties: {
311
312
  name: prog.name,
312
313
  filePath,
313
- startLine: prog.startLine,
314
- endLine: prog.endLine,
314
+ startLine: toZeroBasedLine(prog.startLine),
315
+ endLine: toZeroBasedLine(prog.endLine),
315
316
  language: SupportedLanguages.Cobol,
316
317
  isExported: true,
317
318
  description: `nested-program${prog.isCommon ? ' common' : ''}`,
@@ -349,8 +350,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
349
350
  properties: {
350
351
  name: sec.name,
351
352
  filePath,
352
- startLine: sec.line,
353
- endLine: nextLine,
353
+ startLine: toZeroBasedLine(sec.line),
354
+ endLine: toZeroBasedLine(nextLine),
354
355
  language: SupportedLanguages.Cobol,
355
356
  isExported: true,
356
357
  },
@@ -379,8 +380,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
379
380
  properties: {
380
381
  name: para.name,
381
382
  filePath,
382
- startLine: para.line,
383
- endLine: nextLine,
383
+ startLine: toZeroBasedLine(para.line),
384
+ endLine: toZeroBasedLine(nextLine),
384
385
  language: SupportedLanguages.Cobol,
385
386
  isExported: true,
386
387
  },
@@ -412,8 +413,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
412
413
  properties: {
413
414
  name: item.name,
414
415
  filePath,
415
- startLine: item.line,
416
- endLine: item.line,
416
+ startLine: toZeroBasedLine(item.line),
417
+ endLine: toZeroBasedLine(item.line),
417
418
  language: SupportedLanguages.Cobol,
418
419
  description: `level:${item.level} section:${item.section}${item.pic ? ` pic:${item.pic}` : ''}`,
419
420
  },
@@ -506,8 +507,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
506
507
  properties: {
507
508
  name: `CALL ${call.target}`,
508
509
  filePath,
509
- startLine: call.line,
510
- endLine: call.line,
510
+ startLine: toZeroBasedLine(call.line),
511
+ endLine: toZeroBasedLine(call.line),
511
512
  language: SupportedLanguages.Cobol,
512
513
  description: 'dynamic-call (target is a data item, not resolvable statically)',
513
514
  },
@@ -619,8 +620,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
619
620
  properties: {
620
621
  name: `EXEC SQL ${sql.operation}`,
621
622
  filePath,
622
- startLine: sql.line,
623
- endLine: sql.line,
623
+ startLine: toZeroBasedLine(sql.line),
624
+ endLine: toZeroBasedLine(sql.line),
624
625
  language: SupportedLanguages.Cobol,
625
626
  description: `tables:[${sql.tables.join(',')}] cursors:[${sql.cursors.join(',')}]`,
626
627
  },
@@ -691,8 +692,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
691
692
  properties: {
692
693
  name: `EXEC CICS ${cics.command}`,
693
694
  filePath,
694
- startLine: cics.line,
695
- endLine: cics.line,
695
+ startLine: toZeroBasedLine(cics.line),
696
+ endLine: toZeroBasedLine(cics.line),
696
697
  language: SupportedLanguages.Cobol,
697
698
  description: [
698
699
  cics.mapName && `map:${cics.mapName}`,
@@ -726,8 +727,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
726
727
  properties: {
727
728
  name: `CICS ${cics.command} ${cics.programName}`,
728
729
  filePath,
729
- startLine: cics.line,
730
- endLine: cics.line,
730
+ startLine: toZeroBasedLine(cics.line),
731
+ endLine: toZeroBasedLine(cics.line),
731
732
  language: SupportedLanguages.Cobol,
732
733
  description: `cics-dynamic-program (target is data item ${cics.programName})`,
733
734
  },
@@ -879,8 +880,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
879
880
  properties: {
880
881
  name: entry.name,
881
882
  filePath,
882
- startLine: entry.line,
883
- endLine: entry.line,
883
+ startLine: toZeroBasedLine(entry.line),
884
+ endLine: toZeroBasedLine(entry.line),
884
885
  language: SupportedLanguages.Cobol,
885
886
  isExported: true,
886
887
  description: entry.parameters.length > 0 ? `using:${entry.parameters.join(',')}` : undefined,
@@ -1014,8 +1015,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
1014
1015
  properties: {
1015
1016
  name: `EXEC DLI ${dli.verb}`,
1016
1017
  filePath,
1017
- startLine: dli.line,
1018
- endLine: dli.line,
1018
+ startLine: toZeroBasedLine(dli.line),
1019
+ endLine: toZeroBasedLine(dli.line),
1019
1020
  language: SupportedLanguages.Cobol,
1020
1021
  description: [
1021
1022
  dli.segmentName && `segment:${dli.segmentName}`,
@@ -1148,8 +1149,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
1148
1149
  properties: {
1149
1150
  name: fd.selectName,
1150
1151
  filePath,
1151
- startLine: fd.line,
1152
- endLine: fd.line,
1152
+ startLine: toZeroBasedLine(fd.line),
1153
+ endLine: toZeroBasedLine(fd.line),
1153
1154
  language: SupportedLanguages.Cobol,
1154
1155
  description: `assign:${fd.assignTo}${fd.isOptional ? ' optional' : ''}${fd.organization ? ` org:${fd.organization}` : ''}${fd.access ? ` access:${fd.access}` : ''}`,
1155
1156
  },
@@ -1231,8 +1232,8 @@ function mapToGraph(graph, extracted, file, copyResolutions, moduleNodeIds) {
1231
1232
  properties: {
1232
1233
  name: `CANCEL ${cancel.target}`,
1233
1234
  filePath,
1234
- startLine: cancel.line,
1235
- endLine: cancel.line,
1235
+ startLine: toZeroBasedLine(cancel.line),
1236
+ endLine: toZeroBasedLine(cancel.line),
1236
1237
  language: SupportedLanguages.Cobol,
1237
1238
  description: 'dynamic-cancel (target is a data item, not resolvable statically)',
1238
1239
  },
@@ -44,6 +44,7 @@
44
44
  * ancestor scope, and if THAT produces nothing either the edge is
45
45
  * skipped (with a count returned in `EmitStats.skippedNoCaller`).
46
46
  */
47
+ import { toZeroBasedLine } from './utils/line-base.js';
47
48
  /**
48
49
  * Drain `referenceIndex.bySourceScope` into graph edges.
49
50
  *
@@ -95,8 +96,8 @@ export function emitScopeGraph(input) {
95
96
  properties: {
96
97
  name: scope.kind,
97
98
  filePath: scope.filePath,
98
- startLine: scope.range.startLine,
99
- endLine: scope.range.endLine,
99
+ startLine: toZeroBasedLine(scope.range.startLine),
100
+ endLine: toZeroBasedLine(scope.range.endLine),
100
101
  description: `Scope: ${scope.kind}`,
101
102
  },
102
103
  });
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import path from 'node:path';
9
9
  import { generateId } from '../../lib/utils.js';
10
+ import { toZeroBasedLine } from './utils/line-base.js';
10
11
  const HEADING_RE = /^(#{1,6})\s+(.+)$/;
11
12
  const LINK_RE = /\[([^\]]*)\]\(([^)]+)\)/g;
12
13
  const MD_EXTENSIONS = new Set(['.md', '.mdx']);
@@ -59,8 +60,8 @@ export const processMarkdown = (graph, files, allPathSet) => {
59
60
  properties: {
60
61
  name: heading,
61
62
  filePath: file.path,
62
- startLine: lineNum,
63
- endLine,
63
+ startLine: toZeroBasedLine(lineNum),
64
+ endLine: toZeroBasedLine(endLine),
64
65
  level,
65
66
  description: `h${level}`,
66
67
  },
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Convert a 1-based source line number to the 0-based convention used by
3
+ * GraphNode `startLine`/`endLine`.
4
+ *
5
+ * The graph layer stores line numbers 0-based (tree-sitter `startPosition.row`),
6
+ * and this is load-bearing: the taint/PDG/CFG join and the MCP consumers all add
7
+ * `+ 1` to recover 1-based (see `summary-harvest-driver.ts` — "Function/Method
8
+ * node startLine is 0-based"). Most emitters get 0-based for free from
9
+ * tree-sitter. The exceptions are the regex-based COBOL/JCL processors (their
10
+ * parsers use `lineNum = i + 1`) and the scope-capture path (`Capture` ranges
11
+ * are 1-based per RFC §2.1). Those must convert to 0-based when they build a
12
+ * graph node, or the exact-content slice in `csv-generator.ts` drops the
13
+ * symbol's declaration line (#2379) and reported line numbers are off (#2377).
14
+ *
15
+ * Apply this ONLY at the graph-node `startLine:`/`endLine:` assignment. The
16
+ * parser-internal 1-based values (`.line`, `prog.startLine`) stay 1-based —
17
+ * they feed `L${line}` node/edge IDs and line-range containment checks that
18
+ * must not shift. The clamp guards degenerate inputs (line 0 / empty files).
19
+ */
20
+ export declare const toZeroBasedLine: (oneBasedLine: number) => number;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Convert a 1-based source line number to the 0-based convention used by
3
+ * GraphNode `startLine`/`endLine`.
4
+ *
5
+ * The graph layer stores line numbers 0-based (tree-sitter `startPosition.row`),
6
+ * and this is load-bearing: the taint/PDG/CFG join and the MCP consumers all add
7
+ * `+ 1` to recover 1-based (see `summary-harvest-driver.ts` — "Function/Method
8
+ * node startLine is 0-based"). Most emitters get 0-based for free from
9
+ * tree-sitter. The exceptions are the regex-based COBOL/JCL processors (their
10
+ * parsers use `lineNum = i + 1`) and the scope-capture path (`Capture` ranges
11
+ * are 1-based per RFC §2.1). Those must convert to 0-based when they build a
12
+ * graph node, or the exact-content slice in `csv-generator.ts` drops the
13
+ * symbol's declaration line (#2379) and reported line numbers are off (#2377).
14
+ *
15
+ * Apply this ONLY at the graph-node `startLine:`/`endLine:` assignment. The
16
+ * parser-internal 1-based values (`.line`, `prog.startLine`) stay 1-based —
17
+ * they feed `L${line}` node/edge IDs and line-range containment checks that
18
+ * must not shift. The clamp guards degenerate inputs (line 0 / empty files).
19
+ */
20
+ export const toZeroBasedLine = (oneBasedLine) => Math.max(0, oneBasedLine - 1);
@@ -0,0 +1,21 @@
1
+ import type { NodeLabel } from '../../../_shared/index.js';
2
+ /**
3
+ * Graph-node labels that represent a resolvable code symbol — a definition with
4
+ * its own source span (function, type, member, module-like container).
5
+ *
6
+ * These get EXACT source-span content in the FTS index: `csv-generator.ts`
7
+ * slices exactly `[startLine, endLine]` for them (no ±2 padding), while every
8
+ * other label keeps the context window. That exactness depends on the 0-based
9
+ * `startLine`/`endLine` invariant enforced by `line-base.ts` — the slice is only
10
+ * correct because all emitters store 0-based lines. Keep the two together.
11
+ *
12
+ * Single source of truth so the set can't silently drift the way the inline copy
13
+ * did in #2379.
14
+ *
15
+ * NOTE: `group/extractors/manifest-extractor.ts`'s `CUSTOM_CONTRACT_RESOLVE_QUERY`
16
+ * carries a near-identical hand-list that is intentionally a SUBSET — it excludes
17
+ * `Namespace`, `Variable`, `Module`. Unifying the two needs a contract-resolution
18
+ * behavior check (would widen which nodes resolve as contract symbols), so it is
19
+ * deliberately left separate for now.
20
+ */
21
+ export declare const SYMBOL_NODE_LABELS: ReadonlySet<NodeLabel>;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Graph-node labels that represent a resolvable code symbol — a definition with
3
+ * its own source span (function, type, member, module-like container).
4
+ *
5
+ * These get EXACT source-span content in the FTS index: `csv-generator.ts`
6
+ * slices exactly `[startLine, endLine]` for them (no ±2 padding), while every
7
+ * other label keeps the context window. That exactness depends on the 0-based
8
+ * `startLine`/`endLine` invariant enforced by `line-base.ts` — the slice is only
9
+ * correct because all emitters store 0-based lines. Keep the two together.
10
+ *
11
+ * Single source of truth so the set can't silently drift the way the inline copy
12
+ * did in #2379.
13
+ *
14
+ * NOTE: `group/extractors/manifest-extractor.ts`'s `CUSTOM_CONTRACT_RESOLVE_QUERY`
15
+ * carries a near-identical hand-list that is intentionally a SUBSET — it excludes
16
+ * `Namespace`, `Variable`, `Module`. Unifying the two needs a contract-resolution
17
+ * behavior check (would widen which nodes resolve as contract symbols), so it is
18
+ * deliberately left separate for now.
19
+ */
20
+ export const SYMBOL_NODE_LABELS = new Set([
21
+ 'Function',
22
+ 'Method',
23
+ 'Class',
24
+ 'Interface',
25
+ 'CodeElement',
26
+ 'Struct',
27
+ 'Enum',
28
+ 'Macro',
29
+ 'Typedef',
30
+ 'Union',
31
+ 'Namespace',
32
+ 'Trait',
33
+ 'Impl',
34
+ 'TypeAlias',
35
+ 'Const',
36
+ 'Static',
37
+ 'Variable',
38
+ 'Property',
39
+ 'Record',
40
+ 'Delegate',
41
+ 'Annotation',
42
+ 'Constructor',
43
+ 'Template',
44
+ 'Module',
45
+ ]);
@@ -17,6 +17,7 @@ import path from 'path';
17
17
  import { NODE_TABLES } from './schema.js';
18
18
  import { RelPairRouter } from './rel-pair-routing.js';
19
19
  import { parseTruthyEnv } from '../ingestion/utils/env.js';
20
+ import { SYMBOL_NODE_LABELS } from '../ingestion/utils/symbol-labels.js';
20
21
  import { applyCjkSegmentationIfEnabled } from '../search/cjk-segmentation.js';
21
22
  /**
22
23
  * Deterministic output ordering — optional (out-of-core / windowed-resolve
@@ -187,6 +188,10 @@ class FileContentCache {
187
188
  export const normalizeFtsText = (text) => text.replace(/[\r\n\t]+/g, ' ');
188
189
  /** Composes both FTS-text transforms for the `description` column — one place for the six emission sites below to call, instead of repeating the composition. */
189
190
  const formatFtsDescription = (description) => normalizeFtsText(applyCjkSegmentationIfEnabled(description));
191
+ // Labels that get exact source-span content (no ±2 window). Single source of
192
+ // truth in `symbol-labels.ts` — see there for why the exactness depends on the
193
+ // 0-based line invariant. Kept as a named alias to read intent at the use site.
194
+ const EXACT_SYMBOL_CONTENT_LABELS = SYMBOL_NODE_LABELS;
190
195
  const extractContent = async (node, contentCache) => {
191
196
  const filePath = node.properties.filePath;
192
197
  const content = await contentCache.get(filePath);
@@ -209,8 +214,9 @@ const extractContent = async (node, contentCache) => {
209
214
  if (startLine === undefined || endLine === undefined)
210
215
  return '';
211
216
  const lines = content.split('\n');
212
- const start = Math.max(0, startLine - 2);
213
- const end = Math.min(lines.length - 1, endLine + 2);
217
+ const exactSymbolContent = EXACT_SYMBOL_CONTENT_LABELS.has(node.label);
218
+ const start = Math.max(0, exactSymbolContent ? startLine : startLine - 2);
219
+ const end = Math.min(lines.length - 1, exactSymbolContent ? endLine : endLine + 2);
214
220
  const snippet = lines.slice(start, end + 1).join('\n');
215
221
  const MAX_SNIPPET = 5000;
216
222
  const capped = snippet.length > MAX_SNIPPET ? snippet.slice(0, MAX_SNIPPET) + '\n... [truncated]' : snippet;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Convert a 0-based GraphNode `startLine`/`endLine` to the 1-based line number
3
+ * shown to humans and LLMs in MCP tool output.
4
+ *
5
+ * Storage is 0-based (tree-sitter `startPosition.row`; see
6
+ * `ingestion/utils/line-base.ts`), which matches editors/`sed`/`less -N` only
7
+ * after `+ 1`. The `context`, `query`, and `impact` tools present line numbers a
8
+ * user cross-references against source, so they convert here at the response
9
+ * boundary (#2377).
10
+ *
11
+ * Apply ONLY to a symbol node's 0-based `startLine`/`endLine`. Do NOT apply to:
12
+ * - BasicBlock / CFG `functionStartLine` and PDG statement lines — already
13
+ * 1-based (they use `startPosition.row + 1`);
14
+ * - the internal `sym.startLine + 1` join params that target the 1-based
15
+ * BasicBlock id space;
16
+ * - raw `cypher` results, which pass LadybugDB columns through verbatim and
17
+ * stay 0-based (documented).
18
+ *
19
+ * `undefined`/`null` pass through so optional line fields stay absent.
20
+ */
21
+ export declare function toDisplayLine(zeroBasedLine: number): number;
22
+ export declare function toDisplayLine(zeroBasedLine: number | null | undefined): number | undefined;
@@ -0,0 +1,3 @@
1
+ export function toDisplayLine(zeroBasedLine) {
2
+ return typeof zeroBasedLine === 'number' ? zeroBasedLine + 1 : undefined;
3
+ }
@@ -172,6 +172,14 @@ export declare class LocalBackend {
172
172
  * Cross-repo group tools (CLI). Shares logic with MCP `group_*` handlers.
173
173
  */
174
174
  getGroupService(): GroupService;
175
+ /**
176
+ * Adapt local `trace` to the group port. The assembled group/cross-repo trace
177
+ * presents 1-based endpoints (via resolveSymbolForGroup), so convert the hop
178
+ * lines here too — otherwise one response mixes 1-based endpoints with 0-based
179
+ * hops (#2380). Single-repo `trace` dispatches directly (not through this
180
+ * port) and stays 0-based (documented full-parity follow-up).
181
+ */
182
+ private traceForGroup;
175
183
  /**
176
184
  * Adapt the shared symbol resolver to the GroupToolPort contract. Used by the
177
185
  * cross-repo trace path to locate which member repo an endpoint lives in and
@@ -10,6 +10,7 @@ import path from 'path';
10
10
  import { createHash } from 'crypto';
11
11
  import { initLbug, executeQuery, executeParameterized, closeLbug, isLbugReady, } from '../../core/lbug/pool-adapter.js';
12
12
  import { isValidQueryParams } from '../../core/lbug/query-params.js';
13
+ import { toDisplayLine } from './line-display.js';
13
14
  import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/lbug-config.js';
14
15
  // Embedding imports are lazy (dynamic import) to avoid loading onnxruntime-node
15
16
  // at MCP server startup — crashes on unsupported Node ABI versions (#89)
@@ -453,7 +454,7 @@ export class LocalBackend {
453
454
  query: (r, p) => this.query(r, p),
454
455
  impactByUid: (id, uid, d, o) => this.impactByUid(id, uid, d, o),
455
456
  context: (r, p) => this.context(r, p),
456
- trace: (r, p) => this.trace(r, p),
457
+ trace: (r, p) => this.traceForGroup(r, p),
457
458
  resolveSymbol: (r, q) => this.resolveSymbolForGroup(r, q),
458
459
  pdgFlows: (r, anchor, opts) => this.pdgFlowsForGroup(r, anchor, opts),
459
460
  };
@@ -461,6 +462,23 @@ export class LocalBackend {
461
462
  }
462
463
  return this.groupToolSvc;
463
464
  }
465
+ /**
466
+ * Adapt local `trace` to the group port. The assembled group/cross-repo trace
467
+ * presents 1-based endpoints (via resolveSymbolForGroup), so convert the hop
468
+ * lines here too — otherwise one response mixes 1-based endpoints with 0-based
469
+ * hops (#2380). Single-repo `trace` dispatches directly (not through this
470
+ * port) and stays 0-based (documented full-parity follow-up).
471
+ */
472
+ async traceForGroup(repo, params) {
473
+ const result = await this.trace(repo, params);
474
+ const hops = result.hops;
475
+ if (Array.isArray(hops)) {
476
+ for (const hop of hops) {
477
+ hop.startLine = toDisplayLine(hop.startLine);
478
+ }
479
+ }
480
+ return result;
481
+ }
464
482
  /**
465
483
  * Adapt the shared symbol resolver to the GroupToolPort contract. Used by the
466
484
  * cross-repo trace path to locate which member repo an endpoint lives in and
@@ -478,8 +496,8 @@ export class LocalBackend {
478
496
  name: s.name,
479
497
  type: s.type,
480
498
  filePath: s.filePath,
481
- startLine: s.startLine,
482
- endLine: s.endLine,
499
+ startLine: toDisplayLine(s.startLine),
500
+ endLine: toDisplayLine(s.endLine),
483
501
  },
484
502
  };
485
503
  }
@@ -491,7 +509,7 @@ export class LocalBackend {
491
509
  name: c.name,
492
510
  type: c.type,
493
511
  filePath: c.filePath,
494
- startLine: c.startLine,
512
+ startLine: toDisplayLine(c.startLine),
495
513
  })),
496
514
  };
497
515
  }
@@ -1572,8 +1590,8 @@ export class LocalBackend {
1572
1590
  name: sym.name,
1573
1591
  type: sym.type,
1574
1592
  filePath: sym.filePath,
1575
- startLine: sym.startLine,
1576
- endLine: sym.endLine,
1593
+ startLine: toDisplayLine(sym.startLine),
1594
+ endLine: toDisplayLine(sym.endLine),
1577
1595
  ...(module ? { module } : {}),
1578
1596
  ...(includeContent && content ? { content } : {}),
1579
1597
  };
@@ -1799,8 +1817,11 @@ export class LocalBackend {
1799
1817
  name: sym.name || sym[1],
1800
1818
  type: sym.type || sym[2],
1801
1819
  filePath: sym.filePath || sym[3],
1802
- startLine: sym.startLine || sym[4],
1803
- endLine: sym.endLine || sym[5],
1820
+ // Raw 0-based here — `bm25Search` is only called from `query()`,
1821
+ // whose aggregation loop applies `toDisplayLine` once (see below).
1822
+ // Converting here too would double-shift BM25-matched lines (#2380).
1823
+ startLine: sym.startLine ?? sym[4],
1824
+ endLine: sym.endLine ?? sym[5],
1804
1825
  bm25Score: bm25Result.score,
1805
1826
  });
1806
1827
  }
@@ -2396,7 +2417,7 @@ export class LocalBackend {
2396
2417
  name: c.name,
2397
2418
  kind: c.type,
2398
2419
  filePath: c.filePath,
2399
- line: c.startLine,
2420
+ line: toDisplayLine(c.startLine),
2400
2421
  score: Number(c.score.toFixed(2)),
2401
2422
  })),
2402
2423
  };
@@ -2603,8 +2624,8 @@ export class LocalBackend {
2603
2624
  name: sym.name || sym[1],
2604
2625
  kind: symKind,
2605
2626
  filePath: sym.filePath || sym[3],
2606
- startLine: sym.startLine || sym[4],
2607
- endLine: sym.endLine || sym[5],
2627
+ startLine: toDisplayLine(sym.startLine ?? sym[4]),
2628
+ endLine: toDisplayLine(sym.endLine ?? sym[5]),
2608
2629
  ...(include_content && (sym.content || sym[6]) ? { content: sym.content || sym[6] } : {}),
2609
2630
  ...(methodMetadata ? { methodMetadata } : {}),
2610
2631
  },
@@ -2680,7 +2701,7 @@ export class LocalBackend {
2680
2701
  name: c.name,
2681
2702
  kind: c.type,
2682
2703
  filePath: c.filePath,
2683
- line: c.startLine,
2704
+ line: toDisplayLine(c.startLine),
2684
2705
  score: Number(c.score.toFixed(2)),
2685
2706
  })),
2686
2707
  },
@@ -2698,11 +2719,14 @@ export class LocalBackend {
2698
2719
  return {
2699
2720
  anchorClause: 'a.id STARTS WITH $idPrefix AND a.startLine >= $symStart AND a.startLine <= $symEnd',
2700
2721
  queryParams: { idPrefix, symStart: sym.startLine + 1, symEnd: sym.endLine + 1 },
2722
+ // Display anchor is 1-based, matching the ambiguous-candidate branch and
2723
+ // the context/query/impact tools (#2380). This is display-only — the
2724
+ // BasicBlock join above uses the raw `sym.startLine + 1` in `symStart`.
2701
2725
  anchor: {
2702
2726
  file: sym.filePath,
2703
2727
  symbol: sym.name,
2704
- startLine: sym.startLine,
2705
- endLine: sym.endLine,
2728
+ startLine: toDisplayLine(sym.startLine),
2729
+ endLine: toDisplayLine(sym.endLine),
2706
2730
  },
2707
2731
  };
2708
2732
  }
@@ -4036,7 +4060,7 @@ export class LocalBackend {
4036
4060
  name: c.name,
4037
4061
  kind: c.type,
4038
4062
  filePath: c.filePath,
4039
- line: c.startLine,
4063
+ line: toDisplayLine(c.startLine),
4040
4064
  score: Number(c.score.toFixed(2)),
4041
4065
  })),
4042
4066
  };
@@ -4089,7 +4113,7 @@ export class LocalBackend {
4089
4113
  name: c.name,
4090
4114
  kind: c.type,
4091
4115
  filePath: c.filePath,
4092
- line: c.startLine,
4116
+ line: toDisplayLine(c.startLine),
4093
4117
  score: Number(c.score.toFixed(2)),
4094
4118
  impactedCount: summary?.impactedCount ?? 0,
4095
4119
  risk: summary?.risk ?? 'UNKNOWN',
@@ -34,8 +34,10 @@ export declare function splitCalleeIds(raw: unknown): string[];
34
34
  * Contract version of the mode:'pdg' impact result shape. A stable discriminator
35
35
  * for external MCP/agent consumers — distinct from the DB INCREMENTAL_SCHEMA_VERSION.
36
36
  * Bump on any breaking change to the PDG result fields.
37
+ * v2: `startLine` in the result is now 1-based display (#2380), matching the
38
+ * context/query/impact tools (was 0-based).
37
39
  */
38
- export declare const PDG_RESULT_VERSION: 1;
40
+ export declare const PDG_RESULT_VERSION: 2;
39
41
  /** A reachable dependence block resolved to its source statement. */
40
42
  export interface PdgStatement {
41
43
  /** 1-based source line where the statement's block starts. */
@@ -105,7 +107,7 @@ export interface PdgInterproceduralImpact {
105
107
  export interface PdgImpactBaseResult extends PdgImpactParityFields {
106
108
  mode: 'pdg';
107
109
  /** Contract version of the mode:'pdg' impact result shape; bump on any breaking change to the PDG result fields. */
108
- pdgResultVersion: 1;
110
+ pdgResultVersion: 2;
109
111
  target: PdgImpactTarget;
110
112
  direction: 'upstream' | 'downstream';
111
113
  impactedCount: number;
@@ -172,7 +174,7 @@ export interface PdgImpactDegradedResult extends PdgImpactBaseResult {
172
174
  export interface PdgImpactErrorResult {
173
175
  mode?: 'pdg';
174
176
  /** Contract version of the mode:'pdg' impact result shape; bump on any breaking change to the PDG result fields. */
175
- pdgResultVersion: 1;
177
+ pdgResultVersion: 2;
176
178
  error: string;
177
179
  target: PdgImpactTarget;
178
180
  direction: 'upstream' | 'downstream';
@@ -9,6 +9,7 @@ import path from 'path';
9
9
  import { loadMeta } from '../../storage/repo-manager.js';
10
10
  import { IMPACT_MAX_DEPTH, PDG_QUERY_DEFAULT_LIMIT, PDG_QUERY_MAX_LIMIT } from '../tools.js';
11
11
  import { CALLEES_TRUNCATED_SENTINEL, CALLEE_ID_SEP } from '../../core/ingestion/cfg/emit.js';
12
+ import { toDisplayLine } from './line-display.js';
12
13
  import { decodeCallSummary } from '../../core/ingestion/taint/call-summary-codec.js';
13
14
  import { decodeReachingDefReason } from '../../core/ingestion/cfg/reaching-def-reason-codec.js';
14
15
  import { getProviderForFile } from '../../core/ingestion/languages/index.js';
@@ -84,8 +85,10 @@ export function splitCalleeIds(raw) {
84
85
  * Contract version of the mode:'pdg' impact result shape. A stable discriminator
85
86
  * for external MCP/agent consumers — distinct from the DB INCREMENTAL_SCHEMA_VERSION.
86
87
  * Bump on any breaking change to the PDG result fields.
88
+ * v2: `startLine` in the result is now 1-based display (#2380), matching the
89
+ * context/query/impact tools (was 0-based).
87
90
  */
88
- export const PDG_RESULT_VERSION = 1;
91
+ export const PDG_RESULT_VERSION = 2;
89
92
  /**
90
93
  * FU-B-2 intra-block def→use line walk. Given a block's self REACHING_DEF
91
94
  * def→use line PAIRS (every `defLine → useLine` step decoded from the edge
@@ -519,7 +522,7 @@ function assemblePdgImpactResult(input) {
519
522
  name: s.name,
520
523
  type: s.type,
521
524
  filePath: s.filePath,
522
- ...(s.startLine !== undefined ? { startLine: s.startLine } : {}),
525
+ ...(s.startLine !== undefined ? { startLine: toDisplayLine(s.startLine) } : {}),
523
526
  ...(s.ambiguous ? { ambiguous: true } : {}),
524
527
  ...(s.id === null ? { unresolved: true } : {}),
525
528
  pdgEvidence: (s.id === null ? 'degraded' : 'owner-projection'),
@@ -365,6 +365,7 @@ additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Nam
365
365
 
366
366
  node_properties:
367
367
  common: "name (STRING), filePath (STRING), startLine (INT32), endLine (INT32)"
368
+ line_numbers: "startLine/endLine on symbol nodes are 0-BASED (tree-sitter rows) in storage AND in raw Cypher results. The context, query, impact, group/cross-repo trace, and explain/pdg_query (symbol anchor) tools present them 1-BASED (editor / sed / less -N aligned), so a symbol spans editor lines (startLine+1)..(endLine+1) — e.g. sed '<startLine+1>,<endLine+1>!d' <file>. Single-repo trace symbol lines stay 0-BASED for now (full-parity follow-up). content holds the exact symbol span. (BasicBlock / PDG statement lines are separately 1-based.) (#2377, #2380)"
368
369
  Method: "parameterCount (INT32), returnType (STRING), isVariadic (BOOL), visibility (STRING), isStatic (BOOL), isAbstract (BOOL), isFinal (BOOL), isVirtual (BOOL), isOverride (BOOL), isAsync (BOOL), isPartial (BOOL), requiredParameterCount (INT32), parameterTypes (STRING[]), annotations (STRING[])"
369
370
  Function: "parameterCount (INT32), returnType (STRING), isVariadic (BOOL), visibility (STRING), isStatic (BOOL), isAbstract (BOOL), isFinal (BOOL), isAsync (BOOL), parameterTypes (STRING[]), annotations (STRING[])"
370
371
  Property: "declaredType (STRING) — the field's type annotation (e.g., 'Address', 'City'). Used for field-access chain resolution."
package/dist/mcp/tools.js CHANGED
@@ -379,7 +379,7 @@ MODE (opt-in): "callgraph" (default) walks symbol→symbol edges (CALLS/IMPORTS/
379
379
 
380
380
  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.
381
381
 
382
- PDG OUTPUT CONTRACT: every mode:'pdg' result (success, empty, degraded, or error) carries pdgResultVersion:1 — 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:1, 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.
382
+ 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.
383
383
 
384
384
  WHEN TO USE: Before making code changes — especially refactoring, renaming, or modifying shared code. Shows what would break.
385
385
  AFTER THIS: Review d=1 items (WILL BREAK). Use context() on high-risk symbols.
@@ -242,8 +242,13 @@ export interface RepoMeta {
242
242
  * URL-only id). The incremental writeback preserves unchanged-file rows, so a
243
243
  * top-up against a pre-v5 index would strand old url-keyed Route nodes alongside
244
244
  * new composite-keyed ones — force a full re-analyze instead.
245
+ * v6: line-number storage flipped to uniform 0-based for the last 1-based
246
+ * GraphNode emitters — COBOL/JCL/markdown/scope (#2377/#2379/#2380). Incremental
247
+ * writeback preserves unchanged-file rows, so a top-up against a pre-v6 index
248
+ * would MIX old 1-based rows with new 0-based ones — and the 1-based MCP display
249
+ * would render the stale rows one line too high — so force a full re-analyze.
245
250
  */
246
- export declare const INCREMENTAL_SCHEMA_VERSION = 5;
251
+ export declare const INCREMENTAL_SCHEMA_VERSION = 6;
247
252
  export interface IndexedRepo {
248
253
  repoPath: string;
249
254
  storagePath: string;
@@ -90,8 +90,13 @@ export const registryPathEquals = (a, b) => process.platform === 'win32' ? a.toL
90
90
  * URL-only id). The incremental writeback preserves unchanged-file rows, so a
91
91
  * top-up against a pre-v5 index would strand old url-keyed Route nodes alongside
92
92
  * new composite-keyed ones — force a full re-analyze instead.
93
+ * v6: line-number storage flipped to uniform 0-based for the last 1-based
94
+ * GraphNode emitters — COBOL/JCL/markdown/scope (#2377/#2379/#2380). Incremental
95
+ * writeback preserves unchanged-file rows, so a top-up against a pre-v6 index
96
+ * would MIX old 1-based rows with new 0-based ones — and the 1-based MCP display
97
+ * would render the stale rows one line too high — so force a full re-analyze.
93
98
  */
94
- export const INCREMENTAL_SCHEMA_VERSION = 5;
99
+ export const INCREMENTAL_SCHEMA_VERSION = 6;
95
100
  const GITNEXUS_DIR = '.gitnexus';
96
101
  const GITNEXUS_EXCLUDE_ENTRY = `${GITNEXUS_DIR}/`;
97
102
  export const INDEX_METADATA_FILE = 'gitnexus.json';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.4",
3
+ "version": "1.6.10-rc.5",
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",