Skip to content

fix(test): update neo4j bolt test :Symbol queries to schema v2 :CanNode#51

Open
rahlk wants to merge 34 commits into
test/issue-45-inheritance-slice-coveragefrom
fix/issue-46-bolt-symbol-labels
Open

fix(test): update neo4j bolt test :Symbol queries to schema v2 :CanNode#51
rahlk wants to merge 34 commits into
test/issue-45-inheritance-slice-coveragefrom
fix/issue-46-bolt-symbol-labels

Conversation

@rahlk

@rahlk rahlk commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Closes #46. Epic #26 follow-up.

Stacked on test/issue-45-inheritance-slice-coverage (on top of the compliance cluster). Review this PR's diff in isolation; merge the follow-up stack bottom-up after the cluster. (Docker-gated; verified by schema-label inspection since no container runtime was available.)

Verified: 104 tests / 0 fail, typecheck clean, schema.neo4j.json lockstep; task-scoped review + final whole-branch review (READY TO MERGE).

rahlk and others added 21 commits July 14, 2026 14:11
feat(neo4j): TS twin node labels on every projected node (graph schema 1.1.0)
…icing (-a 3)

Implements the dataflow half of #2: whole-program dependence graphs built
in-process from the ts-morph AST, emitted as a schema-versioned
program_graphs section of analysis.json, gated by -a 3 / --graphs.

- src/dataflow/cfg.ts: exceptional statement-level CFG per callable
  (ENTRY/param/statement/EXIT nodes in source-span order; true/false,
  loop_back, switch_case, exception, await_resume, yield edge kinds;
  region-spliced try/catch/finally; synthetic loop-exit edge keeps EXIT
  the post-dominance root).
- src/dataflow/dominance.ts: CHK iterative post-dominators +
  Ferrante–Ottenstein–Warren control dependence (CDG).
- src/dataflow/defuse.ts: k-limited access paths (declaration-keyed bases:
  local/param/this/captured/module), copy-alias union-find MVP, forward
  reaching definitions, DDG extraction, capture-at-declaration for
  closures, EXIT-as-formal-out routing.
- src/dataflow/summaries.ts: Tarjan SCC condensation of the
  provenance-merged call graph; bottom-up relational summaries
  (param→return, transitive global reads/writes) co-defined to a monotone
  fixpoint inside SCCs; persisted with dependency edges to
  graphs_summaries.json for later incrementality.
- src/dataflow/sdg.ts: HRB stitching — CALL, PARAM_IN (args by position,
  globals as extra params), PARAM_OUT, and SUMMARY edges, all keyed by
  (signature, node_id) with no dangling endpoints.
- src/dataflow/slice.ts: two-phase context-sensitive backward slicing as
  an SDG query.
- CLI: -a 3, --graphs cfg,dfg,pdg,sdg, --graph-field-depth (strictly
  validated); -a 1/-a 2 are untouched (level 3 is fully flag-gated).
- test/fixtures/dataflow-app + test/dataflow.test.ts: every contract gate
  with exact hand-computed expected sets (CFG reachability, CDG sets,
  loop-carried/shadowing/aliasing DDG, intraprocedural and
  interprocedural slices, SUMMARY for the a→b→c chain, global flow,
  mutual-recursion fixpoint, byte-identical determinism).

Follow-ups staged in #2: taint models-as-data (PR E), Jelly points-to
aliasing (PR F), CPG Neo4j projection (PR G), incremental re-analysis
(PR H).
Implements the contract's parallel model on top of the sequential oracle:

- Stage split: fact extraction (AST-bound, once per callable) is hoisted
  out of the summary fixpoint; the reaching-defs solve (defuse.solveDefUse)
  is now pure data and re-runs without touching the AST. CallableGraphData
  is the serializable per-callable projection that crosses the worker
  boundary.
- Stages 1-4 (CFG, dominance, def-use facts, PDG) fan out per callable
  over a Bun worker pool, partitioned by file; each worker materializes
  its own whole-program ts-morph project (ASTs cannot be structured-cloned)
  and returns plain data.
- The call-graph solve overlaps extraction: at -a 3, core.ts posts the
  extraction to the pool BEFORE the provider (tsc resolver + Jelly
  subprocess) runs on the main thread, and joins before summaries.
- Stages 6-7 run as a Kahn-style ready-queue wavefront over the Tarjan SCC
  condensation DAG (per-SCC dependency counters; the SCC and its internal
  fixpoint are the atomic unit, one worker each).
- Determinism: --jobs N output is byte-identical to --jobs 1 (span-ordered
  ids, collect-then-sort emission, sccFixpoint pure), enforced by a
  differential test. --jobs 1 is the sequential debug mode.
- Failure discipline: a dying worker is retired and its queue never
  strands (a stranded queue previously let the process exit 0 without
  emitting output); extraction failure closes the pool so the wavefront
  degrades sequentially too — a warning, never wrong or missing output.
- Compiled binary: the worker is a second bun build --compile entrypoint,
  embedded as /$bunfs/root/dataflow/worker.js; pool resolves the URL per
  runtime. Verified: dist/cants -a 3 -j 2 runs workers with byte-identical
  output.
