gwc-pybundle 1.4.5__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.

Potentially problematic release.


This version of gwc-pybundle might be problematic. Click here for more details.

Files changed (55) hide show
  1. gwc_pybundle-1.4.5.dist-info/METADATA +876 -0
  2. gwc_pybundle-1.4.5.dist-info/RECORD +55 -0
  3. gwc_pybundle-1.4.5.dist-info/WHEEL +5 -0
  4. gwc_pybundle-1.4.5.dist-info/entry_points.txt +2 -0
  5. gwc_pybundle-1.4.5.dist-info/licenses/LICENSE.md +25 -0
  6. gwc_pybundle-1.4.5.dist-info/top_level.txt +1 -0
  7. pybundle/__init__.py +0 -0
  8. pybundle/__main__.py +4 -0
  9. pybundle/cli.py +365 -0
  10. pybundle/context.py +362 -0
  11. pybundle/doctor.py +148 -0
  12. pybundle/filters.py +178 -0
  13. pybundle/manifest.py +77 -0
  14. pybundle/packaging.py +45 -0
  15. pybundle/policy.py +132 -0
  16. pybundle/profiles.py +340 -0
  17. pybundle/roadmap_model.py +42 -0
  18. pybundle/roadmap_scan.py +295 -0
  19. pybundle/root_detect.py +14 -0
  20. pybundle/runner.py +163 -0
  21. pybundle/steps/__init__.py +26 -0
  22. pybundle/steps/bandit.py +72 -0
  23. pybundle/steps/base.py +20 -0
  24. pybundle/steps/compileall.py +76 -0
  25. pybundle/steps/context_expand.py +272 -0
  26. pybundle/steps/copy_pack.py +293 -0
  27. pybundle/steps/coverage.py +101 -0
  28. pybundle/steps/cprofile_step.py +155 -0
  29. pybundle/steps/dependency_sizes.py +120 -0
  30. pybundle/steps/duplication.py +94 -0
  31. pybundle/steps/error_refs.py +204 -0
  32. pybundle/steps/handoff_md.py +167 -0
  33. pybundle/steps/import_time.py +165 -0
  34. pybundle/steps/interrogate.py +84 -0
  35. pybundle/steps/license_scan.py +96 -0
  36. pybundle/steps/line_profiler.py +108 -0
  37. pybundle/steps/memory_profile.py +173 -0
  38. pybundle/steps/mutation_testing.py +136 -0
  39. pybundle/steps/mypy.py +60 -0
  40. pybundle/steps/pip_audit.py +45 -0
  41. pybundle/steps/pipdeptree.py +61 -0
  42. pybundle/steps/pylance.py +562 -0
  43. pybundle/steps/pytest.py +66 -0
  44. pybundle/steps/radon.py +121 -0
  45. pybundle/steps/repro_md.py +161 -0
  46. pybundle/steps/rg_scans.py +78 -0
  47. pybundle/steps/roadmap.py +153 -0
  48. pybundle/steps/ruff.py +111 -0
  49. pybundle/steps/shell.py +74 -0
  50. pybundle/steps/slow_tests.py +170 -0
  51. pybundle/steps/test_flakiness.py +172 -0
  52. pybundle/steps/tree.py +116 -0
  53. pybundle/steps/unused_deps.py +112 -0
  54. pybundle/steps/vulture.py +83 -0
  55. pybundle/tools.py +63 -0
