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,607 @@
1
+ """The ``ckg`` command-line interface: ``index``, ``embed``, ``query``,
2
+ ``map``, and ``serve-mcp``. The engine commands are framework-free (embedding
3
+ uses the configured driver: Bedrock by default, ``fake`` for tests);
4
+ ``serve-mcp`` lazily loads the framework/MCP layer.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import asyncio
11
+ from collections.abc import Sequence
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from agentforge_graph.embed import EmbedReport
16
+ from agentforge_graph.ingest import CodeGraph
17
+ from agentforge_graph.ingest.report import IndexReport
18
+ from agentforge_graph.temporal import parse_history
19
+
20
+
21
+ def _add_repo_arg(parser: argparse.ArgumentParser, *, positional: bool = True) -> None:
22
+ """Attach the standard repo-path argument (ENH-006).
23
+
24
+ Convention: a positional ``[path]`` defaulting to ``.`` on every subcommand,
25
+ with ``--path`` / ``--repo`` accepted as back-compat aliases. Precedence
26
+ (resolved in :func:`main`): positional > ``--path``/``--repo`` > ``.``.
27
+
28
+ ``positional=False`` is for subcommands whose positional slot is already
29
+ taken (e.g. ``query`` / ``tagged``'s leading argument); they keep only the
30
+ ``--path`` / ``--repo`` aliases.
31
+ """
32
+ if positional:
33
+ parser.add_argument("path", nargs="?", default=None, help="repository path (default: .)")
34
+ primary = "repository path" + ("" if positional else " (default: .)")
35
+ parser.add_argument("--path", dest="path_alias", default=None, help=primary)
36
+ parser.add_argument(
37
+ "--repo", dest="path_alias", default=None, help="repository path (alias of --path)"
38
+ )
39
+
40
+
41
+ def _resolve_repo_path(args: argparse.Namespace) -> None:
42
+ """Collapse positional ``path`` + ``--path``/``--repo`` alias into ``args.path``."""
43
+ if hasattr(args, "path") or hasattr(args, "path_alias"):
44
+ args.path = getattr(args, "path", None) or getattr(args, "path_alias", None) or "."
45
+
46
+
47
+ def _format_report(report: IndexReport) -> str:
48
+ lines = [
49
+ f"indexed {report.files_indexed} files: {report.nodes} nodes, {report.edges} edges",
50
+ ]
51
+ if report.by_node_kind:
52
+ lines.append(
53
+ " nodes: " + ", ".join(f"{k}={v}" for k, v in sorted(report.by_node_kind.items()))
54
+ )
55
+ if report.by_edge_kind:
56
+ lines.append(
57
+ " edges: " + ", ".join(f"{k}={v}" for k, v in sorted(report.by_edge_kind.items()))
58
+ )
59
+ r = report.resolve
60
+ lines.append(
61
+ f" resolve: imports {r.imports_resolved} in-repo + {r.imports_external} external, "
62
+ f"calls {r.refs_resolved} resolved / {r.refs_unresolved} unresolved"
63
+ )
64
+ if (
65
+ report.routes_extracted
66
+ or report.models_extracted
67
+ or report.services_extracted
68
+ or report.framework_unresolved
69
+ ):
70
+ lines.append(
71
+ f" frameworks: {report.routes_extracted} routes, "
72
+ f"{report.models_extracted} models, "
73
+ f"{report.relations_resolved} relations, "
74
+ f"{report.services_extracted} services "
75
+ f"({report.framework_unresolved} unresolved)"
76
+ )
77
+ if report.decisions_indexed or report.governs_resolved:
78
+ lines.append(
79
+ f" decisions: {report.decisions_indexed} ADRs, "
80
+ f"{report.governs_resolved} governs ({report.mentions_unresolved} unresolved)"
81
+ )
82
+ if report.docs_indexed or report.commits_indexed:
83
+ extra = f", {report.commits_indexed} commits" if report.commits_indexed else ""
84
+ lines.append(
85
+ f" docs: {report.docs_indexed} files, {report.describes_resolved} describes{extra}"
86
+ )
87
+ if report.skipped:
88
+ shown = ", ".join(report.skipped[:5]) + (" …" if len(report.skipped) > 5 else "")
89
+ lines.append(f" skipped {len(report.skipped)}: {shown}")
90
+ return "\n".join(lines)
91
+
92
+
93
+ def _format_embed(report: EmbedReport) -> str:
94
+ docs = f" + {report.doc_chunks} doc chunks" if report.doc_chunks else ""
95
+ return (
96
+ f"embedded {report.embedded} chunks across {report.files} files{docs} "
97
+ f"({report.skipped_unchanged} unchanged) — model {report.model}, dim {report.dim}"
98
+ )
99
+
100
+
101
+ def _format_backfill(rep: Any) -> str:
102
+ if not rep.ran:
103
+ return f"backfill: skipped ({rep.reason})"
104
+ back = rep.backfilled_through[:10] if rep.backfilled_through else "?"
105
+ return (
106
+ f"backfill: replayed {rep.commits} commits, +{rep.events_added} events "
107
+ f"(history back to {back})"
108
+ )
109
+
110
+
111
+ async def _index(args: argparse.Namespace) -> int:
112
+ cg = await CodeGraph.index(
113
+ repo_path=args.path,
114
+ languages=args.lang or None,
115
+ config=args.config,
116
+ include=args.include or None,
117
+ exclude=args.exclude or None,
118
+ embed=args.embed,
119
+ full=args.full,
120
+ )
121
+ try:
122
+ print(_format_report(cg.stats()))
123
+ if args.embed:
124
+ print(_format_embed(cg.embed_stats()))
125
+ history = parse_history(args.history)
126
+ if history != 0:
127
+ print(_format_backfill(await cg.backfill(history)))
128
+ finally:
129
+ await cg.close()
130
+ return 0
131
+
132
+
133
+ async def _status(args: argparse.Namespace) -> int:
134
+ from agentforge_graph.config import StoreConfig
135
+ from agentforge_graph.core import GraphQuery
136
+ from agentforge_graph.ingest.codegraph import _git_commit
137
+ from agentforge_graph.ingest.incremental import IndexMeta
138
+
139
+ root = Path(args.path) / StoreConfig.load(args.config).path
140
+ meta = IndexMeta.load(root)
141
+ head = _git_commit(args.path)
142
+ dirty = bool(head) and bool(meta.indexed_commit) and head != meta.indexed_commit
143
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
144
+ try:
145
+ nodes = (await cg.store.graph.query(GraphQuery(limit=10_000_000))).nodes
146
+ temporal = await cg.temporal_status()
147
+ finally:
148
+ await cg.close()
149
+ by_kind: dict[str, int] = {}
150
+ for n in nodes:
151
+ by_kind[n.kind.value] = by_kind.get(n.kind.value, 0) + 1
152
+ if not temporal["enabled"]:
153
+ temporal_line = "off"
154
+ elif not temporal["has_sidecar"]:
155
+ temporal_line = "on — no sidecar yet (re-index)"
156
+ else:
157
+ back = temporal.get("backfilled_through") or ""
158
+ temporal_line = f"on — {temporal['events']} events" + (
159
+ f", history back to {back[:10]}" if back else ""
160
+ )
161
+ lines = [
162
+ f"indexed commit: {meta.indexed_commit or '(none)'}",
163
+ f"head commit: {head or '(not a git repo)'}",
164
+ f"dirty: {'yes — run ckg index' if dirty else 'no'}",
165
+ f"files indexed: {len(meta.files)}",
166
+ f"nodes: {len(nodes)}"
167
+ + (" (" + ", ".join(f"{k}={v}" for k, v in sorted(by_kind.items())) + ")" if nodes else ""),
168
+ f"temporal: {temporal_line}",
169
+ f"store: {root}",
170
+ ]
171
+ print("\n".join(lines))
172
+ return 0
173
+
174
+
175
+ def _fmt_ts(ts: int) -> str:
176
+ """Epoch seconds → UTC date, or '—' if unknown."""
177
+ if not ts:
178
+ return "—"
179
+ from datetime import UTC, datetime
180
+
181
+ return datetime.fromtimestamp(ts, tz=UTC).strftime("%Y-%m-%d")
182
+
183
+
184
+ def _short(sha: str) -> str:
185
+ return sha[:10] if sha else "—"
186
+
187
+
188
+ async def _history(args: argparse.Namespace) -> int:
189
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
190
+ try:
191
+ hist = await cg.history(args.symbol)
192
+ finally:
193
+ await cg.close()
194
+ if hist is None:
195
+ print("(no temporal data — enable `temporal:` in ckg.yaml and re-index)")
196
+ return 0
197
+ authors = ", ".join(f"{a.name} ({a.commits})" for a in hist.authors) or "—"
198
+ print(f"symbol: {hist.symbol_id}")
199
+ print(f"introduced: {_fmt_ts(hist.introduced_ts)} ({_short(hist.introduced)})")
200
+ print(f"last changed: {_fmt_ts(hist.last_changed_ts)} ({_short(hist.last_changed)})")
201
+ print(f"churn: {hist.churn_30d} (30d) / {hist.churn_90d} (90d)")
202
+ print(f"authors: {authors}")
203
+ print(f"events: {len(hist.events)}")
204
+ for e in hist.events:
205
+ print(f" {_fmt_ts(e.ts)} {e.event.value:<8} {_short(e.commit)}")
206
+ return 0
207
+
208
+
209
+ async def _changed_since(args: argparse.Namespace) -> int:
210
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
211
+ try:
212
+ changes = await cg.changed_since(args.ref, scope=args.scope)
213
+ finally:
214
+ await cg.close()
215
+ if not changes:
216
+ print("(nothing changed since that ref, or no temporal data)")
217
+ return 0
218
+ width = max(len(c.kind) for c in changes)
219
+ for c in changes:
220
+ sym = c.symbol_id.rsplit(" ", 1)[-1]
221
+ print(f"{_fmt_ts(c.ts)} {c.kind:<{width}} {sym}")
222
+ return 0
223
+
224
+
225
+ async def _embed(args: argparse.Namespace) -> int:
226
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config, languages=args.lang or None)
227
+ try:
228
+ print(_format_embed(await cg.embed()))
229
+ finally:
230
+ await cg.close()
231
+ return 0
232
+
233
+
234
+ async def _routes(args: argparse.Namespace) -> int:
235
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
236
+ try:
237
+ routes = await cg.routes()
238
+ if not routes:
239
+ print("(no routes found)")
240
+ return 0
241
+ width = max(len(r.method) for r in routes)
242
+ for r in routes:
243
+ handler = r.handler.rsplit(" ", 1)[-1] if r.handler else "?"
244
+ print(f"{r.method:<{width}} {r.path} → {handler} ({r.file}:{r.line})")
245
+ finally:
246
+ await cg.close()
247
+ return 0
248
+
249
+
250
+ async def _models(args: argparse.Namespace) -> int:
251
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
252
+ try:
253
+ models = await cg.models()
254
+ if not models:
255
+ print("(no models found)")
256
+ return 0
257
+ for m in models:
258
+ table = f" [{m.table}]" if m.table else ""
259
+ fields = ", ".join(m.fields) if m.fields else "—"
260
+ print(f"{m.name}{table} ({m.file}:{m.line})")
261
+ print(f" fields: {fields}")
262
+ if m.relations:
263
+ rels = ", ".join(
264
+ f"{r['via'] or r['kind']}→{r['to']} ({r['kind']})" for r in m.relations
265
+ )
266
+ print(f" relations: {rels}")
267
+ finally:
268
+ await cg.close()
269
+ return 0
270
+
271
+
272
+ async def _services(args: argparse.Namespace) -> int:
273
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
274
+ try:
275
+ services = await cg.services()
276
+ if not services:
277
+ print("(no services found)")
278
+ return 0
279
+ for s in services:
280
+ consumers = ", ".join(c.rsplit(" ", 1)[-1] for c in s.injected_into) or "—"
281
+ print(f"{s.name} ({s.file}:{s.line})")
282
+ print(f" injected into: {consumers}")
283
+ finally:
284
+ await cg.close()
285
+ return 0
286
+
287
+
288
+ async def _decisions(args: argparse.Namespace) -> int:
289
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
290
+ try:
291
+ decisions = await cg.decisions(scope=args.scope, status=args.status)
292
+ if not decisions:
293
+ print("(no decisions found)")
294
+ return 0
295
+ for d in decisions:
296
+ adr = d.adr_id or d.path
297
+ govs = f" governs {len(d.governs)}" if d.governs else ""
298
+ print(f"{d.status:<10} {d.date or '—':<10} {adr} {d.title}{govs}")
299
+ finally:
300
+ await cg.close()
301
+ return 0
302
+
303
+
304
+ async def _enrich(args: argparse.Namespace) -> int:
305
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
306
+ do_summaries = args.summaries or args.all
307
+ do_decisions = args.decisions or args.all
308
+ # patterns is the default only when no other role was explicitly requested
309
+ do_patterns = args.patterns or args.all or not (args.summaries or args.decisions)
310
+ try:
311
+ if do_patterns:
312
+ r = await cg.enrich(budget_usd=args.budget_usd)
313
+ by = ", ".join(f"{k}={v}" for k, v in sorted(r.by_pattern.items()))
314
+ print(
315
+ f"patterns: {r.candidates} candidates, {r.judged} judged, {r.tagged} tagged "
316
+ f"— ${r.cost_usd:.4f}" + (" [budget tripped]" if r.budget_tripped else "")
317
+ )
318
+ if by:
319
+ print(f" by pattern: {by}")
320
+ if do_summaries:
321
+ s = await cg.summarize(budget_usd=args.budget_usd)
322
+ print(
323
+ f"summaries: {s.files_summarized} files"
324
+ + (" + repo" if s.repo_summarized else "")
325
+ + f" — ${s.cost_usd:.4f}"
326
+ + (" [budget tripped]" if s.budget_tripped else "")
327
+ )
328
+ if do_decisions:
329
+ g = await cg.infer_governs(budget_usd=args.budget_usd)
330
+ print(
331
+ f"decisions: {g.decisions_considered}/{g.decisions_total} considered, "
332
+ f"{g.governs_inferred} GOVERNS inferred — ${g.cost_usd:.4f}"
333
+ + (" [budget tripped]" if g.budget_tripped else "")
334
+ )
335
+ finally:
336
+ await cg.close()
337
+ return 0
338
+
339
+
340
+ async def _summaries(args: argparse.Namespace) -> int:
341
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
342
+ try:
343
+ items = await cg.summaries(level=args.level)
344
+ if not items:
345
+ print("(no summaries — run `ckg enrich --summaries`)")
346
+ return 0
347
+ for s in items:
348
+ where = s.path or "<repo>"
349
+ print(f"[{s.level}] {where}\n {s.text}\n")
350
+ finally:
351
+ await cg.close()
352
+ return 0
353
+
354
+
355
+ async def _tagged(args: argparse.Namespace) -> int:
356
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
357
+ try:
358
+ hits = await cg.tagged(args.pattern, min_confidence=args.min_confidence)
359
+ if not hits:
360
+ print(f"(no symbols tagged {args.pattern})")
361
+ return 0
362
+ for t in hits:
363
+ sym = t.symbol_id.rsplit(" ", 1)[-1]
364
+ print(f"{t.confidence:.2f} {sym} — {t.rationale}")
365
+ finally:
366
+ await cg.close()
367
+ return 0
368
+
369
+
370
+ async def _serve_mcp(args: argparse.Namespace) -> int:
371
+ # lazy import: keeps the engine commands (index/embed/query/map) free of
372
+ # the framework/MCP SDK.
373
+ from typing import cast
374
+
375
+ from agentforge_graph.config import ServeConfig
376
+ from agentforge_graph.serve import serve_mcp
377
+ from agentforge_graph.serve.server import Transport
378
+
379
+ # CLI flags override; otherwise fall back to the serve: block in ckg.yaml.
380
+ cfg = ServeConfig.load(args.config)
381
+ transport = cast(Transport, args.transport or cfg.transport)
382
+ host = args.host or cfg.host
383
+ port = args.port if args.port is not None else cfg.port
384
+ auth_token = args.auth_token or cfg.http_auth_token # env fallback in build_mcp_server
385
+
386
+ await serve_mcp(
387
+ repo_path=args.path,
388
+ config=args.config,
389
+ transport=transport,
390
+ host=host,
391
+ port=port,
392
+ refresh_on_call=args.refresh_on_call,
393
+ auth_token=auth_token,
394
+ allow_unauthenticated=args.allow_unauthenticated,
395
+ )
396
+ return 0
397
+
398
+
399
+ async def _map(args: argparse.Namespace) -> int:
400
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
401
+ try:
402
+ text = await cg.repo_map(
403
+ budget_tokens=args.budget, focus=args.focus or None, scope=args.scope
404
+ )
405
+ print(text if text else "(empty map)")
406
+ finally:
407
+ await cg.close()
408
+ return 0
409
+
410
+
411
+ async def _query(args: argparse.Namespace) -> int:
412
+ from agentforge_graph.temporal import TemporalError
413
+
414
+ cg = await CodeGraph.open(repo_path=args.path, config=args.config)
415
+ try:
416
+ pack = await cg.retrieve(
417
+ query=args.query,
418
+ symbol=args.symbol,
419
+ mode=args.mode,
420
+ k=args.k,
421
+ depth=args.depth,
422
+ as_of=args.as_of,
423
+ )
424
+ rendered = pack.render(args.budget)
425
+ print(rendered if rendered else "(no results)")
426
+ except TemporalError as exc:
427
+ print(f"as_of unavailable: {exc}")
428
+ return 1
429
+ finally:
430
+ await cg.close()
431
+ return 0
432
+
433
+
434
+ def build_parser() -> argparse.ArgumentParser:
435
+ parser = argparse.ArgumentParser(prog="ckg", description="Code Knowledge Graph engine")
436
+ sub = parser.add_subparsers(dest="command", required=True)
437
+
438
+ idx = sub.add_parser("index", help="index a repository into the graph")
439
+ _add_repo_arg(idx)
440
+ idx.add_argument("--lang", action="append", help="restrict to a language (repeatable)")
441
+ idx.add_argument(
442
+ "--include", action="append", help="only index paths matching GLOB (repeatable)"
443
+ )
444
+ idx.add_argument(
445
+ "--exclude", action="append", help="also exclude paths matching GLOB (repeatable)"
446
+ )
447
+ idx.add_argument("--config", default=None, help="path to ckg.yaml")
448
+ idx.add_argument("--embed", action="store_true", help="also chunk + embed after indexing")
449
+ idx.add_argument(
450
+ "--full",
451
+ action="store_true",
452
+ help="force a full rebuild instead of incremental (default: incremental)",
453
+ )
454
+ idx.add_argument(
455
+ "--history",
456
+ default=None,
457
+ metavar="N",
458
+ help="backfill the temporal log from the last N commits (or 'full'); "
459
+ "needs temporal enabled (feat-009)",
460
+ )
461
+ idx.set_defaults(func=_index)
462
+
463
+ st = sub.add_parser("status", help="show the index commit, staleness and node counts")
464
+ _add_repo_arg(st)
465
+ st.add_argument("--config", default=None, help="path to ckg.yaml")
466
+ st.set_defaults(func=_status)
467
+
468
+ hist = sub.add_parser("history", help="show a symbol's git evolution (feat-009 temporal)")
469
+ hist.add_argument("symbol", help="exact symbol id")
470
+ _add_repo_arg(hist, positional=False)
471
+ hist.add_argument("--config", default=None, help="path to ckg.yaml")
472
+ hist.set_defaults(func=_history)
473
+
474
+ cs = sub.add_parser(
475
+ "changed-since", help="list symbols changed since a git ref (feat-009 temporal)"
476
+ )
477
+ cs.add_argument("ref", help="git ref/commit (e.g. HEAD~20, a tag, a sha)")
478
+ _add_repo_arg(cs, positional=False)
479
+ cs.add_argument("--scope", default=None, help="restrict to a path glob/prefix")
480
+ cs.add_argument("--config", default=None, help="path to ckg.yaml")
481
+ cs.set_defaults(func=_changed_since)
482
+
483
+ emb = sub.add_parser("embed", help="chunk + embed an already-indexed repository")
484
+ _add_repo_arg(emb)
485
+ emb.add_argument("--lang", action="append", help="restrict to a language (repeatable)")
486
+ emb.add_argument("--config", default=None, help="path to ckg.yaml")
487
+ emb.set_defaults(func=_embed)
488
+
489
+ qry = sub.add_parser("query", help="retrieve connected context for a question")
490
+ qry.add_argument("query", nargs="?", default=None, help="natural-language query")
491
+ _add_repo_arg(qry, positional=False)
492
+ qry.add_argument("--symbol", default=None, help="anchor at an exact symbol id")
493
+ qry.add_argument(
494
+ "--mode",
495
+ default="context",
496
+ choices=["context", "impact", "definition", "similar"],
497
+ help="retrieval mode (default: context)",
498
+ )
499
+ qry.add_argument("--k", type=int, default=8, help="vector hits (default: 8)")
500
+ qry.add_argument("--depth", type=int, default=1, help="graph expansion hops (default: 1)")
501
+ qry.add_argument("--budget", type=int, default=4000, help="render token budget (default: 4000)")
502
+ qry.add_argument(
503
+ "--as-of",
504
+ dest="as_of",
505
+ default=None,
506
+ metavar="COMMIT",
507
+ help="reconstruct results as of a git commit (feat-009; needs temporal + backfill)",
508
+ )
509
+ qry.add_argument("--config", default=None, help="path to ckg.yaml")
510
+ qry.set_defaults(func=_query)
511
+
512
+ mp = sub.add_parser("map", help="print a budget-aware, centrality-ranked repo map")
513
+ _add_repo_arg(mp)
514
+ mp.add_argument("--budget", type=int, default=2000, help="token budget (default: 2000)")
515
+ mp.add_argument(
516
+ "--focus", action="append", help="path or symbol id to focus ranking (repeatable)"
517
+ )
518
+ mp.add_argument("--scope", default=None, help="restrict to a path subtree")
519
+ mp.add_argument("--config", default=None, help="path to ckg.yaml")
520
+ mp.set_defaults(func=_map)
521
+
522
+ rt = sub.add_parser("routes", help="list extracted framework routes (method, path → handler)")
523
+ _add_repo_arg(rt)
524
+ rt.add_argument("--config", default=None, help="path to ckg.yaml")
525
+ rt.set_defaults(func=_routes)
526
+
527
+ md = sub.add_parser("models", help="list extracted ORM data models (table, fields)")
528
+ _add_repo_arg(md)
529
+ md.add_argument("--config", default=None, help="path to ckg.yaml")
530
+ md.set_defaults(func=_models)
531
+
532
+ sv = sub.add_parser("services", help="list DI-provided services and their injection sites")
533
+ _add_repo_arg(sv)
534
+ sv.add_argument("--config", default=None, help="path to ckg.yaml")
535
+ sv.set_defaults(func=_services)
536
+
537
+ dec = sub.add_parser(
538
+ "decisions", help="list architecture decisions (ADRs) and what they govern"
539
+ )
540
+ _add_repo_arg(dec)
541
+ dec.add_argument("--scope", default=None, help="restrict to decisions governing a path subtree")
542
+ dec.add_argument("--status", default=None, help="filter by status (e.g. accepted)")
543
+ dec.add_argument("--config", default=None, help="path to ckg.yaml")
544
+ dec.set_defaults(func=_decisions)
545
+
546
+ enr = sub.add_parser("enrich", help="LLM enrichment (pattern tags / summaries; Bedrock Claude)")
547
+ _add_repo_arg(enr)
548
+ enr.add_argument("--patterns", action="store_true", help="run pattern tagging (default)")
549
+ enr.add_argument("--summaries", action="store_true", help="run module summaries")
550
+ enr.add_argument(
551
+ "--decisions", action="store_true", help="infer GOVERNS links for ADRs (feat-010)"
552
+ )
553
+ enr.add_argument("--all", action="store_true", help="run patterns, summaries, and decisions")
554
+ enr.add_argument("--budget-usd", type=float, default=None, help="override the per-run USD cap")
555
+ enr.add_argument("--config", default=None, help="path to ckg.yaml")
556
+ enr.set_defaults(func=_enrich)
557
+
558
+ sm = sub.add_parser("summaries", help="list stored module summaries")
559
+ _add_repo_arg(sm)
560
+ sm.add_argument("--level", default=None, help="filter by level (file|repo)")
561
+ sm.add_argument("--config", default=None, help="path to ckg.yaml")
562
+ sm.set_defaults(func=_summaries)
563
+
564
+ tg = sub.add_parser("tagged", help="list symbols carrying a design-pattern tag")
565
+ tg.add_argument("pattern", help="pattern name, e.g. Repository")
566
+ _add_repo_arg(tg)
567
+ tg.add_argument("--min-confidence", type=float, default=0.7, help="confidence floor (0.7)")
568
+ tg.add_argument("--config", default=None, help="path to ckg.yaml")
569
+ tg.set_defaults(func=_tagged)
570
+
571
+ srv = sub.add_parser(
572
+ "serve-mcp", help="run the MCP server (stdio or http) exposing the CKG tools"
573
+ )
574
+ _add_repo_arg(srv)
575
+ srv.add_argument("--config", default=None, help="path to ckg.yaml")
576
+ srv.add_argument(
577
+ "--transport",
578
+ choices=["stdio", "http"],
579
+ default=None,
580
+ help="MCP transport (default: stdio, or serve.transport in ckg.yaml)",
581
+ )
582
+ srv.add_argument("--host", default=None, help="http transport bind host (default: 127.0.0.1)")
583
+ srv.add_argument("--port", type=int, default=None, help="http transport port (default: 8765)")
584
+ srv.add_argument(
585
+ "--auth-token",
586
+ default="",
587
+ help="http: require this bearer token (ENH-005; or $CKG_HTTP_AUTH_TOKEN / ckg.yaml)",
588
+ )
589
+ srv.add_argument(
590
+ "--allow-unauthenticated",
591
+ action="store_true",
592
+ help="http: permit binding a non-loopback host with no auth (deliberate opt-in)",
593
+ )
594
+ srv.add_argument(
595
+ "--refresh-on-call",
596
+ action="store_true",
597
+ help="(0.1: no-op) refresh the index on tool calls",
598
+ )
599
+ srv.set_defaults(func=_serve_mcp)
600
+ return parser
601
+
602
+
603
+ def main(argv: Sequence[str] | None = None) -> int:
604
+ args = build_parser().parse_args(argv)
605
+ _resolve_repo_path(args)
606
+ exit_code: int = asyncio.run(args.func(args))
607
+ return exit_code