Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
8428554
design: Go L3/L4 dataflow and SDK taint support
sinha108 Oct 23, 2026
e1cc39d
feat(ids)!: app-outermost can://<app>/go/... id grammar
sinha108 Oct 23, 2026
988581d
fix(l1): defer marker, alias/defined kinds, co-positioned call keys; …
sinha108 Oct 23, 2026
a7a103f
fix(l2): key call-site index on start+end; classify type kind by unde…
sinha108 Oct 23, 2026
91471c2
feat(l2): keep library call edges; external_symbols; conversions are …
sinha108 Oct 23, 2026
b95c66b
fix(l2): origin signature for generic externals; library = outside pr…
sinha108 Oct 23, 2026
16f74f7
fix(l2): vendored callees are libraries
sinha108 Oct 23, 2026
50a2663
feat(cli): -a 3 and --graph-field-depth
sinha108 Oct 23, 2026
fb7b204
feat(l3): dataflow IR and statement body nodes
sinha108 Oct 23, 2026
159ad37
feat(l3): CFG for structured Go statements
sinha108 Oct 23, 2026
1bb7464
feat(l3): defer chain, panic edges and CFG gates
sinha108 Oct 23, 2026
794714f
fix(l3): more panic-capable constructs; defer chain in differential
sinha108 Oct 23, 2026
a544192
feat(l3): post-dominators and control dependence
sinha108 Oct 24, 2026
50cd3a2
fix(l3): do not anchor @exit in the CDG (Python parity)
sinha108 Oct 24, 2026
a627174
feat(l3): k-limited access paths and def/use sets
sinha108 Oct 24, 2026
3c5b673
fix(l3): weak loop-header defs, reference-typed receivers, sorted def…
sinha108 Oct 24, 2026
fdfa8ff
feat(l3): reaching definitions and syntactic DDG
sinha108 Oct 24, 2026
108ca36
refactor(l3): pair L1 call keys with calls by recording index
sinha108 Oct 24, 2026
7ca38c8
feat(l3): per-callable dataflow driver and v2 emission
sinha108 Oct 24, 2026
0e0f387
fix(l3): contain per-callable panics; own prov slice per DDG edge
sinha108 Oct 24, 2026
f120bf3
feat(neo4j): project the L3 cfg/cdg/ddg overlay
sinha108 Oct 24, 2026
27318c1
docs: L3 real-app evaluation
sinha108 Oct 24, 2026
fd24bdf
style: gofmt files changed on this branch
sinha108 Oct 24, 2026
679f995
fix(l2): keep every init's call edges; resolve in sorted order
sinha108 Oct 24, 2026
6f250ea
fix(neo4j): run the --eager purge in an implicit transaction
sinha108 Oct 24, 2026
883eab1
docs: record the init-determinism and Neo4j --eager fixes
sinha108 Oct 24, 2026
30b2a97
docs: L3 dataflow metrics over the real-app corpus
sinha108 Oct 24, 2026
258b22f
fix(l3): weak-define base[*] for subslice and array-slice arguments
sinha108 Oct 24, 2026
3360a8e
fix(l2): key the call-site index on directive-adjusted positions
sinha108 Oct 24, 2026
6fdc2c5
fix(l3): keep defs in packages with type errors
sinha108 Oct 24, 2026
9ae739e
feat: emit body-node id from L1; Neo4j call_node is the local key
sinha108 Oct 24, 2026
b87c636
fix(v2): set source_file on closures in a cross-file method
sinha108 Oct 24, 2026
123fe18
build: release with the latest Go; warn once on a too-old toolchain
sinha108 Oct 24, 2026
3f31626
fix(l3): deferred calls read receiver and argument state at exit
sinha108 Oct 24, 2026
f551fb3
docs: align README, Neo4j spec and CLAUDE.md with the L2/L3 contract
sinha108 Oct 24, 2026
5508010
docs: correct L2/L3 contract statements in README, Neo4j spec and L3 …
sinha108 Oct 24, 2026
9aeda43
scripts: eval_l3.sh --prepare with a pinned per-app manifest
sinha108 Oct 24, 2026
f06d837
docs: L3 metrics on HEAD with Kubernetes as the scale benchmark
sinha108 Oct 24, 2026
ad53d89
build: upgrade golang.org/x/tools to v0.52.0 for Go 1.27 export data
sinha108 Oct 24, 2026
0659dfe
fix(l1): guard nodeSource against positions with no token.File
sinha108 Oct 25, 2026
99417a0
fix(l1): record calls in go statement arguments
sinha108 Oct 25, 2026
c7eca54
fix(l3): append weak-defines its destination's elements
sinha108 Oct 25, 2026
648375a
test: lock R11 at the DDG, eager purge auto-commit, positional extern…
sinha108 Oct 25, 2026
6c29ffe
docs: lazy Bolt push leaves stale L3 nodes; add an Upgrading section
sinha108 Oct 25, 2026
5560aad
fix(neo4j): run the eager purge only on a full run
sinha108 Oct 25, 2026
3504445
docs: complete the Upgrading notes and correct the eager and conversi…
sinha108 Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ jobs:
- name: Test
run: go test ./... -count=1

