You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Spec: codellm-devkit/.github → docs/design/specs/2026-09-07-java-config-reads-and-entrypoint-report.md. Decisions D30/D31 in .claude/SCHEMA_DECISIONS.md.
Problem
The analyzer projects 9,688 :ConfigKey nodes and no relationship saying which code reads one, so DEFINES_CONFIG is the only config relationship in a Java graph. The keys are known; the readers are not. artifacts/ConfigKeys.java only flattens config-bearing artifacts into declarations — there is no config-use analysis in the analyzer at all.
codeanalyzer-python has had this since its #162: a detection-and-resolution pass feeding app.config_uses / app.config_reads_unresolved, projected as PY_USES_CONFIG / PY_READS_CONFIG_UNRESOLVED.
Scope boundary
In scope: the literal tier, both new analysis.json lists, both new Neo4j relationship types, and the conformance-schema and catalog updates that go with them.
Out of scope: the L3/L4 dataflow tier (child 2 of the epic), the entrypoint report (child 3), python-sdk accessors (child 4, deferred), comment projection and CRUD (see the epic's scope guard).
Goals
JConfigUseEdge{src, dst, prov} and JConfigRead{site, callee, key, reason, prov} on the schema model, and application.config_uses[] / application.config_reads_unresolved[] in analysis.json
Call-site detectors: System.getenv, Environment.getProperty, Properties.getProperty, keyed on receiver_type + method_name, reading the literal from argument_expr
Annotation detectors: @Value("${x}") on a field or parameter, @ConfigurationProperties on a class
Resolver closing a decoded literal against the declared :ConfigKey set by namespace preference order, emitting one edge per matching key — a key legitimately appears in both application.properties and application-dev.properties, and a @ConfigurationProperties prefix claims every key beneath it. Only a zero-match read becomes a JConfigRead, with reason of undefined-key (a literal nobody declares) or non-literal (never closed on a literal)
J_USES_CONFIG in V2SchemaCatalog, from: [JBodyNode, JCallable, JField, JType] → to: [ConfigKey], props {prov}
J_READS_CONFIG_UNRESOLVED, JApplication → JExternal, props {key, reason, prov, _k}, with the callee ghost minted at detection time
analysis.v2.schema.json admits both new application-level members
Tier runs at L1
Caveats and known risks
src is a union endpoint set, wider than python's single PyBodyNode. This is the locked divergence (spec §4 D1): Java's dominant config-read idiom is an annotation with no call site. A consumer must not assume the source of a config-read edge is a body node.
The _k discriminant on J_READS_CONFIG_UNRESOLVED is load-bearing. One callee (System.getenv) legitimately reads many undeclared keys across a codebase; without _k = "<key>|<reason>" a plain endpoint-pair MERGE collapses them onto one relationship and keeps only the last SET. Python hit this exact hazard.
The ghost must be minted, not looked up.:JExternal rows come from the L2 external-symbol set, so a lookup would make unresolved reads invisible at L1 and absent whenever --external-calls is off — precisely the runs where the literal tier is the only tier there is.
@Value placeholders can carry defaults (${x:fallback}) and nesting. Decode the key before matching, and treat a placeholder that does not close on a single key as a non-literal read rather than dropping it.
Definition of done
On a daytrader8 projection, J_USES_CONFIG is non-empty; every dst resolves to a projected :ConfigKey and every src to a projected :JBodyNode/:JCallable/:JField/:JType. Zero dangling endpoints, checked by query, not by eye.
A fixture with a @Value("${a.b}") field, a System.getenv("PATH") call, a getenv of an undeclared key, and a getenv(someVariable) yields, respectively: one J_USES_CONFIG from :JField, one from :JBodyNode, one J_READS_CONFIG_UNRESOLVED with reason="undefined-key", and one with reason="non-literal".
Two unresolved reads through the same callee with different keys survive as two distinct relationships (the _k check).
Runs and produces edges at -a 1, and with --external-calls off.
The L1 conformance gate stays green; analysis.v2.schema.json validates an -a 1 payload carrying both new lists.
--emit schema output includes both new relationship types with the endpoint sets above.
Plan (optional)
Spec:
codellm-devkit/.github→docs/design/specs/2026-09-07-java-config-reads-and-entrypoint-report.md. Decisions D30/D31 in.claude/SCHEMA_DECISIONS.md.Problem
The analyzer projects 9,688
:ConfigKeynodes and no relationship saying which code reads one, soDEFINES_CONFIGis the only config relationship in a Java graph. The keys are known; the readers are not.artifacts/ConfigKeys.javaonly flattens config-bearing artifacts into declarations — there is no config-use analysis in the analyzer at all.codeanalyzer-python has had this since its #162: a detection-and-resolution pass feeding
app.config_uses/app.config_reads_unresolved, projected asPY_USES_CONFIG/PY_READS_CONFIG_UNRESOLVED.Scope boundary
In scope: the literal tier, both new
analysis.jsonlists, both new Neo4j relationship types, and the conformance-schema and catalog updates that go with them.Out of scope: the L3/L4 dataflow tier (child 2 of the epic), the entrypoint report (child 3), python-sdk accessors (child 4, deferred), comment projection and CRUD (see the epic's scope guard).
Goals
JConfigUseEdge{src, dst, prov}andJConfigRead{site, callee, key, reason, prov}on the schema model, andapplication.config_uses[]/application.config_reads_unresolved[]inanalysis.jsonSystem.getenv,Environment.getProperty,Properties.getProperty, keyed onreceiver_type+method_name, reading the literal fromargument_expr@Value("${x}")on a field or parameter,@ConfigurationPropertieson a class:ConfigKeyset by namespace preference order, emitting one edge per matching key — a key legitimately appears in bothapplication.propertiesandapplication-dev.properties, and a@ConfigurationPropertiesprefix claims every key beneath it. Only a zero-match read becomes aJConfigRead, withreasonofundefined-key(a literal nobody declares) ornon-literal(never closed on a literal)J_USES_CONFIGinV2SchemaCatalog,from: [JBodyNode, JCallable, JField, JType] → to: [ConfigKey], props{prov}J_READS_CONFIG_UNRESOLVED,JApplication → JExternal, props{key, reason, prov, _k}, with the callee ghost minted at detection timeanalysis.v2.schema.jsonadmits both new application-level membersCaveats and known risks
srcis a union endpoint set, wider than python's singlePyBodyNode. This is the locked divergence (spec §4 D1): Java's dominant config-read idiom is an annotation with no call site. A consumer must not assume the source of a config-read edge is a body node._kdiscriminant onJ_READS_CONFIG_UNRESOLVEDis load-bearing. One callee (System.getenv) legitimately reads many undeclared keys across a codebase; without_k = "<key>|<reason>"a plain endpoint-pair MERGE collapses them onto one relationship and keeps only the lastSET. Python hit this exact hazard.:JExternalrows come from the L2 external-symbol set, so a lookup would make unresolved reads invisible at L1 and absent whenever--external-callsis off — precisely the runs where the literal tier is the only tier there is.V2SchemaCatalog.SCHEMA_VERSIONstays2.0.0; a re-baseline is Epic: scope every destructive Neo4j statement on the can:// id prefix; retire _module .github#50. So this relationship is undetectable fromschema_versionalone — by design, and recorded in the spec.@Valueplaceholders can carry defaults (${x:fallback}) and nesting. Decode the key before matching, and treat a placeholder that does not close on a single key as anon-literalread rather than dropping it.Definition of done
J_USES_CONFIGis non-empty; everydstresolves to a projected:ConfigKeyand everysrcto a projected:JBodyNode/:JCallable/:JField/:JType. Zero dangling endpoints, checked by query, not by eye.@Value("${a.b}")field, aSystem.getenv("PATH")call, agetenvof an undeclared key, and agetenv(someVariable)yields, respectively: oneJ_USES_CONFIGfrom:JField, one from:JBodyNode, oneJ_READS_CONFIG_UNRESOLVEDwithreason="undefined-key", and one withreason="non-literal"._kcheck).-a 1, and with--external-callsoff.analysis.v2.schema.jsonvalidates an-a 1payload carrying both new lists.--emit schemaoutput includes both new relationship types with the endpoint sets above.