gitnexus 1.6.11-rc.21 → 1.6.11-rc.23

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 (64) hide show
  1. package/README.md +24 -21
  2. package/dist/_shared/graph/types.d.ts +6 -0
  3. package/dist/_shared/graph/types.d.ts.map +1 -1
  4. package/dist/cli/ai-context.d.ts +4 -0
  5. package/dist/cli/ai-context.js +5 -2
  6. package/dist/cli/analyze-config.js +14 -0
  7. package/dist/cli/analyze-options.d.ts +5 -0
  8. package/dist/cli/analyze.js +2 -0
  9. package/dist/cli/index.js +2 -0
  10. package/dist/cli/watch.js +2 -0
  11. package/dist/core/group/cross-impact.d.ts +3 -0
  12. package/dist/core/group/cross-impact.js +26 -4
  13. package/dist/core/group/extractors/http-patterns/java.js +18 -4
  14. package/dist/core/group/extractors/http-patterns/kotlin.js +2 -0
  15. package/dist/core/group/normalization.d.ts +38 -1
  16. package/dist/core/group/normalization.js +59 -3
  17. package/dist/core/group/service.d.ts +11 -0
  18. package/dist/core/group/service.js +8 -0
  19. package/dist/core/group/sync.d.ts +21 -0
  20. package/dist/core/group/sync.js +15 -3
  21. package/dist/core/group/types.d.ts +13 -0
  22. package/dist/core/ingestion/frameworks/spring/actuator-runtime.d.ts +26 -0
  23. package/dist/core/ingestion/frameworks/spring/actuator-runtime.js +843 -0
  24. package/dist/core/ingestion/language-provider.d.ts +38 -1
  25. package/dist/core/ingestion/language-provider.js +22 -0
  26. package/dist/core/ingestion/languages/java/spring-actuator.d.ts +2 -0
  27. package/dist/core/ingestion/languages/java/spring-actuator.js +62 -0
  28. package/dist/core/ingestion/languages/java.js +8 -7
  29. package/dist/core/ingestion/languages/kotlin/spring-actuator.d.ts +4 -0
  30. package/dist/core/ingestion/languages/kotlin/spring-actuator.js +172 -0
  31. package/dist/core/ingestion/languages/kotlin.js +3 -0
  32. package/dist/core/ingestion/pipeline-phases/parse-impl.js +6 -1
  33. package/dist/core/ingestion/pipeline-phases/processes.d.ts +2 -1
  34. package/dist/core/ingestion/pipeline-phases/processes.js +39 -4
  35. package/dist/core/ingestion/pipeline-phases/scan.js +82 -1
  36. package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.d.ts +2 -0
  37. package/dist/core/ingestion/pipeline-phases/spring-auto-configuration.js +8 -0
  38. package/dist/core/ingestion/pipeline.d.ts +8 -0
  39. package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +16 -0
  40. package/dist/core/ingestion/route-extractors/constant-resolver.js +10 -0
  41. package/dist/core/ingestion/route-extractors/java-const-resolver.d.ts +27 -1
  42. package/dist/core/ingestion/route-extractors/java-const-resolver.js +161 -13
  43. package/dist/core/ingestion/route-extractors/kotlin-const-resolver.d.ts +7 -8
  44. package/dist/core/ingestion/route-extractors/kotlin-const-resolver.js +209 -23
  45. package/dist/core/ingestion/workers/parse-worker.js +5 -1
  46. package/dist/core/lbug/csv-generator.js +4 -1
  47. package/dist/core/lbug/graph-emit-sink.d.ts +3 -2
  48. package/dist/core/lbug/graph-emit-sink.js +4 -2
  49. package/dist/core/lbug/lbug-adapter.js +1 -1
  50. package/dist/core/lbug/schema.d.ts +1 -1
  51. package/dist/core/lbug/schema.js +3 -0
  52. package/dist/core/run-analyze.d.ts +5 -0
  53. package/dist/core/run-analyze.js +56 -0
  54. package/dist/mcp/local/local-backend.js +46 -3
  55. package/dist/mcp/resources.js +1 -0
  56. package/dist/mcp/tools.js +4 -4
  57. package/dist/server/analyze-launch.d.ts +1 -0
  58. package/dist/server/analyze-launch.js +1 -0
  59. package/dist/server/api.d.ts +4 -0
  60. package/dist/server/api.js +46 -4
  61. package/dist/storage/parse-cache.js +8 -1
  62. package/dist/storage/repo-meta.d.ts +12 -0
  63. package/package.json +1 -1
  64. package/skills/gitnexus-cli.md +3 -0
@@ -20,7 +20,7 @@ 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 { CfgVisitor } from './cfg/types.js';
23
- import type { NodeLabel } from '../../_shared/index.js';
23
+ import type { GraphNode, NodeLabel } from '../../_shared/index.js';
24
24
  import type { ExtractedRoute } from './route-extractors/laravel.js';
25
25
  import type { SharedSpringType } from './route-extractors/spring-shared.js';
26
26
  import type { ModuleConstants, Operand, RepoConstants } from './route-extractors/constant-resolver.js';
