Skip to content

Commit 5333c30

Browse files
committed
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).
1 parent 82b7cbf commit 5333c30

4 files changed

Lines changed: 80 additions & 41 deletions

File tree

src/content/docs/at-scale/index.mdx

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: Multi-lingual, multi-project analysis at scale
33
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.
44
---
55

6-
import { Tabs, TabItem, Steps, Aside, LinkCard, CardGrid, Badge } from "@astrojs/starlight/components";
6+
import { Tabs, TabItem, Steps, Aside, LinkCard, CardGrid } from "@astrojs/starlight/components";
77

88
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.
99

@@ -55,13 +55,15 @@ Each backend writes a **namespaced** label set so multiple languages share a sin
5555

5656
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.
5757

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+
5860
<Aside type="note" title="Languages supported">
5961
The Neo4j emit/poll path is implemented for **Java, Python, and TypeScript**. C is analyzed in-process only — it has no Neo4j backend.
6062
</Aside>
6163

6264
## Phase 1 — Emit a graph
6365

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.
6567

6668
<Tabs syncKey="lang">
6769
<TabItem label="Java">
@@ -111,12 +113,12 @@ Every backend takes the same `--emit neo4j` flag. With `--neo4j-uri` set it push
111113
Re-running a job is safe and cheap, which is what makes a scheduled fleet practical:
112114

113115
- **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.
115117
- **Index-backed** — constraints exist before any `MERGE`, so every upsert is an index seek rather than a scan, even as the database grows.
116118

117119
### A versioned schema contract
118120

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.
120122

121123
```bash title="Export the schema contract"
122124
codeanalyzer --emit schema -o ./out # -> ./out/schema.neo4j.json
@@ -191,8 +193,29 @@ The Neo4j backends (`JNeo4jBackend`, `PyNeo4jBackend`, `TSNeo4jBackend`) expose
191193
3. Call the usual `get_classes`, `get_call_graph`, `get_callers`, `get_callees`, and related methods. The graph answers; no source is parsed.
192194
</Steps>
193195

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+
194217
<Aside type="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.
196219
</Aside>
197220

198221
## See also

src/content/docs/at-scale/kubernetes.mdx

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ title: Deploy on Kubernetes
33
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.
44
---
55

6-
import { Tabs, TabItem, Steps, Aside, LinkCard, CardGrid, Badge } from "@astrojs/starlight/components";
6+
import { Tabs, TabItem, Steps, Aside, LinkCard, CardGrid } from "@astrojs/starlight/components";
77

88
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.
99

@@ -40,6 +40,10 @@ Three roles:
4040
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.
4141
</Aside>
4242

43+
<Aside type="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+
4347
## The emit CronJob
4448

4549
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
112116

113117
## Walkthrough: Odoo, end to end
114118

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.
116120

117121
<Aside type="note" title="Illustrative">
118122
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.
119123
</Aside>
120124

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+
121129
### 1. Emit both languages into one graph
122130

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.
124132

125133
<Tabs syncKey="lang">
126-
<TabItem label="Python (addons)">
134+
<TabItem label="Python (Odoo addons)">
127135
```bash title="canpy over the Odoo addons"
128136
canpy -i ./odoo/addons --emit neo4j \
129137
--app-name odoo \
@@ -133,53 +141,53 @@ Run one Python job over the addons and one TypeScript job over the web assets. B
133141
# -> ~hundreds of modules; subsequent runs only rewrite changed files
134142
```
135143
</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 \
140-
--app-name odoo \
144+
<TabItem label="TypeScript (storefront service)">
145+
```bash title="cants over a TypeScript service"
146+
cants -i ./storefront/src -a 2 --emit neo4j \
147+
--app-name storefront \
141148
--neo4j-uri bolt://neo4j:7687 \
142149
--neo4j-user writer --neo4j-password "$NEO4J_PASSWORD"
143-
# -> writes :TSApplication {name: "odoo"} and its :TSModule / :TSSymbol graph
150+
# -> writes :TSApplication {name: "storefront"} and its :TSModule / :TSSymbol graph
144151
```
145152
</TabItem>
146153
</Tabs>
147154

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)).
149156

150157
### 2. Query across modules and languages
151158

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`:
153160

154-
```python title="cross-module query against the Odoo graph"
161+
```python title="cross-module, cross-language query against the shared graph"
155162
from cldk import CLDK
156163
from cldk.analysis.commons.backend_config import Neo4jConnectionConfig
157164
158-
def odoo(factory):
165+
def connect(factory, app):
159166
return factory(
160167
backend=Neo4jConnectionConfig(
161168
uri="bolt://neo4j:7687",
162169
username="reader",
163170
password="…",
164-
application_name="odoo",
171+
application_name=app,
165172
),
166173
)
167174
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
170177
171178
# "Which callables across all addons reach SaleOrder.action_confirm?"
172-
callers = py.get_callers("odoo.addons.sale.models.sale_order.SaleOrder", "action_confirm")
179+
# Signatures are rooted at the emit -i directory (./odoo/addons), so no "odoo.addons." prefix.
180+
callers = py.get_callers("sale.models.sale_order.SaleOrder", "action_confirm")
173181
# -> callers spread across the sale, stock, and account addons — one query, no per-addon parsing
174182
175-
# The same agent inspects the frontend's call graph through the same API.
176-
frontend = ts.get_call_graph() # -> networkx.DiGraph over the OWL components
183+
# The same agent inspects the TypeScript service's call graph through the same API.
184+
storefront_cg = ts.get_call_graph() # -> networkx.DiGraph
177185
```
178186

179-
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.
180188

181189
<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.
183191
</Aside>
184192

185193
## See also

0 commit comments

Comments
 (0)