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.
Files changed (47) hide show
  1. cbe/__init__.py +9 -0
  2. cbe/__main__.py +4 -0
  3. cbe/aggregation.py +183 -0
  4. cbe/api_completion.py +410 -0
  5. cbe/batching.py +283 -0
  6. cbe/catalog_query.py +376 -0
  7. cbe/checks.py +390 -0
  8. cbe/cli.py +434 -0
  9. cbe/concrete_facts.py +321 -0
  10. cbe/data/LICENSE.tiktoken +30 -0
  11. cbe/data/o200k_base.tiktoken +199998 -0
  12. cbe/declarations.py +537 -0
  13. cbe/docs.py +929 -0
  14. cbe/generate.py +316 -0
  15. cbe/grammars.py +741 -0
  16. cbe/graph.py +857 -0
  17. cbe/graph_backends/__init__.py +43 -0
  18. cbe/graph_backends/_calls.py +262 -0
  19. cbe/graph_backends/_paths.py +42 -0
  20. cbe/graph_backends/identity.py +93 -0
  21. cbe/graph_backends/imports.py +256 -0
  22. cbe/graph_backends/joern.py +1021 -0
  23. cbe/graph_backends/resolution.py +288 -0
  24. cbe/graph_backends/toolchain.py +396 -0
  25. cbe/grounding.py +57 -0
  26. cbe/host_run.py +1752 -0
  27. cbe/model_settings.py +80 -0
  28. cbe/projection.py +305 -0
  29. cbe/prompts.py +316 -0
  30. cbe/records.py +200 -0
  31. cbe/skeleton.py +474 -0
  32. cbe/source_context.py +709 -0
  33. cbe/symbols.py +136 -0
  34. cbe/token_budget.py +68 -0
  35. cbe/treename.py +398 -0
  36. cbe/usage.py +300 -0
  37. cbe_install/__init__.py +11 -0
  38. cbe_install/cli.py +434 -0
  39. cbe_install/resources/agents/cbe-writer.md +16 -0
  40. cbe_install/resources/codebase-explorer/SKILL.md +101 -0
  41. cbe_install/resources/codebase-explorer/references/claude-code.md +63 -0
  42. cbe_install/resources/codebase-explorer/references/codex.md +37 -0
  43. codebase_explorer-2.2.0.dist-info/METADATA +233 -0
  44. codebase_explorer-2.2.0.dist-info/RECORD +47 -0
  45. codebase_explorer-2.2.0.dist-info/WHEEL +4 -0
  46. codebase_explorer-2.2.0.dist-info/entry_points.txt +3 -0
  47. codebase_explorer-2.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,256 @@
