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,307 @@
1
+ """The read-only CKG tools (feat-008), thin over feat-006/007/009.
2
+
3
+ Each is an AgentForge ``Tool`` holding the shared ``_Engine``; ``run`` clamps
4
+ params to ``ServeConfig`` and returns a JSON string (structured) or text (the
5
+ map) — MCP coerces results via ``str()``, so JSON keeps structure intact.
6
+ Every structured envelope carries ``indexed_commit`` + ``dirty`` (staleness)
7
+ and a ``truncated`` flag. Names & schemas are locked at v1.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from typing import Any, ClassVar
14
+
15
+ from agentforge_core.contracts.tool import Tool
16
+ from pydantic import BaseModel, Field
17
+
18
+ from agentforge_graph.chunking import estimate_tokens
19
+ from agentforge_graph.core import EdgeKind, GraphQuery
20
+
21
+ from .engine import TOOL_API_VERSION, _Engine
22
+
23
+
24
+ class RepoMapInput(BaseModel):
25
+ budget_tokens: int = Field(default=2000, description="token budget for the map")
26
+ focus: list[str] = Field(
27
+ default_factory=list, description="paths or symbol ids to rank the map around"
28
+ )
29
+
30
+
31
+ class SearchInput(BaseModel):
32
+ query: str = Field(description="natural-language question about the code")
33
+ k: int = Field(default=8, description="number of vector hits (clamped to serve.max_k)")
34
+ mode: str = Field(default="context", description="context | similar | definition | impact")
35
+
36
+
37
+ class SymbolInput(BaseModel):
38
+ symbol_id: str = Field(default="", description="exact symbol id, if known")
39
+ name: str = Field(default="", description="symbol name (use with path) when id is unknown")
40
+ path: str = Field(default="", description="file path for a name lookup")
41
+
42
+
43
+ class ImpactInput(BaseModel):
44
+ symbol_id: str = Field(description="symbol whose reverse dependencies to trace")
45
+ depth: int = Field(default=1, description="hops (clamped to serve.max_depth)")
46
+
47
+
48
+ class NeighborsInput(BaseModel):
49
+ symbol_id: str = Field(description="symbol id")
50
+ edge_kinds: list[str] = Field(
51
+ default_factory=list, description="edge kinds to follow (e.g. CALLS, CONTAINS); default all"
52
+ )
53
+ depth: int = Field(default=1, description="hops (clamped to serve.max_depth)")
54
+
55
+
56
+ class RoutesInput(BaseModel):
57
+ method: str = Field(default="", description="filter by HTTP method, e.g. GET (optional)")
58
+ path: str = Field(default="", description="filter by path prefix, e.g. /users (optional)")
59
+
60
+
61
+ class DecisionsInput(BaseModel):
62
+ scope: str = Field(default="", description="restrict to decisions governing a path prefix")
63
+ status: str = Field(default="", description="filter by status, e.g. accepted (optional)")
64
+
65
+
66
+ class ExplainInput(BaseModel):
67
+ symbol_id: str = Field(description="exact symbol id to explain")
68
+
69
+
70
+ class HistoryInput(BaseModel):
71
+ symbol_id: str = Field(description="exact symbol id whose git history to report")
72
+
73
+
74
+ class EmptyInput(BaseModel):
75
+ pass
76
+
77
+
78
+ class _CkgTool(Tool):
79
+ capabilities: ClassVar[frozenset[str]] = frozenset()
80
+
81
+ def __init__(self, engine: _Engine) -> None:
82
+ self._engine = engine
83
+
84
+ async def _pack_json(self, pack: Any) -> str:
85
+ data = pack.to_dict()
86
+ data.update(await self._engine.staleness())
87
+ data["tool_api_version"] = TOOL_API_VERSION
88
+ cap = self._engine.serve.response_token_cap
89
+ truncated = False
90
+ while len(data.get("items", [])) > 1 and estimate_tokens(json.dumps(data)) > cap:
91
+ data["items"].pop()
92
+ truncated = True
93
+ if truncated:
94
+ data.setdefault("notes", []).append("response truncated to fit response_token_cap")
95
+ data["truncated"] = truncated
96
+ return json.dumps(data, indent=2)
97
+
98
+
99
+ class CkgRepoMap(_CkgTool):
100
+ name: ClassVar[str] = "ckg_repo_map"
101
+ description: ClassVar[str] = (
102
+ "Orient in a codebase: a budget-aware, centrality-ranked map of the most "
103
+ "structurally important symbols (signatures), grouped by file. Call this FIRST "
104
+ "to understand an unfamiliar repo. Pass `focus` (paths/symbol ids) to rank the "
105
+ "map around a working set."
106
+ )
107
+ input_schema: ClassVar[type[BaseModel]] = RepoMapInput
108
+
109
+ async def run(self, **kwargs: Any) -> str:
110
+ repomap = await self._engine.repomap()
111
+ budget = min(int(kwargs.get("budget_tokens", 2000)), self._engine.serve.response_token_cap)
112
+ text = await repomap.render(budget_tokens=budget, focus=kwargs.get("focus") or None)
113
+ return text or "(empty repo map)"
114
+
115
+
116
+ class CkgSearch(_CkgTool):
117
+ name: ClassVar[str] = "ckg_search"
118
+ description: ClassVar[str] = (
119
+ "Search the codebase for code relevant to a natural-language question. Returns "
120
+ "ranked, CONNECTED context: matching chunks plus their symbols and neighbors. "
121
+ "`mode`: 'context' (default), 'similar' (just similar code), 'definition' (a "
122
+ "symbol's chunks), 'impact' (reverse deps — prefer ckg_impact). Take a result's "
123
+ "`id` to chain into ckg_impact / ckg_neighbors / ckg_symbol."
124
+ )
125
+ input_schema: ClassVar[type[BaseModel]] = SearchInput
126
+
127
+ async def run(self, **kwargs: Any) -> str:
128
+ retriever = await self._engine.retriever()
129
+ k = min(int(kwargs.get("k", 8)), self._engine.serve.max_k)
130
+ pack = await retriever.retrieve(
131
+ query=kwargs["query"], k=k, mode=kwargs.get("mode", "context")
132
+ )
133
+ return await self._pack_json(pack)
134
+
135
+
136
+ class CkgSymbol(_CkgTool):
137
+ name: ClassVar[str] = "ckg_symbol"
138
+ description: ClassVar[str] = (
139
+ "Look up a specific symbol's definition: the symbol, its chunks and members. "
140
+ "Provide `symbol_id` (exact) or `name` + `path`. When the temporal layer is "
141
+ "enabled, items carry `temporal` (churn_90d, top_authors, introduced, "
142
+ "last_changed) — recency/ownership signals for triage."
143
+ )
144
+ input_schema: ClassVar[type[BaseModel]] = SymbolInput
145
+
146
+ async def run(self, **kwargs: Any) -> str:
147
+ symbol_id = kwargs.get("symbol_id", "")
148
+ if not symbol_id:
149
+ cg = await self._engine.code_graph()
150
+ res = await cg.store.graph.query(
151
+ GraphQuery(name=kwargs.get("name") or None, path_prefix=kwargs.get("path") or None)
152
+ )
153
+ symbol_id = res.nodes[0].id if res.nodes else ""
154
+ if not symbol_id:
155
+ return json.dumps({"error": "symbol not found", "tool_api_version": TOOL_API_VERSION})
156
+ retriever = await self._engine.retriever()
157
+ depth = min(1, self._engine.serve.max_depth)
158
+ pack = await retriever.retrieve(symbol=symbol_id, mode="definition", depth=depth)
159
+ return await self._pack_json(pack)
160
+
161
+
162
+ class CkgImpact(_CkgTool):
163
+ name: ClassVar[str] = "ckg_impact"
164
+ description: ClassVar[str] = (
165
+ "Find what DEPENDS ON a symbol (reverse CALLS/IMPORTS/IMPLEMENTS): 'who calls "
166
+ "this', 'what breaks if I change this'. The impact question grep cannot answer."
167
+ )
168
+ input_schema: ClassVar[type[BaseModel]] = ImpactInput
169
+
170
+ async def run(self, **kwargs: Any) -> str:
171
+ retriever = await self._engine.retriever()
172
+ depth = min(int(kwargs.get("depth", 1)), self._engine.serve.max_depth)
173
+ pack = await retriever.retrieve(symbol=kwargs["symbol_id"], mode="impact", depth=depth)
174
+ return await self._pack_json(pack)
175
+
176
+
177
+ class CkgNeighbors(_CkgTool):
178
+ name: ClassVar[str] = "ckg_neighbors"
179
+ description: ClassVar[str] = (
180
+ "List a symbol's typed graph neighbors (edges in both directions), optionally "
181
+ "filtered by edge kind (CALLS, CONTAINS, IMPORTS, INHERITS, REFERENCES, CHUNK_OF)."
182
+ )
183
+ input_schema: ClassVar[type[BaseModel]] = NeighborsInput
184
+
185
+ async def run(self, **kwargs: Any) -> str:
186
+ cg = await self._engine.code_graph()
187
+ kinds = [EdgeKind(k) for k in kwargs.get("edge_kinds", [])] or None
188
+ depth = min(int(kwargs.get("depth", 1)), self._engine.serve.max_depth)
189
+ start = kwargs["symbol_id"]
190
+ seen: set[tuple[str, str, str]] = set()
191
+ edges: list[dict[str, str]] = []
192
+ frontier = {start}
193
+ visited = {start}
194
+ for _ in range(depth):
195
+ nxt: set[str] = set()
196
+ for nid in frontier:
197
+ for e in await cg.store.graph.adjacent(nid, kinds, "both"):
198
+ key = (e.src, e.dst, e.kind.value)
199
+ if key not in seen:
200
+ seen.add(key)
201
+ edges.append(
202
+ {
203
+ "src": e.src,
204
+ "dst": e.dst,
205
+ "kind": e.kind.value,
206
+ "provenance": e.provenance.source.value,
207
+ }
208
+ )
209
+ other = e.dst if e.src == nid else e.src
210
+ if other not in visited:
211
+ visited.add(other)
212
+ nxt.add(other)
213
+ frontier = nxt
214
+ envelope = await self._engine.staleness()
215
+ return json.dumps(
216
+ {"symbol_id": start, "edges": edges, **envelope, "tool_api_version": TOOL_API_VERSION},
217
+ indent=2,
218
+ )
219
+
220
+
221
+ class CkgStatus(_CkgTool):
222
+ name: ClassVar[str] = "ckg_status"
223
+ description: ClassVar[str] = (
224
+ "Report index status: the indexed git commit, whether it is stale vs the working "
225
+ "tree (`dirty`), node counts by kind, and the tool-API version. If results seem "
226
+ "out of date and `dirty` is true, tell the user to re-run `ckg index`."
227
+ )
228
+ input_schema: ClassVar[type[BaseModel]] = EmptyInput
229
+
230
+ async def run(self, **kwargs: Any) -> str:
231
+ return json.dumps(await self._engine.status(), indent=2)
232
+
233
+
234
+ class CkgRoutes(_CkgTool):
235
+ name: ClassVar[str] = "ckg_routes"
236
+ description: ClassVar[str] = (
237
+ "List the app's HTTP API surface: every framework route (e.g. FastAPI) with its "
238
+ "method, path pattern and handler symbol id. Filter by `method` and/or `path` prefix. "
239
+ "Take a route's `handler` into ckg_symbol / ckg_impact to see the handler and what it "
240
+ "touches. Returns empty if no web framework is detected."
241
+ )
242
+ input_schema: ClassVar[type[BaseModel]] = RoutesInput
243
+
244
+ async def run(self, **kwargs: Any) -> str:
245
+ data = await self._engine.routes(
246
+ method=kwargs.get("method", ""), path=kwargs.get("path", "")
247
+ )
248
+ return json.dumps(data, indent=2)
249
+
250
+
251
+ class CkgDecisions(_CkgTool):
252
+ name: ClassVar[str] = "ckg_decisions"
253
+ description: ClassVar[str] = (
254
+ "List the architecture decisions (ADRs) governing the codebase: each decision's "
255
+ "status, date, title and the symbols/files it governs. Filter by `scope` (a path "
256
+ "prefix the decision governs) and/or `status` (e.g. accepted). Call this before a "
257
+ "refactor to check no documented decision forbids the change. Empty if no ADRs."
258
+ )
259
+ input_schema: ClassVar[type[BaseModel]] = DecisionsInput
260
+
261
+ async def run(self, **kwargs: Any) -> str:
262
+ data = await self._engine.decisions(
263
+ scope=kwargs.get("scope", ""), status=kwargs.get("status", "")
264
+ )
265
+ return json.dumps(data, indent=2)
266
+
267
+
268
+ class CkgExplain(_CkgTool):
269
+ name: ClassVar[str] = "ckg_explain"
270
+ description: ClassVar[str] = (
271
+ "Explain a symbol: its LLM-derived design-pattern tags (e.g. 'Repository', with "
272
+ "confidence + rationale) and its 1-hop typed graph facts. Use to learn what role a "
273
+ "class/function plays before changing it. Tags are [llm]-provenance; empty until "
274
+ "`ckg enrich` has run."
275
+ )
276
+ input_schema: ClassVar[type[BaseModel]] = ExplainInput
277
+
278
+ async def run(self, **kwargs: Any) -> str:
279
+ return json.dumps(await self._engine.explain(kwargs["symbol_id"]), indent=2)
280
+
281
+
282
+ class CkgHistory(_CkgTool):
283
+ name: ClassVar[str] = "ckg_history"
284
+ description: ClassVar[str] = (
285
+ "Report a symbol's git evolution (feat-009 temporal): when it was introduced and "
286
+ "last changed, churn over the last 30/90 days, its top authors, and its lifecycle "
287
+ "events. Use to gauge recency/ownership before changing a symbol or to triage a "
288
+ "regression. Returns `available: false` if the temporal layer is off or unindexed."
289
+ )
290
+ input_schema: ClassVar[type[BaseModel]] = HistoryInput
291
+
292
+ async def run(self, **kwargs: Any) -> str:
293
+ return json.dumps(await self._engine.history(kwargs["symbol_id"]), indent=2)
294
+
295
+
296
+ ALL_TOOLS = [
297
+ CkgRepoMap,
298
+ CkgSearch,
299
+ CkgSymbol,
300
+ CkgImpact,
301
+ CkgNeighbors,
302
+ CkgStatus,
303
+ CkgRoutes,
304
+ CkgDecisions,
305
+ CkgExplain,
306
+ CkgHistory,
307
+ ]
@@ -0,0 +1,32 @@
1
+ """agentforge_graph.store — persistent storage adapters (feat-003).
2
+
3
+ Embedded-first (ADR-0006): the default ``Store`` writes a Kuzu graph DB
4
+ and a LanceDB vector index under ``.ckg/`` with no server. Server adapters
5
+ (Neo4j, FalkorDB) register out-of-tree via entry points. Imports nothing
6
+ from ``agentforge`` (ADR-0001).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from .errors import (
12
+ DriverNotFound,
13
+ SchemaVersionError,
14
+ StoreConfigError,
15
+ StoreError,
16
+ )
17
+ from .facade import Store
18
+ from .kuzu_store import KuzuGraphStore
19
+ from .lance_store import LanceVectorStore
20
+ from .registry import graph_driver, vector_driver
21
+
22
+ __all__ = [
23
+ "Store",
24
+ "KuzuGraphStore",
25
+ "LanceVectorStore",
26
+ "graph_driver",
27
+ "vector_driver",
28
+ "StoreError",
29
+ "StoreConfigError",
30
+ "DriverNotFound",
31
+ "SchemaVersionError",
32
+ ]
@@ -0,0 +1,102 @@
1
+ """Pure ``Node``/``Edge`` ↔ property-row mapping, shared by every property-graph
2
+ adapter (Kuzu embedded, Neo4j server — ENH-004). No DB driver imports: the open
3
+ schema (arbitrary kinds, free-form ``attrs``) is flattened to a fixed set of
4
+ scalar properties (``kind``, ``name``, span, provenance, ``origin_path``) with
5
+ ``attrs`` as a JSON string, so an unrecognized kind round-trips with no DDL
6
+ change (ADR-0005). Each backend supplies the storage; this supplies the shape.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ from typing import Any
13
+
14
+ from agentforge_graph.core import Edge, EdgeKind, Node, NodeKind, Provenance, Source, SymbolID
15
+
16
+ # Trust ordering (PARSED is highest-trust): a node passes a ``min_source`` floor
17
+ # iff its source rank is >= the floor's.
18
+ SOURCE_RANK = {Source.LLM: 0, Source.RESOLVED: 1, Source.MANUAL: 1, Source.PARSED: 2}
19
+
20
+
21
+ def dump_attrs(attrs: dict[str, Any]) -> str:
22
+ return json.dumps(attrs, sort_keys=True)
23
+
24
+
25
+ def load_attrs(s: str | None) -> dict[str, Any]:
26
+ return json.loads(s) if s else {}
27
+
28
+
29
+ def acceptable_sources(floor: Source) -> list[str]:
30
+ threshold = SOURCE_RANK[floor]
31
+ return [s.value for s, rank in SOURCE_RANK.items() if rank >= threshold]
32
+
33
+
34
+ def node_params(node: Node, origin_path: str) -> dict[str, Any]:
35
+ span_start, span_end = node.span if node.span is not None else (None, None)
36
+ p = node.provenance
37
+ return {
38
+ "id": node.id,
39
+ "kind": node.kind.value,
40
+ "name": node.name,
41
+ "span_start": span_start,
42
+ "span_end": span_end,
43
+ "attrs": dump_attrs(node.attrs),
44
+ "sym_path": SymbolID.parse(node.id).path,
45
+ "prov_source": p.source.value,
46
+ "prov_extractor": p.extractor,
47
+ "prov_commit": p.commit,
48
+ "prov_confidence": p.confidence,
49
+ "origin_path": origin_path,
50
+ }
51
+
52
+
53
+ def edge_params(edge: Edge, origin_path: str) -> dict[str, Any]:
54
+ p = edge.provenance
55
+ return {
56
+ "src": edge.src,
57
+ "dst": edge.dst,
58
+ "kind": edge.kind.value,
59
+ "attrs": dump_attrs(edge.attrs),
60
+ "prov_source": p.source.value,
61
+ "prov_extractor": p.extractor,
62
+ "prov_commit": p.commit,
63
+ "prov_confidence": p.confidence,
64
+ # An edge that carries its own owner file (resolver edges, feat-004) wins;
65
+ # otherwise the caller's stamp (the upserted file's path).
66
+ "origin_path": edge.origin_path or origin_path,
67
+ "resolved_from": "",
68
+ }
69
+
70
+
71
+ def prov_from_row(d: dict[str, Any]) -> Provenance:
72
+ # The validating constructor — a corrupt row fails loudly, not silently.
73
+ return Provenance(
74
+ source=Source(d["prov_source"]),
75
+ extractor=d["prov_extractor"],
76
+ commit=d["prov_commit"],
77
+ confidence=d["prov_confidence"],
78
+ )
79
+
80
+
81
+ def node_from_row(d: dict[str, Any]) -> Node:
82
+ # span may be absent (Neo4j drops null properties) or present-but-None (Kuzu).
83
+ span_start = d.get("span_start")
84
+ span = (span_start, d.get("span_end")) if span_start is not None else None
85
+ return Node(
86
+ id=d["id"],
87
+ kind=NodeKind(d["kind"]),
88
+ name=d["name"],
89
+ span=span,
90
+ attrs=load_attrs(d["attrs"]),
91
+ provenance=prov_from_row(d),
92
+ )
93
+
94
+
95
+ def edge_from_row(d: dict[str, Any], src: str, dst: str) -> Edge:
96
+ return Edge(
97
+ src=src,
98
+ dst=dst,
99
+ kind=EdgeKind(d["kind"]),
100
+ attrs=load_attrs(d["attrs"]),
101
+ provenance=prov_from_row(d),
102
+ )
@@ -0,0 +1,22 @@
1
+ """Storage-layer errors. All raised at ``Store.open`` / adapter open time
2
+ (fail-at-startup), never mid-index — a half-written graph is worse than a
3
+ refused one."""
4
+
5
+ from __future__ import annotations
6
+
7
+
8
+ class StoreError(Exception):
9
+ """Base for all storage-adapter errors."""
10
+
11
+
12
+ class StoreConfigError(StoreError):
13
+ """Malformed or unreadable ``ckg.yaml`` ``store:`` block."""
14
+
15
+
16
+ class DriverNotFound(StoreConfigError):
17
+ """Config names a graph/vector driver that isn't registered."""
18
+
19
+
20
+ class SchemaVersionError(StoreError):
21
+ """On-disk index schema version differs from this build's. 0.x policy
22
+ is to rebuild the index (the data is derivable)."""
@@ -0,0 +1,89 @@
1
+ """The ``Store`` facade — one ``GraphStore`` + one ``VectorStore`` resolved
2
+ from ``ckg.yaml``, plus the vector→graph join (``expand``) that retrieval
3
+ (feat-006) builds on. Embedded-first: the default writes ``.ckg/graph.kuzu``
4
+ and ``.ckg/vectors.lance`` under the repo root (ADR-0006).
5
+
6
+ All failure modes (unknown driver, schema mismatch, malformed config) raise
7
+ at ``open`` — never mid-index.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ from pathlib import Path
14
+
15
+ from agentforge_graph.config import StoreConfig
16
+ from agentforge_graph.core import EdgeKind, GraphStore, Node, QueryResult, ScoredRef, VectorStore
17
+
18
+ from .errors import SchemaVersionError
19
+ from .registry import graph_driver, vector_driver
20
+
21
+ # Store-level on-disk layout version. Bumped when the .ckg/ layout changes;
22
+ # 0.x policy on mismatch is rebuild (the index is derivable — ADR-0006).
23
+ STORE_SCHEMA_VERSION = 1
24
+
25
+
26
+ class Store:
27
+ """Owns a graph store and a vector store, resolved from config."""
28
+
29
+ def __init__(self, graph: GraphStore, vectors: VectorStore, config: StoreConfig) -> None:
30
+ self.graph = graph
31
+ self.vectors = vectors
32
+ self.config = config
33
+
34
+ @classmethod
35
+ async def open(cls, repo_path: str | Path = ".", config: str | Path | None = None) -> Store:
36
+ """Resolve drivers from ``config`` (a ckg.yaml path) and open the
37
+ embedded index under ``repo_path``/<store.path>. Raises before any
38
+ store is opened if the config or on-disk schema is bad."""
39
+ cfg = StoreConfig.load(config)
40
+ root = Path(repo_path) / cfg.path
41
+ _check_or_init_meta(root) # fail-at-startup on schema mismatch
42
+ graph_cls = graph_driver(cfg.graph.driver)
43
+ vector_cls = vector_driver(cfg.vectors.driver)
44
+ # Embedded drivers use the path under .ckg/; server drivers (ENH-004)
45
+ # ignore the path and read connection details from their config block.
46
+ graph: GraphStore = await graph_cls.open(root / "graph.kuzu", config=cfg.graph.config)
47
+ vectors: VectorStore = await vector_cls.open(
48
+ root / "vectors.lance", config=cfg.vectors.config
49
+ )
50
+ return cls(graph, vectors, cfg)
51
+
52
+ async def expand(
53
+ self,
54
+ refs: list[ScoredRef],
55
+ kinds: list[EdgeKind] | None = None,
56
+ depth: int = 1,
57
+ ) -> QueryResult:
58
+ """Join vector hits back into the graph: for each ref, collect the
59
+ node and its ``kinds``-edge neighborhood within ``depth`` hops. The
60
+ single place the graph+vector join lives (feat-006)."""
61
+ nodes: dict[str, Node] = {}
62
+ for r in refs:
63
+ hit = await self.graph.get(r.ref)
64
+ if hit is not None:
65
+ nodes[hit.id] = hit
66
+ for nb in await self.graph.neighbors(r.ref, kinds, depth):
67
+ nodes[nb.id] = nb
68
+ return QueryResult(nodes=list(nodes.values()))
69
+
70
+ async def close(self) -> None:
71
+ await self.graph.close()
72
+ await self.vectors.close()
73
+
74
+
75
+ def _check_or_init_meta(root: Path) -> None:
76
+ root.mkdir(parents=True, exist_ok=True)
77
+ meta = root / "meta.json"
78
+ if meta.exists():
79
+ data = json.loads(meta.read_text())
80
+ on_disk = data.get("schema_version")
81
+ if on_disk != STORE_SCHEMA_VERSION:
82
+ raise SchemaVersionError(
83
+ f"index at {root} is schema v{on_disk}, this build expects "
84
+ f"v{STORE_SCHEMA_VERSION}; rebuild the index (0.x policy)"
85
+ )
86
+ else:
87
+ meta.write_text(
88
+ json.dumps({"schema_version": STORE_SCHEMA_VERSION, "indexed_commit": ""}, indent=2)
89
+ )