- Default is sequential: measurement (self-analysis: 36 files, 211
  callables) shows per-worker project load dominates the parallelizable
  graph math at small/mid scale (2.5x slower at -j 14), so -j N is an
  explicit opt-in for large codebases.

Also fixes a latent bug: TSCallable.path is absolute, so the summary
cache's symbol_table[c.path] content-hash lookup always missed; now keyed
by the project-relative file key.
…ine)

Checkpoint of the in-progress schema-v2 migration (emitter in src/schema/v2,
neo4j v2 projection, L3/L4 program-graph wiring) so the #26 contract-correctness
compliance fixes (#27-#33) have a committed base to branch from.
Runs analyze() fresh at -a 1/2/3/4 on dataflow-app and asserts each level's
symbol-table id / body-node key / edge key sets are a strict superset of the
level below, plus a full-depth Neo4j projection vs analysis.json count-parity
check. Supersedes the narrower L3->L4-only spot-check with an end-to-end gate
across all four levels.
GraphNode carried only line/column, so spanOf() in schema/v2/dataflow.ts
hardcoded bytes:[0,0] for every L3 entry/exit/param/statement node —
module.source.slice(...) on any L3 body node returned "". Add
start_offset/end_offset (UTF-16 char offsets, matching the L1 convention)
to GraphNode, populate them at the single construction site
(dataflow/extract.ts's emitNode, from the owning ts-morph node's
getStart()/getEnd() — the whole callable for entry/exit, the param decl
for param nodes), and have spanOf() use them.
Consumers (the CLDK SDK, tooling correlating multiple analysis.json
artifacts) need to know which analyzer produced a given output and at
what version, independent of schema_version. Add a V2Analyzer{name,
version} type and a required analyzer field on V2Application, populated
from the literal package name and the existing ANALYZER_VERSION constant
(src/utils/version.ts) — the same version stamped into the cache, not a
new hardcoded literal.
…json

ANALYZER_VERSION was "0.1.0" while package.json was "0.5.0", so the new
analyzer{name,version} manifest advertised a false version. Bump it to
match, and assert analyzer.version equals package.json's version (imported,
not hardcoded) so future drift fails the schema-v2 gate.
…discarded

The v2 emitter only reads external_symbols/synthesized_callables and
populates call_graph at level >= 2 (homeExternals/homeSynthesized in
src/schema/v2/emit.ts), so running the full provider solve — including
the heavier Jelly leg — at -a 1 computed a result that was thrown away.
Gate provider.build to analysisLevel >= 2; levels 3/4 already require
>= 2 for callee resolution, so this is safe.
…ecisions + neo4j lockstep)

Strengthens the code comment at src/schema/v2/dataflow.ts to record that
"reaching-defs" is a deliberate, sanctioned additive DDG prov token — a
documented deviation from the canonical "ssa" tag — accepted across
JSON, Neo4j (open string[] property, no enum), and the SDK. Full
rationale recorded in .claude/SCHEMA_DECISIONS.md (gitignored, local-
only). No emitter behavior change: the token stays "reaching-defs" and
test/schema-v2.test.ts's assertion is untouched.
…hips

Resolves each type's heritage signatures (base_classes/implements_types)
through idBySig to can:// ids in the v2 emitter, dropping unresolved
external/library supertypes rather than nulling them. Exposes the result
as extends_ids/implements_ids on the V2 type node (JSON props unchanged)
and projects them as EXTENDS/IMPLEMENTS relationships in Neo4j via the
previously-dead RowBuilder.edgeToSymbol deferred gate, which was also
re-pointed at the correct CanNode/id addressing (it had been hardcoded to
a Symbol/signature pair no node in the v2 schema actually carries).

