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/generate.py
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Script-driven, single-turn codebase documentation generation.
|
|
2
|
+
|
|
3
|
+
The controller is ordinary Python: there is no model parent session and no
|
|
4
|
+
per-task native handoff. Every host completion is a fresh, tool-free session.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import csv
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
import time
|
|
13
|
+
from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from cbe import batching, usage
|
|
18
|
+
from cbe.api_completion import FIELDS, CSV_LOCK, invoke
|
|
19
|
+
from cbe.model_settings import DEFAULT_API_CONFIG, load_config
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GenerateError(ValueError):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SourceNotEmbedded(GenerateError):
|
|
27
|
+
"""A script-driver prompt lacks a source body it must embed before any model call.
|
|
28
|
+
|
|
29
|
+
The v2.1.0 failure mode: modules were rebuilt from run state that keeps
|
|
30
|
+
digests, not text, so every prompt named the files but carried empty bodies.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Module and repair detail tasks allow 3 answers; other tasks allow 2 (see
|
|
35
|
+
# host_run). The provider's own pushback (HTTP 429/502/503/504) is waited out inside the
|
|
36
|
+
# call and never uses an answer attempt. A task whose call still ends in
|
|
37
|
+
# ``provider_limit`` goes back to the queue and the whole run pauses; the run
|
|
38
|
+
# stops only after this many such calls in a row with no response in between.
|
|
39
|
+
LIMIT_STREAK_STOP = 3
|
|
40
|
+
LIMIT_PAUSE_SECONDS = 60
|
|
41
|
+
_sleep = time.sleep
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _mark_result(csv_path: Path, task: str, attempt: int, result: str) -> None:
|
|
45
|
+
# This runs after invoke has appended its row. Other workers may append
|
|
46
|
+
# rows meanwhile, so rewrite under the same lock invoke uses.
|
|
47
|
+
with CSV_LOCK:
|
|
48
|
+
with csv_path.open(newline="", encoding="utf-8") as stream:
|
|
49
|
+
rows = list(csv.DictReader(stream))
|
|
50
|
+
for row in reversed(rows):
|
|
51
|
+
if row["task"] == task and row["attempt"] == str(attempt):
|
|
52
|
+
row["result"] = result
|
|
53
|
+
break
|
|
54
|
+
with csv_path.open("w", newline="", encoding="utf-8") as stream:
|
|
55
|
+
writer = csv.DictWriter(stream, fieldnames=FIELDS)
|
|
56
|
+
writer.writeheader()
|
|
57
|
+
writer.writerows(rows)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# A script completion has no file tools, so the model must have been sent the source text. If the
|
|
61
|
+
# model reports far fewer input tokens than the source it was sent, it never saw the code.
|
|
62
|
+
DELIVERY_FLOOR = 0.5
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _delivery_problem(run_dir: Path) -> tuple[str | None, str]:
|
|
66
|
+
"""Report delivery failure only when a source-bearing call proved it.
|
|
67
|
+
|
|
68
|
+
Failed calls have no reported usage. Comparing their missing counts to all
|
|
69
|
+
source sent confuses timeout/provider failures with missing source.
|
|
70
|
+
"""
|
|
71
|
+
state = json.loads((run_dir / "host" / "state.json").read_text(encoding="utf-8"))
|
|
72
|
+
failures = [(task_id, task["errors"][-1]) for task_id, task in state["tasks"].items()
|
|
73
|
+
if task["status"] == "failed" and task["errors"]
|
|
74
|
+
and task["errors"][-1].startswith("source_not_delivered")]
|
|
75
|
+
if failures:
|
|
76
|
+
return "source_not_delivered", "; ".join(f"{task}: {error}" for task, error in failures)
|
|
77
|
+
return None, ""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Tasks whose prompt embeds source bodies; tree naming and system prompts embed none.
|
|
81
|
+
SOURCE_TASK_KINDS = ("module", "repair", "review")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _embedded_paths(state: dict[str, Any], task_id: str) -> list[str]:
|
|
85
|
+
"""Module file paths a task's prompt must embed; empty when it embeds none."""
|
|
86
|
+
task = state["tasks"][task_id]
|
|
87
|
+
if task["kind"] not in SOURCE_TASK_KINDS:
|
|
88
|
+
return []
|
|
89
|
+
return list(state["modules"][task["module"]]["files"])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _verify_embedded_source(run_dir: Path, task_id: str, prompt: str) -> None:
|
|
93
|
+
"""Pre-flight before each script-driver model call: every file the task's
|
|
94
|
+
prompt must embed is present in the prompt with its full body, read back
|
|
95
|
+
from disk and checked against the plan digest.
|
|
96
|
+
|
|
97
|
+
Under the semantic mainline the prompt embeds the frozen context chunks
|
|
98
|
+
rather than whole files, so the check is that each of the task's chunks is
|
|
99
|
+
present verbatim in the prompt. Raises :class:`SourceNotEmbedded` naming the
|
|
100
|
+
file, so the run stops before the call is spent."""
|
|
101
|
+
state = json.loads((run_dir / "host" / "state.json").read_text(encoding="utf-8"))
|
|
102
|
+
paths = _embedded_paths(state, task_id)
|
|
103
|
+
if not paths:
|
|
104
|
+
return
|
|
105
|
+
planned = {row["path"]: row for row in state["files"]}
|
|
106
|
+
repo = Path(state["repo"])
|
|
107
|
+
for path in paths:
|
|
108
|
+
row = planned[path]
|
|
109
|
+
try:
|
|
110
|
+
raw = (repo / path).read_bytes()
|
|
111
|
+
except OSError as exc:
|
|
112
|
+
raise SourceNotEmbedded(f"{path}: cannot be read ({exc.strerror or exc})") from exc
|
|
113
|
+
if hashlib.sha256(raw).hexdigest() != row["digest"]:
|
|
114
|
+
raise SourceNotEmbedded(f"{path}: changed since the plan (digest mismatch)")
|
|
115
|
+
body = raw.decode("utf-8")
|
|
116
|
+
if not body and not row["tokens"]:
|
|
117
|
+
continue # an empty file embeds nothing by design
|
|
118
|
+
if body not in prompt:
|
|
119
|
+
raise SourceNotEmbedded(
|
|
120
|
+
f"{path}: not embedded in the prompt ({row['tokens']} planned tokens); the model was not called")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _call_delivery_problem(run_dir: Path, task_id: str,
|
|
124
|
+
usage: dict[str, int | None] | None) -> str | None:
|
|
125
|
+
"""Per-call delivery check: reject an answer unless the model reported at
|
|
126
|
+
least the delivery floor of the source tokens embedded in its own prompt.
|
|
127
|
+
|
|
128
|
+
Returns None when the call is fine or nothing was reported (the caller
|
|
129
|
+
records ``usage_unknown`` for silent hosts instead of failing)."""
|
|
130
|
+
if not usage:
|
|
131
|
+
return None
|
|
132
|
+
state = json.loads((run_dir / "host" / "state.json").read_text(encoding="utf-8"))
|
|
133
|
+
paths = _embedded_paths(state, task_id)
|
|
134
|
+
if not paths:
|
|
135
|
+
return None
|
|
136
|
+
planned = {row["path"]: row["tokens"] for row in state["files"]}
|
|
137
|
+
sent = sum(planned[path] for path in paths)
|
|
138
|
+
reported = (usage.get("input") or 0) + (usage.get("cache") or 0)
|
|
139
|
+
if sent <= 0 or reported >= DELIVERY_FLOOR * sent:
|
|
140
|
+
return None
|
|
141
|
+
return (f"source_not_delivered: reported {reported} input tokens (input + cache-read) "
|
|
142
|
+
f"against the {sent} source tokens embedded in this prompt")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _csv_task(task_id: str, review: str) -> str:
|
|
146
|
+
"""Keep calls.csv task names comparable with earlier script runs."""
|
|
147
|
+
if task_id == "system":
|
|
148
|
+
return "system_overview"
|
|
149
|
+
if review == "sample" and task_id.startswith(("review_", "repair_")):
|
|
150
|
+
return "sample_review" if task_id.startswith("review_") else "sample_repair"
|
|
151
|
+
return task_id
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def generate(repo: Path, run_dir: Path, *, model: str, config: Path | None = None,
|
|
155
|
+
jobs: int = 4, review: str | None = None, timeout: int | None = None,
|
|
156
|
+
resume: bool = False,
|
|
157
|
+
graph_path: Path | None = None, api_base_url: str | None = None,
|
|
158
|
+
api_key_env: str | None = None, default_timeout: int = 360) -> dict[str, Any]:
|
|
159
|
+
"""Script driver: the shared run state, with each task as one tool-free completion.
|
|
160
|
+
|
|
161
|
+
A task failure never voids the run. Finished modules render; failed parts
|
|
162
|
+
stay pending in the run state, and ``resume=True`` continues the same run.
|
|
163
|
+
|
|
164
|
+
Two hard delivery checks guard every call: a pre-flight that each prompt
|
|
165
|
+
really embeds its module's source (``SourceNotEmbedded`` stops the run
|
|
166
|
+
before the call is spent), and a per-call token check that rejects answers
|
|
167
|
+
whose reported input is under half the embedded source.
|
|
168
|
+
"""
|
|
169
|
+
from cbe import host_run
|
|
170
|
+
|
|
171
|
+
review = review or host_run.DEFAULT_REVIEW
|
|
172
|
+
repo, run_dir = repo.resolve(), run_dir.resolve()
|
|
173
|
+
if not repo.is_dir():
|
|
174
|
+
raise GenerateError("repository root is not a directory")
|
|
175
|
+
if jobs < 1 or default_timeout < 1 or review not in host_run.REVIEW_MODES or (timeout is not None and timeout < 1):
|
|
176
|
+
raise GenerateError("jobs and timeout must be positive; review must be none, sample, or all")
|
|
177
|
+
if not model:
|
|
178
|
+
raise GenerateError("generate needs a model name (--model)")
|
|
179
|
+
has_state = (run_dir / "host" / "state.json").is_file()
|
|
180
|
+
if resume and not has_state:
|
|
181
|
+
raise GenerateError("nothing to resume: the run directory has no run state")
|
|
182
|
+
if not resume and ((run_dir / "calls.csv").exists() or has_state):
|
|
183
|
+
raise GenerateError("run directory already contains a run; pass --resume or choose a fresh run directory")
|
|
184
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
185
|
+
if config is None:
|
|
186
|
+
config = run_dir / "api-completion.json"
|
|
187
|
+
if not config.is_file():
|
|
188
|
+
config.write_text(json.dumps(DEFAULT_API_CONFIG, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
189
|
+
else:
|
|
190
|
+
config = config.resolve()
|
|
191
|
+
if not config.is_file():
|
|
192
|
+
raise GenerateError("config file does not exist")
|
|
193
|
+
api_overrides: dict[str, Any] = {}
|
|
194
|
+
settings = load_config(config, model)
|
|
195
|
+
# Keep the historical Python default (360) and CLI default (420), while
|
|
196
|
+
# distinguishing an explicit --timeout from a model/config default.
|
|
197
|
+
timeout = timeout if timeout is not None else settings.get("timeout", default_timeout)
|
|
198
|
+
# Apply the CLI overrides in memory only: never copy a user config that
|
|
199
|
+
# may carry an auth header into the run directory.
|
|
200
|
+
if api_base_url:
|
|
201
|
+
api_overrides["base_url"] = api_base_url
|
|
202
|
+
if api_key_env:
|
|
203
|
+
api_overrides["api_key_env"] = api_key_env
|
|
204
|
+
previous_wall = 0.0
|
|
205
|
+
if resume and (run_dir / "summary.json").is_file():
|
|
206
|
+
previous_wall = json.loads((run_dir / "summary.json").read_text(encoding="utf-8")).get("wall_seconds", 0)
|
|
207
|
+
started = time.monotonic()
|
|
208
|
+
if resume:
|
|
209
|
+
host_run.resume(run_dir, owner="api", graph_path=graph_path)
|
|
210
|
+
else:
|
|
211
|
+
host_run.plan(repo, run_dir, review=review, jobs=jobs, host="api", driver="script",
|
|
212
|
+
graph_path=graph_path,
|
|
213
|
+
batch_target_tokens=settings.get("batch_target_tokens", batching.TARGET_TOKENS),
|
|
214
|
+
batch_max_tokens=settings.get("batch_max_tokens", batching.MAX_TOKENS))
|
|
215
|
+
review = json.loads((run_dir / "host" / "state.json").read_text(encoding="utf-8"))["review"]
|
|
216
|
+
csv_path = run_dir / "calls.csv"
|
|
217
|
+
stopped: str | None = None
|
|
218
|
+
limit_streak, pause_until = 0, 0.0
|
|
219
|
+
active: dict[Any, tuple[dict[str, Any], str]] = {}
|
|
220
|
+
preflight: SourceNotEmbedded | None = None
|
|
221
|
+
with ThreadPoolExecutor(max_workers=jobs) as pool:
|
|
222
|
+
while True:
|
|
223
|
+
paused = bool(pause_until) and time.monotonic() < pause_until
|
|
224
|
+
if stopped is None and preflight is None and len(active) < jobs and not paused:
|
|
225
|
+
try:
|
|
226
|
+
batch = host_run.next_tasks(run_dir, limit=jobs - len(active), owner="api")["tasks"]
|
|
227
|
+
except host_run.HostRunError as exc:
|
|
228
|
+
# Dispatch itself refused the task (a listed source is
|
|
229
|
+
# missing, unreadable, or drifted); stop the same way.
|
|
230
|
+
preflight = SourceNotEmbedded(str(exc))
|
|
231
|
+
host_run.mark_partial(run_dir, "source_not_embedded", str(preflight))
|
|
232
|
+
batch = []
|
|
233
|
+
for item in batch:
|
|
234
|
+
csv_task = _csv_task(item["task"], review)
|
|
235
|
+
try:
|
|
236
|
+
prompt = host_run.completion_prompt(run_dir, item["task"])
|
|
237
|
+
_verify_embedded_source(run_dir, item["task"], prompt)
|
|
238
|
+
except (SourceNotEmbedded, host_run.HostRunError, OSError) as exc:
|
|
239
|
+
# The prompt cannot carry this module's source (unreadable,
|
|
240
|
+
# drifted, or bodies missing), so the model must not be
|
|
241
|
+
# called; stop the run instead of paying for documentation
|
|
242
|
+
# written from file names.
|
|
243
|
+
preflight = exc if isinstance(exc, SourceNotEmbedded) else SourceNotEmbedded(str(exc))
|
|
244
|
+
host_run.mark_partial(run_dir, "source_not_embedded", str(preflight))
|
|
245
|
+
break
|
|
246
|
+
future = pool.submit(invoke, task=csv_task, attempt=item["attempt"],
|
|
247
|
+
prompt=prompt,
|
|
248
|
+
run_dir=run_dir, model=model, config=config,
|
|
249
|
+
timeout=timeout, csv_path=csv_path,
|
|
250
|
+
overrides=api_overrides or None)
|
|
251
|
+
active[future] = (item, csv_task)
|
|
252
|
+
if not active:
|
|
253
|
+
if stopped is None and preflight is None and paused:
|
|
254
|
+
_sleep(max(pause_until - time.monotonic(), 0.0))
|
|
255
|
+
continue
|
|
256
|
+
break
|
|
257
|
+
done, _ = wait(active, return_when=FIRST_COMPLETED)
|
|
258
|
+
for future in done:
|
|
259
|
+
item, csv_task = active.pop(future)
|
|
260
|
+
try:
|
|
261
|
+
completion = future.result()
|
|
262
|
+
except Exception as exc: # noqa: BLE001 - one broken call must not void the run
|
|
263
|
+
outcome = host_run.submit(run_dir, item["task"], error=f"driver_error:{exc}")
|
|
264
|
+
continue
|
|
265
|
+
if completion.error == "provider_limit":
|
|
266
|
+
# Not an answer: back to the queue without using an attempt.
|
|
267
|
+
host_run.release(run_dir, item["task"])
|
|
268
|
+
limit_streak += 1
|
|
269
|
+
if limit_streak >= LIMIT_STREAK_STOP:
|
|
270
|
+
stopped = "provider_limit"
|
|
271
|
+
else:
|
|
272
|
+
pause_until = max(pause_until, time.monotonic() + LIMIT_PAUSE_SECONDS)
|
|
273
|
+
continue
|
|
274
|
+
limit_streak = 0
|
|
275
|
+
if completion.error:
|
|
276
|
+
outcome = host_run.submit(run_dir, item["task"], error=completion.error,
|
|
277
|
+
raw_text=completion.raw_text or None)
|
|
278
|
+
if completion.error == "provider_auth":
|
|
279
|
+
stopped = completion.error
|
|
280
|
+
continue
|
|
281
|
+
delivery = _call_delivery_problem(run_dir, item["task"], completion.usage)
|
|
282
|
+
if delivery is not None:
|
|
283
|
+
# The model never saw this prompt's source, so its answer
|
|
284
|
+
# is not accepted; the retry/partial logic takes over.
|
|
285
|
+
outcome = host_run.submit(run_dir, item["task"], error=delivery,
|
|
286
|
+
raw_text=completion.raw_text or None)
|
|
287
|
+
_mark_result(csv_path, csv_task, item["attempt"], "source_not_delivered")
|
|
288
|
+
continue
|
|
289
|
+
if completion.usage is None:
|
|
290
|
+
host_run.note_usage_unknown(run_dir, item["task"], item["attempt"])
|
|
291
|
+
Path(item["output_file"]).write_text(completion.raw_text, encoding="utf-8")
|
|
292
|
+
outcome = host_run.submit(run_dir, item["task"])
|
|
293
|
+
if not outcome["accepted"] and outcome["error"].startswith(("schema_error", "incomplete")):
|
|
294
|
+
# The host call completed; its CSV row must still show the check failure.
|
|
295
|
+
_mark_result(csv_path, csv_task, item["attempt"], outcome["error"].split(":", 1)[0])
|
|
296
|
+
elif outcome.get("normalized"):
|
|
297
|
+
_mark_result(csv_path, csv_task, item["attempt"], "normalized")
|
|
298
|
+
if stopped or preflight is not None:
|
|
299
|
+
host_run.release(run_dir)
|
|
300
|
+
cost = (usage.script_cost(run_dir, price=settings.get("price_per_million"))
|
|
301
|
+
if csv_path.is_file() else {"calls": 0})
|
|
302
|
+
if preflight is None:
|
|
303
|
+
host_run.mark_partial(run_dir, *_delivery_problem(run_dir))
|
|
304
|
+
result = host_run.finish(run_dir)
|
|
305
|
+
summary = {**result, "status": result.get("status", "partial"),
|
|
306
|
+
"stopped": stopped, "wall_seconds": round(previous_wall + time.monotonic() - started, 3),
|
|
307
|
+
"run_dir": str(run_dir), "index": str(run_dir / "docs" / "INDEX.md"),
|
|
308
|
+
"progress_log": str(run_dir / "progress.log"), "host": "api", "model": model,
|
|
309
|
+
"input_token_basis": "provider prompt_tokens minus cached_tokens; cache reported separately",
|
|
310
|
+
**cost}
|
|
311
|
+
(run_dir / "summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
312
|
+
if preflight is not None:
|
|
313
|
+
raise preflight
|
|
314
|
+
return summary
|
|
315
|
+
|
|
316
|
+
|