Repository navigation
Description
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 :PyClass and :PyCallable (codeanalyzer/neo4j/project.py:841-842,866-867):
"is_entrypoint": bool(c.entrypoints),
"entrypoint_frameworks": sorted({e.framework for e in (c.entrypoints or [])}),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
PyEntrypointfield 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.
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 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:
: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.- 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:
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
- The routed-class case means
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.- 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.jsonsnapshots. schema_versionand the graphSCHEMA_VERSIONstay2.0.0under the 2026-09-07 hold; consumers gate on the analyzer version floor.- The cross-projection release gate compares
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
- A design decision recorded in
.claude/SCHEMA_DECISIONS.mdpicking the shape, with the rejected alternatives named. - Every
PyEntrypointfield reachable from the graph, with per-record fidelity for owners holding several. - A test asserts, for a fixture with two
@app.routedecorators on one function, that both routes and their respective verbs survive the projection. - A test asserts
viaresolves to a node that exists in the same emission. - Catalog updated and both
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.
Activity