Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
53 changes: 46 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,31 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

Python legs 1 and 1.5 of the CLDK 2.0 agent-facing query facade (see
`docs/design/specs/2026-09-03-agent-facing-query-facade.md` and
`docs/design/specs/2026-09-05-leg-1.5-bounded-queries-and-dataflow.md`). Targets `2.0.0-rc.1`.
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`,
`docs/design/specs/2026-09-05-leg-1.5-bounded-queries-and-dataflow.md` and
`docs/design/specs/2026-09-06-leg-1.6-id-prefix-scoping.md`). Targets `2.0.0-rc.2`.

### Changed
- **Pinned `codeanalyzer-python` 1.4.0 → 1.4.1** (`pyproject.toml` `dependencies` and
`[tool.backend-versions]`). 1.4.1 removes the `_module` node property from every graph it emits
(upstream #183), so the Neo4j backend no longer scopes on it: **every statement is scoped to the
application by its `can://` id prefix** (`n.id STARTS WITH 'can://python/<app>/'`), the rule the
analyzer's own destructive statements use, and a callable's repo-relative `path` is derived from
its id and verified against the application's module keys rather than projected from a property.
No accessor changes name, signature, return type or value; the `_module` scoping is simply gone.
Ghosts (`:PyExternal`) fall inside the prefix, so every call-graph walk pins its traversal source
to `:PyCallable` by label -- an `@external` node is still reached and never traversed through.
- **The schema probe reads the graph's analyzer generation.** Attaching to a graph whose
`:PyApplication.analyzer_version` is below **1.4.0** (or missing) raises `GraphSchemaMismatch`
naming the version found and the floor -- before this it was served with silent empties. A
**1.4.0** graph and a **1.4.1** graph are served identically and silently: both carry the unique
`:PySymbol(id)` range index, so `locate` / `locate_many` and `resolve_callable` anchor on
`(c:PyCallable:PySymbol)` and the prefix *seeks* on either generation (40 positions: 381 → 46 ms
on 1.4.1, 427 → 53 ms on 1.4.0; `resolve_callable` 19.2 → 15.3 ms on 1.4.1, a wash on 1.4.0).
1.4.1's `:PyCanNode(id)` index was measured and rejected -- it spans all 955,961 application
nodes, so seeking it made `resolve_callable` 10× slower -- and no statement names it. Statements
whose prefix is the whole application stay on the `:PyCallable` label scan, which is faster there.
- **BREAKING: Neo4j graph vocabulary migration.** The Python Neo4j backend
(`cldk.analysis.python.neo4j.PyNeo4jBackend`) now queries `PyBodyNode` / `PY_HAS_BODY_NODE`
instead of the pre-1.4.0 `PyCallSite` / `PY_HAS_CALLSITE` / `PySymbol` vocabulary, matching what
Expand Down Expand Up @@ -43,8 +63,9 @@ Python legs 1 and 1.5 of the CLDK 2.0 agent-facing query facade (see
`PyClassOverview.path` and the keys of `get_symbol_table()`, which it previously could not be
joined against. Both backends changed together: the Neo4j projections behind
`get_callables_overview()`, `get_decorated_callables()`, `get_entrypoints()` and
`get_config_readers()` now read `:PyCallable._module` rather than `:PyCallable.path`, and the
local backend projects the symbol table key rather than `PyCallable.path`.
`get_config_readers()` now project the module key rather than `:PyCallable.path` (from the
`_module` property on a 1.4.0 graph in leg 1; derived from the node's `can://` id since leg 1.6),
and the local backend projects the symbol table key rather than `PyCallable.path`.
**Migration:** a caller that stored or persisted these paths will see different strings for the
same callable, and one that stripped a project-root prefix off them must stop.

Expand Down Expand Up @@ -104,7 +125,7 @@ Python legs 1 and 1.5 of the CLDK 2.0 agent-facing query facade (see
| SDK version | Requires `codeanalyzer-python` | Graph vocabulary |
| --- | --- | --- |
| <= 1.5.0 | 0.3.x | `PyCallSite` / `PY_HAS_CALLSITE` / `PySymbol` |
| 2.0.0-rc.1 (this) | >= 1.4.0 | `PyBodyNode` / `PY_HAS_BODY_NODE` |
| 2.0.0-rc.2 (this) | 1.4.1 pinned; graphs emitted by >= 1.4.0 served identically | `PyBodyNode` / `PY_HAS_BODY_NODE`, scope by `can://` id prefix |

### Added
- **Slices and reachability: `slice_backward(src, within=)` / `slice_forward(src, within=)` /
Expand Down Expand Up @@ -312,6 +333,23 @@ Python legs 1 and 1.5 of the CLDK 2.0 agent-facing query facade (see
schema probe at attach time; see the breaking-change note above.

### Fixed
- **`get_entrypoint_coverage()` over Neo4j reads the projected report.** It answered with an
`entrypoint_report_unavailable` diagnostic unconditionally; codeanalyzer-python 1.4.1 (#182)
projects `entrypoint_frameworks` / `entrypoint_report_json` onto `:PyApplication`, so on such a
graph the answer is now the pass's own `PyEntrypointReport`, field for field what the local
backend returns. The diagnostic survives only for a graph that genuinely lacks the property (one
emitted by 1.4.0).
- **Resolved through the 1.4.1 pin** -- python-sdk's upstream reports #176 / #177 / #178, fixed
in codeanalyzer-python as #180 (body nodes and parameters carry their `id` in `analysis.json`;
the SDK's composed body-node id is now pinned equal to the analyzer's own per run), #182 / #185
(entrypoint report projected, Odoo `@http.route` / `http.Controller` detected: 534 callables and
94 classes on the same checkout that 1.4.0 flagged 0 / 0), #181 (`PY_EXTENDS` is actually emitted:
1,573 edges where every 2.0.0 graph had none) and #183 (`_module` retired, `:PyCanNode` range
index on `id`).
- **The N+1 timing assertion measured the coverage tracer, not the query.** The two timed live
tests (`get_symbol_table` / `get_classes` under 15 s) ran under `sys.settrace`, which adds ~5 s
to a ~10 s Python-side reconstruction; they passed the ceiling by luck. They now run with coverage
paused (`pytest-cov`'s `no_cover`), so the ceiling measures the round trips it exists to bound.
- **Java: a missing JDK cache root is refused, not crashed on.** `JCodeanalyzer._get_codeanalyzer_exec`
now raises `CodeanalyzerExecutionException` ("no cache directory and no project directory")
when neither is available, instead of letting `ensure_jdk` fail with `TypeError: ... not
Expand All @@ -328,7 +366,8 @@ Python legs 1 and 1.5 of the CLDK 2.0 agent-facing query facade (see
child fetches (`get_class()` and everything reconstructing one declaration) matched by a bare
`{signature: $sig}` / `{file_key: $fk}` while their bulk twins were application-scoped, so in a
database holding two applications `get_class()` merged another application's members and
`get_all_classes()` did not. All twelve carry the same `_module IN $mods` predicate now, and so
`get_all_classes()` did not. All twelve carry the same application-scope predicate now (`_module
IN $mods` when this landed, the `can://` id prefix since leg 1.6), and so
do the leg-1.5 call-graph statements behind `reaches`, `backward_cone`, `call_paths_between` and
`flows_to_call`, which had matched by signature unscoped too. Statements keyed only by a
body-node or ghost id (`slice_*`, `paths_between`, the value-reachability predicate) are scoped
Expand Down
7 changes: 4 additions & 3 deletions cldk/analysis/commons/results.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,9 +263,10 @@ class EntrypointCoverage(BaseModel):
unresolved: Count of near-misses, keyed by rule/framework, that could not be resolved to a
definite entrypoint — non-zero counts are exactly the under-approximation gap.
errors: Hard failures the detection pass hit while running.
diagnostics: Non-empty when a backend cannot supply this report at all: the Neo4j
projection does not carry ``PyApplication.entrypoint_report`` on the graph (only the
derived ``is_entrypoint``/``entrypoint_frameworks`` per-node properties), so it returns
diagnostics: Non-empty when a backend cannot supply this report at all: a Neo4j graph
emitted by codeanalyzer-python 1.4.0 does not carry ``PyApplication.entrypoint_report``
(only the derived ``is_entrypoint``/``entrypoint_frameworks`` per-node properties;
1.4.1 projects it onto ``:PyApplication``), so the Neo4j backend returns
``entrypoint_report_unavailable`` here instead of fabricating empty-but-clean-looking
fields — the same "say so honestly" precedent as ``LocateResult``'s
``module_source_unavailable``. When ``diagnostics`` is non-empty, the other fields are
Expand Down
6 changes: 3 additions & 3 deletions cldk/analysis/python/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -894,9 +894,9 @@ def get_entrypoint_coverage(self) -> EntrypointCoverage:
so a caller can tell "the pass ran clean and found nothing" apart from "the pass had gaps"
— a distinction :meth:`get_entrypoints`'s empty list alone cannot make. See
:class:`~cldk.analysis.commons.results.EntrypointCoverage` for the field-by-field contract,
including the per-backend availability caveat (the Neo4j projection does not carry this
report at all; that backend answers with a ``diagnostics``-only result rather than
fabricating empty-but-clean-looking coverage fields)."""
including the per-backend availability caveat (a Neo4j graph emitted by codeanalyzer-python
1.4.0 does not carry this report; that backend then answers with a ``diagnostics``-only
result rather than fabricating empty-but-clean-looking coverage fields)."""

@property
@abstractmethod
Expand Down
Loading