Repository navigation
Description
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 :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}andJConfigRead{site, callee, key, reason, prov}on the schema model, andapplication.config_uses[]/application.config_reads_unresolved[]inanalysis.json - Call-site detectors:
System.getenv,Environment.getProperty,Properties.getProperty, keyed onreceiver_type+method_name, reading the literal fromargument_expr - Annotation detectors:
@Value("${x}")on a field or parameter,@ConfigurationPropertieson a class - Resolver closing a decoded literal against the declared
: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 time -
analysis.v2.schema.jsonadmits both new application-level members - Tier runs at L1
Caveats 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.- The
_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. - The ghost must be minted, not looked up.
: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. - The graph contract version does not move.
V2SchemaCatalog.SCHEMA_VERSIONstays2.0.0; a re-baseline is Scope every destructive Neo4j statement on thecan://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
- On a daytrader8 projection,
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. - A fixture with a
@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". - Two unresolved reads through the same callee with different keys survive as two distinct relationships (the
_kcheck). - Runs and produces edges at
-a 1, and with--external-callsoff. - The L1 conformance gate stays green;
analysis.v2.schema.jsonvalidates an-a 1payload carrying both new lists. --emit schemaoutput includes both new relationship types with the endpoint sets above.
Activity
docs(schema): stop calling the graph contract 2.2.0 (#238)