# The release binary is built with the latest Go (release.yml), so test on
# it too; the job above tests the go.mod minimum.
test-stable:
name: Test (Go stable)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: stable
check-latest: true

- name: Build
run: go build ./...

- name: Test
run: go test ./... -count=1

lint:
name: Lint
runs-on: ubuntu-latest
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,13 @@ jobs:
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"
echo ">>> Releasing version $version (prerelease=$prerelease)"

# Build with the latest Go, not the go.mod minimum: the analyzer
# type-checks targets with the go/types compiled into it, so a binary
# built with an older Go degrades on modules with a newer go line.
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.25"
go-version: stable
check-latest: true

# ----- test gate: a broken build must not produce a release -----
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,7 @@ go.work.sum

# Generated analysis output
/output/

# Local planning artifacts (agent plans/specs scratch); not part of the repo
docs/superpowers/
.superpowers/
98 changes: 79 additions & 19 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,11 @@ v2 vocabulary (parity clause: add at the leaves, never rename shared names). The
committed spec is the full transcript: `docs/design/specs/v2-l1-emission.md`.

### Identity & positioning (Group A — reused verbatim by L3/L4)
- **`application.id`** = `can://go/<app>`, where `<app>` is the **`--app-name`** value,
defaulting to the input directory's base name (per the CLI contract; the SDK's Neo4j
backend must use the same anchor). L3/L4 build ids on this unchanged.
- **`application.id`** = `can://<app>` (app outermost, so a polyglot app shares one prefix),
where `<app>` is the **`--app-name`** value, defaulting to the input directory's base name
(per the CLI contract; the SDK's Neo4j backend must use the same anchor). Module ids are
`can://<app>/go/<file>`; externals are language-neutral `can://<app>/@external/<import-path>/<sig>`.
L3/L4 build ids on this unchanged.
- **Callable signature** = existing `signatureOf()` output as the last `can://` path
segment; receiver folded in for methods. One canonicalizer, unchanged from v1.
- **`span.bytes`** = **UTF-8 byte offsets** into `module.source` (Go strings are
Expand Down Expand Up @@ -43,7 +45,10 @@ committed spec is the full transcript: `docs/design/specs/v2-l1-emission.md`.
the declaring file ≠ the nesting module, the callable carries `source_file` = that
file's path (a `symbol_table` key). **Text recovery:** a node's text =
`symbol_table[source_file].source[span.bytes]` if `source_file` present, else the
nesting module's `source`. Emitted only when it differs (absent = same file). This is
nesting module's `source`. Emitted only when it differs (absent = same file). It applies
to **every callable** whose declaring file differs from its containing module, so
**closures and nested closures** inside a cross-file method carry `source_file` too (no
implicit inheritance; text recovery holds as written for them and their body nodes). This is
the leaf-level fix for the cross-file-method empty-span collision the real-app eval
surfaced. Cross-language parity (C# partial classes, Rust `impl`, Ruby reopened
classes, TS declaration merging) is **deferred** — promote `source_file` into the
Expand All @@ -54,10 +59,46 @@ committed spec is the full transcript: `docs/design/specs/v2-l1-emission.md`.
v1). `is_constructor_call` **dropped** — Go has no constructors.
- `callee` is the sanctioned `null → id` refinement slot (null at L1, backfilled at L2).

### `external_symbols` (L2 library calls)
- **L2 keeps library call edges.** A call to a function outside the project (`fmt.Println`,
`(*strings.Builder).WriteString`) is no longer dropped: the call node's `callee` and the
`call_graph` edge `dst` are the callee's `@external` id. Builtins, type conversions
(no longer minted as `<Type>.__new__`) and calls through function values stay `null`.
- **Id** = `can://<app>/@external/<import-path>/<sig>` where `<sig>` is the callee signature
minus the `<import-path>.` prefix (`Println`, `Builder.WriteString`); it may also name a
library *interface* method (`@external/io/Writer.Write`). Methods on generic library types
record the declared (origin) signature, not the instance. `@external` is
language-neutral (not `go/`), so a polyglot app shares one external namespace.
- **Home:** `application.external_symbols{<id>: {id, kind:"external", name, module,
signature, parameters, return_type}}`, omitempty; parameters come from the callee's
`*types.Signature` (receiver excluded, unnamed params `_<i>`).
- Mirrors the Python/Java precedent (library callees are leaf nodes at application scope);
on by default at L2, no flag.

### L3 body and edges (`-a 3`; spec `docs/design/specs/v2-l3-l4-dataflow.md`)
- **Body node `id`** = `<callable-id>@<key>` (`v2.GlobalOrdinal`) on **every** body node,
**from `-a 1` on** (not L3+ only) — Python parity (codeanalyzer-python stamps body ids at
L1). **`parent`** (L3 only) = the local key of the statement a satellite call sits in.
- **Body kinds: shared vocabulary only, no Go-specific kinds** — `if` → `branch`;
`for`/`range` → `loop`; `switch`/type switch/`select` → `switch`; `return`; every other
statement and each case/comm-clause head → `statement`; L1 `call`; synthetic `entry`/`exit`.
`go`/`defer`/`select` facts ride `is_goroutine`/`is_deferred` and the `switch` kind.
- **`@entry` / `@exit` are span-less** (keystone: synthetic CFG endpoints have no span) and
carry no `callee` (a call-node field).
- **Defer two-node rule:** `defer f(x)` is a registration `statement` node (at the `defer`
keyword; evaluates `f`, `x` and weak-defines `<defer>:<call-key>`) **plus** the L1 deferred
`call` node (`is_deferred`), placed only in the exit chain (reverse source order before
`@exit`; every defer assumed registered). `go f(x)` is NOT split — the statement is its call
node.
- **`cfg` kind `defer_resume`** — the one Go add to the shared `cfg` kinds: chain-internal
edges and the chain tail → `@exit`. The edge into the chain keeps its own kind.
- **`k_limit`** (envelope, L3+ only) = `--graph-field-depth` (default 3), the access-path
bound of the syntactic `ddg` (`x.f.g.h` → `x.f.g.*`); `ddg.prov` is `["ssa"]` (parity).

### Deferred (not this train)
- L3 (CFG/CDG/DDG) and L4 (SDG param edges) — reuse Group A ids/spans unchanged.
- Go-specific `cfg` edge kinds (e.g. `defer_resume`, `select`) are recorded when L3
is designed, not now.
- L4 (SDG param edges) — reuses Group A ids/spans and the L3 vocabulary unchanged. Designed in
`docs/design/specs/v2-l3-l4-dataflow.md`. A Go-specific `select` cfg kind was not added
(`select` is a `switch` with `switch_case` edges).

## Neo4j-projection decisions

Expand Down Expand Up @@ -107,6 +148,13 @@ shared neutral vocabulary verbatim; never rename a sibling's shared name). Spec:
- **`is_goroutine` / `is_deferred`** → boolean props on the call `:GoBodyNode` (pruned when
false, matching JSON `omitempty`), not labels, not edge discriminants (the fact lives on
the call site even when the callee is unresolved and no edge exists).
- **`:GoBodyNode.id`** = the JSON body node's `id` (`<callable-id>@<key>`), which the JSON
carries **from `-a 1` on** (not L3+ only) — Python parity: codeanalyzer-python stamps body
ids at L1 (`stamp_body_ids`), so JSON and graph join on the same id at every level.
- **`call_node`** on a satellite call `:GoBodyNode` = the enclosing statement's **local** key
(JSON `parent` verbatim), resolved against the owning callable (the `GO_HAS_BODY_NODE`
source) — not a global id. Python parity: codeanalyzer-python projects `node.parent`
unchanged and python-sdk's `reconstruct` maps `call_node` back to `parent`.
- **`source_file`** (cross-file method, § callable above) → scalar prop on `:GoCallable`;
text recovery joins to the declaring `:GoModule` by that path. Present only when it
differs from the nesting module.
Expand All @@ -123,16 +171,25 @@ shared neutral vocabulary verbatim; never rename a sibling's shared name). Spec:
own resolution claims).

