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.
Files changed (151) hide show
  1. agentforge_graph/__init__.py +6 -0
  2. agentforge_graph/chunking/__init__.py +12 -0
  3. agentforge_graph/chunking/cast.py +159 -0
  4. agentforge_graph/chunking/chunk.py +19 -0
  5. agentforge_graph/chunking/tokens.py +15 -0
  6. agentforge_graph/cli.py +607 -0
  7. agentforge_graph/config.py +259 -0
  8. agentforge_graph/core/__init__.py +54 -0
  9. agentforge_graph/core/conformance.py +270 -0
  10. agentforge_graph/core/contracts.py +163 -0
  11. agentforge_graph/core/kinds.py +68 -0
  12. agentforge_graph/core/models.py +134 -0
  13. agentforge_graph/core/provenance.py +62 -0
  14. agentforge_graph/core/symbols.py +116 -0
  15. agentforge_graph/embed/__init__.py +28 -0
  16. agentforge_graph/embed/base.py +22 -0
  17. agentforge_graph/embed/bedrock.py +85 -0
  18. agentforge_graph/embed/fake.py +34 -0
  19. agentforge_graph/embed/openai.py +67 -0
  20. agentforge_graph/embed/pipeline.py +184 -0
  21. agentforge_graph/embed/registry.py +66 -0
  22. agentforge_graph/embed/report.py +15 -0
  23. agentforge_graph/enrich/__init__.py +70 -0
  24. agentforge_graph/enrich/anthropic.py +38 -0
  25. agentforge_graph/enrich/anthropic_client.py +109 -0
  26. agentforge_graph/enrich/bedrock.py +24 -0
  27. agentforge_graph/enrich/bedrock_client.py +115 -0
  28. agentforge_graph/enrich/bedrock_summarizer.py +23 -0
  29. agentforge_graph/enrich/claude.py +172 -0
  30. agentforge_graph/enrich/enricher.py +108 -0
  31. agentforge_graph/enrich/governs.py +173 -0
  32. agentforge_graph/enrich/governs_enricher.py +152 -0
  33. agentforge_graph/enrich/heuristics.py +224 -0
  34. agentforge_graph/enrich/judge.py +63 -0
  35. agentforge_graph/enrich/registry.py +133 -0
  36. agentforge_graph/enrich/report.py +60 -0
  37. agentforge_graph/enrich/summarizer.py +62 -0
  38. agentforge_graph/enrich/summary_enricher.py +211 -0
  39. agentforge_graph/enrich/taxonomy.py +38 -0
  40. agentforge_graph/frameworks/__init__.py +29 -0
  41. agentforge_graph/frameworks/base.py +75 -0
  42. agentforge_graph/frameworks/detect.py +124 -0
  43. agentforge_graph/frameworks/extractor.py +63 -0
  44. agentforge_graph/frameworks/orm.py +93 -0
  45. agentforge_graph/frameworks/packs/_js_ast.py +56 -0
  46. agentforge_graph/frameworks/packs/_python_ast.py +157 -0
  47. agentforge_graph/frameworks/packs/django/__init__.py +240 -0
  48. agentforge_graph/frameworks/packs/django/models.scm +7 -0
  49. agentforge_graph/frameworks/packs/express/__init__.py +133 -0
  50. agentforge_graph/frameworks/packs/express/routes.scm +8 -0
  51. agentforge_graph/frameworks/packs/fastapi/__init__.py +210 -0
  52. agentforge_graph/frameworks/packs/fastapi/depends.scm +6 -0
  53. agentforge_graph/frameworks/packs/fastapi/routes.scm +10 -0
  54. agentforge_graph/frameworks/packs/flask/__init__.py +143 -0
  55. agentforge_graph/frameworks/packs/flask/routes.scm +11 -0
  56. agentforge_graph/frameworks/packs/nestjs/__init__.py +205 -0
  57. agentforge_graph/frameworks/packs/nestjs/routes.scm +6 -0
  58. agentforge_graph/frameworks/packs/spring/__init__.py +267 -0
  59. agentforge_graph/frameworks/packs/spring/routes.scm +6 -0
  60. agentforge_graph/frameworks/packs/sqlalchemy/__init__.py +250 -0
  61. agentforge_graph/frameworks/packs/sqlalchemy/models.scm +7 -0
  62. agentforge_graph/frameworks/registry.py +44 -0
  63. agentforge_graph/ingest/__init__.py +30 -0
  64. agentforge_graph/ingest/codegraph.py +847 -0
  65. agentforge_graph/ingest/extractor.py +353 -0
  66. agentforge_graph/ingest/incremental/__init__.py +25 -0
  67. agentforge_graph/ingest/incremental/detect.py +118 -0
  68. agentforge_graph/ingest/incremental/dirty.py +61 -0
  69. agentforge_graph/ingest/incremental/indexer.py +218 -0
  70. agentforge_graph/ingest/incremental/meta.py +72 -0
  71. agentforge_graph/ingest/incremental/ports.py +39 -0
  72. agentforge_graph/ingest/pack.py +160 -0
  73. agentforge_graph/ingest/packs/__init__.py +34 -0
  74. agentforge_graph/ingest/packs/cpp/__init__.py +35 -0
  75. agentforge_graph/ingest/packs/cpp/references.scm +15 -0
  76. agentforge_graph/ingest/packs/cpp/structure.scm +49 -0
  77. agentforge_graph/ingest/packs/csharp/__init__.py +35 -0
  78. agentforge_graph/ingest/packs/csharp/references.scm +12 -0
  79. agentforge_graph/ingest/packs/csharp/structure.scm +45 -0
  80. agentforge_graph/ingest/packs/go/__init__.py +38 -0
  81. agentforge_graph/ingest/packs/go/references.scm +12 -0
  82. agentforge_graph/ingest/packs/go/structure.scm +64 -0
  83. agentforge_graph/ingest/packs/java/__init__.py +35 -0
  84. agentforge_graph/ingest/packs/java/references.scm +12 -0
  85. agentforge_graph/ingest/packs/java/structure.scm +38 -0
  86. agentforge_graph/ingest/packs/javascript/__init__.py +34 -0
  87. agentforge_graph/ingest/packs/javascript/references.scm +11 -0
  88. agentforge_graph/ingest/packs/javascript/structure.scm +166 -0
  89. agentforge_graph/ingest/packs/php/__init__.py +35 -0
  90. agentforge_graph/ingest/packs/php/references.scm +15 -0
  91. agentforge_graph/ingest/packs/php/structure.scm +44 -0
  92. agentforge_graph/ingest/packs/python/__init__.py +25 -0
  93. agentforge_graph/ingest/packs/python/references.scm +14 -0
  94. agentforge_graph/ingest/packs/python/structure.scm +57 -0
  95. agentforge_graph/ingest/packs/ruby/__init__.py +37 -0
  96. agentforge_graph/ingest/packs/ruby/references.scm +12 -0
  97. agentforge_graph/ingest/packs/ruby/structure.scm +37 -0
  98. agentforge_graph/ingest/packs/rust/__init__.py +39 -0
  99. agentforge_graph/ingest/packs/rust/references.scm +12 -0
  100. agentforge_graph/ingest/packs/rust/structure.scm +46 -0
  101. agentforge_graph/ingest/packs/typescript/__init__.py +31 -0
  102. agentforge_graph/ingest/packs/typescript/references.scm +11 -0
  103. agentforge_graph/ingest/packs/typescript/structure.scm +99 -0
  104. agentforge_graph/ingest/pipeline.py +134 -0
  105. agentforge_graph/ingest/report.py +84 -0
  106. agentforge_graph/ingest/resolver.py +467 -0
  107. agentforge_graph/ingest/source.py +79 -0
  108. agentforge_graph/knowledge/__init__.py +28 -0
  109. agentforge_graph/knowledge/adr.py +136 -0
  110. agentforge_graph/knowledge/commits.py +152 -0
  111. agentforge_graph/knowledge/ingest.py +312 -0
  112. agentforge_graph/knowledge/mentions.py +71 -0
  113. agentforge_graph/knowledge/report.py +32 -0
  114. agentforge_graph/main.py +21 -0
  115. agentforge_graph/providers.py +36 -0
  116. agentforge_graph/repomap/__init__.py +14 -0
  117. agentforge_graph/repomap/rank.py +161 -0
  118. agentforge_graph/repomap/render.py +55 -0
  119. agentforge_graph/repomap/repomap.py +66 -0
  120. agentforge_graph/retrieve/__init__.py +21 -0
  121. agentforge_graph/retrieve/pack.py +76 -0
  122. agentforge_graph/retrieve/rerank.py +251 -0
  123. agentforge_graph/retrieve/retriever.py +286 -0
  124. agentforge_graph/retrieve/scoring.py +36 -0
  125. agentforge_graph/serve/__init__.py +19 -0
  126. agentforge_graph/serve/engine.py +204 -0
  127. agentforge_graph/serve/http_runner.py +133 -0
  128. agentforge_graph/serve/server.py +110 -0
  129. agentforge_graph/serve/tools.py +307 -0
  130. agentforge_graph/store/__init__.py +32 -0
  131. agentforge_graph/store/_rowmap.py +102 -0
  132. agentforge_graph/store/errors.py +22 -0
  133. agentforge_graph/store/facade.py +89 -0
  134. agentforge_graph/store/kuzu_store.py +380 -0
  135. agentforge_graph/store/lance_store.py +146 -0
  136. agentforge_graph/store/neo4j_store.py +294 -0
  137. agentforge_graph/store/pgvector_store.py +170 -0
  138. agentforge_graph/store/registry.py +45 -0
  139. agentforge_graph/temporal/__init__.py +36 -0
  140. agentforge_graph/temporal/backfill.py +338 -0
  141. agentforge_graph/temporal/events.py +82 -0
  142. agentforge_graph/temporal/index.py +190 -0
  143. agentforge_graph/temporal/mining.py +190 -0
  144. agentforge_graph/temporal/recorder.py +114 -0
  145. agentforge_graph/temporal/store.py +282 -0
  146. agentforge_graph-0.3.2.dist-info/METADATA +291 -0
  147. agentforge_graph-0.3.2.dist-info/RECORD +151 -0
  148. agentforge_graph-0.3.2.dist-info/WHEEL +4 -0
  149. agentforge_graph-0.3.2.dist-info/entry_points.txt +3 -0
  150. agentforge_graph-0.3.2.dist-info/licenses/LICENSE +202 -0
  151. 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