You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
and, at application scope (project.py:83-85), entrypoint_frameworks plus entrypoint_report_json — which is the report (frameworks_detected, unresolved, errors), not the per-entrypoint records.
So the URL, the HTTP verbs, the evidence, the confidence, the rule that fired, and the routed-class dispatch link are all JSON-only. On the graph you can ask "is this an entrypoint, and roughly by which framework", and nothing more. The two questions a consumer actually asks of a web application — what URL invokes this callable and which callable serves this URL — have no Cypher form at all.
Same family as #202/#203 (PR #206, released in 1.5.2): both projections are first-class, so a fact in analysis.json and not in the graph is a bug in the graph. Re-expressing a field as an edge or a flattened property is fine; dropping it is not.
Describe the solution you'd like
Every PyEntrypoint field is reachable from the graph.
MATCH ... WHERE <route predicate> finds the callables serving a URL, and the reverse direction works from a callable.
Several entrypoint records on one owner stay distinguishable, with each route still bound to its own verbs, confidence, rule and evidence.
via keeps its meaning: a can:// id, resolvable to a real node.
Describe alternatives you've considered
Not stated in the original issue.
Additional context
Scope boundary
In scope: every PyEntrypoint field becomes reachable from the graph, and a Cypher predicate on the route works.
Out of scope: new entrypoint detection (frameworks, descriptor-derived routes), and any change to the payload — PyEntrypoint already carries everything; this is projection-only.
This moves the graph contract (a callable can hold several entrypoint records, so representing them faithfully needs either a new label or a new relationship), which puts it at the design rung, not maintenance.
The shape question
A node may hold several entrypoint records — two @app.route decorators, or a function that is both a Celery task and a CLI command. That rules out the naive flatten: parallel entrypoint_routes / entrypoint_http_methods arrays lose the correlation between a route and its verbs, which is the same trap PY_IMPORTS.spellings hit and positions_json had to work around in #206.
Three candidates, to be settled in the design:
:PyEntrypoint node merged on the owning id plus framework plus route, joined by a PY_HAS_ENTRYPOINT edge. Faithful to the N-records shape, and a route predicate is a plain property match. Costs a label.
entrypoints_json on the owner — the arguments_json / value_json / positions_json precedent. Cheapest, loses queryability, which is the whole point here.
Flat properties for the single-entrypoint case, blob for the rest — rejected on sight; a property whose presence depends on cardinality is worse than either.
Preference is (1), for the reason (2) exists at all: the JSON-blob precedents are for values Neo4j cannot hold (arbitrary value, per-spelling positions). A route is a string, and it is what consumers filter on.
Cross-language parity
codeanalyzer-java is concurrently adding descriptor-derived routes (web.xml servlet / filter / welcome-file / error-page, faces-config.xml navigation) as part of its web-application theme. Two names must not be coined twice:
route and http_methods are already coined here and java should adopt them verbatim — not url, path, or methods.
via is already coined here as the can:// id of the routed node dispatching here (entrypoints/matching.py:208, via=cls.id for a routed class dispatching to its methods). Java's proposed view-dispatch edge wants via for the mechanism (forward / include / sendRedirect / view-name / JSF outcome). One of the two has to give; kind is the free name for a mechanism, with the DECLARES_DEPENDENCY.kind and _k merge-discriminant precedent behind it.
Caveats and known risks
The routed-class case means via points at a node that is itself an entrypoint owner — whatever shape wins has to keep that link expressible without a cycle in the merge key.
route is Optional[str]: a Celery task or a CLI command has none. Absent must stay absent rather than becoming "", or a predicate on route silently matches non-HTTP entrypoints.
Additive, so the monotonicity gate is unaffected, but Neo4j is always full-depth: any new label or relationship has to be declared in the catalog and land in both schema.neo4j.json snapshots.
schema_version and the graph SCHEMA_VERSION stay 2.0.0 under the 2026-09-07 hold; consumers gate on the analyzer version floor.
Is your feature request related to a problem? Please describe.
PyEntrypoint(codeanalyzer/schema/py_schema.py:258-275) carries eight fields:framework,confidence,rule,ruleset,evidence,route,http_methods,via. Exactly one of them reaches Neo4j.The projection emits, on
:PyClassand:PyCallable(codeanalyzer/neo4j/project.py:841-842,866-867):and, at application scope (
project.py:83-85),entrypoint_frameworksplusentrypoint_report_json— which is the report (frameworks_detected,unresolved,errors), not the per-entrypoint records.So the URL, the HTTP verbs, the evidence, the confidence, the rule that fired, and the routed-class dispatch link are all JSON-only. On the graph you can ask "is this an entrypoint, and roughly by which framework", and nothing more. The two questions a consumer actually asks of a web application — what URL invokes this callable and which callable serves this URL — have no Cypher form at all.
Same family as #202/#203 (PR #206, released in 1.5.2): both projections are first-class, so a fact in
analysis.jsonand not in the graph is a bug in the graph. Re-expressing a field as an edge or a flattened property is fine; dropping it is not.Describe the solution you'd like
PyEntrypointfield is reachable from the graph.MATCH ... WHERE <route predicate>finds the callables serving a URL, and the reverse direction works from a callable.viakeeps its meaning: acan://id, resolvable to a real node.Describe alternatives you've considered
Not stated in the original issue.
Additional context
Scope boundary
In scope: every
PyEntrypointfield becomes reachable from the graph, and a Cypher predicate on the route works.Out of scope: new entrypoint detection (frameworks, descriptor-derived routes), and any change to the payload —
PyEntrypointalready carries everything; this is projection-only.This moves the graph contract (a callable can hold several entrypoint records, so representing them faithfully needs either a new label or a new relationship), which puts it at the design rung, not maintenance.
The shape question
A node may hold several entrypoint records — two
@app.routedecorators, or a function that is both a Celery task and a CLI command. That rules out the naive flatten: parallelentrypoint_routes/entrypoint_http_methodsarrays lose the correlation between a route and its verbs, which is the same trapPY_IMPORTS.spellingshit andpositions_jsonhad to work around in #206.Three candidates, to be settled in the design:
:PyEntrypointnode merged on the owning id plus framework plus route, joined by aPY_HAS_ENTRYPOINTedge. Faithful to the N-records shape, and a route predicate is a plain property match. Costs a label.entrypoints_jsonon the owner — thearguments_json/value_json/positions_jsonprecedent. Cheapest, loses queryability, which is the whole point here.Preference is (1), for the reason (2) exists at all: the JSON-blob precedents are for values Neo4j cannot hold (arbitrary
value, per-spelling positions). A route is a string, and it is what consumers filter on.Cross-language parity
codeanalyzer-java is concurrently adding descriptor-derived routes (
web.xmlservlet / filter / welcome-file / error-page,faces-config.xmlnavigation) as part of its web-application theme. Two names must not be coined twice:routeandhttp_methodsare already coined here and java should adopt them verbatim — noturl,path, ormethods.viais already coined here as thecan://id of the routed node dispatching here (entrypoints/matching.py:208,via=cls.idfor a routed class dispatching to its methods). Java's proposed view-dispatch edge wantsviafor the mechanism (forward/include/sendRedirect/ view-name / JSF outcome). One of the two has to give;kindis the free name for a mechanism, with theDECLARES_DEPENDENCY.kindand_kmerge-discriminant precedent behind it.Caveats and known risks
viapoints at a node that is itself an entrypoint owner — whatever shape wins has to keep that link expressible without a cycle in the merge key.routeisOptional[str]: a Celery task or a CLI command has none. Absent must stay absent rather than becoming"", or a predicate on route silently matches non-HTTP entrypoints.schema.neo4j.jsonsnapshots.schema_versionand the graphSCHEMA_VERSIONstay2.0.0under the 2026-09-07 hold; consumers gate on the analyzer version floor.can://ids only, so it would not have caught this gap and will not catch the next one of its kind — the properties that carry no id need their own assertion, the way fix(neo4j): the graph projection carries the facts analysis.json carries #206's parity tests do.Definition of done
.claude/SCHEMA_DECISIONS.mdpicking the shape, with the rejected alternatives named.PyEntrypointfield reachable from the graph, with per-record fidelity for owners holding several.@app.routedecorators on one function, that both routes and their respective verbs survive the projection.viaresolves to a node that exists in the same emission.schema.neo4j.jsonsnapshots regenerated.route/http_methods/viaspellings confirmed against codeanalyzer-java's web-application work before landing, so neither side coins a second name.