gitnexus 1.6.9-rc.11 → 1.6.9-rc.13

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.
@@ -732,18 +732,20 @@ async function stitchCrossRepo(deps, p, fromEp, toEp, groupDir, notes) {
732
732
  const FILE_BASENAME_RE = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|php|java|kt|kts|rb|cs|rs|swift|dart|scala|c|cc|cpp|cxx|h|hpp|m|mm)$/i;
733
733
  /**
734
734
  * A provider endpoint's display label. A resolved handler has a real function
735
- * name; the source-scan fallbacks leave a generic token (`'handler'`/`'fetch'`)
736
- * or a file basename. Those are treated as anonymous and shown as
735
+ * name; the source-scan fallbacks leave a generic token (`'handler'`/`'fetch'`,
736
+ * or `'route'` for an unresolved named-controller / closure Laravel route) or a
737
+ * file basename. Those are treated as anonymous and shown as
737
738
  * `<METHOD /path handler>` so the endpoint is still identifiable by route. When
738
739
  * the bridge row carries a resolved `providerUid`, the name IS a real symbol —
739
- * the `'handler'`/`'fetch'` sentinel check is suppressed so a function genuinely
740
- * named `handler` is not mislabeled anonymous.
740
+ * the sentinel check is suppressed so a function genuinely named `handler` (or,
741
+ * hypothetically, `route`) is not mislabeled anonymous.
741
742
  */
742
743
  function providerLabel(providerName, contractId, providerUid) {
743
744
  const resolved = providerUid !== '';
744
745
  const generic = providerName === '' ||
745
746
  FILE_BASENAME_RE.test(providerName) ||
746
- (!resolved && (providerName === 'handler' || providerName === 'fetch'));
747
+ (!resolved &&
748
+ (providerName === 'handler' || providerName === 'fetch' || providerName === 'route'));
747
749
  return generic
748
750
  ? { label: `<${contractId} handler>`, anon: true }
749
751
  : { label: providerName, anon: false };
@@ -9,8 +9,11 @@ import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-s
9
9
  */
10
10
  // ─── Provider: framework routing ──────────────────────────────────────
11
11
  // Matches `\w+\.GET(...)` etc. (gin, echo, chi all share this shape).
12
- // Captures the HTTP method (field name), path literal, and handler
13
- // identifier passed as the second argument.
12
+ // Captures the HTTP method (field name), path literal, and the handler
13
+ // anchored to the LAST argument (`@handler .`) so a variadic middleware
14
+ // chain (`r.GET("/x", mw, handler)`, gin/echo/chi style) binds the real
15
+ // handler, not a middleware identifier (which would otherwise over-match
16
+ // and attach the route to the wrong symbol — see #2276 review).
14
17
  const FRAMEWORK_ROUTE_PATTERNS = compilePatterns({
15
18
  name: 'go-framework-route',
16
19
  language: Go,
@@ -23,7 +26,8 @@ const FRAMEWORK_ROUTE_PATTERNS = compilePatterns({
23
26
  field: (field_identifier) @http_method (#match? @http_method "^(GET|POST|PUT|DELETE|PATCH)$"))
24
27
  arguments: (argument_list
25
28
  (interpreted_string_literal) @path
26
- (identifier) @handler))
29
+ [(identifier) (func_literal)] @handler
30
+ .))
27
31
  `,
28
32
  },
29
33
  ],
@@ -42,7 +46,8 @@ const HANDLE_FUNC_PATTERNS = compilePatterns({
42
46
  field: (field_identifier) @fn (#eq? @fn "HandleFunc"))
43
47
  arguments: (argument_list
44
48
  (interpreted_string_literal) @path
45
- (identifier) @handler))
49
+ [(identifier) (func_literal)] @handler
50
+ .))
46
51
  `,
47
52
  },
48
53
  ],
@@ -125,12 +130,18 @@ export const GO_HTTP_PLUGIN = {
125
130
  const path = unquoteLiteral(pathNode.text);
126
131
  if (path === null)
127
132
  continue;
133
+ // An inline `func(){…}` handler has no name → emit `name: null` and a
134
+ // `line` so it resolves to its containing/closure symbol by line-span
135
+ // containment (like a consumer). A named identifier handler keeps its
136
+ // name and resolves by name; `line` is harmless there.
137
+ const isInlineHandler = handlerNode?.type === 'func_literal';
128
138
  out.push({
129
139
  role: 'provider',
130
140
  framework: 'go-framework',
131
141
  method: methodNode.text.toUpperCase(),
132
142
  path,
133
- name: handlerNode?.text ?? null,
143
+ name: isInlineHandler ? null : (handlerNode?.text ?? null),
144
+ line: (handlerNode ?? pathNode).startPosition.row + 1,
134
145
  confidence: 0.8,
135
146
  });
136
147
  }
@@ -143,12 +154,16 @@ export const GO_HTTP_PLUGIN = {
143
154
  const path = unquoteLiteral(pathNode.text);
144
155
  if (path === null)
145
156
  continue;
157
+ // Inline `func(){…}` handler → resolve by containment (see go-framework
158
+ // note above); a named handler resolves by name.
159
+ const isInlineHandler = handlerNode?.type === 'func_literal';
146
160
  out.push({
147
161
  role: 'provider',
148
162
  framework: 'go-stdlib',
149
163
  method: 'GET',
150
164
  path,
151
- name: handlerNode?.text ?? null,
165
+ name: isInlineHandler ? null : (handlerNode?.text ?? null),
166
+ line: (handlerNode ?? pathNode).startPosition.row + 1,
152
167
  confidence: 0.8,
153
168
  });
154
169
  }
@@ -634,6 +634,13 @@ export const JAVA_HTTP_PLUGIN = {
634
634
  method: route.httpMethod,
635
635
  path: joinPath(prefix, route.rawPath),
636
636
  name: route.methodName,
637
+ // Spring providers are named controller methods resolved BY NAME, so
638
+ // `line` is inert — a named provider never falls through to line-span
639
+ // containment. Gate it on a present name so a (grammar-impossible)
640
+ // nameless provider degrades to file-level rather than resolving by
641
+ // containment to the enclosing class. Wired for consumer-emit parity
642
+ // and a future inline DSL.
643
+ line: route.methodName ? route.methodNode.startPosition.row + 1 : undefined,
637
644
  confidence: 0.8,
638
645
  });
