gitnexus 1.6.8-rc.3 → 1.6.8-rc.5
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/README.md +20 -0
- package/dist/core/group/extractors/http-patterns/java.js +3 -50
- package/dist/core/ingestion/language-provider.d.ts +13 -0
- package/dist/core/ingestion/languages/java.js +3 -0
- package/dist/core/ingestion/route-extractors/spring-shared.d.ts +50 -0
- package/dist/core/ingestion/route-extractors/spring-shared.js +80 -0
- package/dist/core/ingestion/route-extractors/spring.d.ts +35 -0
- package/dist/core/ingestion/route-extractors/spring.js +136 -0
- package/dist/core/ingestion/workers/parse-worker.js +10 -0
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +24 -4
- package/hooks/claude/gitnexus-hook.cjs +18 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -436,6 +436,26 @@ After scope resolution, analyze prunes inert block-local value symbols (a functi
|
|
|
436
436
|
|
|
437
437
|
Programmatic callers can pass `keepLocalValueSymbols: true` in `PipelineOptions` instead of setting the env var.
|
|
438
438
|
|
|
439
|
+
### Hook augmentation/notifications are silently skipped
|
|
440
|
+
|
|
441
|
+
The Claude Code / Antigravity hooks intentionally stay **silent** on normal skip
|
|
442
|
+
paths so strict hook runners (e.g. Codex `PreToolUse`) never see unexpected
|
|
443
|
+
output. A search may not be augmented — or a stale-index reminder may not appear
|
|
444
|
+
on stderr — when the GitNexus MCP server owns the repo DB, when the DB-lock probe
|
|
445
|
+
times out and fails closed, or when the index is already current.
|
|
446
|
+
|
|
447
|
+
To see why a hook skipped, set `GITNEXUS_DEBUG=1` and re-run the action — the hook
|
|
448
|
+
writes the reason (e.g. `[GitNexus] augment skipped: MCP server owns DB`) and the
|
|
449
|
+
stale-index hint to its stderr:
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
GITNEXUS_DEBUG=1 <your command> # surfaces hook skip/diagnostic reasons on stderr
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Only `GITNEXUS_DEBUG=1` and `GITNEXUS_DEBUG=true` enable diagnostics; every other
|
|
456
|
+
value (including `0` and `false`) is treated as off. Diagnostics go to stderr
|
|
457
|
+
only — the hook's structured stdout (the JSON the agent consumes) is unaffected.
|
|
458
|
+
|
|
439
459
|
## Privacy
|
|
440
460
|
|
|
441
461
|
- All processing happens locally on your machine
|
|
@@ -1,31 +1,6 @@
|
|
|
1
1
|
import Java from 'tree-sitter-java';
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
|
-
|
|
4
|
-
* Java HTTP plugin. Handles:
|
|
5
|
-
* - Spring `@RequestMapping` class prefixes + `@(Get|Post|...)Mapping` method annotations
|
|
6
|
-
* - Spring `RestTemplate.getForObject/...`, `exchange(...)`
|
|
7
|
-
* - Spring `WebClient.method(HttpMethod.X, ...)`, `WebClient.get().uri(...)`
|
|
8
|
-
* - OkHttp `new Request.Builder().url("...")`
|
|
9
|
-
* - OpenFeign interfaces with Spring MVC method annotations or
|
|
10
|
-
* native `@RequestLine("METHOD /path")` annotations
|
|
11
|
-
* - Java / Apache HttpClient literal request construction
|
|
12
|
-
*
|
|
13
|
-
* Every route-defining annotation (class/interface `@RequestMapping`
|
|
14
|
-
* prefixes, `@FeignClient(path)` prefixes, `@(Get|...)Mapping` method
|
|
15
|
-
* routes and native `@RequestLine`s) is matched by a single consolidated
|
|
16
|
-
* query (`JAVA_ROUTE_ANNOTATION_PATTERNS`) in one pass via
|
|
17
|
-
* `scanRouteAnnotations`. The `scan` function then walks up from each
|
|
18
|
-
* matched method to its enclosing class/interface to combine the prefix
|
|
19
|
-
* with the method path. Call-site consumers (RestTemplate, WebClient,
|
|
20
|
-
* OkHttp, Java/Apache HttpClient) keep their own focused queries.
|
|
21
|
-
*/
|
|
22
|
-
const METHOD_ANNOTATION_TO_HTTP = {
|
|
23
|
-
GetMapping: 'GET',
|
|
24
|
-
PostMapping: 'POST',
|
|
25
|
-
PutMapping: 'PUT',
|
|
26
|
-
DeleteMapping: 'DELETE',
|
|
27
|
-
PatchMapping: 'PATCH',
|
|
28
|
-
};
|
|
3
|
+
import { METHOD_ANNOTATION_TO_HTTP, isRouteMemberKey, findEnclosingClass, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
29
4
|
// ─── Route-defining annotations (one generic query, one pass) ─────────
|
|
30
5
|
// Every Java route-mapper annotation shares one shape: an annotation carrying a
|
|
31
6
|
// single string argument — positional `"..."` or named `key = "..."` — on a
|
|
@@ -300,19 +275,9 @@ const APACHE_HTTP_CLIENT_PATTERNS = compilePatterns({
|
|
|
300
275
|
],
|
|
301
276
|
});
|
|
302
277
|
/**
|
|
303
|
-
* Find the nearest enclosing
|
|
304
|
-
*
|
|
305
|
-
* SyntaxNode.parent walks one level at a time.
|
|
278
|
+
* Find the nearest enclosing interface declaration ancestor for a node, or
|
|
279
|
+
* null if the node is top-level.
|
|
306
280
|
*/
|
|
307
|
-
function findEnclosingClass(node) {
|
|
308
|
-
let cur = node.parent;
|
|
309
|
-
while (cur) {
|
|
310
|
-
if (cur.type === 'class_declaration')
|
|
311
|
-
return cur;
|
|
312
|
-
cur = cur.parent;
|
|
313
|
-
}
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
281
|
function findEnclosingInterface(node) {
|
|
317
282
|
let cur = node.parent;
|
|
318
283
|
while (cur) {
|
|
@@ -369,18 +334,6 @@ function hasAnnotation(node, names) {
|
|
|
369
334
|
}
|
|
370
335
|
return false;
|
|
371
336
|
}
|
|
372
|
-
/**
|
|
373
|
-
* A named annotation argument contributes a route only when its member key is
|
|
374
|
-
* `path` or `value`; a positional argument (no key node) always qualifies.
|
|
375
|
-
* This is the JS-side replacement for the in-query `^(path|value)$` filter and
|
|
376
|
-
* drops Spring's non-route string attributes (`produces`, `consumes`,
|
|
377
|
-
* `headers`, `name`, `params`) that would otherwise be mis-read as routes.
|
|
378
|
-
*/
|
|
379
|
-
function isRouteMemberKey(keyNode) {
|
|
380
|
-
if (!keyNode)
|
|
381
|
-
return true;
|
|
382
|
-
return keyNode.text === 'path' || keyNode.text === 'value';
|
|
383
|
-
}
|
|
384
337
|
/**
|
|
385
338
|
* Resolve every Java route-defining annotation in a single tree-sitter pass.
|
|
386
339
|
*
|
|
@@ -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. */
|
|
@@ -185,6 +187,17 @@ interface LanguageProviderConfig {
|
|
|
185
187
|
* When true, the worker extracts routes via the language's route extraction logic.
|
|
186
188
|
* Default: undefined (no route files). */
|
|
187
189
|
readonly isRouteFile?: (filePath: string) => boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Extract decorator-style route annotations from a parsed file.
|
|
192
|
+
*
|
|
193
|
+
* When defined, the parse worker calls this after per-file capture processing
|
|
194
|
+
* to extract framework route definitions that require AST-level analysis beyond
|
|
195
|
+
* generic `@decorator` captures (e.g., Java Spring class-level prefix joining,
|
|
196
|
+
* multi-class handling). The returned routes are appended to `decoratorRoutes`.
|
|
197
|
+
*
|
|
198
|
+
* Default: undefined (no language-specific decorator route extraction).
|
|
199
|
+
*/
|
|
200
|
+
readonly extractDecoratorRoutes?: (tree: Parser.Tree, filePath: string, lineOffset: number) => ExtractedDecoratorRoute[];
|
|
188
201
|
/** Built-in/stdlib names that should be filtered from the call graph for this language.
|
|
189
202
|
* Default: undefined (no language-specific filtering). */
|
|
190
203
|
readonly builtInNames?: ReadonlySet<string>;
|
|
@@ -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
|
});
|
|
@@ -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
|
+
}
|
|
@@ -678,6 +678,7 @@ const ROUTE_DECORATOR_NAMES = new Set([
|
|
|
678
678
|
'PostMapping',
|
|
679
679
|
'PutMapping',
|
|
680
680
|
'DeleteMapping',
|
|
681
|
+
'PatchMapping',
|
|
681
682
|
]);
|
|
682
683
|
// ============================================================================
|
|
683
684
|
// ORM Query Detection (Prisma + Supabase)
|
|
@@ -1743,6 +1744,15 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
1743
1744
|
if (language === SupportedLanguages.Python) {
|
|
1744
1745
|
extractFastAPIRouterBindings(file.path, parseContent, result.routerIncludes, result.routerImports, (result.routerModuleAliases ??= []));
|
|
1745
1746
|
}
|
|
1747
|
+
// Language-specific decorator route extraction via provider hook.
|
|
1748
|
+
// The provider's extractDecoratorRoutes walks the AST for framework-specific
|
|
1749
|
+
// route patterns (e.g., Java Spring class-level prefix joining). Routes are
|
|
1750
|
+
// appended to decoratorRoutes for the routes phase to emit as Route nodes.
|
|
1751
|
+
if (provider.extractDecoratorRoutes) {
|
|
1752
|
+
const frameworkRoutes = provider.extractDecoratorRoutes(tree, file.path, lineOffset);
|
|
1753
|
+
for (const r of frameworkRoutes)
|
|
1754
|
+
result.decoratorRoutes.push(r);
|
|
1755
|
+
}
|
|
1746
1756
|
// Vue: emit CALLS edges for components used in <template>
|
|
1747
1757
|
if (language === SupportedLanguages.Vue) {
|
|
1748
1758
|
const templateComponents = extractTemplateComponents(file.content);
|
|
@@ -91,10 +91,20 @@ function hasGitNexusServerOwner(gitNexusDir) {
|
|
|
91
91
|
return hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Whether opt-in diagnostics should be written to the hook's stderr. Strict
|
|
96
|
+
* hook runners validate hook output, so normal, non-error skip paths must stay
|
|
97
|
+
* silent unless the operator explicitly asks for diagnostics via GITNEXUS_DEBUG.
|
|
98
|
+
* See issue #1913.
|
|
99
|
+
*/
|
|
100
|
+
function isDebugEnabled() {
|
|
101
|
+
return process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
|
|
102
|
+
}
|
|
103
|
+
|
|
94
104
|
function extractAugmentContext(stderr) {
|
|
95
105
|
const output = (stderr || '').trim();
|
|
96
106
|
const marker = output.indexOf('[GitNexus]');
|
|
97
|
-
const debug =
|
|
107
|
+
const debug = isDebugEnabled();
|
|
98
108
|
if (debug && output.length > 0) {
|
|
99
109
|
// Emit the FULL discarded prefix (everything before the marker, or all of
|
|
100
110
|
// it when no marker is present) so suppressed diagnostics — LadybugDB lock
|
|
@@ -258,8 +268,14 @@ function buildAfterToolContext(input) {
|
|
|
258
268
|
if (/\bgit\s+(commit|merge|rebase|cherry-pick|pull)(\s|$)/.test(command)) {
|
|
259
269
|
const hint = buildStaleIndexHint(gitNexusDir, cwd);
|
|
260
270
|
if (hint) {
|
|
261
|
-
|
|
271
|
+
// The hint always reaches the agent via additionalContext (parts). Mirror
|
|
272
|
+
// it to stderr (for terminal users) only under GITNEXUS_DEBUG, so strict
|
|
273
|
+
// hook runners see no unexpected output on this normal path (#1913). The
|
|
274
|
+
// claude hook never mirrored this to stderr — this aligns the two adapters.
|
|
262
275
|
parts.push(hint);
|
|
276
|
+
if (isDebugEnabled()) {
|
|
277
|
+
process.stderr.write(`${hint}\n`);
|
|
278
|
+
}
|
|
263
279
|
}
|
|
264
280
|
}
|
|
265
281
|
}
|
|
@@ -269,7 +285,11 @@ function buildAfterToolContext(input) {
|
|
|
269
285
|
|
|
270
286
|
function runAugment(gitNexusDir, cwd, pattern) {
|
|
271
287
|
if (hasGitNexusServerOwner(gitNexusDir)) {
|
|
272
|
-
|
|
288
|
+
// Normal skip path: the MCP server owns the DB. Stay silent for strict
|
|
289
|
+
// hook runners (issue #1913); surface the reason only under GITNEXUS_DEBUG.
|
|
290
|
+
if (isDebugEnabled()) {
|
|
291
|
+
process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
|
|
292
|
+
}
|
|
273
293
|
return '';
|
|
274
294
|
}
|
|
275
295
|
const release = acquireHookSlot(gitNexusDir);
|
|
@@ -338,7 +358,7 @@ function main() {
|
|
|
338
358
|
const handler = handlers[input.hook_event_name || ''];
|
|
339
359
|
if (handler) handler(input);
|
|
340
360
|
} catch (err) {
|
|
341
|
-
if (
|
|
361
|
+
if (isDebugEnabled()) {
|
|
342
362
|
console.error('GitNexus antigravity hook error:', (err.message || '').slice(0, 200));
|
|
343
363
|
}
|
|
344
364
|
}
|
|
@@ -110,10 +110,20 @@ function hasGitNexusServerOwner(gitNexusDir) {
|
|
|
110
110
|
return hasGitNexusDbLockedByGitNexusServer(path.join(gitNexusDir, 'lbug'), process.pid);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Whether opt-in diagnostics should be written to the hook's stderr. Strict
|
|
115
|
+
* hook runners (e.g. Codex `PreToolUse`) validate hook output, so normal,
|
|
116
|
+
* non-error skip paths must stay silent unless the operator explicitly asks
|
|
117
|
+
* for diagnostics via GITNEXUS_DEBUG. See issue #1913.
|
|
118
|
+
*/
|
|
119
|
+
function isDebugEnabled() {
|
|
120
|
+
return process.env.GITNEXUS_DEBUG === '1' || process.env.GITNEXUS_DEBUG === 'true';
|
|
121
|
+
}
|
|
122
|
+
|
|
113
123
|
function extractAugmentContext(stderr) {
|
|
114
124
|
const output = (stderr || '').trim();
|
|
115
125
|
const marker = output.indexOf('[GitNexus]');
|
|
116
|
-
const debug =
|
|
126
|
+
const debug = isDebugEnabled();
|
|
117
127
|
if (debug && output.length > 0) {
|
|
118
128
|
// Emit the FULL discarded prefix (everything before the marker, or all of
|
|
119
129
|
// it when no marker is present) so suppressed diagnostics — KuzuDB lock
|
|
@@ -250,7 +260,12 @@ function handlePreToolUse(input) {
|
|
|
250
260
|
const pattern = extractPattern(toolName, toolInput);
|
|
251
261
|
if (!pattern || pattern.length < 3) return;
|
|
252
262
|
if (hasGitNexusServerOwner(gitNexusDir)) {
|
|
253
|
-
|
|
263
|
+
// Normal skip path: the MCP server owns the DB, so the CLI augment would
|
|
264
|
+
// contend on the lock. Stay silent for strict hook runners (issue #1913);
|
|
265
|
+
// surface the reason only when diagnostics are explicitly requested.
|
|
266
|
+
if (isDebugEnabled()) {
|
|
267
|
+
process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
|
|
268
|
+
}
|
|
254
269
|
return;
|
|
255
270
|
}
|
|
256
271
|
|
|
@@ -361,7 +376,7 @@ function main() {
|
|
|
361
376
|
const handler = handlers[input.hook_event_name || ''];
|
|
362
377
|
if (handler) handler(input);
|
|
363
378
|
} catch (err) {
|
|
364
|
-
if (
|
|
379
|
+
if (isDebugEnabled()) {
|
|
365
380
|
console.error('GitNexus hook error:', (err.message || '').slice(0, 200));
|
|
366
381
|
}
|
|
367
382
|
}
|
package/package.json
CHANGED