gwc-pybundle 2.1.2__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-2.1.2.dist-info/METADATA +903 -0
- gwc_pybundle-2.1.2.dist-info/RECORD +82 -0
- gwc_pybundle-2.1.2.dist-info/WHEEL +5 -0
- gwc_pybundle-2.1.2.dist-info/entry_points.txt +2 -0
- gwc_pybundle-2.1.2.dist-info/licenses/LICENSE.md +25 -0
- gwc_pybundle-2.1.2.dist-info/top_level.txt +1 -0
- pybundle/__init__.py +0 -0
- pybundle/__main__.py +4 -0
- pybundle/cli.py +546 -0
- pybundle/context.py +404 -0
- pybundle/doctor.py +148 -0
- pybundle/filters.py +228 -0
- pybundle/manifest.py +77 -0
- pybundle/packaging.py +45 -0
- pybundle/policy.py +132 -0
- pybundle/profiles.py +454 -0
- pybundle/roadmap_model.py +42 -0
- pybundle/roadmap_scan.py +328 -0
- pybundle/root_detect.py +14 -0
- pybundle/runner.py +180 -0
- pybundle/steps/__init__.py +26 -0
- pybundle/steps/ai_context.py +791 -0
- pybundle/steps/api_docs.py +219 -0
- pybundle/steps/asyncio_analysis.py +358 -0
- pybundle/steps/bandit.py +72 -0
- pybundle/steps/base.py +20 -0
- pybundle/steps/blocking_call_detection.py +291 -0
- pybundle/steps/call_graph.py +219 -0
- pybundle/steps/compileall.py +76 -0
- pybundle/steps/config_docs.py +319 -0
- pybundle/steps/config_validation.py +302 -0
- pybundle/steps/container_image.py +294 -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 +166 -0
- pybundle/steps/dependency_sizes.py +136 -0
- pybundle/steps/django_checks.py +214 -0
- pybundle/steps/dockerfile_lint.py +282 -0
- pybundle/steps/dockerignore.py +311 -0
- pybundle/steps/duplication.py +103 -0
- pybundle/steps/env_completeness.py +269 -0
- pybundle/steps/env_var_usage.py +253 -0
- pybundle/steps/error_refs.py +204 -0
- pybundle/steps/event_loop_patterns.py +280 -0
- pybundle/steps/exception_patterns.py +190 -0
- pybundle/steps/fastapi_integration.py +250 -0
- pybundle/steps/flask_debugging.py +312 -0
- pybundle/steps/git_analytics.py +315 -0
- pybundle/steps/handoff_md.py +176 -0
- pybundle/steps/import_time.py +175 -0
- pybundle/steps/interrogate.py +106 -0
- pybundle/steps/license_scan.py +96 -0
- pybundle/steps/line_profiler.py +117 -0
- pybundle/steps/link_validation.py +287 -0
- pybundle/steps/logging_analysis.py +233 -0
- pybundle/steps/memory_profile.py +176 -0
- pybundle/steps/migration_history.py +336 -0
- pybundle/steps/mutation_testing.py +141 -0
- pybundle/steps/mypy.py +103 -0
- pybundle/steps/orm_optimization.py +316 -0
- pybundle/steps/pip_audit.py +45 -0
- pybundle/steps/pipdeptree.py +62 -0
- pybundle/steps/pylance.py +562 -0
- pybundle/steps/pytest.py +66 -0
- pybundle/steps/query_pattern_analysis.py +334 -0
- pybundle/steps/radon.py +161 -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 +117 -0
- pybundle/steps/secrets_detection.py +235 -0
- pybundle/steps/security_headers.py +309 -0
- pybundle/steps/shell.py +74 -0
- pybundle/steps/slow_tests.py +178 -0
- pybundle/steps/sqlalchemy_validation.py +269 -0
- pybundle/steps/test_flakiness.py +184 -0
- pybundle/steps/tree.py +116 -0
- pybundle/steps/type_coverage.py +277 -0
- pybundle/steps/unused_deps.py +211 -0
- pybundle/steps/vulture.py +167 -0
- pybundle/tools.py +63 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess # nosec B404 - Required for tool execution, paths validated
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .base import StepResult
|
|
10
|
+
from ..context import BundleContext
|
|
11
|
+
from ..filters import should_exclude_from_analysis
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class AIContextStep:
|
|
16
|
+
"""Generate AI_CONTEXT.md file with auto-detected project information."""
|
|
17
|
+
|
|
18
|
+
name: str = "AI context"
|
|
19
|
+
outfile: str = "AI_CONTEXT.md"
|
|
20
|
+
|
|
21
|
+
def run(self, ctx: BundleContext) -> StepResult:
|
|
22
|
+
start = time.time()
|
|
23
|
+
out = ctx.workdir / self.outfile
|
|
24
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
content = self._generate_ai_context(ctx)
|
|
28
|
+
out.write_text(content, encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
elapsed = int((time.time() - start) * 1000)
|
|
31
|
+
return StepResult(self.name, "OK", elapsed, "")
|
|
32
|
+
except Exception as e:
|
|
33
|
+
out.write_text(f"Error generating AI_CONTEXT.md: {e}\n", encoding="utf-8")
|
|
34
|
+
return StepResult(
|
|
35
|
+
self.name, "FAIL", int((time.time() - start) * 1000), str(e)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def _generate_ai_context(self, ctx: BundleContext) -> str:
|
|
39
|
+
"""Generate the AI_CONTEXT.md content with auto-detected information."""
|
|
40
|
+
sections = []
|
|
41
|
+
|
|
42
|
+
# Header
|
|
43
|
+
sections.append(self._generate_header(ctx))
|
|
44
|
+
|
|
45
|
+
# 0) What this project is
|
|
46
|
+
sections.append(self._generate_project_overview(ctx))
|
|
47
|
+
|
|
48
|
+
# 1) How to run it
|
|
49
|
+
sections.append(self._generate_how_to_run(ctx))
|
|
50
|
+
|
|
51
|
+
# 2) Repository map
|
|
52
|
+
sections.append(self._generate_repo_map(ctx))
|
|
53
|
+
|
|
54
|
+
# 3) Architecture & patterns
|
|
55
|
+
sections.append(self._generate_architecture(ctx))
|
|
56
|
+
|
|
57
|
+
# 4) Configuration
|
|
58
|
+
sections.append(self._generate_configuration(ctx))
|
|
59
|
+
|
|
60
|
+
# 5) Data model & storage
|
|
61
|
+
sections.append(self._generate_data_model(ctx))
|
|
62
|
+
|
|
63
|
+
# 6) API surface
|
|
64
|
+
sections.append(self._generate_api_surface(ctx))
|
|
65
|
+
|
|
66
|
+
# 7) Tests & quality gates
|
|
67
|
+
sections.append(self._generate_tests_quality(ctx))
|
|
68
|
+
|
|
69
|
+
# 8) Sharp edges
|
|
70
|
+
sections.append(self._generate_sharp_edges(ctx))
|
|
71
|
+
|
|
72
|
+
# 9) Change guide
|
|
73
|
+
sections.append(self._generate_change_guide(ctx))
|
|
74
|
+
|
|
75
|
+
# 10) AI rules
|
|
76
|
+
sections.append(self._generate_ai_rules(ctx))
|
|
77
|
+
|
|
78
|
+
return "\n\n".join(sections)
|
|
79
|
+
|
|
80
|
+
def _generate_header(self, ctx: BundleContext) -> str:
|
|
81
|
+
"""Generate the header section."""
|
|
82
|
+
project_name = ctx.root.name
|
|
83
|
+
return f"""# AI_CONTEXT.md
|
|
84
|
+
|
|
85
|
+
> **Auto-generated by pybundle** — {time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime())}
|
|
86
|
+
>
|
|
87
|
+
> **Purpose**: Give an AI assistant (or a new dev) just enough truth to make safe, consistent changes without guessing.
|
|
88
|
+
>
|
|
89
|
+
> **Rules**: Prefer facts over vibes. Link to files. Keep it updated.
|
|
90
|
+
|
|
91
|
+
**Project**: `{project_name}`
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
**How to interpret this document:**
|
|
96
|
+
|
|
97
|
+
* **Known Facts** — Extracted directly from code/configs with file:line refs (high confidence)
|
|
98
|
+
* **Inferred** — Pattern matching auto-detection (verify before trusting)
|
|
99
|
+
|
|
100
|
+
When you see file paths or specific references, treat as known fact. When you see "(detected)" without refs, verify first."""
|
|
101
|
+
|
|
102
|
+
def _generate_project_overview(self, ctx: BundleContext) -> str:
|
|
103
|
+
"""Generate section 0: What this project is."""
|
|
104
|
+
# Try to extract from README
|
|
105
|
+
readme_path = ctx.root / "README.md"
|
|
106
|
+
description = "Python project"
|
|
107
|
+
|
|
108
|
+
if readme_path.exists():
|
|
109
|
+
try:
|
|
110
|
+
content = readme_path.read_text(encoding="utf-8", errors="ignore")
|
|
111
|
+
lines = content.splitlines()
|
|
112
|
+
# Look for first non-empty, non-heading line as description
|
|
113
|
+
for line in lines[1:10]: # Check first 10 lines
|
|
114
|
+
line = line.strip()
|
|
115
|
+
if line and not line.startswith("#") and not line.startswith("!"):
|
|
116
|
+
description = line
|
|
117
|
+
break
|
|
118
|
+
except Exception:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
# Detect framework
|
|
122
|
+
framework = self._detect_framework(ctx.root)
|
|
123
|
+
framework_note = f" (using {framework})" if framework else ""
|
|
124
|
+
|
|
125
|
+
return f"""---
|
|
126
|
+
|
|
127
|
+
## 0) What this project is
|
|
128
|
+
|
|
129
|
+
**One-liner:** {description}
|
|
130
|
+
|
|
131
|
+
**Type:** {framework_note if framework_note else "Python application"}
|
|
132
|
+
|
|
133
|
+
**Core functionality:**
|
|
134
|
+
{self._detect_core_functionality(ctx.root)}
|
|
135
|
+
|
|
136
|
+
**Entry points:**
|
|
137
|
+
{self._list_entrypoints(ctx.root)}"""
|
|
138
|
+
|
|
139
|
+
def _generate_how_to_run(self, ctx: BundleContext) -> str:
|
|
140
|
+
"""Generate section 1: How to run it."""
|
|
141
|
+
python_version = "3.9+"
|
|
142
|
+
if ctx.tools.python:
|
|
143
|
+
try:
|
|
144
|
+
result = subprocess.run( # nosec B603
|
|
145
|
+
[ctx.tools.python, "--version"],
|
|
146
|
+
capture_output=True,
|
|
147
|
+
text=True,
|
|
148
|
+
timeout=5,
|
|
149
|
+
)
|
|
150
|
+
if result.returncode == 0:
|
|
151
|
+
python_version = result.stdout.strip().replace("Python ", "")
|
|
152
|
+
except Exception:
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
# Detect requirements file
|
|
156
|
+
req_file = "requirements.txt"
|
|
157
|
+
if (ctx.root / "pyproject.toml").exists():
|
|
158
|
+
req_file = "pyproject.toml (using pip install .)"
|
|
159
|
+
|
|
160
|
+
# Detect how to run
|
|
161
|
+
run_command = self._detect_run_command(ctx.root)
|
|
162
|
+
|
|
163
|
+
return f"""---
|
|
164
|
+
|
|
165
|
+
## 1) How to run it (local)
|
|
166
|
+
|
|
167
|
+
**Prerequisites:**
|
|
168
|
+
|
|
169
|
+
* Python: `{python_version}`
|
|
170
|
+
* Dependencies: See `{req_file}`
|
|
171
|
+
|
|
172
|
+
**Install:**
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
python -m venv .venv
|
|
176
|
+
source .venv/bin/activate # Windows: .venv\\Scripts\\activate
|
|
177
|
+
pip install -r requirements.txt
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Run:**
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
{run_command}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Development tasks:**
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# Format code
|
|
190
|
+
ruff format .
|
|
191
|
+
|
|
192
|
+
# Lint
|
|
193
|
+
ruff check .
|
|
194
|
+
|
|
195
|
+
# Type check
|
|
196
|
+
mypy .
|
|
197
|
+
|
|
198
|
+
# Run tests
|
|
199
|
+
pytest
|
|
200
|
+
```"""
|
|
201
|
+
|
|
202
|
+
def _generate_repo_map(self, ctx: BundleContext) -> str:
|
|
203
|
+
"""Generate section 2: Repository map."""
|
|
204
|
+
# Find key directories
|
|
205
|
+
key_dirs = []
|
|
206
|
+
for dir_name in ["app", "src", "lib", "core", "api", "routers", "models"]:
|
|
207
|
+
dir_path = ctx.root / dir_name
|
|
208
|
+
if dir_path.is_dir():
|
|
209
|
+
key_dirs.append(f"* `{dir_name}/` — {self._describe_directory(dir_path)}")
|
|
210
|
+
|
|
211
|
+
# Find key files
|
|
212
|
+
key_files = []
|
|
213
|
+
for file_name in ["main.py", "app.py", "__main__.py", "server.py", "cli.py"]:
|
|
214
|
+
file_path = ctx.root / file_name
|
|
215
|
+
if file_path.exists():
|
|
216
|
+
key_files.append(f"* `{file_name}` — Main entry point")
|
|
217
|
+
|
|
218
|
+
dirs_section = "\n".join(key_dirs) if key_dirs else "* (Auto-detection found no standard structure)"
|
|
219
|
+
files_section = "\n".join(key_files) if key_files else "* (No main entry files detected)"
|
|
220
|
+
|
|
221
|
+
return f"""---
|
|
222
|
+
|
|
223
|
+
## 2) Repository map (what to read first)
|
|
224
|
+
|
|
225
|
+
### Primary entry points
|
|
226
|
+
|
|
227
|
+
{files_section}
|
|
228
|
+
|
|
229
|
+
### Key directories
|
|
230
|
+
|
|
231
|
+
{dirs_section}
|
|
232
|
+
|
|
233
|
+
### Tests
|
|
234
|
+
|
|
235
|
+
* `tests/` — Test suite (if present)"""
|
|
236
|
+
|
|
237
|
+
def _generate_architecture(self, ctx: BundleContext) -> str:
|
|
238
|
+
"""Generate section 3: Architecture & patterns."""
|
|
239
|
+
framework = self._detect_framework(ctx.root)
|
|
240
|
+
|
|
241
|
+
patterns = []
|
|
242
|
+
if self._file_exists_in_tree(ctx.root, "*router*.py"):
|
|
243
|
+
patterns.append("* Routing-based architecture (routers define endpoints)")
|
|
244
|
+
if self._file_exists_in_tree(ctx.root, "*service*.py"):
|
|
245
|
+
patterns.append("* Service layer pattern (business logic in services)")
|
|
246
|
+
if self._file_exists_in_tree(ctx.root, "*repository*.py") or self._file_exists_in_tree(ctx.root, "*repo.py"):
|
|
247
|
+
patterns.append("* Repository pattern (data access abstraction)")
|
|
248
|
+
if self._file_exists_in_tree(ctx.root, "middleware*.py"):
|
|
249
|
+
patterns.append("* Middleware for cross-cutting concerns")
|
|
250
|
+
|
|
251
|
+
patterns_section = "\n".join(patterns) if patterns else "* (No clear architectural patterns auto-detected)"
|
|
252
|
+
|
|
253
|
+
return f"""---
|
|
254
|
+
|
|
255
|
+
## 3) Architecture & patterns
|
|
256
|
+
|
|
257
|
+
**Framework:** {framework or "Not detected"}
|
|
258
|
+
|
|
259
|
+
**Key patterns:**
|
|
260
|
+
|
|
261
|
+
{patterns_section}
|
|
262
|
+
|
|
263
|
+
**Error handling:**
|
|
264
|
+
|
|
265
|
+
{self._detect_error_handling(ctx.root)}
|
|
266
|
+
|
|
267
|
+
**Logging:**
|
|
268
|
+
|
|
269
|
+
{self._detect_logging(ctx.root)}"""
|
|
270
|
+
|
|
271
|
+
def _generate_configuration(self, ctx: BundleContext) -> str:
|
|
272
|
+
"""Generate section 4: Configuration."""
|
|
273
|
+
env_vars = self._extract_env_vars(ctx.root)
|
|
274
|
+
|
|
275
|
+
required = []
|
|
276
|
+
optional = []
|
|
277
|
+
|
|
278
|
+
for var in sorted(env_vars.keys()):
|
|
279
|
+
refs = env_vars[var]
|
|
280
|
+
# Show first 3 file:line references
|
|
281
|
+
ref_str = ", ".join(f"{path}:{line}" for path, line in refs[:3])
|
|
282
|
+
if len(refs) > 3:
|
|
283
|
+
ref_str += f" (+{len(refs) - 3} more)"
|
|
284
|
+
|
|
285
|
+
# Heuristic: vars with "SECRET", "KEY", "TOKEN" are likely required
|
|
286
|
+
if any(x in var.upper() for x in ["SECRET", "KEY", "TOKEN", "PASSWORD"]):
|
|
287
|
+
required.append(f"* `{var}` — used in [{ref_str}]")
|
|
288
|
+
else:
|
|
289
|
+
optional.append(f"* `{var}` — used in [{ref_str}]")
|
|
290
|
+
|
|
291
|
+
required_section = "\n".join(required) if required else "* (None detected)"
|
|
292
|
+
optional_section = "\n".join(optional) if optional else "* (None detected)"
|
|
293
|
+
|
|
294
|
+
config_files = []
|
|
295
|
+
for cf in ["pyproject.toml", "setup.cfg", "mypy.ini", ".env.example"]:
|
|
296
|
+
if (ctx.root / cf).exists():
|
|
297
|
+
config_files.append(f"* `{cf}`")
|
|
298
|
+
|
|
299
|
+
config_section = "\n".join(config_files) if config_files else "* (None detected)"
|
|
300
|
+
|
|
301
|
+
return f"""---
|
|
302
|
+
|
|
303
|
+
## 4) Configuration (env vars + config files)
|
|
304
|
+
|
|
305
|
+
**Required environment variables:**
|
|
306
|
+
|
|
307
|
+
{required_section}
|
|
308
|
+
|
|
309
|
+
**Optional environment variables:**
|
|
310
|
+
|
|
311
|
+
{optional_section}
|
|
312
|
+
|
|
313
|
+
**Config files:**
|
|
314
|
+
|
|
315
|
+
{config_section}"""
|
|
316
|
+
|
|
317
|
+
def _generate_data_model(self, ctx: BundleContext) -> str:
|
|
318
|
+
"""Generate section 5: Data model & storage."""
|
|
319
|
+
# Detect database usage
|
|
320
|
+
db_type = self._detect_database(ctx.root)
|
|
321
|
+
orm = self._detect_orm(ctx.root)
|
|
322
|
+
|
|
323
|
+
# Find model files
|
|
324
|
+
model_files = []
|
|
325
|
+
for pattern in ["*model*.py", "*schema*.py"]:
|
|
326
|
+
for p in ctx.root.rglob(pattern):
|
|
327
|
+
if not any(x in p.parts for x in [".venv", "venv", "__pycache__", "artifacts"]):
|
|
328
|
+
rel_path = p.relative_to(ctx.root)
|
|
329
|
+
model_files.append(f"* `{rel_path}` — Data models/schemas")
|
|
330
|
+
if len(model_files) >= 3: # Limit to 3 files
|
|
331
|
+
break
|
|
332
|
+
|
|
333
|
+
models_section = "\n".join(model_files) if model_files else "* (No model files detected)"
|
|
334
|
+
|
|
335
|
+
return f"""---
|
|
336
|
+
|
|
337
|
+
## 5) Data model & storage
|
|
338
|
+
|
|
339
|
+
**Database:** {db_type or "Not detected"}
|
|
340
|
+
|
|
341
|
+
**ORM/Data layer:** {orm or "Not detected"}
|
|
342
|
+
|
|
343
|
+
**Model files:**
|
|
344
|
+
|
|
345
|
+
{models_section}
|
|
346
|
+
|
|
347
|
+
**Migrations:**
|
|
348
|
+
|
|
349
|
+
{self._detect_migrations(ctx.root)}"""
|
|
350
|
+
|
|
351
|
+
def _generate_api_surface(self, ctx: BundleContext) -> str:
|
|
352
|
+
"""Generate section 6: API surface."""
|
|
353
|
+
# Detect API routes
|
|
354
|
+
routes = self._extract_routes(ctx.root)
|
|
355
|
+
|
|
356
|
+
if routes:
|
|
357
|
+
routes_section = "\n".join(f"* `{r}`" for r in routes[:10]) # Limit to 10
|
|
358
|
+
else:
|
|
359
|
+
routes_section = "* (No API routes auto-detected)"
|
|
360
|
+
|
|
361
|
+
return f"""---
|
|
362
|
+
|
|
363
|
+
## 6) API surface (what's stable)
|
|
364
|
+
|
|
365
|
+
**Detected endpoints:**
|
|
366
|
+
|
|
367
|
+
{routes_section}
|
|
368
|
+
|
|
369
|
+
**Note:** This is auto-detected. Verify actual endpoints in your router files."""
|
|
370
|
+
|
|
371
|
+
def _generate_tests_quality(self, ctx: BundleContext) -> str:
|
|
372
|
+
"""Generate section 7: Tests & quality gates."""
|
|
373
|
+
test_framework = "pytest" if self._file_exists_in_tree(ctx.root, "test_*.py") else "Not detected"
|
|
374
|
+
test_dir = "tests/" if (ctx.root / "tests").is_dir() else "Not detected"
|
|
375
|
+
|
|
376
|
+
return f"""---
|
|
377
|
+
|
|
378
|
+
## 7) Tests & quality gates
|
|
379
|
+
|
|
380
|
+
**Test framework:** {test_framework}
|
|
381
|
+
|
|
382
|
+
**Test location:** `{test_dir}`
|
|
383
|
+
|
|
384
|
+
**Run tests:**
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
pytest
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**Quality checks:**
|
|
391
|
+
|
|
392
|
+
```bash
|
|
393
|
+
# Linting
|
|
394
|
+
ruff check .
|
|
395
|
+
|
|
396
|
+
# Type checking
|
|
397
|
+
mypy .
|
|
398
|
+
|
|
399
|
+
# Code coverage
|
|
400
|
+
pytest --cov
|
|
401
|
+
```"""
|
|
402
|
+
|
|
403
|
+
def _generate_sharp_edges(self, ctx: BundleContext) -> str:
|
|
404
|
+
"""Generate section 8: Sharp edges."""
|
|
405
|
+
return """---
|
|
406
|
+
|
|
407
|
+
## 8) Sharp edges (gotchas)
|
|
408
|
+
|
|
409
|
+
**Known issues:**
|
|
410
|
+
|
|
411
|
+
* (Add known issues manually)
|
|
412
|
+
|
|
413
|
+
**Common mistakes:**
|
|
414
|
+
|
|
415
|
+
* (Add common mistakes manually)
|
|
416
|
+
|
|
417
|
+
**Performance notes:**
|
|
418
|
+
|
|
419
|
+
* (Add performance considerations manually)"""
|
|
420
|
+
|
|
421
|
+
def _generate_change_guide(self, ctx: BundleContext) -> str:
|
|
422
|
+
"""Generate section 9: Change guide."""
|
|
423
|
+
return """---
|
|
424
|
+
|
|
425
|
+
## 9) Change guide (common tasks)
|
|
426
|
+
|
|
427
|
+
**Add a new API endpoint:**
|
|
428
|
+
|
|
429
|
+
1. Create route handler in appropriate router file
|
|
430
|
+
2. Add validation schemas if needed
|
|
431
|
+
3. Update tests
|
|
432
|
+
4. Document in OpenAPI/docstring
|
|
433
|
+
|
|
434
|
+
**Add a new database model:**
|
|
435
|
+
|
|
436
|
+
1. Define model in models file
|
|
437
|
+
2. Create migration (if using migrations)
|
|
438
|
+
3. Update related services/repositories
|
|
439
|
+
4. Add tests
|
|
440
|
+
|
|
441
|
+
**Add a new dependency:**
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
pip install <package>
|
|
445
|
+
pip freeze > requirements.txt
|
|
446
|
+
```"""
|
|
447
|
+
|
|
448
|
+
def _generate_ai_rules(self, ctx: BundleContext) -> str:
|
|
449
|
+
"""Generate section 10: AI rules."""
|
|
450
|
+
return """---
|
|
451
|
+
|
|
452
|
+
## 10) AI rules (when changing code)
|
|
453
|
+
|
|
454
|
+
**ALWAYS:**
|
|
455
|
+
|
|
456
|
+
* Preserve existing error handling patterns
|
|
457
|
+
* Maintain type hints on all function signatures
|
|
458
|
+
* Update tests when changing behavior
|
|
459
|
+
* Keep existing logging format
|
|
460
|
+
* Follow the patterns in existing code
|
|
461
|
+
|
|
462
|
+
**NEVER:**
|
|
463
|
+
|
|
464
|
+
* Remove type hints
|
|
465
|
+
* Skip error handling
|
|
466
|
+
* Change API contracts without discussion
|
|
467
|
+
* Remove existing tests
|
|
468
|
+
* Modify configuration defaults without noting it
|
|
469
|
+
|
|
470
|
+
**WHEN IN DOUBT:**
|
|
471
|
+
|
|
472
|
+
* Ask before changing database schemas
|
|
473
|
+
* Ask before adding new dependencies
|
|
474
|
+
* Ask before changing API response shapes
|
|
475
|
+
* Check existing code for similar patterns"""
|
|
476
|
+
|
|
477
|
+
# Helper methods for detection
|
|
478
|
+
|
|
479
|
+
def _detect_framework(self, root: Path) -> str | None:
|
|
480
|
+
"""Detect web framework used."""
|
|
481
|
+
for p in root.rglob("*.py"):
|
|
482
|
+
if any(x in p.parts for x in [".venv", "venv", "__pycache__"]):
|
|
483
|
+
continue
|
|
484
|
+
try:
|
|
485
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
486
|
+
if "from fastapi" in content or "import fastapi" in content:
|
|
487
|
+
return "FastAPI"
|
|
488
|
+
if "from flask" in content or "import flask" in content:
|
|
489
|
+
return "Flask"
|
|
490
|
+
if "from django" in content or "import django" in content:
|
|
491
|
+
return "Django"
|
|
492
|
+
except Exception:
|
|
493
|
+
continue
|
|
494
|
+
return None
|
|
495
|
+
|
|
496
|
+
def _detect_core_functionality(self, root: Path) -> str:
|
|
497
|
+
"""Detect core functionality from code structure."""
|
|
498
|
+
features = []
|
|
499
|
+
|
|
500
|
+
if self._file_exists_in_tree(root, "*api*.py") or self._file_exists_in_tree(root, "*router*.py"):
|
|
501
|
+
features.append("* REST API endpoints")
|
|
502
|
+
if self._file_exists_in_tree(root, "*template*.html") or (root / "templates").is_dir():
|
|
503
|
+
features.append("* Server-side HTML rendering")
|
|
504
|
+
if self._file_exists_in_tree(root, "*websocket*.py"):
|
|
505
|
+
features.append("* WebSocket support")
|
|
506
|
+
if self._file_exists_in_tree(root, "*celery*.py") or self._file_exists_in_tree(root, "*task*.py"):
|
|
507
|
+
features.append("* Background task processing")
|
|
508
|
+
if self._file_exists_in_tree(root, "*auth*.py"):
|
|
509
|
+
features.append("* Authentication/authorization")
|
|
510
|
+
|
|
511
|
+
return "\n".join(features) if features else "* (Auto-detection could not determine core features)"
|
|
512
|
+
|
|
513
|
+
def _list_entrypoints(self, root: Path) -> str:
|
|
514
|
+
"""List detected entry points."""
|
|
515
|
+
entrypoints = []
|
|
516
|
+
|
|
517
|
+
# Check for __main__.py
|
|
518
|
+
if (root / "__main__.py").exists():
|
|
519
|
+
entrypoints.append("* `python -m <package>` (via __main__.py)")
|
|
520
|
+
|
|
521
|
+
# Check for main app blocks in common locations
|
|
522
|
+
for pattern in ["main.py", "app.py", "server.py", "*/main.py", "*/app.py"]:
|
|
523
|
+
for p in root.glob(pattern):
|
|
524
|
+
if should_exclude_from_analysis(p):
|
|
525
|
+
continue
|
|
526
|
+
try:
|
|
527
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
528
|
+
if 'if __name__ == "__main__"' in content:
|
|
529
|
+
rel_path = p.relative_to(root)
|
|
530
|
+
entrypoints.append(f"* `python {rel_path}` (__main__ block)")
|
|
531
|
+
except Exception:
|
|
532
|
+
pass
|
|
533
|
+
|
|
534
|
+
# Detect FastAPI/uvicorn apps
|
|
535
|
+
for p in root.rglob("*.py"):
|
|
536
|
+
if should_exclude_from_analysis(p):
|
|
537
|
+
continue
|
|
538
|
+
try:
|
|
539
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
540
|
+
if "FastAPI(" in content:
|
|
541
|
+
# Extract variable name
|
|
542
|
+
import re
|
|
543
|
+
match = re.search(r'(\w+)\s*=\s*FastAPI\(', content)
|
|
544
|
+
if match:
|
|
545
|
+
app_var = match.group(1)
|
|
546
|
+
rel_path = str(p.relative_to(root)).replace("/", ".").replace("\\", ".").replace(".py", "")
|
|
547
|
+
entrypoints.append(f"* `uvicorn {rel_path}:{app_var}` (FastAPI)")
|
|
548
|
+
except Exception:
|
|
549
|
+
pass
|
|
550
|
+
|
|
551
|
+
# Check for console_scripts in pyproject.toml
|
|
552
|
+
pyproject = root / "pyproject.toml"
|
|
553
|
+
if pyproject.exists():
|
|
554
|
+
try:
|
|
555
|
+
import tomllib
|
|
556
|
+
with open(pyproject, "rb") as f:
|
|
557
|
+
data = tomllib.load(f)
|
|
558
|
+
|
|
559
|
+
scripts = data.get("project", {}).get("scripts", {})
|
|
560
|
+
for name, target in scripts.items():
|
|
561
|
+
entrypoints.append(f"* `{name}` → `{target}` (console script)")
|
|
562
|
+
|
|
563
|
+
poetry_scripts = data.get("tool", {}).get("poetry", {}).get("scripts", {})
|
|
564
|
+
for name, target in poetry_scripts.items():
|
|
565
|
+
entrypoints.append(f"* `{name}` → `{target}` (Poetry script)")
|
|
566
|
+
except Exception:
|
|
567
|
+
# Fallback to text search
|
|
568
|
+
try:
|
|
569
|
+
content = pyproject.read_text(encoding="utf-8")
|
|
570
|
+
if "[project.scripts]" in content or "[tool.poetry.scripts]" in content:
|
|
571
|
+
entrypoints.append("* Console scripts (see pyproject.toml)")
|
|
572
|
+
except Exception:
|
|
573
|
+
pass
|
|
574
|
+
|
|
575
|
+
return "\n".join(entrypoints) if entrypoints else "* (No entry points detected)"
|
|
576
|
+
|
|
577
|
+
def _detect_run_command(self, root: Path) -> str:
|
|
578
|
+
"""Detect how to run the application."""
|
|
579
|
+
# Check for FastAPI apps first (most specific)
|
|
580
|
+
for p in root.rglob("*.py"):
|
|
581
|
+
if should_exclude_from_analysis(p):
|
|
582
|
+
continue
|
|
583
|
+
try:
|
|
584
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
585
|
+
if "FastAPI(" in content:
|
|
586
|
+
import re
|
|
587
|
+
match = re.search(r'(\w+)\s*=\s*FastAPI\(', content)
|
|
588
|
+
if match:
|
|
589
|
+
app_var = match.group(1)
|
|
590
|
+
rel_path = str(p.relative_to(root)).replace("/", ".").replace("\\", ".").replace(".py", "")
|
|
591
|
+
return f"uvicorn {rel_path}:{app_var} --reload"
|
|
592
|
+
except Exception:
|
|
593
|
+
pass
|
|
594
|
+
|
|
595
|
+
# Check for common patterns
|
|
596
|
+
if (root / "main.py").exists():
|
|
597
|
+
return "python main.py"
|
|
598
|
+
if (root / "app.py").exists():
|
|
599
|
+
return "python app.py"
|
|
600
|
+
if (root / "__main__.py").exists():
|
|
601
|
+
return f"python -m {root.name}"
|
|
602
|
+
|
|
603
|
+
return "# Run command not auto-detected, see README.md"
|
|
604
|
+
|
|
605
|
+
def _describe_directory(self, dir_path: Path) -> str:
|
|
606
|
+
"""Provide a brief description of a directory's purpose."""
|
|
607
|
+
dir_name = dir_path.name
|
|
608
|
+
|
|
609
|
+
descriptions = {
|
|
610
|
+
"app": "Application code",
|
|
611
|
+
"src": "Source code",
|
|
612
|
+
"api": "API layer",
|
|
613
|
+
"routers": "Route handlers",
|
|
614
|
+
"models": "Data models",
|
|
615
|
+
"services": "Business logic",
|
|
616
|
+
"core": "Core functionality",
|
|
617
|
+
"lib": "Library code",
|
|
618
|
+
"utils": "Utility functions",
|
|
619
|
+
"templates": "HTML templates",
|
|
620
|
+
"static": "Static assets",
|
|
621
|
+
"tests": "Test suite",
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return descriptions.get(dir_name, "Code directory")
|
|
625
|
+
|
|
626
|
+
def _file_exists_in_tree(self, root: Path, pattern: str) -> bool:
|
|
627
|
+
"""Check if any file matching pattern exists in tree."""
|
|
628
|
+
try:
|
|
629
|
+
for _ in root.rglob(pattern):
|
|
630
|
+
return True
|
|
631
|
+
except Exception:
|
|
632
|
+
pass
|
|
633
|
+
return False
|
|
634
|
+
|
|
635
|
+
def _detect_error_handling(self, root: Path) -> str:
|
|
636
|
+
"""Detect error handling patterns."""
|
|
637
|
+
has_middleware = self._file_exists_in_tree(root, "*middleware*.py")
|
|
638
|
+
has_exception_handlers = False
|
|
639
|
+
|
|
640
|
+
for p in root.rglob("*.py"):
|
|
641
|
+
try:
|
|
642
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
643
|
+
if "@app.exception_handler" in content or "HTTPException" in content:
|
|
644
|
+
has_exception_handlers = True
|
|
645
|
+
break
|
|
646
|
+
except Exception:
|
|
647
|
+
continue
|
|
648
|
+
|
|
649
|
+
if has_middleware or has_exception_handlers:
|
|
650
|
+
return "* Centralized exception handling detected"
|
|
651
|
+
return "* (Error handling pattern not auto-detected)"
|
|
652
|
+
|
|
653
|
+
def _detect_logging(self, root: Path) -> str:
|
|
654
|
+
"""Detect logging configuration."""
|
|
655
|
+
for p in root.rglob("*.py"):
|
|
656
|
+
try:
|
|
657
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
658
|
+
if "import logging" in content or "from logging" in content:
|
|
659
|
+
return "* Python logging module in use"
|
|
660
|
+
except Exception:
|
|
661
|
+
continue
|
|
662
|
+
return "* (Logging not detected)"
|
|
663
|
+
|
|
664
|
+
def _extract_env_vars(self, root: Path) -> dict[str, list[tuple[str, int]]]:
|
|
665
|
+
"""
|
|
666
|
+
Extract environment variables used in the project with file+line references.
|
|
667
|
+
Returns dict mapping var name to list of (relative_path, line_number) tuples.
|
|
668
|
+
Filters out OS/toolchain noise.
|
|
669
|
+
"""
|
|
670
|
+
# OS/toolchain vars to filter out - these pollute AI context
|
|
671
|
+
OS_NOISE = {
|
|
672
|
+
# CI/CD platforms
|
|
673
|
+
"GITHUB_ACTIONS", "GITHUB_TOKEN", "GITHUB_WORKSPACE", "GITHUB_SHA",
|
|
674
|
+
"CI", "CONTINUOUS_INTEGRATION", "GITLAB_CI", "CIRCLECI", "TRAVIS",
|
|
675
|
+
# Testing frameworks
|
|
676
|
+
"PYTEST_CURRENT_TEST", "PYTEST_VERSION", "PYTEST_TIMEOUT",
|
|
677
|
+
# Python tooling
|
|
678
|
+
"PYTHONPATH", "PYTHONHOME", "PYTHONDONTWRITEBYTECODE", "PYTHONUNBUFFERED",
|
|
679
|
+
"VIRTUAL_ENV", "CONDA_DEFAULT_ENV", "CONDA_PREFIX",
|
|
680
|
+
# Build/compile
|
|
681
|
+
"CC", "CXX", "CFLAGS", "CXXFLAGS", "LDFLAGS", "AR", "NM", "RANLIB",
|
|
682
|
+
# Android SDK (common on dev machines)
|
|
683
|
+
"ANDROID_HOME", "ANDROID_SDK_ROOT", "ANDROID_NDK_HOME",
|
|
684
|
+
# System/OS
|
|
685
|
+
"PATH", "HOME", "USER", "SHELL", "LANG", "LC_ALL", "TZ", "TERM",
|
|
686
|
+
"PWD", "OLDPWD", "SHLVL", "EDITOR", "PAGER",
|
|
687
|
+
# SSL/Security debugging
|
|
688
|
+
"SSLKEYLOGFILE", "SSL_CERT_FILE", "SSL_CERT_DIR",
|
|
689
|
+
# Package managers
|
|
690
|
+
"PIP_INDEX_URL", "PIP_EXTRA_INDEX_URL", "NPM_TOKEN",
|
|
691
|
+
# Display/X11
|
|
692
|
+
"DISPLAY", "XAUTHORITY", "WAYLAND_DISPLAY",
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
env_vars: dict[str, list[tuple[str, int]]] = {}
|
|
696
|
+
|
|
697
|
+
# Patterns to detect env var usage
|
|
698
|
+
import re
|
|
699
|
+
patterns = [
|
|
700
|
+
re.compile(r'os\.getenv\(["\']([A-Z_]+)["\']\)'),
|
|
701
|
+
re.compile(r'os\.environ\[["\']([A-Z_]+)["\']\]'),
|
|
702
|
+
re.compile(r'os\.environ\.get\(["\']([A-Z_]+)["\']\)'),
|
|
703
|
+
]
|
|
704
|
+
|
|
705
|
+
for p in root.rglob("*.py"):
|
|
706
|
+
if should_exclude_from_analysis(p):
|
|
707
|
+
continue
|
|
708
|
+
try:
|
|
709
|
+
with open(p, "r", encoding="utf-8", errors="ignore") as f:
|
|
710
|
+
for line_num, line in enumerate(f, start=1):
|
|
711
|
+
for pattern in patterns:
|
|
712
|
+
for match in pattern.finditer(line):
|
|
713
|
+
var_name = match.group(1)
|
|
714
|
+
# Filter OS/toolchain noise
|
|
715
|
+
if var_name not in OS_NOISE:
|
|
716
|
+
rel_path = str(p.relative_to(root))
|
|
717
|
+
if var_name not in env_vars:
|
|
718
|
+
env_vars[var_name] = []
|
|
719
|
+
env_vars[var_name].append((rel_path, line_num))
|
|
720
|
+
except Exception:
|
|
721
|
+
continue
|
|
722
|
+
|
|
723
|
+
return env_vars
|
|
724
|
+
|
|
725
|
+
def _detect_database(self, root: Path) -> str | None:
|
|
726
|
+
"""Detect database type."""
|
|
727
|
+
for p in root.rglob("*.py"):
|
|
728
|
+
try:
|
|
729
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
730
|
+
if "postgresql" in content.lower() or "psycopg" in content:
|
|
731
|
+
return "PostgreSQL"
|
|
732
|
+
if "mysql" in content.lower() or "pymysql" in content:
|
|
733
|
+
return "MySQL"
|
|
734
|
+
if "sqlite" in content.lower():
|
|
735
|
+
return "SQLite"
|
|
736
|
+
if "mongodb" in content.lower() or "pymongo" in content:
|
|
737
|
+
return "MongoDB"
|
|
738
|
+
except Exception:
|
|
739
|
+
continue
|
|
740
|
+
return None
|
|
741
|
+
|
|
742
|
+
def _detect_orm(self, root: Path) -> str | None:
|
|
743
|
+
"""Detect ORM/data layer."""
|
|
744
|
+
for p in root.rglob("*.py"):
|
|
745
|
+
try:
|
|
746
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
747
|
+
if "from sqlalchemy" in content or "import sqlalchemy" in content:
|
|
748
|
+
return "SQLAlchemy"
|
|
749
|
+
if "from django.db" in content:
|
|
750
|
+
return "Django ORM"
|
|
751
|
+
if "from tortoise" in content:
|
|
752
|
+
return "Tortoise ORM"
|
|
753
|
+
except Exception:
|
|
754
|
+
continue
|
|
755
|
+
return None
|
|
756
|
+
|
|
757
|
+
def _detect_migrations(self, root: Path) -> str:
|
|
758
|
+
"""Detect migration system."""
|
|
759
|
+
if (root / "migrations").is_dir() or (root / "alembic").is_dir():
|
|
760
|
+
return "* Migration system detected (likely Alembic)"
|
|
761
|
+
return "* (No migrations detected)"
|
|
762
|
+
|
|
763
|
+
def _extract_routes(self, root: Path) -> list[str]:
|
|
764
|
+
"""Extract API routes from code."""
|
|
765
|
+
routes = []
|
|
766
|
+
|
|
767
|
+
for p in root.rglob("*.py"):
|
|
768
|
+
if any(x in p.parts for x in [".venv", "venv", "__pycache__", "artifacts"]):
|
|
769
|
+
continue
|
|
770
|
+
try:
|
|
771
|
+
content = p.read_text(encoding="utf-8", errors="ignore")
|
|
772
|
+
import re
|
|
773
|
+
# FastAPI patterns
|
|
774
|
+
patterns = [
|
|
775
|
+
r'@\w+\.(get|post|put|delete|patch)\(["\']([^"\']+)["\']\)',
|
|
776
|
+
r'@app\.route\(["\']([^"\']+)["\']\)',
|
|
777
|
+
]
|
|
778
|
+
for pattern in patterns:
|
|
779
|
+
matches = re.findall(pattern, content)
|
|
780
|
+
for match in matches:
|
|
781
|
+
if isinstance(match, tuple):
|
|
782
|
+
if len(match) == 2:
|
|
783
|
+
routes.append(f"{match[0].upper()} {match[1]}")
|
|
784
|
+
else:
|
|
785
|
+
routes.append(f"GET {match[0]}")
|
|
786
|
+
else:
|
|
787
|
+
routes.append(f"GET {match}")
|
|
788
|
+
except Exception:
|
|
789
|
+
continue
|
|
790
|
+
|
|
791
|
+
return sorted(set(routes))
|