639
646
  }
@@ -977,6 +977,13 @@ function buildKotlinPlugin(language) {
977
977
  method: httpMethod,
978
978
  path: joinPath(prefix, rawPath),
979
979
  name: nameNode?.text ?? null,
980
+ // Spring providers are named controller methods resolved BY NAME, so
981
+ // `line` is inert — a named provider never falls through to line-span
982
+ // containment. Gate it on a present name so a (grammar-impossible)
983
+ // nameless provider degrades to file-level rather than resolving by
984
+ // containment to the enclosing class. Wired for consumer-emit parity
985
+ // and a future inline DSL.
986
+ line: nameNode?.text ? methodNode.startPosition.row + 1 : undefined,
980
987
  confidence: 0.8,
981
988
  });
982
989
  }
@@ -27,7 +27,9 @@ const LARAVEL_ROUTE_SPEC = {
27
27
  (scoped_call_expression
28
28
  scope: (name) @scope (#eq? @scope "Route")
29
29
  name: (name) @method (#match? @method "^(get|post|put|delete|patch)$")
30
- arguments: (arguments . (argument (string) @path)))
30
+ arguments: (arguments
31
+ . (argument (string) @path)
32
+ (argument [(anonymous_function) (arrow_function)] @closure)?))
31
33
  `,
32
34
  };
33
35
  const HTTP_FACADE_SPEC = {
@@ -125,12 +127,22 @@ export const PHP_HTTP_PLUGIN = {
125
127
  const path = phpStringText(pathNode);
126
128
  if (path === null)
127
129
  continue;
130
+ // A closure handler (`Route::get('/x', function(){…})` / `fn() => …`) has
131
+ // no name → emit `name: null` + the registration line so it resolves to
132
+ // its containing symbol (e.g. a service-provider `boot()` or controller
133
+ // method) by line-span containment. A named-controller route keeps the
134
+ // `'route'` label — resolving its array/string handler to a real method is
135
+ // a separate, graph-backed concern. NOTE: a closure at FILE scope
136
+ // (routes/web.php) has no enclosing function and PHP closures are not yet
137
+ // indexed as symbols, so it still degrades to file-level (see #2276).
138
+ const closureNode = match.captures.closure;
128
139
  out.push({
129
140
  role: 'provider',
130
141
  framework: 'laravel',
131
142
  method: methodNode.text.toUpperCase(),
132
143
  path,
133
- name: 'route',
144
+ name: closureNode ? null : 'route',
145
+ line: (closureNode ?? pathNode).startPosition.row + 1,
134
146
  confidence: 0.8,
135
147
  });
136
148
  }
@@ -967,6 +967,12 @@ export const PYTHON_HTTP_PLUGIN = {
967
967
  method: httpMethod,
968
968
  path,
969
969
  name: null,
970
+ // The decorated handler has no captured name → resolve by line-span
971
+ // containment. Best-effort fallback: FastAPI routes are graph-backed
972
+ // (ingestion decorator routes) and the function span starts at `def`
973
+ // (decorators excluded), so this lands the single-decorator case and
974
+ // degrades to file-level for multi-decorator stacks.
975
+ line: pathNode.startPosition.row + 1,
970
976
  confidence: 0.8,
971
977
  });
972
978
  }
@@ -1008,6 +1014,8 @@ export const PYTHON_HTTP_PLUGIN = {
1008
1014
  method: httpMethod,
1009
1015
  path: p,
1010
1016
  name: null,
1017
+ // Best-effort containment fallback — see the @app provider note above.
1018
+ line: pathNode.startPosition.row + 1,
1011
1019
  confidence: 0.8,
1012
1020
  });
1013
1021
  }
@@ -36,6 +36,12 @@ export interface CreateLoggerOptions {
36
36
  debugEnvVar?: string;
37
37
  /** Override destination stream — primarily for tests. */
38
38
  destination?: DestinationStream;
39
+ /**
40
+ * Explicit level for the destination-override path — primarily for tests that
41
+ * need to capture below the default `info` (e.g. asserting a `debug` record).
42
+ * Ignored unless `destination` is set; `debugEnvVar` still wins when truthy.
43
+ */
44
+ level?: string;
39
45
  }
40
46
  /**
41
47
  * Flush any buffered records on the default destination. Entry-point
@@ -114,7 +120,10 @@ export interface LoggerCapture {
114
120
  * expect(cap.records().some(r => r.msg?.includes('clamping'))).toBe(true);
115
121
  * });
116
122
  *
123
+ * Pass `level` (e.g. 'debug') to capture below the default 'info' — needed to
124
+ * assert that a record was emitted at debug rather than merely absent.
125
+ *
117
126
  * Not a public API; underscore-prefixed and called only from test code.
118
127
  * Throws if a previous capture is still active — see the body for context.
119
128
  */
120
- export declare function _captureLogger(): LoggerCapture;
129
+ export declare function _captureLogger(level?: string): LoggerCapture;
@@ -202,7 +202,7 @@ function buildBaseOptions() {
202
202
  export function createLogger(name, opts) {
203
203
  const debugRequested = opts?.debugEnvVar ? isTruthyEnv(process.env[opts.debugEnvVar]) : false;
204
204
  if (opts?.destination) {
205
- return pino({ level: debugRequested ? 'debug' : 'info', base: undefined, name }, opts.destination);
205
+ return pino({ level: debugRequested ? 'debug' : (opts.level ?? 'info'), base: undefined, name }, opts.destination);
206
206
  }
207
207
  const base = buildBaseOptions();
208
208
  // When using a transport (pino-pretty), pino manages the destination
@@ -226,6 +226,7 @@ export function createLogger(name, opts) {
226
226
  /* Default singleton (Proxy-backed for test capture) */
227
227
  /* ------------------------------------------------------------------ */
228
228
  let _activeDestination;
229
+ let _activeLevel;
229
230
  let _cached;
230
231
  function _getInner() {
231
232
  if (_cached)
@@ -233,7 +234,7 @@ function _getInner() {
233
234
  // Always go through createLogger so future defaults (serializers, redaction,
234
235
  // formatters) apply uniformly. The destination override is honored when set
235
236
  // by `_captureLogger()` below.
236
- _cached = createLogger('gitnexus', _activeDestination ? { destination: _activeDestination } : undefined);
237
+ _cached = createLogger('gitnexus', _activeDestination ? { destination: _activeDestination, level: _activeLevel } : undefined);
237
238
  return _cached;
238
239
  }
239
240
  /**
@@ -293,10 +294,13 @@ export class MemoryWritable extends Writable {
293
294
  * expect(cap.records().some(r => r.msg?.includes('clamping'))).toBe(true);
294
295
  * });
295
296
  *
297
+ * Pass `level` (e.g. 'debug') to capture below the default 'info' — needed to
298
+ * assert that a record was emitted at debug rather than merely absent.
299
+ *
296
300
  * Not a public API; underscore-prefixed and called only from test code.
297
301
  * Throws if a previous capture is still active — see the body for context.
298
302
  */
299
- export function _captureLogger() {
303
+ export function _captureLogger(level) {
300
304
  // Guard against double-capture: forgetting `restore()` between two
301
305
  // `_captureLogger()` calls silently abandoned the previous capture and
302
306
  // corrupted logger state for the rest of the vitest worker. Throwing here
@@ -307,6 +311,7 @@ export function _captureLogger() {
307
311
  }
308
312
  const w = new MemoryWritable();
309
313
  _activeDestination = w;
314
+ _activeLevel = level;
310
315
  _cached = undefined;
311
316
  return {
312
317
  records: () => w.chunks
@@ -317,6 +322,7 @@ export function _captureLogger() {
317
322
  text: () => w.chunks.join(''),
318
323
  restore: () => {
319
324
  _activeDestination = undefined;
325
+ _activeLevel = undefined;
320
326
  _cached = undefined;
321
327
  },
322
328
  };
@@ -193,10 +193,39 @@ export const IMPACT_RELATION_CONFIDENCE = {
193
193
  * Falls back to 0.5 for unknown types so they are not silently elevated.
194
194
  */
195
195
  const confidenceForRelType = (relType) => IMPACT_RELATION_CONFIDENCE[relType ?? ''] ?? 0.5;
196
- /** Structured error logging for query failures — replaces empty catch blocks */
196
+ /**
197
+ * Structured logging for *swallowed* query failures — replaces empty catch
198
+ * blocks. The level reflects telemetry severity, NOT a promise about the
199
+ * caller: most callers catch the failure and degrade to a genuinely safe
200
+ * fallback (a usable result, usually with a caller-visible `partial`/`ftsUsed`
201
+ * flag), so these are not operation-level errors and must not log at `error`:
202
+ *
203
+ * - A benign missing optional table/label/column — a repo analyzed without
204
+ * processes/communities, or a pre-v3 PDG index lacking the `calleeIds`
205
+ * column — is a normal configuration, not a failure. Logged at `debug`
206
+ * (suppressed at the default `info` level; surfaced only when troubleshooting).
207
+ * - Any other swallowed failure is an unexpected-but-handled degradation:
208
+ * logged at `warn` so it stays observable without raising a false `error`
209
+ * alarm that would drown genuine, operation-aborting failures.
210
+ *
211
+ * `error` is intentionally NOT used here — it is reserved for failures that
212
+ * actually abort an operation, which log directly rather than through this
213
+ * best-effort-degradation helper.
214
+ *
215
+ * Contract for callers (#2283 review): only route a failure here when the
216
+ * caller ALSO surfaces the degradation in its result (a `partial` flag,
217
+ * `failed_files`, `traversalComplete:false`, …). A mutating or safety-critical
218
+ * path that would otherwise report success/clean (e.g. `rename` apply, the
219
+ * `detect_changes` safety gate) MUST set that result-level signal — `warn`
220
+ * alone is not a substitute for an honest result.
221
+ */
197
222
  function logQueryError(context, err) {
198
223
  const msg = err instanceof Error ? err.message : String(err);
199
- logger.error({ context, err: msg }, 'GitNexus query failed');
224
+ if (isBenignMissingTableError(err)) {
225
+ logger.debug({ context, err: msg }, 'GitNexus query skipped (missing optional data)');
226
+ return;
227
+ }
228
+ logger.warn({ context, err: msg }, 'GitNexus query failed (degraded)');
200
229
  }
201
230
  /**
202
231
  * A "missing table/label/relation" prepare error is benign for the query tool's
@@ -208,7 +237,12 @@ function logQueryError(context, err) {
208
237
  */
209
238
  function isBenignMissingTableError(err) {
210
239
  const msg = err instanceof Error ? err.message : String(err ?? '');
211
- return /does not exist|no such (table|label|rel)|unknown (table|label)|not (defined|found)/i.test(msg);
240
+ // The `not (defined|found)` arm is scoped to a schema object (table/label/
241
+ // rel/column/property), mirroring lbug-adapter's isMissingColumnError
242
+ // (`/(table|column|property).*not found/i`): an unscoped "not found" matched
243
+ // operation failures like `rg: not found` (ripgrep absent) or `Symbol not
244
+ // found`, which this helper would then silently demote to `debug` (#2283).
245
+ return /does not exist|no such (table|label|rel)|unknown (table|label)|(table|label|rel|column|property)[^\n]*\bnot (defined|found)\b/i.test(msg);
212
246
  }
213
247
  const isReadOnlyDbError = (err) => {
214
248
  // Walk the `cause` chain (bounded) so a wrapped read-only error (e.g. the
@@ -1574,7 +1608,12 @@ export class LocalBackend {
1574
1608
  ftsResponse = await searchFTSFromLbug(query, limit, repo.lbugPath);
1575
1609
  }
1576
1610
  catch (err) {
1577
- logger.error({ err: err.message }, 'GitNexus: BM25/FTS search failed (FTS indexes may not exist) -');
1611
+ // Swallowed, gracefully-degraded failure: the search falls back to
1612
+ // semantic-only (a valid result), and the most common cause is simply an
1613
+ // un-indexed FTS extension — a normal configuration, not an operation
1614
+ // error. Logged at warn (matching the sibling import-failure fallback
1615
+ // above), never error, so it does not raise a false alarm.
1616
+ logger.warn({ err: err.message }, 'GitNexus: BM25/FTS search failed (FTS indexes may not exist) — falling back to semantic-only');
1578
1617
  return { results: [], ftsUsed: false };
1579
1618
  }
1580
1619
  // Guard against unexpected response shape (#1489) — ftsResponse.results
@@ -3115,6 +3154,9 @@ export class LocalBackend {
3115
3154
  }
3116
3155
  // Map diff hunks to indexed symbols via range overlap
3117
3156
  const changedSymbols = [];
3157
+ // Set if a swallowed graph query fails below — surfaces `partial:true` so a
3158
+ // degraded run cannot report a false-clean `risk_level:'low'` (#2283).
3159
+ let queryDegraded = false;
3118
3160
  for (const fileDiff of fileDiffs) {
3119
3161
  if (fileDiff.hunks.length === 0)
3120
3162
  continue;
@@ -3160,6 +3202,12 @@ export class LocalBackend {
3160
3202
  }
3161
3203
  catch (e) {
3162
3204
  logQueryError('detect-changes:file-symbols', e);
3205
+ // The symbol query failed: changedSymbols stays empty and the result
3206
+ // would otherwise look like a clean no-op (`changed_count:0`,
3207
+ // `risk_level:'low'`). detect_changes is the pre-commit safety gate, so
3208
+ // flag the result `partial` rather than let a swallowed failure
3209
+ // masquerade as "nothing changed" (#2283).
3210
+ queryDegraded = true;
3163
3211
  }
3164
3212
  }
3165
3213
  // Find affected processes -- single batched query instead of N+1
@@ -3194,6 +3242,7 @@ export class LocalBackend {
3194
3242
  }
3195
3243
  catch (e) {
3196
3244
  logQueryError('detect-changes:process-lookup', e);
3245
+ queryDegraded = true;
3197
3246
  }
3198
3247
  }
3199
3248
  const processCount = affectedProcesses.size;
@@ -3213,6 +3262,9 @@ export class LocalBackend {
3213
3262
  },
3214
3263
  changed_symbols: changedSymbols,
3215
3264
  affected_processes: Array.from(affectedProcesses.values()),
3265
+ // A swallowed query failure makes the counts/risk above incomplete — tell
3266
+ // the caller so the safety gate isn't trusted as a clean result (#2283).
3267
+ ...(queryDegraded && { partial: true }),
3216
3268
  };
