gitnexus 1.6.7 → 1.6.8-rc.10

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.
Files changed (82) hide show
  1. package/README.md +20 -0
  2. package/dist/cli/analyze-config.js +39 -0
  3. package/dist/cli/analyze.d.ts +16 -0
  4. package/dist/cli/analyze.js +34 -6
  5. package/dist/cli/clean.d.ts +1 -0
  6. package/dist/cli/clean.js +43 -1
  7. package/dist/cli/eval-server.js +43 -0
  8. package/dist/cli/help-i18n.js +6 -0
  9. package/dist/cli/i18n/en.d.ts +11 -0
  10. package/dist/cli/i18n/en.js +11 -0
  11. package/dist/cli/i18n/resources.d.ts +22 -0
  12. package/dist/cli/i18n/zh-CN.d.ts +11 -0
  13. package/dist/cli/i18n/zh-CN.js +11 -0
  14. package/dist/cli/index.js +9 -0
  15. package/dist/cli/list.js +12 -0
  16. package/dist/cli/optional-grammars.d.ts +6 -8
  17. package/dist/cli/optional-grammars.js +8 -11
  18. package/dist/cli/status.js +26 -5
  19. package/dist/cli/tool.d.ts +5 -0
  20. package/dist/cli/tool.js +5 -0
  21. package/dist/core/embeddings/embedder.js +4 -0
  22. package/dist/core/embeddings/embedding-pipeline.js +27 -16
  23. package/dist/core/embeddings/onnxruntime-common-resolver.d.ts +6 -0
  24. package/dist/core/embeddings/onnxruntime-common-resolver.js +130 -0
  25. package/dist/core/group/extractors/grpc-patterns/proto.js +10 -6
  26. package/dist/core/group/extractors/http-patterns/java.js +3 -50
  27. package/dist/core/group/extractors/http-patterns/kotlin.js +7 -7
  28. package/dist/core/group/extractors/include-extractor.js +7 -7
  29. package/dist/core/ingestion/language-provider.d.ts +25 -2
  30. package/dist/core/ingestion/languages/c-cpp.js +11 -4
  31. package/dist/core/ingestion/languages/java.js +3 -0
  32. package/dist/core/ingestion/languages/kotlin/query.js +3 -2
  33. package/dist/core/ingestion/languages/kotlin.js +5 -1
  34. package/dist/core/ingestion/parsing-processor.js +21 -0
  35. package/dist/core/ingestion/pipeline-phases/routes.js +64 -14
  36. package/dist/core/ingestion/pipeline.d.ts +9 -0
  37. package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
  38. package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
  39. package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
  40. package/dist/core/ingestion/route-extractors/spring.js +136 -0
  41. package/dist/core/ingestion/workers/clone-safety.d.ts +109 -0
  42. package/dist/core/ingestion/workers/clone-safety.js +465 -0
  43. package/dist/core/ingestion/workers/parse-worker.d.ts +10 -0
  44. package/dist/core/ingestion/workers/parse-worker.js +27 -52
  45. package/dist/core/ingestion/workers/post-result.d.ts +22 -0
  46. package/dist/core/ingestion/workers/post-result.js +87 -0
  47. package/dist/core/ingestion/workers/result-merge.d.ts +20 -0
  48. package/dist/core/ingestion/workers/result-merge.js +43 -0
  49. package/dist/core/ingestion/workers/worker-pool.d.ts +6 -0
  50. package/dist/core/ingestion/workers/worker-pool.js +28 -14
  51. package/dist/core/lbug/lbug-adapter.d.ts +19 -0
  52. package/dist/core/lbug/lbug-adapter.js +56 -1
  53. package/dist/core/run-analyze.d.ts +45 -0
  54. package/dist/core/run-analyze.js +175 -24
  55. package/dist/core/tree-sitter/parser-loader.js +5 -4
  56. package/dist/core/tree-sitter/vendored-grammars.d.ts +39 -0
  57. package/dist/core/tree-sitter/vendored-grammars.js +57 -0
  58. package/dist/mcp/core/embedder.js +4 -0
  59. package/dist/mcp/local/local-backend.d.ts +55 -2
  60. package/dist/mcp/local/local-backend.js +363 -25
  61. package/dist/mcp/tools.js +32 -0
  62. package/dist/server/analyze-worker-ipc.d.ts +58 -0
  63. package/dist/server/analyze-worker-ipc.js +16 -0
  64. package/dist/server/analyze-worker.js +7 -1
  65. package/dist/storage/branch-index.d.ts +52 -0
  66. package/dist/storage/branch-index.js +65 -0
  67. package/dist/storage/git.d.ts +11 -0
  68. package/dist/storage/git.js +28 -0
  69. package/dist/storage/parse-cache.js +3 -0
  70. package/dist/storage/repo-manager.d.ts +57 -2
  71. package/dist/storage/repo-manager.js +132 -18
  72. package/hooks/antigravity/gitnexus-antigravity-hook.cjs +24 -4
  73. package/hooks/claude/gitnexus-hook.cjs +18 -3
  74. package/package.json +2 -2
  75. package/scripts/assert-publish-grammar-coverage.cjs +31 -0
  76. package/scripts/build-tree-sitter-grammars.cjs +16 -10
  77. package/vendor/tree-sitter-c/package.json +1 -1
  78. package/vendor/tree-sitter-dart/package.json +1 -1
  79. package/vendor/tree-sitter-kotlin/package.json +1 -1
  80. package/vendor/tree-sitter-proto/package.json +1 -1
  81. package/vendor/tree-sitter-swift/package.json +1 -1
  82. package/scripts/materialize-vendor-grammars.cjs +0 -97
