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
codeanalyzer-java projects 9,688 :ConfigKey nodes and 2,604 :JEntrypoint marks, but nothing that says which code reads a key and nothing about how the entrypoint pass behaved. This epic adds application.config_uses[], application.config_reads_unresolved[] and application.entrypoint_report{} to analysis.json, and J_USES_CONFIG, J_READS_CONFIG_UNRESOLVED plus two :JApplication properties to the Neo4j projection.
Contract impact: additive in both projections. No shared field renamed, no shared kind repurposed. The graph contract version does not move — see the release plan.
Affected repos
codeanalyzer-java — the detection pass, the entrypoint report, both projections. Children 1–3.
python-sdk — the Java facade has no config accessor and no entrypoint-coverage accessor at all today, so consuming this data is new surface, not a read-the-new-field change. Child 4, deferred to a later cycle.
docs — contract note, folded into the last analyzer child.
Design decisions
J_ prefix (J_USES_CONFIG, J_READS_CONFIG_UNRESOLVED), matching codeanalyzer-python's PY_-prefixed pair. The unprefixed spelling was genuinely tempting — :ConfigKey is a cross-language merge target, so one MATCH would answer "who reads this key" across every analyzer — but python has shipped PY_USES_CONFIG, and one concept spelled two ways is the parity failure the clause names. Accepted cost: a cross-language config-read query needs one MATCH per language.
src widens to the annotated element. Python anchors a config read on a call body node because Python's config reads are calls. Java's dominant idiom is @Value("${db.url}") / @ConfigurationProperties, an annotation with no call site. So J_USES_CONFIG runs from JBodyNode (call-site reads) orJCallable/JField/JType (annotation reads). Rejected: minting a synthetic body node per annotation — it invents a body-node kind with no AST region behind it, a larger addition to the shared vocabulary than widening one endpoint list. Rejected: call sites only — literal parity that reports almost nothing on a Spring app, which is most of the daytrader8 corpus this was measured on.
Literal tier at L1, dataflow tier at L3/L4. Python gates its literal tier at -a 2; Java's JBodyNode already carries argument_expr/receiver_type/method_name at L1 and annotations are L1 data, so nothing in the tier needs a call graph. Monotonic by construction: config_uses(-a 1) ⊆ config_uses(-a 4), same additive contract as the DDG's ssa → points-to widening. Divergence stated: the same flag gives different availability on the two analyzers.
Unresolved reads follow python exactly — JApplication → JExternal, props {key, reason, prov, _k}, ghost minted at detection time so a read stays visible at L1 and with --external-calls off. _k = "<key>|<reason>" because one callee (System.getenv) reads many undeclared keys and a plain endpoint-pair MERGE keeps only the last.
Entrypoint report is python's four keys, honestly populated: rulesets is the five hardcoded finder names (that is Java's ruleset vocabulary), unresolved counts per-finder near-misses, errors records a finder that threw. Always present, even when empty — the pass under-approximates by design, so an absent report and an empty one must not read the same.
Scope guard — explicitly OUT:
Comment nodes. codeanalyzer-java#231 listed this as a third goal on the stated grounds that codeanalyzer-python projects comment nodes. It does not: there is no :PyComment node type in codeanalyzer/neo4j/schema.py, and project.py:843 collapses comments to a docstring property exactly as V2GraphProjector.docstringOf does. python-sdk documents it as a deliberate ceiling (neo4j_backend.py:73). No parity gap, no term to match — closed by fact. Projecting comments remains available later as a new capability, not as this parity fix.
CRUD (codeanalyzer-java#187).
The can:// id missing from the :JApplication root (codeanalyzer-schema#5).
codeanalyzer-typescript's identical config gap.
Release plan
codeanalyzer-java 3.0.3 → 3.1.0 (MINOR — additive JSON fields, additive graph relationships) carries children 1–3. Nothing gates the analyzer.
The graph contract version stays 2.0.0.V2SchemaCatalog.SCHEMA_VERSION is deliberately held there; the class javadoc's "2.2.0" is stale text. This analyzer already drifted to 2.1.0/2.2.0/3.0.0 alone while codeanalyzer-python stayed at 2.0.0, and that drift was undone. Moving it is a coordinated re-baseline across all three analyzers — Epic: scope every destructive Neo4j statement on the can:// id prefix; retire _module #50, not this epic. Recorded consequence: a consumer cannot detect J_USES_CONFIG or the entrypoint properties from schema_version alone. Detection is by presence until Epic: scope every destructive Neo4j statement on the can:// id prefix; retire _module #50 lands, the same gap the _module removal already lives with.
python-sdk is deferred, by decision. Stated plainly: the analyzer will emit data nothing consumes, and codeanalyzer-java#231's actual complaint — get_config_readers() returning an unconditional [], get_entrypoint_coverage() reporting entrypoint_report_unavailable — stays unfixed until child 4 ships in a later cycle.
Planned children, filed just-in-time as each is picked up:
codeanalyzer-java — literal tier: detectors, resolver, both new JSON lists, both new relationship types.
codeanalyzer-java — dataflow tier over the L3/L4 DDG, prov: ["dataflow"].
codeanalyzer-java — entrypoint report: model, finder plumbing, both projections, docs.
python-sdk — Java facade accessors. Deferred.
Definition of done (epic-level)
Every sub-issue closed with its gate green.
On a daytrader8 projection: J_USES_CONFIG is non-empty, every dst resolves to a projected :ConfigKey, and every src resolves to a projected :JBodyNode/:JCallable/:JField/:JType. No dangling endpoints.
MATCH (a:JApplication) RETURN any(k IN keys(a) WHERE k CONTAINS 'entrypoint') returns TRUE.
Monotonicity holds on a fixture carrying both a literal and a dataflow-only read: config_uses(-a 1) ⊆ config_uses(-a 3) ⊆ config_uses(-a 4).
analysis.v2.schema.json admits the three new application-level members; the L1 conformance gate stays green.
entrypoint_report_json parses with the same model shape as codeanalyzer-python's, and is present on every projection including one with zero entrypoints.
Spec
docs/design/specs/2026-09-07-java-config-reads-and-entrypoint-report.mdSummary
codeanalyzer-java projects 9,688
:ConfigKeynodes and 2,604:JEntrypointmarks, but nothing that says which code reads a key and nothing about how the entrypoint pass behaved. This epic addsapplication.config_uses[],application.config_reads_unresolved[]andapplication.entrypoint_report{}toanalysis.json, andJ_USES_CONFIG,J_READS_CONFIG_UNRESOLVEDplus two:JApplicationproperties to the Neo4j projection.Contract impact: additive in both projections. No shared field renamed, no shared
kindrepurposed. The graph contract version does not move — see the release plan.Affected repos
codeanalyzer-java— the detection pass, the entrypoint report, both projections. Children 1–3.python-sdk— the Java facade has no config accessor and no entrypoint-coverage accessor at all today, so consuming this data is new surface, not a read-the-new-field change. Child 4, deferred to a later cycle.Design decisions
J_prefix (J_USES_CONFIG,J_READS_CONFIG_UNRESOLVED), matching codeanalyzer-python'sPY_-prefixed pair. The unprefixed spelling was genuinely tempting —:ConfigKeyis a cross-language merge target, so oneMATCHwould answer "who reads this key" across every analyzer — but python has shippedPY_USES_CONFIG, and one concept spelled two ways is the parity failure the clause names. Accepted cost: a cross-language config-read query needs oneMATCHper language.srcwidens to the annotated element. Python anchors a config read on acallbody node because Python's config reads are calls. Java's dominant idiom is@Value("${db.url}")/@ConfigurationProperties, an annotation with no call site. SoJ_USES_CONFIGruns fromJBodyNode(call-site reads) orJCallable/JField/JType(annotation reads). Rejected: minting a synthetic body node per annotation — it invents a body-nodekindwith no AST region behind it, a larger addition to the shared vocabulary than widening one endpoint list. Rejected: call sites only — literal parity that reports almost nothing on a Spring app, which is most of the daytrader8 corpus this was measured on.-a 2; Java'sJBodyNodealready carriesargument_expr/receiver_type/method_nameat L1 and annotations are L1 data, so nothing in the tier needs a call graph. Monotonic by construction:config_uses(-a 1) ⊆ config_uses(-a 4), same additive contract as the DDG'sssa→points-towidening. Divergence stated: the same flag gives different availability on the two analyzers.JApplication → JExternal, props{key, reason, prov, _k}, ghost minted at detection time so a read stays visible at L1 and with--external-callsoff._k = "<key>|<reason>"because one callee (System.getenv) reads many undeclared keys and a plain endpoint-pair MERGE keeps only the last.rulesetsis the five hardcoded finder names (that is Java's ruleset vocabulary),unresolvedcounts per-finder near-misses,errorsrecords a finder that threw. Always present, even when empty — the pass under-approximates by design, so an absent report and an empty one must not read the same.Scope guard — explicitly OUT:
:PyCommentnode type incodeanalyzer/neo4j/schema.py, andproject.py:843collapses comments to adocstringproperty exactly asV2GraphProjector.docstringOfdoes. python-sdk documents it as a deliberate ceiling (neo4j_backend.py:73). No parity gap, no term to match — closed by fact. Projecting comments remains available later as a new capability, not as this parity fix.can://id missing from the:JApplicationroot (codeanalyzer-schema#5).Release plan
2.0.0.V2SchemaCatalog.SCHEMA_VERSIONis deliberately held there; the class javadoc's "2.2.0" is stale text. This analyzer already drifted to 2.1.0/2.2.0/3.0.0 alone while codeanalyzer-python stayed at 2.0.0, and that drift was undone. Moving it is a coordinated re-baseline across all three analyzers — Epic: scope every destructive Neo4j statement on the can:// id prefix; retire _module #50, not this epic. Recorded consequence: a consumer cannot detectJ_USES_CONFIGor the entrypoint properties fromschema_versionalone. Detection is by presence until Epic: scope every destructive Neo4j statement on the can:// id prefix; retire _module #50 lands, the same gap the_moduleremoval already lives with.get_config_readers()returning an unconditional[],get_entrypoint_coverage()reportingentrypoint_report_unavailable— stays unfixed until child 4 ships in a later cycle.Planned children, filed just-in-time as each is picked up:
codeanalyzer-java— literal tier: detectors, resolver, both new JSON lists, both new relationship types.codeanalyzer-java— dataflow tier over the L3/L4 DDG,prov: ["dataflow"].codeanalyzer-java— entrypoint report: model, finder plumbing, both projections, docs.python-sdk— Java facade accessors. Deferred.Definition of done (epic-level)
J_USES_CONFIGis non-empty, everydstresolves to a projected:ConfigKey, and everysrcresolves to a projected:JBodyNode/:JCallable/:JField/:JType. No dangling endpoints.MATCH (a:JApplication) RETURN any(k IN keys(a) WHERE k CONTAINS 'entrypoint')returnsTRUE.config_uses(-a 1) ⊆ config_uses(-a 3) ⊆ config_uses(-a 4).analysis.v2.schema.jsonadmits the three new application-level members; the L1 conformance gate stays green.entrypoint_report_jsonparses with the same model shape as codeanalyzer-python's, and is present on every projection including one with zero entrypoints.