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/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
|
+
)
|