@@ -1,19 +1,19 @@
1
1
  import * as path from 'node:path';
2
2
  import * as fs from 'node:fs/promises';
3
- import { createRequire } from 'node:module';
4
3
  import { glob } from 'glob';
5
4
  import Parser from 'tree-sitter';
6
5
  import Cpp from 'tree-sitter-cpp';
7
- // `tree-sitter-c` is vendored prebuild-only (#2116) and may be absent on a
8
- // toolchain-less / `--ignore-scripts` install. Load it via a guarded `_require`
9
- // rather than a top-level `import C from 'tree-sitter-c'`, which would throw
10
- // ERR_MODULE_NOT_FOUND at module-load and crash analyze (#2091/#2093). When the
6
+ import { requireVendoredGrammar } from '../../tree-sitter/vendored-grammars.js';
7
+ // `tree-sitter-c` is vendored (#2116), loaded from `vendor/` by absolute path
8
+ // (NEVER copied into node_modules see vendored-grammars.ts / #2111). Load it
9
+ // via a guarded call rather than a top-level `import C from 'tree-sitter-c'`,
10
+ // which would throw ERR_MODULE_NOT_FOUND at module-load and crash analyze
11
+ // (#2091/#2093). It may be absent on a platform without a prebuild; when the
11
12
  // binding is absent, `getLanguageForFile` returns null for `.c`/`.h` so C
12
13
  // include-extraction is skipped (C++ is unaffected — its binding always ships).
13
- const _require = createRequire(import.meta.url);
14
14
  let C = null;
15
15
  try {
16
- C = _require('tree-sitter-c');
16
+ C = requireVendoredGrammar('tree-sitter-c');
17
17
  }
