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
codedebrief/config.py ADDED
@@ -0,0 +1,226 @@
1
+ from __future__ import annotations
2
+
3
+ import fnmatch
4
+ import sys
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+
8
+ if sys.version_info >= (3, 11):
9
+ import tomllib
10
+ else: # pragma: no cover - Python 3.10
11
+ import tomli as tomllib
12
+
13
+ DEFAULT_EXCLUDES = [
14
+ "**/__generated__/**",
15
+ "**/*.min.js",
16
+ "**/*.gen.*",
17
+ "**/*.generated.*",
18
+ "**/*.pb.*",
19
+ "**/*.d.ts",
20
+ ]
21
+
22
+ DEFAULT_EXCLUDE_DIRS = [
23
+ ".angular",
24
+ ".aws-sam",
25
+ ".bundle",
26
+ ".cache",
27
+ ".expo",
28
+ ".git",
29
+ ".gradle",
30
+ ".hg",
31
+ ".codedebrief",
32
+ ".mypy_cache",
33
+ ".next",
34
+ ".nox",
35
+ ".nuxt",
36
+ ".parcel-cache",
37
+ ".pnpm-store",
38
+ ".pytest_cache",
39
+ ".ruff_cache",
40
+ ".serverless",
41
+ ".svn",
42
+ ".svelte-kit",
43
+ ".terraform",
44
+ ".temp",
45
+ ".tox",
46
+ ".turbo",
47
+ ".tmp",
48
+ ".venv",
49
+ ".venv-*",
50
+ ".vite",
51
+ ".yarn",
52
+ "__generated__",
53
+ "__pycache__",
54
+ "bower_components",
55
+ "build",
56
+ "cdk.out",
57
+ "coverage",
58
+ "DerivedData",
59
+ "dist",
60
+ "env",
61
+ "graphify-out",
62
+ "htmlcov",
63
+ "jspm_packages",
64
+ "codedebrief-out",
65
+ "logs",
66
+ "node_modules",
67
+ "out",
68
+ "Pods",
69
+ "target",
70
+ "temp",
71
+ "tmp",
72
+ "vendor",
73
+ "venv",
74
+ "*.egg-info",
75
+ ]
76
+
77
+ BUILTIN_PROFILES = ("self", "project")
78
+
79
+
80
+ @dataclass(slots=True)
81
+ class CodeDebriefConfig:
82
+ source_roots: list[str] = field(default_factory=lambda: ["."])
83
+ exclude: list[str] = field(default_factory=lambda: list(DEFAULT_EXCLUDES))
84
+ exclude_dirs: list[str] = field(default_factory=lambda: list(DEFAULT_EXCLUDE_DIRS))
85
+ include_public_functions: bool = True
86
+ max_call_depth: int = 4
87
+ output_dir: str = "codedebrief-out"
88
+ self_exclude: bool = True
89
+ entrypoint_include: list[str] = field(default_factory=list)
90
+ entrypoint_exclude: list[str] = field(default_factory=list)
91
+ # Named macro-parts of the codebase, e.g. {"backend": ["backend/**"], "edge": ["edge/**"]}.
92
+ scopes: dict[str, list[str]] = field(default_factory=dict)
93
+
94
+ @classmethod
95
+ def load(cls, root: Path, profile: str | None = None) -> CodeDebriefConfig:
96
+ config = cls()
97
+ config_path = root / "codedebrief.toml"
98
+ if config_path.exists():
99
+ payload = tomllib.loads(config_path.read_text(encoding="utf-8"))
100
+ section = payload.get("codedebrief", {})
101
+ config.source_roots = list(section.get("source_roots", config.source_roots))
102
+ config.exclude.extend(section.get("exclude", []))
103
+ config.exclude_dirs.extend(section.get("exclude_dirs", []))
104
+ config.include_public_functions = bool(
105
+ section.get("include_public_functions", config.include_public_functions)
106
+ )
107
+ config.max_call_depth = int(section.get("max_call_depth", config.max_call_depth))
108
+ config.output_dir = str(section.get("output_dir", config.output_dir))
109
+ config.self_exclude = bool(section.get("self_exclude", config.self_exclude))
110
+ entrypoints = section.get("entrypoints", {})
111
+ config.entrypoint_include = list(entrypoints.get("include", []))
112
+ config.entrypoint_exclude = list(entrypoints.get("exclude", []))
113
+ config.scopes = {
114
+ str(name): [str(pattern) for pattern in patterns]
115
+ for name, patterns in section.get("scopes", {}).items()
116
+ }
117
+
118
+ if profile is not None:
119
+ config = _apply_profile(config, profile)
120
+
121
+ ignore_path = root / ".codedebriefignore"
122
+ if ignore_path.exists():
123
+ for raw_line in ignore_path.read_text(encoding="utf-8").splitlines():
124
+ line = raw_line.strip()
125
+ if line and not line.startswith("#"):
126
+ config.exclude.append(_normalize_pattern(line))
127
+ return config
128
+
129
+ def is_excluded(self, relative_path: str) -> bool:
130
+ normalized = relative_path.replace("\\", "/")
131
+ return any(
132
+ fnmatch.fnmatch(normalized, pattern)
133
+ or fnmatch.fnmatch("/" + normalized, pattern)
134
+ or _directory_pattern_matches(normalized, pattern)
135
+ for pattern in self.exclude
136
+ )
137
+
138
+ def is_excluded_dir(self, relative_path: str) -> bool:
139
+ normalized = relative_path.replace("\\", "/").strip("/")
140
+ if not normalized:
141
+ return False
142
+ name = normalized.rsplit("/", 1)[-1]
143
+ return any(
144
+ _directory_name_or_path_matches(normalized, name, pattern)
145
+ for pattern in self.exclude_dirs
146
+ )
147
+
148
+ def entrypoint_override(self, symbol: str) -> bool | None:
149
+ if any(fnmatch.fnmatch(symbol, item) for item in self.entrypoint_exclude):
150
+ return False
151
+ if any(fnmatch.fnmatch(symbol, item) for item in self.entrypoint_include):
152
+ return True
153
+ return None
154
+
155
+ def scopes_for(self, relative_path: str) -> list[str]:
156
+ """The macro-part(s) a file belongs to.
157
+
158
+ With declared scopes, returns every named scope whose globs match. Otherwise the
159
+ top-level directory is the inferred scope, splitting a codebase into
160
+ backend/frontend/infra-style parts out of the box.
161
+ """
162
+ normalized = relative_path.replace("\\", "/")
163
+ if self.scopes:
164
+ return sorted(
165
+ name
166
+ for name, patterns in self.scopes.items()
167
+ if any(_scope_match(normalized, pattern) for pattern in patterns)
168
+ )
169
+ head, sep, _ = normalized.partition("/")
170
+ return [head] if sep else []
171
+
172
+
173
+ def _apply_profile(config: CodeDebriefConfig, profile: str) -> CodeDebriefConfig:
174
+ """Apply one built-in analysis profile on top of the project config.
175
+
176
+ Profiles keep the normal config/ignore semantics, but give agents explicit choices:
177
+ self artifacts for CodeDebrief internals and project artifacts for the whole checkout.
178
+ Each non-default profile writes to its own output dir so focused dogfood models do not
179
+ overwrite one another.
180
+ """
181
+ if profile not in BUILTIN_PROFILES:
182
+ known = ", ".join(BUILTIN_PROFILES)
183
+ raise ValueError(f"unknown CodeDebrief profile {profile!r}; known profiles: {known}")
184
+ if profile == "self":
185
+ config.source_roots = ["src/codedebrief"]
186
+ config.self_exclude = False
187
+ config.output_dir = "codedebrief-out/self"
188
+ elif profile == "project":
189
+ config.source_roots = ["src", "tests"]
190
+ config.self_exclude = False
191
+ config.output_dir = "codedebrief-out/project"
192
+ return config
193
+
194
+
195
+ def _normalize_pattern(pattern: str) -> str:
196
+ normalized = pattern.replace("\\", "/").lstrip("/")
197
+ if normalized.endswith("/"):
198
+ return normalized + "**"
199
+ return normalized
200
+
201
+
202
+ def _directory_name_or_path_matches(path: str, name: str, pattern: str) -> bool:
203
+ normalized = pattern.replace("\\", "/").strip("/")
204
+ if not normalized:
205
+ return False
206
+ if "/" not in normalized:
207
+ return any(fnmatch.fnmatch(part, normalized) for part in path.split("/") if part)
208
+ return fnmatch.fnmatch(path, normalized) or _directory_pattern_matches(
209
+ path, normalized.rstrip("/") + "/**"
210
+ )
211
+
212
+
213
+ def _directory_pattern_matches(path: str, pattern: str) -> bool:
214
+ if pattern.endswith("/**"):
215
+ directory = pattern[:-3].rstrip("/")
216
+ return path == directory or path.startswith(directory + "/")
217
+ return False
218
+
219
+
220
+ def _scope_match(path: str, pattern: str) -> bool:
221
+ normalized = pattern.replace("\\", "/")
222
+ return (
223
+ fnmatch.fnmatch(path, normalized)
224
+ or fnmatch.fnmatch(path, normalized.rstrip("/") + "/**")
225
+ or _directory_pattern_matches(path, normalized)
226
+ )
codedebrief/doctor.py ADDED
@@ -0,0 +1,175 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.util
4
+ import json
5
+ import sys
6
+ from dataclasses import asdict, dataclass
7
+ from importlib import metadata
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class RuntimeDependency:
14
+ package: str
15
+ import_name: str
16
+ purpose: str
17
+
18
+
19
+ @dataclass(frozen=True, slots=True)
20
+ class MissingDependency:
21
+ package: str
22
+ import_name: str
23
+ purpose: str
24
+
25
+
26
+ @dataclass(frozen=True, slots=True)
27
+ class LanguageCapabilitySummary:
28
+ supported_languages: list[str]
29
+ feature_count: int
30
+ limitation_note_count: int
31
+ contract: str
32
+
33
+
34
+ @dataclass(frozen=True, slots=True)
35
+ class DoctorReport:
36
+ ok: bool
37
+ executable: str
38
+ package_version: str
39
+ package_location: str
40
+ missing_dependencies: list[MissingDependency]
41
+ repair_command: str
42
+ language_capabilities: LanguageCapabilitySummary
43
+
44
+ def to_dict(self) -> dict[str, Any]:
45
+ payload = asdict(self)
46
+ payload["missing_dependencies"] = [asdict(item) for item in self.missing_dependencies]
47
+ return payload
48
+
49
+
50
+ RUNTIME_DEPENDENCIES = (
51
+ RuntimeDependency("jsonschema", "jsonschema", "artifact validation"),
52
+ RuntimeDependency("tree-sitter", "tree_sitter", "parser runtime"),
53
+ RuntimeDependency("tree-sitter-typescript", "tree_sitter_typescript", "TypeScript/JavaScript"),
54
+ RuntimeDependency("tree-sitter-c", "tree_sitter_c", "C"),
55
+ RuntimeDependency("tree-sitter-c-sharp", "tree_sitter_c_sharp", "C#"),
56
+ RuntimeDependency("tree-sitter-go", "tree_sitter_go", "Go"),
57
+ RuntimeDependency("tree-sitter-java", "tree_sitter_java", "Java"),
58
+ RuntimeDependency("tree-sitter-php", "tree_sitter_php", "PHP"),
59
+ RuntimeDependency("tree-sitter-cpp", "tree_sitter_cpp", "C++"),
60
+ RuntimeDependency("tree-sitter-ruby", "tree_sitter_ruby", "Ruby"),
61
+ RuntimeDependency("tree-sitter-rust", "tree_sitter_rust", "Rust"),
62
+ )
63
+
64
+
65
+ def doctor_report(root: Path) -> DoctorReport:
66
+ missing = [
67
+ MissingDependency(item.package, item.import_name, item.purpose)
68
+ for item in RUNTIME_DEPENDENCIES
69
+ if importlib.util.find_spec(item.import_name) is None
70
+ ]
71
+ return DoctorReport(
72
+ ok=not missing,
73
+ executable=sys.executable,
74
+ package_version=_package_version(),
75
+ package_location=_package_location(),
76
+ missing_dependencies=missing,
77
+ repair_command=_repair_command(root),
78
+ language_capabilities=_language_capability_summary(),
79
+ )
80
+
81
+
82
+ def render_doctor(report: DoctorReport) -> str:
83
+ capabilities = report.language_capabilities
84
+ lines = [
85
+ f"CodeDebrief doctor {'OK' if report.ok else 'FAILED'}",
86
+ f"Python: {report.executable}",
87
+ f"Package: codedebrief {report.package_version}",
88
+ ]
89
+ if report.package_location:
90
+ lines.append(f"Location: {report.package_location}")
91
+ lines.append(
92
+ "Language capabilities: "
93
+ f"{len(capabilities.supported_languages)} language ids, "
94
+ f"{capabilities.feature_count} feature flags, "
95
+ f"{capabilities.limitation_note_count} limitation notes"
96
+ )
97
+ lines.append(f"Capability contract: {capabilities.contract}")
98
+ if report.missing_dependencies:
99
+ lines.append("")
100
+ lines.append("Missing runtime dependencies:")
101
+ lines.extend(
102
+ f"- {item.package} (import {item.import_name}) for {item.purpose}"
103
+ for item in report.missing_dependencies
104
+ )
105
+ lines.append("")
106
+ lines.append("Repair this interpreter with:")
107
+ lines.append(f" {report.repair_command}")
108
+ else:
109
+ lines.append("All runtime parser dependencies are importable.")
110
+ return "\n".join(lines)
111
+
112
+
113
+ def render_doctor_json(report: DoctorReport) -> str:
114
+ return json.dumps(report.to_dict(), indent=2)
115
+
116
+
117
+ def _package_version() -> str:
118
+ try:
119
+ return metadata.version("codedebrief")
120
+ except metadata.PackageNotFoundError:
121
+ return "not installed"
122
+
123
+
124
+ def _package_location() -> str:
125
+ try:
126
+ distribution = metadata.distribution("codedebrief")
127
+ except metadata.PackageNotFoundError:
128
+ return ""
129
+ direct_url = distribution.read_text("direct_url.json")
130
+ if direct_url:
131
+ try:
132
+ payload = json.loads(direct_url)
133
+ except json.JSONDecodeError:
134
+ return ""
135
+ url = payload.get("url")
136
+ if isinstance(url, str) and url.startswith("file://"):
137
+ return url.removeprefix("file://")
138
+ return ""
139
+
140
+
141
+ def _repair_command(root: Path) -> str:
142
+ project_root = root.resolve()
143
+ if _looks_like_codedebrief_checkout(project_root):
144
+ return f"{sys.executable} -m pip install -e {project_root}"
145
+ if _looks_like_codedebrief_checkout(Path.cwd()):
146
+ return f"{sys.executable} -m pip install -e {Path.cwd().resolve()}"
147
+ return (
148
+ f"{sys.executable} -m pip install --force-reinstall "
149
+ "git+https://github.com/ferdinandobons/CodeDebrief.git"
150
+ )
151
+
152
+
153
+ def _looks_like_codedebrief_checkout(path: Path) -> bool:
154
+ return (path / "pyproject.toml").exists() and (path / "src" / "codedebrief" / "cli.py").exists()
155
+
156
+
157
+ def _language_capability_summary() -> LanguageCapabilitySummary:
158
+ from codedebrief.analysis.registry import language_capability_matrix
159
+
160
+ matrix = language_capability_matrix()
161
+ feature_names: set[str] = set()
162
+ limitation_note_count = 0
163
+ for payload in matrix.values():
164
+ features = payload.get("features")
165
+ if isinstance(features, dict):
166
+ feature_names.update(str(name) for name in features)
167
+ limitations = payload.get("limitations")
168
+ if isinstance(limitations, dict):
169
+ limitation_note_count += len(limitations)
170
+ return LanguageCapabilitySummary(
171
+ supported_languages=sorted(matrix),
172
+ feature_count=len(feature_names),
173
+ limitation_note_count=limitation_note_count,
174
+ contract="metadata.language_capabilities; smoke-tested by tests/test_registry.py",
175
+ )