context-slice 1.8.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.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +349 -0
  3. package/dist/src/cli.js +199 -0
  4. package/dist/src/indexer/index.js +325 -0
  5. package/dist/src/languages/adapter.js +23 -0
  6. package/dist/src/languages/go/index.js +15 -0
  7. package/dist/src/languages/go/parse.js +443 -0
  8. package/dist/src/languages/go/resolve.js +313 -0
  9. package/dist/src/languages/java/enterprise/dependency-injection.js +175 -0
  10. package/dist/src/languages/java/enterprise/jpa-entity.js +89 -0
  11. package/dist/src/languages/java/enterprise/registry.js +22 -0
  12. package/dist/src/languages/java/enterprise/spring-data.js +239 -0
  13. package/dist/src/languages/java/enterprise/spring-mvc.js +183 -0
  14. package/dist/src/languages/java/enterprise/transactions.js +110 -0
  15. package/dist/src/languages/java.js +84 -0
  16. package/dist/src/languages/javascript/index.js +25 -0
  17. package/dist/src/languages/python/index.js +29 -0
  18. package/dist/src/languages/python/parse.js +415 -0
  19. package/dist/src/languages/python/resolve.js +413 -0
  20. package/dist/src/languages/rust/calls-resolve.js +1405 -0
  21. package/dist/src/languages/rust/index.js +12 -0
  22. package/dist/src/languages/rust/parse.js +545 -0
  23. package/dist/src/languages/rust/resolve.js +284 -0
  24. package/dist/src/languages/typescript/index.js +25 -0
  25. package/dist/src/languages/typescript/parse.js +793 -0
  26. package/dist/src/languages/typescript/resolve.js +463 -0
  27. package/dist/src/package-info.js +16 -0
  28. package/dist/src/parser/java-parser.js +339 -0
  29. package/dist/src/planner/budget.js +1 -0
  30. package/dist/src/planner/composition.js +372 -0
  31. package/dist/src/planner/rank.js +10 -0
  32. package/dist/src/render/compact-context.js +11 -0
  33. package/dist/src/server/mcp-server.js +154 -0
  34. package/dist/src/storage/sqlite.js +105 -0
  35. package/dist/src/types/enterprise.js +1 -0
  36. package/dist/src/types/model.js +1 -0
  37. package/dist/src/workflow/errors.js +10 -0
  38. package/dist/src/workflow/preview.js +220 -0
  39. package/dist/src/workflow/repository.js +56 -0
  40. package/mcp.json +10 -0
  41. package/package.json +76 -0
  42. package/plugin.json +15 -0
  43. package/queries/java/annotations.scm +1 -0
  44. package/queries/java/calls.scm +1 -0
  45. package/queries/java/imports.scm +1 -0
  46. package/queries/java/symbols.scm +2 -0
  47. package/skills/context-slice/SKILL.md +60 -0