Adds a first-party class hierarchy fixture to dataflow-app and extends
both the schema conformance test and the issue #27 Neo4j<->JSON parity
gate (the latter now also has an exhaustive total-edge accounting test).
The JSON envelope advertises analyzer{name,version} (#29), but the
Neo4j :Application node had neither — so the two co-primary
projections diverged on analyzer identity. project() now sources
name/version from the same V2Application.analyzer the envelope uses
(populated from ANALYZER_VERSION in src/utils/version.ts), declared
in the schema catalog and regenerated into schema.neo4j.json.
…sion

Bare name/version on :Application collided with project()'s _appName param
and every other CanNode's bare `name`. Namespace analyzer identity the same
way the JSON envelope does (analyzer{name,version}), unambiguous from the
node's own identity fields.
src/build/neo4j/rows.ts and src/schema/v2/dataflow.ts used raw 0x00
bytes as template-literal key separators (Map keys / sort-comparator
keys), which made git treat both files as binary and hid contract-
adjacent edits from review. Replace each raw NUL with the `\0` escape
sequence — at runtime "\0" is still U+0000, so every key built from it
is byte-identical; only the on-disk representation changes, from
binary to plain text.
…lices (#45)

Adds a first-party `ColoredShape extends Shape` interface to the dataflow-app
hierarchy fixture and asserts the resulting Interface→Interface EXTENDS edge
by node identity in the neo4j schema test — the one heritage path #33 handles
but nothing previously exercised.

Also asserts that L3 `@entry`/`@exit` body nodes byte-slice to the callable's
whole span (per extract.ts's emitNode), alongside the existing statement-node
slice test. Confirms `param` is contracted out before emission (folded onto
`@entry`/`@formal_in`) and is never present as a raw body node, so no slice
test is fabricated for it.
The container-gated bolt test still queried the v1-era :Symbol label,
which no longer exists in schema v2 (every project-owned node carries
:CanNode plus a specific kind label). Since :CanNode spans ALL kinds
(not just the signature-keyed declaration types v1's :Symbol covered),
the exhaustiveness check now enumerates all 11 specific labels that
merge under :CanNode per schema.neo4j.json: Module, Class, Interface,
Enum, TypeAlias, Namespace, Callable, Field, BodyNode, External,
AnonymousCallable.
@rahlk rahlk force-pushed the fix/issue-46-bolt-symbol-labels branch from 05e63a7 to 83fe4b3 Compare July 15, 2026 01:24
rahlk added 6 commits July 15, 2026 00:18
…#70)

A plain endpoint-pair MERGE collapses legitimately-distinct edges of one
type between the same two nodes: the DDG carries one edge per variable
(plus the prov split), and a conditional's true/false CFG_NEXT pair shares
its endpoints — the live graph silently materialized fewer dependence
edges than the projection produced, with var/prov/kind props last-write-wins.
Found in codeanalyzer-python during its v2 stage-5 gates (bolt count test
106 != 109) and fixed there the same way.

- EdgeRow gains an optional `key` discriminant; RowBuilder.edge passes it.
- bolt/cypher writers MERGE keyed families on `(a)-[r:TYPE {_k: row.k}]->(b)`
  and group keyed vs unkeyed rows separately.
- project(): DDG keyed per `(var, prov)`, CFG_NEXT per `kind`; CDG/SUMMARY/
  PARAM_* stay plain (their endpoint pairs are unique by construction).
- schema catalog declares `_k` on DDG/CFG_NEXT; schema.neo4j.json regenerated.
- test/neo4j-edge-identity.test.ts covers survival of per-var DDG rows, the
  keyed MERGE shape, and that undiscriminated families keep the plain MERGE.
#70)

The issue #27 parity gate compares rows to JSON before the writers run, so a
missing or non-unique _k would still collapse edges in the live graph without
failing any test (the bolt container suite pushes at -a 1, where the
discriminated families are empty). Assert on the L4 dataflow projection that
every DDG/CFG_NEXT row carries a key, that (from, to, _k) tuples are fully
distinct, and — non-vacuously — that both families really produce parallel
edges between one endpoint pair on this fixture.
rahlk added 6 commits July 15, 2026 01:12
…; scope + index bolt lookups

A 2.0.0 push onto a 1.1.0 DB left the whole 1.x subgraph orphaned and
poisonous: old nodes carry twin TS labels so they match v2 queries, two
:Application nodes make the version read nondeterministic, and the
vanished-decl sweep never removed them (their id is null → three-valued
logic dropped the row).

- Legacy-generation wipe when the version gate forces a full upsert:
  detect-and-delete 1.x _module carriers, shared External/Package/Decorator
  nodes, and the id-less :Application in one idempotent step before the
  per-module loop. Unanchored on purpose (legacy nodes lack :CanNode).
- Null-guard the vanished-decl sweep (x.id IS NULL OR NOT x.id IN keys).
- Scope the schema-version read to this app's :Application {id} so a shared
  DB can't surface a foreign analyzer's version.
- Anchor the per-module edge-delete and decl sweep on (:CanNode {_module})
  and add the cannode_module index to back them; regenerate schema.neo4j.json.
…d 1.1.0→2.0.0 migration scenario

Replace the stale v1 magic numbers (constraints ≥ 8, indexes ≥ 11) with
counts derived from the exported CONSTRAINTS/INDEXES catalog. Add a
migration scenario: seed a minimal schema-1.1.0 graph (twin labels, an
id-less :Application), run a full 2.0.0 bolt push against the same DB, and
assert exactly one :Application survives (id set, version 2.0.0), no
_module-carrying non-:CanNode residue remains, and TSModule count matches
the fixture (no duplicates).
Add a minimal CI workflow (pull_request to main + push to main): a single
ubuntu-latest job that checks out, sets up Bun, installs with a frozen
lockfile, runs typecheck and the default test suite, then runs the
container-gated Neo4j bolt suite with RUN_CONTAINER_TESTS=1 (ubuntu runners
ship Docker, so testcontainers works out of the box).
- src/schema/schema.ts header: the spine field is external_symbols, not the
  nonexistent 'entrypoints'.
- delete the orphaned TSSymbol interface (zero references).
- rows.ts NodeRef.keyProp comment: only 'id' exists at schema v2.
- project.ts idOf doc: it IS the identity (pass-through), not an @Local strip.
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.

1 participant