Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
63e70fa
refactor(commons): lift the language-neutral bounds, paging and key h…
rahlk Sep 6, 2026
943b934
docs(plan): leg 2.5a — tasks 0-4; T0 recorded as done; TS-5..TS-11 fr…
rahlk Sep 6, 2026
ccf384a
feat(typescript): v2 models mirroring codeanalyzer-typescript 1.2.0
rahlk Sep 6, 2026
14ffb0b
docs(plan): leg 2.5a — T1 as done; synthesized_callables keying and t…
rahlk Sep 6, 2026
bf187c4
feat(typescript): inherit the generic backend ABC and drive codeanaly…
rahlk Sep 6, 2026
35b400f
fix(typescript): address Task 2 review
rahlk Sep 6, 2026
7d1c70a
docs(typescript): record the per-label seek measurements and the writ…
rahlk Sep 6, 2026
c794c57
feat(typescript): Neo4j backend on the codeanalyzer-typescript 1.2.0 …
rahlk Sep 6, 2026
f85926f
fix(typescript): address Task 3 review
rahlk Sep 6, 2026
536ed55
refactor(commons): lift _semver and the artifact reconstructors
rahlk Sep 6, 2026
cd10133
docs(typescript): record the schema-v2 migration and the 1.2.0 pin
rahlk Sep 6, 2026
8240493
docs(typescript): tick Task 2 and correct the plan's scope-predicate …
rahlk Sep 6, 2026
0170f67
fix(typescript): address the leg-2.5a review — populate the graph-bac…
rahlk Sep 6, 2026
f34af3f
docs(typescript): implementation plan for leg 2.5b — the query surface
rahlk Sep 6, 2026
173c5e7
refactor(commons): language-neutral BodyRef and Span; pin codeanalyze…
rahlk Sep 6, 2026
ea73c0a
feat(typescript): addressing surface — locate, resolve, source, describe
rahlk Sep 6, 2026
7b7a05f
feat(typescript): per-callable graphs, slices, reachability and flow …
rahlk Sep 6, 2026
49df63e
feat(typescript): entrypoints and the artifact layer on the facade; r…
rahlk Sep 7, 2026
6cb2945
ci(release): bundle the pinned codeanalyzer-java jar, not releases/la…
rahlk Sep 6, 2026
6dd3813
docs: spec leg 3 — Java to Python's leg-1.6 maturity (#338)
rahlk Sep 6, 2026
197b1ba
docs(java): implementation plan for leg 3a — Java on schema v2
rahlk Sep 6, 2026
8f486b3
test(java): codeanalyzer-java 3.0.1 fixtures for daytrader8 at L1 and L4
rahlk Sep 6, 2026
3b7ba78
test(java): point the Java suites at the 3.0.1 fixtures, retire the 2…
rahlk Sep 6, 2026
20c6bcb
test(java): store the 3.0.1 fixtures gzip-compressed
rahlk Sep 6, 2026
41c366a
feat(java): v2 models mirroring codeanalyzer-java 3.0.1, v1 field nam…
rahlk Sep 6, 2026
029aa33
fix(java): address Task 1 review
rahlk Sep 6, 2026
12c86bb
feat(java): inherit the generic backend ABC, drive codeanalyzer-java …
rahlk Sep 6, 2026
5d52c8b
feat(java): Neo4j backend on the codeanalyzer-java 3.0.1 vocabulary, …
rahlk Sep 6, 2026
c078531
fix(java): qualify local-class names by their declaring callable, hon…
rahlk Sep 6, 2026
ba7e505
docs(java): record the schema-v2 migration, the 3.0.1 floor and the s…
rahlk Sep 6, 2026
f6e30b0
feat(java): drive the analyzer through the codeanalyzer-java wheel an…
rahlk Sep 6, 2026
994b926
fix(java): report the analyzer's own L3/L4 degradation instead of ret…
rahlk Sep 6, 2026
48b522f
fix(java): answer get_test_methods off the graph, repair the changelo…
rahlk Sep 6, 2026
bb47baf
docs: one release-notes block for the three legs
rahlk Sep 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 4 additions & 41 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ jobs:
- name: Check out code
uses: actions/checkout@v4

