codebase-explorer 2.2.0__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.
- cbe/__init__.py +9 -0
- cbe/__main__.py +4 -0
- cbe/aggregation.py +183 -0
- cbe/api_completion.py +410 -0
- cbe/batching.py +283 -0
- cbe/catalog_query.py +376 -0
- cbe/checks.py +390 -0
- cbe/cli.py +434 -0
- cbe/concrete_facts.py +321 -0
- cbe/data/LICENSE.tiktoken +30 -0
- cbe/data/o200k_base.tiktoken +199998 -0
- cbe/declarations.py +537 -0
- cbe/docs.py +929 -0
- cbe/generate.py +316 -0
- cbe/grammars.py +741 -0
- cbe/graph.py +857 -0
- cbe/graph_backends/__init__.py +43 -0
- cbe/graph_backends/_calls.py +262 -0
- cbe/graph_backends/_paths.py +42 -0
- cbe/graph_backends/identity.py +93 -0
- cbe/graph_backends/imports.py +256 -0
- cbe/graph_backends/joern.py +1021 -0
- cbe/graph_backends/resolution.py +288 -0
- cbe/graph_backends/toolchain.py +396 -0
- cbe/grounding.py +57 -0
- cbe/host_run.py +1752 -0
- cbe/model_settings.py +80 -0
- cbe/projection.py +305 -0
- cbe/prompts.py +316 -0
- cbe/records.py +200 -0
- cbe/skeleton.py +474 -0
- cbe/source_context.py +709 -0
- cbe/symbols.py +136 -0
- cbe/token_budget.py +68 -0
- cbe/treename.py +398 -0
- cbe/usage.py +300 -0
- cbe_install/__init__.py +11 -0
- cbe_install/cli.py +434 -0
- cbe_install/resources/agents/cbe-writer.md +16 -0
- cbe_install/resources/codebase-explorer/SKILL.md +101 -0
- cbe_install/resources/codebase-explorer/references/claude-code.md +63 -0
- cbe_install/resources/codebase-explorer/references/codex.md +37 -0
- codebase_explorer-2.2.0.dist-info/METADATA +233 -0
- codebase_explorer-2.2.0.dist-info/RECORD +47 -0
- codebase_explorer-2.2.0.dist-info/WHEEL +4 -0
- codebase_explorer-2.2.0.dist-info/entry_points.txt +3 -0
- codebase_explorer-2.2.0.dist-info/licenses/LICENSE +21 -0
cbe/cli.py
ADDED
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
"""CLI unique entry. Schema is the argparse surface below."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _abs(value: str) -> Path:
|
|
12
|
+
path = Path(value).expanduser()
|
|
13
|
+
if not path.is_absolute():
|
|
14
|
+
raise argparse.ArgumentTypeError(f"expected an absolute path, got {value}")
|
|
15
|
+
return path
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _require_frozen_context(run_dir: Path, action: str, *, published_only: bool = False) -> None:
|
|
19
|
+
"""Say why a published example cannot do this. With ``published_only`` a directory that holds
|
|
20
|
+
no catalog either (empty, mistyped) keeps the command's own error."""
|
|
21
|
+
published = (run_dir / "catalog.json").is_file() or (run_dir / "catalog.json.gz").is_file()
|
|
22
|
+
if not (run_dir / "context.json").is_file() and (published or not published_only):
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"this directory has no frozen source snapshot (context.json), so {action}; "
|
|
25
|
+
"published examples ship only docs and the catalog — run cbe on a checkout to get a full run directory"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _UsageParser(argparse.ArgumentParser):
|
|
30
|
+
def error(self, message: str) -> None:
|
|
31
|
+
command = getattr(self, "example_command", self.prog.rsplit(" ", 1)[-1])
|
|
32
|
+
if command in {"find", "query"} and "example:" not in message:
|
|
33
|
+
value = "LIMIT" if command == "find" else "grp:example"
|
|
34
|
+
message += f"; example: cbe {command} --run-dir /absolute/run {value}"
|
|
35
|
+
super().error(message)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
39
|
+
from cbe.host_run import DEFAULT_REVIEW, DETAIL_MAX_ATTEMPTS, MAX_ATTEMPTS, REVIEW_MODES
|
|
40
|
+
from cbe.model_settings import MODEL_DEFAULTS
|
|
41
|
+
|
|
42
|
+
parser = _UsageParser(
|
|
43
|
+
prog="cbe",
|
|
44
|
+
description="Codebase Explorer: a navigable, source-grounded documentation map of a repository",
|
|
45
|
+
)
|
|
46
|
+
from cbe import __version__
|
|
47
|
+
|
|
48
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
49
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
50
|
+
|
|
51
|
+
generate_p = sub.add_parser("generate", help="Generate documentation through an OpenAI-compatible API")
|
|
52
|
+
generate_p.add_argument("repo", type=lambda value: Path(value).expanduser().resolve())
|
|
53
|
+
generate_p.add_argument("--run-dir", type=_abs, default=None)
|
|
54
|
+
generate_p.add_argument("--model", default=None)
|
|
55
|
+
generate_p.add_argument("--config", type=_abs, default=None,
|
|
56
|
+
help="optional API configuration file")
|
|
57
|
+
generate_p.add_argument("--graph", type=_abs, default=None, help="analyzed graph matching this source snapshot")
|
|
58
|
+
generate_p.add_argument("--base-url", default=None, help="OpenAI-compatible API base URL")
|
|
59
|
+
generate_p.add_argument("--api-key-env", default=None, help="environment variable containing the API key")
|
|
60
|
+
generate_p.add_argument("--jobs", type=int, default=4)
|
|
61
|
+
generate_p.add_argument("--review", choices=REVIEW_MODES, default=DEFAULT_REVIEW)
|
|
62
|
+
generate_p.add_argument("--resume", action="store_true",
|
|
63
|
+
help="continue the run in --run-dir (default: the repository's latest run)")
|
|
64
|
+
generate_p.add_argument("--timeout", type=int, default=None,
|
|
65
|
+
help="seconds per call; overrides API config/model default (otherwise 420). "
|
|
66
|
+
"API model defaults: " + json.dumps(MODEL_DEFAULTS, sort_keys=True) +
|
|
67
|
+
"; override via --config JSON keys timeout,output_max_tokens,reasoning_effort,"
|
|
68
|
+
"response_format,batch_target_tokens,batch_max_tokens,price_per_million")
|
|
69
|
+
|
|
70
|
+
host_p = sub.add_parser("host", help="Host-agent driven generation: the CLI plans and checks, "
|
|
71
|
+
"the host agent (Claude Code, Codex) dispatches one subagent per task")
|
|
72
|
+
host_sub = host_p.add_subparsers(dest="host_command", required=True)
|
|
73
|
+
host_plan = host_sub.add_parser("plan", help="Freeze source and graph context and plan semantic generation")
|
|
74
|
+
host_plan.add_argument("repo", type=lambda value: Path(value).expanduser().resolve())
|
|
75
|
+
host_plan.add_argument("--run-dir", type=_abs, default=None)
|
|
76
|
+
host_plan.add_argument("--graph", type=_abs, default=None, help="analyzed graph matching this source snapshot")
|
|
77
|
+
host_plan.add_argument("--review", choices=REVIEW_MODES, default=DEFAULT_REVIEW)
|
|
78
|
+
host_plan.add_argument("--jobs", type=int, default=4, help="maximum concurrently dispatched subagents")
|
|
79
|
+
host_plan.add_argument("--host", default="other", help="label only, e.g. claude-code or codex")
|
|
80
|
+
for name, text in (("next", "Lease ready tasks up to the concurrency limit and write their instruction files"),
|
|
81
|
+
("submit", f"Check one subagent result: detail (module/repair) allows {DETAIL_MAX_ATTEMPTS} answers, "
|
|
82
|
+
f"others {MAX_ATTEMPTS}; a detail re-ask lists only the gaps and the last detail answer "
|
|
83
|
+
"indexes what stays unexplained; on the final invalid answer of other tasks "
|
|
84
|
+
"normalize usable content or fail"),
|
|
85
|
+
("status", "Show every task's state, attempts, timing, output, and the next action"),
|
|
86
|
+
("resume", "Continue from the run records: adopt finished results, release lost "
|
|
87
|
+
"leases, retry failed tasks"),
|
|
88
|
+
("retry", "Give a failed task (or `all`) fresh answers: detail (module/repair) allows "
|
|
89
|
+
f"{DETAIL_MAX_ATTEMPTS}, others {MAX_ATTEMPTS}"),
|
|
90
|
+
("release", "Return leased tasks to the queue"),
|
|
91
|
+
("render", "Render docs now; pending parts get placeholder pages"),
|
|
92
|
+
("verify", "Coverage, link, catalogue, and source-drift checks"),
|
|
93
|
+
("import-usage", "Read exact per-subagent usage recorded by Claude Code or Codex"),
|
|
94
|
+
("cost", "Write the cost report: exact where imported, otherwise labelled estimates")):
|
|
95
|
+
part = host_sub.add_parser(name, help=text)
|
|
96
|
+
part.add_argument("--run-dir", type=_abs, default=None)
|
|
97
|
+
part.add_argument("--repo", type=_abs, default=None, help="use the latest run of this repository")
|
|
98
|
+
if name == "next":
|
|
99
|
+
part.add_argument("--max", type=int, default=None)
|
|
100
|
+
part.add_argument("--owner", default=None)
|
|
101
|
+
part.add_argument("--reclaim-after", type=int, default=None,
|
|
102
|
+
help="release leases older than this many seconds first")
|
|
103
|
+
if name in ("submit", "retry", "release"):
|
|
104
|
+
part.add_argument("--task", required=name != "release",
|
|
105
|
+
help="task id" + ("; `all` retries every failed task" if name == "retry" else ""))
|
|
106
|
+
if name == "submit":
|
|
107
|
+
part.add_argument("--result", type=_abs, default=None)
|
|
108
|
+
part.add_argument("--usage-tokens", type=int, default=None,
|
|
109
|
+
help="token total the harness reported for this subagent")
|
|
110
|
+
part.add_argument("--tool-uses", type=int, default=None)
|
|
111
|
+
part.add_argument("--duration-ms", type=int, default=None)
|
|
112
|
+
part.add_argument("--usage-source", default=None)
|
|
113
|
+
if name == "resume":
|
|
114
|
+
part.add_argument("--owner", default=None)
|
|
115
|
+
part.add_argument("--keep-failed", action="store_true",
|
|
116
|
+
help="do not give failed tasks fresh attempts")
|
|
117
|
+
if name == "import-usage":
|
|
118
|
+
part.add_argument("--harness", choices=("claude-code", "codex"), required=True)
|
|
119
|
+
part.add_argument("--root", type=_abs, default=None,
|
|
120
|
+
help="Claude projects/session directory or Codex sessions directory")
|
|
121
|
+
if name == "cost":
|
|
122
|
+
part.add_argument("--model", default=None, help="model used for estimated tasks, e.g. claude-sonnet-5-5")
|
|
123
|
+
part.add_argument("--price", default=None,
|
|
124
|
+
help="USD per million tokens: input,output,cache_write,cache_read")
|
|
125
|
+
part.add_argument("--overhead", type=int, default=None,
|
|
126
|
+
help="estimated harness tokens per subagent turn context")
|
|
127
|
+
|
|
128
|
+
update_graph = host_sub.add_parser(
|
|
129
|
+
"update-graph", help="Replace a completed run's graph when only its dataflow layer changed")
|
|
130
|
+
update_graph.add_argument("--run-dir", type=_abs, required=True)
|
|
131
|
+
update_graph.add_argument("--graph", type=_abs, required=True, help="rebuilt graph for the same source snapshot")
|
|
132
|
+
|
|
133
|
+
sub.add_parser("languages", help="Show source grammars, extensions, and availability")
|
|
134
|
+
|
|
135
|
+
graph_p = sub.add_parser("graph", help="Build and query source-linked code graphs")
|
|
136
|
+
graph_sub = graph_p.add_subparsers(dest="graph_command", required=True)
|
|
137
|
+
graph_setup = graph_sub.add_parser(
|
|
138
|
+
"setup", help="Download and unpack Joern and a Java runtime into the user cache directory")
|
|
139
|
+
graph_setup.add_argument("--check", action="store_true",
|
|
140
|
+
help="download nothing; report the Joern and Java found and what is missing")
|
|
141
|
+
graph_setup.add_argument("--force", action="store_true", help="download and unpack again even if installed")
|
|
142
|
+
graph_setup.add_argument("--with-java", action="store_true",
|
|
143
|
+
help="download a JRE even when Java 21+ is already available")
|
|
144
|
+
graph_build = graph_sub.add_parser("build", help="Run the configured open-source analyzers")
|
|
145
|
+
graph_build.add_argument("repo", type=lambda value: Path(value).expanduser().resolve())
|
|
146
|
+
graph_build.add_argument("--output", type=_abs, required=True, help="graph JSON output path")
|
|
147
|
+
graph_build.add_argument("--backend", default="auto", help="analyzer selection; default: auto")
|
|
148
|
+
graph_build.add_argument("--language", action="append", dest="languages", default=None)
|
|
149
|
+
graph_build.add_argument("--joern-home", type=_abs, default=None)
|
|
150
|
+
graph_build.add_argument("--joern-heap", default=None, metavar="SIZE",
|
|
151
|
+
help="Joern JVM maximum heap via JAVA_TOOL_OPTIONS (for example 8g); default: inherit JVM settings")
|
|
152
|
+
graph_build.add_argument("--depcruise-bin", type=_abs, default=None)
|
|
153
|
+
graph_build.add_argument("--timeout", type=int, default=300)
|
|
154
|
+
graph_build.add_argument("--max-methods", type=int, default=None,
|
|
155
|
+
help="maximum methods for CFG export per frontend; default: unlimited; omitted methods are reported")
|
|
156
|
+
graph_build.add_argument("--with-dataflow", action="store_true",
|
|
157
|
+
help="also export dataflow (reaching definitions); off by default. "
|
|
158
|
+
"--max-reaching-defs and --dataflow-path-method imply it")
|
|
159
|
+
graph_build.add_argument("--max-reaching-defs", type=int, default=None,
|
|
160
|
+
help="cap on reaching-definition edges (default 50000 when dataflow is exported)")
|
|
161
|
+
graph_build.add_argument("--dataflow-path-method", action="append", dest="dataflow_path_methods",
|
|
162
|
+
help="opt in to sampled value-flow paths for a named method")
|
|
163
|
+
graph_build.add_argument("--max-paths-per-method", type=int, default=5)
|
|
164
|
+
graph_query = graph_sub.add_parser("query", help="Select relations and their boundary nodes")
|
|
165
|
+
graph_query.add_argument("--graph", type=_abs, required=True,
|
|
166
|
+
help="cbe-graph/1 JSON or a frozen context.json containing a graph")
|
|
167
|
+
graph_query.add_argument("--path", action="append", dest="paths", default=None)
|
|
168
|
+
graph_query.add_argument("--kind", choices=("import", "call", "cfg", "dataflow"), default=None)
|
|
169
|
+
graph_query.add_argument("--direction", choices=("both", "in", "out"), default="both")
|
|
170
|
+
graph_query.add_argument("--node", dest="node_id", default=None)
|
|
171
|
+
graph_caps = graph_sub.add_parser("capabilities", help="Read the analyzed language/type coverage")
|
|
172
|
+
graph_caps.add_argument("--graph", type=_abs, required=True)
|
|
173
|
+
graph_caps.add_argument("--repo", type=_abs, help="Recompute quality with a live repository-file snapshot")
|
|
174
|
+
|
|
175
|
+
context_p = sub.add_parser("context", help="Build or select frozen code chunks with graph evidence")
|
|
176
|
+
context_sub = context_p.add_subparsers(dest="context_command", required=True)
|
|
177
|
+
context_build = context_sub.add_parser("build", help="Parse source and validate an optional graph snapshot")
|
|
178
|
+
context_build.add_argument("repo", type=lambda value: Path(value).expanduser().resolve())
|
|
179
|
+
context_build.add_argument("--output", type=_abs, required=True)
|
|
180
|
+
context_build.add_argument("--graph", type=_abs, default=None)
|
|
181
|
+
context_build.add_argument("--path", action="append", dest="paths", default=None)
|
|
182
|
+
context_query = context_sub.add_parser("query", help="Select code chunks and their graph boundary")
|
|
183
|
+
context_query.add_argument("--context", type=_abs, required=True)
|
|
184
|
+
context_query.add_argument("--path", action="append", dest="paths", required=True)
|
|
185
|
+
|
|
186
|
+
find_p = sub.add_parser("find", help="Search the generated symbol catalogue")
|
|
187
|
+
find_p.set_defaults(usage_parser=find_p)
|
|
188
|
+
find_p.add_argument("--run-dir", type=_abs, required=True)
|
|
189
|
+
find_p.add_argument("term_pos", nargs="?", metavar="TERM")
|
|
190
|
+
find_p.add_argument("--term")
|
|
191
|
+
find_p.add_argument("--limit", type=int, default=25)
|
|
192
|
+
find_p.add_argument("--json", action="store_true", help="machine-readable locator fields")
|
|
193
|
+
|
|
194
|
+
query_p = sub.add_parser("query", help="Return explanation and evidence by unit ID, symbol ID or file path")
|
|
195
|
+
query_p.set_defaults(usage_parser=query_p)
|
|
196
|
+
query_p.add_argument("--run-dir", type=_abs, required=True)
|
|
197
|
+
query_p.add_argument("id_pos", nargs="?", metavar="ID")
|
|
198
|
+
query_p.add_argument("--id")
|
|
199
|
+
query_p.add_argument("--json", action="store_true", help="machine-readable object (no duplicate detail)")
|
|
200
|
+
query_p.add_argument("--edges", default=None, help="expand frozen raw edges: all or comma-separated kinds")
|
|
201
|
+
source_p = sub.add_parser("source", help="Print frozen source by unit ID, symbol ID or file path; verify SHA-256 first")
|
|
202
|
+
source_p.add_argument("--run-dir", type=_abs, required=True)
|
|
203
|
+
source_p.add_argument("--id", required=True)
|
|
204
|
+
source_p.add_argument("--context", type=int, default=0, metavar="N",
|
|
205
|
+
help="include N surrounding lines (nonnegative; default: 0)")
|
|
206
|
+
for source_parser in (generate_p, host_plan, context_build, graph_build):
|
|
207
|
+
source_parser.add_argument("--header-language", choices=("c", "cpp"), default=None,
|
|
208
|
+
help="interpret .h files as C or C++ when planning source context")
|
|
209
|
+
|
|
210
|
+
return parser
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _host(args: argparse.Namespace):
|
|
214
|
+
from cbe import host_run, usage
|
|
215
|
+
|
|
216
|
+
command = args.host_command
|
|
217
|
+
if command == "plan":
|
|
218
|
+
return host_run.plan(args.repo, args.run_dir, review=args.review, jobs=args.jobs, host=args.host,
|
|
219
|
+
graph_path=args.graph)
|
|
220
|
+
if command == "update-graph":
|
|
221
|
+
return host_run.update_graph(args.run_dir, args.graph)
|
|
222
|
+
run_dir = args.run_dir or (host_run.latest_run(args.repo) if args.repo else None)
|
|
223
|
+
if run_dir is None:
|
|
224
|
+
raise host_run.HostRunError("pass --run-dir, or --repo to use the repository's latest run")
|
|
225
|
+
context_actions = {
|
|
226
|
+
"verify": "host verification cannot run",
|
|
227
|
+
"render": "host documents cannot be rendered",
|
|
228
|
+
"status": "host run status cannot be read",
|
|
229
|
+
"cost": "host run cost cannot be reported",
|
|
230
|
+
}
|
|
231
|
+
if command in context_actions:
|
|
232
|
+
_require_frozen_context(run_dir, context_actions[command], published_only=True)
|
|
233
|
+
if command == "next":
|
|
234
|
+
return host_run.next_tasks(run_dir, limit=args.max, owner=args.owner, reclaim_after=args.reclaim_after)
|
|
235
|
+
if command == "submit":
|
|
236
|
+
reported = {"total_tokens": args.usage_tokens, "tool_uses": args.tool_uses,
|
|
237
|
+
"duration_ms": args.duration_ms, "source": args.usage_source}
|
|
238
|
+
return host_run.submit(run_dir, args.task, result=args.result, usage=reported)
|
|
239
|
+
if command == "status":
|
|
240
|
+
return host_run.status(run_dir)
|
|
241
|
+
if command == "resume":
|
|
242
|
+
return host_run.resume(run_dir, owner=args.owner, retry_failed=not args.keep_failed)
|
|
243
|
+
if command == "retry":
|
|
244
|
+
return host_run.retry(run_dir, args.task)
|
|
245
|
+
if command == "release":
|
|
246
|
+
return host_run.release(run_dir, args.task)
|
|
247
|
+
if command == "render":
|
|
248
|
+
return host_run.render(run_dir)
|
|
249
|
+
if command == "verify":
|
|
250
|
+
return host_run.verify(run_dir)
|
|
251
|
+
if command == "import-usage":
|
|
252
|
+
return usage.import_usage(run_dir, args.harness, args.root)
|
|
253
|
+
price = None
|
|
254
|
+
if args.price:
|
|
255
|
+
parts = [float(value) for value in args.price.split(",")]
|
|
256
|
+
if len(parts) != 4:
|
|
257
|
+
raise host_run.HostRunError("--price needs input,output,cache_write,cache_read")
|
|
258
|
+
price = tuple(parts)
|
|
259
|
+
return usage.cost(run_dir, model=args.model, price=price,
|
|
260
|
+
overhead=args.overhead or usage.DEFAULT_OVERHEAD_TOKENS)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def main(argv: list[str] | None = None) -> int:
|
|
264
|
+
from cbe.grammars import ContextError
|
|
265
|
+
|
|
266
|
+
parser = build_parser()
|
|
267
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
268
|
+
parser.example_command = argv[0] if argv else ""
|
|
269
|
+
args = parser.parse_args(argv)
|
|
270
|
+
if args.command in {"find", "query"}:
|
|
271
|
+
field = "term" if args.command == "find" else "id"
|
|
272
|
+
positional, flagged = getattr(args, field + "_pos"), getattr(args, field)
|
|
273
|
+
if positional is None and flagged is None:
|
|
274
|
+
args.usage_parser.error(f"missing {field}")
|
|
275
|
+
if positional is not None and flagged is not None and positional != flagged:
|
|
276
|
+
args.usage_parser.error(f"positional {field} conflicts with --{field}")
|
|
277
|
+
setattr(args, field, flagged if flagged is not None else positional)
|
|
278
|
+
try:
|
|
279
|
+
header_language = getattr(args, "header_language", None)
|
|
280
|
+
if header_language:
|
|
281
|
+
from dataclasses import replace
|
|
282
|
+
from cbe.grammars import GRAMMARS, register_language
|
|
283
|
+
|
|
284
|
+
spec = next(spec for spec in GRAMMARS if spec.name == header_language)
|
|
285
|
+
register_language(replace(spec, suffixes=tuple(dict.fromkeys((*spec.suffixes, ".h")))))
|
|
286
|
+
if args.command == "languages":
|
|
287
|
+
from cbe.grammars import available_languages
|
|
288
|
+
|
|
289
|
+
print(json.dumps(available_languages(), ensure_ascii=False, indent=2))
|
|
290
|
+
return 0
|
|
291
|
+
if args.command == "graph":
|
|
292
|
+
from cbe.graph import analyze_graph, query_graph, read_graph_quality, GraphError, BACKEND_FAILURE_CODES
|
|
293
|
+
|
|
294
|
+
build_error = None
|
|
295
|
+
if args.graph_command == "setup":
|
|
296
|
+
from cbe.graph_backends import toolchain
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
value = (toolchain.check() if args.check else
|
|
300
|
+
toolchain.setup(force=args.force, with_java=args.with_java))
|
|
301
|
+
except toolchain.SetupError as exc:
|
|
302
|
+
print(f"cbe graph setup: {exc}", file=sys.stderr)
|
|
303
|
+
return 2
|
|
304
|
+
print(json.dumps(value, ensure_ascii=False, indent=2))
|
|
305
|
+
return 2 if value["missing"] else 0
|
|
306
|
+
if args.graph_command == "build":
|
|
307
|
+
try:
|
|
308
|
+
graph = analyze_graph(args.repo, args.output, backend=args.backend,
|
|
309
|
+
languages=args.languages, joern_home=args.joern_home,
|
|
310
|
+
joern_heap=args.joern_heap,
|
|
311
|
+
depcruise_bin=args.depcruise_bin, timeout=args.timeout,
|
|
312
|
+
max_methods=args.max_methods, max_reaching_defs=args.max_reaching_defs,
|
|
313
|
+
dataflow_path_methods=args.dataflow_path_methods,
|
|
314
|
+
max_paths_per_method=args.max_paths_per_method,
|
|
315
|
+
with_dataflow=args.with_dataflow)
|
|
316
|
+
except GraphError as exc:
|
|
317
|
+
if exc.graph is None:
|
|
318
|
+
raise
|
|
319
|
+
graph, build_error = exc.graph, str(exc)
|
|
320
|
+
quality = read_graph_quality(graph)
|
|
321
|
+
failures = [d["message"] for d in graph["diagnostics"]
|
|
322
|
+
if d.get("code") in BACKEND_FAILURE_CODES]
|
|
323
|
+
failures += [f"{c.get('language')}/{c.get('graph')}: {c.get('status')}"
|
|
324
|
+
for c in quality["incomplete_capabilities"] if c.get("status") == "failed"]
|
|
325
|
+
if quality["dangling_edges"]["count"]:
|
|
326
|
+
failures.append(f"dangling edges: {quality['dangling_edges']['count']} "
|
|
327
|
+
f"{quality['dangling_edges']['examples']}")
|
|
328
|
+
if build_error:
|
|
329
|
+
failures.append(build_error)
|
|
330
|
+
if failures:
|
|
331
|
+
print("cbe graph build: " + "; ".join(failures), file=sys.stderr)
|
|
332
|
+
value = {"output": str(args.output), "schema": graph["schema"],
|
|
333
|
+
"backend": graph["backend"], "files": len(graph["source_manifest"]),
|
|
334
|
+
"nodes": len(graph["nodes"]), "edges": len(graph["edges"]),
|
|
335
|
+
"quality": quality,
|
|
336
|
+
"capabilities": graph["capabilities"], "diagnostics": graph["diagnostics"]}
|
|
337
|
+
else:
|
|
338
|
+
graph = json.loads(args.graph.read_text(encoding="utf-8"))
|
|
339
|
+
if isinstance(graph, dict) and graph.get("schema") == "cbe-context/1":
|
|
340
|
+
graph = graph.get("graph")
|
|
341
|
+
if not isinstance(graph, dict):
|
|
342
|
+
raise ValueError("input contains no graph; this run may have been planned without one")
|
|
343
|
+
if args.graph_command == "capabilities":
|
|
344
|
+
if graph.get("schema") != "cbe-graph/1":
|
|
345
|
+
raise ValueError("expected a cbe-graph/1 file")
|
|
346
|
+
if args.repo is not None:
|
|
347
|
+
from cbe.graph_backends.identity import repository_files
|
|
348
|
+
quality = read_graph_quality(graph, existing_paths=repository_files(args.repo))
|
|
349
|
+
else:
|
|
350
|
+
quality = read_graph_quality(graph)
|
|
351
|
+
value = {"backend": graph.get("backend"), "quality": quality,
|
|
352
|
+
"capabilities": graph.get("capabilities", []),
|
|
353
|
+
"diagnostics": graph.get("diagnostics", [])}
|
|
354
|
+
else:
|
|
355
|
+
value = query_graph(graph, paths=args.paths, kind=args.kind,
|
|
356
|
+
direction=args.direction, node_id=args.node_id)
|
|
357
|
+
print(json.dumps(value, ensure_ascii=False, indent=2))
|
|
358
|
+
return 2 if build_error else 0
|
|
359
|
+
if args.command == "context":
|
|
360
|
+
from cbe.source_context import build_context, context_for_paths
|
|
361
|
+
|
|
362
|
+
if args.context_command == "build":
|
|
363
|
+
value = build_context(args.repo, args.paths, graph_path=args.graph)
|
|
364
|
+
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
365
|
+
args.output.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
366
|
+
result = {"output": str(args.output), "schema": value["schema"],
|
|
367
|
+
"files": len(value["files"]), "chunks": len(value["chunks"]),
|
|
368
|
+
"diagnostics": value["diagnostics"]}
|
|
369
|
+
else:
|
|
370
|
+
value = json.loads(args.context.read_text(encoding="utf-8"))
|
|
371
|
+
result = context_for_paths(value, args.paths)
|
|
372
|
+
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
373
|
+
return 0
|
|
374
|
+
if args.command == "generate":
|
|
375
|
+
from datetime import datetime, timezone
|
|
376
|
+
from uuid import uuid4
|
|
377
|
+
from cbe.generate import generate
|
|
378
|
+
|
|
379
|
+
repo = args.repo.resolve()
|
|
380
|
+
if args.resume and args.run_dir is None:
|
|
381
|
+
from cbe.host_run import latest_run
|
|
382
|
+
args.run_dir = latest_run(repo)
|
|
383
|
+
run_dir = args.run_dir or (repo / ".codebase-analysis" / "runs" /
|
|
384
|
+
(datetime.now(timezone.utc).strftime("generate-%Y%m%dT%H%M%SZ-") + uuid4().hex[:8]))
|
|
385
|
+
if not args.model:
|
|
386
|
+
raise ValueError("generate requires --model (the model ID at your endpoint); "
|
|
387
|
+
"set the endpoint with --base-url and the key variable with --api-key-env")
|
|
388
|
+
if args.resume and header_language:
|
|
389
|
+
frozen = json.loads((run_dir / "context.json").read_text(encoding="utf-8"))
|
|
390
|
+
if any(row["language"] != header_language for row in frozen["files"]
|
|
391
|
+
if Path(row["path"]).suffix.lower() == ".h"):
|
|
392
|
+
raise ValueError("header language differs from the frozen context; start a new plan")
|
|
393
|
+
value = generate(repo, run_dir, model=args.model, config=args.config,
|
|
394
|
+
jobs=args.jobs, review=args.review, timeout=args.timeout, default_timeout=420,
|
|
395
|
+
resume=args.resume, graph_path=args.graph,
|
|
396
|
+
api_base_url=args.base_url, api_key_env=args.api_key_env)
|
|
397
|
+
print(json.dumps(value, ensure_ascii=False, indent=2))
|
|
398
|
+
# 3 means docs were rendered but some tasks are still pending.
|
|
399
|
+
return 0 if value.get("status") == "complete" else 3
|
|
400
|
+
if args.command == "host":
|
|
401
|
+
value = _host(args)
|
|
402
|
+
print(json.dumps(value, ensure_ascii=False, indent=2))
|
|
403
|
+
return 3 if args.host_command == "verify" and not value["ok"] else 0
|
|
404
|
+
from cbe.catalog_query import find, find_text, query, query_text, source
|
|
405
|
+
|
|
406
|
+
if args.command == "source":
|
|
407
|
+
_require_frozen_context(args.run_dir, "source lines cannot be printed")
|
|
408
|
+
print(source(args.run_dir, args.id, context=args.context), end="")
|
|
409
|
+
return 0
|
|
410
|
+
|
|
411
|
+
if args.command == "find":
|
|
412
|
+
results = find(args.run_dir, args.term, limit=args.limit)
|
|
413
|
+
print(json.dumps(results, ensure_ascii=False, indent=2) if args.json else find_text(results),
|
|
414
|
+
end="\n" if args.json else "")
|
|
415
|
+
return 0
|
|
416
|
+
if args.edges:
|
|
417
|
+
_require_frozen_context(args.run_dir, "raw edges cannot be expanded")
|
|
418
|
+
record = query(args.run_dir, args.id, edges=args.edges)
|
|
419
|
+
print(json.dumps(record, ensure_ascii=False, indent=2) if args.json else query_text(record),
|
|
420
|
+
end="\n" if args.json else "")
|
|
421
|
+
return 0
|
|
422
|
+
except ContextError as exc:
|
|
423
|
+
print(json.dumps(exc.to_dict(), ensure_ascii=False), file=sys.stderr)
|
|
424
|
+
return 2
|
|
425
|
+
except KeyError as exc:
|
|
426
|
+
print(f"cbe: internal error: missing key {exc.args[0]}", file=sys.stderr)
|
|
427
|
+
return 2
|
|
428
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
429
|
+
print(str(exc), file=sys.stderr)
|
|
430
|
+
return 2
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
if __name__ == "__main__":
|
|
434
|
+
raise SystemExit(main())
|