gitnexus 1.6.10-rc.69 → 1.6.10-rc.70

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.
@@ -20,5 +20,11 @@ export interface SpringBeanCandidateAdapter {
20
20
  getClassAnnotationFacts(filePath: string): readonly ClassAnnotationFact[];
21
21
  isPackageVisibilityIncomplete(filePath: string): boolean;
22
22
  }
23
+ type RecognizedAnnotationNames = {
24
+ readonly has: (value: string) => boolean;
25
+ };
26
+ /** Build a scope-aware Spring annotation resolver shared by framework hooks. */
27
+ export declare function createSpringAnnotationNameResolver(indexes: ScopeResolutionIndexes): (rawName: string, parsed: ParsedFile, enclosingScope: ScopeId | null, recognizedAnnotations: RecognizedAnnotationNames, isPackageVisibilityIncomplete: boolean) => string | undefined;
23
28
  /** Build a language hook that enriches Class nodes after scope resolution. */
24
29
  export declare function createSpringBeanCandidateAttacher(adapter: SpringBeanCandidateAdapter): (graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], nodeLookup: GraphNodeLookup, indexes: ScopeResolutionIndexes) => void;
30
+ export {};
@@ -114,7 +114,7 @@ function hasVisibleTypeBinding(startScope, simpleName, indexes) {
114
114
  }
115
115
  return false;
116
116
  }