@@ -31,11 +31,27 @@ export type CaptureMap = Record<string, SyntaxNode | undefined>;
31
31
  export interface DefinitionPropertiesContext {
32
32
  readonly nodeLabel: NodeLabel;
33
33
  readonly nodeName: string;
34
+ readonly filePath: string;
34
35
  readonly definitionNode: SyntaxNode;
35
36
  readonly parsedImports: readonly ParsedImport[];
36
37
  readonly isExported: boolean;
37
38
  }
38
39
  export type DefinitionPropertiesExtractor = (context: DefinitionPropertiesContext) => Readonly<Record<string, unknown>> | undefined;
40
+ export interface RuntimeCallableIdentity {
41
+ readonly name: string;
42
+ readonly descriptorParameterTypes: readonly string[] | undefined;
43
+ }
44
+ /**
45
+ * Optional language-owned bridge from runtime/compiler symbol identities to
46
+ * source graph symbols. Framework importers use this instead of naming
47
+ * languages or reproducing compiler conventions in shared ingestion code.
48
+ */
49
+ export interface RuntimeSymbolStrategy {
50
+ /** Runtime owner names that may contain this callable/property. */
51
+ readonly callableOwnerAliases?: (node: GraphNode, owner: GraphNode | undefined) => readonly string[];
52
+ /** Whether a runtime callable identity can conservatively identify a node. */
53
+ readonly matchesCallable: (node: GraphNode, runtime: RuntimeCallableIdentity) => boolean;
54
+ }
39
55
  /** Run optional provider enrichment without allowing one hook failure to drop
40
56
  * the rest of the worker's language batch. */
41
57
  export declare function runDefinitionPropertiesExtractor(extractor: DefinitionPropertiesExtractor, context: DefinitionPropertiesContext, onError: (error: unknown) => void): Readonly<Record<string, unknown>> | undefined;
