codedebrief 0.11.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.
- codedebrief/__init__.py +12 -0
- codedebrief/analysis/__init__.py +16 -0
- codedebrief/analysis/common.py +527 -0
- codedebrief/analysis/discovery.py +100 -0
- codedebrief/analysis/languages/__init__.py +6 -0
- codedebrief/analysis/languages/_common.py +68 -0
- codedebrief/analysis/languages/c.py +96 -0
- codedebrief/analysis/languages/cpp.py +146 -0
- codedebrief/analysis/languages/csharp.py +137 -0
- codedebrief/analysis/languages/go.py +157 -0
- codedebrief/analysis/languages/java.py +158 -0
- codedebrief/analysis/languages/php.py +83 -0
- codedebrief/analysis/languages/ruby.py +75 -0
- codedebrief/analysis/languages/rust.py +96 -0
- codedebrief/analysis/project.py +373 -0
- codedebrief/analysis/python.py +939 -0
- codedebrief/analysis/registry.py +320 -0
- codedebrief/analysis/treesitter.py +884 -0
- codedebrief/analysis/typescript.py +1019 -0
- codedebrief/artifacts.py +49 -0
- codedebrief/cli.py +585 -0
- codedebrief/config.py +226 -0
- codedebrief/doctor.py +175 -0
- codedebrief/install.py +441 -0
- codedebrief/mcp_server.py +2720 -0
- codedebrief/model.py +189 -0
- codedebrief/py.typed +1 -0
- codedebrief/quality.py +392 -0
- codedebrief/query.py +641 -0
- codedebrief/render/__init__.py +6 -0
- codedebrief/render/assets/generated/codedebrief-viewer-runtime.iife.js +10 -0
- codedebrief/render/assets/panels.js +462 -0
- codedebrief/render/assets/shell.js +1649 -0
- codedebrief/render/assets/styles.css +1715 -0
- codedebrief/render/assets/tree.js +616 -0
- codedebrief/render/html.py +191 -0
- codedebrief/render/markdown.py +153 -0
- codedebrief/render/payload.py +326 -0
- codedebrief/render/snapshot.py +769 -0
- codedebrief/schema/codedebrief.schema.json +449 -0
- codedebrief/util.py +65 -0
- codedebrief/validation.py +214 -0
- codedebrief-0.11.0.dist-info/METADATA +426 -0
- codedebrief-0.11.0.dist-info/RECORD +48 -0
- codedebrief-0.11.0.dist-info/WHEEL +4 -0
- codedebrief-0.11.0.dist-info/entry_points.txt +2 -0
- codedebrief-0.11.0.dist-info/licenses/LICENSE +176 -0
- codedebrief-0.11.0.dist-info/licenses/NOTICE +9 -0
codedebrief/artifacts.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from codedebrief.config import CodeDebriefConfig
|
|
6
|
+
from codedebrief.model import ProjectModel
|
|
7
|
+
from codedebrief.render.html import render_html
|
|
8
|
+
from codedebrief.render.markdown import render_markdown
|
|
9
|
+
from codedebrief.util import read_json, write_json
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def output_paths(root: Path, config: CodeDebriefConfig | None = None) -> tuple[Path, Path, Path]:
|
|
13
|
+
active_config = config or CodeDebriefConfig.load(root)
|
|
14
|
+
project_root = root.resolve()
|
|
15
|
+
output = (project_root / active_config.output_dir).resolve()
|
|
16
|
+
try:
|
|
17
|
+
output.relative_to(project_root)
|
|
18
|
+
except ValueError as error:
|
|
19
|
+
raise ValueError("CodeDebrief output_dir must stay inside the analyzed project") from error
|
|
20
|
+
return (
|
|
21
|
+
output / "codedebrief.json",
|
|
22
|
+
output / "codedebrief.md",
|
|
23
|
+
output / "codedebrief.html",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def write_artifacts(
|
|
28
|
+
root: Path,
|
|
29
|
+
model: ProjectModel,
|
|
30
|
+
*,
|
|
31
|
+
include_html: bool = True,
|
|
32
|
+
config: CodeDebriefConfig | None = None,
|
|
33
|
+
) -> tuple[Path, Path, Path | None]:
|
|
34
|
+
json_path, markdown_path, html_path = output_paths(root, config)
|
|
35
|
+
write_json(json_path, model.to_dict())
|
|
36
|
+
markdown_path.write_text(render_markdown(model), encoding="utf-8")
|
|
37
|
+
if include_html:
|
|
38
|
+
html_path.write_text(render_html(model, source_root=root.resolve()), encoding="utf-8")
|
|
39
|
+
return json_path, markdown_path, html_path
|
|
40
|
+
return json_path, markdown_path, None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def load_model(root: Path, config: CodeDebriefConfig | None = None) -> ProjectModel:
|
|
44
|
+
json_path, _, _ = output_paths(root, config)
|
|
45
|
+
if not json_path.exists():
|
|
46
|
+
raise FileNotFoundError(
|
|
47
|
+
f"No CodeDebrief model found at {json_path}. Run `codedebrief update` first."
|
|
48
|
+
)
|
|
49
|
+
return ProjectModel.from_dict(read_json(json_path))
|
codedebrief/cli.py
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
import webbrowser
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from functools import partial
|
|
9
|
+
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from textwrap import dedent
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from codedebrief import __version__
|
|
15
|
+
from codedebrief.analysis import ProjectAnalyzer
|
|
16
|
+
from codedebrief.artifacts import load_model, output_paths, write_artifacts
|
|
17
|
+
from codedebrief.config import BUILTIN_PROFILES, CodeDebriefConfig
|
|
18
|
+
from codedebrief.doctor import doctor_report, render_doctor, render_doctor_json
|
|
19
|
+
from codedebrief.install import (
|
|
20
|
+
MCP_CONFIG_TARGETS,
|
|
21
|
+
install_agent_instructions,
|
|
22
|
+
install_agent_skill,
|
|
23
|
+
install_mcp_config,
|
|
24
|
+
)
|
|
25
|
+
from codedebrief.quality import render_quality
|
|
26
|
+
from codedebrief.render.html import render_html
|
|
27
|
+
from codedebrief.validation import validate_codedebrief
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class CodeDebriefHelpFormatter(argparse.RawDescriptionHelpFormatter):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CodeDebriefArgumentParser(argparse.ArgumentParser):
|
|
35
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
36
|
+
kwargs.setdefault("formatter_class", CodeDebriefHelpFormatter)
|
|
37
|
+
super().__init__(*args, **kwargs)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
41
|
+
parser = CodeDebriefArgumentParser(
|
|
42
|
+
prog="codedebrief",
|
|
43
|
+
description="Turn a local codebase into source-grounded workflow flowcharts.",
|
|
44
|
+
epilog=dedent(
|
|
45
|
+
"""\
|
|
46
|
+
Quick start:
|
|
47
|
+
codedebrief setup-agent codex
|
|
48
|
+
codedebrief update
|
|
49
|
+
codedebrief view
|
|
50
|
+
codedebrief validate
|
|
51
|
+
codedebrief doctor
|
|
52
|
+
|
|
53
|
+
Add --help after any command for focused examples and advanced options.
|
|
54
|
+
"""
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
58
|
+
subparsers = parser.add_subparsers(
|
|
59
|
+
dest="command",
|
|
60
|
+
required=True,
|
|
61
|
+
parser_class=CodeDebriefArgumentParser,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
setup = subparsers.add_parser(
|
|
65
|
+
"setup-agent",
|
|
66
|
+
help="Configure CodeDebrief once for a coding agent.",
|
|
67
|
+
description=(
|
|
68
|
+
"Install agent instructions, register MCP, create config when needed, "
|
|
69
|
+
"generate artifacts, run doctor, and validate the setup."
|
|
70
|
+
),
|
|
71
|
+
epilog=dedent(
|
|
72
|
+
"""\
|
|
73
|
+
Examples:
|
|
74
|
+
codedebrief setup-agent codex
|
|
75
|
+
codedebrief setup-agent claude ../my-app
|
|
76
|
+
codedebrief setup-agent gemini
|
|
77
|
+
codedebrief setup-agent cursor --full
|
|
78
|
+
|
|
79
|
+
After setup, ask your coding agent ordinary questions about code logic. Use
|
|
80
|
+
codedebrief view when a human wants the manual workflow flowchart UI.
|
|
81
|
+
"""
|
|
82
|
+
),
|
|
83
|
+
)
|
|
84
|
+
setup.add_argument("agent", choices=["codex", "claude", "gemini", "cursor"])
|
|
85
|
+
setup.add_argument(
|
|
86
|
+
"path",
|
|
87
|
+
nargs="?",
|
|
88
|
+
default=".",
|
|
89
|
+
help="Project folder to configure. Defaults to the current directory.",
|
|
90
|
+
)
|
|
91
|
+
setup.add_argument("--full", action="store_true", help="Ignore the incremental cache.")
|
|
92
|
+
setup.add_argument("--no-html", action="store_true", help="Skip the local HTML artifact.")
|
|
93
|
+
_add_profile_argument(setup)
|
|
94
|
+
|
|
95
|
+
update = subparsers.add_parser(
|
|
96
|
+
"update",
|
|
97
|
+
help="Incrementally refresh changed source files.",
|
|
98
|
+
description="Refresh existing CodeDebrief artifacts after source changes.",
|
|
99
|
+
epilog=dedent(
|
|
100
|
+
"""\
|
|
101
|
+
Examples:
|
|
102
|
+
codedebrief update
|
|
103
|
+
codedebrief update ../my-app
|
|
104
|
+
codedebrief update --full
|
|
105
|
+
|
|
106
|
+
Use update during normal development. Use --full after analyzer upgrades or
|
|
107
|
+
when cached file models should be ignored.
|
|
108
|
+
"""
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
update.add_argument(
|
|
112
|
+
"path",
|
|
113
|
+
nargs="?",
|
|
114
|
+
default=".",
|
|
115
|
+
help="Project folder to refresh. Defaults to the current directory.",
|
|
116
|
+
)
|
|
117
|
+
update.add_argument("--full", action="store_true", help="Ignore the incremental cache.")
|
|
118
|
+
update.add_argument("--no-html", action="store_true", help="Skip the local HTML artifact.")
|
|
119
|
+
_add_profile_argument(update)
|
|
120
|
+
|
|
121
|
+
view = subparsers.add_parser(
|
|
122
|
+
"view",
|
|
123
|
+
help="Generate and serve the interactive flowchart.",
|
|
124
|
+
description="Open the local interactive workflow flowchart viewer.",
|
|
125
|
+
epilog=dedent(
|
|
126
|
+
"""\
|
|
127
|
+
Examples:
|
|
128
|
+
codedebrief view
|
|
129
|
+
codedebrief view ../my-app
|
|
130
|
+
codedebrief view --port 8771
|
|
131
|
+
|
|
132
|
+
The viewer is local-only. Use --render-only for CI or artifact generation.
|
|
133
|
+
"""
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
view.add_argument(
|
|
137
|
+
"path",
|
|
138
|
+
nargs="?",
|
|
139
|
+
default=".",
|
|
140
|
+
help="Project folder to view. Defaults to the current directory.",
|
|
141
|
+
)
|
|
142
|
+
view.add_argument("--port", type=int, default=8765, help="Local server port.")
|
|
143
|
+
view.add_argument("--no-open", action="store_true", help="Serve without opening a browser.")
|
|
144
|
+
view.add_argument(
|
|
145
|
+
"--render-only",
|
|
146
|
+
action="store_true",
|
|
147
|
+
help="Write codedebrief.html without starting a server.",
|
|
148
|
+
)
|
|
149
|
+
_add_profile_argument(view)
|
|
150
|
+
|
|
151
|
+
validate = subparsers.add_parser(
|
|
152
|
+
"validate",
|
|
153
|
+
help="Validate the generated CodeDebrief model.",
|
|
154
|
+
description="Validate generated artifacts and optional analyzer-quality checks.",
|
|
155
|
+
epilog=dedent(
|
|
156
|
+
"""\
|
|
157
|
+
Examples:
|
|
158
|
+
codedebrief validate
|
|
159
|
+
codedebrief validate --check-sync
|
|
160
|
+
codedebrief validate --quality
|
|
161
|
+
"""
|
|
162
|
+
),
|
|
163
|
+
)
|
|
164
|
+
validate.add_argument(
|
|
165
|
+
"path",
|
|
166
|
+
nargs="?",
|
|
167
|
+
default=".",
|
|
168
|
+
help="Project folder containing generated CodeDebrief artifacts.",
|
|
169
|
+
)
|
|
170
|
+
validate.add_argument(
|
|
171
|
+
"--check-sync",
|
|
172
|
+
action="store_true",
|
|
173
|
+
help="Re-analyze sources and fail if codedebrief.json is stale.",
|
|
174
|
+
)
|
|
175
|
+
validate.add_argument(
|
|
176
|
+
"--json", action="store_true", dest="json_output", help="Emit JSON output."
|
|
177
|
+
)
|
|
178
|
+
validate.add_argument(
|
|
179
|
+
"--quality",
|
|
180
|
+
action="store_true",
|
|
181
|
+
help="Include deterministic analysis-quality metrics in the report.",
|
|
182
|
+
)
|
|
183
|
+
validate.add_argument(
|
|
184
|
+
"--max-skipped-files",
|
|
185
|
+
type=int,
|
|
186
|
+
help="Fail validation when skipped-file count exceeds this value.",
|
|
187
|
+
)
|
|
188
|
+
validate.add_argument(
|
|
189
|
+
"--max-parse-warnings",
|
|
190
|
+
type=int,
|
|
191
|
+
help="Fail validation when parse-warning count exceeds this value.",
|
|
192
|
+
)
|
|
193
|
+
validate.add_argument(
|
|
194
|
+
"--min-call-resolution",
|
|
195
|
+
type=float,
|
|
196
|
+
help="Fail validation when call-resolution rate is below this 0..1 value.",
|
|
197
|
+
)
|
|
198
|
+
validate.add_argument(
|
|
199
|
+
"--max-generic-label-ratio",
|
|
200
|
+
type=float,
|
|
201
|
+
help="Fail validation when generic-label ratio exceeds this 0..1 value.",
|
|
202
|
+
)
|
|
203
|
+
_add_profile_argument(validate)
|
|
204
|
+
|
|
205
|
+
doctor = subparsers.add_parser("doctor", help="Check the active CodeDebrief installation.")
|
|
206
|
+
doctor.add_argument("path", nargs="?", default=".", help="Project folder to inspect.")
|
|
207
|
+
doctor.add_argument("--json", action="store_true", dest="json_output", help="Emit JSON output.")
|
|
208
|
+
|
|
209
|
+
mcp = subparsers.add_parser("mcp", help="Start the CodeDebrief MCP server over stdio.")
|
|
210
|
+
mcp.add_argument("path", nargs="?", default=".", help="Project folder served over MCP.")
|
|
211
|
+
_add_profile_argument(mcp)
|
|
212
|
+
return parser
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _add_profile_argument(parser: argparse.ArgumentParser) -> None:
|
|
216
|
+
parser.add_argument(
|
|
217
|
+
"--profile",
|
|
218
|
+
choices=BUILTIN_PROFILES,
|
|
219
|
+
default=None,
|
|
220
|
+
help=(
|
|
221
|
+
"Use a built-in analysis profile: self maps CodeDebrief internals, "
|
|
222
|
+
"project maps the whole checkout."
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
228
|
+
args = build_parser().parse_args(argv)
|
|
229
|
+
try:
|
|
230
|
+
if args.command == "setup-agent":
|
|
231
|
+
return _setup_agent(
|
|
232
|
+
Path(args.path),
|
|
233
|
+
args.agent,
|
|
234
|
+
full=args.full,
|
|
235
|
+
include_html=not args.no_html,
|
|
236
|
+
profile=args.profile,
|
|
237
|
+
)
|
|
238
|
+
if args.command == "update":
|
|
239
|
+
return _analyze(
|
|
240
|
+
Path(args.path),
|
|
241
|
+
full=args.full,
|
|
242
|
+
include_html=not args.no_html,
|
|
243
|
+
profile=args.profile,
|
|
244
|
+
)
|
|
245
|
+
if args.command == "view":
|
|
246
|
+
return _view(
|
|
247
|
+
Path(args.path),
|
|
248
|
+
args.port,
|
|
249
|
+
not args.no_open,
|
|
250
|
+
args.render_only,
|
|
251
|
+
args.profile,
|
|
252
|
+
)
|
|
253
|
+
if args.command == "validate":
|
|
254
|
+
return _validate(
|
|
255
|
+
Path(args.path),
|
|
256
|
+
args.check_sync,
|
|
257
|
+
args.json_output,
|
|
258
|
+
args.quality,
|
|
259
|
+
_quality_thresholds(args),
|
|
260
|
+
args.profile,
|
|
261
|
+
)
|
|
262
|
+
if args.command == "doctor":
|
|
263
|
+
return _doctor(Path(args.path), args.json_output)
|
|
264
|
+
if args.command == "mcp":
|
|
265
|
+
from codedebrief.mcp_server import run_mcp
|
|
266
|
+
|
|
267
|
+
config = CodeDebriefConfig.load(Path(args.path).resolve(), profile=args.profile)
|
|
268
|
+
run_mcp(Path(args.path), config)
|
|
269
|
+
return 0
|
|
270
|
+
except (OSError, RuntimeError, ValueError, SyntaxError) as error:
|
|
271
|
+
# OSError subsumes FileNotFoundError/PermissionError, so a missing path or a
|
|
272
|
+
# permission-denied write surfaces as a clean message instead of a raw traceback.
|
|
273
|
+
print("CodeDebrief command FAILED", file=sys.stderr)
|
|
274
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
275
|
+
print("Next steps:", file=sys.stderr)
|
|
276
|
+
print("- Check the path and filesystem permissions.", file=sys.stderr)
|
|
277
|
+
print("- Run `codedebrief doctor` if this looks like an install issue.", file=sys.stderr)
|
|
278
|
+
return 1
|
|
279
|
+
return 0
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _setup_agent(
|
|
283
|
+
root: Path,
|
|
284
|
+
agent: str,
|
|
285
|
+
*,
|
|
286
|
+
full: bool,
|
|
287
|
+
include_html: bool,
|
|
288
|
+
profile: str | None = None,
|
|
289
|
+
) -> int:
|
|
290
|
+
if not root.exists():
|
|
291
|
+
raise FileNotFoundError(f"path does not exist: {root}")
|
|
292
|
+
root = root.resolve()
|
|
293
|
+
display = {
|
|
294
|
+
"codex": "Codex",
|
|
295
|
+
"claude": "Claude",
|
|
296
|
+
"gemini": "Gemini",
|
|
297
|
+
"cursor": "Cursor",
|
|
298
|
+
}[agent]
|
|
299
|
+
print(f"CodeDebrief setup-agent for {display}")
|
|
300
|
+
print(f"Project: {root}")
|
|
301
|
+
|
|
302
|
+
config_path, created_config = _ensure_config(root)
|
|
303
|
+
print("")
|
|
304
|
+
print("Setup:")
|
|
305
|
+
print(f"- Config: {'Created' if created_config else 'Already present'} ({config_path})")
|
|
306
|
+
|
|
307
|
+
changed = install_agent_instructions(root, agent)
|
|
308
|
+
changed.extend(install_agent_skill(root, agent))
|
|
309
|
+
if agent in MCP_CONFIG_TARGETS:
|
|
310
|
+
changed.extend(install_mcp_config(root, agent))
|
|
311
|
+
if changed:
|
|
312
|
+
print(f"- Agent files: updated {len(changed)} file{'s' if len(changed) != 1 else ''}")
|
|
313
|
+
for path in changed:
|
|
314
|
+
print(f" - {path}")
|
|
315
|
+
else:
|
|
316
|
+
print("- Agent files: already up to date")
|
|
317
|
+
|
|
318
|
+
print("")
|
|
319
|
+
analyze_status = _analyze(
|
|
320
|
+
root,
|
|
321
|
+
full=full,
|
|
322
|
+
include_html=include_html,
|
|
323
|
+
profile=profile,
|
|
324
|
+
show_next_steps=False,
|
|
325
|
+
)
|
|
326
|
+
if analyze_status != 0:
|
|
327
|
+
return analyze_status
|
|
328
|
+
|
|
329
|
+
print("")
|
|
330
|
+
doctor_status = _doctor(root, json_output=False, show_next_steps=False)
|
|
331
|
+
if doctor_status != 0:
|
|
332
|
+
return doctor_status
|
|
333
|
+
|
|
334
|
+
print("")
|
|
335
|
+
validate_status = _validate(
|
|
336
|
+
root,
|
|
337
|
+
check_sync=False,
|
|
338
|
+
json_output=False,
|
|
339
|
+
include_quality=False,
|
|
340
|
+
quality_thresholds=None,
|
|
341
|
+
profile=profile,
|
|
342
|
+
show_next_steps=False,
|
|
343
|
+
)
|
|
344
|
+
if validate_status != 0:
|
|
345
|
+
return validate_status
|
|
346
|
+
|
|
347
|
+
print("")
|
|
348
|
+
print("Status: OK - CodeDebrief is ready for your coding agent.")
|
|
349
|
+
print(f"CodeDebrief agent setup complete for {display}.")
|
|
350
|
+
_print_next_steps(
|
|
351
|
+
[
|
|
352
|
+
"Ask your coding agent ordinary questions about the code logic.",
|
|
353
|
+
"Try: How does this feature work?",
|
|
354
|
+
"Try: Which workflows are affected by this change?",
|
|
355
|
+
"Try: Show me the workflow for this feature.",
|
|
356
|
+
"Manual UI: `codedebrief view`",
|
|
357
|
+
]
|
|
358
|
+
)
|
|
359
|
+
return 0
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _ensure_config(root: Path) -> tuple[Path, bool]:
|
|
363
|
+
config_path = root / "codedebrief.toml"
|
|
364
|
+
if config_path.exists():
|
|
365
|
+
return config_path, False
|
|
366
|
+
config_path.write_text(_starter_config_text(), encoding="utf-8")
|
|
367
|
+
return config_path, True
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _analyze(
|
|
371
|
+
root: Path,
|
|
372
|
+
*,
|
|
373
|
+
full: bool,
|
|
374
|
+
include_html: bool,
|
|
375
|
+
profile: str | None = None,
|
|
376
|
+
show_next_steps: bool = True,
|
|
377
|
+
) -> int:
|
|
378
|
+
if not root.exists():
|
|
379
|
+
raise FileNotFoundError(f"path does not exist: {root}")
|
|
380
|
+
root = root.resolve()
|
|
381
|
+
config = CodeDebriefConfig.load(root, profile=profile)
|
|
382
|
+
result = ProjectAnalyzer(root, config).analyze(full=full)
|
|
383
|
+
json_path, markdown_path, html_path = write_artifacts(
|
|
384
|
+
root,
|
|
385
|
+
result.model,
|
|
386
|
+
include_html=include_html,
|
|
387
|
+
config=config,
|
|
388
|
+
)
|
|
389
|
+
print("CodeDebrief update")
|
|
390
|
+
print("Status: OK - artifacts refreshed.")
|
|
391
|
+
print(f"Project: {root}")
|
|
392
|
+
print(f"Summary: {len(result.model.files)} files, {len(result.model.flows)} flows.")
|
|
393
|
+
print(
|
|
394
|
+
"Cache: "
|
|
395
|
+
f"{result.cache_hits} hits, {len(result.changed_files)} changed, "
|
|
396
|
+
f"{len(result.deleted_files)} deleted."
|
|
397
|
+
)
|
|
398
|
+
if result.skipped_files:
|
|
399
|
+
print(
|
|
400
|
+
f"Warning: skipped {len(result.skipped_files)} unparseable file(s):",
|
|
401
|
+
file=sys.stderr,
|
|
402
|
+
)
|
|
403
|
+
for relative, reason in result.skipped_files:
|
|
404
|
+
print(f" - {relative}: {reason}", file=sys.stderr)
|
|
405
|
+
print("Artifacts:")
|
|
406
|
+
print(f"- JSON: {json_path}")
|
|
407
|
+
print(f"- Markdown: {markdown_path}")
|
|
408
|
+
if html_path:
|
|
409
|
+
print(f"- HTML: {html_path}")
|
|
410
|
+
if show_next_steps:
|
|
411
|
+
steps = [
|
|
412
|
+
"Ask your coding agent questions about behavior, workflows, or changed-code context.",
|
|
413
|
+
"Run `codedebrief validate --check-sync` before committing generated artifacts.",
|
|
414
|
+
]
|
|
415
|
+
if html_path:
|
|
416
|
+
steps.append("Open the manual UI with `codedebrief view`.")
|
|
417
|
+
else:
|
|
418
|
+
steps.append("Generate/open the manual UI with `codedebrief view` when needed.")
|
|
419
|
+
_print_next_steps(steps)
|
|
420
|
+
return 0
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _view(
|
|
424
|
+
root: Path,
|
|
425
|
+
port: int,
|
|
426
|
+
should_open: bool,
|
|
427
|
+
render_only: bool,
|
|
428
|
+
profile: str | None = None,
|
|
429
|
+
) -> int:
|
|
430
|
+
root = root.resolve()
|
|
431
|
+
config = CodeDebriefConfig.load(root, profile=profile)
|
|
432
|
+
_, _, html_path = output_paths(root, config)
|
|
433
|
+
model = load_model(root, config)
|
|
434
|
+
html_path.parent.mkdir(parents=True, exist_ok=True)
|
|
435
|
+
html_path.write_text(render_html(model, source_root=root), encoding="utf-8")
|
|
436
|
+
print("CodeDebrief view")
|
|
437
|
+
print("Status: OK - viewer artifact ready.")
|
|
438
|
+
print(f"Project: {root}")
|
|
439
|
+
print(f"HTML: {html_path}")
|
|
440
|
+
if render_only:
|
|
441
|
+
_print_next_steps(["Open the generated HTML file or run `codedebrief view` to serve it."])
|
|
442
|
+
return 0
|
|
443
|
+
|
|
444
|
+
handler = partial(SimpleHTTPRequestHandler, directory=str(html_path.parent))
|
|
445
|
+
server = ThreadingHTTPServer(("127.0.0.1", port), handler)
|
|
446
|
+
url = f"http://127.0.0.1:{port}/{html_path.name}"
|
|
447
|
+
print(f"URL: {url}")
|
|
448
|
+
_print_next_steps(
|
|
449
|
+
[
|
|
450
|
+
"Use the browser to inspect the workflow flowchart.",
|
|
451
|
+
"Press Ctrl+C in this terminal to stop the local server.",
|
|
452
|
+
]
|
|
453
|
+
)
|
|
454
|
+
if should_open:
|
|
455
|
+
webbrowser.open(url)
|
|
456
|
+
try:
|
|
457
|
+
server.serve_forever()
|
|
458
|
+
except KeyboardInterrupt:
|
|
459
|
+
pass
|
|
460
|
+
finally:
|
|
461
|
+
server.server_close()
|
|
462
|
+
return 0
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def _validate(
|
|
466
|
+
root: Path,
|
|
467
|
+
check_sync: bool,
|
|
468
|
+
json_output: bool,
|
|
469
|
+
include_quality: bool,
|
|
470
|
+
quality_thresholds: dict[str, float | int] | None,
|
|
471
|
+
profile: str | None = None,
|
|
472
|
+
show_next_steps: bool = True,
|
|
473
|
+
) -> int:
|
|
474
|
+
root = root.resolve()
|
|
475
|
+
config = CodeDebriefConfig.load(root, profile=profile)
|
|
476
|
+
report = validate_codedebrief(
|
|
477
|
+
root,
|
|
478
|
+
config=config,
|
|
479
|
+
check_sync=check_sync,
|
|
480
|
+
include_quality=include_quality,
|
|
481
|
+
quality_thresholds=quality_thresholds,
|
|
482
|
+
)
|
|
483
|
+
if json_output:
|
|
484
|
+
print(json.dumps(report.to_dict(), indent=2))
|
|
485
|
+
else:
|
|
486
|
+
status = "OK" if report.ok else "FAILED"
|
|
487
|
+
print(f"CodeDebrief validation {status}: {report.artifact}")
|
|
488
|
+
print(
|
|
489
|
+
f"Status: {status} - "
|
|
490
|
+
f"{'artifacts are valid.' if report.ok else 'review the errors below.'}"
|
|
491
|
+
)
|
|
492
|
+
for warning in report.warnings:
|
|
493
|
+
print(f"Warning: {warning}")
|
|
494
|
+
for error in report.errors:
|
|
495
|
+
print(f"Error: {error}", file=sys.stderr)
|
|
496
|
+
if report.quality is not None:
|
|
497
|
+
print(render_quality(report.quality))
|
|
498
|
+
if show_next_steps:
|
|
499
|
+
if report.ok:
|
|
500
|
+
_print_next_steps(
|
|
501
|
+
[
|
|
502
|
+
"No repair needed.",
|
|
503
|
+
(
|
|
504
|
+
"If you changed source logic, commit the updated "
|
|
505
|
+
"`codedebrief-out` artifacts."
|
|
506
|
+
),
|
|
507
|
+
]
|
|
508
|
+
)
|
|
509
|
+
else:
|
|
510
|
+
_print_next_steps(
|
|
511
|
+
[
|
|
512
|
+
"Run `codedebrief update` to refresh stale artifacts.",
|
|
513
|
+
"Fix any listed validation errors, then rerun `codedebrief validate`.",
|
|
514
|
+
]
|
|
515
|
+
)
|
|
516
|
+
return 0 if report.ok else 1
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _quality_thresholds(args: argparse.Namespace) -> dict[str, float | int]:
|
|
520
|
+
thresholds: dict[str, float | int] = {}
|
|
521
|
+
if args.max_skipped_files is not None:
|
|
522
|
+
thresholds["max_skipped_files"] = args.max_skipped_files
|
|
523
|
+
if args.max_parse_warnings is not None:
|
|
524
|
+
thresholds["max_parse_warnings"] = args.max_parse_warnings
|
|
525
|
+
if args.min_call_resolution is not None:
|
|
526
|
+
thresholds["min_call_resolution"] = args.min_call_resolution
|
|
527
|
+
if args.max_generic_label_ratio is not None:
|
|
528
|
+
thresholds["max_generic_label_ratio"] = args.max_generic_label_ratio
|
|
529
|
+
return thresholds
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _doctor(root: Path, json_output: bool, show_next_steps: bool = True) -> int:
|
|
533
|
+
report = doctor_report(root)
|
|
534
|
+
print(render_doctor_json(report) if json_output else render_doctor(report))
|
|
535
|
+
if not json_output and show_next_steps:
|
|
536
|
+
if report.ok:
|
|
537
|
+
_print_next_steps(
|
|
538
|
+
[
|
|
539
|
+
"Run `codedebrief setup-agent codex` once in a new project.",
|
|
540
|
+
"Run `codedebrief update` in an already configured project.",
|
|
541
|
+
]
|
|
542
|
+
)
|
|
543
|
+
else:
|
|
544
|
+
_print_next_steps(
|
|
545
|
+
[
|
|
546
|
+
f"Repair this interpreter with `{report.repair_command}`.",
|
|
547
|
+
"Rerun `codedebrief doctor` after repair.",
|
|
548
|
+
]
|
|
549
|
+
)
|
|
550
|
+
return 0 if report.ok else 1
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _print_next_steps(steps: Sequence[str]) -> None:
|
|
554
|
+
print("Next steps:")
|
|
555
|
+
for step in steps:
|
|
556
|
+
print(f"- {step}")
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _starter_config_text() -> str:
|
|
560
|
+
return """[codedebrief]
|
|
561
|
+
source_roots = ["."]
|
|
562
|
+
exclude = []
|
|
563
|
+
exclude_dirs = []
|
|
564
|
+
# Defaults always prune dependency, VCS, cache, temp, and generated directories such as
|
|
565
|
+
# .git, node_modules, venv/.venv, dist/build/out/target, coverage, .next, .turbo,
|
|
566
|
+
# .svelte-kit, vendor, and codedebrief-out. Add project-specific directories above.
|
|
567
|
+
include_public_functions = true
|
|
568
|
+
max_call_depth = 4
|
|
569
|
+
output_dir = "codedebrief-out"
|
|
570
|
+
self_exclude = true
|
|
571
|
+
|
|
572
|
+
[codedebrief.entrypoints]
|
|
573
|
+
include = []
|
|
574
|
+
exclude = []
|
|
575
|
+
|
|
576
|
+
# Named macro-parts of the codebase (otherwise the top-level directory is the scope):
|
|
577
|
+
# [codedebrief.scopes]
|
|
578
|
+
# backend = ["backend/**", "services/**"]
|
|
579
|
+
# frontend = ["frontend/**", "web/**"]
|
|
580
|
+
# edge = ["edge/**", "workers/**"]
|
|
581
|
+
"""
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
if __name__ == "__main__":
|
|
585
|
+
raise SystemExit(main())
|