### `_k` relationship discriminants (parallel edges survive MERGE)
- `GO_CFG_NEXT` per `kind`; `GO_DDG` per `(var, prov)`; `DECLARES_DEPENDENCY` per `kind`
— same discriminant mechanism as the siblings.

### Reserved L3/L4 overlay (declared now, emits nothing until the levels land)
- `schema.go` declares the full overlay vocabulary **now** — `GO_HAS_BODY_NODE`,
`GO_CFG_NEXT`, `GO_CDG`, `GO_DDG`, `GO_PARAM_IN`, `GO_PARAM_OUT`, `GO_SUMMARY`, and the
body-node statement kinds — with parity-correct names and `_k` discriminants. The
analyzer caps at L2, so the projector walks these sections and emits **zero rows**;
when L3/L4 land they fill with **no schema-contract change**. (`--emit neo4j` is
"full-depth = max implemented" — L2 today; the graph grows additively.)
- `GO_CFG_NEXT` per `kind`; `GO_DDG` per `(var, prov)` as the composite
`_k = var + "|" + join(prov, ",")` (declared as `Discriminant: "var|prov"`);
`DECLARES_DEPENDENCY` per `kind` — same discriminant mechanism as the siblings.

### L3 overlay (live at `-a 3`)
- `GO_CFG_NEXT {kind}`, `GO_CDG`, `GO_DDG {var, prov}` are no longer reserved; the projector
emits one row per JSON `cfg[]`/`cdg[]`/`ddg[]` entry (cross-projection count gate).
Endpoints are `:GoBodyNode` **global ids** (`v2.GlobalOrdinal(callable-id, local-key)`),
never recomposed by hand. `prov` is a string-array prop.
- `:GoBodyNode` covers every body node incl. synthetic `@entry`/`@exit` (id
`<callable-id>@entry`, no `span_json`), each with `GO_HAS_BODY_NODE` from its callable.
It carries `call_node` (JSON `parent` verbatim — the **local** key, see § Flattened
properties) and `var` (JSON `of`; L4 — absent at L3). `GO_HAS_BODY_NODE` may originate at `:GoExternal` (L4 stand-ins).

