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,56 @@
|
|
|
1
|
+
"""Shared tree-sitter helpers for JS/TS framework packs (feat-011).
|
|
2
|
+
|
|
3
|
+
Framework-agnostic primitives over the JavaScript and TypeScript grammars (which
|
|
4
|
+
share the node types these touch). Used by the Express pack and any future JS/TS
|
|
5
|
+
pack (NestJS). Zero ``agentforge`` imports beyond tree-sitter.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from functools import cache
|
|
11
|
+
|
|
12
|
+
from tree_sitter import Language
|
|
13
|
+
from tree_sitter import Node as TSNode
|
|
14
|
+
from tree_sitter_language_pack import get_language
|
|
15
|
+
|
|
16
|
+
# SymbolID slug -> tree-sitter grammar name.
|
|
17
|
+
_GRAMMAR = {"js": "javascript", "ts": "typescript"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@cache
|
|
21
|
+
def js_language(slug: str) -> Language:
|
|
22
|
+
"""The tree-sitter ``Language`` for a JS/TS SymbolID slug (``js``/``ts``)."""
|
|
23
|
+
return get_language(_GRAMMAR[slug])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def text(node: TSNode, src: bytes) -> str:
|
|
27
|
+
return src[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def strip_quotes(s: str) -> str:
|
|
31
|
+
if len(s) >= 2 and s[0] in "\"'`" and s[-1] == s[0]:
|
|
32
|
+
return s[1:-1]
|
|
33
|
+
return s
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def string_value(node: TSNode, src: bytes) -> str | None:
|
|
37
|
+
"""The literal value of a ``string`` node (quotes stripped), or None when the
|
|
38
|
+
node is not a plain string literal."""
|
|
39
|
+
if node.type != "string":
|
|
40
|
+
return None
|
|
41
|
+
return strip_quotes(text(node, src))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def first_arg_string(args: TSNode, src: bytes) -> str | None:
|
|
45
|
+
"""The first argument's string-literal value, or None when it is missing or
|
|
46
|
+
non-literal (a computed/templated path)."""
|
|
47
|
+
if not args.named_children:
|
|
48
|
+
return None
|
|
49
|
+
return string_value(args.named_children[0], src)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def last_named_arg(args: TSNode) -> TSNode | None:
|
|
53
|
+
"""The last argument node — Express's route handler (a function reference or
|
|
54
|
+
an inline function), after any middleware arguments."""
|
|
55
|
+
kids = args.named_children
|
|
56
|
+
return kids[-1] if kids else None
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Shared tree-sitter helpers for Python framework packs (feat-011).
|
|
2
|
+
|
|
3
|
+
Framework-agnostic primitives over the Python grammar — used by the SQLAlchemy
|
|
4
|
+
and Django ORM packs (and any future Python pack: Flask, FastAPI class views).
|
|
5
|
+
Zero ``agentforge`` imports beyond tree-sitter; no framework semantics here.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
from functools import cache
|
|
12
|
+
|
|
13
|
+
from tree_sitter import Language
|
|
14
|
+
from tree_sitter import Node as TSNode
|
|
15
|
+
from tree_sitter_language_pack import get_language
|
|
16
|
+
|
|
17
|
+
from agentforge_graph.core import Descriptor
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@cache
|
|
21
|
+
def python_language() -> Language:
|
|
22
|
+
return get_language("python")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def text(node: TSNode, src: bytes) -> str:
|
|
26
|
+
return src[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def strip_quotes(s: str) -> str:
|
|
30
|
+
if len(s) >= 2 and s[0] in "\"'" and s[-1] == s[0]:
|
|
31
|
+
return s[1:-1]
|
|
32
|
+
return s
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def callee_name(call: TSNode, src: bytes) -> str:
|
|
36
|
+
"""Last segment of a call's function name: ``Column`` for both ``Column(...)``
|
|
37
|
+
and ``sa.Column(...)`` / ``models.ForeignKey(...)``."""
|
|
38
|
+
fn = call.child_by_field_name("function")
|
|
39
|
+
if fn is None:
|
|
40
|
+
return ""
|
|
41
|
+
if fn.type == "attribute":
|
|
42
|
+
attr = fn.child_by_field_name("attribute")
|
|
43
|
+
return text(attr, src) if attr is not None else ""
|
|
44
|
+
return text(fn, src)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def class_body(class_node: TSNode) -> TSNode | None:
|
|
48
|
+
return class_node.child_by_field_name("body")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def dotted_tail(node: TSNode, src: bytes) -> str:
|
|
52
|
+
"""Last segment of an identifier or attribute node — ``Model`` for both
|
|
53
|
+
``Model`` and ``models.Model`` / ``db.models.Model``; ``User`` for
|
|
54
|
+
``myapp.User``. "" for anything else."""
|
|
55
|
+
if node.type == "attribute":
|
|
56
|
+
attr = node.child_by_field_name("attribute")
|
|
57
|
+
return text(attr, src) if attr is not None else ""
|
|
58
|
+
if node.type == "identifier":
|
|
59
|
+
return text(node, src)
|
|
60
|
+
return ""
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def base_classes(class_node: TSNode, src: bytes) -> list[str]:
|
|
64
|
+
"""The dotted-tail names of a class's base classes: ``["Model"]`` for
|
|
65
|
+
``class User(models.Model)``, ``["TimestampedModel"]`` for a subclass."""
|
|
66
|
+
supers = class_node.child_by_field_name("superclasses")
|
|
67
|
+
if supers is None:
|
|
68
|
+
return []
|
|
69
|
+
return [t for c in supers.named_children if (t := dotted_tail(c, src))]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def iter_class_assignments(body: TSNode, src: bytes) -> Iterator[tuple[str, TSNode, TSNode]]:
|
|
73
|
+
"""Yield ``(lhs_name, assignment_node, rhs_node)`` for each class-level
|
|
74
|
+
``name = <expr>``. The block may hold the assignment directly or wrapped in
|
|
75
|
+
an ``expression_statement``; non-identifier LHS is skipped."""
|
|
76
|
+
for stmt in body.named_children:
|
|
77
|
+
if stmt.type == "assignment":
|
|
78
|
+
assign = stmt
|
|
79
|
+
elif stmt.type == "expression_statement" and stmt.named_children:
|
|
80
|
+
assign = stmt.named_children[0]
|
|
81
|
+
if assign.type != "assignment":
|
|
82
|
+
continue
|
|
83
|
+
else:
|
|
84
|
+
continue
|
|
85
|
+
left = assign.child_by_field_name("left")
|
|
86
|
+
right = assign.child_by_field_name("right")
|
|
87
|
+
if left is None or left.type != "identifier" or right is None:
|
|
88
|
+
continue
|
|
89
|
+
yield text(left, src), assign, right
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def first_string_arg(call: TSNode, src: bytes) -> str:
|
|
93
|
+
"""The first string-literal positional arg, stripped (``"Post"`` in
|
|
94
|
+
``relationship("Post")``); "" when there is no string positional."""
|
|
95
|
+
args = call.child_by_field_name("arguments")
|
|
96
|
+
if args is None:
|
|
97
|
+
return ""
|
|
98
|
+
for arg in args.named_children:
|
|
99
|
+
if arg.type == "string":
|
|
100
|
+
return strip_quotes(text(arg, src))
|
|
101
|
+
return ""
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def enclosing_class(node: TSNode, src: bytes) -> str | None:
|
|
105
|
+
"""The name of the nearest enclosing ``class_definition``, or None for a
|
|
106
|
+
module-level definition — lets a class-based handler/consumer resolve to its
|
|
107
|
+
``Class#method`` symbol."""
|
|
108
|
+
anc = node.parent
|
|
109
|
+
while anc is not None:
|
|
110
|
+
if anc.type == "class_definition":
|
|
111
|
+
name = anc.child_by_field_name("name")
|
|
112
|
+
return text(name, src) if name is not None else None
|
|
113
|
+
anc = anc.parent
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def member_descriptor(name: str, enclosing: str | None) -> str:
|
|
118
|
+
"""``Class#method().`` for a method, ``method().`` for a free function."""
|
|
119
|
+
if enclosing is not None:
|
|
120
|
+
return Descriptor.type(enclosing) + Descriptor.method(name)
|
|
121
|
+
return Descriptor.method(name)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def first_string_in(args: TSNode, src: bytes) -> str | None:
|
|
125
|
+
"""The first string-literal positional arg (a route path), or None when the
|
|
126
|
+
arg is dynamic/non-literal."""
|
|
127
|
+
for child in args.named_children:
|
|
128
|
+
if child.type == "string":
|
|
129
|
+
return strip_quotes(text(child, src))
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def string_list_kwarg(args: TSNode, name: str, src: bytes) -> list[str]:
|
|
134
|
+
"""The string elements of a ``name=[...]`` keyword argument (e.g. Flask's
|
|
135
|
+
``methods=["GET", "POST"]``), in order; [] when absent or non-literal."""
|
|
136
|
+
for arg in args.named_children:
|
|
137
|
+
if arg.type != "keyword_argument":
|
|
138
|
+
continue
|
|
139
|
+
key = arg.child_by_field_name("name")
|
|
140
|
+
value = arg.child_by_field_name("value")
|
|
141
|
+
if key is None or text(key, src) != name or value is None or value.type != "list":
|
|
142
|
+
continue
|
|
143
|
+
return [strip_quotes(text(e, src)) for e in value.named_children if e.type == "string"]
|
|
144
|
+
return []
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def first_positional_arg(call: TSNode, src: bytes) -> TSNode | None:
|
|
148
|
+
"""The first non-keyword argument node, or None — the target in
|
|
149
|
+
``ForeignKey(User)`` / ``ForeignKey("app.User")`` / ``CharField(...)``."""
|
|
150
|
+
args = call.child_by_field_name("arguments")
|
|
151
|
+
if args is None:
|
|
152
|
+
return None
|
|
153
|
+
for arg in args.named_children:
|
|
154
|
+
if arg.type == "keyword_argument":
|
|
155
|
+
continue
|
|
156
|
+
return arg
|
|
157
|
+
return None
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""Django framework pack (feat-011) — ORM models.
|
|
2
|
+
|
|
3
|
+
Extracts Django model classes into ``DataModel`` nodes + ``HAS_FIELD`` edges to
|
|
4
|
+
each mapped field (a ``Variable`` carrying its field type, e.g. ``CharField``).
|
|
5
|
+
A class is treated as a model only with declarative evidence — a base whose
|
|
6
|
+
tail is ``Model`` (``class X(models.Model)``) or at least one ``models.*Field``
|
|
7
|
+
assignment (which also catches abstract-base subclasses) — so plain classes in
|
|
8
|
+
a Django app never mint false models (ADR-0004).
|
|
9
|
+
|
|
10
|
+
``ForeignKey`` / ``OneToOneField`` / ``ManyToManyField`` targets (a model class,
|
|
11
|
+
a ``"app.Model"`` string, or ``"self"``) are recorded in pass-1 and stitched
|
|
12
|
+
into cross-file ``RELATES_TO`` edges in pass-2 against the whole-repo model set
|
|
13
|
+
(unique-match-only). FK/O2O are also a ``HAS_FIELD`` column; M2M is relation-
|
|
14
|
+
only. The table name comes from ``class Meta: db_table`` when set (Django's
|
|
15
|
+
``app_label``-derived default is not known statically).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from functools import cache
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
from tree_sitter import Node as TSNode
|
|
24
|
+
from tree_sitter import Parser, Query, QueryCursor
|
|
25
|
+
|
|
26
|
+
from agentforge_graph.core import (
|
|
27
|
+
Descriptor,
|
|
28
|
+
Edge,
|
|
29
|
+
EdgeKind,
|
|
30
|
+
GraphStore,
|
|
31
|
+
NodeKind,
|
|
32
|
+
Provenance,
|
|
33
|
+
SourceFile,
|
|
34
|
+
SymbolID,
|
|
35
|
+
)
|
|
36
|
+
from agentforge_graph.core import Node as GraphNode
|
|
37
|
+
from agentforge_graph.frameworks.base import FrameworkFacts, FrameworkPack
|
|
38
|
+
from agentforge_graph.frameworks.orm import (
|
|
39
|
+
ModelIndex,
|
|
40
|
+
framework_models,
|
|
41
|
+
relations_to_edges,
|
|
42
|
+
)
|
|
43
|
+
from agentforge_graph.frameworks.packs._python_ast import (
|
|
44
|
+
base_classes,
|
|
45
|
+
callee_name,
|
|
46
|
+
class_body,
|
|
47
|
+
dotted_tail,
|
|
48
|
+
first_positional_arg,
|
|
49
|
+
iter_class_assignments,
|
|
50
|
+
python_language,
|
|
51
|
+
strip_quotes,
|
|
52
|
+
text,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
_HERE = Path(__file__).parent
|
|
56
|
+
# relation field types -> RELATES_TO kind; FK/O2O also produce a column. Note
|
|
57
|
+
# `ForeignKey` does NOT end in `Field`, so relation calls are matched by name.
|
|
58
|
+
_RELATION_FIELDS = {"ForeignKey": "fk", "OneToOneField": "o2o", "ManyToManyField": "m2m"}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@cache
|
|
62
|
+
def _query_text() -> str:
|
|
63
|
+
return (_HERE / "models.scm").read_text(encoding="utf-8")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _is_field_call(callee: str) -> bool:
|
|
67
|
+
"""A Django model field: any ``*Field`` class plus the relation classes
|
|
68
|
+
(``ForeignKey`` ends in ``Key``, not ``Field``)."""
|
|
69
|
+
return callee.endswith("Field") or callee in _RELATION_FIELDS
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_models_namespaced_field(call: TSNode, src: bytes) -> bool:
|
|
73
|
+
"""True for ``models.CharField(...)`` / ``models.ForeignKey(...)`` — a field
|
|
74
|
+
call whose receiver tail is ``models``. The namespace check keeps a non-Django
|
|
75
|
+
``SomeField()`` from looking like model evidence."""
|
|
76
|
+
if not _is_field_call(callee_name(call, src)):
|
|
77
|
+
return False
|
|
78
|
+
fn = call.child_by_field_name("function")
|
|
79
|
+
if fn is None or fn.type != "attribute":
|
|
80
|
+
return False
|
|
81
|
+
obj = fn.child_by_field_name("object")
|
|
82
|
+
return obj is not None and dotted_tail(obj, src) == "models"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _relation_target(call: TSNode, src: bytes, self_name: str) -> str:
|
|
86
|
+
"""The related model name from a relation field's first positional arg:
|
|
87
|
+
``User`` for ``ForeignKey(User)`` / ``ForeignKey("app.User")``; the class's
|
|
88
|
+
own name for ``"self"``; "" when non-literal/absent."""
|
|
89
|
+
arg = first_positional_arg(call, src)
|
|
90
|
+
if arg is None:
|
|
91
|
+
return ""
|
|
92
|
+
if arg.type == "string":
|
|
93
|
+
target = strip_quotes(text(arg, src)).rsplit(".", 1)[-1]
|
|
94
|
+
return self_name if target == "self" else target
|
|
95
|
+
return dotted_tail(arg, src)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _meta_db_table(body: TSNode, src: bytes) -> str | None:
|
|
99
|
+
"""``db_table`` from an inner ``class Meta``, or None."""
|
|
100
|
+
for stmt in body.named_children:
|
|
101
|
+
if stmt.type != "class_definition":
|
|
102
|
+
continue
|
|
103
|
+
name = stmt.child_by_field_name("name")
|
|
104
|
+
if name is None or text(name, src) != "Meta":
|
|
105
|
+
continue
|
|
106
|
+
meta_body = class_body(stmt)
|
|
107
|
+
if meta_body is None:
|
|
108
|
+
continue
|
|
109
|
+
for fname, _assign, right in iter_class_assignments(meta_body, src):
|
|
110
|
+
if fname == "db_table" and right.type == "string":
|
|
111
|
+
return strip_quotes(text(right, src))
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class DjangoPack(FrameworkPack):
|
|
116
|
+
name = "django"
|
|
117
|
+
language = "python"
|
|
118
|
+
language_slug = "py" # SymbolID slug — must match the Python language pack
|
|
119
|
+
version = "1"
|
|
120
|
+
dep_names = ("django",)
|
|
121
|
+
import_markers = ("import django", "from django")
|
|
122
|
+
|
|
123
|
+
def extract(self, file: SourceFile, repo: str, commit: str) -> FrameworkFacts:
|
|
124
|
+
src = file.text.encode("utf-8")
|
|
125
|
+
root = Parser(python_language()).parse(src).root_node
|
|
126
|
+
query = Query(python_language(), _query_text())
|
|
127
|
+
prov = Provenance.parsed(f"pack:{self.name}@{self.version}", commit)
|
|
128
|
+
facts = FrameworkFacts()
|
|
129
|
+
|
|
130
|
+
for _pattern, caps in QueryCursor(query).matches(root):
|
|
131
|
+
class_caps = caps.get("model")
|
|
132
|
+
name_caps = caps.get("name")
|
|
133
|
+
if not (class_caps and name_caps):
|
|
134
|
+
continue
|
|
135
|
+
self._extract_model(
|
|
136
|
+
class_caps[0], text(name_caps[0], src), src, repo, file, prov, facts
|
|
137
|
+
)
|
|
138
|
+
return facts
|
|
139
|
+
|
|
140
|
+
def _extract_model(
|
|
141
|
+
self,
|
|
142
|
+
class_node: TSNode,
|
|
143
|
+
class_name: str,
|
|
144
|
+
src: bytes,
|
|
145
|
+
repo: str,
|
|
146
|
+
file: SourceFile,
|
|
147
|
+
prov: Provenance,
|
|
148
|
+
facts: FrameworkFacts,
|
|
149
|
+
) -> None:
|
|
150
|
+
body = class_body(class_node)
|
|
151
|
+
if body is None:
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
has_model_base = any(b == "Model" for b in base_classes(class_node, src))
|
|
155
|
+
fields: list[tuple[str, str, TSNode]] = [] # (name, field_type, assignment node)
|
|
156
|
+
relations: list[dict[str, str]] = []
|
|
157
|
+
has_models_field = False
|
|
158
|
+
for field_name, assign, right in iter_class_assignments(body, src):
|
|
159
|
+
if right.type != "call":
|
|
160
|
+
continue
|
|
161
|
+
callee = callee_name(right, src)
|
|
162
|
+
if not _is_field_call(callee):
|
|
163
|
+
continue
|
|
164
|
+
if _is_models_namespaced_field(right, src):
|
|
165
|
+
has_models_field = True
|
|
166
|
+
kind = _RELATION_FIELDS.get(callee)
|
|
167
|
+
if kind is not None:
|
|
168
|
+
target = _relation_target(right, src, class_name)
|
|
169
|
+
if target:
|
|
170
|
+
relations.append({"field": field_name, "target": target, "kind": kind})
|
|
171
|
+
if kind != "m2m": # FK/O2O carry a real column; M2M does not
|
|
172
|
+
fields.append((field_name, callee, assign))
|
|
173
|
+
else:
|
|
174
|
+
fields.append((field_name, callee, assign))
|
|
175
|
+
|
|
176
|
+
# Conservative: a class is a model only with declarative evidence.
|
|
177
|
+
if not (has_model_base or has_models_field):
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
table = _meta_db_table(body, src)
|
|
181
|
+
model_id = SymbolID.for_symbol(self.language_slug, repo, file.path, f"model({class_name}).")
|
|
182
|
+
class_id = SymbolID.for_symbol(
|
|
183
|
+
self.language_slug, repo, file.path, Descriptor.type(class_name)
|
|
184
|
+
)
|
|
185
|
+
attrs: dict[str, object] = {
|
|
186
|
+
"framework": self.name,
|
|
187
|
+
"class": class_id,
|
|
188
|
+
"model_class": class_name, # cross-file RELATES_TO target lookup (pass-2)
|
|
189
|
+
}
|
|
190
|
+
if table is not None:
|
|
191
|
+
attrs["table"] = table
|
|
192
|
+
if relations:
|
|
193
|
+
attrs["relations"] = relations
|
|
194
|
+
facts.nodes.append(
|
|
195
|
+
GraphNode(
|
|
196
|
+
id=model_id,
|
|
197
|
+
kind=NodeKind.DATA_MODEL,
|
|
198
|
+
name=table or class_name,
|
|
199
|
+
span=(class_node.start_point[0] + 1, class_node.end_point[0] + 1),
|
|
200
|
+
attrs=attrs,
|
|
201
|
+
provenance=prov,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
for field_name, field_type, assign in fields:
|
|
205
|
+
field_id = SymbolID.for_symbol(
|
|
206
|
+
self.language_slug,
|
|
207
|
+
repo,
|
|
208
|
+
file.path,
|
|
209
|
+
Descriptor.type(class_name) + Descriptor.term(field_name),
|
|
210
|
+
)
|
|
211
|
+
facts.nodes.append(
|
|
212
|
+
GraphNode(
|
|
213
|
+
id=field_id,
|
|
214
|
+
kind=NodeKind.VARIABLE,
|
|
215
|
+
name=field_name,
|
|
216
|
+
span=(assign.start_point[0] + 1, assign.end_point[0] + 1),
|
|
217
|
+
attrs={"column_type": field_type, "framework": self.name},
|
|
218
|
+
provenance=prov,
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
facts.edges.append(
|
|
222
|
+
Edge(src=model_id, dst=field_id, kind=EdgeKind.HAS_FIELD, provenance=prov)
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
async def resolve(self, store: GraphStore, commit: str = "") -> list[Edge]:
|
|
226
|
+
"""Pass-2: stitch ``ForeignKey``/``OneToOneField``/``ManyToManyField``
|
|
227
|
+
targets into ``RELATES_TO`` edges. Django targets are model class names,
|
|
228
|
+
so every relation resolves via the class index (unique match only)."""
|
|
229
|
+
models = await framework_models(store, self.name)
|
|
230
|
+
index = ModelIndex(models)
|
|
231
|
+
prov = Provenance.resolved(f"pack:{self.name}@{self.version}", commit)
|
|
232
|
+
return relations_to_edges(models, index, _resolve_target, prov)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _resolve_target(rel: dict[str, str], index: ModelIndex) -> str | None:
|
|
236
|
+
"""Resolve one Django relation to a target model id (unique class match)."""
|
|
237
|
+
return index.unique_class(str(rel.get("target", "")).rsplit(".", 1)[-1])
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
DJANGO_PACK = DjangoPack()
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
; Django models (feat-011). Capture every class; the pack inspects each class
|
|
2
|
+
; in code to decide whether it is a Django model — a base whose tail is `Model`
|
|
3
|
+
; (``class X(models.Model)``) or a ``models.*Field`` body assignment — so a
|
|
4
|
+
; plain class in a Django app never mints a false model. Body analysis is done
|
|
5
|
+
; in Python so direct-child scoping (class-level fields only) stays precise.
|
|
6
|
+
(class_definition
|
|
7
|
+
name: (identifier) @name) @model
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Express framework pack (feat-011) — routes (JavaScript + TypeScript).
|
|
2
|
+
|
|
3
|
+
Extracts `app.get('/x', handler)` / `router.post('/x', mw, handler)` calls into
|
|
4
|
+
``Route`` nodes. The handler is the call's last argument: a **named** function
|
|
5
|
+
reference resolves to its symbol → a ``HANDLED_BY`` edge; an **anonymous** inline
|
|
6
|
+
handler (`(req, res) => {}`) still yields the ``Route`` (the API surface) but no
|
|
7
|
+
edge (recorded in ``attrs.handler = ""``). A non-route call (`app.use`,
|
|
8
|
+
`app.listen`) or a dynamic (non-literal) path is skipped/counted, never dropped.
|
|
9
|
+
|
|
10
|
+
The pack spans both sibling languages: it extracts over ``.js`` and ``.ts`` and
|
|
11
|
+
builds handler symbol ids with the *file's* slug (so a `.ts` handler resolves to
|
|
12
|
+
its TS symbol). Intra-file (the route call and a named handler defined in the
|
|
13
|
+
same file); cross-file handler imports and `app.use('/p', router)` prefix
|
|
14
|
+
mounting are follow-ups.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from functools import cache
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
from tree_sitter import Node as TSNode
|
|
23
|
+
from tree_sitter import Parser, Query, QueryCursor
|
|
24
|
+
|
|
25
|
+
from agentforge_graph.core import (
|
|
26
|
+
Descriptor,
|
|
27
|
+
Edge,
|
|
28
|
+
EdgeKind,
|
|
29
|
+
NodeKind,
|
|
30
|
+
Provenance,
|
|
31
|
+
SourceFile,
|
|
32
|
+
SymbolID,
|
|
33
|
+
)
|
|
34
|
+
from agentforge_graph.core import Node as GraphNode
|
|
35
|
+
from agentforge_graph.frameworks.base import FrameworkFacts, FrameworkPack
|
|
36
|
+
from agentforge_graph.frameworks.packs._js_ast import (
|
|
37
|
+
first_arg_string,
|
|
38
|
+
js_language,
|
|
39
|
+
last_named_arg,
|
|
40
|
+
text,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_HERE = Path(__file__).parent
|
|
44
|
+
_HTTP_METHODS = {"get", "post", "put", "delete", "patch", "head", "options", "all"}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@cache
|
|
48
|
+
def _query_text() -> str:
|
|
49
|
+
return (_HERE / "routes.scm").read_text(encoding="utf-8")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ExpressPack(FrameworkPack):
|
|
53
|
+
name = "express"
|
|
54
|
+
language = "javascript/typescript"
|
|
55
|
+
language_slug = "js" # primary; `slugs` spans js+ts and extract uses file.language
|
|
56
|
+
version = "1"
|
|
57
|
+
dep_names = ("express",)
|
|
58
|
+
import_markers = (
|
|
59
|
+
"require('express')",
|
|
60
|
+
'require("express")',
|
|
61
|
+
"from 'express'",
|
|
62
|
+
'from "express"',
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def slugs(self) -> tuple[str, ...]:
|
|
67
|
+
return ("js", "ts")
|
|
68
|
+
|
|
69
|
+
def extract(self, file: SourceFile, repo: str, commit: str) -> FrameworkFacts:
|
|
70
|
+
slug = file.language # "js" or "ts" — the file's own slug
|
|
71
|
+
src = file.text.encode("utf-8")
|
|
72
|
+
lang = js_language(slug)
|
|
73
|
+
root = Parser(lang).parse(src).root_node
|
|
74
|
+
query = Query(lang, _query_text())
|
|
75
|
+
prov = Provenance.parsed(f"pack:{self.name}@{self.version}", commit)
|
|
76
|
+
facts = FrameworkFacts()
|
|
77
|
+
seen: set[str] = set()
|
|
78
|
+
|
|
79
|
+
for _pattern, caps in QueryCursor(query).matches(root):
|
|
80
|
+
method_caps = caps.get("method")
|
|
81
|
+
args_caps = caps.get("args")
|
|
82
|
+
route_caps = caps.get("call")
|
|
83
|
+
if not (method_caps and args_caps and route_caps):
|
|
84
|
+
continue
|
|
85
|
+
method = text(method_caps[0], src).lower()
|
|
86
|
+
if method not in _HTTP_METHODS:
|
|
87
|
+
continue # app.use / app.listen / a non-router method call
|
|
88
|
+
|
|
89
|
+
path = first_arg_string(args_caps[0], src)
|
|
90
|
+
if path is None:
|
|
91
|
+
facts.unresolved += 1 # dynamic / non-literal path
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
handler_id = self._handler_id(args_caps[0], slug, repo, file, src)
|
|
95
|
+
method_u = "ALL" if method == "all" else method.upper()
|
|
96
|
+
route_id = SymbolID.for_symbol(slug, repo, file.path, f"route({method_u} {path}).")
|
|
97
|
+
if route_id in seen:
|
|
98
|
+
continue
|
|
99
|
+
seen.add(route_id)
|
|
100
|
+
route_node = route_caps[0]
|
|
101
|
+
facts.nodes.append(
|
|
102
|
+
GraphNode(
|
|
103
|
+
id=route_id,
|
|
104
|
+
kind=NodeKind.ROUTE,
|
|
105
|
+
name=f"{method_u} {path}",
|
|
106
|
+
span=(route_node.start_point[0] + 1, route_node.end_point[0] + 1),
|
|
107
|
+
attrs={
|
|
108
|
+
"method": method_u,
|
|
109
|
+
"path": path,
|
|
110
|
+
"framework": self.name,
|
|
111
|
+
"handler": handler_id or "",
|
|
112
|
+
},
|
|
113
|
+
provenance=prov,
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
if handler_id is not None:
|
|
117
|
+
facts.edges.append(
|
|
118
|
+
Edge(src=route_id, dst=handler_id, kind=EdgeKind.HANDLED_BY, provenance=prov)
|
|
119
|
+
)
|
|
120
|
+
return facts
|
|
121
|
+
|
|
122
|
+
def _handler_id(
|
|
123
|
+
self, args: TSNode, slug: str, repo: str, file: SourceFile, src: bytes
|
|
124
|
+
) -> str | None:
|
|
125
|
+
"""The symbol id of a named handler reference (the last argument), or None
|
|
126
|
+
for an anonymous inline handler (no symbol to point at)."""
|
|
127
|
+
handler = last_named_arg(args)
|
|
128
|
+
if handler is None or handler.type != "identifier":
|
|
129
|
+
return None
|
|
130
|
+
return SymbolID.for_symbol(slug, repo, file.path, Descriptor.method(text(handler, src)))
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
EXPRESS_PACK = ExpressPack()
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
; Express route registrations (feat-011): `app.get('/x', handler)` /
|
|
2
|
+
; `router.post('/x', mw, handler)`. The method (get/post/…) and a string path
|
|
3
|
+
; are validated in code so non-route calls (`app.use`, `app.listen`) and dynamic
|
|
4
|
+
; paths are handled, not silently missed. Shared verbatim by the JS and TS
|
|
5
|
+
; grammars (same node types).
|
|
6
|
+
(call_expression
|
|
7
|
+
function: (member_expression object: (_) @obj property: (property_identifier) @method)
|
|
8
|
+
arguments: (arguments) @args) @call
|