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.
- gwc_pybundle-1.4.5.dist-info/METADATA +876 -0
- gwc_pybundle-1.4.5.dist-info/RECORD +55 -0
- gwc_pybundle-1.4.5.dist-info/WHEEL +5 -0
- gwc_pybundle-1.4.5.dist-info/entry_points.txt +2 -0
- gwc_pybundle-1.4.5.dist-info/licenses/LICENSE.md +25 -0
- gwc_pybundle-1.4.5.dist-info/top_level.txt +1 -0
- pybundle/__init__.py +0 -0
- pybundle/__main__.py +4 -0
- pybundle/cli.py +365 -0
- pybundle/context.py +362 -0
- pybundle/doctor.py +148 -0
- pybundle/filters.py +178 -0
- pybundle/manifest.py +77 -0
- pybundle/packaging.py +45 -0
- pybundle/policy.py +132 -0
- pybundle/profiles.py +340 -0
- pybundle/roadmap_model.py +42 -0
- pybundle/roadmap_scan.py +295 -0
- pybundle/root_detect.py +14 -0
- pybundle/runner.py +163 -0
- pybundle/steps/__init__.py +26 -0
- pybundle/steps/bandit.py +72 -0
- pybundle/steps/base.py +20 -0
- pybundle/steps/compileall.py +76 -0
- pybundle/steps/context_expand.py +272 -0
- pybundle/steps/copy_pack.py +293 -0
- pybundle/steps/coverage.py +101 -0
- pybundle/steps/cprofile_step.py +155 -0
- pybundle/steps/dependency_sizes.py +120 -0
- pybundle/steps/duplication.py +94 -0
- pybundle/steps/error_refs.py +204 -0
- pybundle/steps/handoff_md.py +167 -0
- pybundle/steps/import_time.py +165 -0
- pybundle/steps/interrogate.py +84 -0
- pybundle/steps/license_scan.py +96 -0
- pybundle/steps/line_profiler.py +108 -0
- pybundle/steps/memory_profile.py +173 -0
- pybundle/steps/mutation_testing.py +136 -0
- pybundle/steps/mypy.py +60 -0
- pybundle/steps/pip_audit.py +45 -0
- pybundle/steps/pipdeptree.py +61 -0
- pybundle/steps/pylance.py +562 -0
- pybundle/steps/pytest.py +66 -0
- pybundle/steps/radon.py +121 -0
- pybundle/steps/repro_md.py +161 -0
- pybundle/steps/rg_scans.py +78 -0
- pybundle/steps/roadmap.py +153 -0
- pybundle/steps/ruff.py +111 -0
- pybundle/steps/shell.py +74 -0
- pybundle/steps/slow_tests.py +170 -0
- pybundle/steps/test_flakiness.py +172 -0
- pybundle/steps/tree.py +116 -0
- pybundle/steps/unused_deps.py +112 -0
- pybundle/steps/vulture.py +83 -0
- 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)
|