Metadata-Version: 2.5
Name: codeanalyzer-go
Version: 0.3.0
Summary: Prebuilt codeanalyzer-go (cango) backend binary for CLDK (codellm-devkit).
Project-URL: Homepage, https://codellm-devkit.info
Project-URL: Repository, https://github.com/codellm-devkit/codeanalyzer-go
Author: codellm-devkit
License: Apache-2.0
Keywords: call-graph,cldk,code analysis,codellm-devkit,go,golang
Classifier: Development Status :: 4 - Beta
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Go
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.8
Description-Content-Type: text/markdown

<div align="center">

<img src="https://github.com/codellm-devkit/codeanalyzer-python/blob/main/docs/assets/logo.png?raw=true" alt="CodeLLM-DevKit" />

# codeanalyzer-go (`cango`)

**A Go static-analysis toolkit — the CLDK backend that emits a canonical symbol table and call graph as `analysis.json`.**

[![PyPI](https://img.shields.io/pypi/v/codeanalyzer-go?style=for-the-badge&logo=pypi&logoColor=white)](https://pypi.org/project/codeanalyzer-go/)
[![Go](https://img.shields.io/github/go-mod/go-version/codellm-devkit/codeanalyzer-go?style=for-the-badge&logo=go&logoColor=white)](https://go.dev/)
[![Release](https://img.shields.io/github/actions/workflow/status/codellm-devkit/codeanalyzer-go/release.yml?style=for-the-badge&label=release&logo=github)](https://github.com/codellm-devkit/codeanalyzer-go/actions/workflows/release.yml)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue?style=for-the-badge)](./LICENSE)

</div>

---

`cango` is a static analyzer for Go built on [`golang.org/x/tools/go/packages`](https://pkg.go.dev/golang.org/x/tools/go/packages)
(AST + full type resolution). It produces the canonical CodeLLM-DevKit (CLDK) `analysis.json` — a
symbol table plus a resolver-based call graph — consumable by the Python SDK via
`CLDK(language="go").analysis(project_path=...)`. It is the Go backend behind
[CLDK](https://github.com/codellm-devkit/python-sdk), mirroring its
[Python](https://github.com/codellm-devkit/codeanalyzer-python),
[TypeScript](https://github.com/codellm-devkit/codeanalyzer-typescript), and
[Java](https://github.com/codellm-devkit/codeanalyzer-java) siblings.

The binary is fully self-contained: a single static executable with no runtime dependencies.

## Table of Contents

- [Features](#features)
- [Installation](#installation)
  - [Prerequisites](#prerequisites)
  - [Install via shell script](#install-via-shell-script)
  - [Install via Homebrew](#install-via-homebrew)
  - [Install via pip (PyPI)](#install-via-pip-pypi)
  - [Build from source](#build-from-source)
  - [Toolchain](#toolchain)
- [Usage](#usage)
  - [Command-line options](#command-line-options)
  - [Examples](#examples)
- [Analysis levels](#analysis-levels)
- [Output schema](#output-schema)
- [Neo4j projection (`--emit neo4j`)](#neo4j-projection---emit-neo4j)
  - [Emit a snapshot (`graph.cypher`)](#emit-a-snapshot-graphcypher)
  - [Push live to a running Neo4j](#push-live-to-a-running-neo4j)
  - [Load and query](#load-and-query)
  - [Query reference](#query-reference)
- [Python SDK (CLDK) integration](#python-sdk-cldk-integration)
- [Upgrading](#upgrading)
- [Architecture & Tooling](#architecture--tooling)
- [Development](#development)
- [License](#license)

## Features

- **Symbol table** — packages, structs, interfaces, fields, methods, package-level functions,
  imports, struct tags, and generic type parameters, with precise source spans.
- **Call graph** — a resolver-based call graph via `go/types`: each call site is resolved to its
  full import-path signature. Level 2 emits edges to project callees and to library callees
  (homed in `application.external_symbols` under `can://<namespace>/<app>/@external/<import-path>/<sig>`).
  Level 3 (`-a 3`) adds per-callable `cfg`, `cdg` and `ddg`. Level 4 (`-a 4`) adds the
  interprocedural system dependence graph (SDG): parameter vertices, `param_in` / `param_out`,
  call-site `summary` edges, VTA dispatch targets and library stand-ins — what the SDK's
  slicing and `taint()` walk.
- **Generics-aware** — Go 1.18+ generics (`Set[T]`, union-constraint interfaces, multi-type-param
  functions) are modeled in the unified type/callable schema.
- **Self-contained binary** — a single static executable (`go build`, `CGO_ENABLED=0`); no runtime
  dependencies for SDK users.
- **CLDK canonical schema** — output is spine-compatible with the Java/Python/TypeScript analyzers,
  loadable directly by the Python SDK.
- **Neo4j projection** — the same v2 envelope projected into a property graph (`--emit neo4j`),
  either as a replayable `graph.cypher` snapshot or pushed live over Bolt, keyed on the identical
  `can://` ids so JSON and graph join on one string.

## Installation

### Prerequisites

Running a prebuilt `cango` binary requires **nothing** — it is fully self-contained. To *analyze* a
project, that project should be a normal Go module (contain a `go.mod`) so the type checker can
resolve imports. Building `cango` from source requires [Go 1.26+](https://go.dev/dl/), and the
latest Go release is recommended — see [Toolchain](#toolchain).

### Install via shell script

Download and install the prebuilt binary for your platform from the latest release:

```sh
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/codellm-devkit/codeanalyzer-go/releases/latest/download/cango-installer.sh | sh
```

The installer drops `cango` into `~/.local/bin` (override with `CANGO_INSTALL_DIR`) and can pin a
version with `CANGO_VERSION=vX.Y.Z`. It also creates a `codeanalyzer-go` alias symlink. Supports
macOS (arm64/x86_64) and Linux (x86_64/aarch64).

### Install via Homebrew

```sh
brew install codellm-devkit/homebrew-tap/codeanalyzer-go
```

### Install via pip (PyPI)

The wheel bundles the prebuilt, self-contained binary for your platform (no Go toolchain required):

```sh
pip install codeanalyzer-go
cango --help
```

The wheel installs both a `cango` launcher and a `codeanalyzer-go` alias on `PATH`. This is also the
package CLDK's Python SDK depends on to locate the analyzer backend; it exposes
`codeanalyzer_go.bin_path()`.

### Build from source

```sh
git clone https://github.com/codellm-devkit/codeanalyzer-go
cd codeanalyzer-go
go build -o cango ./cmd/codeanalyzer
```

This produces a single static binary `cango` with no runtime dependencies. You can also run the
analyzer directly from source without compiling:

```sh
go run ./cmd/codeanalyzer -i /path/to/project -a 2
```

### Toolchain

`cango` type-checks the projects it analyzes with the `go/types` package compiled into the
binary, so **the Go that builds `cango` must be at least as new as the newest `go` line of the
projects it analyzes**. Build with the latest Go release (the release binaries are). `go.mod`'s
`go 1.26.0` is only the minimum that compiles the analyzer (set by `golang.org/x/tools`, whose
recent releases require Go 1.26).

With a binary built by an older Go, every package that needs a newer Go type-checks only
partially, yet the run still exits 0: callees go unresolved, call edges and L3 defs are lost,
and nothing else looks wrong. `cango` prints one warning when this happens, naming the Go it was
built with, the highest version required and the number of affected packages:

```
[codeanalyzer-go WARN] 12 packages require a newer Go version (highest go1.27) than this analyzer was built with (go1.26.4); …
```

Rebuild `cango` with that Go version or newer. (`-vv` lists the affected packages.)

## Usage

```bash
cango -i /path/to/go/project
```

### Command-line options

```
codeanalyzer-go produces analysis.json (symbol table + call graph) for Go projects.

Usage:
  cango [flags]

Aliases:
  cango, codeanalyzer-go

Flags:
  -a, --analysis-level int      Analysis level: 1=symbol table, 2=+call graph, 3=+intraprocedural cfg/cdg/ddg, 4=+interprocedural SDG (default 1)
      --app-name string         Application anchor name for can:// ids and Neo4j :Application (default: input dir name)
  -c, --cache-dir string        Cache directory (default: ~/.cldk/go-cache)
      --eager                   Force clean rebuild (ignore cache)
      --emit string             Output projection: json|neo4j|schema (default "json")
  -f, --format string           Output format: json|msgpack (default "json")
      --graph-field-depth int   Field-path depth bound for L3 dataflow nodes (k limit; applies at -a 3 and -a 4) (default 3)
  -h, --help                    help for cango
  -i, --input string            Project root to analyze (required)
  -j, --jobs int                Worker parallelism (default: CPU cores)
      --library-models string   JSON file of library stand-in flows overriding the all-to-all default (-a 4 and --emit neo4j only)
      --neo4j-database string   Neo4j database (env NEO4J_DATABASE, optional)
      --neo4j-password string   Neo4j password (env NEO4J_PASSWORD, default neo4j)
      --neo4j-uri string        Live Bolt push target (env NEO4J_URI); omit to write graph.cypher
      --neo4j-user string       Neo4j username (env NEO4J_USERNAME, default neo4j)
  -o, --output string           Output directory for analysis.json (default: stdout)
      --skip-tests              Skip *_test.go files (default true)
      --strict                  Fail non-zero on any L4 degradation (default: degrade and record it)
  -t, --target-files strings    Restrict analysis to specific files (incremental mode)
  -v, --verbose count           Verbosity (repeat for more detail)
      --version                 Print version and exit
```

### Examples

**Symbol table only (Level 1, default):**
```bash
cango -i ./my-go-project
```
Prints `analysis.json` to stdout.

**Symbol table + call graph (Level 2):**
```bash
cango -i ./my-go-project -a 2
```

**Write output to a directory:**
```bash
cango -i ./my-go-project -a 2 -o /path/to/output/
# Writes: /path/to/output/analysis.json
```

**Incremental analysis (specific files only):**
```bash
cango -i ./my-go-project -t pkg/server/server.go -t pkg/server/handler.go
```

**Force rebuild, ignore cache:**
```bash
cango -i ./my-go-project --eager
```

**Verbose output:**
```bash
cango -i ./my-go-project -a 2 -vv
```

## Generating `analysis.json` for a Go app

The end-to-end steps to produce an `analysis.json` file for any Go project.

**1. Make sure you have `cango`.** Either install a released binary (see
[Installation](#installation)) or build from source:

```bash
git clone https://github.com/codellm-devkit/codeanalyzer-go
cd codeanalyzer-go
go build -o cango ./cmd/codeanalyzer
```

**2. Point it at the project root** (the directory containing the app's `go.mod`).
`cango` resolves imports with the Go type checker, so the target must be a normal Go
module. Write the output to a directory with `-o`; `cango` names the file
`analysis.json` inside it.

```bash
# Level 2 (symbol table + call graph):
cango -i /path/to/go/app -a 2 -o /path/to/output/
# → writes /path/to/output/analysis.json
```

> **Schema.** `cango` emits the **canonical CLDK v2** tree (the shape the Python SDK's v2
> loader and the sections below describe). Omit `-o` to stream the JSON to stdout instead
> of writing a file.

**3. (Optional) name the application anchor.** `--app-name` sets the `<app>` in every
`can://<namespace>/<app>/…` id and defaults to the input directory's base name. Set it when the
directory name isn't the identity you want:

```bash
cango -i /path/to/go/app -a 2 --app-name myapp -o ./out/
```

**4. Verify.** A successful run exits `0` and produces a JSON document whose envelope
carries `"schema_version": "2.0.0"`, `"language": "go"`, `"max_level": 2`, and an
`application` tree under `application.symbol_table`:

```bash
cango -i /path/to/go/app -a 2 -o ./out/ && echo "exit=$?"
python3 -c "import json; d=json.load(open('out/analysis.json')); print(d['schema_version'], d['max_level'], len(d['application']['symbol_table']), 'modules')"
```

**Notes for larger apps.** Analysis parallelism defaults to your CPU count (tune with
`-j`); a warm cache (`--cache-dir`, default `~/.cldk/go-cache`) makes re-runs fast, and
`--eager` forces a clean rebuild. Projects that use **cgo** (`import "C"`) are supported:
the toolchain-synthesized wrapper functions are correctly excluded from the output (they
are build artifacts, not project source), so only your own declarations are emitted.

## Analysis levels

| Level | Flag | What runs | Status |
|-------|------|-----------|--------|
| 1 | `-a 1` (default) | Symbol table only — types, functions, call sites | Implemented |
| 2 | `-a 2` | Level 1 + resolver-based call graph via `go/types` | Implemented |
| 3 | `-a 3` | Level 2 + per-callable statement nodes and `cfg` / `cdg` / `ddg` | Implemented |
| 4 | `-a 4` | Level 3 + interprocedural SDG: VTA dispatch, parameter vertices, `param_in` / `param_out`, `summary`, port and `points-to` `ddg`, library stand-ins | Implemented |

**Level 1** loads each package with `packages.NeedSyntax | NeedTypes | NeedTypesInfo` and walks the AST file by file. Call sites are recorded with `callee_signature = null` at this stage.

**Level 3** completes every callable's `body` with its statement, control (`branch`, `loop`,
`switch`, `return`) and synthetic (`@entry`, `@exit`) nodes around the L1 call nodes, and adds
three intra-callable edge lists: `cfg` (control flow, including the `defer` chain and panic
`exception` edges), `cdg` (control dependence from post-dominators) and `ddg` (syntactic
reaching definitions over k-limited access paths, `prov: ["ssa"]`). `--graph-field-depth <k>`
(default 3, minimum 1) bounds the access paths (`x.f.g.h` → `x.f.g.*` at k = 3) and is
recorded in the envelope as `k_limit`. It is a flag error below `-a 3` and with
`--emit neo4j`. Callables are analyzed in parallel (`-j`), and the output is byte-identical
for every `-j`. L3 only adds to the L2 output: every L2 fact stays as it is.

**Level 4** is whole-program. It builds `go/ssa` for the project packages over the same load and
runs VTA (`golang.org/x/tools/go/callgraph/vta`) over the project's functions, so interface and
function-value calls get their concrete targets: `call_graph` gains those edges with
`prov: ["vta"]` (an L2 edge is never changed). Every callable gets `formal_in` / `formal_out`
vertices (parameters, receiver, closure captures, the package variables it reads or writes,
results), and every resolved call site gets `actual_in` / `actual_out` vertices, wired into the
body's `ddg` (`prov: ["reaching-defs"]`). `param_in` / `param_out` (application scope) join each
call site to every target, including the interface method itself for an interface call, and a
bottom-up pass over the call graph's SCCs puts `summary` edges (`actual_in` → `actual_out`) at
each call site. Library callees get a **stand-in** in `external_symbols` (`body` holds only its
formal vertices; summary all inputs → all outputs unless `--library-models` overrides it). A
type-based may-alias oracle adds `ddg` edges with `prov: ["points-to"]`. `-a 3` output is a
subset of `-a 4` output, and `-j 1` and `-j N` output are byte-identical.

- **Degradation.** When L4 cannot build all it needs, it still emits and records why in the
  envelope's `degraded[]`. Each entry has a `code`, an optional `detail`, and optional `packages[]`
  and `callables[]`. The codes are:
  - `no_ssa` / `partial_ssa`: SSA failed, in whole or for some packages;
  - `no_vta`: VTA failed;
  - `no_cha`: the CHA complement for library-originated values failed (its targets are dropped);
  - `partial_types`: some packages have type errors;
  - `unmapped_ssa`: SSA functions or calls that map to no callable;
  - `l3_skipped`: callables without an L3 result, which get a pass-through summary;
  - `range_func`: range-over-func, which is not modelled at L4.

  It also logs one warning per entry. `--strict` makes any degradation a non-zero exit.
- **`--library-models <file.json>`** overrides stand-in summaries:
  `{"strconv/Atoi": [], "strings/TrimSpace": [["s", "<return>"]]}`. A key is the external id's
  `<import-path>/<sig>` tail, and a value is a list of `[in, out]` pairs of `of` names (`[]` = no
  summary edges). A model changes summaries, **not statement-level reachability**: in
  `n, err := strconv.Atoi(id)` the value assigned at the call site keeps its reaching-definition
  edge from `id`, which is correct data flow. A model is therefore not a sanitizer. Cutting such
  a flow is the SDK taint analysis's job
  ([codellm-devkit/.github#103](https://github.com/codellm-devkit/.github/issues/103)). A variable
  cut on `n` is also bypassed, by implicit flow through `err` and by the coarse host wiring. An
  entry naming an `of` the external lacks falls back to all-to-all, with a warning. A key no call
  site targets is ignored, with a warning. The flag is valid only at `-a 4` and with
  `--emit neo4j`.
- **`-t` / `--target-files` is not supported at `-a 4`** (whole-program): it is an error. Use
  `-a 3 -t`, or run without `-t`.
- **Known limits** (see `docs/design/notes/l3-dataflow-metrics.md` § L4):
  - library code calling back into the project (`sort.Slice`, `http.HandleFunc`) is not an
    edge;
  - an interface call on a value that may have come out of library code (`ctx.Value(k).(Store)`,
    `sync.Map.Load`, a library container), directly or through project code (results,
    parameters, captures, fields, globals, container elements), binds every project type
    implementing the interface (a CHA complement). Storage is tracked by container type (every
    instance of a struct field), which is sound but coarser than VTA;
  - range-over-func is not modelled (`range_func`);
  - summaries are close to all-to-all, by design;
  - `summary` edges whose two ends are both `<global>:` vertices are omitted
    (codeanalyzer-go#30). Reachability is unchanged, but a depth-bounded slice can stop earlier
    than it would through that shortcut.

**Level 2** adds a resolver pass: for each call site, `go/types` resolves the callee to its full import-path signature (`pkgImportPath.TypeName.MethodName`). A project callee becomes an edge to its callable; a **library callee** (stdlib, a dependency, or a package vendored under `vendor/`) becomes an edge to an `@external` symbol, recorded once in `application.external_symbols`. A callee that is neither (an interface method of a project type, a conversion, a builtin, a call through a function value) gets no edge. Every resolved call node's `callee` is backfilled.

## Output schema

`cango` emits the **canonical CLDK v2** shape — the shape the v2 Python SDK loader consumes
and the [step-by-step section above](#generating-analysisjson-for-a-go-app) produces.

### Canonical v2 shape

The document is a manifest envelope wrapping one `application` containment tree. Every
node carries a `can://<namespace>/<app>/…` `id`, a `kind`, and a `span`:

```json
{
  "identity_version": 2,
  "schema_version": "2.0.0",
  "language": "go",
  "max_level": 2,
  "analyzer": { "name": "codeanalyzer-go", "version": "0.1.0" },
  "application": {
    "id": "can://myapp/myapp",
    "identity_version": 2,
    "application_name": "myapp",
    "identity_namespace": "myapp",
    "kind": "application",
    "symbol_table": {
      "pkg/greeter/greeter.go": {
        "id": "can://myapp/myapp/go/pkg/greeter/greeter.go",
        "kind": "module",
        "package": "greeter",
        "span": { "start": [1, 1], "end": [40, 2], "bytes": [0, 812] },
        "source": "package greeter\n\n...",
        "content_hash": "…",
        "imports": [ { "name": "fmt", "path": "fmt", "span": {…} } ],
        "types": {
          "Greeter": {
            "id": "can://myapp/myapp/go/pkg/greeter/greeter.go/example.com/pkg/greeter.Greeter",
            "kind": "struct",
            "span": { "start": [5, 1], "end": [7, 2], "bytes": [17, 63] },
            "fields": {
              "Prefix": { "id": "…/Prefix", "kind": "field", "type": "string", "span": {…} }
            },
            "callables": {
              "example.com/pkg/greeter.Greeter.Greet": {
                "id": "can://myapp/myapp/go/pkg/greeter/greeter.go/example.com/pkg/greeter.Greeter/example.com/pkg/greeter.Greeter.Greet",
                "kind": "method",
                "signature": "example.com/pkg/greeter.Greeter.Greet",
                "span": { "start": [9, 1], "end": [11, 2], "bytes": [65, 140] },
                "parameters": [ { "name": "name", "type": "string", "span": {…} } ],
                "return_type": "string",
                "error_channel": ["error"],
                "metrics": { "cyclomatic": 1 },
                "body": {
                  "10:2": {
                    "kind": "call",
                    "id": "can://myapp/myapp/go/pkg/greeter/greeter.go/…/example.com/pkg/greeter.Greeter.Greet@10:2",
                    "span": { "start": [10, 2], "end": [10, 30], "bytes": [90, 118] },
                    "callee": "can://myapp/myapp/go/pkg/greeter/greeter.go/…/example.com/pkg/greeter.format"
                  }
                }
              }
            }
          }
        },
        "functions": {
          "example.com/main.main": { "id": "…", "kind": "function", "signature": "…", "body": {…} }
        }
      }
    },
    "call_graph": [
      {
        "src": "can://myapp/myapp/go/main.go/…/example.com/main.main",
        "dst": "can://myapp/myapp/go/pkg/greeter/greeter.go/…/example.com/pkg/greeter.Greeter.Greet",
        "prov": ["go/types"],
        "weight": 1
      }
    ]
  }
}
```

Key v2 schema properties:
- **Envelope** — `schema_version` (`"2.0.0"`), `language` (`"go"`), `max_level` (1–4),
  `k_limit` (L3+: the `--graph-field-depth` value), `degraded[]` (L4, only when something
  degraded) and `analyzer{name,version}` wrap a single `application` node.
- **One containment tree** — `application → module → type → callable → body`, keyed as
  `symbol_table` (modules, by **project-relative file path**), `types`, `callables`, `body`.
- **`id`** — every node carries a durable `can://<namespace>/<app>/go/<file>/<type>/<signature>` id;
  `<app>` is the `--app-name` anchor. A body node is keyed by `line:col` in its callable's `body`
  map and carries `id` = `<callable-id>@<key>` at every level, from `-a 1` on.
- **`kind`** — `module` · `type` kinds `struct | interface | alias | defined` (collapses v1's
  `is_interface`) · `callable` kinds `function | method | lambda` · body `call`.
- **`base_types: []`** (type, optional) — embedded type ids (struct/interface embedding — the
  explicit spine), as opposed to interfaces the type is *computed* to satisfy.
- **`span`** — `{start:[line,col], end:[line,col], bytes:[from,to]}`; `bytes` are **UTF-8 byte
  offsets** into the owning `module.source` (source is stored once per module, every node slices it).
- **`source_file`** (callable, optional) — set when a method is declared in a *different file*
  than its receiver type, and on every closure (nested ones too) inside such a method; the
  callable stays nested under the type, and its `span.bytes` (and its body nodes' spans) index
  `symbol_table[source_file].source` instead of the nesting module's.
- **`error_channel: []`** — populated from `error`-typed returns (the returns also stay in
  `return_type`). **Closures** nest as `callables{}` on the enclosing callable.
- **`body` call nodes** — `callee` is the sanctioned `null → id` slot: `null` at L1; at L2 a
  project callable's `can://` id, or for a library (stdlib or dependency) callee its
  `can://<namespace>/<app>/@external/<import-path>/<sig>` id, a key of `external_symbols`. It stays `null`
  for conversions, builtins, calls through function values and unresolved sites. `is_goroutine` / `is_deferred`
  are boolean flags emitted **only when true** (a plain call omits them), for `go f()` / `defer f()`.
- **`call_graph` edges** — `{src, dst, prov, weight}`; `src` is a callable id in the tree and
  `dst` a callable id or an `external_symbols` key (never a raw signature); `prov` is resolver
  provenance, e.g. `["go/types"]`.
- **`external_symbols`** (application, L2, optional) — one entry per library callee, keyed by
  its `@external` id: `{id, kind: "external", name, module, signature, parameters[], return_type}`
  (parameters carry no span). Absent when no library call was resolved.
- **L3 (`-a 3`)** — `body` gains `statement` / `branch` / `loop` / `switch` / `return` nodes
  and the synthetic `@entry` / `@exit` (which have no `span`). Every body node carries `id`
  (`<callable-id>@<key>`).
  A call nested in a statement carries `parent`, the statement's key. The callable gains
  `cfg[] {src, dst, kind}`, `cdg[] {src, dst}` and `ddg[] {src, dst, var, prov}`, whose
  endpoints are `body` keys. See `docs/design/specs/v2-schema-structure.md`.
- **L4 (`-a 4`)** — `body` gains span-less vertices `@formal_in:<i>`, `@formal_out[:<i>]`,
  `<call-key>/actual_in:<i>`, `<call-key>/actual_out[:<i>]`, each with `of` (the bound name:
  a parameter or receiver name, `_<i>`, `_recv`, `<return>`, `<return>:<i>`,
  `<capture>:<name>`, `<global>:<pkgpath>::<Name>`); actuals carry `parent` = the call key.
  `ddg` gains `prov: ["reaching-defs"]` (port wiring) and `["points-to"]` edges; a callable gains
  `summary[] {src, dst}` (local keys); the application gains `param_in[]` / `param_out[]`
  `{src, dst, var}` (global body-node ids, `var` = the callee formal's `of`); an external gains
  `receiver {name, type}` and a stand-in `body` of its formal vertices (ids
  `<external-id>@formal_in:<i>`).

## Neo4j projection (`--emit neo4j`)

`--emit neo4j` projects the **same v2 envelope** into a Neo4j property graph. It is a *second
projection of the identical analysis* — JSON nodes and graph nodes carry the same `can://<namespace>/<app>/…`
ids, so the two join on one string and are never recomposed. The graph vocabulary is `Go`-prefixed
labels (`:GoModule`, `:GoType`, `:GoCallable`, `:GoField`, `:GoBodyNode`, `:GoExternal`, plus the
shared merge label `:GoSymbol` and marker `:GoCanNode`) and `GO_`-prefixed relationships
(`GO_HAS_MODULE`, `GO_DECLARES`, `GO_HAS_METHOD`, `GO_HAS_FIELD`, `GO_CALLS`, `GO_RESOLVES_TO`,
`GO_IMPORTS`, `GO_EMBEDS`, `GO_SATISFIES`, …).

There are two output paths, chosen by whether `--neo4j-uri` is set:

| | `--neo4j-uri` unset | `--neo4j-uri` set |
|---|---|---|
| **What happens** | Writes a replayable `graph.cypher` snapshot | Pushes the graph live over Bolt into a running Neo4j |
| **Where** | `<-o dir>/graph.cypher` (or stdout) | The target database |
| **Use for** | Review, diffing, loading on your own schedule, CI | A DB you already have running |

Both paths are **scoped to one application**: every destructive statement is anchored on
`:GoCanNode` and the `can://<namespace>/<app>/` id prefix, so it never touches another app (or a
sibling-language graph) in the same database. The projection is **deterministic** — a second emit
is byte-identical.

What a re-emit replaces depends on the path:

- **`graph.cypher` snapshot** and **`--eager` Bolt push**: the app's subgraph is purged and
  rebuilt, so re-emitting replaces exactly that app's subgraph.
- **Default (lazy) Bolt push**: purely additive. Changed modules are MERGE-upserted and nothing is
  deleted. After source edits, nodes and edges that the new analysis no longer emits stay in the
  database. For L3 this includes `:GoBodyNode`s and their `GO_CFG_NEXT` / `GO_CDG` / `GO_DDG`
  edges, whose ids are keyed on `line:col`: once lines shift, the old body nodes and their edges
  remain beside the new ones. Pass `--eager` when you push an L3 graph after editing source.
  The purge needs a full run: with `--target-files` it is skipped, because the push carries only
  the targeted packages.
- **L4 rows on a lazy push (D5).** L4 facts are not local to a module: editing a callee changes
  its callers' summaries and vertices. So a lazy push upserts **every** L4 row (vertices,
  `GO_PARAM_IN` / `GO_PARAM_OUT`, `GO_SUMMARY`, port `GO_DDG`) on every push, not only the changed
  modules'. It still adds and never removes: stale L4 rows stay until an `--eager` full run.
- **Level changes.** A lazy push whose level is above the stored `:GoApplication.max_level` (a graph
  last pushed at L3, or with `-t`), or whose stored level is unreadable, upserts every module, so
  no L4 row is dropped for a missing L3 endpoint. A push at a *lower* level onto a higher graph
  diffs as usual and leaves the old L4 rows in place (additive). An **interrupted** push can leave
  `:GoApplication` at the new `max_level` while some modules still hold older rows; the next lazy
  push then trusts the diff. Recover with an `--eager` full run.
- **`--target-files` (D9).** L4 is whole-program, so `--emit neo4j -t` projects the targeted modules
  at **L3** and writes no L4 rows; they keep the last full run's facts until the next full run. It
  warns once on stderr: `--target-files: projecting the graph at level 3 (level 4 is
  whole-program); L4 rows keep the last full run's facts until a full run` (plus `;
  --library-models is ignored at level 3` when that flag is set).

> **`--emit neo4j` is always full depth.** The graph is projected at the max implemented level (L4
> — symbol table, call graph, every callable's body nodes with `GO_CFG_NEXT` / `GO_CDG` /
> `GO_DDG`, and the SDG: vertex `:GoBodyNode`s with `var`, `GO_PARAM_IN` / `GO_PARAM_OUT {var}`,
> `GO_SUMMARY`, stand-in vertices under their `:GoExternal`, and `degraded_json` on
> `:GoApplication`) and canonical schema v2. Do **not** pass `-a`/`--analysis-level`
> with it — an explicit level is a flag error (`--analysis-level does not apply to --emit neo4j`),
> because there is no shallower graph to ask for.

### Emit a snapshot (`graph.cypher`)

```bash
cango -i /path/to/go/app --emit neo4j -o ./out/
# → writes ./out/graph.cypher
```

The snapshot is a self-contained script: constraints + indexes, a scoped wipe of this app's subgraph,
then `UNWIND … MERGE` batches for nodes and relationships. Replay it into any Neo4j with
`cypher-shell` (see below). Omit `-o` to stream the Cypher to stdout.

### Push live to a running Neo4j

Set `--neo4j-uri` (or the `NEO4J_URI` env var) to push directly over Bolt instead of writing a file:

```bash
./cango -i /path/to/go/app  --emit neo4j \
  --neo4j-uri bolt://127.0.0.1:7687 \
  --neo4j-user neo4j --neo4j-password <pass>
```

Credentials and target also read from the environment (`NEO4J_URI`, `NEO4J_USERNAME`,
`NEO4J_PASSWORD`, `NEO4J_DATABASE`), so you can keep them out of your shell history:

```bash
export NEO4J_URI=bolt://127.0.0.1:7687
export NEO4J_PASSWORD=<pass>
cango -i /path/to/go/app  --emit neo4j
```

> Use `127.0.0.1` rather than `localhost` if anything else (an SSH tunnel, another DB) might also be
> listening on `7687` — `localhost` can resolve to the wrong listener.

### Load and query

If you emitted a `graph.cypher` snapshot, load it into a running Neo4j with `cypher-shell`:

```bash
cypher-shell -a bolt://127.0.0.1:7687 -u neo4j -p <pass> < ./out/graph.cypher
```

A convenience harness, [`scripts/load_and_query.sh`](scripts/load_and_query.sh), loads the snapshot
and runs a set of sample queries — it auto-detects `cypher-shell` from a Neo4j Desktop or Homebrew
install. It reads `graph.cypher` from its own directory, so emit the snapshot into `scripts/` first
(`-o scripts`):

```bash
# brew install neo4j && neo4j start   # (or Neo4j Desktop) — then set a password
cango -i /path/to/go/app --emit neo4j -o scripts         # writes scripts/graph.cypher

NEO4J_PASSWORD=<pass> ./scripts/load_and_query.sh          # load + query
NEO4J_PASSWORD=<pass> ./scripts/load_and_query.sh --query  # queries only
NEO4J_PASSWORD=<pass> ./scripts/load_and_query.sh --check  # just test the connection
```

Or explore visually in Neo4j Browser at `http://127.0.0.1:7474` (connect to `bolt://127.0.0.1:7687`),
paste a query, and swap `RETURN p` for `RETURN …` to draw the graph.

### Query reference

Always **scope by the `can://` id prefix** (an index-backed seek via the `gocannode_id` range index),
never by a bare label scan — replace `<app>` with your `--app-name`:

```cypher
// Node counts by label.
MATCH (n:GoCanNode) RETURN labels(n)[0] AS label, count(*) AS n ORDER BY n DESC;

// Relationship counts by type.
MATCH ()-[r]->() RETURN type(r) AS rel, count(*) AS n ORDER BY n DESC;

// Everything owned by one application (prefix seek).
MATCH (n:GoCanNode) WHERE n.id STARTS WITH 'can://<namespace>/<app>/' RETURN count(n) AS app_nodes;

// Types ranked by method count (the "god struct" finder).
MATCH (t:GoType) OPTIONAL MATCH (t)-[:GO_HAS_METHOD]->(m)
RETURN t.kind AS kind, t.signature AS type, count(m) AS methods ORDER BY methods DESC;

// Most-called callables (in-degree) — the hot utilities.
MATCH (:GoCallable)-[:GO_CALLS]->(b:GoCallable)
RETURN b.signature AS callee, count(*) AS called_by ORDER BY called_by DESC LIMIT 15;

// Everything Command.Execute reaches, transitively (any depth).
MATCH (:GoCallable {signature:'github.com/spf13/cobra.Command.Execute'})-[:GO_CALLS*]->(b)
RETURN DISTINCT b.signature AS reachable ORDER BY reachable;

// External / stdlib packages reached via imports.
MATCH (:GoCanNode)-[:GO_IMPORTS]->(x:GoExternal) RETURN DISTINCT x.path AS import ORDER BY import;
```

The full, commented catalog — overview/sanity, containment tree, call graph (callers, callees,
reachability, shortest path, entry points, leaves), call-site resolution, imports, inheritance,
metrics, and scoped maintenance — lives in
[`Eval/Neo4j_eval/queries.cypher`](Eval/Neo4j_eval/queries.cypher), validated against the
`spf13/cobra` graph.

## Python SDK (CLDK) integration

```python
from cldk import CLDK

analysis = CLDK(language="go").analysis(project_path="/path/to/go/project")
for file_path, go_file in analysis.get_symbol_table().items():
    print(file_path, go_file.module_name)
```

The SDK locates the analyzer via the `codeanalyzer-go` command on `PATH` (installed by any of the
methods above, including `pip install codeanalyzer-go`). See
[python-sdk](https://github.com/codellm-devkit/python-sdk) for full API documentation.

## Upgrading

This release changes the output contract. If you consume `analysis.json` or the Neo4j graph,
check the following.

- **Ids are app-outermost.** `can://go/<app>/<file>/…` is now `can://<namespace>/<app>/go/<file>/…`. Library
  callees are `can://<namespace>/<app>/@external/<import-path>/<sig>`.
- **Body nodes carry `id` at every level.** Each body node has `id` = `<callable-id>@<local-key>`,
  from `-a 1` on, not only at `-a 3`.
- **L2 includes library calls.** The call graph now has edges to library (stdlib and dependency)
  callees, and `application.external_symbols` describes them. A library call's `callee` is its
  external id instead of `null`. A type conversion is still a call node, but its `callee` stays
  `null`; it is no longer resolved to a `<Type>.__new__` pseudo-callee.
- **L1 records more.**
  - A type's `kind` can now be `alias` or `defined`, not only `struct` or `interface`.
  - Deferred calls carry `is_deferred: true`.
  - Chained calls that start at the same position, such as `s.A().B()`, are now separate call
    nodes keyed `l:c` and `l:c/2`. Previously the inner call was dropped. Do not parse body keys
    as plain `line:col`.
  - Calls inside `go` statement arguments, such as `g()` in `go f(g())`, are now call sites.
  - Consumers that count call nodes will see more of them.
- **L4 (`-a 4`) is new.** It is additive: an `-a 3` consumer sees the same document plus new
  keys (vertices, `of`, `summary`, `param_in` / `param_out`, `points-to` / `reaching-defs` `ddg`,
  VTA `call_graph` edges, external `receiver` / `body`, `degraded`). Code that assumes every body
  node has a `span`, or that every `ddg` edge has `prov: ["ssa"]`, must handle the L4 kinds.
  `-a 4 -t` is an error (whole-program; D9). `--library-models` is new and valid only at L4.
  **Measured cost** (2026-10-28, analyzer built with go1.27.3, eight real apps). On 7 of 8
  apps, the peak RSS of `-a 4` is 1.0–1.5× that of `-a 3`; Kubernetes takes 38 s at 7.9 GB.
  nats-server is the outlier at 3.8×, because its globals fan out through one recursive 822-callable
  SCC. The JSON output is 1.9–23× that of `-a 3`. Per-app figures are in
  `docs/design/notes/l3-dataflow-metrics.md` § L4.
- **Neo4j:**
  - `--emit neo4j` projects **L4** (D4), adding `GO_CFG_NEXT`, `GO_CDG` and `GO_DDG`, and the SDG
    rows `GO_PARAM_IN`, `GO_PARAM_OUT`, `GO_SUMMARY`, vertex `:GoBodyNode`s with `var`, and
    `degraded_json`. `-a` with it is still an error.
  - A lazy push upserts every L4 row on every push and removes none (D5); use `--eager` to drop
    stale ones. `--emit neo4j -t` projects at L3 with a warning (D9). A push at a higher level
    than the stored graph upserts everything; a lower-level push leaves the old L4 rows. See
    [Neo4j projection](#neo4j-projection---emit-neo4j).
  - `:GoBodyNode.call_node` is the enclosing statement's local key (the JSON `parent`), not a
    global id.
  - `--eager` now works on Neo4j 5.x.
- **Build.** Building from source needs Go 1.26 or newer. Release binaries are built with the
  latest Go (see [Toolchain](#toolchain)).

## Architecture & Tooling

| Slot | Choice | Rationale |
|------|--------|-----------|
| Runtime | Go binary | Self-contained; no runtime dep for SDK users |
| Structural parser | `go/ast` (stdlib) | Part of the standard toolchain; no external dep |
| Type resolver | `golang.org/x/tools/go/packages` | Single API for both AST + full type resolution; handles modules natively |
| Build/dep materialization | `go mod download` | Required before `packages.Load` so the module cache is warm; result cached by `go.sum` hash |
| Packaging | Native binary (`go build`) | Zero-runtime-dep distribution; matches Rust/C++ analyzers |
| Analysis depth | Levels 1–4 | Symbol table, resolver call graph, intraprocedural cfg/cdg/ddg, interprocedural SDG |
| Call-graph dispatch | Declared-type resolution via `go/types.Selections` | CHA-equivalent; sufficient for cross-package reachability at Level 1 |
| CFG (L3) | Hand-built from `go/ast` (`internal/dataflow`) | Adds what `go/cfg` does not model as edges: panic `exception` edges and the `defer` chain (`defer_resume`). A differential test checks reachability against `golang.org/x/tools/go/cfg` on every fixture body |
| Def-use / DDG (L3) | Syntactic reaching definitions over k-limited access paths | `go/types` objects as bases (shadowing-safe); weak defs through reference arguments; `prov: ["ssa"]` for parity with the Java and Python L3 |
| SSA (L4) | `golang.org/x/tools/go/ssa` via `ssautil.Packages` over the same load, project packages only, built per package under `recover` | Bodies only for the project (dependencies from types); a panic degrades instead of aborting |
| Dispatch oracle (L4) | VTA (`go/callgraph/vta`) over the project's functions, library targets included | Concrete targets for interface and function-value calls; an interface call also binds the interface method (pass-through) |
| Alias oracle (L4) | Type-based may-alias (structural pointee compatibility, interface pairs, address-taken values, one-level field offset) | Additive `points-to` `ddg` edges; an interface so Andersen points-to can replace it without a schema change |

### Package structure

```
codeanalyzer-go/
├── cmd/codeanalyzer/         # CLI entry point (cobra) — builds the `cango` binary
├── internal/
│   ├── core/                 # Orchestrator — delegates only, no inlined analysis
│   ├── schema/               # GoApplication, GoFile, GoType, GoCallable, … (schema.go)
│   ├── options/              # AnalysisOptions + AnalysisLevel constants
│   ├── syntactic_analysis/   # SymbolTableBuilder (packages.Load → AST walk)
│   ├── semantic_analysis/    # CallGraphBuilder (go/types resolver)
│   ├── dataflow/             # L3: body nodes, CFG, CDG, def-use, DDG; Run fans out per callable
│   ├── sdg/                  # L4: SSA, VTA dispatch, footprints, vertices, port wiring, summaries, stand-ins, may-alias
│   ├── analysis/             # Pluggable pass interface + registry (topo-ordered pipeline)
│   ├── frameworks/           # BaseEntrypointFinder — extension seam for framework passes
│   └── utils/                # DiscoverGoFiles, IsVendored, IsTestFile, logging
├── packaging/
│   ├── python/               # PyPI wheel wrapper (bundles the prebuilt binary per platform)
│   ├── homebrew/             # generate_formula.sh — Homebrew tap formula generator
│   └── install/              # cango-installer.sh — curl | sh installer
├── testdata/
│   ├── greeter/              # Minimal two-package fixture (basic struct/interface/call sites)
│   ├── multipackage/         # Richer fixture covering embedded fields, variadic params, goroutines, …
│   ├── generics/             # Go 1.18+ generics fixture (Set[T], union-constraint interfaces, Map[T,U])
│   └── chi/                  # External-dep fixture (chi v5, vendored) for HTTP handler patterns
```

The `core` package is a pure orchestrator: it calls `syntactic_analysis` → `semantic_analysis` → `analysis.RunPipeline` in sequence, with no inlined parsing logic. Framework-specific analysis extends through the `analysis/` + `frameworks/` layer without touching `core`.

## Development

### Running tests

```bash
go test ./...
```

Tests run against four fixtures: `testdata/greeter/` (basic), `testdata/multipackage/` (multi-file packages, goroutines, variadic params), `testdata/generics/` (Go 1.18+ generics — `Set[T]`, union constraints, multi-type-param functions), and `testdata/chi/` (external dependency via vendored chi v5, HTTP handler patterns). All 105 tests cover symbol table correctness, generic receiver attribution, call graph edges, JSON round-trip, output format validation, caching behaviour, and error paths.

`go test` caches passing results by source hash. To force a full re-run:

```bash
go clean -testcache && go test ./...
```

The analyzer's own `CacheDir` (used inside tests for `analysis_cache.json` and `go_mod_hash`) is written to OS temp directories that are wiped automatically when the test binary exits — there is no persistent on-disk state between test runs. The chi fixture is fully vendored, so tests never require network access.

### Clearing the production cache

By default the CLI writes its cache to `~/.cldk/go-cache`. To bypass it for a single run:

```bash
cango -i ./my-project --eager
```

To delete it entirely:

```bash
rm -rf ~/.cldk/go-cache
```

If you pass a custom `--cache-dir`, remove that directory instead.

### Releasing

Releases are cut by pushing a `vX.Y.Z` tag. The [release workflow](.github/workflows/release.yml)
cross-compiles the `cango` binary for all supported platforms and publishes:

1. the raw binaries + `cango-installer.sh` as **GitHub Release** assets,
2. platform-tagged wheels to **PyPI** as `codeanalyzer-go` (via Trusted Publishing/OIDC), and
3. a **Homebrew** formula pushed to `codellm-devkit/homebrew-tap`.

The version is injected into the binary at build time via `-ldflags "-X main.version=<tag>"`, so
`cango --version`, the wheel version, and the git tag always stay in lockstep. See
[packaging/](packaging/) for the build scripts.

## License

Apache 2.0 — see [LICENSE](./LICENSE).

Application identity uses `--namespace` (default: resolved application name) and
`--app-name` (default: input directory basename). Both segments are always present,
for example `can://team/inventory` or `can://inventory/inventory`. Names are raw,
case-sensitive Unicode strings; reserved UTF-8 bytes are percent encoded. Empty,
whitespace-only, control-containing names, slashes, backslashes, `.` and `..` are
rejected. Explicit empty flags are errors. Outputs and graph anchors carry integer
`identity_version: 2`; regenerate legacy outputs from source. The graph writer
rejects incompatible selected anchors before any DDL or graph write.

Graph relationships retain existing `_k` values and additionally use private
`_fact` identity (declared as `relationship_fact_property` by `--emit schema`).
Both snapshot and Bolt writers derive it from SHA-256 of the canonical JSON of
the existing pruned relationship properties. Equal endpoint/type/key facts with
different site, context, or evidence properties therefore remain distinct.
