gitnexus 1.6.9-rc.12 → 1.6.9-rc.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/group/extractors/http-patterns/java.js +10 -7
- package/dist/core/ingestion/route-extractors/spring.js +46 -5
- package/dist/core/logger.d.ts +10 -1
- package/dist/core/logger.js +9 -3
- package/dist/mcp/local/local-backend.js +82 -9
- package/dist/mcp/tools.js +7 -2
- package/package.json +1 -1
|
@@ -576,13 +576,16 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
576
576
|
language: Java,
|
|
577
577
|
// routeCoverage intentionally LEFT at the default 'partial' (#2138 Part 2).
|
|
578
578
|
// The graph provider set is a strict *subset* of this scan()'s provider set —
|
|
579
|
-
// ingestion does NOT emit a Route node for (1)
|
|
580
|
-
//
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
579
|
+
// ingestion does NOT emit a Route node for (1) a method-level array route
|
|
580
|
+
// nested under a class-level array-form `@RequestMapping` (ingestion suppresses
|
|
581
|
+
// it rather than drop the prefix; bare/scalar-prefixed array methods ARE now
|
|
582
|
+
// emitted — see #2280), (2) interface-inherited Spring routes, or (3) the 2nd
|
|
583
|
+
// verb of a same-URL GET+POST pair (Route nodes are URL-keyed). Declaring
|
|
584
|
+
// 'complete' here would let the parse-skip drop those group-only providers.
|
|
585
|
+
// Java flips to 'complete' only once ingestion provider extraction matches this
|
|
586
|
+
// scan (a follow-up: class-level array-form prefix support + interface-
|
|
587
|
+
// inheritance emission + per-verb Route identity — tracked in #2280).
|
|
588
|
+
// `hasConsumerSignals` below is kept ready for that flip.
|
|
586
589
|
// Consumer signals this plugin's scan() can detect: RestTemplate / WebClient /
|
|
587
590
|
// OkHttp / Java-HttpClient / Apache-HttpClient call sites, OpenFeign
|
|
588
591
|
// (`@FeignClient` + `@RequestLine`) interfaces, and Spring 6 HTTP Interface
|
|
@@ -31,6 +31,18 @@ import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingClass, unquot
|
|
|
31
31
|
* @node → enclosing declaration (class_declaration | method_declaration)
|
|
32
32
|
* @value → the string-literal argument
|
|
33
33
|
* @key → the named-argument member key (absent for positional form)
|
|
34
|
+
*
|
|
35
|
+
* Method-level routes accept both the bare string form `@GetMapping("/x")` and
|
|
36
|
+
* the array form `@GetMapping({"/a","/b"})` (positional or `path =`/`value =`):
|
|
37
|
+
* a multi-element array yields one match per element, so the Phase 2 loop emits
|
|
38
|
+
* one route per path with no special-casing. This mirrors the group-layer
|
|
39
|
+
* `java.ts` query so the two Spring extractors stay in parity (#2138 follow-up;
|
|
40
|
+
* the divergence here was the root of the #2265 array-form gap). The class-level
|
|
41
|
+
* `@RequestMapping` branches also match the array form, but only to *detect* it:
|
|
42
|
+
* an array-form class prefix can't be resolved to a single string, so Phase 2
|
|
43
|
+
* suppresses that class's method-level array routes rather than emit them with a
|
|
44
|
+
* dropped prefix (a wrong route). Full class-array cross-product support is left
|
|
45
|
+
* to a follow-up (#2280).
|
|
34
46
|
*/
|
|
35
47
|
const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
|
|
36
48
|
[
|
|
@@ -38,7 +50,9 @@ const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
|
|
|
38
50
|
(modifiers
|
|
39
51
|
(annotation
|
|
40
52
|
name: (identifier) @ann
|
|
41
|
-
arguments: (annotation_argument_list
|
|
53
|
+
arguments: (annotation_argument_list
|
|
54
|
+
[(string_literal) @value
|
|
55
|
+
(element_value_array_initializer (string_literal) @value)])))) @node
|
|
42
56
|
(class_declaration
|
|
43
57
|
(modifiers
|
|
44
58
|
(annotation
|
|
@@ -46,12 +60,15 @@ const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
|
|
|
46
60
|
arguments: (annotation_argument_list
|
|
47
61
|
(element_value_pair
|
|
48
62
|
key: (identifier) @key
|
|
49
|
-
value: (string_literal) @value
|
|
63
|
+
value: [(string_literal) @value
|
|
64
|
+
(element_value_array_initializer (string_literal) @value)]))))) @node
|
|
50
65
|
(method_declaration
|
|
51
66
|
(modifiers
|
|
52
67
|
(annotation
|
|
53
68
|
name: (identifier) @ann
|
|
54
|
-
arguments: (annotation_argument_list
|
|
69
|
+
arguments: (annotation_argument_list
|
|
70
|
+
[(string_literal) @value
|
|
71
|
+
(element_value_array_initializer (string_literal) @value)])))) @node
|
|
55
72
|
(method_declaration
|
|
56
73
|
(modifiers
|
|
57
74
|
(annotation
|
|
@@ -59,7 +76,8 @@ const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
|
|
|
59
76
|
arguments: (annotation_argument_list
|
|
60
77
|
(element_value_pair
|
|
61
78
|
key: (identifier) @key
|
|
62
|
-
value: (string_literal) @value
|
|
79
|
+
value: [(string_literal) @value
|
|
80
|
+
(element_value_array_initializer (string_literal) @value)]))))) @node
|
|
63
81
|
]
|
|
64
82
|
`);
|
|
65
83
|
/**
|
|
@@ -76,8 +94,15 @@ const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
|
|
|
76
94
|
*/
|
|
77
95
|
export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
|
|
78
96
|
const matches = ROUTE_ANNOTATION_QUERY.matches(tree.rootNode);
|
|
79
|
-
// Phase 1: collect class-level @RequestMapping prefixes keyed by node id
|
|
97
|
+
// Phase 1: collect class-level @RequestMapping prefixes keyed by node id.
|
|
98
|
+
// A scalar prefix (`@RequestMapping("/base")`) is stored in prefixByClassId.
|
|
99
|
+
// A class whose @RequestMapping uses the array form (`@RequestMapping({...})`)
|
|
100
|
+
// is instead recorded in classesWithArrayPrefix: there is no single prefix to
|
|
101
|
+
// store, and Phase 2 uses this to suppress that class's method-level array
|
|
102
|
+
// routes rather than emit them unprefixed (a wrong route — see #2280). Full
|
|
103
|
+
// class-array cross-product support is out of scope here.
|
|
80
104
|
const prefixByClassId = new Map();
|
|
105
|
+
const classesWithArrayPrefix = new Set();
|
|
81
106
|
for (const match of matches) {
|
|
82
107
|
const caps = {};
|
|
83
108
|
for (const { name, node } of match.captures) {
|
|
@@ -92,6 +117,10 @@ export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
|
|
|
92
117
|
if (node.type === 'class_declaration' && annNode.text === 'RequestMapping') {
|
|
93
118
|
if (!isRouteMemberKey(keyNode))
|
|
94
119
|
continue;
|
|
120
|
+
if (valueNode.parent?.type === 'element_value_array_initializer') {
|
|
121
|
+
classesWithArrayPrefix.add(node.id);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
95
124
|
const prefix = unquoteSpringLiteral(valueNode.text);
|
|
96
125
|
if (prefix !== null)
|
|
97
126
|
prefixByClassId.set(node.id, prefix);
|
|
@@ -122,6 +151,18 @@ export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
|
|
|
122
151
|
if (routePath === null)
|
|
123
152
|
continue;
|
|
124
153
|
const enclosingClass = findEnclosingClass(node);
|
|
154
|
+
// Suppress a method-level *array-form* route nested under a class-level
|
|
155
|
+
// array-form @RequestMapping. The class prefix is one of several values that
|
|
156
|
+
// cannot be resolved to a single string here, so emitting the route would
|
|
157
|
+
// drop the prefix and yield a wrong unprefixed Route (a false signal, worse
|
|
158
|
+
// than a missing one). Skipping keeps ingestion a strict subset of the group
|
|
159
|
+
// scan — safe under routeCoverage:'partial'. Full class-array cross-product
|
|
160
|
+
// support is tracked in #2280. (Scalar method paths under an array class
|
|
161
|
+
// prefix are left unchanged: that pre-existing divergence is out of scope.)
|
|
162
|
+
const isArrayElement = valueNode.parent?.type === 'element_value_array_initializer';
|
|
163
|
+
if (isArrayElement && enclosingClass && classesWithArrayPrefix.has(enclosingClass.id)) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
125
166
|
const classPrefix = enclosingClass ? (prefixByClassId.get(enclosingClass.id) ?? '') : '';
|
|
126
167
|
// `node` is the annotated `method_declaration`; its name field is the
|
|
127
168
|
// handler method name (resolved to a symbol UID later by the routes phase).
|
package/dist/core/logger.d.ts
CHANGED
|
@@ -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;
|
package/dist/core/logger.js
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
3671
|
-
(!Number.isInteger(
|
|
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
|
|
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
|
-
|
|
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