codmap 0.0.3__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.
- codemap/__init__.py +10 -0
- codemap/apidiff.py +208 -0
- codemap/arch.py +190 -0
- codemap/cli.py +718 -0
- codemap/diagnostics.py +256 -0
- codemap/extract/__init__.py +10 -0
- codemap/extract/attrflow.py +230 -0
- codemap/extract/behavior.py +771 -0
- codemap/extract/dataflow.py +97 -0
- codemap/extract/dispatch.py +248 -0
- codemap/extract/griffe_extractor.py +496 -0
- codemap/extract/gsource.py +83 -0
- codemap/extract/roots.py +427 -0
- codemap/freshness.py +94 -0
- codemap/incremental.py +195 -0
- codemap/integrations/__init__.py +51 -0
- codemap/integrations/base.py +196 -0
- codemap/integrations/cocoindex.py +78 -0
- codemap/integrations/gate.py +58 -0
- codemap/integrations/gitnexus.py +93 -0
- codemap/integrations/registry.py +69 -0
- codemap/integrations/transport.py +46 -0
- codemap/model.py +178 -0
- codemap/provenance.py +248 -0
- codemap/query.py +1164 -0
- codemap/scope.py +212 -0
- codemap/serve/__init__.py +26 -0
- codemap/serve/_scip_pb2.py +100 -0
- codemap/serve/api_surface.py +60 -0
- codemap/serve/apidiff.py +83 -0
- codemap/serve/architecture.py +101 -0
- codemap/serve/audit.py +176 -0
- codemap/serve/check.py +80 -0
- codemap/serve/ctags.py +203 -0
- codemap/serve/impact.py +84 -0
- codemap/serve/livingdocs.py +174 -0
- codemap/serve/mcp_server.py +278 -0
- codemap/serve/mermaid.py +120 -0
- codemap/serve/pack.py +93 -0
- codemap/serve/rag.py +142 -0
- codemap/serve/review.py +197 -0
- codemap/serve/scip.py +183 -0
- codemap/serve/semantic.py +71 -0
- codemap/serve/server.py +43 -0
- codemap/serve/session.py +482 -0
- codemap/serve/subsystems.py +85 -0
- codemap/serve/vault.py +156 -0
- codemap/store.py +28 -0
- codemap/tomlio.py +59 -0
- codmap-0.0.3.dist-info/METADATA +245 -0
- codmap-0.0.3.dist-info/RECORD +55 -0
- codmap-0.0.3.dist-info/WHEEL +5 -0
- codmap-0.0.3.dist-info/entry_points.txt +2 -0
- codmap-0.0.3.dist-info/licenses/LICENSE +21 -0
- codmap-0.0.3.dist-info/top_level.txt +1 -0
codemap/cli.py
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
"""codemap CLI (DESIGN §6, §14.1). CLI-AI-first: JSON by default, stable exit codes.
|
|
2
|
+
|
|
3
|
+
codemap build <path> [-o graph.json] [--deep]
|
|
4
|
+
[--consumer PATH ...] [--docs PATH ...] [--mode thin|full]
|
|
5
|
+
codemap scope <path> [--consumer PATH ...] [--docs PATH ...] [--no-git] [--json]
|
|
6
|
+
| --diff A.meta.json B.meta.json → input scope manifest (M19.A)
|
|
7
|
+
codemap query <name> (--graph g.json | --build <path>) [--format json|text]
|
|
8
|
+
codemap report <kind> (--graph g.json | --build <path>) [--format markdown|json]
|
|
9
|
+
kinds: api-surface | dependencies | dead-code | behavior | impact --symbol X
|
|
10
|
+
codemap export <kind> (--graph g.json | --build <path>) [-o out]
|
|
11
|
+
rag → JSONL chunks (consumer A)
|
|
12
|
+
vault -o <dir> → Obsidian vault tree (consumer B)
|
|
13
|
+
mermaid --mkind class|deps|calls [--scope X] [--root Y] [--depth N]
|
|
14
|
+
scip -o <file> → SCIP index (defs + symbol info; interop, R1-C1)
|
|
15
|
+
ctags [-o tags] → universal-ctags tags file (defs; editor interop, R1-C2)
|
|
16
|
+
docs → living documentation (subsystem-organized, R1-C15)
|
|
17
|
+
codemap review [diff|-] (--graph g.json | --build <path>) [--format markdown|json]
|
|
18
|
+
unified diff (or stdin) → risk-sorted change-set review (M15/F17)
|
|
19
|
+
codemap serve (--graph g.json | --build <path>) [--source-root DIR] [--mcp]
|
|
20
|
+
warm resident process: line-delimited JSON stdio, or MCP with --mcp (M17)
|
|
21
|
+
codemap refresh <graph.json>
|
|
22
|
+
rebuild a graph from the recipe recorded beside it at build time (M18)
|
|
23
|
+
codemap route <capability> <question> [--root DIR]
|
|
24
|
+
forward a capability to an opt-in external tool (DESIGN §13.1; needs
|
|
25
|
+
codemap.toml [integrations].enabled + the tool installed)
|
|
26
|
+
codemap semantic <query> (--graph g.json | --build <path>) [--root DIR] [--limit N]
|
|
27
|
+
semantic search via an opt-in adapter, enriched to codemap symbols (R1-C16)
|
|
28
|
+
codemap pack (--graph g.json | --build <path>) [--budget N] [--seed X …]
|
|
29
|
+
token-budgeted context pack: most relevant graph slice under N tokens (R1-C6)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import argparse
|
|
35
|
+
import os
|
|
36
|
+
import json
|
|
37
|
+
import sys
|
|
38
|
+
from pathlib import Path
|
|
39
|
+
|
|
40
|
+
from codemap import store
|
|
41
|
+
from codemap.diagnostics import diagnostics
|
|
42
|
+
from codemap.extract import extract, extract_repo
|
|
43
|
+
from codemap.provenance import build_provenance
|
|
44
|
+
from codemap.query import Query
|
|
45
|
+
from codemap.serve import (
|
|
46
|
+
build_query_result,
|
|
47
|
+
build_vault,
|
|
48
|
+
render_api_surface,
|
|
49
|
+
render_architecture,
|
|
50
|
+
render_behavior,
|
|
51
|
+
render_dead_code,
|
|
52
|
+
render_dependencies,
|
|
53
|
+
render_impact,
|
|
54
|
+
render_mermaid,
|
|
55
|
+
render_rag,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
_REPORTS = {
|
|
59
|
+
"api-surface": render_api_surface, # takes Graph
|
|
60
|
+
"dependencies": render_dependencies, # takes Query
|
|
61
|
+
"dead-code": render_dead_code, # takes Query
|
|
62
|
+
"behavior": render_behavior, # takes Query
|
|
63
|
+
"architecture": render_architecture, # takes Query (M16/A9)
|
|
64
|
+
}
|
|
65
|
+
_REPORT_KINDS = sorted(_REPORTS) + ["impact", "communities", "flows"] # extra args
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _graph_from(args):
|
|
69
|
+
if getattr(args, "build", None):
|
|
70
|
+
return extract(args.build, deep=getattr(args, "deep", False))
|
|
71
|
+
if getattr(args, "graph", None):
|
|
72
|
+
return store.load(args.graph)
|
|
73
|
+
raise SystemExit("error: need --graph <file> or --build <path>")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _cmd_build(args) -> int:
|
|
77
|
+
incr_info = None
|
|
78
|
+
if args.consumer or args.docs:
|
|
79
|
+
graph = extract_repo(
|
|
80
|
+
args.path,
|
|
81
|
+
consumers=tuple(args.consumer or ()),
|
|
82
|
+
docs=tuple(args.docs or ()),
|
|
83
|
+
mode=args.mode,
|
|
84
|
+
deep=args.deep,
|
|
85
|
+
)
|
|
86
|
+
elif getattr(args, "incremental", False) and args.out:
|
|
87
|
+
graph, incr_info = _incremental_build(args)
|
|
88
|
+
else:
|
|
89
|
+
graph = extract(args.path, deep=args.deep)
|
|
90
|
+
# R1-C25: stamp the input's identity into the graph itself. The scope manifest is
|
|
91
|
+
# resolved here once and reused for the sidecar below — the graph gets the part that
|
|
92
|
+
# must travel with it (scope_id, source commit), the sidecar keeps the rebuild recipe.
|
|
93
|
+
scope = _resolve_scope_quietly(args)
|
|
94
|
+
graph.provenance = build_provenance(
|
|
95
|
+
tier="deep" if args.deep else "fast", scope=scope,
|
|
96
|
+
# roots come from extract_repo in this same call; never inherited from a graph
|
|
97
|
+
# loaded off disk, which is how a stale scope would sneak into a fresh build.
|
|
98
|
+
roots=graph.provenance.get("roots") if (args.consumer or args.docs) else None,
|
|
99
|
+
inputs=graph.provenance.get("inputs"))
|
|
100
|
+
# R1-C21: a well-formed but vacuous graph must announce itself — silence is what
|
|
101
|
+
# lets an unparsed layout read as a clean bill of health downstream.
|
|
102
|
+
for d in diagnostics(graph):
|
|
103
|
+
# each check owns its severity and its consequence — a note is not a warning (#8)
|
|
104
|
+
text = " ".join(p for p in (d["message"], d.get("consequence")) if p)
|
|
105
|
+
print(f"[{d.get('severity', 'warning')}] {text}", file=sys.stderr)
|
|
106
|
+
if args.out:
|
|
107
|
+
store.save(graph, args.out)
|
|
108
|
+
# M18: record the build recipe beside the graph so `codemap refresh` can
|
|
109
|
+
# rebuild it, and so the graph's age is meaningful to `serve`/stats.
|
|
110
|
+
# M19.A: also record the input scope manifest (scope_id + profile + git).
|
|
111
|
+
from codemap.freshness import write_meta
|
|
112
|
+
write_meta(args.out, argv=getattr(args, "_argv", []),
|
|
113
|
+
cwd=os.getcwd(), target=graph.target, scope=scope)
|
|
114
|
+
if incr_info is not None:
|
|
115
|
+
print(f"[incremental] {incr_info['mode']}: "
|
|
116
|
+
f"{len(incr_info['affected'])} module(s) recomputed", file=sys.stderr)
|
|
117
|
+
print(args.out)
|
|
118
|
+
else:
|
|
119
|
+
print(store.dumps(graph))
|
|
120
|
+
return 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _resolve_scope_quietly(args):
|
|
124
|
+
"""The input scope manifest, or None. Never fatal — a build must not fail on it."""
|
|
125
|
+
from codemap.scope import resolve_scope
|
|
126
|
+
try:
|
|
127
|
+
return resolve_scope(args.path, consumers=tuple(args.consumer or ()),
|
|
128
|
+
docs=tuple(args.docs or ()))
|
|
129
|
+
except Exception:
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _incremental_build(args):
|
|
134
|
+
"""Incremental rebuild (R1-C9): reuse the old --out graph + its scope sidecar.
|
|
135
|
+
|
|
136
|
+
Falls back to a full extract when there's no prior graph/scope to build on (first
|
|
137
|
+
build, missing sidecar, or a different target).
|
|
138
|
+
"""
|
|
139
|
+
from codemap.freshness import read_meta
|
|
140
|
+
from codemap.incremental import update_graph
|
|
141
|
+
from codemap.scope import resolve_scope
|
|
142
|
+
old_meta = read_meta(args.out)
|
|
143
|
+
old_scope = (old_meta or {}).get("scope")
|
|
144
|
+
if not (Path(args.out).exists() and old_scope):
|
|
145
|
+
return extract(args.path, deep=args.deep), {"mode": "full", "affected": []}
|
|
146
|
+
old_graph = store.load(args.out)
|
|
147
|
+
new_scope = resolve_scope(args.path)
|
|
148
|
+
if old_graph.target != Path(args.path).resolve().name:
|
|
149
|
+
return extract(args.path, deep=args.deep), {"mode": "full", "affected": []}
|
|
150
|
+
return update_graph(old_graph, args.path, old_scope, new_scope, deep=args.deep)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _cmd_scope(args) -> int:
|
|
154
|
+
"""Resolve/print the input scope manifest, or diff two (M19.A)."""
|
|
155
|
+
from codemap.scope import resolve_scope, diff_scopes
|
|
156
|
+
if args.diff:
|
|
157
|
+
from codemap.freshness import read_meta
|
|
158
|
+
metas = []
|
|
159
|
+
for p in args.diff:
|
|
160
|
+
m = read_meta(p) if p.endswith(".meta.json") else None
|
|
161
|
+
if m is None: # allow passing the graph path or the sidecar path
|
|
162
|
+
import json as _json
|
|
163
|
+
try:
|
|
164
|
+
m = _json.loads(Path(p).read_text(encoding="utf-8"))
|
|
165
|
+
except (OSError, ValueError):
|
|
166
|
+
raise SystemExit(f"error: cannot read scope/meta from {p!r}")
|
|
167
|
+
metas.append(m.get("scope", m)) # sidecar has {scope:{…}}; or a raw manifest
|
|
168
|
+
d = diff_scopes(metas[0], metas[1])
|
|
169
|
+
print(json.dumps(d, indent=2))
|
|
170
|
+
return 0 if not (d["added"] or d["removed"] or d["changed"]) else 1
|
|
171
|
+
if not args.path:
|
|
172
|
+
raise SystemExit("error: scope needs <path> (or --diff A B)")
|
|
173
|
+
scope = resolve_scope(args.path, consumers=tuple(args.consumer or ()),
|
|
174
|
+
docs=tuple(args.docs or ()), use_git=not args.no_git)
|
|
175
|
+
if args.json:
|
|
176
|
+
print(json.dumps(scope, indent=2, sort_keys=True))
|
|
177
|
+
else:
|
|
178
|
+
p = scope["profile"]
|
|
179
|
+
g = scope["git"]
|
|
180
|
+
print(f"scope_id: {scope['scope_id']}")
|
|
181
|
+
print(f"root: {scope['root']}")
|
|
182
|
+
print(f"files: {p['file_count']} ({p['total_bytes']} bytes, {p['loc_total']} loc)")
|
|
183
|
+
if g.get("mode") == "git":
|
|
184
|
+
print(f"git: {g['ref']} @ {g['commit'][:10]} dirty={g['dirty']}"
|
|
185
|
+
+ (f" ({len(g['dirty_files'])} files)" if g["dirty"] else ""))
|
|
186
|
+
else:
|
|
187
|
+
print("git: (fs mode — not a git repo / --no-git)")
|
|
188
|
+
print("by_role: " + ", ".join(f"{r}={v['files']}" for r, v in p["by_role"].items()))
|
|
189
|
+
print("by_ext: " + ", ".join(f"{e}={v['files']}" for e, v in p["by_ext"].items()))
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _cmd_query(args) -> int:
|
|
194
|
+
q = Query(_graph_from(args))
|
|
195
|
+
result = build_query_result(q, args.name)
|
|
196
|
+
if args.format == "text":
|
|
197
|
+
_print_query_text(result)
|
|
198
|
+
else:
|
|
199
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
200
|
+
return 0 if (result["matches"] or result["defined_at"] or result.get("column")) else 1
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _print_query_text(r) -> None:
|
|
204
|
+
print(f"# {r['name']}")
|
|
205
|
+
print("defined at:", ", ".join(r["defined_at"]) or "—")
|
|
206
|
+
for m in r["matches"]:
|
|
207
|
+
loc = f" — {m['file']}:{m['lines'][0]}" if m.get("file") and m["lines"][0] else ""
|
|
208
|
+
print(f" - {m['id']} ({m['kind']}){loc}")
|
|
209
|
+
for mid, dep in r.get("modules", {}).items():
|
|
210
|
+
print(f"\n[{mid}]")
|
|
211
|
+
print(" imports:", ", ".join(dep["dependencies"]) or "—")
|
|
212
|
+
print(" imported by:", ", ".join(dep["dependents"]) or "—")
|
|
213
|
+
for cid, h in r.get("classes", {}).items():
|
|
214
|
+
print(f"\n[{cid}]")
|
|
215
|
+
print(" bases:", ", ".join(h["bases"]) or "—")
|
|
216
|
+
print(" subclasses:", ", ".join(h["subclasses"]) or "—")
|
|
217
|
+
if h.get("implements"):
|
|
218
|
+
print(" implements:", ", ".join(h["implements"]))
|
|
219
|
+
if h.get("implementers"):
|
|
220
|
+
print(" implementers (registry family):", ", ".join(h["implementers"]))
|
|
221
|
+
if h.get("family"):
|
|
222
|
+
print(" family siblings:", ", ".join(h["family"]))
|
|
223
|
+
if h.get("registered_as"):
|
|
224
|
+
reg = h["registered_as"]
|
|
225
|
+
print(f" register with: @{reg.get('decorator','?').rsplit('.',1)[-1]}('{reg.get('key')}')")
|
|
226
|
+
for fid, h in r.get("functions", {}).items():
|
|
227
|
+
print(f"\n[{fid}]")
|
|
228
|
+
print(" calls:", ", ".join(h["callees"]) or "—")
|
|
229
|
+
print(" called by:", ", ".join(h["callers"]) or "—")
|
|
230
|
+
if h.get("columns"):
|
|
231
|
+
c = h["columns"]
|
|
232
|
+
if c.get("reads"):
|
|
233
|
+
print(" reads columns:", ", ".join(c["reads"]))
|
|
234
|
+
if c.get("writes"):
|
|
235
|
+
print(" writes columns:", ", ".join(c["writes"]))
|
|
236
|
+
for aid, acc in r.get("attributes", {}).items():
|
|
237
|
+
print(f"\n[{aid}] attribute")
|
|
238
|
+
if acc.get("reads"):
|
|
239
|
+
print(" read by:", ", ".join(acc["reads"]))
|
|
240
|
+
if acc.get("writes"):
|
|
241
|
+
print(" written by:", ", ".join(acc["writes"]))
|
|
242
|
+
for sid, by_root in r.get("used_by", {}).items():
|
|
243
|
+
if by_root:
|
|
244
|
+
summary = ", ".join(f"{root}: {n}" for root, n in sorted(by_root.items()))
|
|
245
|
+
print(f"\n[{sid}] used by → {summary}")
|
|
246
|
+
col = r.get("column")
|
|
247
|
+
if col:
|
|
248
|
+
print(f"\n[column '{r['name']}'] string-key dataflow")
|
|
249
|
+
print(" written by:", ", ".join(col["writes"]) or "—")
|
|
250
|
+
print(" read by:", ", ".join(col["reads"]) or "—")
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _cmd_tests(args) -> int:
|
|
254
|
+
"""Which tests exercise a symbol — ending in a line you can paste (R1-C24)."""
|
|
255
|
+
from codemap.serve.session import Session
|
|
256
|
+
session = Session(_graph_from(args))
|
|
257
|
+
op = "covers" if args.covers else "tests"
|
|
258
|
+
key = "test" if args.covers else "symbol"
|
|
259
|
+
env = session.handle({"op": op, "args": {key: args.name,
|
|
260
|
+
"depth": args.depth, "cap": args.cap}})
|
|
261
|
+
if not env.get("ok"):
|
|
262
|
+
print(env.get("error", "not found"), file=sys.stderr)
|
|
263
|
+
return 1
|
|
264
|
+
r = env["result"]
|
|
265
|
+
if args.format == "json":
|
|
266
|
+
print(json.dumps(r, ensure_ascii=False, indent=2))
|
|
267
|
+
return 0 if (r.get("tests") or r.get("symbols")) else 1
|
|
268
|
+
if args.covers:
|
|
269
|
+
print(f"# {r['node_id'] or r['test']} covers {r['total']} symbol(s)")
|
|
270
|
+
for row in r["symbols"]:
|
|
271
|
+
print(f" {row['distance']} {row['id']}")
|
|
272
|
+
else:
|
|
273
|
+
print(f"# tests for {r['symbol']} — confidence: {r['confidence']}"
|
|
274
|
+
+ (f", {r['distance']} hop(s) away" if r["distance"] else ""))
|
|
275
|
+
for row in r["tests"]:
|
|
276
|
+
print(f" {row['node_id']}")
|
|
277
|
+
for c in r["caveats"]:
|
|
278
|
+
print(f" · {c}", file=sys.stderr)
|
|
279
|
+
ids = [row["node_id"] for row in r.get("tests", []) if row.get("node_id")]
|
|
280
|
+
if ids:
|
|
281
|
+
print("\npytest " + " ".join(ids))
|
|
282
|
+
return 0 if (r.get("tests") or r.get("symbols")) else 1
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _cmd_report(args) -> int:
|
|
286
|
+
graph = _graph_from(args)
|
|
287
|
+
if args.format == "json":
|
|
288
|
+
print(store.dumps(graph))
|
|
289
|
+
return 0
|
|
290
|
+
if args.kind == "impact":
|
|
291
|
+
if not args.symbol:
|
|
292
|
+
raise SystemExit("error: report impact needs --symbol <name>")
|
|
293
|
+
print(render_impact(Query(graph), args.symbol, depth=args.depth), end="")
|
|
294
|
+
return 0
|
|
295
|
+
if args.kind in ("communities", "flows"):
|
|
296
|
+
from codemap.serve.subsystems import render_communities, render_flows
|
|
297
|
+
q = Query(graph)
|
|
298
|
+
out = (render_communities(q) if args.kind == "communities"
|
|
299
|
+
else render_flows(q, args.symbol, depth=args.depth))
|
|
300
|
+
print(out, end="")
|
|
301
|
+
return 0
|
|
302
|
+
if args.kind == "dead-code":
|
|
303
|
+
from codemap.serve.audit import load_dead_code_whitelist
|
|
304
|
+
root = getattr(args, "source_root", None) or os.getcwd()
|
|
305
|
+
whitelist, wl_error = load_dead_code_whitelist(root)
|
|
306
|
+
print(render_dead_code(Query(graph),
|
|
307
|
+
whitelist=whitelist,
|
|
308
|
+
min_confidence=args.min_confidence,
|
|
309
|
+
whitelist_error=wl_error), end="")
|
|
310
|
+
return 0
|
|
311
|
+
renderer = _REPORTS[args.kind]
|
|
312
|
+
payload = renderer(graph) if args.kind == "api-surface" else renderer(Query(graph))
|
|
313
|
+
print(payload, end="")
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _cmd_export(args) -> int:
|
|
318
|
+
q = Query(_graph_from(args))
|
|
319
|
+
if args.kind == "rag":
|
|
320
|
+
_emit(render_rag(q), args.out)
|
|
321
|
+
elif args.kind == "mermaid":
|
|
322
|
+
_emit(render_mermaid(q, args.mkind, scope=args.scope, root=args.root,
|
|
323
|
+
depth=args.depth), args.out)
|
|
324
|
+
elif args.kind == "vault":
|
|
325
|
+
if not args.out:
|
|
326
|
+
raise SystemExit("error: export vault needs -o <dir>")
|
|
327
|
+
_write_vault(build_vault(q), args.out)
|
|
328
|
+
print(args.out)
|
|
329
|
+
elif args.kind == "docs":
|
|
330
|
+
from codemap.serve.livingdocs import render_docs
|
|
331
|
+
_emit(render_docs(q), args.out)
|
|
332
|
+
elif args.kind == "scip":
|
|
333
|
+
if not args.out:
|
|
334
|
+
raise SystemExit("error: export scip needs -o <file> (binary output)")
|
|
335
|
+
from codemap.serve.scip import build_scip, write_scip
|
|
336
|
+
from codemap import __version__
|
|
337
|
+
index = build_scip(
|
|
338
|
+
q,
|
|
339
|
+
project_root=args.project_root or os.getcwd(),
|
|
340
|
+
package=args.package,
|
|
341
|
+
version=args.package_version,
|
|
342
|
+
tool_version=__version__,
|
|
343
|
+
)
|
|
344
|
+
Path(args.out).write_bytes(write_scip(index))
|
|
345
|
+
print(f"{args.out} ({len(index.documents)} documents)")
|
|
346
|
+
elif args.kind == "ctags":
|
|
347
|
+
from codemap.serve.ctags import build_ctags
|
|
348
|
+
from codemap import __version__
|
|
349
|
+
# --project-root doubles as the source root for /^…$/ pattern addresses;
|
|
350
|
+
# if lines are unreadable, build_ctags falls back to line-number addresses.
|
|
351
|
+
_emit(build_ctags(q, source_root=args.project_root or os.getcwd(),
|
|
352
|
+
tool_version=__version__), args.out)
|
|
353
|
+
return 0
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _cmd_review(args) -> int:
|
|
357
|
+
"""Change-set review from a unified diff → what to review (M15/F17)."""
|
|
358
|
+
from codemap.serve.review import build_review, parse_unified_diff, render_review
|
|
359
|
+
text = (sys.stdin.read() if args.diff in (None, "-")
|
|
360
|
+
else Path(args.diff).read_text(encoding="utf-8"))
|
|
361
|
+
hunks = parse_unified_diff(text)
|
|
362
|
+
q = Query(_graph_from(args))
|
|
363
|
+
base = store.load(args.base) if getattr(args, "base", None) else None
|
|
364
|
+
if args.format == "json":
|
|
365
|
+
print(json.dumps(build_review(q, hunks=hunks, base_graph=base), indent=2, sort_keys=True))
|
|
366
|
+
else:
|
|
367
|
+
print(render_review(q, hunks=hunks, base_graph=base), end="")
|
|
368
|
+
return 0
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _cmd_refresh(args) -> int:
|
|
372
|
+
"""Rebuild a graph from the recipe recorded beside it at build time (M18)."""
|
|
373
|
+
from codemap.freshness import read_meta
|
|
374
|
+
meta = read_meta(args.graph)
|
|
375
|
+
if not meta or not meta.get("argv"):
|
|
376
|
+
raise SystemExit(
|
|
377
|
+
f"error: no rebuild recipe for {args.graph!r} "
|
|
378
|
+
"(build it with `codemap build … -o {graph}` first)")
|
|
379
|
+
cwd = meta.get("cwd")
|
|
380
|
+
if cwd and os.path.isdir(cwd):
|
|
381
|
+
os.chdir(cwd) # recorded argv may use paths relative to the build cwd
|
|
382
|
+
print(f"rebuilding {args.graph} …", file=sys.stderr)
|
|
383
|
+
return main(meta["argv"])
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _cmd_route(args) -> int:
|
|
387
|
+
"""Route a capability question to an opt-in external tool (DESIGN §13.1).
|
|
388
|
+
|
|
389
|
+
Capability-first: the tool is picked from the registry by capability, gated on
|
|
390
|
+
opt-in (codemap.toml) + install. A non-commercial tool's licensing notice is
|
|
391
|
+
shown once (unless acknowledged in config). The answer is forwarded as-is —
|
|
392
|
+
it never enters the graph.
|
|
393
|
+
"""
|
|
394
|
+
from codemap.integrations import load_config, resolve
|
|
395
|
+
cfg = load_config(args.root)
|
|
396
|
+
integ = resolve(args.capability, config=cfg, root=args.root)
|
|
397
|
+
if integ is None:
|
|
398
|
+
# R1-C27: telling a user to enable a tool in codemap.toml is bad advice when they
|
|
399
|
+
# already did and the file has a typo — name the read failure instead.
|
|
400
|
+
if cfg.error:
|
|
401
|
+
raise SystemExit(
|
|
402
|
+
f"error: no tool provides {args.capability!r}, and nothing could be "
|
|
403
|
+
f"enabled: {cfg.error}")
|
|
404
|
+
raise SystemExit(
|
|
405
|
+
f"error: no enabled + installed tool provides {args.capability!r}. "
|
|
406
|
+
f"Enable one in codemap.toml [integrations].enabled and install it.")
|
|
407
|
+
notice = integ.disclaimer() # §13.1 п.3 — worded on use, not reselling
|
|
408
|
+
if notice and not cfg.is_acknowledged(integ.name):
|
|
409
|
+
print(notice, file=sys.stderr)
|
|
410
|
+
answer = integ.route(args.capability, args.question)
|
|
411
|
+
print(json.dumps(answer.to_dict(), ensure_ascii=False, indent=2))
|
|
412
|
+
return 0
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def _cmd_semantic(args) -> int:
|
|
416
|
+
"""Semantic search via an opt-in adapter, enriched to codemap symbols (R1-C16).
|
|
417
|
+
|
|
418
|
+
Resolves an installed + opted-in ADAPTER providing `semantic-search` (cocoindex
|
|
419
|
+
today), runs it in `--root`, and resolves each fuzzy hit to the exact codemap
|
|
420
|
+
symbol at that location. `--root` is the repo the tool's index was built in (and
|
|
421
|
+
where file paths resolve); it defaults to cwd. The core needs no adapter — with
|
|
422
|
+
none enabled+installed this prints an actionable hint, never crashes.
|
|
423
|
+
"""
|
|
424
|
+
from codemap.serve.semantic import semantic_search
|
|
425
|
+
q = Query(_graph_from(args))
|
|
426
|
+
result = semantic_search(q, args.query, root=args.root, limit=args.limit)
|
|
427
|
+
if args.format == "json":
|
|
428
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
429
|
+
return 0
|
|
430
|
+
if not result["resolver"]:
|
|
431
|
+
raise SystemExit(
|
|
432
|
+
"error: no enabled + installed adapter provides 'semantic-search'. "
|
|
433
|
+
"Install one (e.g. `uv tool install 'cocoindex-code[full]'` + `ccc index`) "
|
|
434
|
+
"and enable it in codemap.toml [integrations].enabled. "
|
|
435
|
+
"For a router-only tool (e.g. gitnexus), use `codemap route semantic-search`.")
|
|
436
|
+
if result["disclaimer"]:
|
|
437
|
+
print(result["disclaimer"], file=sys.stderr)
|
|
438
|
+
print(f"# semantic: {args.query!r} (via {result['resolver']})")
|
|
439
|
+
if not result["hits"]:
|
|
440
|
+
print("_no hits._")
|
|
441
|
+
return 0
|
|
442
|
+
for h in result["hits"]:
|
|
443
|
+
sym = h["symbol"] or f"(unresolved) {h['file']}"
|
|
444
|
+
lines = h["lines"]
|
|
445
|
+
print(f" {h['score']:.3f} {sym} [{h['file']}:{lines[0]}-{lines[1]}]")
|
|
446
|
+
return 0
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def _cmd_pack(args) -> int:
|
|
450
|
+
"""Token-budgeted context pack — most relevant graph slice under N tokens (R1-C6)."""
|
|
451
|
+
from codemap.serve.pack import build_pack, render_pack
|
|
452
|
+
q = Query(_graph_from(args))
|
|
453
|
+
seeds = tuple(args.seed or ())
|
|
454
|
+
if args.format == "json":
|
|
455
|
+
print(json.dumps(build_pack(q, budget=args.budget, seeds=seeds),
|
|
456
|
+
ensure_ascii=False, indent=2, sort_keys=True))
|
|
457
|
+
else:
|
|
458
|
+
print(render_pack(q, budget=args.budget, seeds=seeds), end="")
|
|
459
|
+
return 0
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _cmd_check(args) -> int:
|
|
463
|
+
"""Enforce the [architecture] contract → exit 2 on any violation (R1-C3).
|
|
464
|
+
|
|
465
|
+
The CI gate: reads codemap.toml [architecture] under --root, evaluates every
|
|
466
|
+
rule against the graph, prints the report, and exits non-zero if the contract
|
|
467
|
+
is broken (2 = violations) so a pipeline can fail on it. An empty/absent
|
|
468
|
+
contract is a no-op success unless --require-contract is set. A contract that
|
|
469
|
+
could not be *read* is also 2 (R1-C27) — see below.
|
|
470
|
+
"""
|
|
471
|
+
from codemap.arch import check_contract, load_contract
|
|
472
|
+
from codemap.serve.check import render_check
|
|
473
|
+
q = Query(_graph_from(args))
|
|
474
|
+
contract = load_contract(args.root)
|
|
475
|
+
# R1-C27: check `error` before anything else. A malformed codemap.toml used to be
|
|
476
|
+
# indistinguishable from an absent one, so removing a single `]` turned a gate that
|
|
477
|
+
# reported 14 violations into exit 0. Exit 2 — the same code as a violation, not a new
|
|
478
|
+
# one: the status answers "may the pipeline proceed?" (no, either way), and a new code
|
|
479
|
+
# would sort an unreadable contract into the success branch of every `if rc == 2` that
|
|
480
|
+
# already exists in someone's pipeline.
|
|
481
|
+
if contract.error:
|
|
482
|
+
print(render_check(q, contract, []), end="")
|
|
483
|
+
return 2
|
|
484
|
+
if contract.is_empty() and args.require_contract:
|
|
485
|
+
print(render_check(q, contract, []), end="")
|
|
486
|
+
raise SystemExit("error: no [architecture] contract found (--require-contract)")
|
|
487
|
+
violations = check_contract(q, contract)
|
|
488
|
+
print(render_check(q, contract, violations), end="")
|
|
489
|
+
return 2 if violations else 0
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def _cmd_diff(args) -> int:
|
|
493
|
+
"""Two-graph API diff → added/removed/changed + breaking-change (R1-C5).
|
|
494
|
+
|
|
495
|
+
Renders the public-API delta between two graph.json snapshots. With
|
|
496
|
+
``--exit-code`` it behaves like a release gate: exit 1 when any breaking
|
|
497
|
+
change (a removed public symbol or an incompatible signature change) is found.
|
|
498
|
+
"""
|
|
499
|
+
from codemap.provenance import comparability
|
|
500
|
+
from codemap.serve.apidiff import build_apidiff, render_apidiff
|
|
501
|
+
old, new = store.load(args.old), store.load(args.new)
|
|
502
|
+
# R1-C25/D4: two graphs built by different tools are a before/after of the *tool*,
|
|
503
|
+
# not of the code. Never a refusal — comparing across an upgrade is legitimate; what
|
|
504
|
+
# was missing is being told, since a clean "no breaking changes" reads as proof.
|
|
505
|
+
cmp = comparability(old.provenance, new.provenance)
|
|
506
|
+
if not cmp["comparable"]:
|
|
507
|
+
for line in cmp["differences"]:
|
|
508
|
+
print(f"[warning] {line}", file=sys.stderr)
|
|
509
|
+
print(f"[warning] differences below may be tool changes, not code changes "
|
|
510
|
+
f"(old: {cmp['old']} | new: {cmp['new']})", file=sys.stderr)
|
|
511
|
+
print(render_apidiff(old, new), end="")
|
|
512
|
+
if args.exit_code and not build_apidiff(old, new)["ok"]:
|
|
513
|
+
return 1
|
|
514
|
+
return 0
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _cmd_serve(args) -> int:
|
|
518
|
+
"""Load the graph once, then serve it warm (M3.1).
|
|
519
|
+
|
|
520
|
+
Default transport is line-delimited JSON over stdio; ``--mcp`` serves the same
|
|
521
|
+
ops over the Model Context Protocol instead (needs the optional `mcp` extra).
|
|
522
|
+
"""
|
|
523
|
+
from codemap.serve.session import Session
|
|
524
|
+
session = Session(_graph_from(args), source_root=args.source_root,
|
|
525
|
+
graph_path=getattr(args, "graph", None))
|
|
526
|
+
if getattr(args, "mcp", False):
|
|
527
|
+
from codemap.serve.mcp_server import build_mcp_server
|
|
528
|
+
build_mcp_server(session).run("stdio")
|
|
529
|
+
return 0
|
|
530
|
+
from codemap.serve.server import serve_stdio
|
|
531
|
+
return serve_stdio(session)
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
def _emit(text: str, out: str | None) -> None:
|
|
535
|
+
if out:
|
|
536
|
+
Path(out).write_text(text, encoding="utf-8")
|
|
537
|
+
print(out)
|
|
538
|
+
else:
|
|
539
|
+
print(text, end="")
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
def _write_vault(files: dict[str, str], out_dir: str) -> None:
|
|
543
|
+
base = Path(out_dir)
|
|
544
|
+
for rel, content in files.items():
|
|
545
|
+
path = base / rel
|
|
546
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
547
|
+
path.write_text(content, encoding="utf-8")
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _add_source(p) -> None:
|
|
551
|
+
p.add_argument("--graph", help="Read an existing graph.json.")
|
|
552
|
+
p.add_argument("--build", help="Build fresh from this package path.")
|
|
553
|
+
p.add_argument("--deep", action="store_true",
|
|
554
|
+
help="Deep call resolution via jedi (richer, ~1 min; default fast).")
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
558
|
+
p = argparse.ArgumentParser(prog="codemap", description="Static code-graph builder.")
|
|
559
|
+
sub = p.add_subparsers(dest="command", required=True)
|
|
560
|
+
|
|
561
|
+
b = sub.add_parser("build", help="Build the canonical graph from a package path.")
|
|
562
|
+
b.add_argument("path", help="Path to the package directory (holds __init__.py).")
|
|
563
|
+
b.add_argument("-o", "--out", help="Write graph.json here (default: stdout JSON).")
|
|
564
|
+
b.add_argument("--deep", action="store_true",
|
|
565
|
+
help="Deep call resolution via jedi (richer, ~1 min; default fast).")
|
|
566
|
+
b.add_argument("--consumer", action="append", metavar="PATH",
|
|
567
|
+
help="Repo-scope: extra root that USES the core (tests/, examples/, "
|
|
568
|
+
"scripts/). Repeatable. Adds inbound refs for impact analysis.")
|
|
569
|
+
b.add_argument("--docs", action="append", metavar="PATH",
|
|
570
|
+
help="Repo-scope: docs root (*.md) → doc nodes + references. Repeatable.")
|
|
571
|
+
b.add_argument("--mode", choices=["thin", "full"], default="thin",
|
|
572
|
+
help="Consumer granularity: thin=per-file (default), full=per-function.")
|
|
573
|
+
b.add_argument("--incremental", action="store_true",
|
|
574
|
+
help="Reuse an existing --out graph + its scope sidecar: recompute "
|
|
575
|
+
"only changed modules (R1-C9). Identical to a full build; much "
|
|
576
|
+
"faster on --deep. Single-package only (no --consumer/--docs).")
|
|
577
|
+
b.set_defaults(func=_cmd_build)
|
|
578
|
+
|
|
579
|
+
sc = sub.add_parser("scope", help="Resolve the input scope manifest (scope_id + profile), or --diff two.")
|
|
580
|
+
sc.add_argument("path", nargs="?", help="Package/dir to scope (like build's path).")
|
|
581
|
+
sc.add_argument("--consumer", action="append", metavar="PATH",
|
|
582
|
+
help="Extra consumer root (tests/examples/scripts). Repeatable.")
|
|
583
|
+
sc.add_argument("--docs", action="append", metavar="PATH", help="Docs root. Repeatable.")
|
|
584
|
+
sc.add_argument("--no-git", action="store_true",
|
|
585
|
+
help="Force filesystem enumeration instead of git ls-files.")
|
|
586
|
+
sc.add_argument("--diff", nargs=2, metavar=("A", "B"),
|
|
587
|
+
help="Diff two scopes: each is a <graph>.meta.json (or a manifest JSON).")
|
|
588
|
+
sc.add_argument("--json", action="store_true", help="Full manifest as JSON (default: summary).")
|
|
589
|
+
sc.set_defaults(func=_cmd_scope)
|
|
590
|
+
|
|
591
|
+
q = sub.add_parser("query", help="Look up a symbol: where defined, deps both ways.")
|
|
592
|
+
q.add_argument("name", help="Short symbol name (e.g. analyze_zones).")
|
|
593
|
+
_add_source(q)
|
|
594
|
+
q.add_argument("--format", choices=["json", "text"], default="json")
|
|
595
|
+
q.set_defaults(func=_cmd_query)
|
|
596
|
+
|
|
597
|
+
t = sub.add_parser("tests", help="Which tests exercise a symbol (or --covers: the "
|
|
598
|
+
"inverse). Needs a repo-scoped graph.")
|
|
599
|
+
t.add_argument("name", help="Symbol (short or full), or a test id with --covers.")
|
|
600
|
+
_add_source(t)
|
|
601
|
+
t.add_argument("--covers", action="store_true",
|
|
602
|
+
help="Inverse: what does this test reach.")
|
|
603
|
+
t.add_argument("--depth", type=int, default=3,
|
|
604
|
+
help="Hops to search back. Beyond 3 the answer is low-confidence "
|
|
605
|
+
"(measured: precision 1.00 at 3 hops, 0.33 at 5).")
|
|
606
|
+
t.add_argument("--cap", type=int, default=25, help="Max tests listed (default 25).")
|
|
607
|
+
t.add_argument("--format", choices=["text", "json"], default="text")
|
|
608
|
+
t.set_defaults(func=_cmd_tests)
|
|
609
|
+
|
|
610
|
+
r = sub.add_parser("report", help="Render a report over the graph.")
|
|
611
|
+
r.add_argument("kind", choices=_REPORT_KINDS)
|
|
612
|
+
_add_source(r)
|
|
613
|
+
r.add_argument("--symbol", help="Symbol for `report impact` (short or full name).")
|
|
614
|
+
r.add_argument("--depth", type=int, default=2,
|
|
615
|
+
help="report impact: transitive BFS depth (default 2).")
|
|
616
|
+
r.add_argument("--min-confidence", choices=["low", "medium", "high"], default=None,
|
|
617
|
+
help="report dead-code: only show candidates at/above this confidence.")
|
|
618
|
+
r.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
|
619
|
+
r.set_defaults(func=_cmd_report)
|
|
620
|
+
|
|
621
|
+
e = sub.add_parser("export", help="Export a view: rag (JSONL) | vault | mermaid | scip | ctags | docs.")
|
|
622
|
+
e.add_argument("kind", choices=["rag", "vault", "mermaid", "scip", "ctags", "docs"])
|
|
623
|
+
_add_source(e)
|
|
624
|
+
e.add_argument("-o", "--out", help="Output file (rag/mermaid/scip/ctags) or dir (vault).")
|
|
625
|
+
e.add_argument("--mkind", choices=["class", "deps", "calls"], default="class",
|
|
626
|
+
help="mermaid diagram kind (default: class).")
|
|
627
|
+
e.add_argument("--scope", help="mermaid class/deps: restrict to this id-prefix.")
|
|
628
|
+
e.add_argument("--root", help="mermaid calls: root symbol.")
|
|
629
|
+
e.add_argument("--depth", type=int, default=2, help="mermaid calls: BFS depth.")
|
|
630
|
+
e.add_argument("--project-root", help="scip/ctags: filesystem root the paths are relative to "
|
|
631
|
+
"(default: cwd). SCIP writes it as project_root URI; "
|
|
632
|
+
"ctags reads source lines from it for /^…$/ addresses.")
|
|
633
|
+
e.add_argument("--package", help="scip: package name in symbol strings (default: graph target).")
|
|
634
|
+
e.add_argument("--package-version", default=".",
|
|
635
|
+
help="scip: package version in symbol strings (default: '.' — unversioned).")
|
|
636
|
+
e.set_defaults(func=_cmd_export)
|
|
637
|
+
|
|
638
|
+
rv = sub.add_parser("review", help="Change-set review from a unified diff → what to review.")
|
|
639
|
+
rv.add_argument("diff", nargs="?", default="-",
|
|
640
|
+
help="Unified-diff file (or '-'/omit for stdin, e.g. `git diff | codemap review`).")
|
|
641
|
+
_add_source(rv)
|
|
642
|
+
rv.add_argument("--base", help="Baseline graph.json to API-diff against (adds removed/"
|
|
643
|
+
"added/breaking symbols the hunks miss — R1-C5).")
|
|
644
|
+
rv.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
|
645
|
+
rv.set_defaults(func=_cmd_review)
|
|
646
|
+
|
|
647
|
+
s = sub.add_parser("serve", help="Warm resident process: JSON (or MCP) over stdin/stdout.")
|
|
648
|
+
_add_source(s)
|
|
649
|
+
s.add_argument("--source-root", help="Base dir for the `source` op to read files "
|
|
650
|
+
"(node paths are repo-relative; default: cwd).")
|
|
651
|
+
s.add_argument("--mcp", action="store_true",
|
|
652
|
+
help="Serve over the Model Context Protocol instead of JSON "
|
|
653
|
+
"(needs the optional 'mcp' extra: pip install 'codemap[mcp]').")
|
|
654
|
+
s.set_defaults(func=_cmd_serve)
|
|
655
|
+
|
|
656
|
+
rf = sub.add_parser("refresh", help="Rebuild a graph from the recipe recorded at build time.")
|
|
657
|
+
rf.add_argument("graph", help="Path to the graph.json to rebuild (needs its .meta.json sidecar).")
|
|
658
|
+
rf.set_defaults(func=_cmd_refresh)
|
|
659
|
+
|
|
660
|
+
rt = sub.add_parser("route", help="Forward a capability to an opt-in external tool (§13.1).")
|
|
661
|
+
rt.add_argument("capability", help="Capability to route, e.g. semantic-search.")
|
|
662
|
+
rt.add_argument("question", help="The query to forward to the tool.")
|
|
663
|
+
rt.add_argument("--root", default=".",
|
|
664
|
+
help="Dir with codemap.toml + the target tree (default: cwd).")
|
|
665
|
+
rt.set_defaults(func=_cmd_route)
|
|
666
|
+
|
|
667
|
+
sm = sub.add_parser("semantic", help="Semantic search via an opt-in adapter, "
|
|
668
|
+
"enriched to codemap symbols (R1-C16).")
|
|
669
|
+
sm.add_argument("query", help="Natural-language query (e.g. 'detect swing pivots').")
|
|
670
|
+
_add_source(sm)
|
|
671
|
+
sm.add_argument("--root", default=".",
|
|
672
|
+
help="Repo the adapter's index was built in + where paths resolve "
|
|
673
|
+
"(also holds codemap.toml [integrations]); default: cwd.")
|
|
674
|
+
sm.add_argument("--limit", type=int, default=10, help="Max hits (default: 10).")
|
|
675
|
+
sm.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
|
676
|
+
sm.set_defaults(func=_cmd_semantic)
|
|
677
|
+
|
|
678
|
+
pk = sub.add_parser("pack", help="Token-budgeted context pack: the most relevant "
|
|
679
|
+
"graph slice under N tokens (R1-C6).")
|
|
680
|
+
_add_source(pk)
|
|
681
|
+
pk.add_argument("--budget", type=int, default=2000, help="Max tokens (default: 2000).")
|
|
682
|
+
pk.add_argument("--seed", action="append", metavar="X",
|
|
683
|
+
help="Bias relevance to this symbol / file (repeatable). "
|
|
684
|
+
"Omit for global importance.")
|
|
685
|
+
pk.add_argument("--format", choices=["markdown", "json"], default="markdown")
|
|
686
|
+
pk.set_defaults(func=_cmd_pack)
|
|
687
|
+
|
|
688
|
+
df = sub.add_parser("diff", help="API diff two graph.json snapshots (added/removed/changed + breaking).")
|
|
689
|
+
df.add_argument("old", help="Baseline graph.json (the 'before').")
|
|
690
|
+
df.add_argument("new", help="Current graph.json (the 'after').")
|
|
691
|
+
df.add_argument("--exit-code", action="store_true",
|
|
692
|
+
help="Exit 1 if any breaking change is found (release gate).")
|
|
693
|
+
df.set_defaults(func=_cmd_diff)
|
|
694
|
+
|
|
695
|
+
ck = sub.add_parser("check", help="Enforce the [architecture] contract (CI gate; exit 2 on violation).")
|
|
696
|
+
_add_source(ck)
|
|
697
|
+
ck.add_argument("--root", default=".",
|
|
698
|
+
help="Dir with codemap.toml holding [architecture] (default: cwd).")
|
|
699
|
+
ck.add_argument("--require-contract", action="store_true",
|
|
700
|
+
help="Fail if no [architecture] contract is present (default: no-op success).")
|
|
701
|
+
ck.set_defaults(func=_cmd_check)
|
|
702
|
+
|
|
703
|
+
return p
|
|
704
|
+
|
|
705
|
+
|
|
706
|
+
def main(argv: list[str] | None = None) -> int:
|
|
707
|
+
raw = list(sys.argv[1:] if argv is None else argv)
|
|
708
|
+
args = build_parser().parse_args(raw)
|
|
709
|
+
args._argv = raw # M18: kept so `build` can record its own invocation (refresh)
|
|
710
|
+
try:
|
|
711
|
+
return args.func(args)
|
|
712
|
+
except Exception as exc: # noqa: BLE001 - CLI boundary: report, don't traceback
|
|
713
|
+
print(f"error: {exc}", file=sys.stderr)
|
|
714
|
+
return 1
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
if __name__ == "__main__":
|
|
718
|
+
raise SystemExit(main())
|