gitnexus 1.6.10 → 1.6.11-rc.2
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/kotlin.js +522 -7
- package/dist/core/group/extractors/http-patterns/php.js +279 -11
- package/dist/core/index-freshness.d.ts +2 -2
- package/dist/core/index-freshness.js +13 -0
- package/dist/core/ingestion/parsing-processor.d.ts +2 -0
- package/dist/core/ingestion/parsing-processor.js +5 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +7 -0
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +4 -0
- package/dist/core/ingestion/pipeline.js +4 -1
- package/dist/core/ingestion/route-extractors/kotlin-const-resolver.d.ts +377 -0
- package/dist/core/ingestion/route-extractors/kotlin-const-resolver.js +1203 -0
- package/dist/core/ingestion/scope-extractor-bridge.js +8 -2
- package/dist/core/ingestion/scope-resolution/pipeline/phase.d.ts +2 -0
- package/dist/core/ingestion/scope-resolution/pipeline/phase.js +14 -2
- package/dist/core/ingestion/scope-resolution/pipeline/run.d.ts +2 -0
- package/dist/core/ingestion/scope-resolution/pipeline/run.js +11 -1
- package/dist/core/ingestion/scope-resolution/scope-extraction-failures.d.ts +14 -0
- package/dist/core/ingestion/scope-resolution/scope-extraction-failures.js +35 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +6 -0
- package/dist/core/ingestion/workers/parse-worker.js +7 -1
- package/dist/core/ingestion/workers/result-merge.js +4 -1
- package/dist/core/run-analyze.js +6 -0
- package/dist/mcp/local/local-backend.d.ts +2 -0
- package/dist/mcp/local/local-backend.js +29 -0
- package/dist/mcp/tools.js +6 -4
- package/dist/storage/parse-cache.js +6 -6
- package/dist/storage/repo-meta.d.ts +17 -1
- package/dist/storage/repo-meta.js +1 -1
- package/dist/types/pipeline.d.ts +4 -0
- package/package.json +1 -1
|
@@ -6,20 +6,36 @@ import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-s
|
|
|
6
6
|
* Providers:
|
|
7
7
|
* - Laravel `Route::get/post/...`
|
|
8
8
|
*
|
|
9
|
-
* Consumers (string-literal URLs only):
|
|
9
|
+
* Consumers (string-literal URLs only, unless noted):
|
|
10
10
|
* - Laravel HTTP client: `Http::get/post/put/delete/patch($url)`
|
|
11
11
|
* - Guzzle / generic object method: `$client->get/post/...($url)`
|
|
12
12
|
* - `file_get_contents($url)`
|
|
13
|
+
* - `new Request($method, $host . $resourcePath)` — the openapi-generator-php
|
|
14
|
+
* / swagger-codegen client shape. `$resourcePath` is resolved via a
|
|
15
|
+
* single-scope backward constant fold (see `resolveLocalStringLiteral`),
|
|
16
|
+
* not a string literal at the call site itself.
|
|
13
17
|
*
|
|
14
18
|
* The pipeline already uses `PHP.php_only` for ingesting plain `.php`
|
|
15
19
|
* files (see `core/tree-sitter/parser-loader.ts`), and we do the same
|
|
16
20
|
* here so Laravel route files are parsed with the right grammar dialect.
|
|
17
21
|
*
|
|
18
|
-
* Scope notes: consumer patterns match string literals only
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* scope to
|
|
22
|
+
* Scope notes: consumer patterns match string literals only, with one
|
|
23
|
+
* narrow exception (above). URLs built via `sprintf`, config lookup
|
|
24
|
+
* (`config('services.foo.base').'/path'`), or a variable resolved from
|
|
25
|
+
* outside its own function/method body are intentionally left for a
|
|
26
|
+
* follow-up — they require constant-folding beyond one local scope to
|
|
27
|
+
* be meaningful.
|
|
28
|
+
*
|
|
29
|
+
* That narrow exception (`resolveLocalStringLiteral`) is a temporary,
|
|
30
|
+
* single-scope fallback, not this language's entry into the shared
|
|
31
|
+
* cross-file constant-fold used by the other languages in this plugin
|
|
32
|
+
* (`constant-resolver.ts`, wired in via `java-const-resolver.ts` /
|
|
33
|
+
* `python-const-resolver.ts` / `js-const-resolver.ts`). PHP has no such
|
|
34
|
+
* binding yet — adding one is a real, separate project (this repo's PHP
|
|
35
|
+
* import resolution for `use`-statements is its own multi-file subsystem
|
|
36
|
+
* under `ingestion/import-resolvers/php.ts`, built for symbol/scope
|
|
37
|
+
* resolution, not constant extraction) and is intentionally out of scope
|
|
38
|
+
* here. Tracked as a follow-up, not silently punted.
|
|
23
39
|
*/
|
|
24
40
|
const LARAVEL_ROUTE_SPEC = {
|
|
25
41
|
meta: {},
|
|
@@ -57,6 +73,24 @@ const FILE_GET_CONTENTS_SPEC = {
|
|
|
57
73
|
arguments: (arguments . (argument (string) @path)))
|
|
58
74
|
`,
|
|
59
75
|
};
|
|
76
|
+
/**
|
|
77
|
+
* `new Request($method, $host . $resourcePath)` — the shape swagger-codegen /
|
|
78
|
+
* openapi-generator-php emit for every operation of a generated API client
|
|
79
|
+
* (Guzzle's `\GuzzleHttp\Psr7\Request`, or a bare `Request` behind a `use`
|
|
80
|
+
* import). Matches both `(name)` and `(qualified_name)` class references;
|
|
81
|
+
* `scan()` below filters to the last path segment being exactly `Request`
|
|
82
|
+
* and resolves the concatenated path argument (see `resolveLocalStringLiteral`).
|
|
83
|
+
*/
|
|
84
|
+
const GUZZLE_REQUEST_CTOR_SPEC = {
|
|
85
|
+
meta: {},
|
|
86
|
+
query: `
|
|
87
|
+
(object_creation_expression
|
|
88
|
+
[(name) (qualified_name)] @class
|
|
89
|
+
(arguments
|
|
90
|
+
. (argument (_) @methodArg)
|
|
91
|
+
. (argument (_) @pathArg)))
|
|
92
|
+
`,
|
|
93
|
+
};
|
|
60
94
|
const mk = (spec, suffix) => compilePatterns({
|
|
61
95
|
name: `php-${suffix}`,
|
|
62
96
|
language: PHP.php_only,
|
|
@@ -67,6 +101,7 @@ const PHP_PATTERNS = {
|
|
|
67
101
|
httpFacade: mk(HTTP_FACADE_SPEC, 'http-facade'),
|
|
68
102
|
guzzleMember: mk(GUZZLE_MEMBER_SPEC, 'guzzle-member'),
|
|
69
103
|
fileGetContents: mk(FILE_GET_CONTENTS_SPEC, 'file-get-contents'),
|
|
104
|
+
guzzleRequestCtor: mk(GUZZLE_REQUEST_CTOR_SPEC, 'guzzle-request-ctor'),
|
|
70
105
|
};
|
|
71
106
|
/**
|
|
72
107
|
* Extract the inner text of a PHP `string` node. The tree-sitter-php
|
|
@@ -103,6 +138,180 @@ function isHttpClientPath(path) {
|
|
|
103
138
|
function isHttpUrlLiteral(path) {
|
|
104
139
|
return path.startsWith('http://') || path.startsWith('https://');
|
|
105
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Last identifier segment of a class-name reference: `(name)` returns its
|
|
143
|
+
* own text, `(qualified_name)` returns the text of its last child (the
|
|
144
|
+
* unqualified class name — `\GuzzleHttp\Psr7\Request` → `Request`).
|
|
145
|
+
*/
|
|
146
|
+
function lastNameSegment(node) {
|
|
147
|
+
if (node.type === 'qualified_name') {
|
|
148
|
+
const last = node.child(node.childCount - 1);
|
|
149
|
+
return last ? last.text : node.text;
|
|
150
|
+
}
|
|
151
|
+
return node.text;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Return the variable at the LAST position of a `.`-concatenation
|
|
155
|
+
* expression, if (and only if) that position is a plain variable —
|
|
156
|
+
* generated clients build `<host> . <resourcePath>`, so the path segment
|
|
157
|
+
* is the one closest to the end.
|
|
158
|
+
*
|
|
159
|
+
* No fallback to an earlier operand: if the rightmost position is anything
|
|
160
|
+
* other than a variable, a parenthesized sub-expression, or a nested `.`
|
|
161
|
+
* concatenation (a literal, a function call, ...), that position is a real
|
|
162
|
+
* value we simply can't resolve — falling back to an EARLIER operand would
|
|
163
|
+
* silently substitute a different value (e.g. the host) for the one that's
|
|
164
|
+
* actually there. `null` here is a miss, not a signal to keep looking.
|
|
165
|
+
*/
|
|
166
|
+
function lastConcatVariable(node) {
|
|
167
|
+
if (node.type === 'variable_name')
|
|
168
|
+
return node;
|
|
169
|
+
if (node.type === 'parenthesized_expression') {
|
|
170
|
+
const inner = node.namedChild(0);
|
|
171
|
+
return inner ? lastConcatVariable(inner) : null;
|
|
172
|
+
}
|
|
173
|
+
if (node.type === 'binary_expression') {
|
|
174
|
+
const operator = node.childForFieldName('operator');
|
|
175
|
+
if (!operator || operator.text !== '.')
|
|
176
|
+
return null; // not concatenation
|
|
177
|
+
const right = node.childForFieldName('right');
|
|
178
|
+
return right ? lastConcatVariable(right) : null;
|
|
179
|
+
}
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* True if `node`'s subtree assigns to `$target` ANYWHERE inside it, at any
|
|
184
|
+
* depth (including inside nested functions — deliberately over-broad: a
|
|
185
|
+
* false positive here only costs a miss in the caller, never a wrong
|
|
186
|
+
* answer, so there's no need to be precise about scoping inside the probe
|
|
187
|
+
* itself).
|
|
188
|
+
*/
|
|
189
|
+
function containsAssignmentTo(node, target) {
|
|
190
|
+
if (node.type === 'assignment_expression') {
|
|
191
|
+
const lhs = node.childForFieldName('left');
|
|
192
|
+
if (lhs && lhs.type === 'variable_name' && lhs.text === target)
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
196
|
+
const child = node.namedChild(i);
|
|
197
|
+
if (child && containsAssignmentTo(child, target))
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* True if an `anonymous_function` node's `use (...)` clause lists
|
|
204
|
+
* `$target`. PHP closures capture NOTHING automatically — only variables
|
|
205
|
+
* named in `use (...)` are visible inside — unlike arrow functions
|
|
206
|
+
* (`fn() => ...`), which auto-capture everything by value and have no
|
|
207
|
+
* `compound_statement` body of their own, so they're never seen as a
|
|
208
|
+
* `scope` by the walk below in the first place.
|
|
209
|
+
*/
|
|
210
|
+
function anonymousFunctionCaptures(anonFn, target) {
|
|
211
|
+
for (let i = 0; i < anonFn.namedChildCount; i++) {
|
|
212
|
+
const child = anonFn.namedChild(i);
|
|
213
|
+
if (!child || child.type !== 'anonymous_function_use_clause')
|
|
214
|
+
continue;
|
|
215
|
+
for (let j = 0; j < child.namedChildCount; j++) {
|
|
216
|
+
const v = child.namedChild(j);
|
|
217
|
+
if (v && v.type === 'variable_name' && v.text === target)
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
return false; // has a use(...) clause, but $target isn't in it
|
|
221
|
+
}
|
|
222
|
+
return false; // no use(...) clause at all — nothing is captured
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Best-effort, single-scope constant fold: given a `variable_name` node
|
|
226
|
+
* referenced inside a `new Request(...)` argument, walk BACKWARD through
|
|
227
|
+
* the preceding statements of its immediately enclosing function/method
|
|
228
|
+
* body (or file scope, for top-level script code) looking for the nearest
|
|
229
|
+
* `$var = '<literal>';` assignment.
|
|
230
|
+
*
|
|
231
|
+
* "Enclosing body" is resolved level by level, not just the nearest
|
|
232
|
+
* `compound_statement` — a call site nested in `if`/`foreach`/`try` inside
|
|
233
|
+
* that function is still within the same function/method body, and a
|
|
234
|
+
* preceding assignment above that conditional must still be found. Each
|
|
235
|
+
* level searches only its own preceding siblings, then the search
|
|
236
|
+
* continues from the enclosing block itself one level up, UNLESS that
|
|
237
|
+
* block IS the body of a function/method/closure:
|
|
238
|
+
* - a regular `function_definition` or `method_declaration` boundary
|
|
239
|
+
* always stops the search — PHP gives a function or method no access
|
|
240
|
+
* to anything outside its own body (no automatic capture, no implicit
|
|
241
|
+
* global), so widening past one into the containing class or
|
|
242
|
+
* file-level scope would resolve a variable the call site could never
|
|
243
|
+
* actually see at runtime;
|
|
244
|
+
* - an `anonymous_function` boundary stops UNLESS `$target` is
|
|
245
|
+
* explicitly captured via `use (...)` — closures capture nothing
|
|
246
|
+
* automatically either.
|
|
247
|
+
* It stops at `program` regardless, for the case where the call site was
|
|
248
|
+
* at file/script scope all along.
|
|
249
|
+
*
|
|
250
|
+
* A preceding sibling that ISN'T a plain assignment but might reassign the
|
|
251
|
+
* target somewhere inside itself (an `if`/`foreach`/`try`/`switch`, ...)
|
|
252
|
+
* stops the search rather than being skipped over: whether that branch ran
|
|
253
|
+
* is unknown, so an older literal further back can't be trusted either.
|
|
254
|
+
*
|
|
255
|
+
* Deliberately conservative and bounded — no interprocedural resolution,
|
|
256
|
+
* no constant/property lookups. A miss just means the endpoint stays
|
|
257
|
+
* undetected, never a wrong one: this is exactly the class of case the
|
|
258
|
+
* module docblock flags as in-scope only for one local scope.
|
|
259
|
+
*/
|
|
260
|
+
function resolveLocalStringLiteral(varNode) {
|
|
261
|
+
const target = varNode.text; // includes the `$` sigil, e.g. "$resourcePath"
|
|
262
|
+
let cursor = varNode;
|
|
263
|
+
for (;;) {
|
|
264
|
+
let scope = cursor.parent;
|
|
265
|
+
while (scope && scope.type !== 'compound_statement' && scope.type !== 'program') {
|
|
266
|
+
scope = scope.parent;
|
|
267
|
+
}
|
|
268
|
+
if (!scope)
|
|
269
|
+
return null;
|
|
270
|
+
let stmt = cursor;
|
|
271
|
+
while (stmt && stmt.parent !== scope)
|
|
272
|
+
stmt = stmt.parent;
|
|
273
|
+
if (!stmt)
|
|
274
|
+
return null;
|
|
275
|
+
let sibling = stmt.previousNamedSibling;
|
|
276
|
+
while (sibling) {
|
|
277
|
+
if (sibling.type === 'expression_statement') {
|
|
278
|
+
const inner = sibling.namedChild(0);
|
|
279
|
+
if (inner && inner.type === 'assignment_expression') {
|
|
280
|
+
const lhs = inner.childForFieldName('left');
|
|
281
|
+
if (lhs && lhs.type === 'variable_name' && lhs.text === target) {
|
|
282
|
+
// The NEAREST assignment to this variable wins, full stop — an
|
|
283
|
+
// older literal further back is shadowed by this one even when
|
|
284
|
+
// this one isn't itself a resolvable string (`$v = f();`).
|
|
285
|
+
const rhs = inner.childForFieldName('right');
|
|
286
|
+
return rhs && rhs.type === 'string' ? phpStringText(rhs) : null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else if (containsAssignmentTo(sibling, target)) {
|
|
291
|
+
return null; // reassigned somewhere inside a conditional/loop/try
|
|
292
|
+
}
|
|
293
|
+
sibling = sibling.previousNamedSibling;
|
|
294
|
+
}
|
|
295
|
+
if (scope.type === 'program')
|
|
296
|
+
return null;
|
|
297
|
+
const enclosing = scope.parent;
|
|
298
|
+
if (enclosing && enclosing.type === 'anonymous_function') {
|
|
299
|
+
// Closures capture nothing automatically — only what's use()'d.
|
|
300
|
+
if (!anonymousFunctionCaptures(enclosing, target))
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
else if (enclosing &&
|
|
304
|
+
(enclosing.type === 'function_definition' || enclosing.type === 'method_declaration')) {
|
|
305
|
+
// A regular function or method boundary — NOT a closure. PHP gives
|
|
306
|
+
// these no access to anything outside their own body (no automatic
|
|
307
|
+
// capture, no implicit global): widening past one into the
|
|
308
|
+
// containing class body or file-level scope would resolve a
|
|
309
|
+
// variable the call site could never actually see at runtime.
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
cursor = scope; // one block up: search resumes from this block's own position
|
|
313
|
+
}
|
|
314
|
+
}
|
|
106
315
|
export const PHP_HTTP_PLUGIN = {
|
|
107
316
|
name: 'php-http',
|
|
108
317
|
language: PHP.php_only,
|
|
@@ -110,12 +319,14 @@ export const PHP_HTTP_PLUGIN = {
|
|
|
110
319
|
// ingestion, so the graph is authoritative for PHP providers (#2138 Part 2).
|
|
111
320
|
routeCoverage: 'complete',
|
|
112
321
|
// Consumer signals scan() can detect: Laravel `Http::<verb>`, Guzzle client
|
|
113
|
-
// `->get/post/.../request(...)`,
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
322
|
+
// `->get/post/.../request(...)`, `file_get_contents` of an HTTP URL, and a
|
|
323
|
+
// generated-client `new ...Request(...)` constructor call. A provider-covered
|
|
324
|
+
// file with any of these must still be parsed (ingestion emits no FETCHES for
|
|
325
|
+
// PHP). Conservative — the `->verb(`/`new ...Request(` shapes over-match
|
|
326
|
+
// ordinary method calls and unrelated constructors, which only costs a
|
|
327
|
+
// parse, never data.
|
|
117
328
|
hasConsumerSignals(content) {
|
|
118
|
-
return /Http::|file_get_contents|->\s*(get|post|put|delete|patch|request)\s*\(/i.test(content);
|
|
329
|
+
return /Http::|file_get_contents|->\s*(get|post|put|delete|patch|request)\s*\(|new\s+[\\\w]*Request\s*\(/i.test(content);
|
|
119
330
|
},
|
|
120
331
|
scan(tree) {
|
|
121
332
|
const out = [];
|
|
@@ -199,6 +410,63 @@ export const PHP_HTTP_PLUGIN = {
|
|
|
199
410
|
confidence: 0.7,
|
|
200
411
|
});
|
|
201
412
|
}
|
|
413
|
+
for (const match of runCompiledPatterns(PHP_PATTERNS.guzzleRequestCtor, tree)) {
|
|
414
|
+
const classNode = match.captures.class;
|
|
415
|
+
const methodArg = match.captures.methodArg;
|
|
416
|
+
const pathArg = match.captures.pathArg;
|
|
417
|
+
if (!classNode || !methodArg || !pathArg)
|
|
418
|
+
continue;
|
|
419
|
+
// PHP class names are case-insensitive at the language level, and
|
|
420
|
+
// `hasConsumerSignals` above matches case-insensitively (`/i`) for
|
|
421
|
+
// the same reason — this comparison must agree with it, or a valid
|
|
422
|
+
// `new request(...)` / `new \NS\REQUEST(...)` call would be waved
|
|
423
|
+
// through the parse-skip gate as a signal and then silently dropped
|
|
424
|
+
// here.
|
|
425
|
+
if (lastNameSegment(classNode).toLowerCase() !== 'request')
|
|
426
|
+
continue;
|
|
427
|
+
// Path: a direct string literal, or the last variable in a
|
|
428
|
+
// concatenation chain (see `lastConcatVariable`) resolved to a
|
|
429
|
+
// locally-assigned literal.
|
|
430
|
+
let path = null;
|
|
431
|
+
if (pathArg.type === 'string') {
|
|
432
|
+
path = phpStringText(pathArg);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
const lastVar = lastConcatVariable(pathArg);
|
|
436
|
+
path = lastVar ? resolveLocalStringLiteral(lastVar) : null;
|
|
437
|
+
}
|
|
438
|
+
if (path === null || !isHttpClientPath(path))
|
|
439
|
+
continue;
|
|
440
|
+
// The HTTP verb is a literal, a local variable resolved the same way
|
|
441
|
+
// as the path (see `resolveLocalStringLiteral` above), or — commonly
|
|
442
|
+
// in generated clients — a parameter of the enclosing builder method
|
|
443
|
+
// fixed by ITS caller, not by this call site. That last case needs
|
|
444
|
+
// the same interprocedural reach the module docblock rules out, so it
|
|
445
|
+
// falls through to a wildcard verb, matching this project's own
|
|
446
|
+
// convention for a contract whose verb isn't pinned (see manifest
|
|
447
|
+
// links, `http::*::`).
|
|
448
|
+
let method = null;
|
|
449
|
+
if (methodArg.type === 'string') {
|
|
450
|
+
method = phpStringText(methodArg);
|
|
451
|
+
}
|
|
452
|
+
else if (methodArg.type === 'variable_name') {
|
|
453
|
+
method = resolveLocalStringLiteral(methodArg);
|
|
454
|
+
}
|
|
455
|
+
out.push({
|
|
456
|
+
role: 'consumer',
|
|
457
|
+
framework: 'guzzle-request-ctor',
|
|
458
|
+
method: method ? method.toUpperCase() : '*',
|
|
459
|
+
path,
|
|
460
|
+
name: null,
|
|
461
|
+
// Line of the path ARGUMENT, not the `new Request(` call — same
|
|
462
|
+
// choice the other three consumer patterns in this file make, but
|
|
463
|
+
// this is the one pattern where the two routinely differ (generated
|
|
464
|
+
// clients wrap the call across multiple lines). Line-span
|
|
465
|
+
// containment still resolves to the right symbol either way.
|
|
466
|
+
line: pathArg.startPosition.row + 1,
|
|
467
|
+
confidence: 0.6,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
202
470
|
return out;
|
|
203
471
|
},
|
|
204
472
|
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { RepoMeta } from '../storage/repo-manager.js';
|
|
2
|
-
export declare const INDEX_INCOMPLETE_REASONS: readonly ["incremental-in-progress", "embedding-checkpoint-pending", "embedding-count-unverified", "graph-write-collapsed"];
|
|
2
|
+
export declare const INDEX_INCOMPLETE_REASONS: readonly ["incremental-in-progress", "embedding-checkpoint-pending", "embedding-count-unverified", "graph-write-collapsed", "scope-extraction-unverified", "scope-extraction-failed"];
|
|
3
3
|
export type IndexIncompleteReason = (typeof INDEX_INCOMPLETE_REASONS)[number];
|
|
4
4
|
/**
|
|
5
5
|
* Fraction of the pipeline's relationship count that must survive into the DB
|
|
@@ -77,4 +77,4 @@ export declare function detectGraphWriteCollapse(expected: number,
|
|
|
77
77
|
*/
|
|
78
78
|
persisted: number | undefined): GraphWriteCollapseVerdict;
|
|
79
79
|
/** Stable machine-readable reasons an index cannot be certified complete. */
|
|
80
|
-
export declare function getIndexIncompleteReasons(meta: Pick<RepoMeta, 'incrementalInProgress' | 'embeddingCheckpoint' | 'graphWriteCollapsed'> | null | undefined): IndexIncompleteReason[];
|
|
80
|
+
export declare function getIndexIncompleteReasons(meta: Pick<RepoMeta, 'incrementalInProgress' | 'embeddingCheckpoint' | 'graphWriteCollapsed' | 'scopeExtractionFailures' | 'scopeExtractionReceipt'> | null | undefined): IndexIncompleteReason[];
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { checkpointKind } from './embedding-checkpoint.js';
|
|
2
|
+
import { scopeExtractionFailureTotal } from './ingestion/scope-resolution/scope-extraction-failures.js';
|
|
2
3
|
export const INDEX_INCOMPLETE_REASONS = [
|
|
3
4
|
'incremental-in-progress',
|
|
4
5
|
'embedding-checkpoint-pending',
|
|
5
6
|
'embedding-count-unverified',
|
|
6
7
|
'graph-write-collapsed',
|
|
8
|
+
'scope-extraction-unverified',
|
|
9
|
+
'scope-extraction-failed',
|
|
7
10
|
];
|
|
8
11
|
/**
|
|
9
12
|
* Fraction of the pipeline's relationship count that must survive into the DB
|
|
@@ -105,6 +108,16 @@ export function getIndexIncompleteReasons(meta) {
|
|
|
105
108
|
// from a codebase that genuinely has no such relationships.
|
|
106
109
|
if (meta?.graphWriteCollapsed)
|
|
107
110
|
reasons.push('graph-write-collapsed');
|
|
111
|
+
if (meta?.scopeExtractionReceipt !== 1) {
|
|
112
|
+
reasons.push('scope-extraction-unverified');
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const total = scopeExtractionFailureTotal(meta.scopeExtractionFailures);
|
|
116
|
+
if (total === undefined)
|
|
117
|
+
reasons.push('scope-extraction-unverified');
|
|
118
|
+
else if (total > 0)
|
|
119
|
+
reasons.push('scope-extraction-failed');
|
|
120
|
+
}
|
|
108
121
|
if (meta?.embeddingCheckpoint) {
|
|
109
122
|
// The three checkpoint kinds are not one operator-facing state. GUARDRAILS
|
|
110
123
|
// and the runbook document `embedding-checkpoint-pending` as "N node(s)
|
|
@@ -31,6 +31,8 @@ export interface WorkerExtractedData {
|
|
|
31
31
|
* finalize-orchestrator.
|
|
32
32
|
*/
|
|
33
33
|
parsedFiles: ParsedFile[];
|
|
34
|
+
/** Scope-extraction omissions represented by this worker/cache result. */
|
|
35
|
+
scopeExtractionFailures: string[];
|
|
34
36
|
}
|
|
35
37
|
/**
|
|
36
38
|
* Merge a list of `ParseWorkerResult`s into the running graph + symbol
|
|
@@ -57,6 +57,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
|
|
|
57
57
|
const allORMQueries = [];
|
|
58
58
|
const fileScopeBindingsByFile = [];
|
|
59
59
|
const allParsedFiles = [];
|
|
60
|
+
const scopeExtractionFailures = [];
|
|
60
61
|
for (const result of chunkResults) {
|
|
61
62
|
// Worker jobs and input files are already merged in stable start-index/path
|
|
62
63
|
// order. Canonicalize the final per-result node boundary once so graph
|
|
@@ -123,6 +124,9 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
|
|
|
123
124
|
if (result.parsedFiles)
|
|
124
125
|
for (const item of result.parsedFiles)
|
|
125
126
|
allParsedFiles.push(item);
|
|
127
|
+
for (const filePath of result.scopeExtractionFailures ?? []) {
|
|
128
|
+
scopeExtractionFailures.push(filePath);
|
|
129
|
+
}
|
|
126
130
|
}
|
|
127
131
|
return {
|
|
128
132
|
routes: allRoutes,
|
|
@@ -139,6 +143,7 @@ export const mergeChunkResults = (graph, symbolTable, chunkResults, exportedType
|
|
|
139
143
|
springTypes: allSpringTypes,
|
|
140
144
|
fileScopeBindings: fileScopeBindingsByFile,
|
|
141
145
|
parsedFiles: allParsedFiles,
|
|
146
|
+
scopeExtractionFailures,
|
|
142
147
|
};
|
|
143
148
|
};
|
|
144
149
|
/**
|
|
@@ -99,5 +99,8 @@ export declare function runChunkedParseAndResolve(graph: KnowledgeGraph, scanned
|
|
|
99
99
|
* cache analyze run can skip the dominant `extractParsedFile` cost
|
|
100
100
|
* (otherwise ~58s on a 1000-file repo). */
|
|
101
101
|
parsedFiles: import('../../../_shared/index.js').ParsedFile[];
|
|
102
|
+
scopeExtractionFailures: string[];
|
|
103
|
+
/** Files excluded because their non-standalone language parser was unavailable. */
|
|
104
|
+
unavailableScopeLanguageFiles: number;
|
|
102
105
|
}>;
|
|
103
106
|
export {};
|
|
@@ -366,6 +366,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
366
366
|
logger.warn(`Skipping ${count} ${lang} file(s) — ${lang} parser not available (native binding may not have built). Try: npm rebuild tree-sitter-${lang}`);
|
|
367
367
|
}
|
|
368
368
|
}
|
|
369
|
+
const unavailableScopeLanguageFiles = [...skippedByLang.values()].reduce((total, count) => total + count, 0);
|
|
369
370
|
// Sort parseableScanned alphabetically for stable chunk membership
|
|
370
371
|
// across runs (Finding 4). Without this, filesystem-scan order can
|
|
371
372
|
// shift between runs (notably on macOS APFS where directory entry
|
|
@@ -563,6 +564,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
563
564
|
// the second-half of the parse-cache speedup since scope-resolution's
|
|
564
565
|
// re-parse otherwise dominates the warm-cache wall-clock time.
|
|
565
566
|
const allParsedFiles = [];
|
|
567
|
+
const scopeExtractionFailures = new Set();
|
|
566
568
|
// Incremental parse cache (Option B): chunk-level content-addressed.
|
|
567
569
|
// When the chunk's (filePath, content-hash) signature matches a prior
|
|
568
570
|
// run's, replay the cached ParseWorkerResult[] instead of dispatching
|
|
@@ -638,6 +640,9 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
638
640
|
// (which was the only path that passed null) is gone.
|
|
639
641
|
const applyChunkResults = async (chunkWorkerData, chunkIdx, chunkFiles, chunkStartMs) => {
|
|
640
642
|
if (chunkWorkerData) {
|
|
643
|
+
for (const filePath of chunkWorkerData.scopeExtractionFailures) {
|
|
644
|
+
scopeExtractionFailures.add(filePath);
|
|
645
|
+
}
|
|
641
646
|
if (chunkWorkerData.parsedFiles?.length) {
|
|
642
647
|
if (parsedFileStorePath) {
|
|
643
648
|
await persistParsedFileChunk(parsedFileStorePath, `chunk-${chunkIdx}`, chunkWorkerData.parsedFiles);
|
|
@@ -1336,5 +1341,7 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1336
1341
|
// cache: when the file's ParsedFile is here, scope-resolution skips its own
|
|
1337
1342
|
// `extractParsedFile` call.
|
|
1338
1343
|
parsedFiles: allParsedFiles,
|
|
1344
|
+
scopeExtractionFailures: [...scopeExtractionFailures].sort(),
|
|
1345
|
+
unavailableScopeLanguageFiles,
|
|
1339
1346
|
};
|
|
1340
1347
|
}
|
|
@@ -68,5 +68,9 @@ export interface ParseOutput {
|
|
|
68
68
|
* costing ~58s on a 1000-file repo).
|
|
69
69
|
*/
|
|
70
70
|
readonly parsedFiles: readonly ParsedFile[];
|
|
71
|
+
/** Files whose scope extraction failed while legacy parsing continued. */
|
|
72
|
+
readonly scopeExtractionFailures: readonly string[];
|
|
73
|
+
/** Files omitted because their non-standalone language parser was unavailable. */
|
|
74
|
+
readonly unavailableScopeLanguageFiles: number;
|
|
71
75
|
}
|
|
72
76
|
export declare const parsePhase: PipelinePhase<ParseOutput>;
|
|
@@ -109,10 +109,11 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
109
109
|
graphEmitSink?.close();
|
|
110
110
|
}
|
|
111
111
|
// Extract final results for the PipelineResult contract
|
|
112
|
-
const { totalFiles, usedWorkerPool } = getPhaseOutput(results, 'parse');
|
|
112
|
+
const { totalFiles, usedWorkerPool, unavailableScopeLanguageFiles } = getPhaseOutput(results, 'parse');
|
|
113
113
|
let communityResult;
|
|
114
114
|
let processResult;
|
|
115
115
|
const scopeResolutionOutput = getPhaseOutput(results, 'scopeResolution');
|
|
116
|
+
const scopeExtractionFailures = scopeResolutionOutput.scopeExtractionFailures;
|
|
116
117
|
const resolutionOutcomes = scopeResolutionOutput.resolutionOutcomes;
|
|
117
118
|
const undecidedSatisfaction = scopeResolutionOutput.undecidedSatisfaction;
|
|
118
119
|
// Streamed PDG-emit manifest (#2202): present only when streaming was on.
|
|
@@ -155,6 +156,8 @@ export const runPipelineFromRepo = async (repoPath, onProgress, options) => {
|
|
|
155
156
|
resolutionOutcomes,
|
|
156
157
|
undecidedSatisfaction,
|
|
157
158
|
usedWorkerPool,
|
|
159
|
+
scopeExtractionFailures,
|
|
160
|
+
unavailableScopeLanguageFiles,
|
|
158
161
|
pdgEmitManifest,
|
|
159
162
|
propertyInference,
|
|
160
163
|
};
|