pybundle/manifest.py ADDED
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import subprocess # nosec B404 - Required for git operations, paths validated
5
+ from dataclasses import asdict
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .context import BundleContext
10
+
11
+
12
+ def _git_commit_hash(root: Path, git_path: str | None) -> str | None:
13
+ """Return HEAD commit hash if root is inside a git repo, else None."""
14
+ if not git_path:
15
+ return None
16
+ try:
17
+ # If this fails, we're not in a repo or git isn't functional.
18
+ p = subprocess.run( # nosec B603
19
+ [git_path, "rev-parse", "HEAD"],
20
+ cwd=str(root),
21
+ capture_output=True,
22
+ text=True,
23
+ check=True,
24
+ )
25
+ return p.stdout.strip() or None
26
+ except Exception:
27
+ return None
28
+
29
+
30
+ def write_manifest(
31
+ *,
32
+ ctx: BundleContext,
33
+ profile_name: str,
34
+ archive_path: Path,
35
+ archive_format_used: str,
36
+ ) -> None:
37
+ """Write a stable, machine-readable manifest for automation."""
38
+ git_hash = _git_commit_hash(ctx.root, ctx.tools.git)
39
+
40
+ manifest: dict[str, Any] = {
41
+ "schema_version": 1,
42
+ "tool": {"name": "pybundle"},
43
+ "timestamp_utc": ctx.ts,
44
+ "profile": profile_name,
45
+ "paths": {
46
+ "root": str(ctx.root),
47
+ "workdir": str(ctx.workdir),
48
+ "srcdir": str(ctx.srcdir),
49
+ "logdir": str(ctx.logdir),
50
+ "metadir": str(ctx.metadir),
51
+ },
52
+ "outputs": {
53
+ "archive": {
54
+ "path": str(archive_path),
55
+ "name": archive_path.name,
56
+ "format": archive_format_used,
57
+ },
58
+ "summary_json": str(ctx.summary_json),
59
+ "manifest_json": str(ctx.manifest_json),
60
+ "runlog": str(ctx.runlog),
61
+ },
62
+ "options": asdict(ctx.options),
63
+ "run": {
64
+ "strict": ctx.strict,
65
+ "redact": ctx.redact,
66
+ "keep_workdir": ctx.keep_workdir,
67
+ "archive_format_requested": ctx.archive_format,
68
+ "name_prefix": ctx.name_prefix,
69
+ },
70
+ "tools": asdict(ctx.tools),
71
+ "git": {"commit": git_hash},
72
+ }
73
+
74
+ ctx.manifest_json.write_text(
75
+ json.dumps(manifest, indent=2, sort_keys=True),
76
+ encoding="utf-8",
77
+ )
pybundle/packaging.py ADDED
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess # nosec B404 - Required for archive creation, paths validated
4
+ from pathlib import Path
5
+
6
+ from .context import BundleContext
7
+
8
+
9
+ def resolve_archive_format(ctx: BundleContext) -> str:
10
+ fmt_used = ctx.archive_format
11
+ if fmt_used == "auto":
12
+ fmt_used = "zip" if ctx.tools.zip else "tar.gz"
13
+ return fmt_used
14
+
15
+
16
+ def archive_output_path(ctx: BundleContext, fmt_used: str) -> Path:
17
+ if fmt_used == "zip":
18
+ return ctx.outdir / f"{ctx.name_prefix}.zip"
19
+ return ctx.outdir / f"{ctx.name_prefix}.tar.gz"
20
+
21
+
22
+ def make_archive(ctx: BundleContext) -> tuple[Path, str]:
23
+ fmt_used = resolve_archive_format(ctx)
24
+
25
+ if fmt_used == "zip":
26
+ if not ctx.tools.zip:
27
+ raise RuntimeError("zip tool not available")
28
+ out = archive_output_path(ctx, fmt_used)
29
+ # zip wants working dir above target folder
30
+ subprocess.run( # nosec B603
31
+ [ctx.tools.zip, "-qr", str(out), ctx.workdir.name],
32
+ cwd=str(ctx.workdir.parent),
33
+ check=False,
34
+ )
35
+ return out, fmt_used
36
+
37
+ if not ctx.tools.tar:
38
+ raise RuntimeError("tar tool not available")
39
+ out = archive_output_path(ctx, fmt_used)
40
+ subprocess.run( # nosec B603
41
+ [ctx.tools.tar, "-czf", str(out), ctx.workdir.name],
42
+ cwd=str(ctx.workdir.parent),
43
+ check=False,
44
+ )
45
+ return out, fmt_used
pybundle/policy.py ADDED
@@ -0,0 +1,132 @@
1
+ # pybundle/policy.py
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+
7
+ from pybundle.filters import (
8
+ DEFAULT_EXCLUDE_DIRS,
9
+ DEFAULT_EXCLUDE_FILE_EXTS,
10
+ DEFAULT_INCLUDE_FILES,
11
+ DEFAULT_INCLUDE_DIRS,
12
+ DEFAULT_INCLUDE_GLOBS,
13
+ EXCLUDE_PATTERNS,
14
+ is_excluded_by_name,
15
+ )
16
+
17
+
18
+ @dataclass(frozen=True)
19
+ class AIContextPolicy:
20
+ exclude_dirs: set[str] = field(default_factory=lambda: set(DEFAULT_EXCLUDE_DIRS))
21
+ exclude_patterns: set[str] = field(default_factory=lambda: set(EXCLUDE_PATTERNS))
22
+ exclude_file_exts: set[str] = field(
23
+ default_factory=lambda: set(DEFAULT_EXCLUDE_FILE_EXTS)
24
+ )
25
+
26
+ include_files: list[str] = field(
27
+ default_factory=lambda: list(DEFAULT_INCLUDE_FILES)
28
+ )
29
+ include_dirs: list[str] = field(default_factory=lambda: list(DEFAULT_INCLUDE_DIRS))
30
+ include_globs: list[str] = field(
31
+ default_factory=lambda: list(DEFAULT_INCLUDE_GLOBS)
32
+ )
33
+
34
+ # AI-friendly knobs
35
+ tree_max_depth: int = 4
36
+ largest_limit: int = 80
37
+ roadmap_max_files: int = 20000
38
+ roadmap_mermaid_depth: int = 2
39
+ roadmap_mermaid_max_edges: int = 180
40
+
41
+ def include_dir_candidates(self, root: Path) -> list[Path]:
42
+ out: list[Path] = []
43
+ for d in self.include_dirs:
44
+ p = root / d
45
+ if p.exists():
46
+ out.append(p)
47
+ return out or [root]
48
+
49
+
50
+ @dataclass
51
+ class PathFilter:
52
+ """
53
+ Shared filtering logic across steps:
54
+ - prune excluded dir names
55
+ - prune venvs by structure (any name)
56
+ - optionally exclude noisy file types by extension
57
+ """
58
+
59
+ exclude_dirs: set[str]
60
+ exclude_file_exts: set[str]
61
+ exclude_patterns: set[str] = field(default_factory=set)
62
+ detect_venvs: bool = True
63
+
64
+ def is_venv_root(self, p: Path) -> bool:
65
+ if not p.is_dir():
66
+ return False
67
+
68
+ if (p / "pyvenv.cfg").is_file():
69
+ return True
70
+
71
+ if (p / "bin").is_dir():
72
+ if (p / "bin" / "activate").is_file() and (
73
+ (p / "bin" / "python").exists() or (p / "bin" / "python3").exists()
74
+ ):
75
+ return True
76
+ if any((p / "lib").glob("python*/site-packages")):
77
+ return True
78
+
79
+ if (p / "Scripts").is_dir():
80
+ if (p / "Scripts" / "activate").is_file() and (
81
+ (p / "Scripts" / "python.exe").is_file()
82
+ or (p / "Scripts" / "python").exists()
83
+ ):
84
+ return True
85
+ if (p / "Lib" / "site-packages").is_dir():
86
+ return True
87
+
88
+ if (p / ".Python").exists():
89
+ return True
90
+
91
+ return False
92
+
93
+ def should_prune_dir(self, parent_dir: Path, child_name: str) -> bool:
94
+ if is_excluded_by_name(
95
+ child_name,
96
+ exclude_names=self.exclude_dirs,
97
+ exclude_patterns=self.exclude_patterns,
98
+ ):
99
+ return True
100
+ if self.detect_venvs and self.is_venv_root(parent_dir / child_name):
101
+ return True
102
+ return False
103
+
104
+ def should_include_file(self, root: Path, p: Path) -> bool:
105
+ try:
106
+ rel = p.relative_to(root)
107
+ except Exception:
108
+ return False
109
+
110
+ # reject files under excluded dirs by name/pattern
111
+ for part in rel.parts[:-1]:
112
+ if is_excluded_by_name(
113
+ part,
114
+ exclude_names=self.exclude_dirs,
115
+ exclude_patterns=self.exclude_patterns,
116
+ ):
117
+ return False
118
+
119
+ # reject excluded file names by pattern (e.g. *.egg, *.rej)
120
+ if is_excluded_by_name(
121
+ rel.name,
122
+ exclude_names=self.exclude_dirs,
123
+ exclude_patterns=self.exclude_patterns,
124
+ ):
125
+ return False
126
+
127
+ # reject excluded extensions
128
+ ext = p.suffix.lower()
129
+ if ext in self.exclude_file_exts:
130
+ return False
131
+
132
+ return True
pybundle/profiles.py ADDED
@@ -0,0 +1,340 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ import dataclasses
5
+ from .context import RunOptions
6
+ from .steps.shell import ShellStep
7
+ from .steps.tree import TreeStep, LargestFilesStep
8
+ from .steps.compileall import CompileAllStep
9
+ from .steps.ruff import RuffCheckStep, RuffFormatCheckStep
10
+ from .steps.mypy import MypyStep
11
+ from .steps.pylance import PylanceStep
12
+ from .steps.pytest import PytestStep
13
+ from .steps.bandit import BanditStep
14
+ from .steps.pip_audit import PipAuditStep
15
+ from .steps.coverage import CoverageStep
16
+ from .steps.rg_scans import default_rg_steps
17
+ from .steps.error_refs import ErrorReferencedFilesStep
18
+ from .steps.context_expand import ErrorContextExpandStep
19
+ from .steps.copy_pack import CuratedCopyStep
20
+ from .steps.repro_md import ReproMarkdownStep
21
+ from .steps.handoff_md import HandoffMarkdownStep
22
+ from .steps.roadmap import RoadmapStep
23
+ # Code quality tools (v1.3.0)
24
+ from .steps.vulture import VultureStep
25
+ from .steps.radon import RadonStep
26
+ from .steps.interrogate import InterrogateStep
27
+ from .steps.duplication import DuplicationStep
28
+ # Dependency analysis tools (v1.3.1)
29
+ from .steps.pipdeptree import PipdeptreeStep
30
+ from .steps.unused_deps import UnusedDependenciesStep
31
+ from .steps.license_scan import LicenseScanStep
32
+ from .steps.dependency_sizes import DependencySizesStep
33
+ # Performance profiling tools (v1.4.0)
34
+ from .steps.cprofile_step import CProfileStep
35
+ from .steps.import_time import ImportTimeStep
36
+ from .steps.memory_profile import MemoryProfileStep
37
+ from .steps.line_profiler import LineProfilerStep
38
+ # Test quality & coverage tools (v1.4.1)
39
+ from .steps.test_flakiness import TestFlakinessStep
40
+ from .steps.slow_tests import SlowTestsStep
41
+ from .steps.mutation_testing import MutationTestingStep
42
+ from .policy import AIContextPolicy
43
+ from .steps.handoff_md import HandoffMarkdownStep
44
+ from .steps.roadmap import RoadmapStep
45
+ from .policy import AIContextPolicy
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class Profile:
50
+ name: str
51
+ steps: list
52
+
53
+
54
+ def _dedupe_steps(steps: list) -> list:
55
+ seen = set()
56
+ out = []
57
+ for s in steps:
58
+ key = (
59
+ getattr(s, "out", None)
60
+ or getattr(s, "out_md", None)
61
+ or getattr(s, "name", None)
62
+ )
63
+ # fallback to class name if needed
64
+ key = key or s.__class__.__name__
65
+ if key in seen:
66
+ continue
67
+ seen.add(key)
68
+ out.append(s)
69
+ return out
70
+
71
+
72
+ def resolve_defaults(profile: str, opts: RunOptions) -> RunOptions:
73
+ if profile == "ai":
74
+ return dataclasses.replace(
75
+ opts,
76
+ no_ruff=opts.no_ruff if opts.no_ruff is not None else True,
77
+ no_mypy=opts.no_mypy if opts.no_mypy is not None else True,
78
+ no_pytest=opts.no_pytest if opts.no_pytest is not None else True,
79
+ no_rg=opts.no_rg if opts.no_rg is not None else True,
80
+ no_error_refs=opts.no_error_refs
81
+ if opts.no_error_refs is not None
82
+ else True,
83
+ no_context=opts.no_context if opts.no_context is not None else True,
84
+ no_compileall=opts.no_compileall
85
+ if opts.no_compileall is not None
86
+ else True,
87
+ )
88
+ return opts
89
+
90
+
91
+ def _analysis_steps(options: RunOptions) -> list:
92
+ """
93
+ Analysis profile: What the code IS
94
+ - Structure, metrics, dependencies
95
+ - Documentation and codebase understanding
96
+ - No error detection or linting
97
+ """
98
+ policy = AIContextPolicy()
99
+
100
+ steps: list = [
101
+ # Environment & git status
102
+ ShellStep(
103
+ "git status", "meta/00_git_status.txt", ["git", "status"], require_cmd="git"
104
+ ),
105
+ ShellStep(
106
+ "git diff", "meta/01_git_diff.txt", ["git", "diff"], require_cmd="git"
107
+ ),
108
+ ShellStep(
109
+ "uname -a", "meta/21_uname.txt", ["uname", "-a"], require_cmd="uname"
110
+ ),
111
+ ShellStep(
112
+ "python -V",
113
+ "meta/20_python_version.txt",
114
+ ["python", "-V"],
115
+ require_cmd="python",
116
+ ),
117
+ ShellStep(
118
+ "pip freeze",
119
+ "meta/22_pip_freeze.txt",
120
+ ["python", "-m", "pip", "freeze"],
121
+ require_cmd="python",
122
+ ),
123
+
124
+ # Code structure
125
+ TreeStep(max_depth=policy.tree_max_depth, policy=policy),
126
+ LargestFilesStep(limit=policy.largest_limit, policy=policy),
127
+ ]
128
+
129
+ # Code quality metrics (what the code looks like)
130
+ if not options.no_radon:
131
+ steps += [RadonStep()]
132
+
133
+ if not options.no_interrogate:
134
+ steps += [InterrogateStep()]
135
+
136
+ if not options.no_duplication:
137
+ steps += [DuplicationStep()]
138
+
139
+ # Dependency analysis (what the project uses)
140
+ if not options.no_pipdeptree:
141
+ steps += [PipdeptreeStep()]
142
+
143
+ if not options.no_license_scan:
144
+ steps += [LicenseScanStep()]
145
+
146
+ if not options.no_dependency_sizes:
147
+ steps += [DependencySizesStep()]
148
+
149
+ # Performance profiling (what the code does)
150
+ if not options.no_profile:
151
+ steps += [CProfileStep()]
152
+ steps += [ImportTimeStep()]
153
+
154
+ if options.profile_memory:
155
+ steps += [MemoryProfileStep()]
156
+
157
+ if options.enable_line_profiler:
158
+ steps += [LineProfilerStep()]
159
+
160
+ # Source snapshot and documentation
161
+ steps += [
162
+ CuratedCopyStep(policy=policy),
163
+ RoadmapStep(policy=policy),
164
+ HandoffMarkdownStep(),
165
+ ]
166
+
167
+ return _dedupe_steps(steps)
168
+
169
+
170
+ def _debug_steps(options: RunOptions) -> list:
171
+ """
172
+ Debug profile: What's WRONG with the code
173
+ - Linting, type checking, testing
174
+ - Security vulnerabilities
175
+ - Code quality issues
176
+ - Error detection and context
177
+ """
178
+ policy = AIContextPolicy()
179
+
180
+ steps: list = [
181
+ # Environment & git status
182
+ ShellStep(
183
+ "git status", "meta/00_git_status.txt", ["git", "status"], require_cmd="git"
184
+ ),
185
+ ShellStep(
186
+ "git diff", "meta/01_git_diff.txt", ["git", "diff"], require_cmd="git"
187
+ ),
188
+ ShellStep(
189
+ "uname -a", "meta/21_uname.txt", ["uname", "-a"], require_cmd="uname"
190
+ ),
191
+ ShellStep(
192
+ "python -V",
193
+ "meta/20_python_version.txt",
194
+ ["python", "-V"],
195
+ require_cmd="python",
196
+ ),
197
+ ShellStep(
198
+ "pip freeze",
199
+ "meta/22_pip_freeze.txt",
200
+ ["python", "-m", "pip", "freeze"],
201
+ require_cmd="python",
202
+ ),
203
+ ShellStep(
204
+ "pip check",
205
+ "logs/25_pip_check.txt",
206
+ ["python", "-m", "pip", "check"],
207
+ require_cmd="python",
208
+ ),
209
+
210
+ # Code structure (for context)
211
+ TreeStep(max_depth=policy.tree_max_depth, policy=policy),
212
+ LargestFilesStep(limit=policy.largest_limit, policy=policy),
213
+ ]
214
+
215
+ # Compilation errors
216
+ if not options.no_compileall:
217
+ steps.append(CompileAllStep())
218
+
219
+ # Linting & type checking (what's wrong with code style/types)
220
+ if not options.no_ruff:
221
+ steps += [
222
+ RuffCheckStep(target=options.ruff_target),
223
+ RuffFormatCheckStep(target=options.ruff_target),
224
+ ]
225
+
226
+ if not options.no_mypy:
227
+ steps += [MypyStep(target=options.mypy_target)]
228
+
229
+ if not options.no_pylance:
230
+ steps += [PylanceStep()]
231
+
232
+ # Testing (what's broken in functionality)
233
+ if not options.no_pytest:
234
+ steps += [PytestStep(args=options.pytest_args or ["-q"])]
235
+
236
+ if not options.no_coverage:
237
+ steps += [CoverageStep()]
238
+
239
+ # Test quality issues
240
+ steps += [TestFlakinessStep()] # Non-deterministic tests
241
+ steps += [SlowTestsStep()] # Performance issues in tests
242
+
243
+ if options.enable_mutation_testing:
244
+ steps += [MutationTestingStep()] # Test effectiveness
245
+
246
+ # Security vulnerabilities
247
+ if not options.no_bandit:
248
+ steps += [BanditStep()]
249
+
250
+ if not options.no_pip_audit:
251
+ steps += [PipAuditStep()]
252
+
253
+ # Code quality issues (dead code, complexity)
254
+ if not options.no_vulture:
255
+ steps += [VultureStep()]
256
+
257
+ if not options.no_radon:
258
+ steps += [RadonStep()]
259
+
260
+ # Dependency issues
261
+ if not options.no_unused_deps:
262
+ steps += [UnusedDependenciesStep()]
263
+
264
+ if not options.no_pipdeptree:
265
+ steps += [PipdeptreeStep()] # For conflict detection
266
+
267
+ # Pattern scanning (anti-patterns, TODOs)
268
+ if not options.no_rg:
269
+ steps += list(default_rg_steps(target="."))
270
+
271
+ # Error context extraction
272
+ if not options.no_error_refs:
273
+ steps += [ErrorReferencedFilesStep(max_files=options.error_max_files)]
274
+
275
+ if not options.no_context:
276
+ steps += [
277
+ ErrorContextExpandStep(
278
+ depth=options.context_depth,
279
+ max_files=options.context_max_files,
280
+ )
281
+ ]
282
+
283
+ # Source snapshot + repro documentation
284
+ steps += [
285
+ CuratedCopyStep(policy=policy),
286
+ ReproMarkdownStep(),
287
+ HandoffMarkdownStep(),
288
+ ]
289
+
290
+ return _dedupe_steps(steps)
291
+
292
+
293
+ def get_profile(name: str, options: RunOptions) -> Profile:
294
+ if name == "analysis":
295
+ return Profile(name="analysis", steps=_analysis_steps(options))
296
+
297
+ if name == "debug":
298
+ return Profile(name="debug", steps=_debug_steps(options))
299
+
300
+ if name == "backup":
301
+ # Minimal backup: source + environment, no analysis
302
+ policy = AIContextPolicy()
303
+ return Profile(
304
+ name="backup",
305
+ steps=[
306
+ ShellStep(
307
+ "git status",
308
+ "meta/00_git_status.txt",
309
+ ["git", "status"],
310
+ require_cmd="git",
311
+ ),
312
+ ShellStep(
313
+ "git diff",
314
+ "meta/01_git_diff.txt",
315
+ ["git", "diff"],
316
+ require_cmd="git",
317
+ ),
318
+ ShellStep(
319
+ "python -V",
320
+ "meta/20_python_version.txt",
321
+ ["python", "-V"],
322
+ require_cmd="python",
323
+ ),
324
+ ShellStep(
325
+ "pip freeze",
326
+ "meta/22_pip_freeze.txt",
327
+ ["python", "-m", "pip", "freeze"],
328
+ require_cmd="python",
329
+ ),
330
+ CuratedCopyStep(policy=policy), # Copy source code
331
+ ],
332
+ )
333
+
334
+ if name == "ai":
335
+ # AI profile: debug optimized for AI consumption
336
+ # Disables some noisy tools but keeps everything for problem understanding
337
+ opts = resolve_defaults("ai", options)
338
+ return Profile(name="ai", steps=_debug_steps(opts))
339
+
340
+ raise ValueError(f"unknown profile: {name}")
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from typing import Literal
5
+
6
+ Lang = Literal["python", "js", "ts", "rust", "html", "css", "config", "unknown"]
7
+ EdgeType = Literal["import", "require", "use", "mod", "include", "script", "entrypoint"]
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class Node:
12
+ id: str # stable id (usually path)
13
+ path: str # repo-relative
14
+ lang: Lang
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Edge:
19
+ src: str # node id
20
+ dst: str # node id (or synthetic id)
21
+ type: EdgeType
22
+ note: str = "" # e.g. "from X import Y", "package.json script: dev"
23
+
24
+
25
+ @dataclass
26
+ class EntryPoint:
27
+ node: str # node id
28
+ reason: str # why we think it's an entry
29
+ confidence: int = 2 # 1-3
30
+
31
+
32
+ @dataclass
33
+ class RoadmapGraph:
34
+ version: int
35
+ root: str
36
+ nodes: list[Node]
37
+ edges: list[Edge]
38
+ entrypoints: list[EntryPoint]
39
+ stats: dict[str, int] # counts by lang/edge types/etc.
40
+
41
+ def to_dict(self) -> dict:
42
+ return asdict(self)