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.
- package/dist/core/group/cross-trace.js +7 -5
- package/dist/core/group/extractors/http-patterns/go.js +21 -6
- package/dist/core/group/extractors/http-patterns/java.js +7 -0
- package/dist/core/group/extractors/http-patterns/kotlin.js +7 -0
- package/dist/core/group/extractors/http-patterns/php.js +14 -2
- package/dist/core/group/extractors/http-patterns/python.js +8 -0
- 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
|
@@ -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
|
|
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
|
|
740
|
-
*
|
|
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 &&
|
|
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
|
-
//
|
|
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
|
|
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
|
}
|
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