3217
3269
  }
3218
3270
  /**
@@ -3358,6 +3410,7 @@ export class LocalBackend {
3358
3410
  // Step 4: Apply or preview
3359
3411
  const allChanges = Array.from(changes.values());
3360
3412
  const totalEdits = allChanges.reduce((sum, c) => sum + c.edits.length, 0);
3413
+ const failedFiles = [];
3361
3414
  if (!dry_run) {
3362
3415
  // Apply edits to files
3363
3416
  for (const change of allChanges) {
@@ -3369,12 +3422,16 @@ export class LocalBackend {
3369
3422
  await fs.writeFile(fullPath, content, 'utf-8');
3370
3423
  }
3371
3424
  catch (e) {
3425
+ // A swallowed write failure must not be reported as a full success
3426
+ // (#2283): record the file so the result can degrade to 'partial'
3427
+ // with the unwritten files listed, rather than masquerading as done.
3372
3428
  logQueryError('rename:apply-edit', e);
3429
+ failedFiles.push(change.file_path);
3373
3430
  }
3374
3431
  }
3375
3432
  }
3376
3433
  return {
3377
- status: 'success',
3434
+ status: failedFiles.length > 0 ? 'partial' : 'success',
3378
3435
  old_name: oldName,
3379
3436
  new_name,
3380
3437
  files_affected: allChanges.length,
@@ -3383,6 +3440,7 @@ export class LocalBackend {
3383
3440
  text_search_edits: astSearchEdits,
3384
3441
  changes: allChanges,
3385
3442
  applied: !dry_run,
3443
+ ...(failedFiles.length > 0 && { failed_files: failedFiles }),
3386
3444
  };
3387
3445
  }
3388
3446
  async trace(repo, params) {
@@ -3655,9 +3713,20 @@ export class LocalBackend {
3655
3713
  };
3656
3714
  }
3657
3715
  const mode = modeResult.mode;
3716
+ // #2279: some MCP client/agent adapters serialize an *omitted* optional
3717
+ // numeric field as `0` rather than dropping it, so callgraph calls arrive
3718
+ // carrying a spurious `line: 0`. `line` is meaningless on the callgraph path
3719
+ // (the symbol→symbol BFS has no statement notion), so treat a literal `0`
3720
+ // there as omitted and let the normal traversal run. The coercion is
3721
+ // deliberately narrow — only the literal `0`, only when mode !== 'pdg':
3722
+ // a genuine positive `line` on callgraph still errors (real mode mistake),
3723
+ // negative/fractional values still error, and pdg mode is untouched (the
3724
+ // normalization is an identity there, so `line: 0` is still rejected below —
3725
+ // there is no 1-based source line `0` to anchor on).
3726
+ const effectiveLine = mode !== 'pdg' && params.line === 0 ? undefined : params.line;
3658
3727
  // `line` is a PDG-only statement anchor. Reject it on the callgraph path
3659
3728
  // rather than silently ignore (the symbol→symbol BFS has no statement notion).
3660
- if (params.line !== undefined && mode !== 'pdg') {
3729
+ if (effectiveLine !== undefined && mode !== 'pdg') {
3661
3730
  return {
3662
3731
  error: `Parameter 'line' is only supported with mode:'pdg' (it anchors the dependence slice on a statement). Remove it or set mode:'pdg'.`,
3663
3732
  target: { name: params.target },
@@ -3667,8 +3736,8 @@ export class LocalBackend {
3667
3736
  };
3668
3737
  }
3669
3738
  // A provided `line` must be a positive integer.
3670
- if (params.line !== undefined &&
3671
- (!Number.isInteger(params.line) || params.line < 1)) {
3739
+ if (effectiveLine !== undefined &&
3740
+ (!Number.isInteger(effectiveLine) || effectiveLine < 1)) {
3672
3741
  // Line param fails validation before target resolution → partial-but-typed
3673
3742
  // target on the pdg path (typed PdgImpactTarget, not an inline literal).
3674
3743
  const badLineTarget = { name: params.target };
@@ -3965,7 +4034,11 @@ export class LocalBackend {
3965
4034
  symType,
3966
4035
  direction,
3967
4036
  maxDepth,
3968
- line: params.line,
4037
+ // Use the normalized line, not raw params.line, so the gate and the
4038
+ // engine share one source of truth (#2283). Identity in pdg mode today
4039
+ // — effectiveLine === params.line when mode === 'pdg' — but this stays
4040
+ // correct if the normalization ever stops being an identity here.
4041
+ line: effectiveLine,
3969
4042
  limit: Number.isFinite(params.limit) ? params.limit : 100,
3970
4043
  // KTD2 extraction-seam discipline: hand the engine its DB dependency
3971
4044
  // explicitly rather than `this.`-binding it. LocalBackend owns repo
package/dist/mcp/tools.js CHANGED
@@ -425,8 +425,13 @@ SERVICE: optional monorepo path prefix (case-sensitive path segments). When "rep
425
425
  },
426
426
  line: {
427
427
  type: 'integer',
428
- minimum: 1,
429
- description: "1-based source line PDG statement anchor (mode:'pdg'). Seeds affectedStatements on the statement at this line; inter-procedural symbols are still returned in interproceduralByDepth/pdgInterprocedural and the compatibility byDepth bucket.",
428
+ // `minimum: 0` (not 1) so strict client/agent adapters that materialize
429
+ // an omitted optional numeric field as `0` do not reject the request
430
+ // before sending (#2279). A positive line is still required for a real
431
+ // pdg anchor — the backend enforces that — but `0`/omitted means "no
432
+ // statement anchor" and is tolerated on the callgraph path.
433
+ minimum: 0,
434
+ description: "1-based source line — PDG statement anchor (mode:'pdg'). Seeds affectedStatements on the statement at this line; inter-procedural symbols are still returned in interproceduralByDepth/pdgInterprocedural and the compatibility byDepth bucket. Omit line for whole-symbol pdg (whole-symbol reach + diagnostics); a positive line anchors a statement slice. Literal 0 is tolerated only as an omitted-line compatibility sentinel on the callgraph path and is rejected for mode:'pdg'.",
430
435
  },
431
436
  file_path: {
432
437
  type: 'string',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.9-rc.11",
3
+ "version": "1.6.9-rc.13",
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",