Skip to content

Remove CodeQL stub and v1 schema emission (v2-only) - #13

Merged
pavuluri merged 29 commits into
codellm-devkit:mainfrom
lamwassi:remove-codeql-v1-schema
Oct 9, 2026
Merged

pavuluri merged 29 commits into
codellm-devkit:mainfrom
lamwassi:remove-codeql-v1-schema

Conversation

@lamwassi

@lamwassi lamwassi commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Remove two pieces of legacy/dead machinery now that the analyzer is v2-first, leaving cango v2-only: the always-stubbed CodeQL path and the v1 analysis.json serializer behind --analysis-schema.

Top of a stack — review only the last commit. This branch sits on PR #10 (v2 migration) → PR #12 (Neo4j), so the diff against main restates their 12 commits. The only new commit here is ed4b93b. Merge order is #10 → #12 → this; once both land, this collapses to that single commit. (A base of Neo4j-projection isn't possible — it's a fork branch, not an upstream one.)

Motivation and Context

Two leftovers outlived their purpose once v2 became the default (--analysis-schema 2, #10) and --emit neo4j forced v2 (#12):

  • CodeQL — the --codeql path was a wired stub. Every operation returned ErrCodeQLNotImplemented; there were no .ql files, no binary dependency, and no CI wiring. It was surface area with no behavior behind it.
  • v1 schema — the v1 GoApplication output shape was a compatibility shim selectable via --analysis-schema. With v2 the default and the only consumer target, the shim is dead weight on the serializer.

Part of epic codellm-devkit/.github#94 (Go → canonical schema v2). This is the cleanup unit the v2 train deferred: #10's PR body noted "the default flip and v1-emitter removal are later units, filed when due" — this is that unit.

What changed

  • Deleted the internal/semantic_analysis/codeql package, the Phase-5 enrichment block in the analyzer, the --codeql flag, and the UseCodeQL option.
  • Collapsed RenderJSON to always emit the canonical v2 tree via v2emit; dropped the --analysis-schema flag and the SchemaVersion option (the v2/neo4j SchemaVersion constant — "2.0.0" — is unrelated and unchanged).
  • Updated tests to assert the v2 shape.

Not touched: the internal GoApplication IR and analysis_cache.json, the shared parser/resolver/pass pipeline, v2emit, schema/v2, and the neo4j projection. Only the output serializer and its two removed flags are affected (golden rule: replace what serializes, keep what computes).

How Has This Been Tested?

# Full suite, forced/uncached
$ go test -count=1 ./...        # all green

# 15-app regression eval (L1+L2) against validate_v2.py
15/15 pass; check-counts identical to the prior baseline

Confirmed the removed surface is gone: internal/semantic_analysis/codeql/ deleted; no --codeql / --analysis-schema / UseCodeQL references remain in non-test source.

Breaking Changes

  • --codeql and --analysis-schema are removed flags. --codeql never produced output (always errored), so no behavior is lost. --analysis-schema is gone because v2 is now the sole output; v1 analysis.json is no longer emitted.
  • No downstream consumer is pinned to v1 (no SDK Go model consumes it yet), so this breaks nothing in the ecosystem today.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the Codellm-Devkit Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

Leaves the analyzer v2-only end to end: one output serializer, no compat branch, no stubbed enrichment phase. L3/L4 remain deferred to a later train, reusing the Group A ids/spans unchanged.

🤖 Generated with Claude Code

lamwassi and others added 29 commits September 28, 2026 22:13
Roadmap, cross-repo spec, schema decisions, and drafted parent/child issue
bodies for migrating codeanalyzer-go and its python-sdk Go models to the v2
canonical schema. Reach: L1 + L2 to parity; L3/L4 deferred to a later train.

Group A vocabulary (can:// ids, span.bytes UTF-8) decided once here per the
parity clause, since L3/L4 reuse it verbatim. Full transcript in
docs/design/specs/v2-l1-emission.md and CLAUDE.md.
Add the flags the CLDK CLI contract requires so the SDK facade can shell
out to the Go backend with the same code path as the other language
backends:

- --emit <json|neo4j|schema>: output projection selector. neo4j and
  schema return a clear non-zero "not yet implemented" error (never a
  silent fallback to JSON) until the Neo4j child lands. schema needs no
  --input, matching the contract.
- --app-name: application anchor for can://go/<app> ids and the Neo4j
  :Application node; defaults to the input dir base name.
- --neo4j-uri/user/password/database: accepted and validated now with
  explicit-flag > env-var > default precedence; consumed by the Neo4j
  child.
- -j/--jobs: worker parallelism, defaulting to CPU cores.

No v2 emission logic yet; default output stays the v1 JSON shape.
Declare the v2 canonical-schema Go types in internal/schema/v2, a
faithful transcript of the keystone (canonical-schema.md) and the
Go-specific leaf decisions in CLAUDE.md.

Models the L1 additive containment tree
(application -> module -> type -> callable -> body) with typed edge
overlays, plus the L2 refinement/edge slots so the shape is stable:

- Span{start,end,bytes} with UTF-8 byte offsets into module.source.
- Module carries source once; no per-node code field.
- Type.kind (struct|interface|alias|defined) replaces v1 is_interface;
  base_types (embedding) vs interfaces (computed satisfaction).
- Callable.error_channel, nested callables{} for closures, metrics map.
- BodyNode 'call' with the sanctioned callee null->id refinement slot
  (pointer, the one allowed null) and is_goroutine/is_deferred.
- Edge{src,dst,prov,weight} for call_graph.

No emitter or wiring yet; these types are not referenced by any code
path. L3/L4 fields (cfg/cdg/ddg/param_*) are deliberately not declared.
Add the Group A identity/positioning helpers in internal/schema/v2,
pure functions so they are testable in isolation and reused verbatim by
the deferred L3/L4 train:

- can:// durable-id builders (AppID/ModuleID/TypeID/CallableID) that
  assemble the containment path can://<lang>/<app>/<file>/<type>/<sig>,
  with signatureOf() output as the last segment.
- @line:col ordinal-id builders (OrdinalID/TagID/LocalID) for body nodes
  below the callable line.
- NewSpan + Span.Slice: build a span from line/col and UTF-8 byte
  offsets, and slice module.source in O(1) (the replacement for the v1
  per-node code field), degrading to "" on a malformed span.

Unit tests reproduce the keystone worked-example id chain and cover path
normalization, module-level function parents, ordinal/tag ids, and span
slicing edge cases.

Not wired into any emitter yet.
Add the v2emit package with lineIndex: reads a file's source once and
maps 1-based (line, col) positions to UTF-8 byte offsets, the one new
datum the v2 span needs. go/token columns are byte columns, so no
rune/byte reconciliation is required.

This keeps the v1 model diff-free (per the migration spec): the emitter
derives span.bytes from the existing line/col positions instead of
threading offsets through internal/schema. lineEndOffset/lineLen give a
line-only v1 position (callables/types carry no columns) a precise end.

Positions outside the file clamp to [0, len(source)] rather than
panicking. Tests cover line-start tabulation, offset lookup, clamping,
the last-line-without-newline case, and empty source.

Emitter tree walk lands next; nothing wired yet.
Emit() transforms the v1 GoApplication into the v2 Analysis payload: a
pure re-serialization walking application -> module -> type -> callable
-> body, computing no new facts beyond span byte offsets.

- Module carries source once (read from projectDir/relPath); every node
  span slices off it via the lineIndex-derived byte offsets.
- can:// ids assembled from the id builder, signatureOf() as last
  segment; methods resolve under their receiver type's callables{}.
- type.kind collapses is_interface (struct|interface); base_types kept;
  error-typed returns lift into error_channel; cyclomatic into metrics;
  closures nest in callables{}.
- body holds one 'call' node per call site keyed by line:col, with the
  callee null->id slot carried straight from the v1 pointer.
- call_graph renamed to {src,dst,prov,weight}.

Also drop omitempty from BodyNode.Callee so the sanctioned refinement
slot always serializes (null at L1, id at L2) as the keystone requires.

Tests build the greeter fixture through the real symbol-table builder
and assert the envelope, module source/ids, span slicing, call-node
bodies, callee-null serialization, dropped is_constructor_call, and
type/method kinds. Not wired to the CLI yet.
Wire the v2 emitter behind an opt-in flag. Default stays the v1 shape so
the current SDK keeps parsing output during the migration; --analysis-
schema 2 emits the canonical v2 tree.

- Add options.SchemaVersion (1 default, 2 v2) and AnalyzerVersion.
- core.RenderJSON centralizes the v1/v2 marshal decision so the stdout
  and file paths agree; WriteOutput now takes opts and routes through it.
  An unknown schema returns a non-zero error, never a silent fallback.
- --analysis-schema flag; --app-name/version threaded into the v2
  manifest (v2 surfaces the analyzer version, which v1 never carried).

Update WriteOutput callers (now opts-based) in core tests. CLI tests
assert the default is v1 (top-level symbol_table, no schema_version),
that --analysis-schema 2 emits schema_version 2.0.0 with
application.id can://go/greeter, and that an unknown schema errors.
Lock the v2 L1 output shape against a known multi-file fixture so the
level cannot silently regress. Asserts concrete values for every item on
the L1 coverage checklist (testing-and-validation.md):

  - multi-file compilation unit: four modules keyed by relative path,
    each with source-once and a whole-file span that slices back
  - exported + unexported symbols (Worker.Run vs Worker.execute)
  - interface kind collapse (Processor -> kind=interface)
  - error_channel from an error-typed return (Processor.Process)
  - variadic parameter (Combine(results ...Result))
  - language-specific call flag (is_goroutine on `go w.execute(...)`)
  - sanctioned callee:null refinement slot present at L1
  - can:// durable ids at module/type/callable depth

Plus the superset gate: walk the same v1 model the emitter walked and
assert every file/type/method/function reappears in v2, and that the
sanctioned drops (is_constructor_call, per-node code) are absent from the
serialized payload.
L2 is a pure refinement over the L1 tree: it fills the sanctioned
callee null->id slot and translates call_graph edge endpoints onto
real callable ids. Both need one thing v1 does not have: a mapping from
v1 signature identity to v2 can:// node id.

Add buildSigIndex, a one-pass signature->id index built with the same id
builders emitCallable uses, so an indexed id is byte-identical to the id
the callable node carries (no dangling endpoints by construction). Then:

  - emitBody: backfill body.callee to the callee's can:// id when the
    resolver-set signature names an in-tree callable; keep it null for
    external/stdlib callees (the honest-unresolved fallback).
  - emitCallGraph: map src/dst from v1 signatures to can:// ids; drop any
    edge whose endpoint does not resolve rather than emit a dangling one
    (defensive — the v1 resolver only emits in-project targets).

At L1 (no resolver run) callee stays null, so L1 output is unchanged.
Lock the v2 L2 output shape by building the fixture through the real
resolver (the -a 2 wiring) and asserting the L2 checklist
(testing-and-validation.md):

  - no dangling endpoints: every edge src/dst resolves to a real
    callable id in the tree
  - every edge carries a non-empty prov naming the resolver
  - a NAMED expected edge (Worker.Run -> Worker.execute) by exact
    can:// id pair, so the gate proves correctness not just non-emptiness
  - a NAMED cross-package edge (main -> server.New)
  - callee backfilled to an in-tree can:// id on the goroutine call site
  - the L1 ⊆ L2 superset: emitting the same model at level 1 vs 2 leaves
    every module/type/callable id, span, kind and body node identical,
    changing only callee null -> id (the one sanctioned mutation).
The real-app eval (dae/openbao/cockroach) surfaced a datamodel collision:
v2 nests a method under its receiver type, but Go allows a method to be
declared in a different file than its type, so span.bytes computed against
the nesting module's source collapse to empty (~3292 cases; cobra clean).

Design-loop decision (Go-only for now; cross-language parity deferred):
add an optional callable.source_file field, present only when the declaring
file differs from the nesting module. span.bytes then index that file's
module.source; text = symbol_table[source_file].source[span.bytes] when
present, else the nesting module. Containment stays receiver-based.

Records the decision in the committed spec and CLAUDE.md; python-sdk
source_file support is a tracked follow-up on the SDK rung.
A Go method may be declared in a different file than its receiver type.
The v2 tree nests it under the receiver type's module, but its line/col
positions are relative to its OWN file, so computing span.bytes against the
nesting module's source collapsed them to an empty [n,n] slice (~3292 cases
across dae/openbao/cockroach; cobra clean).

Compute a cross-file method's span against its declaring file (c.Path) via a
per-file lineIndex cache, and record that file in the new optional
callable.source_file field. Text recovery: symbol_table[source_file].source
[span.bytes] when set, else the nesting module. Closures inherit their
enclosing callable's file. Containment stays receiver-based.

Adds TestGateL1_CrossFileMethodSourceFile over the multipackage fixture
(Server declared in server.go, its Describe method in middleware.go):
asserts source_file names the declaring file, the span is non-empty, and it
slices to the method text; plus the negative case (in-file method carries no
source_file). Design: docs/design/specs/v2-l1-emission.md, CLAUDE.md.
cgo synthesizes wrapper functions (_Cfunc_*, _Cgo_*, _cgo_runtime_*) into a
generated file under $GOCACHE; go/packages resolves their token.Pos to that
cache path. Emitting them produced callables whose source_file pointed at the
build cache and whose span could not be sliced against any project module
(ISSUE-2 in the real-app eval: 85 such callables on cockroach, 106 validation
failures at L1 and L2; cobra/dae/openbao have no cgo and were unaffected).

These are toolchain artifacts, not project source. buildCallable now returns
nil when the declaration's file is not within projectDir, at the point its
(cache) file is first known; both callers already skip nil. Adds utils.IsWithin
and a unit test.

Verified: cockroach L1 106->0 failures, 85 dropped signatures all cgo glue,
real pkg/geo/geos project code preserved.
A quick key-skeleton map of the canonical v2 output: the node tree, the recursive
callable node, span shape, presence rules (always-present vs omitempty vs
emitted-when-true), and can:// id shapes. Authored from the JSON tags in
internal/schema/v2/schema.go; links to v2-l1-emission.md for the decisions.
- Add a step-by-step 'Generating analysis.json for a Go app' section
  (--analysis-schema 2 -a 2 -o <dir>, --app-name, verify, cgo note).
- Document the canonical v2 output shape alongside the legacy v1 shape,
  with a full JSON example verified against real emitter output.
- Fix the stale CLI options block (add --analysis-schema, --app-name).
The v2 call_graph edge list was emitted in Go-map iteration order, so two
runs of the same input (even at the same -j) produced byte-differing
analysis.json, failing the release determinism gate. Sort the emitted edges
by (src, dst, prov, weight) in emitCallGraph, matching the sorted-key
discipline already used for module output. Add a determinism gate test that
emits a reversed edge slice and asserts identical serialized call_graph.
Epic codellm-devkit/.github#94 and its two codeanalyzer-go children
(codellm-devkit#8 L1 tree emission, codellm-devkit#9 L2 call_graph refinement), plus the PR body.
Corrects child-1's app-id anchor to --app-name (matches the code and
CLAUDE.md) and aligns issue headings to the live feature_request form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The committed design record for the Go Neo4j projection (--emit neo4j),
collision group D of the v1->v2 migration. A pure second projection of the
frozen v2 envelope into a GO_/Go-namespaced property graph, keyed on the same
can:// ids so JSON and graph never diverge. Cross-repo (analyzer + python-sdk
GoAnalysisBackend), tracked under epic codellm-devkit/.github#94.