18
18
  catch {
19
19
  /* C grammar unavailable — C include extraction degrades to a no-op. */
@@ -20,6 +20,8 @@ import type { VariableExtractor } from './variable-types.js';
20
20
  import type { ImportResolverFn } from './import-resolvers/types.js';
21
21
  import type { SyntaxNode } from './utils/ast-helpers.js';
22
22
  import type { NodeLabel } from '../../_shared/index.js';
23
+ import type Parser from 'tree-sitter';
24
+ import type { ExtractedDecoratorRoute } from './workers/parse-worker.js';
23
25
  /** Tree-sitter query captures: capture name → AST node (or undefined if not captured). */
24
26
  export type CaptureMap = Record<string, SyntaxNode | undefined>;
25
27
  /** Configuration for AST-based framework detection patterns. */
@@ -145,6 +147,12 @@ interface LanguageProviderConfig {
145
147
  * `undefined` when no constraints exist / the node isn't a templated
146
148
  * function. Languages without SFINAE / concept semantics leave this
147
149
  * undefined and the disambiguation is a pass-through.
150
+ *
151
+ * Cloneability contract: the returned payload crosses the worker boundary
152
+ * via structured clone, so it MUST be structured-clone-safe (no functions,
153
+ * symbols, or tree-sitter `SyntaxNode`s — only plain data). Wrap the return
154
+ * with `assertCloneable` from `workers/clone-safety.ts` so a future leak is a
155
+ * compile error at the source instead of a runtime DataCloneError (#2143).
148
156
  */
149
157
  readonly extractTemplateConstraints?: (definitionNode: SyntaxNode) => unknown;
150
158
  /** Override the default node label for definition.function captures.
@@ -185,6 +193,17 @@ interface LanguageProviderConfig {
185
193
  * When true, the worker extracts routes via the language's route extraction logic.
186
194
  * Default: undefined (no route files). */
187
195
  readonly isRouteFile?: (filePath: string) => boolean;
196
+ /**
197
+ * Extract decorator-style route annotations from a parsed file.
198
+ *
199
+ * When defined, the parse worker calls this after per-file capture processing
200
+ * to extract framework route definitions that require AST-level analysis beyond
201
+ * generic `@decorator` captures (e.g., Java Spring class-level prefix joining,
202
+ * multi-class handling). The returned routes are appended to `decoratorRoutes`.
203
+ *
204
+ * Default: undefined (no language-specific decorator route extraction).
205
+ */
206
+ readonly extractDecoratorRoutes?: (tree: Parser.Tree, filePath: string, lineOffset: number) => ExtractedDecoratorRoute[];
188
207
  /** Built-in/stdlib names that should be filtered from the call graph for this language.
189
208
  * Default: undefined (no language-specific filtering). */
190
209
  readonly builtInNames?: ReadonlySet<string>;
@@ -255,8 +274,12 @@ interface LanguageProviderConfig {
255
274
  * disk store WITHOUT a main-thread re-parse. The main thread restores them
256
275
  * via the matching `ScopeResolver.applyCaptureSideChannel` hook.
257
276
  *
258
- * MUST return plain data (objects / arrays / primitives) so it round-trips
259
- * through `JSON.stringify` + the parsedfile-store interning reviver.
277
+ * Cloneability contract: MUST return plain data (objects / arrays /
278
+ * primitives no functions, symbols, or tree-sitter `SyntaxNode`s) so it
279
+ * survives BOTH the worker→main structured clone AND `JSON.stringify` + the
280
+ * parsedfile-store interning reviver. Wrap the return with `assertCloneable`
281
+ * from `workers/clone-safety.ts` so a future non-serializable leak is a
282
+ * compile error at the source instead of a runtime DataCloneError (#2143).
260
283
  *
261
284
  * Default: undefined (provider has no capture-time module-level side effects).
262
285
  */
@@ -38,7 +38,8 @@ import { cCallConfig, cppCallConfig } from '../call-extractors/configs/c-cpp.js'
38
38
  import { stripUeMacros } from '../cpp-ue-preprocessor.js';
39
39
  import { emitCScopeCaptures, interpretCImport, interpretCTypeBinding, cArityCompatibility, cBindingScopeFor, cImportOwningScope, cReceiverBinding, collectCStaticLinkageSideChannel, } from './c/index.js';
40
40
  import { emitCppScopeCaptures, interpretCppImport, interpretCppTypeBinding, cppArityCompatibility, cppBindingScopeFor, cppImportOwningScope, cppReceiverBinding, collectCppCaptureSideChannel, } from './cpp/index.js';
41
- import { extractCppTemplateConstraints } from './cpp/constraint-extractor.js';
41
+ import { extractCppTemplateConstraints, } from './cpp/constraint-extractor.js';
42
+ import { assertCloneable } from '../workers/clone-safety.js';
42
43
  const C_BUILT_INS = new Set([
43
44
  'printf',
44
45
  'fprintf',
@@ -358,7 +359,10 @@ export const cProvider = defineLanguage({
358
359
  // `static` functions look non-file-local on the main thread and leak into
359
360
  // cross-file global free-call resolution / wildcard imports. See
360
361
  // `c/capture-side-channel.ts`.
361
- collectCaptureSideChannel: collectCStaticLinkageSideChannel,
362
+ // `assertCloneable` is a runtime identity; it makes a future non-serializable
363
+ // value in the side-channel payload a compile error here, at the source, rather
364
+ // than a DataCloneError at the worker boundary (#2143).
365
+ collectCaptureSideChannel: (filePath) => assertCloneable(collectCStaticLinkageSideChannel(filePath)),
362
366
  interpretImport: interpretCImport,
363
367
  interpretTypeBinding: interpretCTypeBinding,
364
368
  bindingScopeFor: cBindingScopeFor,
@@ -431,7 +435,7 @@ export const cppProvider = defineLanguage({
431
435
  // just populated for this file into plain data on `ParsedFile.captureSideChannel`,
432
436
  // so the main thread can restore them via `applyCaptureSideChannel` WITHOUT a
433
437
  // re-parse (#1983). See `cpp/capture-side-channel.ts`.
434
- collectCaptureSideChannel: collectCppCaptureSideChannel,
438
+ collectCaptureSideChannel: (filePath) => assertCloneable(collectCppCaptureSideChannel(filePath)),
435
439
  interpretImport: interpretCppImport,
436
440
  interpretTypeBinding: interpretCppTypeBinding,
437
441
  bindingScopeFor: cppBindingScopeFor,
@@ -482,5 +486,8 @@ function extractCppTemplateConstraintsForProvider(definitionNode) {
482
486
  }
483
487
  break;
484
488
  }
485
- return extractCppTemplateConstraints(templateDecl, declarator);
489
+ // Guard the boundary at the source: a future non-cloneable member of the
490
+ // constraint payload becomes a compile error here, not a runtime
491
+ // DataCloneError at the worker post (#2143).
492
+ return assertCloneable(extractCppTemplateConstraints(templateDecl, declarator));
486
493
  }
@@ -11,6 +11,7 @@ import { createClassExtractor } from '../class-extractors/generic.js';
11
11
  import { javaClassConfig } from '../class-extractors/configs/jvm.js';
12
12
  import { defineLanguage } from '../language-provider.js';
13
13
  import { javaTypeConfig } from '../type-extractors/jvm.js';
14
+ import { extractSpringRoutes } from '../route-extractors/spring.js';
14
15
  import { javaExportChecker } from '../export-detection.js';
15
16
  import { createImportResolver } from '../import-resolvers/resolver-factory.js';
16
17
  import { javaImportConfig } from '../import-resolvers/configs/jvm.js';
@@ -103,4 +104,6 @@ export const javaProvider = defineLanguage({
103
104
  arityCompatibility: javaArityCompatibility,
104
105
  resolveImportTarget: resolveJavaImportTarget,
105
106
  orderSameNameTypeCandidates: orderJavaSameNameTypeCandidates,
107
+ // ── Route extraction ──
108
+ extractDecoratorRoutes: extractSpringRoutes,
106
109
  });
@@ -1,7 +1,8 @@
1
1
  import Parser from 'tree-sitter';
2
2
  import { SupportedLanguages } from '../../../../_shared/index.js';
3
- // `tree-sitter-kotlin` is an optionalDependency that may be absent on a default
4
- // install (or fail its native build). Loaded lazily + guarded via parser-loader
3
+ // `tree-sitter-kotlin` is a vendored grammar (loaded from vendor/ by absolute
4
+ // path, never node_modules vendored-grammars.ts / #2111) that may be absent on
5
+ // a platform without a matching prebuild. Loaded lazily + guarded via parser-loader
5
6
  // rather than statically imported: this module is pulled onto the main thread
6
7
  // eagerly by the scope-resolution registry and the language-provider index, so
7
8
  // a top-level `import Kotlin from 'tree-sitter-kotlin'` would throw
@@ -10,6 +10,7 @@ import { SupportedLanguages } from '../../../_shared/index.js';
10
10
  import { createClassExtractor } from '../class-extractors/generic.js';
11
11
  import { kotlinClassConfig } from '../class-extractors/configs/jvm.js';
12
12
  import { defineLanguage } from '../language-provider.js';
13
+ import { assertCloneable } from '../workers/clone-safety.js';
13
14
  import { kotlinTypeConfig } from '../type-extractors/jvm.js';
14
15
  import { kotlinExportChecker } from '../export-detection.js';
15
16
  import { createImportResolver } from '../import-resolvers/resolver-factory.js';
@@ -166,7 +167,10 @@ export const kotlinProvider = defineLanguage({
166
167
  // so the main thread can restore them via `applyCaptureSideChannel` WITHOUT a
167
168
  // re-parse (#1983). Without this, companion/static dispatch emits no CALLS
168
169
  // edges on the worker path. See `kotlin/capture-side-channel.ts`.
169
- collectCaptureSideChannel: collectKotlinCaptureSideChannel,
170
+ // `assertCloneable` is a runtime identity; it makes a future non-serializable
171
+ // value in the side-channel payload a compile error here, at the source, rather
172
+ // than a DataCloneError at the worker boundary (#2143).
173
+ collectCaptureSideChannel: (filePath) => assertCloneable(collectKotlinCaptureSideChannel(filePath)),
170
174
  interpretImport: interpretKotlinImport,
171
175
  interpretTypeBinding: interpretKotlinTypeBinding,
172
176
  bindingScopeFor: kotlinBindingScopeFor,
@@ -144,6 +144,27 @@ chunkHash) => {
144
144
  .join(', ');
145
145
  logger.warn(` Skipped unsupported languages: ${summary}`);
146
146
  }
147
+ // Clone-safety telemetry (#2112): files whose parse output carried a value
148
+ // the structured-clone algorithm couldn't serialize across the worker
149
+ // boundary. The worker sanitized/dropped the offending value so the run
150
+ // could complete; surface the (rare) data loss so it's visible and the
151
+ // offending extractor can be fixed at source.
152
+ const skippedPaths = [];
153
+ for (const result of chunkResults) {
154
+ for (const entry of result.skippedPaths ?? [])
155
+ skippedPaths.push(entry);
156
+ }
157
+ if (skippedPaths.length > 0) {
158
+ // Keep the per-file reason ("stripped N value(s) from nodes" /
159
+ // "dropped non-serializable parsedFiles entry") — it distinguishes a
160
+ // recoverable strip from a whole-record drop, which a path-only line loses.
161
+ const shown = skippedPaths
162
+ .slice(0, 10)
163
+ .map((e) => `${e.path} (${e.reason})`)
164
+ .join(', ');
165
+ const more = skippedPaths.length > 10 ? ` …and ${skippedPaths.length - 10} more` : '';
166
+ logger.warn(` Sanitized ${skippedPaths.length} file(s) with non-serializable parse output: ${shown}${more}`);
167
+ }
147
168
  onFileProgress?.(total, total, 'done');
148
169
  return chunkResults;
149
170
  };
@@ -297,22 +297,72 @@ export const routesPhase = {
297
297
  // scan JS/TS consumer files for calls to those wrapper functions with
298
298
  // URL-like string arguments and add them to allFetchCalls so
299
299
  // processNextjsFetchRoutes can create FETCHES edges.
300
- if (allFetchWrapperDefs && allFetchWrapperDefs.length > 0 && routeRegistry.size > 0) {
301
- const wrapperNames = new Set(allFetchWrapperDefs.map((d) => d.functionName));
300
+ // Wrapper names come from two sources: functions the parse phase
301
+ // auto-detected as calling the bare global `fetch()`, plus any names the
302
+ // user declared in `.gitnexusrc` `fetchWrappers` (#1589/#1852 residual).
303
+ // Config names let an axios/custom-client wrapper — or one named outside the
304
+ // built-in convention — still produce route_map consumers; without them it
305
+ // silently falls back to `consumers: []`. Configured names alone are enough
306
+ // to run the scan even when nothing was auto-detected.
307
+ // Configured names are already validated/trimmed/de-duped/capped by
308
+ // analyze-config.ts — trusted as-is (#1589/#1852 review F9, dropped the
309
+ // redundant re-trim/re-filter). The single filter below guards only the
310
+ // auto-detected `functionName`s, which have no shape guarantee.
311
+ const configuredWrappers = ctx.options?.fetchWrappers ?? [];
312
+ const wrapperNames = new Set([...(allFetchWrapperDefs ?? []).map((d) => d.functionName), ...configuredWrappers].filter((n) => typeof n === 'string' && n.trim().length > 0));
313
+ if (wrapperNames.size > 0 && routeRegistry.size > 0) {
302
314
  const jsFiles = allPaths.filter((p) => /\.[jt]sx?$/.test(p));
303
- if (jsFiles.length > 0 && wrapperNames.size > 0) {
304
- const jsContents = await readFileContents(ctx.repoPath, jsFiles);
305
- for (const [filePath, content] of jsContents) {
306
- for (const name of wrapperNames) {
307
- const regex = new RegExp(`\\b${escapeRegex(name)}\\s*\\(\\s*['"\`](/[^'"\`\\s)]+)['"\`]`, 'g');
308
- let match;
309
- while ((match = regex.exec(content)) !== null) {
310
- allFetchCalls.push({
311
- filePath,
312
- fetchURL: match[1],
313
- lineNumber: content.substring(0, match.index).split('\n').length,
314
- });
315
+ if (jsFiles.length > 0) {
316
+ // Reuse contents already read for handler extraction; only read the
317
+ // remainder (mirrors the Expo block above). Avoids a second full read of
318
+ // files we already have in memory.
319
+ const unreadJsFiles = jsFiles.filter((p) => !handlerContents?.has(p));
320
+ const extraContents = unreadJsFiles.length > 0
321
+ ? await readFileContents(ctx.repoPath, unreadJsFiles)
322
+ : new Map();
323
+ // One alternation regex over every wrapper name per file — O(files), not
324
+ // O(files × wrappers) (#1852 review F3). Names are escaped and grouped
325
+ // non-capturing so capture group 1 stays the URL. The left boundary is a
326
+ // negative lookbehind, not `\b`: a bare configured name like `get` must
327
+ // match the free call `get('/x')` but NOT a member access `client.get(`
328
+ // (a `.get(` on an unrelated object), and `apiFetch` must not match
329
+ // `myApiFetch`. Member-style wrappers are configured with the dot
330
+ // (`client.get`), where the `.` is part of the pattern. The `u` flag +
331
+ // Unicode property classes make the boundary cover non-ASCII identifier
332
+ // characters too — ASCII `\w` would let `caféget('/x')` match `get`
333
+ // (#1852 review F10).
334
+ const alternation = [...wrapperNames].map(escapeRegex).join('|');
335
+ const wrapperCallRegex = new RegExp(`(?<![.\\p{L}\\p{N}_$])(?:${alternation})\\s*\\(\\s*['"\`](/[^'"\`\\s)]+)['"\`]`, 'gu');
336
+ const scanContent = (filePath, content) => {
337
+ wrapperCallRegex.lastIndex = 0;
338
+ // 1-based line number via a running newline counter: matches arrive in
339
+ // ascending index, so accumulate newlines incrementally instead of
340
+ // re-allocating `content.substring(0, match.index).split('\n')` on
341
+ // every match (#1852 review F12). Output is identical.
342
+ let line = 1;
343
+ let scanned = 0;
344
+ let match;
345
+ while ((match = wrapperCallRegex.exec(content)) !== null) {
346
+ for (; scanned < match.index; scanned++) {
347
+ if (content.charCodeAt(scanned) === 10 /* '\n' */)
348
+ line++;
315
349
  }
350
+ allFetchCalls.push({
351
+ filePath,
352
+ fetchURL: match[1],
353
+ lineNumber: line,
354
+ });
355
+ }
356
+ };
357
+ for (const [filePath, content] of extraContents)
358
+ scanContent(filePath, content);
359
+ // Also scan already-read JS/TS handler files (a handler can itself
360
+ // consume another route through a wrapper).
361
+ if (handlerContents) {
362
+ for (const p of jsFiles) {
363
+ const cached = handlerContents.get(p);
364
+ if (cached !== undefined)
365
+ scanContent(p, cached);
316
366
  }
317
367
  }
318
368
  }
@@ -104,6 +104,15 @@ export interface PipelineOptions {
104
104
  * `process.env` state across invocations. When undefined, the env var decides.
105
105
  */
106
106
  keepLocalValueSymbols?: boolean;
107
+ /**
108
+ * Extra fetch-wrapper function names to treat as HTTP consumers, threaded
109
+ * from `.gitnexusrc` `fetchWrappers` via `AnalyzeOptions` (#1589/#1852
110
+ * residual). The routes phase unions these with the auto-detected `fetch()`
111
+ * wrappers when scanning for `route_map` consumers, so a wrapper named outside
112
+ * the built-in convention (or built on axios / a custom client) is still
113
+ * traced. Empty/undefined leaves behavior unchanged.
114
+ */
115
+ fetchWrappers?: readonly string[];
107
116
  }
108
117
  /**
109
118
  * All pipeline phases with their dependency relationships.
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Shared Spring route-annotation primitives.
3
+ *
4
+ * These are the low-level building blocks the two Spring route extractors —
5
+ * the ingestion-layer `route-extractors/spring.ts` (produces graph `Route`
6
+ * nodes) and the group-layer `group/extractors/http-patterns/java.ts`
7
+ * (produces cross-repo HTTP contracts) — would otherwise each maintain
8
+ * independently. Centralising the annotation→method map, the enclosing-class
9
+ * lookup, and the route-key filter keeps those semantics in one place so the
10
+ * two extractors can't drift apart.
11
+ *
12
+ * This module lives in `ingestion/` (the lower layer); the group layer imports
13
+ * from it, matching the existing `group → ingestion` dependency direction
14
+ * (e.g. `group/extractors/include-extractor.ts` already imports
15
+ * `ingestion/import-resolvers/utils.ts`). It MUST NOT import anything from
16
+ * `group/` to avoid a dependency cycle.
17
+ */
18
+ import type Parser from 'tree-sitter';
19
+ /**
20
+ * Spring shortcut method-annotation → HTTP verb.
21
+ *
22
+ * `@RequestMapping` is intentionally absent: on a method it carries no implicit
23
+ * verb (the verb lives in its `method = RequestMethod.X` attribute), and on a
24
+ * class it is a URL prefix rather than a route. Callers handle `@RequestMapping`
25
+ * separately.
26
+ */
27
+ export declare const METHOD_ANNOTATION_TO_HTTP: Record<string, string>;
28
+ /**
29
+ * A named annotation argument contributes a route only when its member key is
30
+ * `path` or `value`; a positional argument (no key node) always qualifies.
31
+ * Drops Spring's non-route string attributes (`produces`, `consumes`,
32
+ * `headers`, `name`, `params`) that would otherwise be mis-read as routes.
33
+ */
34
+ export declare function isRouteMemberKey(keyNode: Parser.SyntaxNode | undefined): boolean;
35
+ /**
36
+ * Find the nearest enclosing `class_declaration` ancestor for a node, or null
37
+ * if the node is top-level. Tree-sitter's `SyntaxNode.parent` walks one level
38
+ * at a time.
39
+ */
40
+ export declare function findEnclosingClass(node: Parser.SyntaxNode): Parser.SyntaxNode | null;
41
+ /**
42
+ * Strip enclosing quotes from a tree-sitter string-literal node's text.
43
+ * Handles single / double / template (backtick) quotes and triple-quoted
44
+ * strings. Mirrors the safer semantics of the group layer's `unquoteLiteral`:
45
+ * returns `null` for empty / nullish input so callers can uniformly skip
46
+ * captures whose value is missing, and returns the text unchanged when it
47
+ * carries no recognisable surrounding quotes (some grammars expose string
48
+ * content without quotes already).
49
+ */
50
+ export declare function unquoteSpringLiteral(raw: string): string | null;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Shared Spring route-annotation primitives.
3
+ *
4
+ * These are the low-level building blocks the two Spring route extractors —
5
+ * the ingestion-layer `route-extractors/spring.ts` (produces graph `Route`
6
+ * nodes) and the group-layer `group/extractors/http-patterns/java.ts`
7
+ * (produces cross-repo HTTP contracts) — would otherwise each maintain
8
+ * independently. Centralising the annotation→method map, the enclosing-class
9
+ * lookup, and the route-key filter keeps those semantics in one place so the
10
+ * two extractors can't drift apart.
11
+ *
12
+ * This module lives in `ingestion/` (the lower layer); the group layer imports
13
+ * from it, matching the existing `group → ingestion` dependency direction
14
+ * (e.g. `group/extractors/include-extractor.ts` already imports
15
+ * `ingestion/import-resolvers/utils.ts`). It MUST NOT import anything from
16
+ * `group/` to avoid a dependency cycle.
17
+ */
18
+ /**
19
+ * Spring shortcut method-annotation → HTTP verb.
20
+ *
21
+ * `@RequestMapping` is intentionally absent: on a method it carries no implicit
22
+ * verb (the verb lives in its `method = RequestMethod.X` attribute), and on a
23
+ * class it is a URL prefix rather than a route. Callers handle `@RequestMapping`
24
+ * separately.
25
+ */
26
+ export const METHOD_ANNOTATION_TO_HTTP = {
27
+ GetMapping: 'GET',
28
+ PostMapping: 'POST',
29
+ PutMapping: 'PUT',
30
+ DeleteMapping: 'DELETE',
31
+ PatchMapping: 'PATCH',
32
+ };
33
+ /**
34
+ * A named annotation argument contributes a route only when its member key is
35
+ * `path` or `value`; a positional argument (no key node) always qualifies.
36
+ * Drops Spring's non-route string attributes (`produces`, `consumes`,
37
+ * `headers`, `name`, `params`) that would otherwise be mis-read as routes.
38
+ */
39
+ export function isRouteMemberKey(keyNode) {
40
+ if (!keyNode)
41
+ return true;
42
+ return keyNode.text === 'path' || keyNode.text === 'value';
43
+ }
44
+ /**
45
+ * Find the nearest enclosing `class_declaration` ancestor for a node, or null
46
+ * if the node is top-level. Tree-sitter's `SyntaxNode.parent` walks one level
47
+ * at a time.
48
+ */
49
+ export function findEnclosingClass(node) {
50
+ let cur = node.parent;
51
+ while (cur) {
52
+ if (cur.type === 'class_declaration')
53
+ return cur;
54
+ cur = cur.parent;
55
+ }
56
+ return null;
57
+ }
58
+ /**
59
+ * Strip enclosing quotes from a tree-sitter string-literal node's text.
60
+ * Handles single / double / template (backtick) quotes and triple-quoted
61
+ * strings. Mirrors the safer semantics of the group layer's `unquoteLiteral`:
62
+ * returns `null` for empty / nullish input so callers can uniformly skip
63
+ * captures whose value is missing, and returns the text unchanged when it
64
+ * carries no recognisable surrounding quotes (some grammars expose string
65
+ * content without quotes already).
66
+ */
67
+ export function unquoteSpringLiteral(raw) {
68
+ if (!raw)
69
+ return null;
70
+ if ((raw.startsWith('"""') && raw.endsWith('"""')) ||
71
+ (raw.startsWith("'''") && raw.endsWith("'''"))) {
72
+ return raw.slice(3, -3);
73
+ }
74
+ const first = raw[0];
75
+ const last = raw[raw.length - 1];
76
+ if ((first === '"' || first === "'" || first === '`') && last === first && raw.length >= 2) {
77
+ return raw.slice(1, -1);
78
+ }
79
+ return raw;
80
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Spring route annotation extractor for the ingestion pipeline.
3
+ *
4
+ * Extracts `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`,
5
+ * `@PatchMapping`, and `@RequestMapping` annotations from Java source files
6
+ * and returns `ExtractedDecoratorRoute[]` with class-level `@RequestMapping`
7
+ * prefixes already resolved per-class.
8
+ *
9
+ * This module is the ingestion-layer counterpart of
10
+ * `group/extractors/http-patterns/java.ts` (which extracts HTTP contracts
11
+ * for cross-repo matching). It uses the same tree-sitter capture approach:
12
+ * a single predicate-free query matches all route annotations generically,
13
+ * then a for-loop discriminates class-level prefixes from method-level routes
14
+ * by reading `@node.type` and the annotation name.
15
+ *
16
+ * The query is predicate-free to avoid the tree-sitter 0.21.x hazard where
17
+ * `#match?` / `#eq?` predicates in a top-level `[...]` alternation silently
18
+ * drop sibling-branch matches (see group-layer `JAVA_ROUTE_ANNOTATION_PATTERNS`
19
+ * header comment for details).
20
+ */
21
+ import Parser from 'tree-sitter';
22
+ import type { ExtractedDecoratorRoute } from '../workers/parse-worker.js';
23
+ /**
24
+ * Extract Spring route annotations from a parsed Java file.
25
+ *
26
+ * Uses a single tree-sitter query pass to capture all annotations, then
27
+ * discriminates class-level prefixes from method-level routes in a loop.
28
+ * Handles multiple classes per file, each with its own prefix.
29
+ *
30
+ * @param tree - tree-sitter parse tree
31
+ * @param filePath - relative file path (for `ExtractedDecoratorRoute.filePath`)
32
+ * @param lineOffset - line offset for pre-processing (usually 0)
33
+ * @returns Decorator routes with prefix already set per-class
34
+ */
35
+ export declare function extractSpringRoutes(tree: Parser.Tree, filePath: string, lineOffset?: number): ExtractedDecoratorRoute[];
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Spring route annotation extractor for the ingestion pipeline.
3
+ *
4
+ * Extracts `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`,
5
+ * `@PatchMapping`, and `@RequestMapping` annotations from Java source files
6
+ * and returns `ExtractedDecoratorRoute[]` with class-level `@RequestMapping`
7
+ * prefixes already resolved per-class.
8
+ *
9
+ * This module is the ingestion-layer counterpart of
10
+ * `group/extractors/http-patterns/java.ts` (which extracts HTTP contracts
11
+ * for cross-repo matching). It uses the same tree-sitter capture approach:
12
+ * a single predicate-free query matches all route annotations generically,
13
+ * then a for-loop discriminates class-level prefixes from method-level routes
14
+ * by reading `@node.type` and the annotation name.
15
+ *
16
+ * The query is predicate-free to avoid the tree-sitter 0.21.x hazard where
17
+ * `#match?` / `#eq?` predicates in a top-level `[...]` alternation silently
18
+ * drop sibling-branch matches (see group-layer `JAVA_ROUTE_ANNOTATION_PATTERNS`
19
+ * header comment for details).
20
+ */
21
+ import Parser from 'tree-sitter';
22
+ import Java from 'tree-sitter-java';
23
+ import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingClass, unquoteSpringLiteral, } from './spring-shared.js';
24
+ /**
25
+ * Single predicate-free tree-sitter query that captures all route annotations
26
+ * on classes and methods. Discrimination by annotation name and node type
27
+ * happens in the loop below.
28
+ *
29
+ * Captures:
30
+ * @ann → annotation name identifier (RequestMapping, GetMapping, etc.)
31
+ * @node → enclosing declaration (class_declaration | method_declaration)
32
+ * @value → the string-literal argument
33
+ * @key → the named-argument member key (absent for positional form)
34
+ */
35
+ const ROUTE_ANNOTATION_QUERY = new Parser.Query(Java, `
36
+ [
37
+ (class_declaration
38
+ (modifiers
39
+ (annotation
40
+ name: (identifier) @ann
41
+ arguments: (annotation_argument_list (string_literal) @value)))) @node
42
+ (class_declaration
43
+ (modifiers
44
+ (annotation
45
+ name: (identifier) @ann
46
+ arguments: (annotation_argument_list
47
+ (element_value_pair
48
+ key: (identifier) @key
49
+ value: (string_literal) @value))))) @node
50
+ (method_declaration
51
+ (modifiers
52
+ (annotation
53
+ name: (identifier) @ann
54
+ arguments: (annotation_argument_list (string_literal) @value)))) @node
55
+ (method_declaration
56
+ (modifiers
57
+ (annotation
58
+ name: (identifier) @ann
59
+ arguments: (annotation_argument_list
60
+ (element_value_pair
61
+ key: (identifier) @key
62
+ value: (string_literal) @value))))) @node
63
+ ]
64
+ `);
65
+ /**
66
+ * Extract Spring route annotations from a parsed Java file.
67
+ *
68
+ * Uses a single tree-sitter query pass to capture all annotations, then
69
+ * discriminates class-level prefixes from method-level routes in a loop.
70
+ * Handles multiple classes per file, each with its own prefix.
71
+ *
72
+ * @param tree - tree-sitter parse tree
73
+ * @param filePath - relative file path (for `ExtractedDecoratorRoute.filePath`)
74
+ * @param lineOffset - line offset for pre-processing (usually 0)
75
+ * @returns Decorator routes with prefix already set per-class
76
+ */
77
+ export function extractSpringRoutes(tree, filePath, lineOffset = 0) {
78
+ const matches = ROUTE_ANNOTATION_QUERY.matches(tree.rootNode);
79
+ // Phase 1: collect class-level @RequestMapping prefixes keyed by node id
80
+ const prefixByClassId = new Map();
81
+ for (const match of matches) {
82
+ const caps = {};
83
+ for (const { name, node } of match.captures) {
84
+ caps[name] = node;
85
+ }
86
+ const annNode = caps['ann'];
87
+ const node = caps['node'];
88
+ const valueNode = caps['value'];
89
+ const keyNode = caps['key'];
90
+ if (!annNode || !node || !valueNode)
91
+ continue;
92
+ if (node.type === 'class_declaration' && annNode.text === 'RequestMapping') {
93
+ if (!isRouteMemberKey(keyNode))
94
+ continue;
95
+ const prefix = unquoteSpringLiteral(valueNode.text);
96
+ if (prefix !== null)
97
+ prefixByClassId.set(node.id, prefix);
98
+ }
99
+ }
100
+ // Phase 2: collect method-level routes and resolve their class prefix
101
+ const routes = [];
102
+ for (const match of matches) {
103
+ const caps = {};
104
+ for (const { name, node } of match.captures) {
105
+ caps[name] = node;
106
+ }
107
+ const annNode = caps['ann'];
108
+ const node = caps['node'];
109
+ const valueNode = caps['value'];
110
+ const keyNode = caps['key'];
111
+ if (!annNode || !node || !valueNode)
112
+ continue;
113
+ if (node.type !== 'method_declaration')
114
+ continue;
115
+ const ann = annNode.text;
116
+ const httpMethod = METHOD_ANNOTATION_TO_HTTP[ann];
117
+ if (!httpMethod)
118
+ continue; // skip @RequestMapping on methods (ambiguous verb)
119
+ if (!isRouteMemberKey(keyNode))
120
+ continue;
121
+ const routePath = unquoteSpringLiteral(valueNode.text);
122
+ if (routePath === null)
123
+ continue;
124
+ const enclosingClass = findEnclosingClass(node);
125
+ const classPrefix = enclosingClass ? (prefixByClassId.get(enclosingClass.id) ?? '') : '';
126
+ routes.push({
127
+ filePath,
128
+ routePath,
129
+ httpMethod,
130
+ decoratorName: ann,
131
+ lineNumber: annNode.startPosition.row + lineOffset,
132
+ ...(classPrefix ? { prefix: classPrefix } : {}),
133
+ });
134
+ }
135
+ return routes;
136
+ }