@@ -125,6 +141,12 @@ interface LanguageProviderConfig {
125
141
  * Default: undefined (no preprocessing — `file.content` is parsed verbatim).
126
142
  */
127
143
  readonly preprocessSource?: (sourceText: string, filePath: string) => string;
144
+ /**
145
+ * Runtime/compiler identity reconciliation for framework metadata. The
146
+ * central importer owns ambiguity handling; providers only supply aliases
147
+ * and language-specific callable compatibility.
148
+ */
149
+ readonly runtimeSymbolStrategy?: RuntimeSymbolStrategy;
128
150
  /** Type extraction: declarations, initializers, for-loop bindings */
129
151
  readonly typeConfig: LanguageTypeConfig;
130
152
  /** Export detection: is this AST node a public/exported symbol? */
@@ -372,6 +394,15 @@ interface LanguageProviderConfig {
372
394
  * {@link extractModuleConstants} accepts.
373
395
  */
374
396
  readonly moduleConstantHeuristic?: (content: string) => boolean;
397
+ /**
398
+ * Prepare this language's harvested constants once the complete repo map is
399
+ * available and before route operands are folded. The parse phase passes only
400
+ * entries owned by this provider, so implementations can build one reusable
401
+ * language-specific index and may materialize deferred bindings in place.
402
+ *
403
+ * Default: undefined (the harvested constants are already fold-ready).
404
+ */
405
+ readonly prepareRouteConstants?: (repo: RepoConstants) => void;
375
406
  /**
376
407
  * Fold one file's non-literal route-path operand list
377
408
  * (`routePathExpr`/`routePathOperands` of an `ExtractedDecoratorRoute`)
@@ -723,6 +754,12 @@ export interface LanguageProvider extends Omit<LanguageProviderConfig, 'mroStrat
723
754
  /** Check if a name is a built-in/stdlib function that should be filtered from the call graph. */
724
755
  readonly isBuiltInName: (name: string) => boolean;
725
756
  }
757
+ /**
758
+ * Run each provider's repo-constant preparation hook once over only the files
759
+ * that provider owns. Values are shared with `repo`, so in-place preparation
760
+ * is visible to the subsequent fold without copying the complete map.
761
+ */
762
+ export declare function prepareRouteConstantsByProvider(repo: RepoConstants, providerForFile: (filePath: string) => Pick<LanguageProvider, 'prepareRouteConstants'> | null): void;
726
763
  /** Define a language provider — required fields must be supplied, optional fields get sensible defaults. */
727
764
  export declare function defineLanguage(config: LanguageProviderConfig): LanguageProvider;
728
765
  export {};
@@ -45,6 +45,28 @@ export function shouldHarvestModuleConstants(provider, content) {
45
45
  return false;
46
46
  return !provider.moduleConstantHeuristic || provider.moduleConstantHeuristic(content);
47
47
  }
48
+ /**
49
+ * Run each provider's repo-constant preparation hook once over only the files
50
+ * that provider owns. Values are shared with `repo`, so in-place preparation
51
+ * is visible to the subsequent fold without copying the complete map.
52
+ */
53
+ export function prepareRouteConstantsByProvider(repo, providerForFile) {
54
+ const slices = new Map();
55
+ for (const [filePath, constants] of repo) {
56
+ const provider = providerForFile(filePath);
57
+ if (!provider?.prepareRouteConstants)
58
+ continue;
59
+ let slice = slices.get(provider);
60
+ if (!slice) {
61
+ slice = new Map();
62
+ slices.set(provider, slice);
63
+ }
64
+ slice.set(filePath, constants);
65
+ }
66
+ for (const [provider, slice] of slices) {
67
+ provider.prepareRouteConstants?.(slice);
68
+ }
69
+ }
48
70
  const DEFAULTS = {
49
71
  mroStrategy: 'first-wins',
50
72
  };
@@ -0,0 +1,2 @@
1
+ import type { RuntimeSymbolStrategy } from '../../language-provider.js';
2
+ export declare const javaRuntimeSymbolStrategy: RuntimeSymbolStrategy;
@@ -0,0 +1,62 @@
1
+ const JVM_PRIMITIVES = {
2
+ B: 'byte',
3
+ C: 'char',
4
+ D: 'double',
5
+ F: 'float',
6
+ I: 'int',
7
+ J: 'long',
8
+ S: 'short',
9
+ Z: 'boolean',
10
+ };
11
+ function normalizedType(value, runtime) {
12
+ let erased = value.trim();
13
+ let arrayDimensions = 0;
14
+ if (erased.endsWith('...')) {
15
+ arrayDimensions++;
16
+ erased = erased.slice(0, -3);
17
+ }
18
+ while (erased.endsWith('[]')) {
19
+ arrayDimensions++;
20
+ erased = erased.slice(0, -2);
21
+ }
22
+ erased = erased.replace(/<.*>$/, '').replaceAll('$', '.').replaceAll('/', '.');
23
+ const simple = erased.slice(erased.lastIndexOf('.') + 1);
24
+ const base = runtime ? (JVM_PRIMITIVES[simple] ?? simple) : simple;
25
+ return `${base}${'[]'.repeat(arrayDimensions)}`;
26
+ }
27
+ function sourceTypeIsUnknown(value) {
28
+ const type = normalizedType(value, false).replace(/(?:\[\])+$/, '');
29
+ return type === '?' || /^[A-Z]$/.test(type);
30
+ }
31
+ function matchesJavaCallable(node, runtime) {
32
+ if (node.label !== 'Method' || node.properties.name !== runtime.name)
33
+ return false;
34
+ const descriptorTypes = runtime.descriptorParameterTypes;
35
+ if (descriptorTypes === undefined)
36
+ return true;
37
+ const parameterCount = node.properties.parameterCount;
38
+ if (typeof parameterCount === 'number' && parameterCount !== descriptorTypes.length)
39
+ return false;
40
+ const sourceTypes = node.properties.parameterTypes;
41
+ if (!Array.isArray(sourceTypes) ||
42
+ sourceTypes.length !== descriptorTypes.length ||
43
+ !sourceTypes.every((type) => typeof type === 'string')) {
44
+ return true;
45
+ }
46
+ return sourceTypes.every((sourceType, index) => {
47
+ if (sourceTypeIsUnknown(sourceType))
48
+ return true;
49
+ const source = normalizedType(sourceType, false);
50
+ const descriptor = normalizedType(descriptorTypes[index] ?? '', true);
51
+ if (source === descriptor)
52
+ return true;
53
+ // Java parser metadata currently drops the ellipsis from varargs and also
54
+ // leaves parameterCount open-ended. Only in that shape may T match JVM T[].
55
+ return (typeof parameterCount !== 'number' &&
56
+ descriptor.endsWith('[]') &&
57
+ source === descriptor.slice(0, -2));
58
+ });
59
+ }
60
+ export const javaRuntimeSymbolStrategy = {
61
+ matchesCallable: matchesJavaCallable,
62
+ };
@@ -13,7 +13,7 @@ import { defineLanguage } from '../language-provider.js';
13
13
  import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
14
14
  import { javaTypeConfig } from '../type-extractors/jvm.js';
15
15
  import { extractSpringRoutes, extractSpringTypes } from '../route-extractors/spring.js';
16
- import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, } from '../route-extractors/java-const-resolver.js';
16
+ import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, prepareJavaRouteConstants, } from '../route-extractors/java-const-resolver.js';
17
17
  import { javaExportChecker } from '../export-detection.js';
18
18
  import { createImportResolver } from '../import-resolvers/resolver-factory.js';
19
19
  import { javaImportConfig } from '../import-resolvers/configs/jvm.js';
@@ -30,6 +30,7 @@ import { collectJavaCaptureSideChannel } from './java/capture-side-channel.js';
30
30
  import { javaRecordMethodExtractor, shouldSkipJavaRecordComponentDefinition, } from './java/record-components.js';
31
31
  import { synthesizeLombokAccessors } from './java/lombok-synthesizer.js';
32
32
  import { emitJavaScopeCaptures, interpretJavaImport, interpretJavaTypeBinding, javaBindingScopeFor, javaImportOwningScope, javaMergeBindings, javaReceiverBinding, javaArityCompatibility, resolveJavaImportTarget, } from './java/index.js';