- specs/neo4j-projection.md: triage, the two design-loop decision tables
  (analyzer graph vocabulary + SDK type-centric facade), package layout,
  release plan, definition of done.
- roadmap.md: milestones M1-M7 with commit boundaries, dependency DAG, and the
  test gate each must pass.
- issues/: ready-to-file feature_request drafts (parent under #94, M1 analyzer
  child, M7 SDK child) for maintainer filing where access is pull-only.
- CLAUDE.md: Neo4j-projection decisions section (auditable decision record).
- Carries the TS/Python design references and the prompt as design inputs.
Milestone M1 of the Neo4j projection (Closes codellm-devkit#11). The declarative single
source of truth for the graph vocabulary, before any projector or writer.

internal/neo4j/schema.go declares the GO_/Go-namespaced contract: node labels
(the Go set is narrower than Python's — no decorator/attribute/variable), the
shared :GoSymbol merge label, the :GoCanNode marker, relationship types incl.
GO_EMBEDS vs GO_SATISFIES (Go's two distinct inheritance mechanisms) and the
reserved L3/L4 overlay (declared now, zero rows until the levels land), and the
_k discriminants.

internal/neo4j/ddl.go derives the DDL from the node specs — one uniqueness
constraint per distinct (mergeLabel, key), so the symbol family collapses to a
single GoSymbol.id constraint — plus the curated :GoCanNode id range index that
makes scoped statements prefix-seekable. BuildSchemaDocument assembles the
--emit schema document.

internal/neo4j/emit.go: EmitSchema writes schema.neo4j.json (static, needs no
project). Wired into the CLI --emit schema branch, which now returns the
written path instead of "not yet implemented"; --emit neo4j stays stubbed until
its writers (M4-M6) land.

Gate (green): --emit schema writes a parseable schema.neo4j.json listing every
family; derived DDL has exactly one constraint per distinct (mergeLabel, key)
with the symbol family collapsed; every relationship endpoint names a declared
label; prefixes match the SDK's N/P. The conformance test
(projector-emits-only-declared) is a scaffold M3 fills.
Milestone M2 of the Neo4j projection. internal/neo4j/rows.go: the deterministic,
deduped bag both writers consume identically — GraphRows / NodeRow / EdgeRow /
NodeRef — plus the RowBuilder with in-memory MERGE semantics:

- Node() dedups on (labels[0], value): re-seeing an identity merges props
  (last-write-wins) and unions labels, so a hot external symbol collapses to one
  row. Returns a NodeRef edges address it by.
- Edge() is append-only; EdgeToSymbol() DEFERS an edge whose target may be
  external, kept at Finish() only if the target node was emitted (the
  edge-only-when-resolved rule — GO_EMBEDS/GO_SATISFIES/GO_RESOLVES_TO never
  dangle).
- prune() drops nil/empty-slice props (in Neo4j absence IS null).
- Finish() resolves deferred edges and sorts nodes/edges to a total order for
  byte-stable output.
- applicationPrefix() refuses a non-bare app id (so rows with no proper root get
  no destructive statement); descendantPrefix() appends "/" for strict
  containment. NodeRow.module is in-memory only — never emitted as a graph prop.
- cypherValue/cypherString/cypherMap render Neo4j-legal literals with sorted keys.

Gate (green): dedup+label-union, last-write-wins, deferred-edge drop/keep,
prune, order-stable Finish, applicationPrefix refusal across bare/empty/too-deep
ids, Cypher escaping + deterministic map rendering.
Milestone M3 of the Neo4j projection — the intellectual core. project.go walks
the canonical v2 Analysis tree in spine order (application -> module -> type ->
callable -> body call nodes -> call_graph -> imports) and emits a GraphRows
bag, reading can:// ids off the stamped tree so graph and JSON join on one
string (two-projection agreement). No I/O, no driver.

- Types: one :GoType node + kind property; base_types -> GO_EMBEDS and
  interfaces -> GO_SATISFIES via the deferred edge path (never dangle); struct
  fields -> :GoField/GO_HAS_FIELD; methods nest under the receiver type
  (GO_HAS_METHOD).
- Callables: error_channel/source_file/metrics flattened; closures ->
  GO_DECLARES; body call nodes keyed by the <callable-id>@<local> global
  ordinal, is_goroutine/is_deferred pruned when false; a resolved callee ->
  GO_RESOLVES_TO only when it names a declared callable.
- call_graph -> GO_CALLS (weight, prov); endpoints resolve to a declared
  callable or an id-keyed :GoExternal ghost, so no endpoint dangles.
- imports -> GO_IMPORTS aggregated per target (:GoModule in-project, else a
  :GoExternal ghost keyed by import path).
- span/metrics flatten to *_json string props (Neo4j can't store nested maps).

Fills M1's conformance scaffold: TestConformance_ProjectorEmitsOnlyDeclared
projects the multipackage fixture at L2 (real builder + resolver, 63 nodes/83
edges) and asserts the emitter produces no undeclared label/relationship/
property. Plus no-dangling, JSON<->graph id parity, determinism, and
Go-specific-shape gates. All green.
Milestone M4 of the Neo4j projection. cypher.go renders a GraphRows bag into a
self-contained, idempotent graph.cypher script that rebuilds this application's
subgraph from scratch. Block order: constraints + indexes (IF NOT EXISTS) ->
scoped wipe -> batched UNWIND...MERGE nodes -> batched UNWIND...MERGE edges
(500/batch).

The scoped wipe is anchored on :GoCanNode and the can://<app>/ id prefix, with
the root id matched separately (the root is outside its own descendant prefix).
It uses applicationPrefix, so a bag with no bare application root emits NO
destructive statement (visible as a SKIPPED comment) — one application by
construction, never the store, never a sibling analyzer's graph. Symbol-family
nodes MERGE on the shared :GoSymbol label and SET the specific kind as an extra
label; keyed edges MERGE on {_k: ...}.

Gate (green, DB-free): the script carries DDL + wipe + node/edge batches; is
byte-identical across runs; every DETACH DELETE is scoped on :GoCanNode + the
prefix; a no-root bag emits no wipe.
Milestone M5 of the Neo4j projection. bolt.go pushes a GraphRows bag into a
live Neo4j over Bolt, updating only what changed with the module subgraph as
the unit of idempotent replacement. Adds github.com/neo4j/neo4j-go-driver/v5
(pure Go — CGO_ENABLED=0 static builds still link; only exercised when
EmitNeo4j runs with a --neo4j-uri, off the JSON path).

- ensure constraints + indexes; schema-version gate (a stored-vs-producer
  mismatch forces a full re-upsert, never a delete);
- partition nodes by owning module (NodeRow.module); shared nodes (externals,
  neutral repo nodes) are MERGE-only, always upserted;
- diff each module's content_hash against the DB -> the changed set;
- per changed module, upsert; on --eager, purge its owned subtree first;
- upsert edges; on a full + eager run, orphan-prune vanished modules.

A push NEVER deletes by default (#171): the purge/prune steps run under --eager
only; lazy is purely additive. Every destructive statement is scoped on the
can://<app>/ id prefix anchored on :GoCanNode (#173), extracted into pure
query-builders so the scope invariant is unit-testable.

Gate: DB-free unit tests (partitioning, module-hash, param marshalling, and the
destructive-queries-are-scoped-and-anchored invariant) run on every `go test`;
a live round-trip (push, idempotent re-push, lazy-never-deletes) is in
bolt_integration_test.go behind the `neo4j_it` build tag. Static CGO-free build
verified.
Milestone M6 of the Neo4j projection, completing the analyzer arc. EmitNeo4j
projects the v2 Analysis (the SAME tree the JSON path emits) and writes it:
--neo4j-uri set → live Bolt push (incremental, additive unless --eager);
absent → a self-contained graph.cypher snapshot in the output dir.

CLI: --emit neo4j is unstubbed and shares the analysis path, but is ALWAYS
full-depth — it forces level 2 (max implemented) + canonical v2, and an
explicit -a/--analysis-level alongside it is a flag error ("--analysis-level
does not apply to --emit neo4j; the graph is always projected at full depth").
The Neo4j connection config threads through from the existing env-precedence
flags; --eager and a full (non-target-files) run gate the destructive push
steps.

Gate (green): --emit neo4j with no URI writes graph.cypher end-to-end (verified
byte-identical across runs — idempotent); the flag-error gate rejects -a with a
clear non-zero exit; --emit schema still writes the static contract. Static
CGO-free binary builds and runs. Full suite green.
cango now emits the canonical v2 tree by default; --analysis-schema 1 is the
opt-in escape hatch for the legacy v1 shape. Done before the first release, so
there are no v1-default consumers to break — the cleanest moment to flip it.

- main.go: flag default 1 -> 2, help text reordered to name v2 as default.
- main_test.go: DefaultSchemaIsV1 -> DefaultSchemaIsV2 (default output now
  carries schema_version + application); add AnalysisSchema1EmitsLegacyV1 to
  pin the escape hatch; the Level1/Level2 call-graph tests now read
  application.call_graph (v2) instead of the v1 top-level key.
- README: the four "v1 is the default" references corrected to v2.

The python-sdk GoCodeanalyzer already passes --analysis-schema 2 explicitly
and unconditionally, so this does not change SDK behaviour — it fixes the shape
a hand-run `cango` produces.
Fix gosimple S1016 in internal/neo4j/ddl.go: convert NodeSpec/RelSpec
to NodeDoc/RelDoc via type conversion instead of struct literals.

Fix errcheck on best-effort deferred cleanup:
- internal/neo4j/bolt.go: driver.Close / session.Close
- internal/utils/fs.go: f.Close
- internal/core/testsetup_test.go: os.RemoveAll
Go permits many package-level `func init()` per package (one per file),
all sharing the signature "<pkgpath>.init". These are distinct callables
with distinct can:// ids (the id embeds the declaring file via its
module), but the emitter's flat signature->id index collapsed them with
last-writer-wins while ranging a Go map. The surviving id — and therefore
which init an edge's source resolved to — varied run to run, so the
call_graph edge set was nondeterministic and failed the release
determinism gate (byte-identical across runs and -j values). The downstream
edge sort was correct but ordered an unstable set.

Resolve an edge's source by (signature, declaring file) instead of bare
signature: GoCallEdge now carries SourcePath (the source callable's file,
already known where the edge is built), and emitCallGraph routes src
through a (signature, path)-keyed index so each init edge points to the
init that actually owns its call site. The flat index is kept for the
unique common case (and for targets, which are never init) and is now
deterministic on collision (smallest id wins) as a backstop. No change to
the shared signatureOf canonicalizer; the Neo4j projection reuses the
emitter's ids and inherits the fix with no change of its own.

Add a fresh-build determinism gate over the chi fixture (the only one with
multiple init per package): it rebuilds the symbol table + call graph from
disk 16 times and asserts a byte-identical call_graph, catching the class
the existing single-build determinism test could not. A focused test
asserts the three middleware init nodes are distinct and each init-source
edge resolves to its own file.
Remove two pieces of dead/legacy machinery, leaving the analyzer v2-only.

CodeQL: the --codeql path was a wired stub whose operations always
returned ErrCodeQLNotImplemented (no .ql files, no binary dependency, no
CI wiring). Delete the internal/semantic_analysis/codeql package, the
Phase-5 enrichment block, the --codeql flag, and the UseCodeQL option.

v1 schema: since the default flipped to v2 (and Neo4j forces v2), the v1
GoApplication shape was a compat shim behind --analysis-schema. Collapse
RenderJSON to always emit the canonical v2 tree via v2emit, and drop the
--analysis-schema flag and SchemaVersion option entirely. The internal
GoApplication IR and analysis_cache.json are unchanged; only the output
serializer is affected.

Tests updated to assert the v2 shape; the shared parser/resolver/pass
pipeline, v2emit, schema/v2, and neo4j projection are untouched. Verified
with a 15-app regression eval (L1+L2) against validate_v2.py: 15/15 pass,
check-counts identical to the prior baseline.
@pavuluri
pavuluri merged commit 6d142b7 into codellm-devkit:main Oct 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants