Skip to content

fix(neo4j)!: scope the v2 purge on the can:// id prefix; retire _module from v2 - #220

Merged
rahlk merged 2 commits into
mainfrom
fix/issue-219-prefix-scope
Sep 2, 2026
Merged

rahlk merged 2 commits into
mainfrom
fix/issue-219-prefix-scope

Conversation

@rahlk

@rahlk rahlk commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Epic: codellm-devkit/.github#50 · Spec: 2026-09-02-prune-scope-on-can-id-prefix.md

Closes #219, #218, #217.

Problem

The purge scoped deletion by _module — a bare project-relative file key stamped on every node. It carries no language and no application, so even after #213 anchored it on java-owned labels it was still application-blind: two java applications in one database sharing src/main/java/Foo.java have identical labels and an identical file key. Only identity distinguishes them, which is why no label could fix it.

Fix

The v2 purge scopes on the module's own can:// id — module by equality, declarations by id STARTS WITH <id> + '/'. The id is a path, so a prefix match is containment, and it is scoped to one language, one application and one module at once. Shared nodes fall outside by construction: artifacts are can://artifact/…, packages are pkg:…, and name-keyed nodes have no id to match at all.

_module retires from the v2 contract. It survives as NodeRow.moduleKey, an in-memory field the writer uses to group nodes — the graph never needed telling, since a node's owning module is already its id prefix. RowBuilder lifts it off the emitted properties in one place, so no projector call site changed.

The v1 carve-out, which the spec did not anticipate

v1 ids are FQN-shaped and carry no application segment:

com.l4.Arity#caller(int, int)@22:16-22:25

There is nothing to prefix, so v1 cannot be prefix-scoped. It keeps exactly what it had — _module plus the #213 label anchor — and BoltWriter branches on whether the module's id is a can:// id. MODULE_OWNED is consequently now the v1 vocabulary alone, which its test now asserts rather than assumes.

This means _module retires from the v2 contract but survives on the legacy path, contradicting the spec's claim that it retires outright. The spec needs amending; flagged rather than glossed.

:JCanNode

Nodes keyed on a can:// id gain a :JCanNode marker, declared as a marker label and indexed.

It exists only so the prefix predicate can seek — Neo4j property indexes are label-scoped, so the match would otherwise scan the whole node store once per changed module (that is #217, resolved here by making the property unnecessary rather than by indexing it). It makes no semantic claim, unlike :JSymbol, and being java-namespaced means an anchoring mistake still cannot reach another language's nodes.

STARTS WITH is index-backed on a range index; CONTAINS/ENDS WITH are not — the predicate is written that way deliberately.

One detail worth not inlining

The descendant separator is a named function, not a + "/" at the call site, because dropping it is silent: can://java/app/src/Foo.java also prefixes can://java/app/src/Foo.javaX, and would purge a different module's nodes. There is a test on it.

Verification

suite 512 tests, 1 failure — the pre-existing Docker CodeAnalyzerIntegrationTest
v2 graph.cypher (daytrader8) 0 _module properties, 45 JCanNode, 1 id index
v1 graph.cypher 59 _module, 0 JCanNode — legacy path untouched
analysis.json byte-identical — _module was always projection-only

Breaking

Graph contract 2.2.0 → 3.0.0: a property is removed. analysis.json is unaffected.

…le from v2

The incremental purge scoped deletion by `_module`, a bare project-relative file
key stamped onto every node. That key carries no language and no application, so
after #213 anchored it on java-owned labels it was still application-blind: two
java applications in one database sharing src/main/java/Foo.java have identical
labels and an identical file key, and only identity distinguishes them.

The v2 purge now scopes on the module's own can:// id -- the module by equality,
its declarations by `id STARTS WITH <id> + '/'`. The id is a path, so a prefix
match is containment, and it is simultaneously scoped to one language, one
application and one module. Cross-language and shared nodes fall outside by
construction: artifacts are can://artifact/..., packages are pkg:..., and
name-keyed nodes have no id to match.

`_module` retires from the v2 contract. It survives as NodeRow.moduleKey, an
in-memory field the writer uses to group nodes -- the graph never needed telling,
since a node's owning module is already its id prefix. RowBuilder lifts it off
the emitted properties in one place, so no projector call site changes.

v1 keeps exactly what it had. Its ids are FQN-shaped (com.l4.Chain#a(int)@5:16)
with no application segment, so there is no prefix to scope by; that path keeps
`_module` and the #213 label anchor, and BoltWriter branches on whether the
module's id is a can:// id. MODULE_OWNED is therefore now the v1 vocabulary
alone, which its test asserts rather than assumes.

Nodes keyed on a can:// id gain a :JCanNode marker, declared as a marker label
and indexed. It exists only so the prefix predicate can seek: Neo4j property
indexes are label-scoped, so the match would otherwise scan the whole node store
once per changed module. It makes no semantic claim, unlike :JSymbol, and being
java-namespaced means an anchoring mistake still cannot reach another language.

The descendant separator is a named function rather than an inlined `+ "/"`,
because dropping it is silent: `can://java/app/src/Foo.java` also prefixes
`can://java/app/src/Foo.javaX`.

Graph contract 2.2.0 -> 3.0.0: a property is removed. analysis.json is untouched
and byte-identical on daytrader8; `_module` was always projection-only.

Part of codellm-devkit/.github#50. Closes #219. Closes #218. Closes #217.
… mechanisms

v2 is the schema as of 3.0.0. Its ids are can:// paths, so a prefix match scopes
a purge to one language, one application and one module at once. v1 ids are
FQN-shaped and carry no application segment, so they cannot be scoped that way at
all -- and rather than keep a second, weaker mechanism alive for a legacy path,
the purge is now skipped for v1 rows and says so in the log. A v1 push still
upserts; it just never deletes.

That retires MODULE_OWNED and both label-anchored statements from #213, which
existed only to make the _module match safe. Nothing anchors on _module any more,
so there is nothing left for them to protect.

The purge test now pins the properties that make the new scoping correct -- the
predicate shape, the label that lets the prefix seek, the separator that stops a
sibling path matching, and the can:// recognition that selects the path -- rather
than the label vocabulary that no longer guards anything.
@rahlk
rahlk merged commit 07b4a3b into main Sep 2, 2026
@rahlk
rahlk deleted the fix/issue-219-prefix-scope branch September 2, 2026 16:37
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.

Neo4j: scope destructive statements on the can:// id prefix; retire _module

1 participant