Skip to content

feat(entrypoints): report on the application root, framework attribution on the node - #235

Merged
rahlk merged 2 commits into
mainfrom
feat/issue-234-entrypoint-report
Sep 7, 2026
Merged

feat(entrypoints): report on the application root, framework attribution on the node#235
rahlk merged 2 commits into
mainfrom
feat/issue-234-entrypoint-report

Conversation

@rahlk

@rahlk rahlk commented Sep 7, 2026

Copy link
Copy Markdown
Collaborator

Closes #234. Spec: codellm-devkit/.github docs/design/specs/2026-09-07-java-config-reads-and-entrypoint-report.md (decision D31, codellm-devkit/.github#63), epic codellm-devkit/.github#62.

What

:JApplication carried exactly name, schema_version, analyzer_name, analyzer_version. The analyzer marks 2,604 :JEntrypoint nodes and said nothing about how the pass that found them behaved, so a consumer could not tell "this application has no entrypoints" from "the pass found nothing". The five finders returned bare booleans and TypeBuilder.java:66 / CallableBuilder.java:92 collapsed them with anyMatch, so even which framework matched was discarded.

  • JEntrypointReport{frameworks_detected, rulesets, unresolved, errors} on application.entrypoint_report — codeanalyzer-python's four keys, so one SDK model parses either analyzer's report
  • entrypoint_frameworks: string[] on type and callable
  • entrypoint_frameworks and entrypoint_report_json on :JApplication; entrypoint_frameworks on :JType / :JCallable
  • AbstractEntrypointFinder.framework() so attribution survives the collapse

The load-bearing decision: attribution lives on the node

L1Extractor.java:139-145 reuses a cached module byte-for-byte and skips the build, so the finders never run for that file. A tally kept during the walk would under-report frameworks_detected on a warm cache — the exact ambiguous empty this record exists to prevent. The tree carries the attribution whether the module was rebuilt or reused, and the application-level union is taken over the built tree at assembly time.

It is also more python parity, not less: PySymbol/PyCallable already carry is_entrypoint alongside entrypoint_frameworks (schema.py:110-111,133-134).

Two traps worth reviewing

Prune order. entrypoint_frameworks is set on the application node after RowBuilder.prune. It is an empty list on exactly the applications the report was written for, and prune drops empty collections — set it before, and the property silently vanishes from the only graphs that needed it. theApplicationNodeCarriesTheReportEvenWithZeroEntrypoints pins this.

rulesets is not decoration. An empty frameworks_detected next to a populated rulesets is the signal. Emitting the report only when non-empty would defeat the whole record, so it is emitted unconditionally — the one field on JApplication that deliberately breaks the "absence means no fact" rule.

Verification

daytrader8, -a 1 --no-build:

entrypoint classes / methods 68 / 135
frameworks_detected [jakarta, jaxrs, spring], all ⊆ the five rulesets
boolean vs. attribution 0 disagreements across the tree
schema conformance validates against analysis.v2.schema.json
warm cache -c <dir> twice ⇒ byte-identical report
graph :JApplication carries both properties; MATCH (a:JApplication) RETURN any(k IN keys(a) WHERE k CONTAINS 'entrypoint') is now TRUE

EntrypointReportTest covers the populated case, the boolean/attribution invariant, rulesets containment, and — the ones that matter — the zero-entrypoint fixture and the pruning trap.

Full suite: 525 tests, 1 failure — CodeAnalyzerIntegrationTest cannot reach a Docker daemon on this machine and fails identically on a clean main.

Known limitations, named rather than left implicit

Contract version

Stays at 2.0.0. A re-baseline is codellm-devkit/.github#50.

…ion on the node

`:JApplication` carried exactly `name`, `schema_version`, `analyzer_name` and
`analyzer_version`. The analyzer marked 2,604 `:JEntrypoint` nodes on a
daytrader8 + thingsboard projection and said nothing about how the pass that
found them behaved, so a consumer could not tell "this application has no
entrypoints" from "the detection pass found nothing". The five finders returned
bare booleans and `TypeBuilder`/`CallableBuilder` collapsed them with `anyMatch`,
so even which framework matched was discarded.

Adds `JEntrypointReport{frameworks_detected, rulesets, unresolved, errors}` —
codeanalyzer-python's four keys, so one SDK model parses either analyzer's
report — on `application.entrypoint_report`, plus `entrypoint_frameworks` on
`type` and `callable`, and the two properties python already projects onto its
application root.

Attribution lives on the NODE, not in a run-scoped tally, and that is the load-
bearing decision. `L1Extractor` reuses a cached module byte-for-byte and skips
the build, so the finders never run for that file; a tally kept during the walk
would under-report `frameworks_detected` on a warm cache — the exact ambiguous
empty this record exists to prevent. The tree carries the attribution whether the
module was rebuilt or reused, and the application-level union is taken over the
built tree at assembly time. It also happens to be more python parity, not less:
`PySymbol`/`PyCallable` already carry `is_entrypoint` alongside
`entrypoint_frameworks`.

`rulesets` is the five finder names — Java's finders are hardcoded classes rather
than rule files, so the finder set IS the ruleset vocabulary. Emitting it
alongside an empty `frameworks_detected` is what says the pass looked and came
back empty-handed.

The report is projected unconditionally, and `entrypoint_frameworks` is set on
the application node AFTER prune: it is an empty list on exactly the
applications the report was written for, and prune drops empty collections.

Verified on daytrader8 (`-a 1`, `--no-build`): 68 entrypoint classes, 135
entrypoint methods, `frameworks_detected` = [jakarta, jaxrs, spring] against five
rulesets, zero disagreements between the boolean and the attribution, and the
payload validates against `analysis.v2.schema.json`. A warm-cache run produces a
byte-identical report to the cold one.

Known limitation, named rather than left implicit: `unresolved` and `errors`
capture a finder that THROWS, and the finders currently swallow their own
resolution failures internally (`StrutsEntrypointFinder.java:41` logs and returns
false), so those near-misses are not yet counted. Finder accuracy is out of scope
here.

`schema.neo4j.json` regenerated with `--emit schema`.

Closes #234
Resolves three "both sides added adjacent content" conflicts by keeping both
halves: the application-scope wiring in CodeAnalyzer, the two field groups on
JApplication, and the new `$defs` in the conformance schema.

Regenerates `schema.neo4j.json` with `--emit schema` so the checked-in snapshot
carries both changes. It is a whole-catalog artifact, not a per-change diff, so
each branch had regenerated it against itself and neither copy was complete.

Verified on the merged tree: 535 tests green apart from the Docker-dependent
integration test, and a daytrader8 `-a 1` run emits config_uses (15), the
unresolved reads (12) and the entrypoint report together, validating against
analysis.v2.schema.json.
@rahlk
rahlk merged commit 52212d2 into main Sep 7, 2026
@rahlk rahlk mentioned this pull request Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Entrypoint report on the application root, and per-framework attribution on the node

1 participant