# Load-bearing for **Maven**, not for the analyzer. The analyzer jar and the JVM it runs
# on come from the `codeanalyzer-java` wheel (#339), but codeanalyzer-java's own auto-build
# shells out to `mvn`, which needs a JDK on PATH; without one the Java e2e degrades to a
# declared-only call graph and CI goes green on a partial answer. Do not delete this step.
- name: Set up GraalVM CE Java 11
uses: graalvm/setup-graalvm@v1
with:
Expand Down Expand Up @@ -65,50 +69,9 @@ jobs:
git push --delete origin ${GITHUB_REF#refs/tags/}
exit 1

- name: Inject the latest Code Analyzer JAR
run: |
# The release has multiple .jar assets (the versioned codeanalyzer-<v>.jar and an
# unversioned codeanalyzer.jar) — select only the versioned one so $CODE_ANALYZER_URL
# is a single URL.
CODE_ANALYZER_URL=$(curl -s https://api.github.com/repos/codellm-devkit/codeanalyzer-java/releases/latest | jq -r '.assets[] | select(.name | test("^codeanalyzer-[0-9].*\\.jar$")) | .browser_download_url')
echo "Downloading: $CODE_ANALYZER_URL"
wget -q "$CODE_ANALYZER_URL"
mkdir -p ${{ github.workspace }}/cldk/analysis/java/codeanalyzer/jar/
mv codeanalyzer-*.jar ${{ github.workspace }}/cldk/analysis/java/codeanalyzer/jar/

- name: Build Package
run: uv build

- name: Verify the codeanalyzer JAR is bundled
# Guard against the hatchling/.gitignore regression (issue #284): a jarless wheel
# installs fine but fails at runtime with "codeanalyzer jar not found". Fail the
# release here rather than publish a broken artifact to PyPI.
#
# The listing is captured before grepping: piping `tar tzf` (which decompresses the
# whole 32MB sdist) straight into `grep -q` lets grep close the pipe on first match,
# SIGPIPE-killing tar and — under `pipefail` — reporting a false "missing JAR".
run: |
set -euo pipefail
jar_re='codeanalyzer/jar/codeanalyzer-[0-9][^/]*\.jar$'
fail=0
for f in dist/*.whl dist/*.tar.gz; do
case "$f" in
*.whl) listing=$(unzip -l "$f") ;;
*.tar.gz) listing=$(tar tzf "$f") ;;
esac
if grep -qE "$jar_re" <<<"$listing"; then
echo " ✓ $f"
else
echo "::error::$f is missing the codeanalyzer JAR"
grep -i '\.jar' <<<"$listing" || echo " (no .jar entries at all)"
fail=1
fi
done
if [ "$fail" -ne 0 ]; then
echo "Refusing to publish a jarless release."; exit 1
fi
echo "codeanalyzer JAR present in wheel and sdist ✓"

- name: Extract release notes from CHANGELOG.md
id: notes
# Source the release body from the hand-written CHANGELOG.md section for this tag —
Expand Down
11 changes: 5 additions & 6 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
.mtj.tmp/

# Package Files #
*.jar
*.war
*.nar
*.ear
Expand Down Expand Up @@ -53,11 +52,11 @@ scratch*
*.json
!devcontainer.json

# Blessed TS unit-test fixture: hand-built from a sample app whose source (incl. src/external.ts)
# was never committed, so it cannot be regenerated by running codeanalyzer-typescript again. The
# bulk-accessor tests assert exact-set constants (signature counts, ownerless sets) against this
# exact file -- losing it breaks the suite for every fresh clone (#298).
!tests/resources/typescript/analysis_json/slim/analysis.json
# Blessed TS unit-test fixtures: analysis.json at each level, generated by the pinned
# codeanalyzer-typescript from tests/resources/typescript/application (see the README there), and
# the sample app's own manifests, which the analyzer's artifact layer reads.
!tests/resources/typescript/analysis_json/v2/*/analysis.json
!tests/resources/typescript/application/*.json


# Python compiled files and env
Expand Down
85 changes: 85 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,91 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Three legs of the 2.0 line: **TypeScript on schema v2** (2.5a) and **its query surface** (2.5b), and
**Java on schema v2** (3a, with the analyzer wheel and honest degradation reporting). Design records:
`docs/design/specs/2026-09-06-leg-2.5-typescript.md` and `docs/design/specs/2026-09-06-leg-3-java.md`.

### Breaking

- **Java: `get_call_graph()` nodes are `"<type fqn>.<signature>"` strings, not `(signature, klass)` tuples.**
Take the owning class from `cg.nodes[key]["method_detail"].klass` rather than splitting the key.
- **Java: single-file `source_code` mode is gone.** Analyze the project directory instead.
- **Java: a local or anonymous class's qualified name carries its declaring callable** (`p.Outer.m(int).$anon$0`).
Without it, sibling callables collide. Take class keys from `get_all_classes()` rather than composing them.
- **A graph emitted below the analyzer floor is refused at attach** with `GraphSchemaMismatch` instead of
answering every query with zero rows: codeanalyzer-java 3.0.1 and codeanalyzer-typescript 1.3.0. Re-emit;
there is no in-place upgrade. `TSNeo4jBackend` also speaks a graph vocabulary that shares nothing with 0.4.3's.
- **Values that changed shape:** `JGraphEdges` and `TSCallEdge` are now `{src, dst, prov, weight}`; Java's
`calling_lines` is a sorted list of absolute file lines; Java's `get_config_keys()` is keyed by the
artifact-relative key; Java's `get_test_methods()` reads the analyzer's annotations rather than re-parsing
source; `TSCallable` lost `path`/`call_sites`/`accessed_symbols`/`local_variables`/`code_start_line` and
`TSModule` lost `file_path`/`module_name`, since v2 keys modules by path and stores source once;
`TSCallableOverview.from_callable` takes a required keyword-only `path`.
- **Removed:** `TypeScriptAnalysis.get_entry_point_methods` and `get_service_entry_point_methods`, which only
ever raised — the working entrypoint accessors below replace them.

### Added

- **The agent-facing query surface on `TypeScriptAnalysis` — 29 accessors, each with `PythonAnalysis`'s
signature and semantics.** Addressing (`locate`, `locate_many`, `resolve_callable`, `resolve_value`,
`get_source`, `describe`, `has_resolution_edges`); per-callable graphs and dataflow (`get_cfg`/`get_cdg`/
`get_ddg`, `slice_backward`/`slice_forward`/`backward_cone`, `reaches`, `callers_of`/`callees_of`,
`paths_between`/`call_paths_between`, `flows_to_call`/`flows_to_argument`); entrypoints
(`get_entrypoints`, `get_entrypoint_classes`, `get_entrypoint_coverage`, `get_config_readers`); and the five
repository-artifact getters. Both backends answer identically, including on the miss paths.
- **Java reaches analysis levels 3 and 4** — control flow, control and data dependence, and the interprocedural
graph. The level now reaches the analyzer, which it never did before.
- **A `java` install extra.** `pip install "cldk[java]"` brings the analyzer and its bundled JVM;
`pip install "cldk[all]"` reproduces the previous behaviour. Bare `cldk` no longer carries either. The Python
and TypeScript analyzers are still installed unconditionally; #340 moves them into extras of their own.
- **JavaScript modules are in scope** for TypeScript analysis, under their own id prefix.
- **A degraded Java level-3 or level-4 run is reported rather than silent.** Those levels need compiled classes;
without them the analyzer emits a declared-only graph and still reports the level. The SDK now surfaces the
analyzer's own warnings and records them beside the payload, so a cached run still knows. The graph is
returned either way.
- `cldk.models.typescript.TSClassOverview`, the class-level projection `get_entrypoint_classes` returns.

### Changed

- **Pins:** `codeanalyzer-java` 2.4.1 → 3.0.2, `codeanalyzer-typescript` 0.4.3 → 1.3.0.
- **The Java analyzer ships as a wheel, not a jar in this repo.** The 35 MB checked-in jar, the Temurin download
in `_jdk.py`, and the release workflow's jar injection are gone; no `JAVA_HOME` is read or set, and no JDK is
downloaded. The published wheel drops from about 35 MB to 320 KB.
- **Both languages' backends inherit the generic `AnalysisBackend`**, so each answers the shared artifact,
dependency and configuration accessors.
- **Where a backend cannot answer, it says so instead of returning an empty value.** On Neo4j: Java's file-keyed
comment accessors, and TypeScript's `get_imports`, `get_all_exports`, `get_unresolved_config_reads`,
`get_method_parameters` for a found method, and `get_extended_classes`/`get_implemented_interfaces` when the
relationship type is absent. The remaining documented gaps, and where the two backends legitimately differ,
are listed in `docs/agent-api-reference.md`.
- **Every Cypher statement is scoped per bound variable, not per statement** — both endpoints of a call edge, a
quantified path's far end, and a slice's reached body nodes. A statement that matched one endpoint by a
signature two applications both declare could previously return the other application's node.
- **`LocateResult.body` is a language-neutral `BodyRef`**, so `cldk/analysis/commons/` no longer imports a
language package for it.
- Internal: the language-neutral query helpers moved from `cldk/analysis/python/` to `cldk/analysis/commons/`;
Python re-imports every name unchanged.

### Fixed

- **`JavaAnalysis.get_method_parameters()`** is annotated `List[JCallableParameter]`, which is what it has always
returned.
- **`get_call_graph()` on Java no longer re-parses each method body once per edge** — 145.7s to 41.0s on a
4,100-file project.
- Two documentation errors: `EntrypointCoverage.unresolved` is a `dict[str, int]`, not a `list[str]`; and the
`get_config_keys()` example used a key form that never worked.

### Known limitations

- Java CRUD accessors raise: schema v2 does not carry CRUD yet (codeanalyzer-java#187).
- The Java graph's `JCallable.code` is the declaration slice where the JSON's is the body block, and the graph
cannot recover the body block (codeanalyzer-java#176).
- TypeScript's DDG has one provenance tier; Java's has two; Python's has three.
- codeanalyzer-typescript mints one id for a value and a type of the same name under declaration merging
(codeanalyzer-typescript#177); such a node resolves to the facet its kind names, or not at all.
- `get_config_keys()` is still keyed by a `can://` id on Python and TypeScript, where Java now uses the
artifact-relative key (#346).

## [v2.0.0-rc.2] - 2026-09-06
Python legs 1, 1.5 and 1.6 of the CLDK 2.0 agent-facing query facade (see
`docs/design/specs/2026-09-03-agent-facing-query-facade.md`,
Expand Down
9 changes: 7 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,14 @@ an optional read-only Neo4j backend — selected by the *type* of the `backend=`

| Language | Entry point | Local backend | Neo4j backend | Models |
|----------|-------------|---------------|---------------|--------|
| Java | `CLDK.java(...)` | `JCodeanalyzer` (bundled JAR, subprocess) | `JNeo4jBackend` | `cldk/models/java/` |
| Java | `CLDK.java(...)` (needs the `cldk[java]` extra) | `JCodeanalyzer` (the `codeanalyzer-java` 3.0.2 wheel's jar on its bundled JVM, subprocess, `-a 1..4` — no jar in this repo, no JDK download) | `JNeo4jBackend` (3.0.1 graph, probed at attach) | `cldk/models/java/` (schema v2 mirror) |
| Python | `CLDK.python(...)` | `PyCodeanalyzer` (in-process `codeanalyzer-python`) | `PyNeo4jBackend` | re-exported from `codeanalyzer-python` |
| TypeScript | `CLDK.typescript(...)` | `TSCodeanalyzer` (`codeanalyzer-typescript` binary, subprocess) | `TSNeo4jBackend` | `cldk/models/typescript/` |
| TypeScript (+ JavaScript modules) | `CLDK.typescript(...)` | `TSCodeanalyzer` (`codeanalyzer-typescript` 1.3.0 binary from the wheel, subprocess; `-a 1..4`, but `--emit neo4j` takes no `-a` and is always full depth) | `TSNeo4jBackend` (graphs emitted by ≥ 1.3.0; older refused at attach) | `cldk/models/typescript/` (schema v2 mirror) |

**Java, since leg 3a (#310):** the models are an `extra="forbid"` mirror of canonical schema v2, so
a 1.x `analysis.json` (and a pre-3.0.1 Neo4j graph) is refused, not parsed; `get_call_graph()` keys
nodes by the string `"<type fqn>.<signature>"`; the `source_code` single-file mode is gone; the CRUD
accessors raise (codeanalyzer-java#187). The leg-1.5/1.6 query surface reaches Java in 3b (#311).

The legacy `CLDK(language="<lang>").analysis(...)` entry still works as a compat shim. Adding a
language means a new factory method + facade + backend ABC/impl(s) + models + tests — **update this
Expand Down
15 changes: 2 additions & 13 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,9 @@ clean: ## Cleans up from previous compiles
$(info Cleaning up compile artifacts...)
rm -fr dist

.PHONY: refresh
refresh: ## Refresh code analyzer
$(info Refreshing CodeAnalyzer...)
wget $(curl -s https://api.github.com/repos/IBM/codenet-minerva-code-analyzer/releases/latest | grep "browser_download_url" | grep codeanalyzer.jar | cut -d '"' -f 4)
mv codeanalyzer.jar cldk/analysis/java/codeanalyzer/jar/codeanalyzer.jar

.PHONY: build
build: ## Builds a new Python wheel
$(info Building artifacts...)

# Inject the latest Code Analyzer JAR
wget -q $(shell curl -s https://api.github.com/repos/IBM/codenet-minerva-code-analyzer/releases/latest | jq -r '.assets[] | .browser_download_url')
mkdir -p cldk/analysis/java/codeanalyzer/jar/
mv codeanalyzer-*.jar cldk/analysis/java/codeanalyzer/jar/

# Build the package
# No jar is fetched or injected: the Java analyzer is the `codeanalyzer-java` wheel
# (the `java` extra), a normal locked dependency. Nothing is downloaded at build time.
uv build
105 changes: 105 additions & 0 deletions cldk/analysis/commons/artifacts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
################################################################################
# Copyright IBM Corporation 2026
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
################################################################################


"""The shared repository-artifact layer, rebuilt from the Neo4j projection: property maps →
``PyArtifact`` / ``PyConfigKey`` / ``PyDependency``.

Every codeanalyzer projects this layer identically and unprefixed (``:Artifact``, ``:ConfigKey``,
``:Package``; ``HAS_ARTIFACT``, ``DEFINES_CONFIG``, ``DECLARES_DEPENDENCY``, ``LOCKS``), and the
generic ABC (:mod:`cldk.analysis.commons.backend`) promises the same four ``Py*`` models from every
language, so the reconstructors live here once. Lifted verbatim from
``cldk/analysis/python/neo4j/reconstruct.py`` (leg 2.5a), which re-exports them.
"""

from __future__ import annotations

from typing import Any, List, Mapping

from cldk.models.python import PyArtifact, PyConfigKey, PyDependency

Props = Mapping[str, Any]


def config_key(props: Props) -> PyConfigKey:
"""Rebuild a :class:`PyConfigKey` from a ``:ConfigKey`` node's properties.

Line-only ``span`` (see :func:`body_node`): the projection writes ``start_line``/``end_line``
and nothing finer, so the columns and byte offsets rehydrate as ``0``. ``span`` stays ``None``
when the node carries no lines at all (best-effort extraction never located the key in the
artifact's source). It is spelled as a mapping rather than built from
``cldk.models.python.Span``: ``PyConfigKey.span`` is annotated on *that* class, so only it
validates -- and importing it here would put one language's declaration schema back into
``commons/`` (TS-1), which the shared artifact layer is exempt from only for the five ``Py*``
models this module rebuilds.
"""
lines = (props.get("start_line"), props.get("end_line"))
return PyConfigKey(
id=props.get("id", ""),
key=props.get("key", ""),
namespace=props.get("namespace", ""),
value=props.get("value"),
span={"start": (lines[0], 0), "end": (lines[1], 0), "bytes": (0, 0)} if None not in lines else None,
references=list(props.get("references", []) or []),
)


def artifact(props: Props, *, config_keys: List[PyConfigKey] | None = None) -> PyArtifact:
"""Rebuild a :class:`PyArtifact` from an ``:Artifact`` node's properties plus its fetched
:class:`PyConfigKey` children (``[:DEFINES_CONFIG]``).

``kind`` is not a projected property — every ``PyArtifact`` the analyzer emits carries the
model's own default (``"artifact"``; see ``codeanalyzer/artifacts/discovery.py``), so it is
supplied here rather than queried for.
"""
return PyArtifact(
id=props.get("id", ""),
kind="artifact",
path=props.get("path", ""),
format=props.get("format", ""),
roles=list(props.get("roles", []) or []),
size_bytes=props.get("size_bytes", 0),
sha256=props.get("sha256", ""),
source=props.get("source", ""),
extraction=props.get("extraction", "none"),
config_keys=config_keys or [],
)


def dependency(props: Props, *, name: str, ecosystem: str, declared_in: str) -> PyDependency:
"""Rebuild a :class:`PyDependency` from a ``[:DECLARES_DEPENDENCY]`` edge's properties plus its
endpoints (``name``/``ecosystem`` off the ``:Package`` node, ``declared_in`` off the
``:Artifact`` node). ``ecosystem`` is a real ``Package`` property (``neo4j/schema.py``'s
``Package`` node type carries it); ``"pypi"`` is only ever what the analyzer happens to write
there today (its only ecosystem, per ``PyDependency.ecosystem``'s own docstring) — read off the
node rather than hardcoded, so this doesn't silently go stale the day a second ecosystem ships.

``locked_version``/``provides_imports`` are projection-lossy here: the graph carries them on
the separate ``[:LOCKS]``/``[:PY_PROVIDES]`` edges (per-package facts, not per-declaration), and
no caller of this reconstruction chases those yet, so they come back at the model's own empty
defaults — the same class of gap :func:`callsite` documents for ``argument_types``.
"""
return PyDependency(
name=name,
ecosystem=ecosystem,
spec=props.get("spec", ""),
kind=props.get("kind", "runtime"),
extras=list(props.get("extras", []) or []),
declared_in=declared_in,
direct=props.get("direct", True),
provides_imports=[],
prov=list(props.get("prov", []) or []),
)
Loading