33
+ import { javaRuntimeSymbolStrategy } from './java/spring-actuator.js';
33
34
  /**
34
35
  * Java names the platform owns, matched against a BARE IDENTIFIER — a dropped
35
36
  * receiver's chain base (`System` in `System.out.println(...)`) or the bare
@@ -168,6 +169,7 @@ export const javaProvider = defineLanguage({
168
169
  shouldSkipDefinitionCapture: shouldSkipJavaRecordComponentDefinition,
169
170
  variableExtractor: createVariableExtractor(javaVariableConfig),
170
171
  classExtractor: createClassExtractor(javaClassConfig),
172
+ runtimeSymbolStrategy: javaRuntimeSymbolStrategy,
171
173
  // ── Javadoc → description (issue #2270) ──
172
174
  descriptionExtractor: createLeadingDocDescriptionExtractor(),
173
175
  builtInNames: BUILT_INS,
@@ -202,11 +204,10 @@ export const javaProvider = defineLanguage({
202
204
  // INTERFACES on this side only, which cost the graph its Route nodes while
203
205
  // the group still published the contract).
204
206
  moduleConstantHeuristic: (content) => isJavaConstantFile(content) ||
205
- // `import com.winning.opt.common.ApiPaths;` — ANY class import can bind a
206
- // constant ref (`ApiPaths.X` at an annotation site), so gate on the
207
- // general import shape, not on the imported name. Ingestion-only: this
208
- // side needs the importing controller's own import table, which the group
209
- // side instead derives lazily from the tree it already holds.
210
- /\bimport\s+(?:static\s+)?[\w.]+\s*;/.test(content),
207
+ // Class imports and static (including on-demand) imports can bind a
208
+ // constant ref. Ordinary `import a.b.*;` is not a Java type import and is
209
+ // not expanded by extractJavaModuleConstants, so it must not harvest.
210
+ /\bimport\s+(?:static\s+[\w.]+(?:\.\*)?|[\w.]+)\s*;/.test(content),
211
+ prepareRouteConstants: prepareJavaRouteConstants,
211
212
  foldRoutePathOperands: foldJavaOperands,
212
213
  });
@@ -0,0 +1,4 @@
1
+ import type { DefinitionPropertiesContext, RuntimeSymbolStrategy } from '../../language-provider.js';
2
+ /** Transient graph metadata used only by the same analysis run's runtime import. */
3
+ export declare function extractKotlinRuntimeSymbolProperties(context: DefinitionPropertiesContext): Readonly<Record<string, unknown>> | undefined;
4
+ export declare const kotlinRuntimeSymbolStrategy: RuntimeSymbolStrategy;
@@ -0,0 +1,172 @@
1
+ import path from 'node:path';
2
+ const RUNTIME_OWNER_ALIASES = 'runtimeOwnerAliases';
3
+ const RUNTIME_CALLABLE_ALIASES = 'runtimeCallableAliases';
4
+ const KOTLIN_SUSPEND = 'kotlinSuspend';
5
+ const fileFacadeMetadataCache = new WeakMap();
6
+ function rootNode(node) {
7
+ let current = node;
8
+ while (current.parent)
9
+ current = current.parent;
10
+ return current;
11
+ }
12
+ function packageName(root) {
13
+ const header = root.namedChildren.find((child) => child.type === 'package_header');
14
+ return header?.text.replace(/^package\s+/, '').trim() ?? '';
15
+ }
16
+ function qualify(packageNameValue, simpleName) {
17
+ return packageNameValue.length === 0 ? simpleName : `${packageNameValue}.${simpleName}`;
18
+ }
19
+ function standardFacadeName(filePath) {
20
+ const stem = path.basename(filePath).replace(/\.(?:kt|kts)$/i, '');
21
+ return `${stem.charAt(0).toUpperCase()}${stem.slice(1)}Kt`;
22
+ }
23
+ function jvmNameIdentifiers(imports, allowUnqualified) {
24
+ const names = new Set(['kotlin.jvm.JvmName']);
25
+ if (allowUnqualified)
26
+ names.add('JvmName');
27
+ for (const parsedImport of imports) {
28
+ if (parsedImport.kind !== 'named' && parsedImport.kind !== 'alias')
29
+ continue;
30
+ if (parsedImport.importedName !== 'JvmName')
31
+ continue;
32
+ const target = parsedImport.targetRaw.replace(/\\/g, '/');
33
+ if (target === 'kotlin.jvm' || target === 'kotlin.jvm.JvmName') {
34
+ names.add(parsedImport.localName);
35
+ }
36
+ }
37
+ return [...names];
38
+ }
39
+ function annotationJvmName(source, target = '', imports = [], allowUnqualified = true) {
40
+ const names = jvmNameIdentifiers(imports, allowUnqualified);
41
+ if (names.length === 0)
42
+ return undefined;
43
+ const escapedTarget = target.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
44
+ const prefix = target.length === 0 ? '' : `${escapedTarget}:`;
45
+ const namePattern = [...names]
46
+ .map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
47
+ .join('|');
48
+ return new RegExp(`@${prefix}(?:${namePattern})\\s*\\(\\s*["']([^"']+)["']\\s*\\)`).exec(source)?.[1];
49
+ }
50
+ function fileFacadeMetadata(root, imports, allowUnqualified) {
51
+ const cached = fileFacadeMetadataCache.get(root);
52
+ if (cached !== undefined)
53
+ return cached;
54
+ const metadata = {
55
+ packageName: packageName(root),
56
+ customFacade: annotationJvmName(root.text, 'file', imports, allowUnqualified),
57
+ };
58
+ fileFacadeMetadataCache.set(root, metadata);
59
+ return metadata;
60
+ }
61
+ function hasEnclosingType(node) {
62
+ let current = node.parent;
63
+ while (current) {
64
+ if (current.type === 'class_declaration' ||
65
+ current.type === 'object_declaration' ||
66
+ current.type === 'companion_object') {
67
+ return true;
68
+ }
69
+ current = current.parent;
70
+ }
71
+ return false;
72
+ }
73
+ /** Transient graph metadata used only by the same analysis run's runtime import. */
74
+ export function extractKotlinRuntimeSymbolProperties(context) {
75
+ const properties = {};
76
+ const source = context.definitionNode.text;
77
+ const root = rootNode(context.definitionNode);
78
+ const allowUnqualifiedJvmName = !/\bannotation\s+class\s+JvmName\b/.test(root.text);
79
+ if ((context.nodeLabel === 'Function' || context.nodeLabel === 'Method') &&
80
+ context.definitionNode.type === 'function_declaration') {
81
+ if (/\bsuspend\b/.test(source.slice(0, source.indexOf('fun') + 3))) {
82
+ properties[KOTLIN_SUSPEND] = true;
83
+ }
84
+ const callableJvmName = annotationJvmName(source, '', context.parsedImports, allowUnqualifiedJvmName);
85
+ if (callableJvmName !== undefined) {
86
+ properties[RUNTIME_CALLABLE_ALIASES] = [callableJvmName];
87
+ }
88
+ if (!hasEnclosingType(context.definitionNode)) {
89
+ const facade = fileFacadeMetadata(root, context.parsedImports, allowUnqualifiedJvmName);
90
+ properties[RUNTIME_OWNER_ALIASES] = [
91
+ qualify(facade.packageName, facade.customFacade ?? standardFacadeName(context.filePath)),
92
+ ];
93
+ }
94
+ }
95
+ else if (context.nodeLabel === 'Property') {
96
+ const getterJvmName = annotationJvmName(source, 'get', context.parsedImports, allowUnqualifiedJvmName);
97
+ if (getterJvmName !== undefined) {
98
+ properties[RUNTIME_CALLABLE_ALIASES] = [getterJvmName];
99
+ }
100
+ if (!hasEnclosingType(context.definitionNode)) {
101
+ const facade = fileFacadeMetadata(root, context.parsedImports, allowUnqualifiedJvmName);
102
+ properties[RUNTIME_OWNER_ALIASES] = [
103
+ qualify(facade.packageName, facade.customFacade ?? standardFacadeName(context.filePath)),
104
+ ];
105
+ }
106
+ }
107
+ return Object.keys(properties).length === 0 ? undefined : properties;
108
+ }
109
+ function stringArrayProperty(node, property) {
110
+ const value = node.properties[property];
111
+ return Array.isArray(value)
112
+ ? value.filter((item) => typeof item === 'string')
113
+ : [];
114
+ }
115
+ function callableNames(node) {
116
+ return [String(node.properties.name), ...stringArrayProperty(node, RUNTIME_CALLABLE_ALIASES)];
117
+ }
118
+ function propertyGetterNames(node) {
119
+ const name = String(node.properties.name);
120
+ const capitalized = `${name.charAt(0).toUpperCase()}${name.slice(1)}`;
121
+ return [
122
+ name.startsWith('is') && name.length > 2 && /[A-Z]/.test(name.charAt(2))
123
+ ? name
124
+ : `get${capitalized}`,
125
+ ...stringArrayProperty(node, RUNTIME_CALLABLE_ALIASES),
126
+ ];
127
+ }
128
+ function sourceCallableName(runtimeName) {
129
+ return runtimeName.endsWith('$default') ? runtimeName.slice(0, -'$default'.length) : runtimeName;
130
+ }
131
+ function matchesKotlinCallable(node, runtime) {
132
+ // Kotlin property declarations and their synthesized JVM accessor Methods
133
+ // coexist in the graph. Bind runtime getters to the source Property so the
134
+ // synthetic accessor cannot turn an otherwise exact match into ambiguity.
135
+ if (node.properties.synthetic === 'kotlin-jvm')
136
+ return false;
137
+ const runtimeName = sourceCallableName(runtime.name);
138
+ if (node.label === 'Property') {
139
+ const names = propertyGetterNames(node);
140
+ if (!names.includes(runtime.name) && !names.includes(runtimeName))
141
+ return false;
142
+ }
143
+ else if (!callableNames(node).includes(runtimeName)) {
144
+ return false;
145
+ }
146
+ const parameterCount = node.properties.parameterCount;
147
+ const descriptorTypes = runtime.descriptorParameterTypes;
148
+ if (typeof parameterCount !== 'number' ||
149
+ descriptorTypes === undefined ||
150
+ runtime.name.endsWith('$default')) {
151
+ return true;
152
+ }
153
+ if (parameterCount === descriptorTypes.length)
154
+ return true;
155
+ return (node.properties[KOTLIN_SUSPEND] === true &&
156
+ parameterCount + 1 === descriptorTypes.length &&
157
+ descriptorTypes.at(-1) === 'kotlin/coroutines/Continuation');
158
+ }
159
+ export const kotlinRuntimeSymbolStrategy = {
160
+ callableOwnerAliases(node, owner) {
161
+ const aliases = [...stringArrayProperty(node, RUNTIME_OWNER_ALIASES)];
162
+ const ownerName = owner?.properties.qualifiedName;
163
+ if (typeof ownerName === 'string') {
164
+ aliases.push(ownerName);
165
+ if (node.properties.isStatic === true && !ownerName.endsWith('.Companion')) {
166
+ aliases.push(`${ownerName}.Companion`);
167
+ }
168
+ }
169
+ return aliases;
170
+ },
171
+ matchesCallable: matchesKotlinCallable,
172
+ };
@@ -29,6 +29,7 @@ import { createVariableExtractor } from '../variable-extractors/generic.js';
29
29
  import { kotlinVariableConfig } from '../variable-extractors/configs/jvm.js';
