Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
46dfd5a
feat(models): cpg — canonical schema-v2 models, modeled once (F2 / #2…
rahlk Jul 15, 2026
74510fb
feat(graph): L3/L4 program-slice engine core (Tasks 1–6 of #270) (#271)
rahlk Jul 15, 2026
4a43e75
feat(typescript)!: speak graph schema 2.0.0 — TS-prefixed vocabulary,…
rahlk Jul 15, 2026
8ba29ab
fix(typescript): guard ambiguous application match; mark 2.0.0 e2e-ve…
rahlk Jul 15, 2026
98ce326
feat(python)!: speak schema 2.0.0 — canonical model fields, CanNode c…
rahlk Jul 17, 2026
b3f3e8a
chore(release): 2.0.0-rc.1
rahlk Jul 17, 2026
87dd6a5
chore(design): track the facade decision log (.claude/FACADE_DECISION…
rahlk Jul 22, 2026
ec618bc
fix: backport JAR-bundling and release-workflow fixes to 2.0 (#284, #…
rahlk Jul 22, 2026
6f5ddde
feat(graph): language-neutral local cpg provider mixin (#270)
rahlk Jul 15, 2026
42e7317
feat(python): pass -a 1-4 through and record the envelope's max_level…
rahlk Jul 27, 2026
e001c72
fix(python): accept underscore analysis-level names in level plumbing…
rahlk Jul 27, 2026
9f39ef8
feat(python): local backend joins the program-graph seam; duck-type h…
rahlk Jul 27, 2026
e0ef44f
feat(python): five slice/flow facade verbs + cldk.graph public export…
rahlk Jul 27, 2026
88c1166
fix(test): correct mock ddg edge direction in facade delegate test (#…
rahlk Jul 27, 2026
09030fe
feat(python): Neo4j program-graph provider + backend ABC joins the se…
rahlk Jul 27, 2026
b353884
fix(python): source_slice degrades on synthetic parameter vertices in…
rahlk Jul 27, 2026
ed875de
test(python): live dual-backend parity + capability-degrade goldens (…
rahlk Jul 27, 2026
eef47fe
fix(python): Neo4j provider speaks the emitter's real can:// vertex i…
rahlk Jul 27, 2026
b0e00a3
fix(graph): index nested callables and inner-class methods in the loc…
rahlk Jul 27, 2026
688b371
fix(python): backend-identical resolve_location suffix match + source…
rahlk Jul 27, 2026
16e8e49
test(python): parity suite covers prov, source_slice, resolve_locatio…
rahlk Jul 27, 2026
54ef5be
chore(python): tidy guards, vacuous asserts, scoping symmetry, stale …
rahlk Jul 27, 2026
8af06cd
chore(design): log the staged L3/L4 wiring + provider contracts (#270)
rahlk Jul 27, 2026
049e600
merge release/2.0 — pick up the JAR-bundling/release-workflow backpor…
rahlk Aug 1, 2026
c3573ab
chore(python): bump codeanalyzer-python pin to 1.1.0 (#270)
rahlk Aug 1, 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
53 changes: 53 additions & 0 deletions .claude/FACADE_DECISIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Facade decisions — python-sdk

Decision log for SDK-surface design (per designing-cldk-changes, sdk-facade-design-loop).
One line per locked decision; newest section first.

## 2026-07-27 — L3/L4 verb wiring is staged per release

- **Staging:** rc.1 wires the five slice/flow verbs on the Python facade only (local + Neo4j);
rc.2 adds TypeScript; rc.3 adds the Java honest-degrade leg; rc.4 (Go) and rc.5 (C++) are
new-language legs entering via designing-cldk-changes (the C++ leg absorbs the existing C
facade); 2.0.0 final swaps the Rust query core (#279) in under the verbs, restores the
all-language DoD, and closes #270.
- **URI minting:** providers mint body-vertex URIs as `<callable_id>@<local_key sans leading @>`,
matching the analyzers' own param_in/param_out vocabulary (the real 1.0.2 Neo4j emitter stores
these can:// ids directly on PyCFGNode). Local keys never escape a provider.
- **source_slice contract:** existing span-less (synthetic) vertex → `(module_path, None)`;
unknown vertex → `(None, None)`; never derive a line from key shape. Both backends identical.
- **Rust hand-off unchanged:** the Rust query core (#279) replaces the Python engine underneath
these verbs post-M1; the facade surface added here is the stable contract it must satisfy.

## 2026-07-21 — Rust query engine (fluent API core)

- **M1 scope:** Epic B success criteria — the two Odoo PoE audit queries (#155), single-language,
in-memory + Neo4j backends, `.explain()` reproduces the manual audit evidence. Cross-service
(services/gRPC/proto, the RFC's boutique examples) is Epic E, out of scope; requires an
analyzer-side schema design that has not happened.
- **Identity scheme:** `can://` (what analyzers emit today), extended as needed. The 2026-07-09
fluent-query spec's `cldk://` is amended to `can://`; no parallel `service://`/`proto://`
schemes — Epic E extends the `can://` grammar instead.
- **Plan algebra:** redesigned fresh, taking the 2026-07-09 spec's six primitives
(Descend/Ascend/Relate/Filter/PathQuery/Project) and the RFC's LogicalOp sketch as inputs.
Deliverable: an algebra ADR locked before the Rust core builds.
- **Data plane:** `cldk-query-core` consumes schema-2.0.0 `analysis.json` natively (serde CPG
models) AND speaks Bolt directly (neo4rs) for the Neo4j backend. Core tests are cargo-only on
fixture JSONs; no Python in the core.
- **Opaque(fn):** plan-split semantics — Rust executes the prefix, returns URIs, Python applies
the lambda, execution re-enters Rust for remaining steps; `explain()` marks the split point.
- **Packaging:** fat wheel — `cldk` itself becomes a maturin/PyO3 platform wheel (abi3).
Consequence accepted: `cldk` is no longer pure-Python; release workflow becomes a per-platform
build matrix; platforms without a prebuilt wheel need a Rust toolchain for the sdist.
- **L3/L4 slicer:** the Rust core REPLACES the `cldk.graph` slice engine (#270/#271); the Python
engine is deprecated once the Rust slicer passes the same exact-set gates. Single dataflow
semantics owner; replacement staged post-M1.
- **Extraction boundary:** no PyO3 types/exceptions/callbacks in `cldk-query-core`; versioned
`PlanEnvelope` wire format (semver string, house convention, not u32); language-neutral result
structs; extraction only when independently consumed/released (per RFC criteria).
- **Repo layout (amends the RFC's `rust/crates/` sketch):** root-level `crates/` with the
workspace `Cargo.toml` at the repo root (polars/ruff idiom; canonical Cargo layout, zero-config
rust-analyzer, maturin driven from the root pyproject via
`tool.maturin.manifest-path = "crates/cldk-python/Cargo.toml"`). The extension module compiles
to the private submodule `cldk._native` — users import `cldk.query`; the public namespace never
admits Rust exists. Crate names unchanged: `cldk-query-core` (survives extraction),
`cldk-python` (bindings).
76 changes: 57 additions & 19 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,28 +65,66 @@ jobs:
- name: Build Package
run: uv build

- name: Read Changelog Entry
id: changelog_reader
uses: mindsers/changelog-reader-action@v2
with:
validation_level: warn
version: ${{ steps.tag_name.outputs.current_version }}
path: ./CHANGELOG.md

- name: Build Changelog
id: gen_changelog
uses: mikepenz/release-changelog-builder-action@v5
with:
failOnError: "true"
configuration: .github/workflows/release_config.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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 —
# deterministic and independent of PR labels — and refuse to publish/announce a blank
# body. The previous label-based changelog scraper emitted nothing for unlabeled PRs,
# which blanked the release and crashed the org announcement. See issue #289.
run: |
set -euo pipefail
version="${GITHUB_REF#refs/tags/}" # e.g. v1.4.4 — matches the "## [v1.4.4]" heading
notes=$(awk -v h="## [$version]" '
!seen && index($0, h) == 1 { seen = 1; next }
seen && index($0, "## [") == 1 { exit }
seen { print }
' CHANGELOG.md | sed '/./,$!d' | tac | sed '/./,$!d' | tac) # strip blank edges
if [ -z "$notes" ]; then
echo "::error::No CHANGELOG.md entry for $version — refusing to publish a blank release."
exit 1
fi
{
echo "notes<<__CHANGELOG_EOF__"
echo "$notes"
echo "__CHANGELOG_EOF__"
} >> "$GITHUB_OUTPUT"
echo "Release notes for $version:"; echo "$notes"

- name: Publish Release on GitHub
uses: softprops/action-gh-release@v2
with:
files: dist/*
body: ${{ steps.gen_changelog.outputs.changelog }}
body: ${{ steps.notes.outputs.notes }}
# Auto-open a repo-level Discussion linked to this release, seeded with
# the same notes. Requires Discussions enabled and this category to exist.
discussion_category_name: Announcements
Expand All @@ -96,13 +134,13 @@ jobs:
# Mirror the release announcement into the ORG-level discussions, which are
# backed by codellm-devkit/.github. GITHUB_TOKEN can't write cross-repo, so
# this uses a PAT (ORG_DISCUSSIONS_TOKEN) with repo scope, and posts via the
# createDiscussion GraphQL mutation. The body (the generated changelog) is
# createDiscussion GraphQL mutation. The body (the CHANGELOG.md notes) is
# passed via env to avoid shell-injection, matching the repo-level post.
- name: Announce in org-level discussions (codellm-devkit/.github)
continue-on-error: true # a failed org post must not fail an otherwise-good release
env:
GH_TOKEN: ${{ secrets.ORG_DISCUSSIONS_TOKEN }}
BODY: ${{ steps.gen_changelog.outputs.changelog }}
BODY: ${{ steps.notes.outputs.notes }}
run: |
set -uo pipefail
VERSION="${GITHUB_REF#refs/tags/v}"
Expand Down
65 changes: 0 additions & 65 deletions .github/workflows/release_config.json

This file was deleted.

5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,8 @@ poetry.lock
!CLAUDE.md
!AGENTS.md
!GEMINI.md

# Track the design decision log (overrides the global .claude ignore; everything else in .claude/ stays ignored)
!.claude/
.claude/*
!.claude/FACADE_DECISIONS.md
47 changes: 47 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,53 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed
- **Published wheels bundle the `codeanalyzer-java` JAR again.** `2.0.0-rc.1` (like the 1.2.0–1.4.3
line) shipped without the bundled JAR, so `CLDK.java(...)` after a plain `pip install` raised
`CodeanalyzerExecutionException: codeanalyzer jar not found`. Hatchling applied the root
`.gitignore` `*.jar` rule at build time but not the nested `!codeanalyzer-*.jar` negation that
keeps the JAR tracked in git; a `[tool.hatch.build] artifacts` rule force-includes it, and the
release workflow now fails fast if a built artifact is missing the JAR. (#284)

## [v2.0.0-rc.1] - 2026-07-16

First release candidate for 2.0.0 — the schema-v2 release. Both the TypeScript and Python
facades now speak the analyzers' **schema 2.0.0** end to end and fail fast on any other version.

### Changed
- **BREAKING (Python): the SDK speaks analysis schema 2.0.0 and requires `codeanalyzer-python>=1.0.2`.**
The re-exported Python models follow the schema-v2 renames: `PyModule.classes` → `types`,
`PyClass.methods`/`inner_classes` → `callables`/`types`, `PyCallable.inner_callables`/`inner_classes`
→ `callables`/`types`, and `PyCallEdge` is now `src`/`dst`/`prov`. A callable's source text no
longer lives on a `code` field — the SDK recovers it by slicing `PyModule.source` with the
callable's byte-offset `span` (accessor behavior such as `get_method_bodies` is unchanged).
Call-graph node keys remain dotted signatures: schema v2's CanNode (`can://`) edge endpoints are
translated back to signatures; unresolved externals keep their raw `can://` ids. Both Python
backends fail fast on a schema mismatch: the in-process backend checks the `Analysis` envelope's
`schema_version`, and the Neo4j backend checks the stamp on the scoped `:PyApplication` node —
re-analyze / re-emit with `codeanalyzer-python>=1.0.2` if you hit `CldkSchemaMismatchException`.
- **BREAKING (TypeScript): the SDK speaks graph schema 2.0.0 and requires `codeanalyzer-typescript 1.0.0`.**
TS-prefixed graph vocabulary, CanNode keys, and a fail-fast `schema_version` check on the
`:Application` node. Accessors whose vocabulary is not projected into graph schema 2.0.0
(decorators, fields, imports/exports, variables) raise `NotImplementedError` on the Neo4j
backend instead of silently returning wrong data. (#268)

### Added
- **Canonical schema-v2 CPG models** (`cldk.models.cpg`): the shared, language-neutral
`Application` model tree for schema-2.0.0 analyzer output, modeled once and validated against
real L1–L4 samples from multiple analyzers. (#240, #274)
- **L3/L4 program-slice engine core** (`cldk.graph`): forward/backward slicing over schema-v2
CFG/CDG/DDG program graphs. (#270, #271)

### Fixed
- **TypeScript Neo4j backend guards ambiguous application matches** instead of silently merging
two applications' module scopes. (#268)

### Dependencies
- `codeanalyzer-python` 0.3.1 → **1.0.2** (schema 2.0.0; includes the emitter fix for null `code`
node properties, codellm-devkit/codeanalyzer-python#104)
- `codeanalyzer-typescript` 0.4.3 → **1.0.0** (graph schema 2.0.0)

## [v1.4.3] - 2026-07-14

### Fixed
Expand Down
23 changes: 23 additions & 0 deletions cldk/analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,26 @@ class AnalysisLevel(str, Enum):
call_graph = "call graph"
program_dependency_graph = "program dependency graph"
system_dependency_graph = "system dependency graph"


def to_analysis_level(value) -> AnalysisLevel:
"""Normalize a level given as an AnalysisLevel, its value ("call graph"),
or its name ("call_graph") — both spellings are in the wild."""
if isinstance(value, AnalysisLevel):
return value
try:
return AnalysisLevel(value)
except ValueError:
try:
return AnalysisLevel[value]
except KeyError:
raise ValueError(f"unknown analysis level: {value!r}") from None


#: Facade-level vocabulary → the analyzers' integer analysis level (schema 2.0 ``max_level``).
ANALYSIS_LEVEL_TO_INT = {
AnalysisLevel.symbol_table: 1,
AnalysisLevel.call_graph: 2,
AnalysisLevel.program_dependency_graph: 3,
AnalysisLevel.system_dependency_graph: 4,
}
3 changes: 2 additions & 1 deletion cldk/analysis/python/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

import networkx as nx

from cldk.graph.provider import ProgramGraphProvider
from cldk.models.python import (
PyApplication,
PyCallable,
Expand All @@ -45,7 +46,7 @@
)


class PythonAnalysisBackend(ABC):
class PythonAnalysisBackend(ProgramGraphProvider, ABC):
"""Abstract base every Python analysis backend implements.

A backend owns all indexing and query logic for a Python application; the
Expand Down
Loading