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,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