feat(config): config-read literal tier — J_USES_CONFIG and J_READS_CONFIG_UNRESOLVED - #233
Merged
Merged
Conversation
D30 fixes the config-read vocabulary: `J_USES_CONFIG` / `J_READS_CONFIG_UNRESOLVED` with a `src` widened to the annotated element, a literal tier at L1 and a dataflow tier at L3/L4. D31 fixes the entrypoint report's four keys and what Java puts in each. Both note that the graph contract version stays at 2.0.0, and what that costs a consumer. Spec: codellm-devkit/.github docs/design/specs/2026-09-07-java-config-reads-and-entrypoint-report.md
…NFIG_UNRESOLVED
The analyzer projected 9,688 `:ConfigKey` nodes and no relationship saying which
code reads one, so `DEFINES_CONFIG` was the only config relationship in a Java
graph. `ConfigKeys` answers "which keys are declared"; nothing answered "who
reads one" — there was no config-use analysis in the analyzer at all.
Adds `application.config_uses[]` and `application.config_reads_unresolved[]` to
`analysis.json`, and `J_USES_CONFIG` / `J_READS_CONFIG_UNRESOLVED` to the Neo4j
catalog and projection.
Two deliberate divergences from codeanalyzer-python's equivalent pass, both
recorded in D30:
- `src` is a union, not a call body node alone. Python anchors every read on a
call site because Python's config reads are calls; Java's dominant idiom is
`@Value("${x}")` / `@ConfigurationProperties`, an annotation with no call site.
So a read is attributed to the annotated field, callable, or type, and a
call-site read to its body node. Reading only call sites would report almost
nothing on a Spring application.
- The literal tier runs at L1, not python's L2. A `call` body node already
carries `argument_expr`/`receiver_type`/`method_name` at L1 and an annotation
is pure L1 data, so nothing in the tier needs a call graph. The L3/L4 dataflow
tier widens it additively.
Detection is by declared receiver type, never by bare method name: a project's
own `getProperty(String)` is not a config read. A `@ConfigurationProperties`
prefix claims every declared key beneath it, since the accessor's real question
is which class binds a given key.
The unresolved edge's `JExternal` ghost is minted at detection time rather than
looked up in the L2 external-symbol set — a lookup would make every unresolved
read invisible at L1 and absent whenever `--external-calls` is off, precisely
the runs where the literal tier is the only tier there is. `_k` = (key, reason)
discriminates the edge because one callee legitimately reads many undeclared
keys, and a plain endpoint-pair MERGE would keep only the last key SET.
`RowBuilder.refTo` lets an edge address a node by id alone, so `J_USES_CONFIG`
does not need its caller to know which of four merge labels the source landed
on.
Verified on daytrader8 (`-a 1`, `--no-build`): 15 resolved reads, every `dst` a
declared `:ConfigKey`; 12 undeclared-key reads; the payload validates against
`analysis.v2.schema.json`; and the projected graph carries both relationship
families with the ghost minted and `_k` collapsing repeats per (key, reason).
The graph contract version stays at 2.0.0 — a re-baseline is
codellm-devkit/.github#50, and the consequence is recorded in D30.
Closes #232
This was referenced Sep 7, 2026
Closed
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #232. Spec:
codellm-devkit/.githubdocs/design/specs/2026-09-07-java-config-reads-and-entrypoint-report.md(codellm-devkit/.github#63), epic codellm-devkit/.github#62.What
The analyzer projected 9,688
:ConfigKeynodes and no relationship saying which code reads one.ConfigKeysanswers "which keys are declared"; nothing answered "who reads one" — there was no config-use analysis in the analyzer at all, so this is a new pass, not projection wiring.application.config_uses[]andapplication.config_reads_unresolved[]inanalysis.jsonJ_USES_CONFIGandJ_READS_CONFIG_UNRESOLVEDinV2SchemaCatalogandV2GraphProjectorSystem.getenv,System.getProperty,Properties.getProperty,Environment/PropertyResolver.getProperty/getRequiredProperty,@Value("\${x}"),@ConfigurationPropertiesDivergences from codeanalyzer-python, both deliberate (D30)
srcis a union, not a call body node alone. Python anchors every read on a call site because Python's config reads are calls. Java's dominant idiom is an annotation with no call site, so a read is attributed to the annotated:JField/:JCallable/:JType, and a call-site read to its:JBodyNode. Call-sites-only would be literal parity that reports almost nothing on a Spring app. A consumer must not assume the source of a config-read edge is a body node.The literal tier runs at L1, not L2. A
callbody node already carriesargument_expr/receiver_type/method_nameat L1 and an annotation is pure L1 data, so nothing here needs a call graph. Same flag therefore yields different availability on the two analyzers; the L3/L4 dataflow tier (#232's sibling, epic child 2) widens this additively.Notable choices
getProperty(String)is not a config read.@ConfigurationPropertiesprefix claims every declared key beneath it, because the accessor's real question is which class binds a given key.@Value("a plain default")with no placeholder is not a read at all and appears in neither list.JExternalghost is minted at detection time, not looked up in the L2 external-symbol set — a lookup would make every unresolved read invisible at L1 and absent whenever--external-callsis off, precisely the runs where the literal tier is the only tier there is._k= (key, reason) on the unresolved edge: one callee legitimately reads many undeclared keys, and a plain endpoint-pair MERGE would keep only the last key SET. Python hit this hazard first.RowBuilder.refTolets an edge address a node by id alone, soJ_USES_CONFIGdoes not need its caller to know which of four merge labels the source landed on.Verification
On daytrader8,
-a 1 --no-build:dsta declared:ConfigKeyanalysis.v2.schema.json--external-callsoff;_kcollapses repeats per (key, reason)ConfigUsesTestcovers all four outcomes on one fixture — resolved annotation read, resolved call-site read, undefined key, non-literal key — plus prefix binding, the_kdiscriminant, no-dangling-endpoints, JSON/graph edge-count agreement, and sort determinism.Full suite: 528 tests, 1 failure —
CodeAnalyzerIntegrationTestcannot reach a Docker daemon on this machine, and fails identically on a cleanmain(verified by stashing).schema.neo4j.jsonregenerated with--emit schema.Contract version
Stays at
2.0.0. A re-baseline is codellm-devkit/.github#50, not a local bump. Recorded consequence: a consumer cannot detect these relationships fromschema_versionalone — detection is by presence until #50 lands, the same gap the_moduleremoval already lives with.