fix(neo4j)!: scope every destructive statement on the can:// id prefix; retire _module - #166
Merged
Merged
Conversation
…x; retire _module
The per-module purge matched `(x:CanNode {_module: $m})`. That predicate is true
across languages and false across applications: two TypeScript projects in one
database that share a file key deleted each other's nodes for it. And the --eager
purge, scoped on the bare application id, never reached a `can://javascript/`
node at all -- the JS namespace (#115) was invisible to it.
Every destructive statement now scopes on the can:// id: the node by equality,
its descendants by `id + '/'`. The separator is the point -- a bare prefix also
matches `.../foo.tsx` under `.../foo.ts` and `appXtra` under `app`. The --eager
purge, the per-module purge and the orphan prune (bolt) and the snapshot wipe
(cypher) all use it, per namespace: this analyzer emits two, so each statement
runs once under `can://typescript/<app>/` and once under `can://javascript/<app>/`.
`_module` is gone from the graph, the catalog and the index. The RowBuilder lifts
it off the row into NodeRow.module, which only groups rows for the incremental
diff; that diff is keyed by module id inside the application's prefixes instead of
by a file key that collides across applications.
`TSCanNode` / `JSCanNode` ride every node keyed by a `can://<lang>/` id, as index
anchors: property indexes are label-scoped, and `STARTS WITH` seeks only on a range
index. They carry no safety claim; the prefix does. `CanNode` stays until #95.
An empty application is refused on any push -- the diff itself is app-scoped, and
`STARTS WITH ''` is the whole store. The snapshot renderer emits no wipe at all
when its rows carry no can:// application id.
Container test: the same fixture pushed as `sa` and `saX` -- every file key
collides, and `can://typescript/sa` is a string prefix of `can://typescript/saX`
-- survives each other's --eager purge and prune. Mutation-checked: dropping the
`/` from the descendant prefix makes that test fail.
SCHEMA_VERSION stays 2.0.0 (#144, python #186) despite the removed property; that
supersedes #140's "MAJOR bump" goal. The sample-app snapshot diff against main is
exactly: `_module` gone, the marker added, the wipe rewritten.
rahlk
force-pushed
the
fix/issue-140-prefix-scoped-prune
branch
from
September 6, 2026 13:00
0f60c15 to
030f291
Compare
This was referenced Sep 6, 2026
Merged
rahlk
added a commit
that referenced
this pull request
Sep 6, 2026
ANALYZER_VERSION moves with package.json because it is the cache-invalidation key: this release changes the cached tree shape (body nodes and parameters carry ids, modules carry INTERNAL call_sites), so a warm 1.2.0 cache must not be reused. README --help block regenerated for --program, --list-programs, --emit-ir and --no-repo-sections (#149), which shipped without it. Released as a MINOR despite one breaking-marked commit (#166: `_module` retired from the Neo4j graph, marker labels added), by the same decision as 1.2.0; the release notes lead with it and its migration. Graph contract stays 2.0.0 (#144).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #140. Implements the org spec
2026-09-02-prune-scope-on-can-id-prefix.md(epic codellm-devkit/.github#50) for TypeScript, matching python's #183.The bug
The per-module purge matched
(x:CanNode {_module: $m}). That predicate is true across languages and false across applications: two TypeScript projects in one database sharing a file key deleted each other's nodes for it. Found while implementing: the--eagerpurge, scoped on the bare application idcan://typescript/<app>, never reached acan://javascript/node at all — the JS namespace from #115 was invisible to it.What changes
can://id: the node by equality, descendants byid + '/'. The separator is the point — a bare prefix also matches…/foo.tsxunder…/foo.tsandappXtraunderapp. Applied to the--eagerpurge, the per-module purge, the orphan prune (bolt) and the snapshot wipe (cypher), each run once per namespace this analyzer emits._moduleretired from the graph, the catalog (schema.neo4j.json) and its index.RowBuilderlifts it off the row intoNodeRow.module, which only groups rows; the incremental diff is now keyed by module id inside the app prefixes, not by a file key that collides across apps.TSCanNode/JSCanNodemarkers on every node keyed by acan://<lang>/id, with a range index onideach. Index anchors only —STARTS WITHseeks only on a range index, and property indexes are label-scoped.CanNodestays until neo4j: merge labels Application and CanNode are unprefixed — reconcile with the cross-repo prefix rule #95 retires it.can://application id, rather thanSTARTS WITH ''.Verification
saandsaX— every file key collides, andcan://typescript/sais a string prefix ofcan://typescript/saX— survives each other's--eagerpurge and prune, and no node carries_module. The existing vanished-module test was rewritten to find nodes by id prefix./from the descendant prefix makes that test fail._modulenever reaching props, and the shape of every destructive statement including the snapshot wipe.mainonsample-app: 415 lines each way, accounted for entirely by_moduleremovals (398), marker-label additions (14) and the wipe header. No other property, label or edge changed.bun testgreen; container suite 5 pass;schema.neo4j.jsonregenerated; decisions recorded in.claude/SCHEMA_DECISIONS.mdDeviations from #140 as written
SCHEMA_VERSIONstays2.0.0. Neo4j: scope destructive statements on the can:// id prefix; retire _module #140 calls for a MAJOR bump because a property is removed; chore(schema)!: collapse the schema version back to 2.0.0 #144 (and python's --app-name is not emitted as a name: the application object has no name field, so the graph anchor is unnamed #186, which pulled its 3.0.0 back) decided there is one version until every analyzer re-baselines together. Recorded as P5 in the decisions file._module— it still queries the v1 vocabulary (:Module,m.file_key,:Symbol {signature}), so it is out of step with v2 for reasons well beyond this change. Worth its own issue.