117
- function wildcardImportTarget(parsed, simpleName) {
117
+ function wildcardImportTarget(parsed, simpleName, recognizedAnnotations) {
118
118
  const wildcardPackages = new Set(parsed.parsedImports
119
119
  .filter((entry) => entry.kind === 'wildcard')
120
120
  .map((entry) => entry.targetRaw.replace(/\.\*$/, '')));
@@ -122,33 +122,37 @@ function wildcardImportTarget(parsed, simpleName) {
122
122
  return undefined;
123
123
  const [packageName] = wildcardPackages;
124
124
  const target = `${packageName}.${simpleName}`;
125
- return SPRING_BEAN_STEREOTYPES.has(target) ? target : undefined;
125
+ return recognizedAnnotations.has(target) ? target : undefined;
126
126
  }
127
- function resolveSpringAnnotation(rawName, parsed, enclosingScope, indexes, ownedTypeNamesByOwner, isPackageVisibilityIncomplete) {
128
- if (rawName.includes('.')) {
129
- return SPRING_BEAN_STEREOTYPES.has(rawName) ? rawName : undefined;
130
- }
131
- if (hasLexicalTypeDeclaration(enclosingScope, rawName, indexes))
132
- return undefined;
133
- if (hasInheritedTypeDeclaration(enclosingScope, rawName, indexes, ownedTypeNamesByOwner)) {
134
- return undefined;
135
- }
136
- const explicitImports = explicitImportTargets(parsed, rawName);
137
- if (explicitImports.size > 0) {
138
- if (explicitImports.size !== 1)
127
+ /** Build a scope-aware Spring annotation resolver shared by framework hooks. */
128
+ export function createSpringAnnotationNameResolver(indexes) {
129
+ const ownedTypeNamesByOwner = buildOwnedTypeNamesByOwner(indexes);
130
+ return (rawName, parsed, enclosingScope, recognizedAnnotations, isPackageVisibilityIncomplete) => {
131
+ if (rawName.includes('.')) {
132
+ return recognizedAnnotations.has(rawName) ? rawName : undefined;
133
+ }
134
+ if (hasLexicalTypeDeclaration(enclosingScope, rawName, indexes))
139
135
  return undefined;
140
- const [imported] = explicitImports;
141
- return SPRING_BEAN_STEREOTYPES.has(imported) ? imported : undefined;
142
- }
143
- const wildcardTarget = wildcardImportTarget(parsed, rawName);
144
- if (wildcardTarget === undefined || isPackageVisibilityIncomplete)
145
- return undefined;
146
- return hasVisibleTypeBinding(enclosingScope, rawName, indexes) ? undefined : wildcardTarget;
136
+ if (hasInheritedTypeDeclaration(enclosingScope, rawName, indexes, ownedTypeNamesByOwner)) {
137
+ return undefined;
138
+ }
139
+ const explicitImports = explicitImportTargets(parsed, rawName);
140
+ if (explicitImports.size > 0) {
141
+ if (explicitImports.size !== 1)
142
+ return undefined;
143
+ const [imported] = explicitImports;
144
+ return recognizedAnnotations.has(imported) ? imported : undefined;
145
+ }
146
+ const wildcardTarget = wildcardImportTarget(parsed, rawName, recognizedAnnotations);
147
+ if (wildcardTarget === undefined || isPackageVisibilityIncomplete)
148
+ return undefined;
149
+ return hasVisibleTypeBinding(enclosingScope, rawName, indexes) ? undefined : wildcardTarget;
150
+ };
147
151
  }
148
152
  /** Build a language hook that enriches Class nodes after scope resolution. */
149
153
  export function createSpringBeanCandidateAttacher(adapter) {
150
154
  return (graph, parsedFiles, nodeLookup, indexes) => {
151
- const ownedTypeNamesByOwner = buildOwnedTypeNamesByOwner(indexes);
155
+ const resolveSpringAnnotation = createSpringAnnotationNameResolver(indexes);
152
156
  for (const parsed of parsedFiles) {
153
157
  for (const fact of adapter.getClassAnnotationFacts(parsed.filePath)) {
154
158
  const classScope = indexes.scopeTree.getScope(fact.classScopeId);
@@ -165,7 +169,7 @@ export function createSpringBeanCandidateAttacher(adapter) {
165
169
  continue;
166
170
  const recognized = new Set();
167
171
  for (const rawName of fact.annotationNames) {
168
- const annotation = resolveSpringAnnotation(rawName, parsed, classScope.parent, indexes, ownedTypeNamesByOwner, adapter.isPackageVisibilityIncomplete(parsed.filePath));
172
+ const annotation = resolveSpringAnnotation(rawName, parsed, classScope.parent, SPRING_BEAN_STEREOTYPES, adapter.isPackageVisibilityIncomplete(parsed.filePath));
169
173
  if (annotation !== undefined)
170
174
  recognized.add(annotation);
171
175
  }
@@ -0,0 +1,24 @@
1
+ import type { KnowledgeGraph } from '../../../graph/types.js';
2
+ export declare const SPRING_CONFIG_DESCRIPTION = "Spring configuration property";
3
+ export interface SpringValueConsumer {
4
+ readonly kind: 'value';
5
+ readonly fieldName: string;
6
+ readonly line: number;
7
+ readonly keys: readonly string[];
8
+ }
9
+ export interface SpringConfigurationPropertiesConsumer {
10
+ readonly kind: 'configuration-properties';
11
+ readonly className: string;
12
+ readonly line: number;
13
+ readonly prefix: string;
14
+ }
15
+ export type SpringConfigConsumer = SpringValueConsumer | SpringConfigurationPropertiesConsumer;
16
+ export interface SpringConfigConsumerBatch {
17
+ readonly filePath: string;
18
+ readonly consumers: readonly SpringConfigConsumer[];
19
+ }
20
+ /**
21
+ * Attach normalized, language-provider-produced Spring consumers to config
22
+ * keys already present in the shared graph.
23
+ */
24
+ export declare function bindSpringConfigConsumers(graph: KnowledgeGraph, batches: readonly SpringConfigConsumerBatch[]): void;
@@ -0,0 +1,115 @@
1
+ import { generateId } from '../../../../lib/utils.js';
2
+ export const SPRING_CONFIG_DESCRIPTION = 'Spring configuration property';
3
+ function closestNode(candidates, filePath, name, line) {
4
+ return candidates
5
+ .filter((node) => node.properties.filePath === filePath && node.properties.name === name)
6
+ .sort((left, right) => Math.abs(Number(left.properties.startLine ?? 0) - line) -
7
+ Math.abs(Number(right.properties.startLine ?? 0) - line))[0];
8
+ }
9
+ function markUnresolved(node, key) {
10
+ const marker = `Spring config unresolved: ${key}`;
11
+ const existing = typeof node.properties.description === 'string' ? node.properties.description : '';
12
+ if (existing.includes(marker))
13
+ return;
14
+ node.properties.description = existing.length > 0 ? `${existing}; ${marker}` : marker;
15
+ }
16
+ function relaxedName(value) {
17
+ return value.toLowerCase().replace(/[-_.]/g, '');
18
+ }
19
+ function isSpringConfigNode(node) {
20
+ return (node.label === 'Property' &&
21
+ typeof node.properties.description === 'string' &&
22
+ node.properties.description.startsWith(SPRING_CONFIG_DESCRIPTION));
23
+ }
24
+ /**
25
+ * Attach normalized, language-provider-produced Spring consumers to config
26
+ * keys already present in the shared graph.
27
+ */
28
+ export function bindSpringConfigConsumers(graph, batches) {
29
+ if (batches.length === 0)
30
+ return;
31
+ const configNodes = [];
32
+ const propertyNodes = [];
33
+ const classNodes = [];
34
+ for (const node of graph.iterNodes()) {
35
+ if (isSpringConfigNode(node))
36
+ configNodes.push(node);
37
+ else if (node.label === 'Property')
38
+ propertyNodes.push(node);
39
+ else if (node.label === 'Class' || node.label === 'Record')
40
+ classNodes.push(node);
41
+ }
42
+ const keyNodes = new Map();
43
+ for (const node of configNodes) {
44
+ const key = String(node.properties.name);
45
+ const bucket = keyNodes.get(key) ?? [];
46
+ bucket.push(node);
47
+ keyNodes.set(key, bucket);
48
+ }
49
+ const propertiesByOwner = new Map();
50
+ for (const rel of graph.iterRelationshipsByType('HAS_PROPERTY')) {
51
+ const property = graph.getNode(rel.targetId);
52
+ if (property?.label !== 'Property' || isSpringConfigNode(property))
53
+ continue;
54
+ const members = propertiesByOwner.get(rel.sourceId) ?? [];
55
+ members.push(property);
56
+ propertiesByOwner.set(rel.sourceId, members);
57
+ }
58
+ const addBinding = (source, target, reason, confidence) => {
59
+ const edgeId = generateId('USES', `${source.id}->${target.id}:${reason}`);
60
+ graph.addRelationship({
61
+ id: edgeId,
62
+ sourceId: source.id,
63
+ targetId: target.id,
64
+ type: 'USES',
65
+ confidence,
66
+ reason,
67
+ });
68
+ };
69
+ for (const { filePath, consumers } of batches) {
70
+ for (const consumer of consumers) {
71
+ if (consumer.kind === 'value') {
72
+ const field = closestNode(propertyNodes, filePath, consumer.fieldName, consumer.line);
73
+ if (field === undefined)
74
+ continue;
75
+ for (const key of consumer.keys) {
76
+ const matches = keyNodes.get(key) ?? [];
77
+ if (matches.length === 0) {
78
+ markUnresolved(field, key);
79
+ continue;
80
+ }
81
+ for (const match of matches) {
82
+ addBinding(field, match, `spring-config:@Value ${key}`, 1);
83
+ }
84
+ }
85
+ continue;
86
+ }
87
+ const owner = closestNode(classNodes, filePath, consumer.className, consumer.line);
88
+ if (owner === undefined)
89
+ continue;
90
+ const prefix = `${consumer.prefix}.`;
91
+ const matches = configNodes.filter((node) => {
92
+ const key = String(node.properties.name);
93
+ return key === consumer.prefix || key.startsWith(prefix);
94
+ });
95
+ if (matches.length === 0) {
96
+ markUnresolved(owner, consumer.prefix);
97
+ continue;
98
+ }
99
+ for (const match of matches) {
100
+ addBinding(owner, match, `spring-config:@ConfigurationProperties ${consumer.prefix}`, 0.95);
101
+ }
102
+ for (const field of propertiesByOwner.get(owner.id) ?? []) {
103
+ const fieldName = relaxedName(String(field.properties.name));
104
+ for (const match of matches) {
105
+ const key = String(match.properties.name);
106
+ const suffix = key === consumer.prefix ? '' : key.slice(prefix.length);
107
+ const firstSegment = suffix.split(/[.\[]/, 1)[0];
108
+ if (firstSegment.length === 0 || relaxedName(firstSegment) !== fieldName)
109
+ continue;
110
+ addBinding(field, match, `spring-config:@ConfigurationProperties field ${consumer.prefix}`, 0.95);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,3 @@
1
+ import type { AnalysisFeatureDescriptor } from '../../../analysis-features.js';
2
+ /** Durable completeness contract for Java Spring configuration bindings. */
3
+ export declare const SPRING_CONFIG_BINDINGS_FEATURE: AnalysisFeatureDescriptor;
@@ -0,0 +1,13 @@
1
+ function isSpringApplicationConfig(filePath) {
2
+ const base = filePath.replaceAll('\\', '/').split('/').pop() ?? '';
3
+ return /^application(?:-[^.]+)?\.(?:properties|ya?ml)$/i.test(base);
4
+ }
5
+ /** Durable completeness contract for Java Spring configuration bindings. */
6
+ export const SPRING_CONFIG_BINDINGS_FEATURE = {
7
+ id: 'spring.config-bindings',
8
+ version: 1,
9
+ // Java sources need consumer extraction even without config files (missing
10
+ // placeholders still get unresolved markers). Config-only repositories also
11
+ // need a one-time rebuild to backfill language-agnostic Property nodes.
12
+ appliesTo: (filePaths) => filePaths.some((filePath) => filePath.toLowerCase().endsWith('.java') || isSpringApplicationConfig(filePath)),
13
+ };
@@ -1,16 +1,20 @@
1
1
  import type { ParsedFile } from '../../../../_shared/index.js';
2
2
  import { type ClassAnnotationFact } from '../../frameworks/spring/bean-candidates.js';
3
3
  import { type JvmPackageFact } from '../jvm/package-facts.js';
4
+ import type { JavaSpringConfigConsumerFact } from './spring-config-bindings.js';
4
5
  export type JavaClassAnnotationFact = ClassAnnotationFact;
5
6
  export interface JavaCaptureSideChannel {
6
7
  readonly kind: 'java';
7
8
  readonly packageFact: JvmPackageFact;
8
9
  readonly classAnnotations: readonly JavaClassAnnotationFact[];
10
+ readonly springConfigConsumers?: readonly JavaSpringConfigConsumerFact[];
9
11
  }
10
12
  /** Clear facts retained by a prior workspace pass in a long-lived process. */
11
13
  export declare function clearJavaClassAnnotationFacts(): void;
12
14
  /** Store the annotation syntax collected by Java's existing scope-query traversal. */
13
15
  export declare function setJavaClassAnnotationFacts(filePath: string, facts: readonly JavaClassAnnotationFact[]): void;
16
+ export declare function setJavaSpringConfigConsumerFacts(filePath: string, facts: readonly JavaSpringConfigConsumerFact[]): void;
17
+ export declare function getJavaSpringConfigConsumerFacts(filePath: string): readonly JavaSpringConfigConsumerFact[];
14
18
  /** Snapshot worker-local Java annotation facts for ParsedFile serialization. */
15
19
  export declare function collectJavaCaptureSideChannel(filePath: string): JavaCaptureSideChannel | undefined;
16
20
  export declare function getJavaClassAnnotationFacts(filePath: string): readonly JavaClassAnnotationFact[];
@@ -2,24 +2,38 @@ import { createClassAnnotationFactStore, } from '../../frameworks/spring/bean-ca
2
2
  import { isJvmPackageFact, UNKNOWN_JVM_PACKAGE_FACT, } from '../jvm/package-facts.js';
3
3
  import { getJavaPackageFact, setJavaPackageFact } from './package-facts.js';
4
4
  const classAnnotations = createClassAnnotationFactStore();
5
+ const springConfigConsumers = new Map();
5
6
  /** Clear facts retained by a prior workspace pass in a long-lived process. */
6
7
  export function clearJavaClassAnnotationFacts() {
7
8
  classAnnotations.clear();
9
+ springConfigConsumers.clear();
8
10
  }
9
11
  /** Store the annotation syntax collected by Java's existing scope-query traversal. */
10
12
  export function setJavaClassAnnotationFacts(filePath, facts) {
11
13
  classAnnotations.set(filePath, facts);
12
14
  }
15
+ export function setJavaSpringConfigConsumerFacts(filePath, facts) {
16
+ if (facts.length === 0)
17
+ springConfigConsumers.delete(filePath);
18
+ else
19
+ springConfigConsumers.set(filePath, facts);
20
+ }
21
+ export function getJavaSpringConfigConsumerFacts(filePath) {
22
+ return springConfigConsumers.get(filePath) ?? [];
23
+ }
13
24
  /** Snapshot worker-local Java annotation facts for ParsedFile serialization. */
14
25
  export function collectJavaCaptureSideChannel(filePath) {
15
26
  const facts = classAnnotations.get(filePath);
27
+ const configConsumers = springConfigConsumers.get(filePath) ?? [];
16
28
  const packageFact = getJavaPackageFact(filePath);
17
- if (facts.length === 0 && packageFact === undefined)
29
+ if (facts.length === 0 && configConsumers.length === 0 && packageFact === undefined) {
18
30
  return undefined;
31
+ }
19
32
  return {
20
33
  kind: 'java',
21
34
  packageFact: packageFact ?? UNKNOWN_JVM_PACKAGE_FACT,
22
35
  classAnnotations: facts,
36
+ ...(configConsumers.length > 0 ? { springConfigConsumers: configConsumers } : {}),
23
37
  };
24
38
  }
25
39
  export function getJavaClassAnnotationFacts(filePath) {
@@ -34,9 +48,11 @@ export function applyJavaCaptureSideChannel(parsed) {
34
48
  data.kind !== 'java' ||
35
49
  !Array.isArray(data.classAnnotations)) {
36
50
  setJavaClassAnnotationFacts(parsed.filePath, []);
51
+ setJavaSpringConfigConsumerFacts(parsed.filePath, []);
37
52
  setJavaPackageFact(parsed.filePath, UNKNOWN_JVM_PACKAGE_FACT);
38
53
  return;
39
54
  }
40
55
  setJavaClassAnnotationFacts(parsed.filePath, data.classAnnotations);
56
+ setJavaSpringConfigConsumerFacts(parsed.filePath, Array.isArray(data.springConfigConsumers) ? data.springConfigConsumers : []);
41
57
  setJavaPackageFact(parsed.filePath, isJvmPackageFact(data.packageFact) ? data.packageFact : UNKNOWN_JVM_PACKAGE_FACT);
42
58
  }
@@ -7,9 +7,10 @@ import { getJavaParser, getJavaScopeQuery } from './query.js';
7
7
  import { recordCacheHit, recordCacheMiss } from './cache-stats.js';
8
8
  import { getTreeSitterBufferSize } from '../../constants.js';
9
9
  import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
10
- import { setJavaClassAnnotationFacts } from './capture-side-channel.js';
10
+ import { setJavaClassAnnotationFacts, setJavaSpringConfigConsumerFacts, } from './capture-side-channel.js';
11
11
  import { captureJavaPackageFact } from './package-facts.js';
12
12
  import { synthesizeCallableFlowCaptures } from '../../utils/callable-flow-captures.js';
13
+ import { captureJavaSpringConfigConsumerFacts } from './spring-config-bindings.js';
13
14
  /** Declaration anchors that carry function-like arity metadata. */
14
15
  const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.constructor'];
15
16
  /** tree-sitter-java node types that the method extractor accepts. */
@@ -195,6 +196,7 @@ export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
195
196
  out.push(grouped);
196
197
  }
197
198
  setJavaClassAnnotationFacts(filePath, materializeClassAnnotationFacts(classAnnotations));
199
+ setJavaSpringConfigConsumerFacts(filePath, captureJavaSpringConfigConsumerFacts(tree.rootNode, filePath));
198
200
  return [
199
201
  ...resolveVarTypeBindings(out),
200
202
  ...synthesizeJavaInheritanceReferences(tree.rootNode),
@@ -14,6 +14,7 @@ import { javaProvider } from '../java.js';
14
14
  import { javaArityCompatibility, javaMergeBindings, resolveJavaImportTarget, } from './index.js';
15
15
  import { populateJavaPackageSiblings } from './package-siblings.js';
16
16
  import { attachSpringBeanCandidateMetadata } from './spring-bean-metadata.js';
17
+ import { attachJavaSpringConfigBindings } from './spring-config-bindings.js';
17
18
  import { applyJavaCaptureSideChannel, clearJavaClassAnnotationFacts, } from './capture-side-channel.js';
18
19
  import { clearJavaPackageFacts } from './package-facts.js';
19
20
  const javaScopeResolver = {
@@ -51,7 +52,10 @@ const javaScopeResolver = {
51
52
  freeCallsRequireInstanceOwnership: true,
52
53
  populateNamespaceSiblings: populateJavaPackageSiblings,
53
54
  populateRangeBindings: populateJavaCrossFileReturnTypes,
54
- emitPostResolutionEdges: attachSpringBeanCandidateMetadata,
55
+ emitPostResolutionEdges: (graph, parsedFiles, nodeLookup, indexes, ctx) => {
56
+ attachSpringBeanCandidateMetadata(graph, parsedFiles, nodeLookup, indexes);
57
+ attachJavaSpringConfigBindings(graph, parsedFiles, nodeLookup, indexes, ctx);
58
+ },
55
59
  };
56
60
  export { javaScopeResolver };
57
61
  function populateJavaCrossFileReturnTypes(parsedFiles, indexes) {
@@ -0,0 +1,23 @@
1
+ import type { KnowledgeGraph } from '../../../graph/types.js';
2
+ import type { GraphNodeLookup } from '../../scope-resolution/graph-bridge/node-lookup.js';
3
+ import type { ScopeResolutionIndexes } from '../../model/scope-resolution-indexes.js';
4
+ import { type ParsedFile, type ScopeId } from '../../../../_shared/index.js';
5
+ import { type SpringConfigConsumer } from '../../frameworks/spring/config-bindings.js';
6
+ import { type SyntaxNode } from '../../utils/ast-helpers.js';
7
+ export interface JavaSpringConfigConsumerFact {
8
+ readonly consumer: SpringConfigConsumer;
9
+ readonly annotationName: string;
10
+ readonly classScopeId: ScopeId;
11
+ }
12
+ /** Extract statically readable Spring placeholder keys from a Java annotation. */
13
+ export declare function parseValuePlaceholderKeys(annotation: SyntaxNode): string[];
14
+ /** Extract `prefix`/`value` (or the positional value) from the annotation. */
15
+ export declare function parseConfigurationPropertiesPrefix(annotation: SyntaxNode): string | null;
16
+ /** Collect config facts from the Java parser's existing AST (no reparse). */
17
+ export declare function captureJavaSpringConfigConsumerFacts(root: SyntaxNode, filePath: string): JavaSpringConfigConsumerFact[];
18
+ /** Parse Java consumers for focused unit tests; production reuses the worker AST. */
19
+ export declare function extractJavaSpringConfigConsumers(source: string): SpringConfigConsumer[];
20
+ /** Java ScopeResolver post-resolution hook for Spring configuration consumers. */
21
+ export declare function attachJavaSpringConfigBindings(graph: KnowledgeGraph, parsedFiles: readonly ParsedFile[], _nodeLookup: GraphNodeLookup, indexes: ScopeResolutionIndexes, _ctx: {
22
+ readonly fileContents: ReadonlyMap<string, string>;
23
+ }): void;
@@ -0,0 +1,215 @@
1
+ import { makeScopeId } from '../../../../_shared/index.js';
2
+ import { bindSpringConfigConsumers, } from '../../frameworks/spring/config-bindings.js';
3
+ import { createSpringAnnotationNameResolver } from '../../frameworks/spring/bean-candidates.js';
4
+ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
5
+ import { nodeToCapture } from '../../utils/ast-helpers.js';
6
+ import { getJavaParser } from './query.js';
7
+ import { getJavaSpringConfigConsumerFacts } from './capture-side-channel.js';
8
+ import { isJavaPackageSiblingVisibilityIncomplete } from './package-siblings.js';
9
+ const VALUE_ANNOTATION = 'org.springframework.beans.factory.annotation.Value';
10
+ const CONFIGURATION_PROPERTIES_ANNOTATION = 'org.springframework.boot.context.properties.ConfigurationProperties';
11
+ function collectJavaImports(root) {
12
+ const exact = new Set();
13
+ const wildcard = new Set();
14
+ const localTypes = new Set();
15
+ for (const node of root.descendantsOfType('import_declaration')) {
16
+ const imported = node.text
17
+ .replace(/^\s*import\s+(?:static\s+)?/, '')
18
+ .replace(/;\s*$/, '')
19
+ .trim();
20
+ if (imported.endsWith('.*'))
21
+ wildcard.add(imported.slice(0, -2));
22
+ else
23
+ exact.add(imported);
24
+ }
25
+ for (const type of [
26
+ 'class_declaration',
27
+ 'interface_declaration',
28
+ 'enum_declaration',
29
+ 'record_declaration',
30
+ 'annotation_type_declaration',
31
+ ]) {
32
+ for (const node of root.descendantsOfType(type)) {
33
+ const name = node.childForFieldName('name')?.text;
34
+ if (name)
35
+ localTypes.add(name);
36
+ }
37
+ }
38
+ return { exact, wildcard, localTypes };
39
+ }
40
+ function annotationsOn(node) {
41
+ const modifiers = node.namedChildren.find((child) => child.type === 'modifiers');
42
+ if (modifiers === undefined)
43
+ return [];
44
+ const annotations = [];
45
+ for (const child of modifiers.namedChildren) {
46
+ if (child.type !== 'annotation' && child.type !== 'marker_annotation')
47
+ continue;
48
+ const name = child.childForFieldName('name')?.text ?? child.firstNamedChild?.text;
49
+ if (name)
50
+ annotations.push({ name, node: child });
51
+ }
52
+ return annotations;
53
+ }
54
+ function resolvesToAnnotation(rawName, canonicalName, imports) {
55
+ if (rawName.includes('.'))
56
+ return rawName === canonicalName;
57
+ if (imports.localTypes.has(rawName))
58
+ return false;
59
+ if (imports.exact.has(canonicalName))
60
+ return true;
61
+ const packageName = canonicalName.slice(0, canonicalName.lastIndexOf('.'));
62
+ return imports.wildcard.has(packageName);
63
+ }
64
+ function decodeJavaStringLiteral(literal) {
65
+ const delimiterLength = literal.startsWith('"""') && literal.endsWith('"""') ? 3 : 1;
66
+ return literal
67
+ .slice(delimiterLength, -delimiterLength)
68
+ .replace(/\\u([0-9a-fA-F]{4})/g, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
69
+ .replace(/\\(["'\\btnfr])/g, (_match, escaped) => {
70
+ const controls = {
71
+ b: '\b',
72
+ t: '\t',
73
+ n: '\n',
74
+ f: '\f',
75
+ r: '\r',
76
+ };
77
+ return controls[escaped] ?? escaped;
78
+ });
79
+ }
80
+ function javaStringLiterals(annotation) {
81
+ return annotation
82
+ .descendantsOfType('string_literal')
83
+ .map((literal) => decodeJavaStringLiteral(literal.text));
84
+ }
85
+ /** Extract statically readable Spring placeholder keys from a Java annotation. */
86
+ export function parseValuePlaceholderKeys(annotation) {
87
+ const keys = new Set();
88
+ for (const literal of javaStringLiterals(annotation)) {
89
+ for (const match of literal.matchAll(/\$\{([^{}]+)\}/g)) {
90
+ const key = match[1].split(':', 1)[0].trim();
91
+ if (/^[A-Za-z0-9_.-]+$/.test(key))
92
+ keys.add(key);
93
+ }
94
+ }
95
+ return [...keys];
96
+ }
97
+ /** Extract `prefix`/`value` (or the positional value) from the annotation. */
98
+ export function parseConfigurationPropertiesPrefix(annotation) {
99
+ const named = annotation.descendantsOfType('element_value_pair').find((pair) => {
100
+ const key = pair.childForFieldName('key')?.text;
101
+ return key === 'prefix' || key === 'value';
102
+ });
103
+ const namedValue = named?.childForFieldName('value');
104
+ const argumentsNode = annotation.childForFieldName('arguments');
105
+ const literalNode = (namedValue?.type === 'string_literal'
106
+ ? namedValue
107
+ : namedValue?.descendantsOfType('string_literal')[0]) ??
108
+ (named === undefined
109
+ ? argumentsNode?.namedChildren.find((child) => child.type === 'string_literal')
110
+ : undefined);
111
+ if (literalNode === undefined)
112
+ return null;
113
+ const prefix = decodeJavaStringLiteral(literalNode.text)
114
+ .trim()
115
+ .replace(/^\.+|\.+$/g, '');
116
+ return /^[A-Za-z0-9_.-]+$/.test(prefix) ? prefix : null;
117
+ }
118
+ function classScopeId(filePath, declaration) {
119
+ return makeScopeId({
120
+ filePath,
121
+ range: nodeToCapture('@scope.class', declaration).range,
122
+ kind: 'Class',
123
+ });
124
+ }
125
+ function enclosingClass(node) {
126
+ let current = node.parent;
127
+ while (current !== null) {
128
+ if (current.type === 'class_declaration' || current.type === 'record_declaration') {
129
+ return current;
130
+ }
131
+ current = current.parent;
132
+ }
133
+ return undefined;
134
+ }
135
+ /** Collect config facts from the Java parser's existing AST (no reparse). */
136
+ export function captureJavaSpringConfigConsumerFacts(root, filePath) {
137
+ const imports = collectJavaImports(root);
138
+ const facts = [];
139
+ for (const field of root.descendantsOfType('field_declaration')) {
140
+ const annotations = annotationsOn(field).filter((annotation) => resolvesToAnnotation(annotation.name, VALUE_ANNOTATION, imports));
141
+ if (annotations.length === 0)
142
+ continue;
143
+ const owner = enclosingClass(field);
144
+ if (owner === undefined)
145
+ continue;
146
+ for (const declarator of field.namedChildren.filter((child) => child.type === 'variable_declarator')) {
147
+ const fieldName = declarator.childForFieldName('name')?.text;
148
+ if (!fieldName)
149
+ continue;
150
+ for (const annotation of annotations) {
151
+ const keys = parseValuePlaceholderKeys(annotation.node);
152
+ if (keys.length > 0) {
153
+ facts.push({
154
+ consumer: { kind: 'value', fieldName, line: field.startPosition.row + 1, keys },
155
+ annotationName: annotation.name,
156
+ classScopeId: classScopeId(filePath, owner),
157
+ });
158
+ }
159
+ }
160
+ }
161
+ }
162
+ for (const type of ['class_declaration', 'record_declaration']) {
163
+ for (const declaration of root.descendantsOfType(type)) {
164
+ const className = declaration.childForFieldName('name')?.text;
165
+ if (!className)
166
+ continue;
167
+ for (const annotation of annotationsOn(declaration)) {
168
+ if (!resolvesToAnnotation(annotation.name, CONFIGURATION_PROPERTIES_ANNOTATION, imports)) {
169
+ continue;
170
+ }
171
+ const prefix = parseConfigurationPropertiesPrefix(annotation.node);
172
+ if (prefix !== null) {
173
+ facts.push({
174
+ consumer: {
175
+ kind: 'configuration-properties',
176
+ className,
177
+ line: declaration.startPosition.row + 1,
178
+ prefix,
179
+ },
180
+ annotationName: annotation.name,
181
+ classScopeId: classScopeId(filePath, declaration),
182
+ });
183
+ }
184
+ }
185
+ }
186
+ }
187
+ return facts;
188
+ }
189
+ /** Parse Java consumers for focused unit tests; production reuses the worker AST. */
190
+ export function extractJavaSpringConfigConsumers(source) {
191
+ const tree = parseSourceSafe(getJavaParser(), source);
192
+ return captureJavaSpringConfigConsumerFacts(tree.rootNode, '<memory>').map((fact) => fact.consumer);
193
+ }
194
+ /** Java ScopeResolver post-resolution hook for Spring configuration consumers. */
195
+ export function attachJavaSpringConfigBindings(graph, parsedFiles, _nodeLookup, indexes, _ctx) {
196
+ const resolveAnnotation = createSpringAnnotationNameResolver(indexes);
197
+ const recognizedAnnotations = new Set([VALUE_ANNOTATION, CONFIGURATION_PROPERTIES_ANNOTATION]);
198
+ const batches = [];
199
+ for (const parsed of parsedFiles) {
200
+ const consumers = [];
201
+ for (const fact of getJavaSpringConfigConsumerFacts(parsed.filePath)) {
202
+ const classScope = indexes.scopeTree.getScope(fact.classScopeId);
203
+ if (classScope === undefined || classScope.kind !== 'Class')
204
+ continue;
205
+ const expectedAnnotation = fact.consumer.kind === 'value' ? VALUE_ANNOTATION : CONFIGURATION_PROPERTIES_ANNOTATION;
206
+ const enclosingScope = fact.consumer.kind === 'value' ? classScope.id : classScope.parent;
207
+ const resolved = resolveAnnotation(fact.annotationName, parsed, enclosingScope, recognizedAnnotations, isJavaPackageSiblingVisibilityIncomplete(parsed.filePath));
208
+ if (resolved === expectedAnnotation)
209
+ consumers.push(fact.consumer);
210
+ }
211
+ if (consumers.length > 0)
212
+ batches.push({ filePath: parsed.filePath, consumers });
213
+ }
214
+ bindSpringConfigConsumers(graph, batches);
215
+ }
@@ -14,6 +14,7 @@ export { toolsPhase, type ToolsOutput, type ToolDef } from './tools.js';
14
14
  export { ormPhase, type ORMOutput } from './orm.js';
15
15
  export { crossFilePhase, type CrossFileOutput } from './cross-file.js';
16
16
  export { scopeResolutionPhase, type ScopeResolutionOutput, } from '../scope-resolution/pipeline/phase.js';
17
+ export { springConfigPhase, type SpringConfigOutput } from './spring-config.js';
17
18
  export { pruneLocalSymbolsPhase, type PruneLocalSymbolsOutput } from './prune-local-symbols.js';
18
19
  export { taintSummariesPhase, type TaintSummariesOutput } from './taint-summaries.js';
19
20
  export { callSummariesPhase, type CallSummariesOutput } from './call-summaries.js';
@@ -15,6 +15,7 @@ export { toolsPhase } from './tools.js';
15
15
  export { ormPhase } from './orm.js';
16
16
  export { crossFilePhase } from './cross-file.js';
17
17
  export { scopeResolutionPhase, } from '../scope-resolution/pipeline/phase.js';
18
+ export { springConfigPhase } from './spring-config.js';
18
19
  export { pruneLocalSymbolsPhase } from './prune-local-symbols.js';
19
20
  export { taintSummariesPhase } from './taint-summaries.js';
20
21
  export { callSummariesPhase } from './call-summaries.js';
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Phase: springConfig
3
+ *
4
+ * Adds key-only nodes for statically readable Spring
5
+ * `application*.properties` / `application*.yml` / `application*.yaml` files.
6
+ * Language-specific ScopeResolver hooks attach consumers later. Configuration
7
+ * values are deliberately never copied into the graph because they may contain
8
+ * credentials and key identity is sufficient for impact analysis.
9
+ *
10
+ * @deps structure
11
+ * @reads Spring application configuration files
12
+ * @writes Property nodes and DEFINES edges
13
+ */
14
+ import type { PipelinePhase } from './types.js';
15
+ export interface SpringConfigKey {
16
+ readonly key: string;
17
+ readonly filePath: string;
18
+ readonly line: number;
19
+ readonly profile?: string;
20
+ readonly format: 'properties' | 'yaml';
21
+ }
22
+ interface SpringConfigFile {
23
+ readonly filePath: string;
24
+ readonly profile?: string;
25
+ readonly format: SpringConfigKey['format'];
26
+ }
27
+ export interface SpringConfigOutput {
28
+ readonly configKeys: number;
29
+ }
30
+ /** Match only Spring Boot's conventional application config file names. */
31
+ export declare function classifySpringConfigFile(filePath: string): SpringConfigFile | null;
32
+ /** Parse `.properties` keys without retaining their values. */
33
+ export declare function parseSpringProperties(content: string, filePath: string, profile?: string): SpringConfigKey[];
34
+ /** Parse and flatten YAML leaves without retaining their values. */
35
+ export declare function parseSpringYaml(content: string, filePath: string, profile?: string): SpringConfigKey[];
36
+ export declare const springConfigPhase: PipelinePhase<SpringConfigOutput>;
37
+ export {};
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Phase: springConfig
3
+ *
4
+ * Adds key-only nodes for statically readable Spring
5
+ * `application*.properties` / `application*.yml` / `application*.yaml` files.
6
+ * Language-specific ScopeResolver hooks attach consumers later. Configuration
7
+ * values are deliberately never copied into the graph because they may contain
8
+ * credentials and key identity is sufficient for impact analysis.
9
+ *
10
+ * @deps structure
11
+ * @reads Spring application configuration files
12
+ * @writes Property nodes and DEFINES edges
13
+ */
14
+ import fs from 'node:fs/promises';
15
+ import path from 'node:path';
16
+ import { createRequire } from 'node:module';
17
+ import { SPRING_CONFIG_DESCRIPTION } from '../frameworks/spring/config-bindings.js';
18
+ import { generateId } from '../../../lib/utils.js';
19
+ import { getPhaseOutput } from './types.js';
20
+ const require = createRequire(import.meta.url);
21
+ const yaml = require('js-yaml');
22
+ const MAX_CONFIG_FILE_BYTES = 2 * 1024 * 1024;
23
+ const MAX_YAML_TRAVERSAL_DEPTH = 128;
24
+ const MAX_YAML_TRAVERSAL_NODES = 100_000;
25
+ /** Match only Spring Boot's conventional application config file names. */
26
+ export function classifySpringConfigFile(filePath) {
27
+ const base = path.posix.basename(filePath.replaceAll('\\', '/'));
28
+ const match = /^application(?:-([^.]+))?\.(properties|ya?ml)$/i.exec(base);
29
+ if (match === null)
30
+ return null;
31
+ return {
32
+ filePath,
33
+ ...(match[1] ? { profile: match[1] } : {}),
34
+ format: match[2].toLowerCase() === 'properties' ? 'properties' : 'yaml',
35
+ };
36
+ }
37
+ function unescapePropertyKey(raw) {
38
+ return raw
39
+ .replace(/\\u([0-9a-fA-F]{4})/g, (_match, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
40
+ .replace(/\\([:=#!\\ ])/g, '$1');
41
+ }
42
+ function logicalPropertiesLines(content) {
43
+ const physical = content.split(/\r?\n/);
44
+ const logical = [];
45
+ let current = '';
46
+ let startLine = 1;
47
+ for (let index = 0; index < physical.length; index++) {
48
+ const line = physical[index];
49
+ if (current.length === 0)
50
+ startLine = index + 1;
51
+ current += current.length === 0 ? line : line.trimStart();
52
+ let trailingBackslashes = 0;
53
+ for (let cursor = current.length - 1; cursor >= 0 && current[cursor] === '\\'; cursor--) {
54
+ trailingBackslashes++;
55
+ }
56
+ if (trailingBackslashes % 2 === 1) {
57
+ current = current.slice(0, -1);
58
+ continue;
59
+ }
60
+ logical.push({ text: current, line: startLine });
61
+ current = '';
62
+ }
63
+ if (current.length > 0)
64
+ logical.push({ text: current, line: startLine });
65
+ return logical;
66
+ }
67
+ /** Parse `.properties` keys without retaining their values. */
68
+ export function parseSpringProperties(content, filePath, profile) {
69
+ const keys = [];
70
+ const seen = new Set();
71
+ for (const logical of logicalPropertiesLines(content)) {
72
+ const trimmed = logical.text.trimStart();
73
+ if (trimmed.length === 0 || trimmed.startsWith('#') || trimmed.startsWith('!'))
74
+ continue;
75
+ let separator = -1;
76
+ let escaped = false;
77
+ for (let index = 0; index < trimmed.length; index++) {
78
+ const char = trimmed[index];
79
+ if (!escaped && (char === '=' || char === ':' || /\s/.test(char))) {
80
+ separator = index;
81
+ break;
82
+ }
83
+ escaped = !escaped && char === '\\';
84
+ if (char !== '\\')
85
+ escaped = false;
86
+ }
87
+ const rawKey = (separator === -1 ? trimmed : trimmed.slice(0, separator)).trim();
88
+ const key = unescapePropertyKey(rawKey);
89
+ if (key.length === 0 || seen.has(key))
90
+ continue;
91
+ seen.add(key);
92
+ keys.push({
93
+ key,
94
+ filePath,
95
+ line: logical.line,
96
+ ...(profile ? { profile } : {}),
97
+ format: 'properties',
98
+ });
99
+ }
100
+ return keys;
101
+ }
102
+ function consumeYamlTraversalBudget(state, depth) {
103
+ if (depth > MAX_YAML_TRAVERSAL_DEPTH) {
104
+ throw new Error(`Spring YAML traversal depth exceeds ${MAX_YAML_TRAVERSAL_DEPTH}`);
105
+ }
106
+ state.remainingNodes--;
107
+ if (state.remainingNodes < 0) {
108
+ throw new Error(`Spring YAML traversal exceeds ${MAX_YAML_TRAVERSAL_NODES} nodes`);
109
+ }
110
+ }
111
+ function isObjectValue(value) {
112
+ return value !== null && typeof value === 'object';
113
+ }
114
+ function resolveYamlAliasEvent(event, objectEvents) {
115
+ if (event?.kind !== null || !isObjectValue(event.result))
116
+ return event;
117
+ return objectEvents.get(event.result) ?? event;
118
+ }
119
+ function yamlMappingPairs(event) {
120
+ const pairs = [];
121
+ for (let index = 0; index + 1 < event.children.length; index += 2) {
122
+ const keyEvent = event.children[index];
123
+ const valueEvent = event.children[index + 1];
124
+ if (keyEvent.kind !== 'scalar')
125
+ continue;
126
+ pairs.push({ key: String(keyEvent.result), keyEvent, valueEvent });
127
+ }
128
+ return pairs;
129
+ }
130
+ function findYamlMappingLocation(event, key, objectEvents, traversal, visited = new Set(), depth = 0) {
131
+ consumeYamlTraversalBudget(traversal, depth);
132
+ const resolved = resolveYamlAliasEvent(event, objectEvents);
133
+ if (resolved === undefined || visited.has(resolved))
134
+ return undefined;
135
+ visited.add(resolved);
136
+ if (resolved.kind === 'sequence') {
137
+ for (const child of resolved.children) {
138
+ const found = findYamlMappingLocation(child, key, objectEvents, traversal, visited, depth + 1);
139
+ if (found !== undefined)
140
+ return found;
141
+ }
142
+ return undefined;
143
+ }
144
+ if (resolved.kind !== 'mapping')
145
+ return undefined;
146
+ const pairs = yamlMappingPairs(resolved);
147
+ const direct = pairs.find((pair) => pair.key === key);
148
+ if (direct !== undefined) {
149
+ return { valueEvent: direct.valueEvent, line: direct.keyEvent.startLine };
150
+ }
151
+ for (const merge of pairs.filter((pair) => pair.key === '<<')) {
152
+ const found = findYamlMappingLocation(merge.valueEvent, key, objectEvents, traversal, visited, depth + 1);
153
+ if (found !== undefined)
154
+ return found;
155
+ }
156
+ return undefined;
157
+ }
158
+ function flattenYamlValue(value, event, prefix, out, objectEvents, traversal, sourceLine = event?.startLine ?? 1, depth = 0) {
159
+ consumeYamlTraversalBudget(traversal, depth);
160
+ const resolvedEvent = resolveYamlAliasEvent(event, objectEvents);
161
+ const trackedObject = isObjectValue(value) ? value : undefined;
162
+ if (trackedObject !== undefined && traversal.activeObjects.has(trackedObject))
163
+ return;
164
+ if (trackedObject !== undefined)
165
+ traversal.activeObjects.add(trackedObject);
166
+ try {
167
+ if (Array.isArray(value)) {
168
+ if (value.length === 0 && prefix.length > 0 && !out.has(prefix))
169
+ out.set(prefix, sourceLine);
170
+ value.forEach((item, index) => flattenYamlValue(item, resolvedEvent?.children[index], `${prefix}[${index}]`, out, objectEvents, traversal, sourceLine, depth + 1));
171
+ return;
172
+ }
173
+ if (value !== null &&
174
+ typeof value === 'object' &&
175
+ (resolvedEvent?.kind === 'mapping' || resolvedEvent === undefined)) {
176
+ const entries = Object.entries(value);
177
+ if (entries.length === 0 && prefix.length > 0 && !out.has(prefix))
178
+ out.set(prefix, sourceLine);
179
+ for (const [key, nested] of entries) {
180
+ const next = prefix.length === 0 ? key : `${prefix}.${key}`;
181
+ const location = findYamlMappingLocation(resolvedEvent, key, objectEvents, traversal);
182
+ flattenYamlValue(nested, location?.valueEvent, next, out, objectEvents, traversal, location?.line ?? sourceLine, depth + 1);
183
+ }
184
+ return;
185
+ }
186
+ if (prefix.length > 0 && !out.has(prefix))
187
+ out.set(prefix, sourceLine);
188
+ }
189
+ finally {
190
+ if (trackedObject !== undefined)
191
+ traversal.activeObjects.delete(trackedObject);
192
+ }
193
+ }
194
+ /** Parse and flatten YAML leaves without retaining their values. */
195
+ export function parseSpringYaml(content, filePath, profile) {
196
+ const flattened = new Map();
197
+ const eventStack = [];
198
+ const documentEvents = [];
199
+ const objectEvents = new WeakMap();
200
+ const documents = [];
201
+ const traversal = {
202
+ remainingNodes: MAX_YAML_TRAVERSAL_NODES,
203
+ activeObjects: new Set(),
204
+ };
205
+ yaml.loadAll(content, (document) => documents.push(document), {
206
+ schema: yaml.DEFAULT_SCHEMA,
207
+ json: true,
208
+ listener: (eventType, state) => {
209
+ if (eventType === 'open') {
210
+ eventStack.push({
211
+ startLine: state.line + 1,
212
+ kind: null,
213
+ result: undefined,
214
+ tag: null,
215
+ children: [],
216
+ });
217
+ return;
218
+ }
219
+ const event = eventStack.pop();
220
+ if (event === undefined)
221
+ return;
222
+ event.kind = state.kind ?? null;
223
+ event.result = state.result;
224
+ event.tag = state.tag ?? null;
225
+ if (isObjectValue(event.result) && event.kind !== null) {
226
+ objectEvents.set(event.result, event);
227
+ }
228
+ const parent = eventStack[eventStack.length - 1];
229
+ if (parent === undefined)
230
+ documentEvents.push(event);
231
+ else
232
+ parent.children.push(event);
233
+ },
234
+ });
235
+ documents.forEach((document, index) => flattenYamlValue(document, documentEvents[index], '', flattened, objectEvents, traversal));
236
+ return [...flattened.entries()]
237
+ .sort(([left], [right]) => left.localeCompare(right))
238
+ .map(([key, line]) => ({
239
+ key,
240
+ filePath,
241
+ line,
242
+ ...(profile ? { profile } : {}),
243
+ format: 'yaml',
244
+ }));
245
+ }
246
+ function configKeyNodeId(entry) {
247
+ return generateId('Property', `spring-config:${entry.filePath}:${entry.key}`);
248
+ }
249
+ async function readConfigKeys(repoPath, scannedFiles) {
250
+ const keys = [];
251
+ for (const scanned of scannedFiles) {
252
+ const classified = classifySpringConfigFile(scanned.path);
253
+ if (classified === null || scanned.size > MAX_CONFIG_FILE_BYTES)
254
+ continue;
255
+ try {
256
+ const content = await fs.readFile(path.join(repoPath, scanned.path), 'utf8');
257
+ keys.push(...(classified.format === 'properties'
258
+ ? parseSpringProperties(content, classified.filePath, classified.profile)
259
+ : parseSpringYaml(content, classified.filePath, classified.profile)));
260
+ }
261
+ catch {
262
+ // Malformed configuration is not a reason to fail the entire code index.
263
+ // Fail closed: no keys and therefore no misleading bindings for this file.
264
+ }
265
+ }
266
+ return keys;
267
+ }
268
+ export const springConfigPhase = {
269
+ name: 'springConfig',
270
+ deps: ['structure'],
271
+ async execute(ctx, deps) {
272
+ const { scannedFiles } = getPhaseOutput(deps, 'structure');
273
+ const configKeys = await readConfigKeys(ctx.repoPath, scannedFiles);
274
+ for (const entry of configKeys) {
275
+ const nodeId = configKeyNodeId(entry);
276
+ ctx.graph.addNode({
277
+ id: nodeId,
278
+ label: 'Property',
279
+ properties: {
280
+ name: entry.key,
281
+ filePath: entry.filePath,
282
+ startLine: entry.line,
283
+ endLine: entry.line,
284
+ description: entry.profile
285
+ ? `${SPRING_CONFIG_DESCRIPTION} (profile: ${entry.profile})`
286
+ : SPRING_CONFIG_DESCRIPTION,
287
+ },
288
+ });
289
+ const fileId = generateId('File', entry.filePath);
290
+ if (ctx.graph.getNode(fileId) !== undefined) {
291
+ ctx.graph.addRelationship({
292
+ id: generateId('DEFINES', `${fileId}->${nodeId}`),
293
+ sourceId: fileId,
294
+ targetId: nodeId,
295
+ type: 'DEFINES',
296
+ confidence: 1,
297
+ reason: 'spring-config:key',
298
+ });
299
+ }
300
+ }
301
+ return { configKeys: configKeys.length };
302
+ },
303
+ };
@@ -201,7 +201,7 @@ export interface PipelineOptions {
201
201
  *
202
202
  * Phase dependency graph:
203
203
  *
204
- * scan → structure → [markdown, cobol] → parse → [routes, tools, orm]
204
+ * scan → structure → [springConfig, markdown, cobol] → parse → [routes, tools, orm]
205
205
  * → crossFile → scopeResolution → pruneLocalSymbols
206
206
  * → mro → di → communities → processes
207
207
  *
@@ -15,14 +15,14 @@
15
15
  * See ARCHITECTURE.md for the full phase dependency diagram.
16
16
  */
17
17
  import { createKnowledgeGraph } from '../graph/graph.js';
18
- import { runPipeline, getPhaseOutput, scanPhase, structurePhase, markdownPhase, cobolPhase, parsePhase, routesPhase, toolsPhase, ormPhase, crossFilePhase, scopeResolutionPhase, pruneLocalSymbolsPhase, taintSummariesPhase, callSummariesPhase, mroPhase, diPhase, communitiesPhase, processesPhase, PhaseRegistry, } from './pipeline-phases/index.js';
18
+ import { runPipeline, getPhaseOutput, scanPhase, structurePhase, markdownPhase, cobolPhase, parsePhase, routesPhase, toolsPhase, ormPhase, crossFilePhase, scopeResolutionPhase, springConfigPhase, pruneLocalSymbolsPhase, taintSummariesPhase, callSummariesPhase, mroPhase, diPhase, communitiesPhase, processesPhase, PhaseRegistry, } from './pipeline-phases/index.js';
19
19
  // ── Phase registry ─────────────────────────────────────────────────────────
20
20
  /**
21
21
  * All pipeline phases with their dependency relationships.
22
22
  *
23
23
  * Phase dependency graph:
24
24
  *
25
- * scan → structure → [markdown, cobol] → parse → [routes, tools, orm]
25
+ * scan → structure → [springConfig, markdown, cobol] → parse → [routes, tools, orm]
26
26
  * → crossFile → scopeResolution → pruneLocalSymbols
27
27
  * → mro → di → communities → processes
28
28
  *
@@ -40,6 +40,7 @@ export function buildPhaseList(options) {
40
40
  return (new PhaseRegistry()
41
41
  .register(scanPhase)
42
42
  .register(structurePhase)
43
+ .register(springConfigPhase)
43
44
  .register(markdownPhase)
44
45
  .register(cobolPhase)
45
46
  .register(parsePhase)
@@ -43,7 +43,7 @@ export declare const ROUTE_SCHEMA = "\nCREATE NODE TABLE Route (\n id STRING,\n
43
43
  export declare const TOOL_SCHEMA = "\nCREATE NODE TABLE Tool (\n id STRING,\n name STRING,\n filePath STRING,\n description STRING,\n PRIMARY KEY (id)\n)";
44
44
  export declare const SECTION_SCHEMA = "\nCREATE NODE TABLE Section (\n id STRING,\n name STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n level INT64,\n content STRING,\n description STRING,\n PRIMARY KEY (id)\n)";
45
45
  export declare const BASICBLOCK_SCHEMA = "\nCREATE NODE TABLE BasicBlock (\n id STRING,\n filePath STRING,\n startLine INT64,\n endLine INT64,\n text STRING,\n callees STRING,\n calleeIds STRING,\n PRIMARY KEY (id)\n)";
46
- export declare const RELATION_SCHEMA = "\nCREATE REL TABLE CodeRelation (\n FROM File TO File,\n FROM File TO Folder,\n FROM File TO Function,\n FROM File TO Class,\n FROM File TO Interface,\n FROM File TO Method,\n FROM File TO CodeElement,\n FROM File TO `Struct`,\n FROM File TO `Enum`,\n FROM File TO `Macro`,\n FROM File TO `Typedef`,\n FROM File TO `Union`,\n FROM File TO `Namespace`,\n FROM File TO `Trait`,\n FROM File TO `Impl`,\n FROM File TO `TypeAlias`,\n FROM File TO `Const`,\n FROM File TO `Static`,\n FROM File TO `Variable`,\n FROM File TO `Property`,\n FROM File TO `Record`,\n FROM File TO `Delegate`,\n FROM File TO `Annotation`,\n FROM File TO `Constructor`,\n FROM File TO `Template`,\n FROM File TO `Module`,\n FROM File TO Section,\n FROM Folder TO Folder,\n FROM Folder TO File,\n FROM Function TO Function,\n FROM Function TO Method,\n FROM Function TO Class,\n FROM Function TO Community,\n FROM Function TO `Macro`,\n FROM Function TO `Struct`,\n FROM Function TO `Template`,\n FROM Function TO `Enum`,\n FROM Function TO `Namespace`,\n FROM Function TO `TypeAlias`,\n FROM Function TO `Module`,\n FROM Function TO `Impl`,\n FROM Function TO Interface,\n FROM Function TO `Constructor`,\n FROM Function TO `Const`,\n FROM Function TO `Typedef`,\n FROM Function TO `Union`,\n FROM Function TO `Property`,\n FROM Function TO CodeElement,\n FROM Class TO Method,\n FROM Class TO Function,\n FROM Class TO Class,\n FROM Class TO Interface,\n FROM Class TO Community,\n FROM Class TO `Template`,\n FROM Class TO `TypeAlias`,\n FROM Class TO `Struct`,\n FROM Class TO `Enum`,\n FROM Class TO `Annotation`,\n FROM Class TO `Constructor`,\n FROM Class TO `Trait`,\n FROM Class TO `Macro`,\n FROM Class TO `Impl`,\n FROM Class TO `Union`,\n FROM Class TO `Namespace`,\n FROM Class TO `Typedef`,\n FROM Class TO `Property`,\n FROM Method TO Function,\n FROM Method TO Method,\n FROM Method TO Class,\n FROM Method TO Community,\n FROM Method TO `Template`,\n FROM Method TO `Struct`,\n FROM Method TO `TypeAlias`,\n FROM Method TO `Enum`,\n FROM Method TO `Macro`,\n FROM Method TO `Namespace`,\n FROM Method TO `Module`,\n FROM Method TO `Impl`,\n FROM Method TO Interface,\n FROM Method TO `Constructor`,\n FROM Method TO `Property`,\n FROM Method TO CodeElement,\n FROM `Template` TO `Template`,\n FROM `Template` TO Function,\n FROM `Template` TO Method,\n FROM `Template` TO Class,\n FROM `Template` TO `Struct`,\n FROM `Template` TO `TypeAlias`,\n FROM `Template` TO `Enum`,\n FROM `Template` TO `Macro`,\n FROM `Template` TO Interface,\n FROM `Template` TO `Constructor`,\n FROM `Module` TO `Module`,\n FROM Section TO Section,\n FROM Section TO File,\n FROM File TO Route,\n FROM Function TO Route,\n FROM Method TO Route,\n FROM File TO Tool,\n FROM Function TO Tool,\n FROM Method TO Tool,\n FROM CodeElement TO Community,\n FROM Interface TO Community,\n FROM Interface TO Function,\n FROM Interface TO Method,\n FROM Interface TO Class,\n FROM Interface TO Interface,\n FROM Interface TO `TypeAlias`,\n FROM Interface TO `Struct`,\n FROM Interface TO `Constructor`,\n FROM Interface TO `Property`,\n FROM `Struct` TO Community,\n FROM `Struct` TO `Trait`,\n FROM `Struct` TO `Struct`,\n FROM `Struct` TO Class,\n FROM `Struct` TO `Enum`,\n FROM `Struct` TO Function,\n FROM `Struct` TO Method,\n FROM `Struct` TO Interface,\n FROM `Struct` TO `Constructor`,\n FROM `Struct` TO `Property`,\n FROM `Enum` TO `Enum`,\n FROM `Enum` TO Community,\n FROM `Enum` TO Class,\n FROM `Enum` TO Interface,\n FROM `Macro` TO Community,\n FROM `Macro` TO Function,\n FROM `Macro` TO Method,\n FROM `Module` TO Function,\n FROM `Module` TO Method,\n FROM `Typedef` TO Community,\n FROM `Union` TO Community,\n FROM `Namespace` TO Community,\n FROM `Namespace` TO `Struct`,\n FROM `Trait` TO Method,\n FROM `Trait` TO `Constructor`,\n FROM `Trait` TO `Property`,\n FROM `Trait` TO Community,\n FROM `Impl` TO Method,\n FROM `Impl` TO `Constructor`,\n FROM `Impl` TO `Property`,\n FROM `Impl` TO Community,\n FROM `Impl` TO `Trait`,\n FROM `Impl` TO `Struct`,\n FROM `Impl` TO `Impl`,\n FROM `TypeAlias` TO Community,\n FROM `TypeAlias` TO `Trait`,\n FROM `TypeAlias` TO Class,\n FROM `Const` TO Community,\n FROM `Static` TO Community,\n FROM `Variable` TO Community,\n FROM `Property` TO Community,\n FROM `Record` TO Method,\n FROM `Record` TO `Constructor`,\n FROM `Record` TO `Property`,\n FROM `Record` TO Community,\n FROM `Delegate` TO Community,\n FROM `Annotation` TO Community,\n FROM `Constructor` TO Community,\n FROM `Constructor` TO Interface,\n FROM `Constructor` TO Class,\n FROM `Constructor` TO Method,\n FROM `Constructor` TO Function,\n FROM `Constructor` TO `Constructor`,\n FROM `Constructor` TO `Struct`,\n FROM `Constructor` TO `Macro`,\n FROM `Constructor` TO `Template`,\n FROM `Constructor` TO `TypeAlias`,\n FROM `Constructor` TO `Enum`,\n FROM `Constructor` TO `Annotation`,\n FROM `Constructor` TO `Impl`,\n FROM `Constructor` TO `Namespace`,\n FROM `Constructor` TO `Module`,\n FROM `Constructor` TO `Property`,\n FROM `Constructor` TO `Typedef`,\n FROM `Template` TO Community,\n FROM `Module` TO Community,\n FROM Function TO Process,\n FROM Method TO Process,\n FROM Class TO Process,\n FROM Interface TO Process,\n FROM `Struct` TO Process,\n FROM `Constructor` TO Process,\n FROM `Module` TO Process,\n FROM `Macro` TO Process,\n FROM `Impl` TO Process,\n FROM `Typedef` TO Process,\n FROM `TypeAlias` TO Process,\n FROM `Enum` TO Process,\n FROM `Union` TO Process,\n FROM `Namespace` TO Process,\n FROM `Trait` TO Process,\n FROM `Const` TO Process,\n FROM `Static` TO Process,\n FROM `Variable` TO Process,\n FROM `Property` TO Process,\n FROM `Record` TO Process,\n FROM `Delegate` TO Process,\n FROM `Annotation` TO Process,\n FROM `Template` TO Process,\n FROM CodeElement TO Process,\n FROM Route TO Process,\n FROM Tool TO Process,\n FROM BasicBlock TO BasicBlock,\n type STRING,\n confidence DOUBLE,\n reason STRING,\n step INT32\n)";
46
+ export declare const RELATION_SCHEMA = "\nCREATE REL TABLE CodeRelation (\n FROM File TO File,\n FROM File TO Folder,\n FROM File TO Function,\n FROM File TO Class,\n FROM File TO Interface,\n FROM File TO Method,\n FROM File TO CodeElement,\n FROM File TO `Struct`,\n FROM File TO `Enum`,\n FROM File TO `Macro`,\n FROM File TO `Typedef`,\n FROM File TO `Union`,\n FROM File TO `Namespace`,\n FROM File TO `Trait`,\n FROM File TO `Impl`,\n FROM File TO `TypeAlias`,\n FROM File TO `Const`,\n FROM File TO `Static`,\n FROM File TO `Variable`,\n FROM File TO `Property`,\n FROM File TO `Record`,\n FROM File TO `Delegate`,\n FROM File TO `Annotation`,\n FROM File TO `Constructor`,\n FROM File TO `Template`,\n FROM File TO `Module`,\n FROM File TO Section,\n FROM Folder TO Folder,\n FROM Folder TO File,\n FROM Function TO Function,\n FROM Function TO Method,\n FROM Function TO Class,\n FROM Function TO Community,\n FROM Function TO `Macro`,\n FROM Function TO `Struct`,\n FROM Function TO `Template`,\n FROM Function TO `Enum`,\n FROM Function TO `Namespace`,\n FROM Function TO `TypeAlias`,\n FROM Function TO `Module`,\n FROM Function TO `Impl`,\n FROM Function TO Interface,\n FROM Function TO `Constructor`,\n FROM Function TO `Const`,\n FROM Function TO `Typedef`,\n FROM Function TO `Union`,\n FROM Function TO `Property`,\n FROM Function TO CodeElement,\n FROM Class TO Method,\n FROM Class TO Function,\n FROM Class TO Class,\n FROM Class TO Interface,\n FROM Class TO Community,\n FROM Class TO `Template`,\n FROM Class TO `TypeAlias`,\n FROM Class TO `Struct`,\n FROM Class TO `Enum`,\n FROM Class TO `Annotation`,\n FROM Class TO `Constructor`,\n FROM Class TO `Trait`,\n FROM Class TO `Macro`,\n FROM Class TO `Impl`,\n FROM Class TO `Union`,\n FROM Class TO `Namespace`,\n FROM Class TO `Typedef`,\n FROM Class TO `Property`,\n FROM Method TO Function,\n FROM Method TO Method,\n FROM Method TO Class,\n FROM Method TO Community,\n FROM Method TO `Template`,\n FROM Method TO `Struct`,\n FROM Method TO `TypeAlias`,\n FROM Method TO `Enum`,\n FROM Method TO `Macro`,\n FROM Method TO `Namespace`,\n FROM Method TO `Module`,\n FROM Method TO `Impl`,\n FROM Method TO Interface,\n FROM Method TO `Constructor`,\n FROM Method TO `Property`,\n FROM Method TO CodeElement,\n FROM `Template` TO `Template`,\n FROM `Template` TO Function,\n FROM `Template` TO Method,\n FROM `Template` TO Class,\n FROM `Template` TO `Struct`,\n FROM `Template` TO `TypeAlias`,\n FROM `Template` TO `Enum`,\n FROM `Template` TO `Macro`,\n FROM `Template` TO Interface,\n FROM `Template` TO `Constructor`,\n FROM `Module` TO `Module`,\n FROM Section TO Section,\n FROM Section TO File,\n FROM File TO Route,\n FROM Function TO Route,\n FROM Method TO Route,\n FROM File TO Tool,\n FROM Function TO Tool,\n FROM Method TO Tool,\n FROM CodeElement TO Community,\n FROM Interface TO Community,\n FROM Interface TO Function,\n FROM Interface TO Method,\n FROM Interface TO Class,\n FROM Interface TO Interface,\n FROM Interface TO `TypeAlias`,\n FROM Interface TO `Struct`,\n FROM Interface TO `Constructor`,\n FROM Interface TO `Property`,\n FROM `Struct` TO Community,\n FROM `Struct` TO `Trait`,\n FROM `Struct` TO `Struct`,\n FROM `Struct` TO Class,\n FROM `Struct` TO `Enum`,\n FROM `Struct` TO Function,\n FROM `Struct` TO Method,\n FROM `Struct` TO Interface,\n FROM `Struct` TO `Constructor`,\n FROM `Struct` TO `Property`,\n FROM `Enum` TO `Enum`,\n FROM `Enum` TO Community,\n FROM `Enum` TO Class,\n FROM `Enum` TO Interface,\n FROM `Macro` TO Community,\n FROM `Macro` TO Function,\n FROM `Macro` TO Method,\n FROM `Module` TO Function,\n FROM `Module` TO Method,\n FROM `Typedef` TO Community,\n FROM `Union` TO Community,\n FROM `Namespace` TO Community,\n FROM `Namespace` TO `Struct`,\n FROM `Trait` TO Method,\n FROM `Trait` TO `Constructor`,\n FROM `Trait` TO `Property`,\n FROM `Trait` TO Community,\n FROM `Impl` TO Method,\n FROM `Impl` TO `Constructor`,\n FROM `Impl` TO `Property`,\n FROM `Impl` TO Community,\n FROM `Impl` TO `Trait`,\n FROM `Impl` TO `Struct`,\n FROM `Impl` TO `Impl`,\n FROM `TypeAlias` TO Community,\n FROM `TypeAlias` TO `Trait`,\n FROM `TypeAlias` TO Class,\n FROM `Const` TO Community,\n FROM `Static` TO Community,\n FROM `Variable` TO Community,\n FROM `Property` TO Community,\n FROM `Property` TO `Property`,\n FROM `Record` TO Method,\n FROM `Record` TO `Constructor`,\n FROM `Record` TO `Property`,\n FROM `Record` TO Community,\n FROM `Delegate` TO Community,\n FROM `Annotation` TO Community,\n FROM `Constructor` TO Community,\n FROM `Constructor` TO Interface,\n FROM `Constructor` TO Class,\n FROM `Constructor` TO Method,\n FROM `Constructor` TO Function,\n FROM `Constructor` TO `Constructor`,\n FROM `Constructor` TO `Struct`,\n FROM `Constructor` TO `Macro`,\n FROM `Constructor` TO `Template`,\n FROM `Constructor` TO `TypeAlias`,\n FROM `Constructor` TO `Enum`,\n FROM `Constructor` TO `Annotation`,\n FROM `Constructor` TO `Impl`,\n FROM `Constructor` TO `Namespace`,\n FROM `Constructor` TO `Module`,\n FROM `Constructor` TO `Property`,\n FROM `Constructor` TO `Typedef`,\n FROM `Template` TO Community,\n FROM `Module` TO Community,\n FROM Function TO Process,\n FROM Method TO Process,\n FROM Class TO Process,\n FROM Interface TO Process,\n FROM `Struct` TO Process,\n FROM `Constructor` TO Process,\n FROM `Module` TO Process,\n FROM `Macro` TO Process,\n FROM `Impl` TO Process,\n FROM `Typedef` TO Process,\n FROM `TypeAlias` TO Process,\n FROM `Enum` TO Process,\n FROM `Union` TO Process,\n FROM `Namespace` TO Process,\n FROM `Trait` TO Process,\n FROM `Const` TO Process,\n FROM `Static` TO Process,\n FROM `Variable` TO Process,\n FROM `Property` TO Process,\n FROM `Record` TO Process,\n FROM `Delegate` TO Process,\n FROM `Annotation` TO Process,\n FROM `Template` TO Process,\n FROM CodeElement TO Process,\n FROM Route TO Process,\n FROM Tool TO Process,\n FROM BasicBlock TO BasicBlock,\n type STRING,\n confidence DOUBLE,\n reason STRING,\n step INT32\n)";
47
47
  export declare const EMBEDDING_DIMS: number;
48
48
  /** HNSW vector index name for the CodeEmbedding table. */
49
49
  export declare const EMBEDDING_INDEX_NAME = "code_embedding_idx";
@@ -380,6 +380,7 @@ CREATE REL TABLE ${REL_TABLE_NAME} (
380
380
  FROM \`Static\` TO Community,
381
381
  FROM \`Variable\` TO Community,
382
382
  FROM \`Property\` TO Community,
383
+ FROM \`Property\` TO \`Property\`,
383
384
  FROM \`Record\` TO Method,
384
385
  FROM \`Record\` TO \`Constructor\`,
385
386
  FROM \`Record\` TO \`Property\`,
@@ -42,11 +42,13 @@ import { EMBEDDING_TABLE_NAME } from './lbug/schema.js';
42
42
  import { STALE_HASH_SENTINEL } from './lbug/schema.js';
43
43
  import { isSpringBeanCandidateSourceFile } from './ingestion/frameworks/spring/bean-catalog.js';
44
44
  import { SPRING_BEAN_INVENTORY_FEATURE } from './ingestion/frameworks/spring/analysis-features.js';
45
+ import { SPRING_CONFIG_BINDINGS_FEATURE } from './ingestion/languages/java/analysis-features.js';
45
46
  import { CLASS_FRAMEWORK_ANNOTATIONS_FEATURE, findAnalysisFeatureMismatches, resolveAnalysisFeatureVersions, } from './analysis-features.js';
46
47
  import { analyzerRunnerIdentitiesEqual, finalizeAnalyzerRunnerIdentity, resolveAnalyzerRunnerIdentity, } from './analyzer-identity.js';
47
48
  const ANALYSIS_FEATURES = [
48
49
  CLASS_FRAMEWORK_ANNOTATIONS_FEATURE,
49
50
  SPRING_BEAN_INVENTORY_FEATURE,
51
+ SPRING_CONFIG_BINDINGS_FEATURE,
50
52
  ];
51
53
  function stringList(value) {
52
54
  return Array.isArray(value)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.69",
3
+ "version": "1.6.10-rc.70",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",