agentforge-graph 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentforge_graph/__init__.py +6 -0
- agentforge_graph/chunking/__init__.py +12 -0
- agentforge_graph/chunking/cast.py +159 -0
- agentforge_graph/chunking/chunk.py +19 -0
- agentforge_graph/chunking/tokens.py +15 -0
- agentforge_graph/cli.py +607 -0
- agentforge_graph/config.py +259 -0
- agentforge_graph/core/__init__.py +54 -0
- agentforge_graph/core/conformance.py +270 -0
- agentforge_graph/core/contracts.py +163 -0
- agentforge_graph/core/kinds.py +68 -0
- agentforge_graph/core/models.py +134 -0
- agentforge_graph/core/provenance.py +62 -0
- agentforge_graph/core/symbols.py +116 -0
- agentforge_graph/embed/__init__.py +28 -0
- agentforge_graph/embed/base.py +22 -0
- agentforge_graph/embed/bedrock.py +85 -0
- agentforge_graph/embed/fake.py +34 -0
- agentforge_graph/embed/openai.py +67 -0
- agentforge_graph/embed/pipeline.py +184 -0
- agentforge_graph/embed/registry.py +66 -0
- agentforge_graph/embed/report.py +15 -0
- agentforge_graph/enrich/__init__.py +70 -0
- agentforge_graph/enrich/anthropic.py +38 -0
- agentforge_graph/enrich/anthropic_client.py +109 -0
- agentforge_graph/enrich/bedrock.py +24 -0
- agentforge_graph/enrich/bedrock_client.py +115 -0
- agentforge_graph/enrich/bedrock_summarizer.py +23 -0
- agentforge_graph/enrich/claude.py +172 -0
- agentforge_graph/enrich/enricher.py +108 -0
- agentforge_graph/enrich/governs.py +173 -0
- agentforge_graph/enrich/governs_enricher.py +152 -0
- agentforge_graph/enrich/heuristics.py +224 -0
- agentforge_graph/enrich/judge.py +63 -0
- agentforge_graph/enrich/registry.py +133 -0
- agentforge_graph/enrich/report.py +60 -0
- agentforge_graph/enrich/summarizer.py +62 -0
- agentforge_graph/enrich/summary_enricher.py +211 -0
- agentforge_graph/enrich/taxonomy.py +38 -0
- agentforge_graph/frameworks/__init__.py +29 -0
- agentforge_graph/frameworks/base.py +75 -0
- agentforge_graph/frameworks/detect.py +124 -0
- agentforge_graph/frameworks/extractor.py +63 -0
- agentforge_graph/frameworks/orm.py +93 -0
- agentforge_graph/frameworks/packs/_js_ast.py +56 -0
- agentforge_graph/frameworks/packs/_python_ast.py +157 -0
- agentforge_graph/frameworks/packs/django/__init__.py +240 -0
- agentforge_graph/frameworks/packs/django/models.scm +7 -0
- agentforge_graph/frameworks/packs/express/__init__.py +133 -0
- agentforge_graph/frameworks/packs/express/routes.scm +8 -0
- agentforge_graph/frameworks/packs/fastapi/__init__.py +210 -0
- agentforge_graph/frameworks/packs/fastapi/depends.scm +6 -0
- agentforge_graph/frameworks/packs/fastapi/routes.scm +10 -0
- agentforge_graph/frameworks/packs/flask/__init__.py +143 -0
- agentforge_graph/frameworks/packs/flask/routes.scm +11 -0
- agentforge_graph/frameworks/packs/nestjs/__init__.py +205 -0
- agentforge_graph/frameworks/packs/nestjs/routes.scm +6 -0
- agentforge_graph/frameworks/packs/spring/__init__.py +267 -0
- agentforge_graph/frameworks/packs/spring/routes.scm +6 -0
- agentforge_graph/frameworks/packs/sqlalchemy/__init__.py +250 -0
- agentforge_graph/frameworks/packs/sqlalchemy/models.scm +7 -0
- agentforge_graph/frameworks/registry.py +44 -0
- agentforge_graph/ingest/__init__.py +30 -0
- agentforge_graph/ingest/codegraph.py +847 -0
- agentforge_graph/ingest/extractor.py +353 -0
- agentforge_graph/ingest/incremental/__init__.py +25 -0
- agentforge_graph/ingest/incremental/detect.py +118 -0
- agentforge_graph/ingest/incremental/dirty.py +61 -0
- agentforge_graph/ingest/incremental/indexer.py +218 -0
- agentforge_graph/ingest/incremental/meta.py +72 -0
- agentforge_graph/ingest/incremental/ports.py +39 -0
- agentforge_graph/ingest/pack.py +160 -0
- agentforge_graph/ingest/packs/__init__.py +34 -0
- agentforge_graph/ingest/packs/cpp/__init__.py +35 -0
- agentforge_graph/ingest/packs/cpp/references.scm +15 -0
- agentforge_graph/ingest/packs/cpp/structure.scm +49 -0
- agentforge_graph/ingest/packs/csharp/__init__.py +35 -0
- agentforge_graph/ingest/packs/csharp/references.scm +12 -0
- agentforge_graph/ingest/packs/csharp/structure.scm +45 -0
- agentforge_graph/ingest/packs/go/__init__.py +38 -0
- agentforge_graph/ingest/packs/go/references.scm +12 -0
- agentforge_graph/ingest/packs/go/structure.scm +64 -0
- agentforge_graph/ingest/packs/java/__init__.py +35 -0
- agentforge_graph/ingest/packs/java/references.scm +12 -0
- agentforge_graph/ingest/packs/java/structure.scm +38 -0
- agentforge_graph/ingest/packs/javascript/__init__.py +34 -0
- agentforge_graph/ingest/packs/javascript/references.scm +11 -0
- agentforge_graph/ingest/packs/javascript/structure.scm +166 -0
- agentforge_graph/ingest/packs/php/__init__.py +35 -0
- agentforge_graph/ingest/packs/php/references.scm +15 -0
- agentforge_graph/ingest/packs/php/structure.scm +44 -0
- agentforge_graph/ingest/packs/python/__init__.py +25 -0
- agentforge_graph/ingest/packs/python/references.scm +14 -0
- agentforge_graph/ingest/packs/python/structure.scm +57 -0
- agentforge_graph/ingest/packs/ruby/__init__.py +37 -0
- agentforge_graph/ingest/packs/ruby/references.scm +12 -0
- agentforge_graph/ingest/packs/ruby/structure.scm +37 -0
- agentforge_graph/ingest/packs/rust/__init__.py +39 -0
- agentforge_graph/ingest/packs/rust/references.scm +12 -0
- agentforge_graph/ingest/packs/rust/structure.scm +46 -0
- agentforge_graph/ingest/packs/typescript/__init__.py +31 -0
- agentforge_graph/ingest/packs/typescript/references.scm +11 -0
- agentforge_graph/ingest/packs/typescript/structure.scm +99 -0
- agentforge_graph/ingest/pipeline.py +134 -0
- agentforge_graph/ingest/report.py +84 -0
- agentforge_graph/ingest/resolver.py +467 -0
- agentforge_graph/ingest/source.py +79 -0
- agentforge_graph/knowledge/__init__.py +28 -0
- agentforge_graph/knowledge/adr.py +136 -0
- agentforge_graph/knowledge/commits.py +152 -0
- agentforge_graph/knowledge/ingest.py +312 -0
- agentforge_graph/knowledge/mentions.py +71 -0
- agentforge_graph/knowledge/report.py +32 -0
- agentforge_graph/main.py +21 -0
- agentforge_graph/providers.py +36 -0
- agentforge_graph/repomap/__init__.py +14 -0
- agentforge_graph/repomap/rank.py +161 -0
- agentforge_graph/repomap/render.py +55 -0
- agentforge_graph/repomap/repomap.py +66 -0
- agentforge_graph/retrieve/__init__.py +21 -0
- agentforge_graph/retrieve/pack.py +76 -0
- agentforge_graph/retrieve/rerank.py +251 -0
- agentforge_graph/retrieve/retriever.py +286 -0
- agentforge_graph/retrieve/scoring.py +36 -0
- agentforge_graph/serve/__init__.py +19 -0
- agentforge_graph/serve/engine.py +204 -0
- agentforge_graph/serve/http_runner.py +133 -0
- agentforge_graph/serve/server.py +110 -0
- agentforge_graph/serve/tools.py +307 -0
- agentforge_graph/store/__init__.py +32 -0
- agentforge_graph/store/_rowmap.py +102 -0
- agentforge_graph/store/errors.py +22 -0
- agentforge_graph/store/facade.py +89 -0
- agentforge_graph/store/kuzu_store.py +380 -0
- agentforge_graph/store/lance_store.py +146 -0
- agentforge_graph/store/neo4j_store.py +294 -0
- agentforge_graph/store/pgvector_store.py +170 -0
- agentforge_graph/store/registry.py +45 -0
- agentforge_graph/temporal/__init__.py +36 -0
- agentforge_graph/temporal/backfill.py +338 -0
- agentforge_graph/temporal/events.py +82 -0
- agentforge_graph/temporal/index.py +190 -0
- agentforge_graph/temporal/mining.py +190 -0
- agentforge_graph/temporal/recorder.py +114 -0
- agentforge_graph/temporal/store.py +282 -0
- agentforge_graph-0.3.2.dist-info/METADATA +291 -0
- agentforge_graph-0.3.2.dist-info/RECORD +151 -0
- agentforge_graph-0.3.2.dist-info/WHEEL +4 -0
- agentforge_graph-0.3.2.dist-info/entry_points.txt +3 -0
- agentforge_graph-0.3.2.dist-info/licenses/LICENSE +202 -0
- agentforge_graph-0.3.2.dist-info/licenses/NOTICE +14 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""FastAPI framework pack (feat-011) — routes + dependency injection.
|
|
2
|
+
|
|
3
|
+
Routes: ``@app.get("/x")`` / ``@router.post(...)`` decorators become ``Route``
|
|
4
|
+
nodes + ``HANDLED_BY`` edges to the handler ``Function``. DI: a parameter
|
|
5
|
+
defaulting to ``Depends(provider)`` / ``Security(provider)`` becomes a
|
|
6
|
+
``Service`` node (the provider) + an ``INJECTED_INTO`` edge to the consuming
|
|
7
|
+
function. Both are intra-file (decorator/param and the function share a file, so
|
|
8
|
+
the edge endpoints are in the file's ``FileSubgraph``). Class-based handlers /
|
|
9
|
+
consumers resolve to their ``Class#method`` symbol. Cross-file ``include_router``
|
|
10
|
+
prefix composition and grounding the provider name to its definition are
|
|
11
|
+
follow-ups; a dynamic (non-literal) route path is counted as unresolved.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from functools import cache
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from tree_sitter import Node as TSNode
|
|
20
|
+
from tree_sitter import Parser, Query, QueryCursor
|
|
21
|
+
|
|
22
|
+
from agentforge_graph.core import (
|
|
23
|
+
Edge,
|
|
24
|
+
EdgeKind,
|
|
25
|
+
NodeKind,
|
|
26
|
+
Provenance,
|
|
27
|
+
SourceFile,
|
|
28
|
+
SymbolID,
|
|
29
|
+
)
|
|
30
|
+
from agentforge_graph.core import Node as GraphNode
|
|
31
|
+
from agentforge_graph.frameworks.base import FrameworkFacts, FrameworkPack
|
|
32
|
+
from agentforge_graph.frameworks.packs._python_ast import (
|
|
33
|
+
callee_name,
|
|
34
|
+
dotted_tail,
|
|
35
|
+
enclosing_class,
|
|
36
|
+
first_positional_arg,
|
|
37
|
+
first_string_in,
|
|
38
|
+
member_descriptor,
|
|
39
|
+
python_language,
|
|
40
|
+
text,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_HERE = Path(__file__).parent
|
|
44
|
+
_HTTP_METHODS = {"get", "post", "put", "delete", "patch", "options", "head"}
|
|
45
|
+
_DI_CALLS = {"Depends", "Security"} # FastAPI dependency markers
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@cache
|
|
49
|
+
def _routes_query() -> str:
|
|
50
|
+
return (_HERE / "routes.scm").read_text(encoding="utf-8")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@cache
|
|
54
|
+
def _depends_query() -> str:
|
|
55
|
+
return (_HERE / "depends.scm").read_text(encoding="utf-8")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class FastAPIPack(FrameworkPack):
|
|
59
|
+
name = "fastapi"
|
|
60
|
+
language = "python"
|
|
61
|
+
language_slug = "py" # SymbolID slug — must match the Python language pack
|
|
62
|
+
version = "1"
|
|
63
|
+
dep_names = ("fastapi",)
|
|
64
|
+
import_markers = ("import fastapi", "from fastapi")
|
|
65
|
+
|
|
66
|
+
def extract(self, file: SourceFile, repo: str, commit: str) -> FrameworkFacts:
|
|
67
|
+
src = file.text.encode("utf-8")
|
|
68
|
+
root = Parser(python_language()).parse(src).root_node
|
|
69
|
+
prov = Provenance.parsed(f"pack:{self.name}@{self.version}", commit)
|
|
70
|
+
facts = FrameworkFacts()
|
|
71
|
+
self._extract_routes(root, src, repo, file, prov, facts)
|
|
72
|
+
self._extract_depends(root, src, repo, file, prov, facts)
|
|
73
|
+
return facts
|
|
74
|
+
|
|
75
|
+
def _extract_routes(
|
|
76
|
+
self,
|
|
77
|
+
root: TSNode,
|
|
78
|
+
src: bytes,
|
|
79
|
+
repo: str,
|
|
80
|
+
file: SourceFile,
|
|
81
|
+
prov: Provenance,
|
|
82
|
+
facts: FrameworkFacts,
|
|
83
|
+
) -> None:
|
|
84
|
+
query = Query(python_language(), _routes_query())
|
|
85
|
+
seen: set[str] = set()
|
|
86
|
+
for _pattern, caps in QueryCursor(query).matches(root):
|
|
87
|
+
method_caps = caps.get("method")
|
|
88
|
+
handler_caps = caps.get("handler")
|
|
89
|
+
args_caps = caps.get("args")
|
|
90
|
+
if not (method_caps and handler_caps and args_caps):
|
|
91
|
+
continue
|
|
92
|
+
method = text(method_caps[0], src).lower()
|
|
93
|
+
if method not in _HTTP_METHODS:
|
|
94
|
+
continue # @app.middleware / @app.on_event / etc. — not a route
|
|
95
|
+
|
|
96
|
+
# A route we recognise but can't pin down statically is counted,
|
|
97
|
+
# not dropped: a dynamic (non-literal) path.
|
|
98
|
+
path = first_string_in(args_caps[0], src)
|
|
99
|
+
if path is None:
|
|
100
|
+
facts.unresolved += 1
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
handler = text(handler_caps[0], src)
|
|
104
|
+
handler_desc = member_descriptor(handler, enclosing_class(handler_caps[0], src))
|
|
105
|
+
handler_id = SymbolID.for_symbol(self.language_slug, repo, file.path, handler_desc)
|
|
106
|
+
method_u = method.upper()
|
|
107
|
+
route_id = SymbolID.for_symbol(
|
|
108
|
+
self.language_slug, repo, file.path, f"route({method_u} {path})."
|
|
109
|
+
)
|
|
110
|
+
if route_id in seen:
|
|
111
|
+
continue
|
|
112
|
+
seen.add(route_id)
|
|
113
|
+
route_node = caps["route"][0]
|
|
114
|
+
facts.nodes.append(
|
|
115
|
+
GraphNode(
|
|
116
|
+
id=route_id,
|
|
117
|
+
kind=NodeKind.ROUTE,
|
|
118
|
+
name=f"{method_u} {path}",
|
|
119
|
+
span=(route_node.start_point[0] + 1, route_node.end_point[0] + 1),
|
|
120
|
+
attrs={
|
|
121
|
+
"method": method_u,
|
|
122
|
+
"path": path,
|
|
123
|
+
"framework": self.name,
|
|
124
|
+
"handler": handler_id,
|
|
125
|
+
},
|
|
126
|
+
provenance=prov,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
facts.edges.append(
|
|
130
|
+
Edge(src=route_id, dst=handler_id, kind=EdgeKind.HANDLED_BY, provenance=prov)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def _extract_depends(
|
|
134
|
+
self,
|
|
135
|
+
root: TSNode,
|
|
136
|
+
src: bytes,
|
|
137
|
+
repo: str,
|
|
138
|
+
file: SourceFile,
|
|
139
|
+
prov: Provenance,
|
|
140
|
+
facts: FrameworkFacts,
|
|
141
|
+
) -> None:
|
|
142
|
+
query = Query(python_language(), _depends_query())
|
|
143
|
+
seen_service: set[str] = set()
|
|
144
|
+
seen_edge: set[tuple[str, str]] = set()
|
|
145
|
+
for _pattern, caps in QueryCursor(query).matches(root):
|
|
146
|
+
fn_caps = caps.get("fn")
|
|
147
|
+
name_caps = caps.get("func")
|
|
148
|
+
if not (fn_caps and name_caps):
|
|
149
|
+
continue
|
|
150
|
+
fn_node = fn_caps[0]
|
|
151
|
+
# MVP: only module-level functions (class-based consumers, like
|
|
152
|
+
# class-based route handlers, are a follow-up) — counted, not dropped.
|
|
153
|
+
params = fn_node.child_by_field_name("parameters")
|
|
154
|
+
if params is None:
|
|
155
|
+
continue
|
|
156
|
+
providers = self._depends_providers(params, src)
|
|
157
|
+
if not providers:
|
|
158
|
+
continue
|
|
159
|
+
consumer_desc = member_descriptor(
|
|
160
|
+
text(name_caps[0], src), enclosing_class(name_caps[0], src)
|
|
161
|
+
)
|
|
162
|
+
consumer_id = SymbolID.for_symbol(self.language_slug, repo, file.path, consumer_desc)
|
|
163
|
+
for provider in providers:
|
|
164
|
+
service_id = SymbolID.for_symbol(
|
|
165
|
+
self.language_slug, repo, file.path, f"service({provider})."
|
|
166
|
+
)
|
|
167
|
+
if service_id not in seen_service:
|
|
168
|
+
seen_service.add(service_id)
|
|
169
|
+
facts.nodes.append(
|
|
170
|
+
GraphNode(
|
|
171
|
+
id=service_id,
|
|
172
|
+
kind=NodeKind.SERVICE,
|
|
173
|
+
name=provider,
|
|
174
|
+
span=(fn_node.start_point[0] + 1, fn_node.start_point[0] + 1),
|
|
175
|
+
attrs={"framework": self.name, "provider": provider},
|
|
176
|
+
provenance=prov,
|
|
177
|
+
)
|
|
178
|
+
)
|
|
179
|
+
edge_key = (service_id, consumer_id)
|
|
180
|
+
if edge_key not in seen_edge:
|
|
181
|
+
seen_edge.add(edge_key)
|
|
182
|
+
facts.edges.append(
|
|
183
|
+
Edge(
|
|
184
|
+
src=service_id,
|
|
185
|
+
dst=consumer_id,
|
|
186
|
+
kind=EdgeKind.INJECTED_INTO,
|
|
187
|
+
provenance=prov,
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def _depends_providers(self, params: TSNode, src: bytes) -> list[str]:
|
|
192
|
+
"""Provider names from ``= Depends(provider)`` / ``= Security(provider)``
|
|
193
|
+
parameter defaults, in source order (deduped)."""
|
|
194
|
+
providers: list[str] = []
|
|
195
|
+
for param in params.named_children:
|
|
196
|
+
if param.type not in ("default_parameter", "typed_default_parameter"):
|
|
197
|
+
continue
|
|
198
|
+
value = param.child_by_field_name("value")
|
|
199
|
+
if value is None or value.type != "call":
|
|
200
|
+
continue
|
|
201
|
+
if callee_name(value, src) not in _DI_CALLS:
|
|
202
|
+
continue
|
|
203
|
+
arg = first_positional_arg(value, src)
|
|
204
|
+
provider = dotted_tail(arg, src) if arg is not None else ""
|
|
205
|
+
if provider and provider not in providers:
|
|
206
|
+
providers.append(provider)
|
|
207
|
+
return providers
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
FASTAPI_PACK = FastAPIPack()
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
; FastAPI dependency injection (feat-011). Capture every function so the pack
|
|
2
|
+
; can inspect its parameters for `= Depends(provider)` / `= Security(provider)`
|
|
3
|
+
; defaults in code (both `default_parameter` and `typed_default_parameter`
|
|
4
|
+
; carry the call in their `value` field).
|
|
5
|
+
(function_definition
|
|
6
|
+
name: (identifier) @func) @fn
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
; FastAPI route decorators (feat-011). Matches `@app.get("/x")` /
|
|
2
|
+
; `@router.post(...)` on a function. The method/path are validated in code
|
|
3
|
+
; (HTTP verb + a string-literal path) so non-route or dynamic-path decorators
|
|
4
|
+
; can be counted as unresolved rather than silently missed.
|
|
5
|
+
(decorated_definition
|
|
6
|
+
(decorator
|
|
7
|
+
(call
|
|
8
|
+
function: (attribute object: (identifier) @app attribute: (identifier) @method)
|
|
9
|
+
arguments: (argument_list) @args))
|
|
10
|
+
definition: (function_definition name: (identifier) @handler)) @route
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Flask framework pack (feat-011) — routes.
|
|
2
|
+
|
|
3
|
+
Extracts ``@app.route("/x", methods=[...])`` / blueprint ``@bp.route(...)`` and
|
|
4
|
+
the Flask 2.0 shortcuts (``@app.get("/x")`` …) into ``Route`` nodes + ``HANDLED_BY``
|
|
5
|
+
edges to the handler ``Function``/method. A ``route`` decorator defaults to
|
|
6
|
+
``GET`` and may list several ``methods`` → one ``Route`` per method. Class-based
|
|
7
|
+
handlers resolve to their ``Class#method`` symbol; a dynamic (non-literal) path
|
|
8
|
+
is counted unresolved, never dropped. Intra-file (decorator and handler share a
|
|
9
|
+
file), so it rides the file's ``FileSubgraph`` + feat-004 incrementality.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from functools import cache
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from tree_sitter import Node as TSNode
|
|
18
|
+
from tree_sitter import Parser, Query, QueryCursor
|
|
19
|
+
|
|
20
|
+
from agentforge_graph.core import (
|
|
21
|
+
Edge,
|
|
22
|
+
EdgeKind,
|
|
23
|
+
NodeKind,
|
|
24
|
+
Provenance,
|
|
25
|
+
SourceFile,
|
|
26
|
+
SymbolID,
|
|
27
|
+
)
|
|
28
|
+
from agentforge_graph.core import Node as GraphNode
|
|
29
|
+
from agentforge_graph.frameworks.base import FrameworkFacts, FrameworkPack
|
|
30
|
+
from agentforge_graph.frameworks.packs._python_ast import (
|
|
31
|
+
enclosing_class,
|
|
32
|
+
first_string_in,
|
|
33
|
+
member_descriptor,
|
|
34
|
+
python_language,
|
|
35
|
+
string_list_kwarg,
|
|
36
|
+
text,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
_HERE = Path(__file__).parent
|
|
40
|
+
_HTTP_METHODS = {"get", "post", "put", "delete", "patch", "options", "head"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@cache
|
|
44
|
+
def _query_text() -> str:
|
|
45
|
+
return (_HERE / "routes.scm").read_text(encoding="utf-8")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class FlaskPack(FrameworkPack):
|
|
49
|
+
name = "flask"
|
|
50
|
+
language = "python"
|
|
51
|
+
language_slug = "py" # SymbolID slug — must match the Python language pack
|
|
52
|
+
version = "1"
|
|
53
|
+
dep_names = ("flask",)
|
|
54
|
+
import_markers = ("import flask", "from flask")
|
|
55
|
+
|
|
56
|
+
def extract(self, file: SourceFile, repo: str, commit: str) -> FrameworkFacts:
|
|
57
|
+
src = file.text.encode("utf-8")
|
|
58
|
+
root = Parser(python_language()).parse(src).root_node
|
|
59
|
+
query = Query(python_language(), _query_text())
|
|
60
|
+
prov = Provenance.parsed(f"pack:{self.name}@{self.version}", commit)
|
|
61
|
+
facts = FrameworkFacts()
|
|
62
|
+
seen: set[str] = set()
|
|
63
|
+
|
|
64
|
+
for _pattern, caps in QueryCursor(query).matches(root):
|
|
65
|
+
method_caps = caps.get("method")
|
|
66
|
+
handler_caps = caps.get("handler")
|
|
67
|
+
args_caps = caps.get("args")
|
|
68
|
+
if not (method_caps and handler_caps and args_caps):
|
|
69
|
+
continue
|
|
70
|
+
decorator = text(method_caps[0], src).lower()
|
|
71
|
+
methods = self._methods_for(decorator, args_caps[0], src)
|
|
72
|
+
if methods is None:
|
|
73
|
+
continue # not a route decorator (e.g. @app.before_request)
|
|
74
|
+
|
|
75
|
+
path = first_string_in(args_caps[0], src)
|
|
76
|
+
if path is None:
|
|
77
|
+
facts.unresolved += 1 # dynamic / non-literal path
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
handler = text(handler_caps[0], src)
|
|
81
|
+
handler_id = SymbolID.for_symbol(
|
|
82
|
+
self.language_slug,
|
|
83
|
+
repo,
|
|
84
|
+
file.path,
|
|
85
|
+
member_descriptor(handler, enclosing_class(handler_caps[0], src)),
|
|
86
|
+
)
|
|
87
|
+
route_node = caps["route"][0]
|
|
88
|
+
for method in methods:
|
|
89
|
+
self._emit_route(
|
|
90
|
+
method, path, handler_id, route_node, repo, file, prov, facts, seen
|
|
91
|
+
)
|
|
92
|
+
return facts
|
|
93
|
+
|
|
94
|
+
def _methods_for(self, decorator: str, args: TSNode, src: bytes) -> list[str] | None:
|
|
95
|
+
"""The HTTP methods a decorator declares: the ``methods=[...]`` kwarg for
|
|
96
|
+
``@app.route`` (default ``GET``), the verb itself for ``@app.get`` …, or
|
|
97
|
+
None when the decorator is not a route."""
|
|
98
|
+
if decorator == "route":
|
|
99
|
+
listed = [m.upper() for m in string_list_kwarg(args, "methods", src)]
|
|
100
|
+
return listed or ["GET"]
|
|
101
|
+
if decorator in _HTTP_METHODS:
|
|
102
|
+
return [decorator.upper()]
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def _emit_route(
|
|
106
|
+
self,
|
|
107
|
+
method: str,
|
|
108
|
+
path: str,
|
|
109
|
+
handler_id: str,
|
|
110
|
+
route_node: TSNode,
|
|
111
|
+
repo: str,
|
|
112
|
+
file: SourceFile,
|
|
113
|
+
prov: Provenance,
|
|
114
|
+
facts: FrameworkFacts,
|
|
115
|
+
seen: set[str],
|
|
116
|
+
) -> None:
|
|
117
|
+
route_id = SymbolID.for_symbol(
|
|
118
|
+
self.language_slug, repo, file.path, f"route({method} {path})."
|
|
119
|
+
)
|
|
120
|
+
if route_id in seen:
|
|
121
|
+
return
|
|
122
|
+
seen.add(route_id)
|
|
123
|
+
facts.nodes.append(
|
|
124
|
+
GraphNode(
|
|
125
|
+
id=route_id,
|
|
126
|
+
kind=NodeKind.ROUTE,
|
|
127
|
+
name=f"{method} {path}",
|
|
128
|
+
span=(route_node.start_point[0] + 1, route_node.end_point[0] + 1),
|
|
129
|
+
attrs={
|
|
130
|
+
"method": method,
|
|
131
|
+
"path": path,
|
|
132
|
+
"framework": self.name,
|
|
133
|
+
"handler": handler_id,
|
|
134
|
+
},
|
|
135
|
+
provenance=prov,
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
facts.edges.append(
|
|
139
|
+
Edge(src=route_id, dst=handler_id, kind=EdgeKind.HANDLED_BY, provenance=prov)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
FLASK_PACK = FlaskPack()
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
; Flask route decorators (feat-011). Matches `@app.route("/x", methods=[...])`,
|
|
2
|
+
; blueprint `@bp.route(...)`, and the Flask 2.0 shortcuts `@app.get("/x")` etc.
|
|
3
|
+
; on a function. The decorator attribute (route/get/post/…) and the path/methods
|
|
4
|
+
; are validated in code so non-route decorators (`@app.before_request`) and
|
|
5
|
+
; dynamic paths are handled, not silently missed.
|
|
6
|
+
(decorated_definition
|
|
7
|
+
(decorator
|
|
8
|
+
(call
|
|
9
|
+
function: (attribute object: (_) @app attribute: (identifier) @method)
|
|
10
|
+
arguments: (argument_list) @args))
|
|
11
|
+
definition: (function_definition name: (identifier) @handler)) @route
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""NestJS framework pack (feat-011) — controller routes (TypeScript).
|
|
2
|
+
|
|
3
|
+
Extracts NestJS endpoints into ``Route`` nodes + ``HANDLED_BY`` edges. A class is
|
|
4
|
+
a route source only when it carries an ``@Controller`` decorator (ADR-0004); its
|
|
5
|
+
optional ``@Controller('base')`` argument is the base path. Each method decorated
|
|
6
|
+
with ``@Get`` / ``@Post`` / ``@Put`` / ``@Delete`` / ``@Patch`` / ``@All``
|
|
7
|
+
becomes a ``Route`` whose path is the base joined with the decorator's path
|
|
8
|
+
argument, handled by the ``Class#method`` symbol. TypeScript decorators are
|
|
9
|
+
preceding siblings of the node they annotate, so the pack reads the class's
|
|
10
|
+
preceding decorators and walks the class body in order.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from functools import cache
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from tree_sitter import Node as TSNode
|
|
19
|
+
from tree_sitter import Parser, Query, QueryCursor
|
|
20
|
+
|
|
21
|
+
from agentforge_graph.core import (
|
|
22
|
+
Descriptor,
|
|
23
|
+
Edge,
|
|
24
|
+
EdgeKind,
|
|
25
|
+
NodeKind,
|
|
26
|
+
Provenance,
|
|
27
|
+
SourceFile,
|
|
28
|
+
SymbolID,
|
|
29
|
+
)
|
|
30
|
+
from agentforge_graph.core import Node as GraphNode
|
|
31
|
+
from agentforge_graph.frameworks.base import FrameworkFacts, FrameworkPack
|
|
32
|
+
from agentforge_graph.frameworks.packs._js_ast import js_language, string_value, text
|
|
33
|
+
|
|
34
|
+
_HERE = Path(__file__).parent
|
|
35
|
+
_SLUG = "ts"
|
|
36
|
+
_MAPPING = {
|
|
37
|
+
"Get": "GET",
|
|
38
|
+
"Post": "POST",
|
|
39
|
+
"Put": "PUT",
|
|
40
|
+
"Delete": "DELETE",
|
|
41
|
+
"Patch": "PATCH",
|
|
42
|
+
"All": "ALL",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@cache
|
|
47
|
+
def _query_text() -> str:
|
|
48
|
+
return (_HERE / "routes.scm").read_text(encoding="utf-8")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _decorator_name_and_path(dec: TSNode, src: bytes) -> tuple[str, str]:
|
|
52
|
+
"""``(name, path)`` for a decorator: ``@Get(':id')`` -> ``("Get", ":id")``,
|
|
53
|
+
``@Get()`` / ``@Get`` -> ``("Get", "")``."""
|
|
54
|
+
child = dec.named_children[0] if dec.named_children else None
|
|
55
|
+
if child is None:
|
|
56
|
+
return "", ""
|
|
57
|
+
if child.type == "call_expression":
|
|
58
|
+
fn = child.child_by_field_name("function")
|
|
59
|
+
name = text(fn, src) if fn is not None else ""
|
|
60
|
+
args = child.child_by_field_name("arguments")
|
|
61
|
+
path = ""
|
|
62
|
+
if args is not None and args.named_children:
|
|
63
|
+
path = string_value(args.named_children[0], src) or ""
|
|
64
|
+
return name, path
|
|
65
|
+
if child.type == "identifier":
|
|
66
|
+
return text(child, src), ""
|
|
67
|
+
return "", ""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _preceding_decorators(node: TSNode, src: bytes) -> list[tuple[str, str]]:
|
|
71
|
+
"""The ``(name, path)`` of each decorator immediately preceding ``node``
|
|
72
|
+
(TS attaches decorators as previous siblings; an ``export`` keyword may sit
|
|
73
|
+
between)."""
|
|
74
|
+
out: list[tuple[str, str]] = []
|
|
75
|
+
sib = node.prev_sibling
|
|
76
|
+
while sib is not None:
|
|
77
|
+
if sib.type == "decorator":
|
|
78
|
+
out.append(_decorator_name_and_path(sib, src))
|
|
79
|
+
elif sib.is_named:
|
|
80
|
+
break # a real preceding statement ends the decorator run
|
|
81
|
+
sib = sib.prev_sibling
|
|
82
|
+
return out
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _join(base: str, path: str) -> str:
|
|
86
|
+
"""Join NestJS path segments (which omit leading slashes) into one absolute
|
|
87
|
+
pattern: ``users`` + ``:id`` -> ``/users/:id``."""
|
|
88
|
+
segments = [s.strip("/") for s in (base, path) if s.strip("/")]
|
|
89
|
+
return "/" + "/".join(segments) if segments else "/"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class NestJSPack(FrameworkPack):
|
|
93
|
+
name = "nestjs"
|
|
94
|
+
language = "typescript"
|
|
95
|
+
language_slug = _SLUG # SymbolID slug — must match the TypeScript language pack
|
|
96
|
+
version = "1"
|
|
97
|
+
dep_names = ("@nestjs/common", "@nestjs/core")
|
|
98
|
+
import_markers = ("@nestjs/common", "from '@nestjs", 'from "@nestjs')
|
|
99
|
+
|
|
100
|
+
def extract(self, file: SourceFile, repo: str, commit: str) -> FrameworkFacts:
|
|
101
|
+
src = file.text.encode("utf-8")
|
|
102
|
+
lang = js_language(_SLUG)
|
|
103
|
+
root = Parser(lang).parse(src).root_node
|
|
104
|
+
query = Query(lang, _query_text())
|
|
105
|
+
prov = Provenance.parsed(f"pack:{self.name}@{self.version}", commit)
|
|
106
|
+
facts = FrameworkFacts()
|
|
107
|
+
seen: set[str] = set()
|
|
108
|
+
|
|
109
|
+
for _pattern, caps in QueryCursor(query).matches(root):
|
|
110
|
+
class_caps = caps.get("decl")
|
|
111
|
+
name_caps = caps.get("class")
|
|
112
|
+
if not (class_caps and name_caps):
|
|
113
|
+
continue
|
|
114
|
+
self._extract_controller(
|
|
115
|
+
class_caps[0], text(name_caps[0], src), src, repo, file, prov, facts, seen
|
|
116
|
+
)
|
|
117
|
+
return facts
|
|
118
|
+
|
|
119
|
+
def _extract_controller(
|
|
120
|
+
self,
|
|
121
|
+
class_node: TSNode,
|
|
122
|
+
class_name: str,
|
|
123
|
+
src: bytes,
|
|
124
|
+
repo: str,
|
|
125
|
+
file: SourceFile,
|
|
126
|
+
prov: Provenance,
|
|
127
|
+
facts: FrameworkFacts,
|
|
128
|
+
seen: set[str],
|
|
129
|
+
) -> None:
|
|
130
|
+
class_decos = _preceding_decorators(class_node, src)
|
|
131
|
+
controller = next((d for d in class_decos if d[0] == "Controller"), None)
|
|
132
|
+
if controller is None:
|
|
133
|
+
return # not a controller -> not a route source (ADR-0004)
|
|
134
|
+
base_path = controller[1]
|
|
135
|
+
|
|
136
|
+
body = class_node.child_by_field_name("body")
|
|
137
|
+
if body is None:
|
|
138
|
+
return
|
|
139
|
+
pending: list[tuple[str, str]] = []
|
|
140
|
+
for member in body.named_children:
|
|
141
|
+
if member.type == "decorator":
|
|
142
|
+
pending.append(_decorator_name_and_path(member, src))
|
|
143
|
+
continue
|
|
144
|
+
if member.type == "method_definition":
|
|
145
|
+
name_node = member.child_by_field_name("name")
|
|
146
|
+
if name_node is not None:
|
|
147
|
+
for deco_name, deco_path in pending:
|
|
148
|
+
verb = _MAPPING.get(deco_name)
|
|
149
|
+
if verb is None:
|
|
150
|
+
continue
|
|
151
|
+
self._emit_route(
|
|
152
|
+
verb,
|
|
153
|
+
_join(base_path, deco_path),
|
|
154
|
+
class_name,
|
|
155
|
+
text(name_node, src),
|
|
156
|
+
member,
|
|
157
|
+
repo,
|
|
158
|
+
file,
|
|
159
|
+
prov,
|
|
160
|
+
facts,
|
|
161
|
+
seen,
|
|
162
|
+
)
|
|
163
|
+
pending = []
|
|
164
|
+
|
|
165
|
+
def _emit_route(
|
|
166
|
+
self,
|
|
167
|
+
method: str,
|
|
168
|
+
path: str,
|
|
169
|
+
class_name: str,
|
|
170
|
+
method_name: str,
|
|
171
|
+
member: TSNode,
|
|
172
|
+
repo: str,
|
|
173
|
+
file: SourceFile,
|
|
174
|
+
prov: Provenance,
|
|
175
|
+
facts: FrameworkFacts,
|
|
176
|
+
seen: set[str],
|
|
177
|
+
) -> None:
|
|
178
|
+
handler_id = SymbolID.for_symbol(
|
|
179
|
+
_SLUG, repo, file.path, Descriptor.type(class_name) + Descriptor.method(method_name)
|
|
180
|
+
)
|
|
181
|
+
route_id = SymbolID.for_symbol(_SLUG, repo, file.path, f"route({method} {path}).")
|
|
182
|
+
if route_id in seen:
|
|
183
|
+
return
|
|
184
|
+
seen.add(route_id)
|
|
185
|
+
facts.nodes.append(
|
|
186
|
+
GraphNode(
|
|
187
|
+
id=route_id,
|
|
188
|
+
kind=NodeKind.ROUTE,
|
|
189
|
+
name=f"{method} {path}",
|
|
190
|
+
span=(member.start_point[0] + 1, member.end_point[0] + 1),
|
|
191
|
+
attrs={
|
|
192
|
+
"method": method,
|
|
193
|
+
"path": path,
|
|
194
|
+
"framework": self.name,
|
|
195
|
+
"handler": handler_id,
|
|
196
|
+
},
|
|
197
|
+
provenance=prov,
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
facts.edges.append(
|
|
201
|
+
Edge(src=route_id, dst=handler_id, kind=EdgeKind.HANDLED_BY, provenance=prov)
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
NESTJS_PACK = NestJSPack()
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
; NestJS controllers (feat-011). Capture every class; the pack inspects each
|
|
2
|
+
; class's decorators (`@Controller('base')`) and its methods' decorators
|
|
3
|
+
; (`@Get(':id')` …) in code, since TypeScript decorators are preceding siblings
|
|
4
|
+
; of the node they annotate (not children) — so ordered traversal stays precise.
|
|
5
|
+
(class_declaration
|
|
6
|
+
name: (type_identifier) @class) @decl
|