Plan (optional)
Spec: codellm-devkit/.github → docs/design/specs/2026-09-07-java-config-reads-and-entrypoint-report.md (decision D30, tiers). Epic: codellm-devkit/.github#62. Builds on #232 / PR #233 — branch from that branch, not main.
Problem
#232 shipped the literal tier: a config read resolves when its key argument is written as a string literal at the call site. Everything else lands in config_reads_unresolved with reason="non-literal", which on a real codebase is the common shape:
String key = "spring.datasource.url";
env.getProperty(key); // non-literal today
String read(String name) { return System.getenv(name); } // non-literal today
codeanalyzer-python closes both over its dataflow graph (its #162 Task 3), so a Java graph reports strictly fewer readers than a Python one for the same code shape.
Scope boundary
In scope: the two dataflow tiers over the existing L3/L4 overlays, and the prov bookkeeping that goes with them.
Out of scope: any new dataflow machinery — both tiers read the DDG and call graph the analyzer already emits. Also out: the entrypoint report (#234), python-sdk accessors (epic child 4, deferred), CRUD (#187).
Goals
Caveats and known risks
- The
ssa filter is what keeps -a 3 ⊆ -a 4 true, and it is not optional. At -a 4, L4WalaOverlays.java:201 adds points-to to the prov of existing edges and mints alias-widened ones. An alias edge can connect this variable's use to an unrelated write that is not a name = "literal" shape; the shape check then refuses it, and — since any non-closing reaching def kills resolution — that refusal would remove an edge the ssa-only L3 set resolved cleanly. Monotonicity broken by a widening, which is the exact failure the additive contract forbids. codeanalyzer-python documents hitting this.
- Any non-closing reaching def must kill the resolution, not be skipped. Two paths assigning different literals means the read is genuinely ambiguous, and picking one would be a confident wrong answer.
- The span-containment check is load-bearing. The CFG/DDG is statement-level while a
call body node is keyed by its own narrower span, so a def's recorded use site is the enclosing statement. Matching by containment covers both the bare-expression-statement case and the common return env.getProperty(key); nesting without special-casing either.
SdgPortEdges also emits prov:["ssa"] ddg edges, whose endpoints are synthetic @formal_in/@actual_out vertices with no span. The span guard rejects them; do not relax it.
- The interproc tier needs completeness of the call-site set. A callable reachable through a call edge the analyzer did not resolve must not close, or a caller supplying a different key is silently ignored.
- A read the intra tier closed to a literal that matches no declared key must not be re-derived from callers by the interproc tier: the local rebinding is what the callee actually reads, and the caller's argument would misattribute.
Definition of done
- On a fixture carrying
String k = "server.port"; env.getProperty(k);, -a 1 reports it non-literal and -a 3 resolves it with prov: ["dataflow"].
- On a fixture whose key is a parameter with two callers passing the same literal,
-a 4 resolves it; with two callers passing different literals, it stays unresolved at every level.
- A local reassigned to two different literals on two paths stays unresolved — no arbitrary pick.
- Monotonicity on a fixture that carries a literal read, an intra-closable read and an interproc-closable read:
config_uses(-a 1) ⊆ config_uses(-a 3) ⊆ config_uses(-a 4), checked as set containment of (src, dst) pairs, and the literal-tier edges keep prov: ["literal"] at every level.
- Running
-a 4 with --l3-engine wala (which adds points-to prov) resolves the same set as -a 3 plus interproc — never fewer.
analysis.v2.schema.json still validates a -a 4 payload; the L1/L3/L4 conformance gates stay green.
Plan (optional)
Spec:
codellm-devkit/.github→docs/design/specs/2026-09-07-java-config-reads-and-entrypoint-report.md(decision D30, tiers). Epic: codellm-devkit/.github#62. Builds on #232 / PR #233 — branch from that branch, notmain.Problem
#232 shipped the literal tier: a config read resolves when its key argument is written as a string literal at the call site. Everything else lands in
config_reads_unresolvedwithreason="non-literal", which on a real codebase is the common shape:codeanalyzer-python closes both over its dataflow graph (its #162 Task 3), so a Java graph reports strictly fewer readers than a Python one for the same code shape.
Scope boundary
In scope: the two dataflow tiers over the existing L3/L4 overlays, and the
provbookkeeping that goes with them.Out of scope: any new dataflow machinery — both tiers read the DDG and call graph the analyzer already emits. Also out: the entrypoint report (#234), python-sdk accessors (epic child 4, deferred), CRUD (#187).
Goals
prov.contains("ssa"); require the DDG use node's span to contain the call node's span; accept only a single-target<name> = "literal"definition (VariableDeclarationExprwith one declarator, orAssignExpronto aNameExpr).prov: ["dataflow"]on tier-produced edges; unresolved records carry every tier attempted, so["literal", "dataflow"]at L3+.-a 3+, interproc at-a 4+. A read resolved at a lower tier is never recomputed.ConfigUsestakes the analysis level, module source, and the L2 call graph it now needs.Caveats and known risks
ssafilter is what keeps-a 3 ⊆ -a 4true, and it is not optional. At-a 4,L4WalaOverlays.java:201addspoints-toto the prov of existing edges and mints alias-widened ones. An alias edge can connect this variable's use to an unrelated write that is not aname = "literal"shape; the shape check then refuses it, and — since any non-closing reaching def kills resolution — that refusal would remove an edge the ssa-only L3 set resolved cleanly. Monotonicity broken by a widening, which is the exact failure the additive contract forbids. codeanalyzer-python documents hitting this.callbody node is keyed by its own narrower span, so a def's recorded use site is the enclosing statement. Matching by containment covers both the bare-expression-statement case and the commonreturn env.getProperty(key);nesting without special-casing either.SdgPortEdgesalso emitsprov:["ssa"]ddg edges, whose endpoints are synthetic@formal_in/@actual_outvertices with no span. The span guard rejects them; do not relax it.Definition of done
String k = "server.port"; env.getProperty(k);,-a 1reports itnon-literaland-a 3resolves it withprov: ["dataflow"].-a 4resolves it; with two callers passing different literals, it stays unresolved at every level.config_uses(-a 1) ⊆ config_uses(-a 3) ⊆ config_uses(-a 4), checked as set containment of(src, dst)pairs, and the literal-tier edges keepprov: ["literal"]at every level.-a 4with--l3-engine wala(which addspoints-toprov) resolves the same set as-a 3plus interproc — never fewer.analysis.v2.schema.jsonstill validates a-a 4payload; the L1/L3/L4 conformance gates stay green.