agentforge-graph 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agentforge_graph/__init__.py +6 -0
- agentforge_graph/chunking/__init__.py +12 -0
- agentforge_graph/chunking/cast.py +159 -0
- agentforge_graph/chunking/chunk.py +19 -0
- agentforge_graph/chunking/tokens.py +15 -0
- agentforge_graph/cli.py +607 -0
- agentforge_graph/config.py +259 -0
- agentforge_graph/core/__init__.py +54 -0
- agentforge_graph/core/conformance.py +270 -0
- agentforge_graph/core/contracts.py +163 -0
- agentforge_graph/core/kinds.py +68 -0
- agentforge_graph/core/models.py +134 -0
- agentforge_graph/core/provenance.py +62 -0
- agentforge_graph/core/symbols.py +116 -0
- agentforge_graph/embed/__init__.py +28 -0
- agentforge_graph/embed/base.py +22 -0
- agentforge_graph/embed/bedrock.py +85 -0
- agentforge_graph/embed/fake.py +34 -0
- agentforge_graph/embed/openai.py +67 -0
- agentforge_graph/embed/pipeline.py +184 -0
- agentforge_graph/embed/registry.py +66 -0
- agentforge_graph/embed/report.py +15 -0
- agentforge_graph/enrich/__init__.py +70 -0
- agentforge_graph/enrich/anthropic.py +38 -0
- agentforge_graph/enrich/anthropic_client.py +109 -0
- agentforge_graph/enrich/bedrock.py +24 -0
- agentforge_graph/enrich/bedrock_client.py +115 -0
- agentforge_graph/enrich/bedrock_summarizer.py +23 -0
- agentforge_graph/enrich/claude.py +172 -0
- agentforge_graph/enrich/enricher.py +108 -0
- agentforge_graph/enrich/governs.py +173 -0
- agentforge_graph/enrich/governs_enricher.py +152 -0
- agentforge_graph/enrich/heuristics.py +224 -0
- agentforge_graph/enrich/judge.py +63 -0
- agentforge_graph/enrich/registry.py +133 -0
- agentforge_graph/enrich/report.py +60 -0
- agentforge_graph/enrich/summarizer.py +62 -0
- agentforge_graph/enrich/summary_enricher.py +211 -0
- agentforge_graph/enrich/taxonomy.py +38 -0
- agentforge_graph/frameworks/__init__.py +29 -0
- agentforge_graph/frameworks/base.py +75 -0
- agentforge_graph/frameworks/detect.py +124 -0
- agentforge_graph/frameworks/extractor.py +63 -0
- agentforge_graph/frameworks/orm.py +93 -0
- agentforge_graph/frameworks/packs/_js_ast.py +56 -0
- agentforge_graph/frameworks/packs/_python_ast.py +157 -0
- agentforge_graph/frameworks/packs/django/__init__.py +240 -0
- agentforge_graph/frameworks/packs/django/models.scm +7 -0
- agentforge_graph/frameworks/packs/express/__init__.py +133 -0
- agentforge_graph/frameworks/packs/express/routes.scm +8 -0
- agentforge_graph/frameworks/packs/fastapi/__init__.py +210 -0
- agentforge_graph/frameworks/packs/fastapi/depends.scm +6 -0
- agentforge_graph/frameworks/packs/fastapi/routes.scm +10 -0
- agentforge_graph/frameworks/packs/flask/__init__.py +143 -0
- agentforge_graph/frameworks/packs/flask/routes.scm +11 -0
- agentforge_graph/frameworks/packs/nestjs/__init__.py +205 -0
- agentforge_graph/frameworks/packs/nestjs/routes.scm +6 -0
- agentforge_graph/frameworks/packs/spring/__init__.py +267 -0
- agentforge_graph/frameworks/packs/spring/routes.scm +6 -0
- agentforge_graph/frameworks/packs/sqlalchemy/__init__.py +250 -0
- agentforge_graph/frameworks/packs/sqlalchemy/models.scm +7 -0
- agentforge_graph/frameworks/registry.py +44 -0
- agentforge_graph/ingest/__init__.py +30 -0
- agentforge_graph/ingest/codegraph.py +847 -0
- agentforge_graph/ingest/extractor.py +353 -0
- agentforge_graph/ingest/incremental/__init__.py +25 -0
- agentforge_graph/ingest/incremental/detect.py +118 -0
- agentforge_graph/ingest/incremental/dirty.py +61 -0
- agentforge_graph/ingest/incremental/indexer.py +218 -0
- agentforge_graph/ingest/incremental/meta.py +72 -0
- agentforge_graph/ingest/incremental/ports.py +39 -0
- agentforge_graph/ingest/pack.py +160 -0
- agentforge_graph/ingest/packs/__init__.py +34 -0
- agentforge_graph/ingest/packs/cpp/__init__.py +35 -0
- agentforge_graph/ingest/packs/cpp/references.scm +15 -0
- agentforge_graph/ingest/packs/cpp/structure.scm +49 -0
- agentforge_graph/ingest/packs/csharp/__init__.py +35 -0
- agentforge_graph/ingest/packs/csharp/references.scm +12 -0
- agentforge_graph/ingest/packs/csharp/structure.scm +45 -0
- agentforge_graph/ingest/packs/go/__init__.py +38 -0
- agentforge_graph/ingest/packs/go/references.scm +12 -0
- agentforge_graph/ingest/packs/go/structure.scm +64 -0
- agentforge_graph/ingest/packs/java/__init__.py +35 -0
- agentforge_graph/ingest/packs/java/references.scm +12 -0
- agentforge_graph/ingest/packs/java/structure.scm +38 -0
- agentforge_graph/ingest/packs/javascript/__init__.py +34 -0
- agentforge_graph/ingest/packs/javascript/references.scm +11 -0
- agentforge_graph/ingest/packs/javascript/structure.scm +166 -0
- agentforge_graph/ingest/packs/php/__init__.py +35 -0
- agentforge_graph/ingest/packs/php/references.scm +15 -0
- agentforge_graph/ingest/packs/php/structure.scm +44 -0
- agentforge_graph/ingest/packs/python/__init__.py +25 -0
- agentforge_graph/ingest/packs/python/references.scm +14 -0
- agentforge_graph/ingest/packs/python/structure.scm +57 -0
- agentforge_graph/ingest/packs/ruby/__init__.py +37 -0
- agentforge_graph/ingest/packs/ruby/references.scm +12 -0
- agentforge_graph/ingest/packs/ruby/structure.scm +37 -0
- agentforge_graph/ingest/packs/rust/__init__.py +39 -0
- agentforge_graph/ingest/packs/rust/references.scm +12 -0
- agentforge_graph/ingest/packs/rust/structure.scm +46 -0
- agentforge_graph/ingest/packs/typescript/__init__.py +31 -0
- agentforge_graph/ingest/packs/typescript/references.scm +11 -0
- agentforge_graph/ingest/packs/typescript/structure.scm +99 -0
- agentforge_graph/ingest/pipeline.py +134 -0
- agentforge_graph/ingest/report.py +84 -0
- agentforge_graph/ingest/resolver.py +467 -0
- agentforge_graph/ingest/source.py +79 -0
- agentforge_graph/knowledge/__init__.py +28 -0
- agentforge_graph/knowledge/adr.py +136 -0
- agentforge_graph/knowledge/commits.py +152 -0
- agentforge_graph/knowledge/ingest.py +312 -0
- agentforge_graph/knowledge/mentions.py +71 -0
- agentforge_graph/knowledge/report.py +32 -0
- agentforge_graph/main.py +21 -0
- agentforge_graph/providers.py +36 -0
- agentforge_graph/repomap/__init__.py +14 -0
- agentforge_graph/repomap/rank.py +161 -0
- agentforge_graph/repomap/render.py +55 -0
- agentforge_graph/repomap/repomap.py +66 -0
- agentforge_graph/retrieve/__init__.py +21 -0
- agentforge_graph/retrieve/pack.py +76 -0
- agentforge_graph/retrieve/rerank.py +251 -0
- agentforge_graph/retrieve/retriever.py +286 -0
- agentforge_graph/retrieve/scoring.py +36 -0
- agentforge_graph/serve/__init__.py +19 -0
- agentforge_graph/serve/engine.py +204 -0
- agentforge_graph/serve/http_runner.py +133 -0
- agentforge_graph/serve/server.py +110 -0
- agentforge_graph/serve/tools.py +307 -0
- agentforge_graph/store/__init__.py +32 -0
- agentforge_graph/store/_rowmap.py +102 -0
- agentforge_graph/store/errors.py +22 -0
- agentforge_graph/store/facade.py +89 -0
- agentforge_graph/store/kuzu_store.py +380 -0
- agentforge_graph/store/lance_store.py +146 -0
- agentforge_graph/store/neo4j_store.py +294 -0
- agentforge_graph/store/pgvector_store.py +170 -0
- agentforge_graph/store/registry.py +45 -0
- agentforge_graph/temporal/__init__.py +36 -0
- agentforge_graph/temporal/backfill.py +338 -0
- agentforge_graph/temporal/events.py +82 -0
- agentforge_graph/temporal/index.py +190 -0
- agentforge_graph/temporal/mining.py +190 -0
- agentforge_graph/temporal/recorder.py +114 -0
- agentforge_graph/temporal/store.py +282 -0
- agentforge_graph-0.3.2.dist-info/METADATA +291 -0
- agentforge_graph-0.3.2.dist-info/RECORD +151 -0
- agentforge_graph-0.3.2.dist-info/WHEEL +4 -0
- agentforge_graph-0.3.2.dist-info/entry_points.txt +3 -0
- agentforge_graph-0.3.2.dist-info/licenses/LICENSE +202 -0
- agentforge_graph-0.3.2.dist-info/licenses/NOTICE +14 -0
agentforge_graph/cli.py
ADDED
|
@@ -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
|