30
30
  import { collectKotlinCaptureSideChannel, emitKotlinScopeCaptures, interpretKotlinImport, interpretKotlinTypeBinding, kotlinArityCompatibility, kotlinBindingScopeFor, kotlinImportOwningScope, kotlinMergeBindings, kotlinReceiverBinding, } from './kotlin/index.js';
31
31
  import { synthesizeLombokAccessors } from './kotlin/lombok-synthesizer.js';
32
+ import { extractKotlinRuntimeSymbolProperties, kotlinRuntimeSymbolStrategy, } from './kotlin/spring-actuator.js';
32
33
  /** Check if a Kotlin function_declaration capture is inside a class_body (i.e., a method).
33
34
  * Kotlin grammar uses function_declaration for both top-level functions and class methods.
34
35
  * Returns true when the captured definition node has a class_body ancestor. */
@@ -157,6 +158,8 @@ export const kotlinProvider = defineLanguage({
157
158
  builtInNames: BUILT_INS,
158
159
  // ── KDoc → description (issue #2270) ──
159
160
  descriptionExtractor: createLeadingDocDescriptionExtractor(),
161
+ definitionPropertiesExtractor: extractKotlinRuntimeSymbolProperties,
162
+ runtimeSymbolStrategy: kotlinRuntimeSymbolStrategy,
160
163
  labelOverride: (functionNode, defaultLabel) => {
161
164
  if (defaultLabel !== 'Function')
162
165
  return defaultLabel;
@@ -30,7 +30,8 @@ import { SCOPE_RESOLVERS } from '../scope-resolution/pipeline/registry.js';
30
30
  import { DATA_ROUTE_TABLE_SOURCE } from '../route-extractors/data-route-table.js';
31
31
  import { createWorkerPool, workerPoolDisabledByEnv, resolveAutoPoolSize, WorkerPoolInitializationError, WorkerPoolDisabledError, } from '../workers/worker-pool.js';
32
32
  import { normalizeExtractedRoutePath } from '../route-extractors/route-path.js';
33
- import { resolveOperands, } from '../route-extractors/python-const-resolver.js';
33
+ import { resolveOperands } from '../route-extractors/python-const-resolver.js';
34
+ import { prepareRouteConstantsByProvider } from '../language-provider.js';
34
35
  import { resolveInheritedSpringRoutes, } from '../route-extractors/spring-shared.js';
35
36
  import fs from 'node:fs';
36
37
  import { effectiveRamBytes, memoryAutopilotDisabled } from '../utils/effective-ram.js';
@@ -1022,6 +1023,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
1022
1023
  for (const { filePath, constants } of allModuleConstants) {
1023
1024
  repoConstants.set(filePath, constants);
1024
1025
  }
1026
+ // Let each language prepare only its own constants slice before folding.
1027
+ // This is where deferred wildcard bindings can be materialized once per
1028
+ // provider without naming a language in the shared parse phase.
1029
+ prepareRouteConstantsByProvider(repoConstants, getProviderForFile);
1025
1030
  const resolvedRoutes = [];
1026
1031
  let skipped = 0;
1027
1032
  for (const dr of allDecoratorRoutes) {
@@ -4,7 +4,8 @@
4
4
  * Detects execution flows (processes) and creates Process nodes +
5
5
  * STEP_IN_PROCESS edges. Also links Route/Tool nodes to processes.
6
6
  *
7
- * @deps communities, routes, tools, pruneLocalSymbols, structure, parse
7
+ * @deps communities, routes, tools, springAutoConfiguration,
8
+ * pruneLocalSymbols, structure, parse
8
9
  * @reads graph (all nodes and relationships), communityResult, routeRegistry,
9
10
  * toolDefs, parse's allFetchCalls + allORMQueries (R3-6 sink sites)
10
11
  * @writes graph (Process nodes, STEP_IN_PROCESS edges, ENTRY_POINT_OF edges)
@@ -4,7 +4,8 @@
4
4
  * Detects execution flows (processes) and creates Process nodes +
5
5
  * STEP_IN_PROCESS edges. Also links Route/Tool nodes to processes.
6
6
  *
7
- * @deps communities, routes, tools, pruneLocalSymbols, structure, parse
7
+ * @deps communities, routes, tools, springAutoConfiguration,
8
+ * pruneLocalSymbols, structure, parse
8
9
  * @reads graph (all nodes and relationships), communityResult, routeRegistry,
9
10
  * toolDefs, parse's allFetchCalls + allORMQueries (R3-6 sink sites)
10
11
  * @writes graph (Process nodes, STEP_IN_PROCESS edges, ENTRY_POINT_OF edges)
@@ -39,7 +40,15 @@ export const processesPhase = {
39
40
  // sinks rather than failing the phase. `pruneLocalSymbols` is declared
40
41
  // explicitly so process extraction always reads the trimmed graph even if a
41
42
  // future option drops the intervening `mro`/`communities` phases.
42
- deps: ['communities', 'routes', 'tools', 'pruneLocalSymbols', 'structure', 'parse'],
43
+ deps: [
44
+ 'communities',
45
+ 'routes',
46
+ 'tools',
47
+ 'springAutoConfiguration',
48
+ 'pruneLocalSymbols',
49
+ 'structure',
50
+ 'parse',
51
+ ],
43
52
  async execute(ctx, deps) {
44
53
  const { totalFiles } = getPhaseOutput(deps, 'structure');
45
54
  const { communityResult } = getPhaseOutput(deps, 'communities');
@@ -172,8 +181,34 @@ export const processesPhase = {
172
181
  step: step.step,
173
182
  });
174
183
  });
184
+ // The static registry is finalized before Spring runtime enrichment. Merge
185
+ // runtime-confirmed Route nodes from the graph after the explicit
186
+ // springAutoConfiguration dependency has completed, so Actuator-only
187
+ // mappings participate in the same process-linking path.
188
+ const processRouteRegistry = new Map(routeRegistry);
189
+ ctx.graph.forEachNode((node) => {
190
+ if (node.label !== 'Route' ||
191
+ node.properties.runtimeSource !== 'spring-actuator' ||
192
+ node.properties.runtimeConfirmed !== true) {
193
+ return;
194
+ }
195
+ const url = typeof node.properties.name === 'string' ? node.properties.name : undefined;
196
+ const filePath = typeof node.properties.filePath === 'string' ? node.properties.filePath : undefined;
197
+ const method = typeof node.properties.method === 'string' ? node.properties.method : undefined;
198
+ if (url === undefined || filePath === undefined)
199
+ return;
200
+ const key = routeNodeKey(method, url);
201
+ if (!processRouteRegistry.has(key)) {
202
+ processRouteRegistry.set(key, {
203
+ filePath,
204
+ source: 'spring-actuator-runtime',
205
+ url,
206
+ ...(method === undefined ? {} : { method }),
207
+ });
208
+ }
209
+ });
175
210
  // Link Route and Tool nodes to Processes
176
- if (routeRegistry.size > 0 || toolDefs.length > 0) {
211
+ if (processRouteRegistry.size > 0 || toolDefs.length > 0) {
177
212
  // Two-tier route lookup, mirroring the tool tables 10 lines below.
178
213
  // Routes whose handler resolved key by `handlerSymbolId` (read from
179
214
  // the Route node's graph properties — routes.ts stamps it there) and
@@ -188,7 +223,7 @@ export const processesPhase = {
188
223
  // routes phase stamps on the Route node was never consulted.
189
224
  const routesByHandlerId = new Map();
190
225
  const routesWithoutHandlerByFile = new Map();
191
- for (const [, entry] of routeRegistry) {
226
+ for (const [, entry] of processRouteRegistry) {
192
227
  // Push the Route node identity (`routeNodeKey`), not the bare URL, so the
193
228
  // ENTRY_POINT_OF edge targets the same node id the routes phase created
194
229
  // (#2289: a same-URL GET/POST pair is two distinct Route nodes).
@@ -10,6 +10,79 @@
10
10
  * @output ScannedFile[], allPaths[], totalFiles
11
11
  */
12
12
  import { walkRepositoryPaths } from '../filesystem-walker.js';
13
+ import fs from 'node:fs/promises';
14
+ import path from 'node:path';
15
+ const SPRING_ACTUATOR_ENDPOINT_FILES = new Set([
16
+ 'mappings.json',
17
+ 'beans.json',
18
+ 'conditions.json',
19
+ 'configprops.json',
20
+ 'env.json',
21
+ ]);
22
+ async function canonicalPath(filePath) {
23
+ return fs.realpath(filePath).catch(() => path.resolve(filePath));
24
+ }
25
+ async function compileActuatorExclusions(repoPath, inputPaths) {
26
+ const repo = await canonicalPath(repoPath);
27
+ const lexicalRepo = path.resolve(repoPath);
28
+ const compiled = [];
29
+ const seen = new Set();
30
+ for (const inputPath of inputPaths) {
31
+ const lexicalInput = path.resolve(repoPath, inputPath);
32
+ const lexicalRelative = path.relative(lexicalRepo, lexicalInput);
33
+ const canonicalInput = await canonicalPath(lexicalInput);
34
+ const candidateInputs = new Set([canonicalInput]);
35
+ if (lexicalRelative === '' ||
36
+ (lexicalRelative !== '..' &&
37
+ !lexicalRelative.startsWith(`..${path.sep}`) &&
38
+ !path.isAbsolute(lexicalRelative))) {
39
+ // Preserve the configured in-repo alias as an exclusion even when its
40
+ // real target is outside the repository.
41
+ candidateInputs.add(path.resolve(repo, lexicalRelative));
42
+ }
43
+ for (const input of candidateInputs) {
44
+ const inputRelativeToRepo = path.relative(repo, input);
45
+ if (inputRelativeToRepo === '') {
46
+ if (seen.has(''))
47
+ continue;
48
+ seen.add('');
49
+ compiled.push({ kind: 'repo-root-endpoints' });
50
+ continue;
51
+ }
52
+ if (inputRelativeToRepo === '..' ||
53
+ inputRelativeToRepo.startsWith(`..${path.sep}`) ||
54
+ path.isAbsolute(inputRelativeToRepo)) {
55
+ continue;
56
+ }
57
+ if (seen.has(input))
58
+ continue;
59
+ seen.add(input);
60
+ compiled.push({ kind: 'dir', resolved: input });
61
+ }
62
+ }
63
+ return { repo, exclusions: compiled };
64
+ }
65
+ function matchesActuatorExclusion(canonicalRepoPath, filePath, exclusions) {
66
+ if (exclusions.length === 0)
67
+ return false;
68
+ const candidate = path.resolve(canonicalRepoPath, filePath);
69
+ for (const exclusion of exclusions) {
70
+ if (exclusion.kind === 'repo-root-endpoints') {
71
+ const candidateRelativeToRepo = path.relative(canonicalRepoPath, candidate);
72
+ if (path.dirname(candidateRelativeToRepo) === '.' &&
73
+ SPRING_ACTUATOR_ENDPOINT_FILES.has(path.basename(candidateRelativeToRepo).toLowerCase())) {
74
+ return true;
75
+ }
76
+ continue;
77
+ }
78
+ const relative = path.relative(exclusion.resolved, candidate);
79
+ if (relative === '' ||
80
+ (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))) {
81
+ return true;
82
+ }
83
+ }
84
+ return false;
85
+ }
13
86
  export const scanPhase = {
14
87
  name: 'scan',
15
88
  deps: [],
@@ -19,15 +92,20 @@ export const scanPhase = {
19
92
  percent: 0,
20
93
  message: 'Scanning repository...',
21
94
  });
95
+ const { repo: canonicalRepoPath, exclusions: actuatorExclusions } = await compileActuatorExclusions(ctx.repoPath, [
96
+ ...(ctx.options?.springActuatorPath === undefined ? [] : [ctx.options.springActuatorPath]),
97
+ ...(ctx.options?.springActuatorScanExclusions ?? []),
98
+ ]);
22
99
  let scannedFiles;
23
100
  try {
24
101
  scannedFiles = await walkRepositoryPaths(ctx.repoPath, (current, total, filePath) => {
25
102
  const scanProgress = Math.round((current / total) * 15);
103
+ const isRuntimeInput = matchesActuatorExclusion(canonicalRepoPath, filePath, actuatorExclusions);
26
104
  ctx.onProgress({
27
105
  phase: 'extracting',
28
106
  percent: scanProgress,
29
107
  message: 'Scanning repository...',
30
- detail: filePath,
108
+ ...(isRuntimeInput ? {} : { detail: filePath }),
31
109
  stats: {
32
110
  filesProcessed: current,
33
111
  totalFiles: total,
@@ -45,6 +123,9 @@ export const scanPhase = {
45
123
  }
46
124
  throw err;
47
125
  }
126
+ if (actuatorExclusions.length > 0) {
127
+ scannedFiles = scannedFiles.filter((file) => !matchesActuatorExclusion(canonicalRepoPath, file.path, actuatorExclusions));
128
+ }
48
129
  const totalFiles = scannedFiles.length;
49
130
  const allPaths = scannedFiles.map((f) => f.path);
50
131
  ctx.onProgress({
@@ -10,6 +10,7 @@
10
10
  * @reads META-INF/spring.factories and AutoConfiguration.imports
11
11
  * @writes Class nodes and DECLARES edges
12
12
  */
13
+ import { type SpringActuatorImportStats } from '../frameworks/spring/actuator-runtime.js';
13
14
  import type { PipelinePhase } from './types.js';
14
15
  export interface SpringAutoConfigurationEntry {
15
16
  readonly className: string;
@@ -24,6 +25,7 @@ export interface SpringAutoConfigurationOutput {
24
25
  readonly metadataFiles: number;
25
26
  readonly autoConfigurations: number;
26
27
  readonly ambiguousAutoConfigurations: number;
28
+ readonly actuatorRuntime?: SpringActuatorImportStats;
27
29
  }
28
30
  export declare function classifySpringAutoConfigurationMetadata(filePath: string): SpringAutoConfigurationMetadataFile | null;
29
31
  /** Parse Boot 2.7+/3.x one-class-per-line auto-configuration imports. */