Skip to content

Commit 6d9455f

Browse files
rahlkclaude
andcommitted
docs: document Neo4j property-graph output (scale, Kubernetes, enterprise)
Reframe the docs around the new `--emit neo4j` target: project the analysis into a labeled Neo4j property graph that scales beyond a single JSON file. Introduce the producer/consumer split — `cants` runs out-of-band as a CI/Kubernetes job pushing app-scoped subgraphs to Neo4j over Bolt, while the CLDK Python SDK and agents read the graph read-only via Neo4jConnectionConfig. - New Neo4j guide + graph-schema reference pages; sidebar entries - Neo4j property-graph hero visual (src/components/Neo4jPropertyGraph.astro) - Document --emit json|neo4j|schema, --app-name, --neo4j-* flags and NEO4J_* env - graph.cypher snapshot vs incremental Bolt push; per-app scoping; schema_version 2.0.0 - CLDK read-back examples; extended architecture/concept mermaid diagrams - Fix stale CLI facts and cross-links; verified with `astro build` Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e728b86 commit 6d9455f

14 files changed

Lines changed: 1615 additions & 194 deletions

astro.config.mjs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export default defineConfig({
2121
title: "codeanalyzer-typescript",
2222
tagline: "Static analysis for TypeScript your agents can call.",
2323
description:
24-
"codeanalyzer-typescript turns a TypeScript/JavaScript project into one typed artifact — symbol table, call graph, and external symbols — using the TypeScript compiler via ts-morph. The TypeScript backend behind CLDK.",
24+
"codeanalyzer-typescript turns a TypeScript/JavaScript project into a canonical symbol table and call graph — emitted as one typed analysis.json artifact or projected into a queryable Neo4j property graph — using the TypeScript compiler via ts-morph. The TypeScript backend behind CLDK.",
2525
logo: {
2626
src: "./src/assets/logo.png",
2727
replacesTitle: true,
@@ -106,13 +106,15 @@ export default defineConfig({
106106
label: "Level 2: CodeQL & entrypoints",
107107
slug: "guides/level-2",
108108
},
109+
{ label: "Neo4j", slug: "guides/neo4j" },
109110
],
110111
},
111112
{
112113
label: "Reference",
113114
items: [
114115
{ label: "CLI options", slug: "reference/cli" },
115116
{ label: "Output schema", slug: "reference/schema" },
117+
{ label: "Neo4j graph schema", slug: "reference/neo4j-schema" },
116118
],
117119
},
118120
],
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
---
2+
// AUTO-GENERATED property-graph hero for typescript. Encodes the real Neo4j schema
3+
// (node labels, typed relationships, key properties). Theme-aware via Starlight
4+
// CSS custom properties; renders as static SVG (no client JS).
5+
---
6+
<figure class="pgraph-figure">
7+
<svg class="pgraph" viewBox="0 0 1080 600" role="img" aria-label="codeanalyzer-typescript · Neo4j property graph" xmlns="http://www.w3.org/2000/svg">
8+
<defs>
9+
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
10+
<path d="M 0 0 L 10 5 L 0 10 z" fill="var(--pg-edge)"/>
11+
</marker>
12+
<marker id="arrow-accent" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="8" markerHeight="8" orient="auto-start-reverse">
13+
<path d="M 0 0 L 10 5 L 0 10 z" fill="#DA7194"/>
14+
</marker>
15+
<radialGradient id="gloss" cx="35%" cy="30%" r="75%">
16+
<stop offset="0%" stop-color="#ffffff" stop-opacity="0.35"/>
17+
<stop offset="55%" stop-color="#ffffff" stop-opacity="0.0"/>
18+
</radialGradient>
19+
<filter id="nshadow" x="-30%" y="-30%" width="160%" height="160%">
20+
<feDropShadow dx="0" dy="3" stdDeviation="4" flood-color="#0b1020" flood-opacity="0.28"/>
21+
</filter>
22+
</defs>
23+
<path d="M 155.8 271.1 L 292.7 160.2" class="rel" fill="none" marker-end="url(#arrow)"/>
24+
<g transform="translate(224.2,215.6)"><rect x="-49.5" y="-11" width="99.0" height="22" rx="11" class="pill"/><text x="0" y="4" class="rtype" text-anchor="middle">TS_HAS_MODULE</text></g>
25+
<path d="M 330.0 176.0 L 330.0 422.0" class="rel" fill="none" marker-end="url(#arrow)"/>
26+
<g transform="translate(330.0,333.4)"><rect x="-42.5" y="-11" width="85.0" height="22" rx="11" class="pill"/><text x="0" y="4" class="rtype" text-anchor="middle">TS_DECLARES</text></g>
27+
<path d="M 284.0 470.0 L 168.0 470.0" class="rel" fill="none" marker-end="url(#arrow)"/>
28+
<g transform="translate(226.0,470.0)"><rect x="-49.5" y="-11" width="99.0" height="22" rx="11" class="pill"/><text x="0" y="4" class="rtype" text-anchor="middle">TS_IMPLEMENTS</text></g>
29+
<path d="M 367.0 442.7 L 521.4 328.5" class="rel" fill="none" marker-end="url(#arrow)"/>
30+
<g transform="translate(444.2,385.6)"><rect x="-49.5" y="-11" width="99.0" height="22" rx="11" class="pill"/><text x="0" y="4" class="rtype" text-anchor="middle">TS_HAS_METHOD</text></g>
31+
<path d="M 560.0 346.0 L 560.0 422.0" class="rel" fill="none" marker-end="url(#arrow)"/>
32+
<g transform="translate(560.0,394.6)"><rect x="-56.5" y="-11" width="113.0" height="22" rx="11" class="pill"/><text x="0" y="4" class="rtype" text-anchor="middle">TS_DECORATED_BY</text></g>
33+
<path d="M 597.0 272.7 L 751.4 158.5" class="rel" fill="none" marker-end="url(#arrow)"/>
34+
<g transform="translate(674.2,215.6)"><rect x="-56.5" y="-11" width="113.0" height="22" rx="11" class="pill"/><text x="0" y="4" class="rtype" text-anchor="middle">TS_HAS_CALLSITE</text></g>
35+
<path d="M 823.9 161.1 L 939.7 267.5" class="rel" fill="none" marker-end="url(#arrow)"/>
36+
<g transform="translate(881.8,214.3)"><rect x="-53.0" y="-11" width="106.0" height="22" rx="11" class="pill"/><text x="0" y="4" class="rtype" text-anchor="middle">TS_RESOLVES_TO</text></g>
37+
<path d="M 606.0 300.0 Q 734.4 170.0 927.0 300.0" class="rel-accent" fill="none" marker-end="url(#arrow-accent)"/>
38+
<g transform="translate(725.2,236.7)"><rect x="-32.0" y="-11" width="64.0" height="22" rx="11" class="pill-accent"/><text x="0" y="4" class="rtype" text-anchor="middle">TS_CALLS</text></g>
39+
<circle cx="120" cy="300" r="46" fill="#C990C0" stroke="#ffffff" stroke-width="2.5" filter="url(#nshadow)"/>
40+
<circle cx="120" cy="300" r="46" fill="url(#gloss)"/>
41+
<text x="120" y="304" class="nlabel" text-anchor="middle">:TSApplication</text>
42+
<text x="120" y="362" class="prop" text-anchor="middle">name</text>
43+
<text x="120" y="378" class="prop" text-anchor="middle">schema_version</text>
44+
<circle cx="330" cy="130" r="46" fill="#4C8EDA" stroke="#ffffff" stroke-width="2.5" filter="url(#nshadow)"/>
45+
<circle cx="330" cy="130" r="46" fill="url(#gloss)"/>
46+
<text x="330" y="134" class="nlabel" text-anchor="middle">:TSModule</text>
47+
<text x="330" y="192" class="prop" text-anchor="middle">module_name</text>
48+
<text x="330" y="208" class="prop" text-anchor="middle">is_tsx</text>
49+
<circle cx="330" cy="470" r="46" fill="#F79767" stroke="#ffffff" stroke-width="2.5" filter="url(#nshadow)"/>
50+
<circle cx="330" cy="470" r="46" fill="url(#gloss)"/>
51+
<text x="330" y="466" class="nlabel" text-anchor="middle">:TSClass</text>
52+
<text x="330" y="483" class="nsub" text-anchor="middle">:TSSymbol</text>
53+
<text x="330" y="532" class="prop" text-anchor="middle">name</text>
54+
<text x="330" y="548" class="prop" text-anchor="middle">http_methods</text>
55+
<circle cx="120" cy="470" r="46" fill="#57C7E3" stroke="#ffffff" stroke-width="2.5" filter="url(#nshadow)"/>
56+
<circle cx="120" cy="470" r="46" fill="url(#gloss)"/>
57+
<text x="120" y="474" class="nlabel" text-anchor="middle">:TSInterface</text>
58+
<text x="120" y="532" class="prop" text-anchor="middle">name</text>
59+
<text x="120" y="548" class="prop" text-anchor="middle">is_exported</text>
60+
<circle cx="560" cy="300" r="53" fill="none" stroke="#FFC454" stroke-width="3" stroke-dasharray="5 5" opacity="0.95"/>
61+
<circle cx="560" cy="300" r="46" fill="#8DCC93" stroke="#ffffff" stroke-width="2.5" filter="url(#nshadow)"/>
62+
<circle cx="560" cy="300" r="46" fill="url(#gloss)"/>
63+
<text x="560" y="296" class="nlabel" text-anchor="middle">:TSCallable</text>
64+
<text x="560" y="313" class="nsub" text-anchor="middle">:TSSymbol</text>
65+
<g transform="translate(560,238)"><rect x="-52" y="-12" width="104" height="20" rx="10" fill="#1b1b2b" opacity="0.92"/><text x="0" y="2" class="badge" text-anchor="middle">★ :TSEntrypoint</text></g>
66+
<text x="560" y="362" class="prop" text-anchor="middle">signature</text>
67+
<text x="560" y="378" class="prop" text-anchor="middle">is_exported</text>
68+
<circle cx="560" cy="470" r="46" fill="#ECB5C9" stroke="#ffffff" stroke-width="2.5" filter="url(#nshadow)"/>
69+
<circle cx="560" cy="470" r="46" fill="url(#gloss)"/>
70+
<text x="560" y="474" class="nlabel" text-anchor="middle">:TSDecorator</text>
71+
<text x="560" y="532" class="prop" text-anchor="middle">name</text>
72+
<circle cx="790" cy="130" r="46" fill="#FFC454" stroke="#ffffff" stroke-width="2.5" filter="url(#nshadow)"/>
73+
<circle cx="790" cy="130" r="46" fill="url(#gloss)"/>
74+
<text x="790" y="134" class="nlabel" text-anchor="middle">:TSCallSite</text>
75+
<text x="790" y="192" class="prop" text-anchor="middle">method_name</text>
76+
<text x="790" y="208" class="prop" text-anchor="middle">receiver_type</text>
77+
<circle cx="975" cy="300" r="46" fill="#569480" stroke="#ffffff" stroke-width="2.5" filter="url(#nshadow)"/>
78+
<circle cx="975" cy="300" r="46" fill="url(#gloss)"/>
79+
<text x="975" y="304" class="nlabel" text-anchor="middle">:TSExternal</text>
80+
<text x="975" y="362" class="prop" text-anchor="middle">name</text>
81+
<text x="975" y="378" class="prop" text-anchor="middle">module</text>
82+
</svg>
83+
<figcaption>
84+
The analysis is a <strong>Neo4j property graph</strong>: every node carries a
85+
<em>label</em> (its color) and <em>properties</em>; every relationship carries a
86+
<em>type</em>. The dashed ring marks an <code>:TSEntrypoint</code>;
87+
the <span class="calls">TS_CALLS</span> edge is the resolved call graph.
88+
</figcaption>
89+
</figure>
90+
91+
<style>
92+
.pgraph-figure {
93+
margin: 1.5rem 0 2rem;
94+
padding: 0.5rem 0.25rem 0.25rem;
95+
border: 1px solid var(--sl-color-gray-5);
96+
border-radius: 14px;
97+
background:
98+
radial-gradient(120% 80% at 15% 0%, color-mix(in srgb, var(--sl-color-accent-low) 55%, transparent), transparent 60%),
99+
var(--sl-color-black);
100+
overflow: hidden;
101+
}
102+
.pgraph { width: 100%; height: auto; display: block; }
103+
/* edges + labels adapt to theme */
104+
.pgraph { --pg-edge: var(--sl-color-gray-3); }
105+
.pgraph .rel { stroke: var(--pg-edge); stroke-width: 2.2; }
106+
.pgraph .rel-accent { stroke: #DA7194; stroke-width: 3.2; }
107+
.pgraph .nlabel { fill: #fff; font: 700 14px/1.1 ui-sans-serif, system-ui, sans-serif; letter-spacing: .2px; }
108+
.pgraph .nsub { fill: rgba(255,255,255,.92); font: 600 12px ui-sans-serif, system-ui, sans-serif; }
109+
.pgraph .prop { fill: var(--sl-color-gray-2); font: 500 12px ui-monospace, "SFMono-Regular", monospace; }
110+
.pgraph .rtype { fill: #fff; font: 700 11px ui-monospace, monospace; letter-spacing: .3px; }
111+
.pgraph .pill { fill: #3a4252; stroke: rgba(255,255,255,.18); }
112+
.pgraph .pill-accent { fill: #DA7194; }
113+
.pgraph .badge { fill: #ffd98a; font: 700 11px ui-monospace, monospace; }
114+
.pgraph-figure figcaption {
115+
color: var(--sl-color-gray-2);
116+
font-size: 0.85rem; line-height: 1.5;
117+
padding: 0.4rem 1rem 0.75rem; text-align: center;
118+
}
119+
.pgraph-figure .calls { color: #DA7194; font-weight: 700; font-family: ui-monospace, monospace; }
120+
</style>

src/content/docs/guides/call-graph.mdx

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import { Aside, LinkCard, CardGrid } from "@astrojs/starlight/components";
77

88
The call graph is the answer to *"who calls whom?"* — a flat list of identity-only `TSCallEdge` objects whose `source` and `target` are signature strings. This page is the deep dive on how the **level-1** graph is built: the TypeScript checker resolves each recorded call site, Rapid Type Analysis expands virtual dispatch, and phantom nodes capture the calls that leave the project. It's the always-on base graph; [level 2](/codeanalyzer-typescript/guides/level-2/) enriches it.
99

10+
The same graph has two shapes. Inside `analysis.json` it's the identity-only edge list described below. Projected into Neo4j with `--emit neo4j`, those same edges become `(:TSCallable)-[:TS_CALLS]->(:TSCallable|:TSExternal)` relationships you can traverse with Cypher — the [In the Neo4j graph](#in-the-neo4j-graph) section maps one onto the other.
11+
1012
```mermaid
1113
flowchart TB
1214
ST[symbol table] --> CS["recorded call sites
@@ -25,6 +27,15 @@ provenance: import"]
2527
DT --> CG[call_graph]
2628
RTA --> CG
2729
EXT --> CG
30+
CG --> IR["analysis IR
31+
(symbol_table, call_graph, external_symbols)"]
32+
IR -->|"--emit json (default)"| JSON[analysis.json]
33+
IR -->|"--emit neo4j"| PG["labeled property graph
34+
TSCallable -[:TS_CALLS]-> TSCallable|TSExternal"]
35+
PG -->|"no --neo4j-uri"| SNAP["graph.cypher snapshot
36+
(scoped wipe + batched MERGE)"]
37+
PG -->|"--neo4j-uri (live Bolt)"| BOLT["incremental push
38+
(diff by content_hash)"]
2839
```
2940

3041
## The tsc resolver
@@ -90,6 +101,12 @@ A specifier is external only if it isn't a relative or absolute path. Packages (
90101

91102
Disable phantom resolution entirely with [`--no-phantoms`](/codeanalyzer-typescript/guides/cli-usage/#phantom-nodes) if you want a graph restricted to in-project targets.
92103

104+
## Picking a backend: tsc, jelly, or both
105+
106+
The tsc resolver is the default, but it's not the only flow analyzer. Pass `--call-graph-provider tsc|jelly|both` to choose: `jelly` runs the embedded [cs-au-dk/jelly](https://github.com/cs-au-dk/jelly) flow analyzer — which ships *inside* the `cants` binary, so there's nothing extra to install — and `both` unions the two.
107+
108+
The provider doesn't change the *shape* of the graph: edges still go between the same signatures, and in Neo4j they're still `:TS_CALLS` relationships. What changes is `provenance` — a tsc edge carries `["tsc"]`, a jelly edge carries its own provenance tag, and an edge both agree on carries both. That makes provenance a filter you can lean on: keep only edges a given provider vouches for, or trust edges both providers found.
109+
93110
## Why identity-only edges
94111

95112
The graph stores only `source`/`target`/`weight`/`provenance`/`tags` — no embedded node objects. The nodes already exist: they're the `TSCallable` entries in the symbol table and the `TSExternalSymbol` entries in `external_symbols`. Keeping edges identity-only means the graph is a plain list of string pairs that loads into any graph library directly, and the rich per-call detail (receiver expression, argument types, location, `is_optional_chain`) lives where it belongs — on the `TSCallsite` inside the calling callable.
@@ -106,11 +123,76 @@ for e in app["call_graph"]:
106123
exact = [(u, v) for u, v, d in g.edges(data=True) if not d["rta"]]
107124
```
108125

126+
## In the Neo4j graph
127+
128+
`analysis.json` holds one project's call graph in memory. Project it into Neo4j with `--emit neo4j` and the same edges become a queryable, persistent system of record that composes across a whole portfolio — many applications in one database, each anchored at its own `:TSApplication` node, traversed with Cypher instead of parsed out of giant JSON blobs.
129+
130+
```bash
131+
# Live, incremental push over Bolt (prefer the env var for the password)
132+
export NEO4J_PASSWORD=secret
133+
cants --input ./my-ts-project --emit neo4j --app-name my-ts-app \
134+
--neo4j-uri bolt://localhost:7687 --neo4j-user neo4j --neo4j-database neo4j
135+
```
136+
137+
Omit `--neo4j-uri` and `--emit neo4j` writes a self-contained `graph.cypher` snapshot instead — constraints, indexes, a wipe scoped to this `--app-name`, then batched `MERGE`s — which you load with `cypher-shell < graph.cypher`. The live push is incremental: it diffs each module by `content_hash` and only re-pushes what changed.
138+
139+
The call graph maps onto two relationship types:
140+
141+
- **Aggregated calls** become `(:TSCallable)-[:TS_CALLS]->(:TSCallable|:TSExternal)` — the property-graph twin of `analysis.json`'s `call_graph`. The edge carries `weight` (integer), `provenance` (string array), `dispatch` (string), `external` (boolean), and `module` (string). The `tags["ts.dispatch"]` you'd read off the JSON edge is the **`dispatch` property** here, so an RTA-expanded edge is `dispatch = 'rta'`, and a phantom edge into a library lands on a `:TSExternal` node with `external = true`.
142+
- **Per-call detail** becomes `(:TSCallable)-[:TS_HAS_CALLSITE]->(:TSCallSite)`, and each `:TSCallSite` resolves to its callee with `(:TSCallSite)-[:TS_RESOLVES_TO]->(:TSCallable|:TSExternal)`. The `:TSCallSite` node keeps the rich metadata — `method_name`, `receiver_expr`, `receiver_type`, `argument_types`, `return_type`, `callee_signature` — that lives on the `TSCallsite` in the JSON model.
143+
144+
So the identity-only `source → target` pair you'd traverse in `networkx` is the `:TS_CALLS` edge, and the call-site detail you'd read off the calling callable is one hop further out through `:TS_HAS_CALLSITE`.
145+
146+
Reachability — *"who can reach this sink?"* — that needs the whole `call_graph` in memory in JSON becomes a variable-length traversal in Cypher, scoped to one application by its `--app-name`:
147+
148+
```cypher
149+
// Every callable in my-ts-app that can transitively reach a SQL sink
150+
MATCH (app:TSApplication {name: 'my-ts-app'})-[:TS_HAS_MODULE]->(:TSModule)
151+
-[:TS_DECLARES*0..]->(src:TSCallable)
152+
MATCH path = (src)-[:TS_CALLS*1..]->(sink)
153+
WHERE sink.name = 'query' OR sink.signature CONTAINS 'mysql'
154+
RETURN src.signature AS reaches_sink, length(path) AS hops
155+
ORDER BY hops
156+
LIMIT 50;
157+
```
158+
159+
To see only edges a provider actually vouched for — the tsc-vs-jelly distinction from above — filter on the `provenance` array; to separate exact dispatch from RTA fan-out, filter on `dispatch`:
160+
161+
```cypher
162+
// Exact (non-RTA) tsc edges out of one callable
163+
MATCH (a:TSCallable {signature: 'src/svc.process'})-[c:TS_CALLS]->(b)
164+
WHERE 'tsc' IN c.provenance AND c.dispatch <> 'rta'
165+
RETURN b.signature, c.weight, c.dispatch;
166+
```
167+
168+
<Aside type="note" title="Read the graph from Python — no binary, no source">
169+
The CLDK Python SDK reads this graph back read-only over Bolt: `CLDK.typescript(backend=Neo4jConnectionConfig(uri=..., application_name="my-ts-app"))` reconstructs the **same** `networkx.DiGraph` call graph (`get_call_graph()`) and typed `TSCallable`/`TSExternal` objects the in-process analyzer produces — with no JDK, no `cants` binary, and no project source on the consumer. The `application_name` must equal the `--app-name` the graph was loaded with.
170+
</Aside>
171+
172+
```python
173+
# Read the call graph back from Neo4j — analysis produced once, read cheaply everywhere
174+
from cldk import CLDK
175+
from cldk.analysis.commons.backend_config import Neo4jConnectionConfig
176+
177+
analysis = CLDK.typescript(
178+
backend=Neo4jConnectionConfig(
179+
uri="bolt://localhost:7687",
180+
username="neo4j",
181+
password="neo4j", # read-only credentials are sufficient
182+
application_name="my-ts-app", # == the --app-name used at emit time
183+
),
184+
)
185+
cg = analysis.get_call_graph() # networkx.DiGraph, same as the in-process backend
186+
externals = analysis.get_external_symbols() # phantom library targets, for source→sink reachability
187+
```
188+
189+
Install the driver extra with `pip install cldk[neo4j]` (or `pip install neo4j`).
190+
109191
## Where to go next
110192

111193
<CardGrid>
112194
<LinkCard title="Core concepts" description="How the call graph sits alongside the symbol table and external symbols." href="/codeanalyzer-typescript/guides/concepts/" />
113195
<LinkCard title="Output schema" description="The TSCallEdge and TSExternalSymbol models in full." href="/codeanalyzer-typescript/reference/schema/#tscalledge" />
114196
<LinkCard title="Level 2: CodeQL & entrypoints" description="What enrichment will add on top of this graph." href="/codeanalyzer-typescript/guides/level-2/" />
115-
<LinkCard title="CLI usage" description="Flags that change the graph: analysis level, phantoms." href="/codeanalyzer-typescript/guides/cli-usage/" />
197+
<LinkCard title="CLI usage" description="Flags that change the graph: --emit, --call-graph-provider, phantoms." href="/codeanalyzer-typescript/guides/cli-usage/" />
116198
</CardGrid>

0 commit comments

Comments
 (0)