1
+ """File/module import backends: dependency-cruiser (JS/TS/TSX) and Grimp (Python).
2
+
3
+ Joern exposes import *declarations*; these two tools resolve them to concrete
4
+ files/modules and are the source of truth for the ``import`` graph kind where
5
+ they apply. Both are optional: a missing binary becomes a diagnostic, not a
6
+ crash, and a run that succeeds with no dependencies reports ``empty`` rather
7
+ than an unexplained absence.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import json
13
+ import os
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+
18
+ from ._paths import canonical_repo_path
19
+ from .identity import repository_files, file_id, external_id
20
+ from .resolution import ImportResolver
21
+
22
+ DEPCRUISE_LANGUAGES = frozenset({"javascript", "typescript", "tsx"})
23
+ _JS_EXTENSIONS = (".js", ".jsx", ".mjs", ".cjs", ".ts", ".mts", ".cts", ".tsx")
24
+
25
+
26
+ def find_depcruise(depcruise_bin=None):
27
+ if depcruise_bin is not None:
28
+ candidate = os.path.expanduser(str(depcruise_bin))
29
+ if os.path.isfile(candidate):
30
+ return candidate
31
+ env = os.environ.get("CBE_DEPCRUISE_BIN")
32
+ if env and os.path.isfile(env):
33
+ return env
34
+ which = shutil.which("depcruise")
35
+ return which or None
36
+
37
+
38
+ def build_with_depcruise(repo, run_dir, depcruise_bin, timeout: int, languages: list[str]) -> dict:
39
+ os.makedirs(str(run_dir), exist_ok=True)
40
+ try:
41
+ completed = subprocess.run(
42
+ [str(depcruise_bin), "--no-config", "--output-type", "json", "."],
43
+ cwd=str(repo), check=False, capture_output=True, text=True, timeout=timeout,
44
+ stdin=subprocess.DEVNULL,
45
+ )
46
+ except subprocess.TimeoutExpired:
47
+ return _result([], [], [{"path": None, "code": "backend_timeout",
48
+ "message": f"dependency-cruiser timed out after {timeout}s"}],
49
+ languages, status="failed")
50
+ except OSError as exc:
51
+ return _result([], [], [{"path": None, "code": "backend_error",
52
+ "message": f"cannot run dependency-cruiser: {exc}"}],
53
+ languages, status="failed")
54
+ if completed.returncode != 0:
55
+ return _result([], [], [{"path": None, "code": "backend_error",
56
+ "message": f"dependency-cruiser failed (exit {completed.returncode})"}],
57
+ languages, status="failed")
58
+ try:
59
+ payload = json.loads(completed.stdout or "{}")
60
+ except json.JSONDecodeError as exc:
61
+ return _result([], [], [{"path": None, "code": "backend_error",
62
+ "message": f"dependency-cruiser output unreadable: {exc}"}],
63
+ languages, status="failed")
64
+
65
+ nodes: list[dict] = []
66
+ edges: list[dict] = []
67
+ seen: set[str] = set()
68
+ counter = 0
69
+ files = repository_files(repo)
70
+ if (not payload.get("modules") and any(lang in {"typescript", "tsx"} for lang in languages)
71
+ and any(path.endswith((".ts", ".tsx", ".mts", ".cts")) for path in files)):
72
+ return _result([], [], [{"path": None, "code": "depcruise_no_modules",
73
+ "message": "dependency-cruiser returned zero modules for TypeScript input; this usually means "
74
+ "the 'typescript' package is missing or unsupported (require >=2 <6)"}],
75
+ languages, status="failed")
76
+
77
+ def add_file(rel: str) -> str | None:
78
+ node_id = file_id(rel, files)
79
+ if node_id is None:
80
+ return None
81
+ if node_id not in seen:
82
+ seen.add(node_id)
83
+ nodes.append({"id": node_id, "kind": "file", "path": rel,
84
+ "start_line": None, "end_line": None, "name": rel,
85
+ "backend": "dependency-cruiser"})
86
+ return node_id
87
+
88
+ for module in payload.get("modules", []):
89
+ source = canonical_repo_path(module.get("source", ""))
90
+ if not source or not source.endswith(_JS_EXTENSIONS):
91
+ continue
92
+ source_id = add_file(source)
93
+ if source_id is None:
94
+ continue
95
+ for dep in module.get("dependencies", []):
96
+ resolved = dep.get("resolved")
97
+ expression = dep.get("module", "")
98
+ counter += 1
99
+ if dep.get("couldNotResolve"):
100
+ target_id = None
101
+ status = "unresolved"
102
+ elif resolved and not dep.get("coreModule"):
103
+ target_rel = canonical_repo_path(resolved)
104
+ target_id = add_file(target_rel) if target_rel else None
105
+ status = "resolved" if target_id else "unresolved"
106
+ elif dep.get("coreModule"):
107
+ target_rel = canonical_repo_path(str(expression))
108
+ target_id = external_id(target_rel or expression)
109
+ if target_id not in seen:
110
+ seen.add(target_id)
111
+ nodes.append({"id": target_id, "kind": "external", "path": None,
112
+ "start_line": None, "end_line": None, "name": expression,
113
+ "external": True, "backend": "dependency-cruiser"})
114
+ status = "external"
115
+ else:
116
+ target_id = None
117
+ status = "unresolved"
118
+ edges.append({
119
+ "id": f"dc/import/{counter}",
120
+ "source": source_id,
121
+ "target": target_id,
122
+ "kind": "import",
123
+ "status": status,
124
+ "path": source,
125
+ "expression": expression,
126
+ "analyzer_id": None,
127
+ "backend": "dependency-cruiser",
128
+ })
129
+ return _result(nodes, edges, [], languages, status="resolved" if edges else "empty")
130
+
131
+
132
+ def build_with_grimp(repo, languages: list[str], suffix_map=None) -> dict:
133
+ if "python" not in languages:
134
+ return {"nodes": [], "edges": [], "diagnostics": [], "capabilities": []}
135
+ try:
136
+ import grimp # noqa: PLC0415
137
+ except ImportError:
138
+ return {"nodes": [], "edges": [], "diagnostics": [
139
+ {"path": None, "code": "backend_unavailable",
140
+ "message": "grimp is not installed; Python import graph skipped (install the 'graphs' extra)"}],
141
+ "capabilities": []}
142
+ resolver = ImportResolver(repo, suffix_map=suffix_map)
143
+ package_roots = _python_package_roots(repo)
144
+ if not package_roots:
145
+ return _result([], [], [{"path": None, "code": "no_python_packages",
146
+ "message": "no importable top-level Python package was found"}],
147
+ None, status="empty", language="python")
148
+ diagnostics: list[dict] = []
149
+ nodes: list[dict] = []
150
+ edges: list[dict] = []
151
+ seen: set[str] = set()
152
+ built = 0
153
+ hard_failures = 0
154
+ not_packages = 0
155
+ inserted = []
156
+ for _, root in reversed(package_roots):
157
+ value = str(root)
158
+ sys.path.insert(0, value)
159
+ inserted.append(value)
160
+ try:
161
+ for package, _ in package_roots:
162
+ try:
163
+ graph = grimp.build_graph(package)
164
+ except Exception as exc: # grimp has several failure modes for arbitrary trees
165
+ kind = type(exc).__name__
166
+ if kind.startswith("NotATopLevel"):
167
+ not_packages += 1
168
+ diagnostics.append({"path": None, "code": "not_a_package",
169
+ "message": f"grimp treats {package!r} as a top-level module, not a package"})
170
+ else:
171
+ hard_failures += 1
172
+ diagnostics.append({"path": None, "code": "backend_error",
173
+ "message": f"grimp could not build {package}: {kind}"})
174
+ continue
175
+ built += 1
176
+ for module in graph.modules:
177
+ source_path = resolver.python_file(module)
178
+ if source_path is None:
179
+ diagnostics.append({"path": None, "code": "module_file_missing",
180
+ "message": f"grimp module {module!r} has no repository file"})
181
+ continue
182
+ module_id = _add_module(nodes, seen, module, source_path, resolver.files)
183
+ for imported in graph.find_modules_directly_imported_by(module):
184
+ target_path = resolver.python_file(imported)
185
+ status = "resolved" if target_path else ("unresolved" if resolver.python_local(imported) else "external")
186
+ target_id = _add_module(nodes, seen, imported, target_path, resolver.files) if status != "unresolved" else None
187
+ edges.append({
188
+ "id": f"grimp/import/{module}->{imported}",
189
+ "source": module_id,
190
+ "target": target_id,
191
+ "kind": "import",
192
+ "status": status,
193
+ "path": source_path,
194
+ "expression": imported,
195
+ "analyzer_id": None,
196
+ "backend": "grimp",
197
+ })
198
+ finally:
199
+ for value in inserted:
200
+ try:
201
+ sys.path.remove(value)
202
+ except ValueError:
203
+ pass
204
+ if edges:
205
+ status = "resolved"
206
+ elif built:
207
+ status = "empty"
208
+ elif hard_failures:
209
+ status = "failed"
210
+ elif not_packages:
211
+ status = "unsupported"
212
+ else:
213
+ status = "empty"
214
+ return _result(nodes, edges, diagnostics, None, status=status, language="python")
215
+
216
+
217
+ def _add_module(nodes: list[dict], seen: set[str], module: str, path: str | None, files) -> str:
218
+ module_id = file_id(path, files) if path else external_id(module)
219
+ if module_id not in seen:
220
+ seen.add(module_id)
221
+ nodes.append({"id": module_id, "kind": "file" if path else "external",
222
+ "path": path,
223
+ "start_line": None, "end_line": None, "name": module,
224
+ "backend": "grimp", **({"external": True} if path is None else {})})
225
+ return module_id
226
+
227
+
228
+ def _result(nodes, edges, diagnostics, languages, *, status, language=None) -> dict:
229
+ if language is not None:
230
+ capabilities = [{"language": language, "graph": "import", "backend": "grimp",
231
+ "status": status,
232
+ "limitations": "module-level imports only"}]
233
+ else:
234
+ capabilities = [{"language": lang, "graph": "import", "backend": "dependency-cruiser",
235
+ "status": status, "limitations": None} for lang in (languages or [])]
236
+ return {"nodes": nodes, "edges": edges, "diagnostics": diagnostics, "capabilities": capabilities}
237
+
238
+
239
+ def _python_package_roots(repo) -> list[tuple[str, object]]:
240
+ """Find top-level import names and the source roots that expose them."""
241
+ import pathlib
242
+
243
+ repo = pathlib.Path(repo)
244
+ files = repository_files(repo)
245
+ found: dict[tuple[str, str], None] = {}
246
+ for path in files:
247
+ pure = pathlib.PurePosixPath(path)
248
+ if path.endswith("/__init__.py"):
249
+ top = pure.parent
250
+ while str(top.parent) != "." and str(top.parent / "__init__.py") in files:
251
+ top = top.parent
252
+ root = repo if str(top.parent) == "." else repo / top.parent
253
+ found[(top.name, str(root))] = None
254
+ elif "/" not in path and pure.suffix == ".py" and pure.name != "setup.py":
255
+ found[(pure.stem, str(repo))] = None
256
+ return [(name, pathlib.Path(root)) for name, root in sorted(found)]