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
docs(at-scale): correct verified inaccuracies and add Neo4j power features
Adversarial verification against the backend sources surfaced real errors in
the at-scale/Neo4j docs, now fixed:
- HIGH: the full-run orphan prune is app-anchor-scoped only for Python
(codeanalyzer-python bolt.py). The Java and TypeScript Bolt writers prune any
unit/module absent from the run regardless of app, so the docs no longer
promise shared-DB safety for Java/TS — they direct per-app databases instead.
- Odoo example: cants discovers only .ts/.tsx/.mts/.cts (discovery.ts SOURCE_EXTS),
so it cannot analyze Odoo's OWL JavaScript frontend. Reframed the multi-lingual
walkthrough as Odoo (Python addons) + a real TypeScript service, with an honest
note; fixed the get_callers qualified name (rooted at the emit -i dir).
- Flags: targeted-run is --file-name on canpy, --target-files on codeanalyzer/cants.
- Jelly is experimental and opt-in (--call-graph-provider jelly|both), not
"planned/not implemented" — corrected all three mentions in codeanalyzer-ts.
- codeanalyzer-python: primary CLI is canpy (codeanalyzer is a deprecated alias);
added the --emit/--app-name/--neo4j-* options to the table.
Added the "all the goodies" coverage the at-scale page was missing: a raw-Cypher
section (app enumeration, full-text code search over j_code_fts/py_code_fts/
code_fts, Java-only CRUD + system-dependency-graph methods), external/phantom
nodes, an actionable schema_version check, the snapshot-vs-live tradeoff, and a
Neo4j Browser tip. Dropped an unused Badge import.
Findings produced by an adversarial multi-agent verification workflow (22
confirmed against source, 1 refuted).
Copy file name to clipboardExpand all lines: src/content/docs/at-scale/index.mdx
+28-5Lines changed: 28 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -3,7 +3,7 @@ title: Multi-lingual, multi-project analysis at scale
3
3
description: Analyze many projects in many languages once, push the results into a shared Neo4j graph, and let agents query all of it through one read-only analysis API.
In the [default workflow](/quickstart/), CLDK runs the analysis in-process: you point `CLDK.java(...)` at a project, the backend parses it, and the typed models live in memory for the lifetime of that object. That is the right model for a single project on a single machine.
9
9
@@ -55,13 +55,15 @@ Each backend writes a **namespaced** label set so multiple languages share a sin
55
55
56
56
Within a language, **multiple projects** coexist too. Every node is scoped to an application anchor identified by its `--app-name`, so one database can hold `payments-service`, `web-frontend`, and `billing-core` side by side. On the read side, `application_name` selects which one a query sees.
57
57
58
+
**External (phantom) nodes.** The Python and TypeScript graphs add `:PyExternal` / `:TSExternal` nodes for call targets *outside* the analyzed app, third-party libraries, the standard library, and Node built-ins, so call edges never dangle. They carry no `_module` and are shared rather than owned by one app, which is why a query can see that an app calls `os.path.join` or `node:crypto.createHash` even though those are not part of its source.
59
+
58
60
<Asidetype="note"title="Languages supported">
59
61
The Neo4j emit/poll path is implemented for **Java, Python, and TypeScript**. C is analyzed in-process only — it has no Neo4j backend.
60
62
</Aside>
61
63
62
64
## Phase 1 — Emit a graph
63
65
64
-
Every backend takes the same `--emit neo4j` flag. With `--neo4j-uri` set it pushes to a live database over Bolt; without it, it writes a self-contained `graph.cypher` snapshot you can load with `cypher-shell`. Connection settings also read the `NEO4J_URI`, `NEO4J_USERNAME`, `NEO4J_PASSWORD`, and `NEO4J_DATABASE` environment variables; an explicit flag wins over the environment.
66
+
Every backend takes the same `--emit neo4j` flag. With `--neo4j-uri` set it pushes to a live database over Bolt; without it, it writes a self-contained `graph.cypher` snapshot you can load with `cypher-shell`. The two modes differ in more than destination: the live Bolt push is content-hash incremental (it rewrites only changed modules and prunes orphans on a full run), while the `graph.cypher` snapshot wipes and rebuilds that application's subgraph in full every time it is applied. Connection settings also read the `NEO4J_URI`, `NEO4J_USERNAME`, `NEO4J_PASSWORD`, and `NEO4J_DATABASE` environment variables; an explicit flag wins over the environment.
65
67
66
68
<TabssyncKey="lang">
67
69
<TabItemlabel="Java">
@@ -111,12 +113,12 @@ Every backend takes the same `--emit neo4j` flag. With `--neo4j-uri` set it push
111
113
Re-running a job is safe and cheap, which is what makes a scheduled fleet practical:
112
114
113
115
-**Idempotent** — both writers create the schema constraints and indexes first, then upsert with `MERGE` (never blind `CREATE`). Re-emitting the same project produces the same graph.
114
-
-**Incremental** — a live Bolt push diffs each module against the graph by content hash and rewrites **only what changed**. On a full run, modules whose source file disappeared are pruned, scoped to that application's anchor so a shared database stays consistent.
116
+
-**Incremental** — a live Bolt push diffs each module against the graph by content hash and rewrites **only what changed**. On a full run, modules whose source file disappeared are pruned. The Python writer scopes this prune to the application's anchor, so other apps in a shared database are untouched; the Java and TypeScript writers currently prune any compilation unit or module absent from the current run, regardless of app. On a shared instance, give each Java or TypeScript application its own Neo4j database (or run its full job against a dedicated one) so a full run for app B never deletes app A.
115
117
-**Index-backed** — constraints exist before any `MERGE`, so every upsert is an index seek rather than a scan, even as the database grows.
116
118
117
119
### A versioned schema contract
118
120
119
-
Each backend can emit its graph schema as a machine-readable, version-stamped contract with `--emit schema` (no project needed). The `schema_version` is also stamped on every graph's `:*Application` node, so a consumer can check compatibility before querying.
121
+
Each backend can emit its graph schema as a machine-readable, version-stamped contract with `--emit schema` (no project needed). The `schema_version` is also stamped on every graph's `:*Application` node, so a consumer can check compatibility before querying — read it back per application with `MATCH (a:JApplication {name: $app}) RETURN a.schema_version` (use `:PyApplication` / `:TSApplication` for the others). Each backend versions its schema independently, so pin the contract you generated against rather than assuming one shared version.
3. Call the usual `get_classes`, `get_call_graph`, `get_callers`, `get_callees`, and related methods. The graph answers; no source is parsed.
192
194
</Steps>
193
195
196
+
## Query the graph directly with Cypher
197
+
198
+
The `analysis` API covers the common structural questions, but the graph is a full Neo4j property graph: anything the API does not model, an agent can reach with raw Cypher over the same Bolt endpoint. The label namespacing is the query contract — scope by the `:*Application` anchor and the language prefix.
199
+
200
+
**Discover what a shared database holds.** The SDK expects you to know the `application_name` up front (the Neo4j backends raise if it is missing, and offer no enumeration method), so taking inventory is itself a Cypher query:
201
+
202
+
```cypher
203
+
MATCH (a)
204
+
WHERE a:JApplication OR a:PyApplication OR a:TSApplication
205
+
RETURN labels(a)[0] AS language, a.name AS app, a.schema_version
206
+
```
207
+
208
+
**Full-text code search.** Every backend creates a Neo4j full-text index over each callable's `code` and `docstring`, named per language (`j_code_fts`, `py_code_fts`, `code_fts`). The CLDK API does not wrap it, so reach it over Bolt:
209
+
210
+
```cypher
211
+
CALL db.index.fulltext.queryNodes('py_code_fts', 'jwt OR authenticate') YIELD node, score
212
+
RETURN node.signature, score ORDER BY score DESC LIMIT 20
213
+
```
214
+
215
+
**Java carries more in its graph.** The Java emitter projects CRUD operations and a system dependency graph that the other languages do not, and `JNeo4jBackend` surfaces them with no re-parsing: `get_all_crud_operations()` (plus `get_all_read_operations()` / `…_create…` / `…_update…` / `…_delete…`), `get_system_dependency_graph()`, and `get_all_entry_point_methods()`. See the [Java API reference](/reference/python-api/java/).
216
+
194
217
<Asidetype="caution"title="What “at scale” does and doesn’t mean here">
195
-
Scale here comes from **architecture**, not tuning knobs: idempotent index-backed `MERGE`, content-hash incrementality, and the freedom to fan emit jobs out horizontally. The writers do **not** expose a configurable batch size or `PERIODIC COMMIT`, and a **targeted run** (`--target-files`) skips orphan pruning, so deleted files are only cleaned up on a full run. Plan your jobs as periodic full runs with incremental pushes in between.
218
+
Scale here comes from **architecture**, not tuning knobs: idempotent index-backed `MERGE`, content-hash incrementality, and the freedom to fan emit jobs out horizontally. The writers do **not** expose a configurable batch size or `PERIODIC COMMIT`, and a **targeted run** (`--target-files` on Java/TypeScript, `--file-name` on `canpy`) skips orphan pruning, so deleted files are only cleaned up on a full run. Plan your jobs as periodic full runs with incremental pushes in between.
Copy file name to clipboardExpand all lines: src/content/docs/at-scale/kubernetes.mdx
+30-22Lines changed: 30 additions & 22 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -3,7 +3,7 @@ title: Deploy on Kubernetes
3
3
description: A Job/CronJob topology that emits analysis graphs into a shared Neo4j and serves read-only CLDK agents — walked through end to end on Odoo.
The [emit/poll split](/at-scale/) maps cleanly onto Kubernetes. The expensive analysis becomes scheduled batch work, the database is a stateful service, and the agents are stateless readers that scale on demand.
9
9
@@ -40,6 +40,10 @@ Three roles:
40
40
Keeping analysis out of the request path is the point. A model that re-analyzes per request can't scale; one that reads a precomputed graph can. Emit on a schedule (or trigger a job from CI on merge), and let the read replicas stay thin.
41
41
</Aside>
42
42
43
+
<Asidetype="tip"title="Eyeball the graph">
44
+
Because the emit step produces a real property graph, you can point Neo4j Browser (or Bloom) at the same Bolt endpoint with read-only credentials to see what landed. Scope to one app and run, e.g., `MATCH (a:JApplication {name: 'payments-service'})-[:J_HAS_UNIT]->(:JCompilationUnit)-[:J_DECLARES_TYPE]->(t:JType) RETURN * LIMIT 50`.
45
+
</Aside>
46
+
43
47
## The emit CronJob
44
48
45
49
Each backend ships as a self-contained binary (`codeanalyzer` for Java, `canpy` for Python, `cants` for TypeScript), so an emit job is a container that has the binary, the source checkout, and the Neo4j connection in its environment. Connection settings are read from `NEO4J_URI`, `NEO4J_USERNAME`, and `NEO4J_PASSWORD`, so the command line carries no secrets.
@@ -112,18 +116,22 @@ Because no source is parsed at query time, an agent pod starts instantly and ans
112
116
113
117
## Walkthrough: Odoo, end to end
114
118
115
-
[Odoo](https://github.com/odoo/odoo) is a good stress test for "at scale": it is large, it is split into **hundreds of addon modules**, and it is **multi-lingual** — a Python backend plus a JavaScript/OWL web frontend. Analyzing it once and querying the result is exactly the shape this section is built for.
119
+
[Odoo](https://github.com/odoo/odoo) is a good stress test for multi-project analysis at scale: it is large and split into **hundreds of addon modules**, all Python. A real fleet rarely stops at one language, though, so we pair Odoo's Python addons with a **TypeScript service** in the same graph and query both through one API.
116
120
117
121
<Aside type="note" title="Illustrative">
118
122
The commands below are real and copy-pasteable; the counts and timings are representative of a large multi-module codebase, shown to make the workflow concrete. They are illustrative, not measured.
119
123
</Aside>
120
124
125
+
<Aside type="note" title="Odoo's frontend is JavaScript, not TypeScript">
126
+
Odoo's web layer (OWL) is JavaScript with JSDoc. `cants` discovers only `.ts`/`.tsx`/`.mts`/`.cts` sources, so it does not analyze Odoo's `web/static/src` today (`allowJs` only resolves `.js` files referenced *from* a TypeScript entry point). The TypeScript half of this walkthrough therefore uses a separate TypeScript service in the same fleet.
127
+
</Aside>
128
+
121
129
### 1. Emit both languages into one graph
122
130
123
-
Run one Python job over the addons and one TypeScript job over the web assets. Both carry `--app-name odoo`, so they land under a single application anchor in the same database — the namespaced labels keep the Python and frontend graphs distinct while letting an agent see both.
131
+
Run one Python job over Odoo's addons and one TypeScript job over the storefront service. They use distinct `--app-name`s, so both land in the same database as separate application anchors — the namespaced labels keep the Python and TypeScript graphs from colliding, and an agent can query either.
124
132
125
133
<Tabs syncKey="lang">
126
-
<TabItem label="Python (addons)">
134
+
<TabItem label="Python (Odoo addons)">
127
135
```bash title="canpy over the Odoo addons"
128
136
canpy -i ./odoo/addons --emit neo4j \
129
137
--app-name odoo \
@@ -133,53 +141,53 @@ Run one Python job over the addons and one TypeScript job over the web assets. B
133
141
# -> ~hundreds of modules; subsequent runs only rewrite changed files
134
142
```
135
143
</TabItem>
136
-
<TabItem label="TypeScript (web frontend)">
137
-
```bash title="cants over the web assets"
138
-
# JavaScript/OWL source is analyzed through the TypeScript backend.
139
-
cants -i ./odoo/addons/web/static/src -a 2 --emit neo4j \
# -> writes :TSApplication {name: "odoo"} and its :TSModule / :TSSymbol graph
150
+
# -> writes :TSApplication {name: "storefront"} and its :TSModule / :TSSymbol graph
144
151
```
145
152
</TabItem>
146
153
</Tabs>
147
154
148
-
In a cluster these are two `CronJob`s on the same schedule. After the first full run, each subsequent run is an incremental Bolt push: only modules whose content hash changed are rewritten.
155
+
In a cluster these are two `CronJob`s on the same schedule. After the first full run, each subsequent run is an incremental Bolt push: only modules whose content hash changed are rewritten. The two apps are different languages, so their full-run prunes never touch each other; keep two same-language apps in separate databases (see the [prune note](/at-scale/#writes-are-idempotent-and-incremental)).
149
156
150
157
### 2. Query across modules and languages
151
158
152
-
An agent now answers structural questions about Odoo without touching the source tree. Construct one analysis per language, both anchored to `application_name="odoo"`:
159
+
An agent now answers structural questions without touching either source tree. Construct one analysis per app, each anchored to its own `application_name`:
153
160
154
-
```python title="cross-modulequery against the Odoo graph"
161
+
```python title="cross-module, cross-language query against the shared graph"
155
162
from cldk import CLDK
156
163
from cldk.analysis.commons.backend_config import Neo4jConnectionConfig
157
164
158
-
def odoo(factory):
165
+
def connect(factory, app):
159
166
return factory(
160
167
backend=Neo4jConnectionConfig(
161
168
uri="bolt://neo4j:7687",
162
169
username="reader",
163
170
password="…",
164
-
application_name="odoo",
171
+
application_name=app,
165
172
),
166
173
)
167
174
168
-
py = odoo(CLDK.python) # the addons graph
169
-
ts = odoo(CLDK.typescript) # the web-frontend graph
175
+
py = connect(CLDK.python, "odoo") # the Odoo addons graph
176
+
ts = connect(CLDK.typescript, "storefront") # the TypeScript service graph
170
177
171
178
# "Which callables across all addons reach SaleOrder.action_confirm?"
The query touched modules from several addons in a single call, and reached into a second language through the same `analysis` vocabulary — the multi-lingual, multi-project payoff. Re-analyzing Odoo from scratch on every such question would be untenable; reading a precomputed graph is constant work.
187
+
The first query touched modules from several addons in a single call, and the agent reached a second language through the same `analysis` vocabulary — the multi-lingual, multi-project payoff. Re-analyzing these projects from scratch on every such question would be untenable; reading a precomputed graph is constant work.
180
188
181
189
<Aside type="caution" title="Operational notes">
182
-
Schedule the emit jobs as periodic **full** runs (a full run prunes modules whose files were deleted); use `--target-files` only for fast mid-cycle refreshes, since targeted runs skip orphan pruning. Give the agents a **read-only** Neo4j role, and keep write credentials on the CronJobs alone.
190
+
Schedule the emit jobs as periodic **full** runs (a full run prunes modules whose files were deleted); use the backend's targeted-run flag (`--file-name <path>` for `canpy`, `--target-files <paths…>` for `codeanalyzer` and `cants`) only for fast mid-cycle refreshes, since targeted runs skip orphan pruning. Give the agents a **read-only** Neo4j role, and keep write credentials on the CronJobs alone.
0 commit comments