@@ -0,0 +1,239 @@
1
+ import { registerEnterpriseExtractor, registerEnterpriseResolver } from "./registry.js";
2
+ // Two-phase like jpa-entity.ts: provisional PERSISTS_ENTITY carries the raw entity simple
3
+ // name in targetLabel; this file's own resolver (separate from jpa-entity's, each file
4
+ // self-contained) applies the unique-simple-name rule. REPOSITORY_QUERY is a structural
5
+ // fact about the method itself, so it is emitted "exact" and never touched by the resolver.
6
+ const BASE_RE = /\b(?:JpaRepository|CrudRepository|PagingAndSortingRepository|Repository)\s*</g;
7
+ const DERIVED_RE = /^(?:find|exists|delete|count)By(?=[A-Z])/;
8
+ /** Strips a leading "@" and any dotted package prefix, e.g. "@org.springframework...Query" -> "Query". */
9
+ function bareName(annotation) {
10
+ return annotation.slice(annotation.lastIndexOf(".") + 1).replace("@", "");
11
+ }
12
+ /** Top-level type arguments of the `<...>` opening at `open`, or undefined if unbalanced. */
13
+ function typeArguments(text, open) {
14
+ const args = [];
15
+ let depth = 0;
16
+ let start = open + 1;
17
+ for (let i = open; i < text.length; i++) {
18
+ const ch = text[i];
19
+ if (ch === "<")
20
+ depth++;
21
+ else if (ch === ">" && --depth === 0) {
22
+ args.push(text.slice(start, i).trim());
23
+ return { args, end: i + 1 };
24
+ }
25
+ else if (ch === "," && depth === 1) {
26
+ args.push(text.slice(start, i).trim());
27
+ start = i + 1;
28
+ }
29
+ }
30
+ return undefined;
31
+ }
32
+ /** First known base-repository supertype with exactly two plain type arguments. */
33
+ function repositoryShape(iface) {
34
+ const header = iface.source.slice(0, iface.source.includes("{") ? iface.source.indexOf("{") : undefined);
35
+ for (const m of header.matchAll(BASE_RE)) {
36
+ const open = (m.index ?? 0) + m[0].length - 1;
37
+ const parsed = typeArguments(header, open);
38
+ if (!parsed || parsed.args.length !== 2)
39
+ continue;
40
+ const [entity, id] = parsed.args;
41
+ // A generic entity argument (Map<String, Long>) names no single project type: drop, never guess.
42
+ if (!/^[\w.]+$/.test(entity))
43
+ continue;
44
+ return {
45
+ matched: header.slice(m.index, parsed.end).replace(/\s+/g, " "),
46
+ entity: entity.slice(entity.lastIndexOf(".") + 1),
47
+ id,
48
+ };
49
+ }
50
+ return undefined;
51
+ }
52
+ /** Split only where And/Or sits between a lowercase/digit end and an uppercase start. */
53
+ function derivedProperties(name) {
54
+ if (!DERIVED_RE.test(name))
55
+ return undefined;
56
+ return name
57
+ .replace(DERIVED_RE, "")
58
+ .split(/(?<=[a-z0-9])(?:And|Or)(?=[A-Z])/)
59
+ .filter(Boolean)
60
+ .map((seg) => `property: ${seg[0].toLowerCase()}${seg.slice(1)}`);
61
+ }
62
+ /**
63
+ * Verbatim @Query(...) argument text (string-literal aware); undefined if absent/unbalanced.
64
+ * Detection of whether @Query is present happens via SymbolRecord.annotations first -- never
65
+ * via the search regex below on its own, which previously matched inside comments or string
66
+ * literals anywhere in the method's source (a method body, not just its header, can contain
67
+ * arbitrary text). The qualified-name-tolerant prefix mirrors every prior phase's fix for a
68
+ * fully-qualified annotation name.
69
+ */
70
+ function queryText(method) {
71
+ if (!method.annotations.some((a) => bareName(a) === "Query"))
72
+ return undefined;
73
+ const at = method.source.search(/@(?:[\w.]+\.)?Query\s*\(/);
74
+ if (at === -1)
75
+ return undefined;
76
+ const open = method.source.indexOf("(", at);
77
+ let depth = 0;
78
+ let inString = false;
79
+ for (let i = open; i < method.source.length; i++) {
80
+ const ch = method.source[i];
81
+ if (ch === '"' && method.source[i - 1] !== "\\")
82
+ inString = !inString;
83
+ if (inString)
84
+ continue;
85
+ if (ch === "(")
86
+ depth++;
87
+ else if (ch === ")" && --depth === 0)
88
+ return method.source.slice(open + 1, i).trim();
89
+ }
90
+ return undefined;
91
+ }
92
+ function extractSpringData(symbols, filePath, _source) {
93
+ const relations = [];
94
+ const repositories = new Set();
95
+ for (const iface of symbols) {
96
+ if (iface.filePath !== filePath || iface.kind !== "interface")
97
+ continue;
98
+ const shape = repositoryShape(iface);
99
+ if (!shape)
100
+ continue;
101
+ repositories.add(iface.id);
102
+ relations.push({
103
+ kind: "PERSISTS_ENTITY",
104
+ family: "spring-data-jpa",
105
+ sourceSymbolId: iface.id,
106
+ targetLabel: shape.entity,
107
+ confidence: "unresolved", // provisional until resolvePersistsEntity runs
108
+ evidence: [`extends ${shape.matched}`, `id type: ${shape.id}`],
109
+ range: iface.range,
110
+ filePath,
111
+ });
112
+ }
113
+ for (const method of symbols) {
114
+ if (method.kind !== "method" || !method.parentId || !repositories.has(method.parentId))
115
+ continue;
116
+ const properties = derivedProperties(method.name);
117
+ const query = queryText(method);
118
+ if (!properties && query === undefined)
119
+ continue;
120
+ relations.push({
121
+ kind: "REPOSITORY_QUERY",
122
+ family: "spring-data-jpa",
123
+ sourceSymbolId: method.id,
124
+ targetLabel: properties ? `${method.name.match(DERIVED_RE)[0]}: ${properties.map((p) => p.slice(10)).join(", ")}` : "query",
125
+ confidence: "exact",
126
+ evidence: [...(properties ?? []), ...(query !== undefined ? [query] : [])],
127
+ range: method.range,
128
+ filePath,
129
+ });
130
+ }
131
+ return relations;
132
+ }
133
+ /** Same rule as DI bean identity: 1 match exact, 0 dropped, 2+ unresolved (never guessed). */
134
+ function resolvePersistsEntity(relations, allSymbols) {
135
+ const projectTypes = allSymbols.filter((s) => s.kind === "class" || s.kind === "interface");
136
+ return relations.flatMap((r) => {
137
+ if (r.kind !== "PERSISTS_ENTITY" || r.family !== "spring-data-jpa")
138
+ return [r];
139
+ const candidates = projectTypes.filter((s) => s.name === r.targetLabel);
140
+ if (candidates.length === 0)
141
+ return [];
142
+ const { targetSymbolId: _stale, ...rest } = r;
143
+ if (candidates.length > 1)
144
+ return [{ ...rest, confidence: "unresolved" }];
145
+ return [{ ...rest, confidence: "exact", targetSymbolId: candidates[0].id }];
146
+ });
147
+ }
148
+ /**
149
+ * Real-repository finding (petclinic-rest): a "plain" repository interface with NO generic of
150
+ * its own (e.g. `OwnerRepository`, no `<Entity, Id>`) declares the derived-query methods the
151
+ * service layer actually calls, while a separate sibling (e.g. `SpringDataOwnerRepository`)
152
+ * `extends OwnerRepository, Repository<Owner, Integer>` and re-declares only SOME of them. Under
153
+ * per-file extraction alone, the plain interface's methods get zero REPOSITORY_QUERY relations,
154
+ * because repositoryShape() only recognizes an interface that itself carries the `<Entity, Id>`
155
+ * generic. This resolver closes that gap: once an interface's PERSISTS_ENTITY has resolved
156
+ * "exact" (so we know its entity with certainty, never a guess), walk its own supertype chain
157
+ * (SymbolRecord.supertypes — plain identifier list, already stripped of generics by the parser)
158
+ * and, for every reachable supertype that is itself a project interface with NO generic shape of
159
+ * its own (so it wasn't already extracted directly), run the exact same derived-query/@Query
160
+ * extraction on its methods, attributing REPOSITORY_QUERY relations to the SAME entity the
161
+ * subinterface resolved. A supertype name that doesn't resolve to exactly one project interface
162
+ * (ambiguous or external, e.g. the framework's own `Repository`) is skipped, never guessed —
163
+ * same discipline as resolvePersistsEntity and resolveEntityRelations.
164
+ *
165
+ * ponytail: SymbolRecord.supertypes (java-parser.ts) is built from a regex that stops at the
166
+ * first `<`, so a supertype list like `extends Repository<Owner, Integer>, OwnerRepository`
167
+ * (generic-bearing supertype listed FIRST) loses `OwnerRepository` entirely — this only works
168
+ * reliably when the plain supertype is listed before the generic one, which happens to be every
169
+ * real occurrence seen so far (petclinic-rest always writes the plain interface first). If a
170
+ * real repository is found with the generic-first ordering, the parser's supertypes regex needs
171
+ * fixing (shared file — full shared-file discipline applies), not this resolver.
172
+ */
173
+ function resolveRepositoryQueryPropagation(relations, allSymbols) {
174
+ const persistsByInterface = new Map();
175
+ for (const r of relations) {
176
+ if (r.kind === "PERSISTS_ENTITY" && r.family === "spring-data-jpa" && r.confidence === "exact") {
177
+ persistsByInterface.set(r.sourceSymbolId, r);
178
+ }
179
+ }
180
+ if (persistsByInterface.size === 0)
181
+ return relations;
182
+ const interfacesByName = new Map();
183
+ const interfaceById = new Map();
184
+ for (const s of allSymbols) {
185
+ if (s.kind !== "interface")
186
+ continue;
187
+ interfaceById.set(s.id, s);
188
+ const list = interfacesByName.get(s.name) ?? [];
189
+ list.push(s);
190
+ interfacesByName.set(s.name, list);
191
+ }
192
+ const existingQuerySources = new Set(relations.filter((r) => r.kind === "REPOSITORY_QUERY" && r.family === "spring-data-jpa").map((r) => r.sourceSymbolId));
193
+ const added = [];
194
+ for (const ifaceId of persistsByInterface.keys()) {
195
+ const iface = interfaceById.get(ifaceId);
196
+ if (!iface)
197
+ continue;
198
+ const visited = new Set([iface.id]);
199
+ const queue = [...(iface.supertypes ?? [])];
200
+ while (queue.length) {
201
+ const name = queue.shift();
202
+ const candidates = interfacesByName.get(name);
203
+ if (!candidates || candidates.length !== 1)
204
+ continue; // unresolved/ambiguous/external: never guess
205
+ const supIface = candidates[0];
206
+ if (visited.has(supIface.id))
207
+ continue;
208
+ visited.add(supIface.id);
209
+ queue.push(...(supIface.supertypes ?? []));
210
+ if (persistsByInterface.has(supIface.id))
211
+ continue; // has its own generic: already extracted directly
212
+ for (const method of allSymbols) {
213
+ if (method.kind !== "method" || method.parentId !== supIface.id)
214
+ continue;
215
+ if (existingQuerySources.has(method.id))
216
+ continue;
217
+ const properties = derivedProperties(method.name);
218
+ const query = queryText(method);
219
+ if (!properties && query === undefined)
220
+ continue;
221
+ added.push({
222
+ kind: "REPOSITORY_QUERY",
223
+ family: "spring-data-jpa",
224
+ sourceSymbolId: method.id,
225
+ targetLabel: properties ? `${method.name.match(DERIVED_RE)[0]}: ${properties.map((p) => p.slice(10)).join(", ")}` : "query",
226
+ confidence: "exact",
227
+ evidence: [...(properties ?? []), ...(query !== undefined ? [query] : []), `propagated from ${supIface.name} via ${iface.name}`],
228
+ range: method.range,
229
+ filePath: method.filePath,
230
+ });
231
+ existingQuerySources.add(method.id);
232
+ }
233
+ }
234
+ }
235
+ return added.length === 0 ? relations : [...relations, ...added];
236
+ }
237
+ registerEnterpriseExtractor(extractSpringData);
238
+ registerEnterpriseResolver(resolvePersistsEntity);
239
+ registerEnterpriseResolver(resolveRepositoryQueryPropagation);
@@ -0,0 +1,183 @@
1
+ import { registerEnterpriseExtractor } from "./registry.js";
2
+ /**
3
+ * Spring MVC mapping annotation -> HTTP method label. A bare @RequestMapping's
4
+ * `method = RequestMethod.X` attribute is out of scope for this phase (known
5
+ * limitation): every @RequestMapping, regardless of any method= attribute,
6
+ * maps to "REQUEST" rather than being parsed into a real verb.
7
+ */
8
+ const MAPPING_ANNOTATIONS = {
9
+ RequestMapping: "REQUEST",
10
+ GetMapping: "GET",
11
+ PostMapping: "POST",
12
+ PutMapping: "PUT",
13
+ DeleteMapping: "DELETE",
14
+ PatchMapping: "PATCH",
15
+ };
16
+ const CONST_RE_TEMPLATE = (name) => new RegExp(`(?:static\\s+final|final\\s+static)\\s+String\\s+${name}\\s*=\\s*"([^"]*)"`);
17
+ /** Strips a leading "@" and any dotted package prefix, e.g. "@org.springframework...GetMapping" -> "GetMapping". */
18
+ function bareName(annotation) {
19
+ return annotation.slice(annotation.lastIndexOf(".") + 1).replace("@", "");
20
+ }
21
+ /**
22
+ * Matches ONE specific, already-AST-confirmed mapping annotation's own argument text.
23
+ * Detection of WHICH annotation (if any) is present happens via SymbolRecord.annotations,
24
+ * never via this regex — this only ever runs to extract an argument list for an annotation
25
+ * name already known to be real, eliminating the false-positive risk a generic multi-name
26
+ * scan over raw text would have (e.g. matching annotation-shaped text inside a comment).
27
+ * The optional `(?:[\w.]+\.)?` prefix tolerates a fully-qualified annotation name (e.g.
28
+ * "@org.springframework.web.bind.annotation.GetMapping(...)") the same way bareName()
29
+ * already tolerates one for detection — without it, a qualified annotation would be
30
+ * correctly DETECTED (via .annotations) but its argument text would never be found,
31
+ * silently downgrading every qualified-annotation route to a path-less "probable" result
32
+ * instead of the fully-resolved "exact" one it should get.
33
+ */
34
+ function mappingArgsRegex(name) {
35
+ return new RegExp(`@(?:[\\w.]+\\.)?${name}(?:\\(([^)]*)\\))?`);
36
+ }
37
+ /**
38
+ * The symbol's own header text (annotations + declaration), stopping before its body's
39
+ * opening "{". A naive indexOf("{") breaks when a mapping annotation's path literal itself
40
+ * contains "{" (e.g. "/{id}"), so this tracks string-literal and paren depth to find the
41
+ * real body brace instead.
42
+ */
43
+ function header(symbol) {
44
+ const text = symbol.source;
45
+ let inString = false;
46
+ let depth = 0;
47
+ for (let i = 0; i < text.length; i++) {
48
+ const ch = text[i];
49
+ if (ch === '"' && text[i - 1] !== "\\")
50
+ inString = !inString;
51
+ if (inString)
52
+ continue;
53
+ if (ch === "(")
54
+ depth++;
55
+ else if (ch === ")")
56
+ depth--;
57
+ else if (ch === "{" && depth === 0)
58
+ return text.slice(0, i);
59
+ }
60
+ return text;
61
+ }
62
+ /**
63
+ * Resolves a mapping annotation's argument text to a path.
64
+ * - A quoted string literal is exact.
65
+ * - A same-class `static final String NAME = "literal"` constant resolves through
66
+ * one hop, downgrading confidence to at most "probable".
67
+ * - An array literal (`{"/a", "/b"}`, including a single-element `{"/a"}`) is never
68
+ * treated as an unambiguous single route; its first element resolves through the
69
+ * same "probable" downgrade as a const hop, with the raw `{...}` text kept in
70
+ * evidence so a developer can see it was an array.
71
+ * - Anything else (a call, a qualified constant, a placeholder) is unresolved;
72
+ * the raw expression is kept for evidence/targetLabel, never guessed into a path.
73
+ */
74
+ function resolvePath(rawInside, classSource) {
75
+ if (rawInside === undefined)
76
+ return { kind: "absent" };
77
+ let text = rawInside.trim();
78
+ if (text === "")
79
+ return { kind: "absent" };
80
+ // produces=/consumes=/a bare method= attribute (no value=/path= alongside it) isn't a
81
+ // path argument at all — treat it the same as no path, never as a fabricated route.
82
+ if (/^(?:produces|consumes|method)\s*=/.test(text) && !/\b(?:value|path)\s*=/.test(text)) {
83
+ return { kind: "absent" };
84
+ }
85
+ text = text.replace(/^(?:value|path)\s*=\s*/, "").trim();
86
+ // An array literal (`{"/a", "/b"}`) is never a single exact route — take the first
87
+ // element but downgrade to "probable" (like a const hop), never "exact".
88
+ if (text.startsWith("{")) {
89
+ const firstElement = text.match(/"([^"]*)"/);
90
+ if (firstElement)
91
+ return { kind: "array", value: firstElement[1] };
92
+ return { kind: "unresolved", raw: text };
93
+ }
94
+ // Only a literal that IS the whole argument value counts — not string concatenation
95
+ // (`Paths.P + "/x"`) or a quoted substring buried inside other tokens.
96
+ const quoted = text.match(/^"([^"]*)"$/);
97
+ if (quoted)
98
+ return { kind: "literal", value: quoted[1] };
99
+ const identMatch = text.match(/^([A-Za-z_$][\w$]*)$/);
100
+ if (identMatch) {
101
+ const match = classSource.match(CONST_RE_TEMPLATE(identMatch[1]));
102
+ if (match)
103
+ return { kind: "const", value: match[1] };
104
+ }
105
+ return { kind: "unresolved", raw: text };
106
+ }
107
+ function joinPaths(a, b) {
108
+ if (!a)
109
+ return b;
110
+ if (!b)
111
+ return a;
112
+ return `${a.replace(/\/+$/, "")}/${b.replace(/^\/+/, "")}`;
113
+ }
114
+ function describeAnnotation(name, rawInside, kind, symbolName) {
115
+ const args = rawInside !== undefined ? `(${rawInside.trim()})` : "";
116
+ return `@${name}${args} on ${kind} ${symbolName}`;
117
+ }
118
+ function extractSpringMvcRelations(symbols, filePath, source) {
119
+ const relations = [];
120
+ const classes = symbols.filter((s) => s.kind === "class");
121
+ for (const method of symbols.filter((s) => s.kind === "method")) {
122
+ const parent = classes.find((c) => c.id === method.parentId);
123
+ const methodAnnotationName = method.annotations
124
+ .map(bareName)
125
+ .find((name) => name in MAPPING_ANNOTATIONS);
126
+ if (!methodAnnotationName)
127
+ continue; // not a handler: no annotation spam for non-mapped methods
128
+ const httpMethod = MAPPING_ANNOTATIONS[methodAnnotationName];
129
+ const methodMatch = header(method).match(mappingArgsRegex(methodAnnotationName));
130
+ const classSourceForConstants = parent?.source ?? "";
131
+ const methodResolution = resolvePath(methodMatch?.[1], classSourceForConstants);
132
+ const evidence = [];
133
+ let classAnnotationName;
134
+ let classRawInside;
135
+ if (parent) {
136
+ classAnnotationName = parent.annotations.map(bareName).find((name) => name in MAPPING_ANNOTATIONS);
137
+ if (classAnnotationName) {
138
+ const classMatch = header(parent).match(mappingArgsRegex(classAnnotationName));
139
+ classRawInside = classMatch?.[1];
140
+ evidence.push(describeAnnotation(classAnnotationName, classRawInside, "class", parent.name));
141
+ }
142
+ }
143
+ evidence.push(describeAnnotation(methodAnnotationName, methodMatch?.[1], "method", method.name));
144
+ let confidence;
145
+ let targetLabel;
146
+ const classResolution = classAnnotationName
147
+ ? resolvePath(classRawInside, classSourceForConstants)
148
+ : { kind: "absent" };
149
+ if (methodResolution.kind === "unresolved") {
150
+ confidence = "unresolved";
151
+ targetLabel = methodResolution.raw;
152
+ }
153
+ else if (classResolution.kind === "unresolved") {
154
+ confidence = "unresolved";
155
+ targetLabel = classResolution.raw;
156
+ }
157
+ else {
158
+ const classPath = classResolution.kind === "absent" ? "" : classResolution.value;
159
+ const methodPath = methodResolution.kind === "absent" ? "" : methodResolution.value;
160
+ const usedConstOrArrayHop = classResolution.kind === "const" ||
161
+ methodResolution.kind === "const" ||
162
+ classResolution.kind === "array" ||
163
+ methodResolution.kind === "array";
164
+ // No path text resolved anywhere (e.g. a produces=-only mapping with no class prefix):
165
+ // never claim "exact" for a route that has no actual path evidence behind it.
166
+ const noPathAtAll = classPath === "" && methodPath === "";
167
+ confidence = usedConstOrArrayHop || noPathAtAll ? "probable" : "exact";
168
+ targetLabel = `${httpMethod} ${joinPaths(classPath, methodPath)}`.trimEnd();
169
+ }
170
+ relations.push({
171
+ kind: "ROUTE_TO_HANDLER",
172
+ family: "spring-mvc",
173
+ sourceSymbolId: method.id,
174
+ targetLabel,
175
+ confidence,
176
+ evidence,
177
+ range: method.range,
178
+ filePath,
179
+ });
180
+ }
181
+ return relations;
182
+ }
183
+ registerEnterpriseExtractor(extractSpringMvcRelations);
@@ -0,0 +1,110 @@
1
+ import { registerEnterpriseExtractor } from "./registry.js";
2
+ const KEPT_ATTRS = new Set(["readOnly", "propagation", "isolation", "rollbackFor", "noRollbackFor", "timeout"]);
3
+ /** Strips a leading "@" and any dotted package prefix, e.g. "@org.springframework...Transactional" -> "Transactional". */
4
+ function bareName(annotation) {
5
+ return annotation.slice(annotation.lastIndexOf(".") + 1).replace("@", "");
6
+ }
7
+ /**
8
+ * Matches an already-AST-confirmed @Transactional annotation's own argument text. Detection
9
+ * of whether the annotation is present happens via SymbolRecord.annotations, never via this
10
+ * regex -- this only ever runs to extract an argument list once .annotations has confirmed
11
+ * the annotation is real. The optional `(?:[\w.]+\.)?` prefix tolerates a fully-qualified
12
+ * annotation name the same way bareName() already tolerates one for detection -- without it,
13
+ * a qualified @Transactional would be correctly DETECTED but its attributes would never be
14
+ * found, silently downgrading it to "no fact to record" (the same brief's Review Focus item 2).
15
+ */
16
+ function transactionalArgsRegex() {
17
+ return /@(?:[\w.]+\.)?Transactional(?:\(([^]*?)\))?/;
18
+ }
19
+ /**
20
+ * The symbol's own header text (annotations + declaration), stopping before its body's
21
+ * opening "{". Same string/paren-depth-aware scan as spring-mvc.ts's header(), re-derived
22
+ * here (each family extractor is self-contained, per convention) rather than imported —
23
+ * a naive indexOf("{") would misfire if an attribute value ever contained a brace.
24
+ */
25
+ function header(symbol) {
26
+ const text = symbol.source;
27
+ let inString = false;
28
+ let depth = 0;
29
+ for (let i = 0; i < text.length; i++) {
30
+ const ch = text[i];
31
+ if (ch === '"' && text[i - 1] !== "\\")
32
+ inString = !inString;
33
+ if (inString)
34
+ continue;
35
+ if (ch === "(")
36
+ depth++;
37
+ else if (ch === ")")
38
+ depth--;
39
+ else if (ch === "{" && depth === 0)
40
+ return text.slice(0, i);
41
+ }
42
+ return text;
43
+ }
44
+ /** Splits an annotation argument list on commas outside (), strings, so dotted constants
45
+ * (Propagation.REQUIRES_NEW) and .class literals never get mis-split. */
46
+ function splitTopLevel(text) {
47
+ const parts = [];
48
+ let inString = false;
49
+ let depth = 0;
50
+ let last = 0;
51
+ for (let i = 0; i < text.length; i++) {
52
+ const ch = text[i];
53
+ if (ch === '"' && text[i - 1] !== "\\")
54
+ inString = !inString;
55
+ if (inString)
56
+ continue;
57
+ if (ch === "(")
58
+ depth++;
59
+ else if (ch === ")")
60
+ depth--;
61
+ else if (ch === "," && depth === 0) {
62
+ parts.push(text.slice(last, i));
63
+ last = i + 1;
64
+ }
65
+ }
66
+ parts.push(text.slice(last));
67
+ return parts.map((p) => p.trim()).filter(Boolean);
68
+ }
69
+ function extractTransactionRelations(symbols, filePath, source) {
70
+ const relations = [];
71
+ for (const method of symbols.filter((s) => s.kind === "method")) {
72
+ const hasTransactional = method.annotations.some((a) => bareName(a) === "Transactional");
73
+ if (!hasTransactional)
74
+ continue;
75
+ const match = header(method).match(transactionalArgsRegex());
76
+ const rawArgs = match?.[1];
77
+ if (rawArgs === undefined || rawArgs.trim() === "")
78
+ continue; // bare or empty-parens: no fact to record
79
+ const evidence = [];
80
+ for (const pair of splitTopLevel(rawArgs)) {
81
+ const eq = pair.indexOf("=");
82
+ if (eq === -1)
83
+ continue;
84
+ const name = pair.slice(0, eq).trim();
85
+ if (!KEPT_ATTRS.has(name))
86
+ continue;
87
+ evidence.push(pair.replace(/\s+/g, " ").trim());
88
+ }
89
+ if (evidence.length === 0)
90
+ continue;
91
+ const targetLabel = evidence
92
+ .map((e) => {
93
+ const eq = e.indexOf("=");
94
+ return `${e.slice(0, eq).trim()}=${e.slice(eq + 1).trim()}`;
95
+ })
96
+ .join(", ");
97
+ relations.push({
98
+ kind: "TRANSACTION_BOUNDARY",
99
+ family: "transactions",
100
+ sourceSymbolId: method.id,
101
+ targetLabel,
102
+ confidence: "exact",
103
+ evidence,
104
+ range: method.range,
105
+ filePath,
106
+ });
107
+ }
108
+ return relations;
109
+ }
110
+ registerEnterpriseExtractor(extractTransactionRelations);
@@ -0,0 +1,84 @@
1
+ import { parseJava } from "../parser/java-parser.js";
2
+ import { registerLanguage, } from "./adapter.js";
3
+ export const javaAdapter = {
4
+ id: "java",
5
+ label: "Java",
6
+ extensions: [".java"],
7
+ ignoredDirectories: ["target", ".gradle"],
8
+ parse(filePath, source) {
9
+ const parsed = parseJava(filePath, source);
10
+ return { ...parsed, imports: [], exports: [] };
11
+ },
12
+ resolveCalls({ symbols, calls, sourceOf }) {
13
+ for (const call of calls) {
14
+ const caller = symbols.find((symbol) => symbol.id === call.callerId);
15
+ if (!caller)
16
+ continue;
17
+ const parent = symbols.find((symbol) => symbol.id === caller.parentId);
18
+ const candidates = symbols.filter((symbol) => (symbol.kind === "method" || symbol.kind === "constructor") &&
19
+ symbol.name === call.calleeName);
20
+ const sameType = candidates.filter((candidate) => candidate.parentId === caller.parentId);
21
+ const source = sourceOf(caller);
22
+ const declaredType = call.receiverText
23
+ ? (source.match(new RegExp(`(?:\\b([A-Za-z_$][\\w$]*)\\s+${call.receiverText}\\b|\\b([A-Za-z_$][\\w$]*)\\s+${call.receiverText}\\s*[=;])`))?.[1] ??
24
+ source.match(new RegExp(`\\b([A-Za-z_$][\\w$]*)\\s+${call.receiverText}\\b`))?.[1])
25
+ : undefined;
26
+ const receiverType = declaredType ??
27
+ (call.receiverText && /^[A-Z]/.test(call.receiverText)
28
+ ? call.receiverText
29
+ : undefined);
30
+ const typed = receiverType
31
+ ? candidates.filter((candidate) => symbols.find((symbol) => symbol.id === candidate.parentId)
32
+ ?.name === receiverType)
33
+ : [];
34
+ const inherited = parent?.supertypes?.flatMap((supertype) => candidates.filter((candidate) => symbols.find((symbol) => symbol.id === candidate.parentId)
35
+ ?.name === supertype)) ?? [];
36
+ const narrowed = (items) => call.argumentCount === undefined
37
+ ? items
38
+ : items.filter((candidate) => (candidate.signature?.match(/\(([^)]*)\)/)?.[1].trim()
39
+ ? candidate.signature.match(/\(([^)]*)\)/)[1].split(",")
40
+ .length
41
+ : 0) === call.argumentCount);
42
+ const options = call.resolutionKind === "constructor"
43
+ ? narrowed(candidates.filter((candidate) => candidate.kind === "constructor"))
44
+ : receiverType
45
+ ? narrowed(typed)
46
+ : sameType.length
47
+ ? narrowed(sameType)
48
+ : narrowed(inherited);
49
+ if (options.length === 1) {
50
+ const target = options[0];
51
+ call.declaredTargetId = target.id;
52
+ call.resolvedTargetId = target.id;
53
+ call.confidence =
54
+ receiverType &&
55
+ symbols.find((symbol) => symbol.id === target.parentId)?.kind ===
56
+ "interface"
57
+ ? "probable"
58
+ : "exact";
59
+ call.resolutionKind =
60
+ call.resolutionKind === "constructor"
61
+ ? "constructor"
62
+ : inherited.includes(target)
63
+ ? "inherited"
64
+ : receiverType
65
+ ? symbols.find((symbol) => symbol.id === target.parentId)
66
+ ?.kind === "interface"
67
+ ? "interface"
68
+ : /^[A-Z]/.test(call.receiverText ?? "")
69
+ ? "static"
70
+ : "explicit-receiver"
71
+ : "same-type";
72
+ call.evidence = [
73
+ `unique ${call.resolutionKind} target ${target.qualifiedName}`,
74
+ ];
75
+ }
76
+ else if (options.length > 1) {
77
+ call.confidence = "unresolved";
78
+ call.resolutionKind = receiverType ? "interface" : "unresolved";
79
+ call.evidence = [`${options.length} plausible targets remain`];
80
+ }
81
+ }
82
+ },
83
+ };
84
+ registerLanguage(javaAdapter);
@@ -0,0 +1,25 @@
1
+ import { registerLanguage } from "../adapter.js";
2
+ import { JAVASCRIPT_LANGUAGE_ID, parseTypeScript } from "../typescript/parse.js";
3
+ import { resolveTypeScriptCalls } from "../typescript/resolve.js";
4
+ // Reuses the TypeScript grammar/parser: JS syntax is a subset of TS, and parseTypeScript
5
+ // already derives the right language id and JSX-vs-plain grammar per file extension.
6
+ export const javaScriptAdapter = {
7
+ id: JAVASCRIPT_LANGUAGE_ID,
8
+ label: "JavaScript",
9
+ extensions: [".jsx", ".mjs", ".cjs", ".js"],
10
+ ignoredDirectories: [
11
+ "node_modules",
12
+ ".next",
13
+ ".nuxt",
14
+ ".turbo",
15
+ ".cache",
16
+ "coverage",
17
+ "storybook-static",
18
+ ],
19
+ parse: parseTypeScript,
20
+ resolveCalls: (context) => {
21
+ resolveTypeScriptCalls(context);
22
+ },
23
+ };
24
+ registerLanguage(javaScriptAdapter);
25
+ export { JAVASCRIPT_LANGUAGE_ID };
@@ -0,0 +1,29 @@
1
+ import { registerLanguage } from "../adapter.js";
2
+ import { LANGUAGE_ID, parsePython } from "./parse.js";
3
+ import { pythonDiagnostics, resolvePythonCalls } from "./resolve.js";
4
+ export const pythonAdapter = {
5
+ id: LANGUAGE_ID,
6
+ label: "Python",
7
+ extensions: [".py", ".pyi"],
8
+ ignoredDirectories: [
9
+ "__pycache__",
10
+ ".venv",
11
+ "venv",
12
+ "env",
13
+ "site-packages",
14
+ ".pytest_cache",
15
+ ".mypy_cache",
16
+ ".pyright",
17
+ ".ruff_cache",
18
+ ".tox",
19
+ ".nox",
20
+ ".eggs",
21
+ ],
22
+ parse: parsePython,
23
+ resolveCalls: (context) => {
24
+ resolvePythonCalls(context);
25
+ },
26
+ };
27
+ registerLanguage(pythonAdapter);
28
+ export { pythonDiagnostics };
29
+ export { LANGUAGE_ID as PYTHON_LANGUAGE_ID };