### Reserved L4 overlay (declared now, emits nothing until L4 lands)
- `schema.go` still declares `GO_PARAM_IN`, `GO_PARAM_OUT`, `GO_SUMMARY` (`Reserved`) with
parity-correct names; the projector emits **zero rows** for them. When L4 lands they fill
with **no schema-contract change**. (`--emit neo4j` is "full-depth = max implemented" —
L3 today; the graph grows additively.)

### SDK facade (`GoAnalysisBackend`, on the `python-sdk` fork — frontend rung)
- **Type-centric surface** (Go is the SDK's first class-less facade; no C precedent exists
Expand All @@ -143,9 +200,12 @@ shared neutral vocabulary verbatim; never rename a sibling's shared name). Spec:
take the callable, not class+method strings).
- **Scope this train:** Tier-A lifecycle + L1/L2 leaf accessors + repository-artifact
getters + `get_source`. Slicing / CFG / CDG / DDG / reachability are **omitted** (they
need L3/L4 the analyzer does not produce) per the ABC contract.
need L3/L4 the analyzer does not produce) per the ABC contract. (2026-10-24: L3 is now
emitted at `-a 3` — per-callable `cfg`/`cdg`/`ddg` and `GO_CFG_NEXT`/`GO_CDG`/`GO_DDG` —
so L3 accessors are supportable now; interprocedural slicing and taint still need L4:
`param_in`/`param_out`/`summary` and the points-to `ddg`.)

### Deferred (not this train)
- SDK consumer slicing/taint surface and L3/L4 facade methods — arrive with the L3/L4
analyzer train, reusing this graph vocabulary unchanged.
- Struct-tag modeling; Go-specific `cfg` edge kinds (`defer_resume`, `select`) — with L3.
- Struct-tag modeling. (`defer_resume` landed with L3; see § L3 body and edges.)
Loading
Loading