Spec: docs/design/specs/2026-09-02-prune-scope-on-can-id-prefix.md
Summary
Every statement that deletes stops matching on labels plus an internal _module property, and starts matching on the can:// id prefix of the application being pushed. The id is already a hierarchical path — can://<lang>/<app>/<file>/<type>/<signature> — so a prefix match is containment: language, application and module scoping all come from identity that already exists, instead of from a denormalized property and a hand-maintained label list kept correct by hand.
_module retires along with every index on it.
Why
Three defects in six weeks, all the same shape — a delete whose scope was expressed by something other than identity:
|
|
| codeanalyzer-typescript#116 |
an unanchored wipe deleted the java and python graphs outright. Surfaced only as an OOM, because the delete was big enough to exhaust dbms.memory.transaction.total.max and roll back. A smaller foreign graph would have gone silently. Fixed in c1c27f3. |
| codeanalyzer-java#213 |
an unlabelled _module match deleted a sibling analyzer's nodes wherever a file key collided across languages. Fixed. |
| open, in all three |
the per-module purge is application-blind. _module is a bare project-relative path, so two applications in the same language sharing src/main/java/Foo.java delete each other's nodes for it. |
The first two were fixed by adding labels to the match. That treats the symptom. The third is what remains afterwards — and it is the one a label cannot fix, because both applications' nodes carry identical labels. Only identity separates them.
What each analyzer does
- scope both the per-module purge and the orphan prune on the application's
can:// id prefix
- drop
_module from its projection and its graph catalog, and any index on it
- carry a language-namespace marker label (
JCanNode, PyCanNode, TSCanNode, JSCanNode) purely as an index anchor — Neo4j property indexes are label-scoped, so a prefix seek needs one
- MAJOR graph-contract bump: a property is removed
No lockstep. Each analyzer changes its own writers and cuts its own release, because no analyzer reads another's nodes — which is the entire point of the change.
Not decided here
Left open in the spec §9: whether --eager becomes the universal gate on deletion (typescript already refuses to delete without it; java and python delete unconditionally), and whether any polyglot consumer needs a shared cross-language label alongside the per-language markers.
Order
Cheapest verification first: typescript (already prefix-scopes its prune and gates on --eager; the work is retiring _module and app-scoping the purge), then java (largest gap), then python. Docs follow the last analyzer.
Spec:
docs/design/specs/2026-09-02-prune-scope-on-can-id-prefix.mdSummary
Every statement that deletes stops matching on labels plus an internal
_moduleproperty, and starts matching on thecan://id prefix of the application being pushed. The id is already a hierarchical path —can://<lang>/<app>/<file>/<type>/<signature>— so a prefix match is containment: language, application and module scoping all come from identity that already exists, instead of from a denormalized property and a hand-maintained label list kept correct by hand._moduleretires along with every index on it.Why
Three defects in six weeks, all the same shape — a delete whose scope was expressed by something other than identity:
dbms.memory.transaction.total.maxand roll back. A smaller foreign graph would have gone silently. Fixed inc1c27f3._modulematch deleted a sibling analyzer's nodes wherever a file key collided across languages. Fixed._moduleis a bare project-relative path, so two applications in the same language sharingsrc/main/java/Foo.javadelete each other's nodes for it.The first two were fixed by adding labels to the match. That treats the symptom. The third is what remains afterwards — and it is the one a label cannot fix, because both applications' nodes carry identical labels. Only identity separates them.
What each analyzer does
can://id prefix_modulefrom its projection and its graph catalog, and any index on itJCanNode,PyCanNode,TSCanNode,JSCanNode) purely as an index anchor — Neo4j property indexes are label-scoped, so a prefix seek needs oneNo lockstep. Each analyzer changes its own writers and cuts its own release, because no analyzer reads another's nodes — which is the entire point of the change.
Not decided here
Left open in the spec §9: whether
--eagerbecomes the universal gate on deletion (typescript already refuses to delete without it; java and python delete unconditionally), and whether any polyglot consumer needs a shared cross-language label alongside the per-language markers.Order
Cheapest verification first: typescript (already prefix-scopes its prune and gates on
--eager; the work is retiring_moduleand app-scoping the purge), then java (largest gap), then python. Docs follow the last analyzer.