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.
Files changed (48) hide show
  1. codedebrief/__init__.py +12 -0
  2. codedebrief/analysis/__init__.py +16 -0
  3. codedebrief/analysis/common.py +527 -0
  4. codedebrief/analysis/discovery.py +100 -0
  5. codedebrief/analysis/languages/__init__.py +6 -0
  6. codedebrief/analysis/languages/_common.py +68 -0
  7. codedebrief/analysis/languages/c.py +96 -0
  8. codedebrief/analysis/languages/cpp.py +146 -0
  9. codedebrief/analysis/languages/csharp.py +137 -0
  10. codedebrief/analysis/languages/go.py +157 -0
  11. codedebrief/analysis/languages/java.py +158 -0
  12. codedebrief/analysis/languages/php.py +83 -0
  13. codedebrief/analysis/languages/ruby.py +75 -0
  14. codedebrief/analysis/languages/rust.py +96 -0
  15. codedebrief/analysis/project.py +373 -0
  16. codedebrief/analysis/python.py +939 -0
  17. codedebrief/analysis/registry.py +320 -0
  18. codedebrief/analysis/treesitter.py +884 -0
  19. codedebrief/analysis/typescript.py +1019 -0
  20. codedebrief/artifacts.py +49 -0
  21. codedebrief/cli.py +585 -0
  22. codedebrief/config.py +226 -0
  23. codedebrief/doctor.py +175 -0
  24. codedebrief/install.py +441 -0
  25. codedebrief/mcp_server.py +2720 -0
  26. codedebrief/model.py +189 -0
  27. codedebrief/py.typed +1 -0
  28. codedebrief/quality.py +392 -0
  29. codedebrief/query.py +641 -0
  30. codedebrief/render/__init__.py +6 -0
  31. codedebrief/render/assets/generated/codedebrief-viewer-runtime.iife.js +10 -0
  32. codedebrief/render/assets/panels.js +462 -0
  33. codedebrief/render/assets/shell.js +1649 -0
  34. codedebrief/render/assets/styles.css +1715 -0
  35. codedebrief/render/assets/tree.js +616 -0
  36. codedebrief/render/html.py +191 -0
  37. codedebrief/render/markdown.py +153 -0
  38. codedebrief/render/payload.py +326 -0
  39. codedebrief/render/snapshot.py +769 -0
  40. codedebrief/schema/codedebrief.schema.json +449 -0
  41. codedebrief/util.py +65 -0
  42. codedebrief/validation.py +214 -0
  43. codedebrief-0.11.0.dist-info/METADATA +426 -0
  44. codedebrief-0.11.0.dist-info/RECORD +48 -0
  45. codedebrief-0.11.0.dist-info/WHEEL +4 -0
  46. codedebrief-0.11.0.dist-info/entry_points.txt +2 -0
  47. codedebrief-0.11.0.dist-info/licenses/LICENSE +176 -0
  48. codedebrief-0.11.0.dist-info/licenses/NOTICE +9 -0
@@ -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())