debtscanner 0.2.0__tar.gz

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.
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: debtscanner
3
+ Version: 0.2.0
4
+ Summary: AI-powered technical debt scanner for codebases
5
+ Requires-Dist: click
6
+ Requires-Dist: openai
7
+ Requires-Dist: tqdm
8
+ Requires-Dist: colorama
9
+ Requires-Dist: rich
10
+ Requires-Dist: python-dotenv
11
+ Provides-Extra: watch
12
+ Requires-Dist: watchdog; extra == "watch"
13
+ Dynamic: provides-extra
14
+ Dynamic: requires-dist
15
+ Dynamic: summary
@@ -0,0 +1,44 @@
1
+ # debtscanner
2
+
3
+ `debtscanner` is a Python CLI that scans codebases for technical debt using AI and produces prioritized results in terminal and HTML.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install -e .
9
+ ```
10
+
11
+ ## Configure
12
+
13
+ Create a `.env` file in the project root:
14
+
15
+ ```env
16
+ GITHUB_TOKEN=ghp_your_token_here
17
+ ```
18
+
19
+ Or export `GITHUB_TOKEN` in your shell.
20
+
21
+ ## Commands
22
+
23
+ ```bash
24
+ debtscanner scan .
25
+ debtscanner scan src/ --output report.html
26
+ debtscanner report
27
+ debtscanner fix auth.py
28
+ ```
29
+
30
+ ## Options
31
+
32
+ - `--ignore`: skip specific folders (repeatable or comma-separated)
33
+ - `--severity`: minimum level to include (`LOW`, `MEDIUM`, `HIGH`, `CRITICAL`)
34
+
35
+ ## Supported languages
36
+
37
+ - `.py`, `.js`, `.ts`, `.jsx`, `.tsx`, `.java`, `.go`, `.rb`
38
+
39
+ ## Behavior
40
+
41
+ - Skips: `node_modules`, `.git`, `__pycache__`, `dist`, `build`, `.env`
42
+ - Skips files larger than 100KB with warning
43
+ - Skips binary files automatically
44
+ - Retries once on API rate limits with a 2-second pause
@@ -0,0 +1,2 @@
1
+ __all__ = ["__version__"]
2
+ __version__ = "0.2.0"
@@ -0,0 +1,109 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+ from typing import Any
6
+
7
+ from dotenv import load_dotenv
8
+ from openai import OpenAI
9
+
10
+ from .prompts import FIX_PROMPT, SCAN_PROMPT
11
+
12
+ PROVIDERS: dict[str, dict[str, Any]] = {
13
+ "github": {
14
+ "label": "GitHub Models (free with GitHub token)",
15
+ "base_url": "https://models.inference.ai.azure.com",
16
+ "default_model": "gpt-4o-mini",
17
+ "env_key": "GITHUB_TOKEN",
18
+ "env_hint": "ghp_xxxxxxxxxxxxxxxxxxxx",
19
+ },
20
+ "openai": {
21
+ "label": "OpenAI (requires API key)",
22
+ "base_url": "https://api.openai.com/v1",
23
+ "default_model": "gpt-4o-mini",
24
+ "env_key": "OPENAI_API_KEY",
25
+ "env_hint": "sk-xxxxxxxxxxxxxxxxxxxx",
26
+ },
27
+ "anthropic": {
28
+ "label": "Anthropic Claude (via OpenAI-compat proxy)",
29
+ "base_url": "https://api.anthropic.com/v1",
30
+ "default_model": "claude-sonnet-4-20250514",
31
+ "env_key": "ANTHROPIC_API_KEY",
32
+ "env_hint": "sk-ant-xxxxxxxxxxxxxxxxxxxx",
33
+ },
34
+ "ollama": {
35
+ "label": "Ollama (local, no key needed)",
36
+ "base_url": "http://localhost:11434/v1",
37
+ "default_model": "llama3",
38
+ "env_key": "",
39
+ "env_hint": "",
40
+ },
41
+ }
42
+
43
+
44
+ def _resolve_provider() -> tuple[str, str, str]:
45
+ load_dotenv()
46
+
47
+ provider_name = os.getenv("DEBTSCANNER_PROVIDER", "github").lower()
48
+ info = PROVIDERS.get(provider_name)
49
+
50
+ if info is None:
51
+ raise RuntimeError(
52
+ f"Unknown provider '{provider_name}'. "
53
+ f"Valid choices: {', '.join(PROVIDERS)}"
54
+ )
55
+
56
+ model = os.getenv("DEBTSCANNER_MODEL", info["default_model"])
57
+ base_url = os.getenv("DEBTSCANNER_BASE_URL", info["base_url"])
58
+
59
+ if provider_name == "ollama":
60
+ return base_url, "ollama", model
61
+
62
+ env_key = info["env_key"]
63
+ token = os.getenv(env_key, "")
64
+ if not token:
65
+ raise RuntimeError(
66
+ f"Missing {env_key} for provider '{provider_name}'. "
67
+ f"Run `debtscanner init` or add it to your .env file."
68
+ )
69
+
70
+ return base_url, token, model
71
+
72
+
73
+ class AIClient:
74
+ def __init__(self) -> None:
75
+ base_url, api_key, model = _resolve_provider()
76
+ self.model = model
77
+ self.client = OpenAI(base_url=base_url, api_key=api_key)
78
+
79
+ def _create_completion(self, user_content: str) -> str:
80
+ for attempt in range(2):
81
+ try:
82
+ response = self.client.chat.completions.create(
83
+ model=self.model,
84
+ messages=[
85
+ {
86
+ "role": "user",
87
+ "content": user_content,
88
+ }
89
+ ],
90
+ temperature=0.2,
91
+ )
92
+ return (response.choices[0].message.content or "").strip()
93
+ except Exception as exc:
94
+ error_text = str(exc).lower()
95
+ should_retry = "429" in error_text or "rate limit" in error_text
96
+ if should_retry and attempt == 0:
97
+ time.sleep(2)
98
+ continue
99
+ raise
100
+
101
+ return ""
102
+
103
+ def analyze_code(self, filename: str, content: str) -> str:
104
+ prompt = f"{SCAN_PROMPT}\n\nFILE UNDER REVIEW: {filename}\n\nCODE:\n{content}"
105
+ return self._create_completion(prompt)
106
+
107
+ def suggest_fixes(self, filename: str, content: str) -> str:
108
+ prompt = f"{FIX_PROMPT}\n\nFILE UNDER REVIEW: {filename}\n\nCODE:\n{content}"
109
+ return self._create_completion(prompt)
@@ -0,0 +1,516 @@
1
+
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import sys
6
+ import time
7
+ from pathlib import Path
8
+ from typing import Sequence
9
+
10
+ import click
11
+ from colorama import Fore, Style
12
+ from colorama import init as colorama_init
13
+ from rich.console import Console
14
+ from tqdm import tqdm
15
+
16
+ from .ai import AIClient, PROVIDERS
17
+ from .reporter import (
18
+ render_file_scores_table,
19
+ render_json,
20
+ render_markdown,
21
+ render_terminal_table,
22
+ summarize,
23
+ summary_line,
24
+ write_html_report,
25
+ )
26
+ from .scanner import (
27
+ MAX_FILE_SIZE_BYTES,
28
+ filter_min_severity,
29
+ parse_ai_scan_response,
30
+ scan_files,
31
+ sort_debt_items,
32
+ top_critical_files,
33
+ )
34
+
35
+ colorama_init(autoreset=True)
36
+ console = Console()
37
+
38
+ SEVERITY_CHOICES = ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
39
+ FORMAT_CHOICES = ["table", "json", "markdown"]
40
+
41
+
42
+ def _read_file_for_fix(file_path: str) -> str:
43
+ if os.path.getsize(file_path) > MAX_FILE_SIZE_BYTES:
44
+ raise click.ClickException("File too large (>100KB). Please scan a smaller file.")
45
+
46
+ with open(file_path, "rb") as binary_file:
47
+ chunk = binary_file.read(2048)
48
+ if b"\x00" in chunk:
49
+ raise click.ClickException("Binary files are not supported for fix suggestions.")
50
+
51
+ try:
52
+ with open(file_path, "r", encoding="utf-8", errors="ignore") as source_file:
53
+ return source_file.read()
54
+ except OSError as exc:
55
+ raise click.ClickException(f"Unable to read file: {exc}") from exc
56
+
57
+
58
+ def _run_scan(
59
+ target: str,
60
+ ignore: Sequence[str],
61
+ min_severity: str,
62
+ output: str | None = None,
63
+ fmt: str = "table",
64
+ ) -> int:
65
+ target_path = Path(target).resolve()
66
+ if not target_path.exists() or not target_path.is_dir():
67
+ raise click.ClickException(f"Directory not found: {target}")
68
+
69
+ files, warnings = scan_files(str(target_path), ignore_dirs=ignore)
70
+
71
+ for warning in warnings:
72
+ click.echo(f"{Fore.YELLOW}Warning:{Style.RESET_ALL} {warning}")
73
+
74
+ if not files:
75
+ raise click.ClickException(
76
+ "No supported source files were found. Try a different path or adjust --ignore."
77
+ )
78
+
79
+ try:
80
+ ai_client = AIClient()
81
+ except RuntimeError as exc:
82
+ raise click.ClickException(str(exc)) from exc
83
+
84
+ all_items = []
85
+ for source_file in tqdm(files, desc="Scanning files", unit="file"):
86
+ relative_name = os.path.relpath(source_file.path, str(target_path))
87
+ response = ai_client.analyze_code(relative_name, source_file.content)
88
+ all_items.extend(parse_ai_scan_response(response, relative_name))
89
+
90
+ filtered = sort_debt_items(filter_min_severity(all_items, min_severity))
91
+
92
+ if fmt == "json":
93
+ click.echo(render_json(filtered, total_files=len(files)))
94
+ elif fmt == "markdown":
95
+ click.echo(render_markdown(filtered, total_files=len(files)))
96
+ else:
97
+ top5 = top_critical_files(filtered)
98
+ if top5:
99
+ render_file_scores_table(top5)
100
+ console.print()
101
+
102
+ if filtered:
103
+ render_terminal_table(filtered)
104
+ else:
105
+ console.print(
106
+ "[yellow]No debt items found for the selected severity threshold.[/yellow]"
107
+ )
108
+
109
+ console.print(summary_line(len(files), filtered))
110
+ console.print(summarize(filtered))
111
+
112
+ if output:
113
+ report_path = write_html_report(filtered, output, total_files=len(files))
114
+ click.echo(f"Saved HTML report to {report_path}")
115
+
116
+ return len(filtered)
117
+
118
+
119
+ @click.group()
120
+ def main() -> None:
121
+ pass
122
+
123
+
124
+ @main.command()
125
+ def init() -> None:
126
+ click.echo(f"\n{Fore.CYAN}Welcome to debtscanner setup!{Style.RESET_ALL}\n")
127
+
128
+ click.echo("Choose an AI provider:\n")
129
+ provider_keys = list(PROVIDERS.keys())
130
+ for idx, key in enumerate(provider_keys, 1):
131
+ info = PROVIDERS[key]
132
+ click.echo(f" {Fore.GREEN}{idx}{Style.RESET_ALL}) {info['label']}")
133
+
134
+ click.echo()
135
+ choice = click.prompt(
136
+ "Enter provider number",
137
+ type=click.IntRange(1, len(provider_keys)),
138
+ default=1,
139
+ )
140
+ provider_name = provider_keys[choice - 1]
141
+ provider_info = PROVIDERS[provider_name]
142
+
143
+ env_key = provider_info["env_key"]
144
+ token = ""
145
+ if env_key:
146
+ from dotenv import dotenv_values
147
+
148
+ env_path = Path.cwd() / ".env"
149
+ env_example_path = Path.cwd() / ".env.example"
150
+
151
+ existing_env: dict[str, str | None] = {}
152
+ if env_path.exists():
153
+ existing_env = dotenv_values(env_path)
154
+ elif env_example_path.exists():
155
+ existing_env = dotenv_values(env_example_path)
156
+
157
+ existing_token = (
158
+ os.getenv(env_key, "")
159
+ or (existing_env.get(env_key) or "")
160
+ )
161
+
162
+ hint = provider_info["env_hint"]
163
+ is_placeholder = (
164
+ not existing_token
165
+ or existing_token == hint
166
+ or existing_token.startswith("ghp_your_")
167
+ or "xxxx" in existing_token
168
+ )
169
+
170
+ if existing_token and not is_placeholder:
171
+ masked = existing_token[:6] + "..." + existing_token[-4:]
172
+ click.echo(
173
+ f"\n{Fore.GREEN}Found existing {env_key}{Style.RESET_ALL} ({masked})"
174
+ )
175
+ keep = click.confirm("Keep this token?", default=True)
176
+ if keep:
177
+ token = existing_token
178
+ else:
179
+ click.echo(f"\nPaste your new {env_key} (input is visible):")
180
+ token = click.prompt(
181
+ f"{env_key}",
182
+ default="",
183
+ show_default=False,
184
+ ).strip()
185
+ if not token:
186
+ click.echo(
187
+ f"{Fore.YELLOW}No token provided - keeping the existing one.{Style.RESET_ALL}"
188
+ )
189
+ token = existing_token
190
+ else:
191
+ click.echo(
192
+ f"\n{provider_info['label']} requires the environment variable "
193
+ f"{Fore.YELLOW}{env_key}{Style.RESET_ALL}."
194
+ )
195
+ click.echo("Paste your token below (input is visible):")
196
+ token = click.prompt(
197
+ f"{env_key}",
198
+ default="",
199
+ show_default=False,
200
+ ).strip()
201
+ if not token:
202
+ click.echo(
203
+ f"{Fore.YELLOW}No token provided - you can add it later "
204
+ f"by editing .env{Style.RESET_ALL}"
205
+ )
206
+ token = hint
207
+ else:
208
+ click.echo(f"\n{provider_info['label']} - no API key needed.")
209
+
210
+ default_model = provider_info["default_model"]
211
+ click.echo(f"\nDefault model: {Fore.CYAN}{default_model}{Style.RESET_ALL}")
212
+ change_model = click.confirm("Use a different model?", default=False)
213
+ if change_model:
214
+ model = click.prompt("Enter model name").strip()
215
+ if not model:
216
+ model = default_model
217
+ else:
218
+ model = default_model
219
+
220
+ env_lines = [
221
+ f"# debtscanner configuration (generated by `debtscanner init`)",
222
+ f"DEBTSCANNER_PROVIDER={provider_name}",
223
+ ]
224
+ if env_key and token:
225
+ env_lines.append(f"{env_key}={token}")
226
+ if model != default_model:
227
+ env_lines.append(f"DEBTSCANNER_MODEL={model}")
228
+
229
+ env_path = Path.cwd() / ".env"
230
+ if env_path.exists():
231
+ overwrite = click.confirm(
232
+ f"\n{Fore.YELLOW}.env already exists. Overwrite?{Style.RESET_ALL}",
233
+ default=False,
234
+ )
235
+ if not overwrite:
236
+ click.echo("Aborted - .env was not modified.")
237
+ return
238
+
239
+ env_path.write_text("\n".join(env_lines) + "\n", encoding="utf-8")
240
+ click.echo(f"\n{Fore.GREEN}Created .env{Style.RESET_ALL} with provider={provider_name}")
241
+ click.echo("Run `debtscanner scan .` to start scanning!\n")
242
+
243
+
244
+ @main.command()
245
+ @click.argument("target", default=".")
246
+ @click.option("--output", type=click.Path(dir_okay=False), default=None, help="Write HTML report")
247
+ @click.option(
248
+ "--ignore",
249
+ multiple=True,
250
+ help="Folder(s) to ignore. Can be repeated or comma-separated.",
251
+ )
252
+ @click.option(
253
+ "--severity",
254
+ "min_severity",
255
+ type=click.Choice(SEVERITY_CHOICES, case_sensitive=False),
256
+ default="LOW",
257
+ show_default=True,
258
+ help="Minimum severity level to include.",
259
+ )
260
+ @click.option(
261
+ "--format",
262
+ "fmt",
263
+ type=click.Choice(FORMAT_CHOICES, case_sensitive=False),
264
+ default="table",
265
+ show_default=True,
266
+ help="Output format: table, json, or markdown.",
267
+ )
268
+ @click.option(
269
+ "--watch",
270
+ is_flag=True,
271
+ default=False,
272
+ help="Re-scan automatically when source files change.",
273
+ )
274
+ def scan(
275
+ target: str,
276
+ output: str | None,
277
+ ignore: Sequence[str],
278
+ min_severity: str,
279
+ fmt: str,
280
+ watch: bool,
281
+ ) -> None:
282
+ if watch:
283
+ _watch_loop(target=target, ignore=ignore, min_severity=min_severity, output=output, fmt=fmt)
284
+ else:
285
+ _run_scan(
286
+ target=target,
287
+ ignore=ignore,
288
+ min_severity=min_severity.upper(),
289
+ output=output,
290
+ fmt=fmt,
291
+ )
292
+
293
+
294
+ @main.command()
295
+ @click.argument("target", default=".")
296
+ @click.option(
297
+ "--output",
298
+ type=click.Path(dir_okay=False),
299
+ default="report.html",
300
+ show_default=True,
301
+ help="Output path for the HTML report.",
302
+ )
303
+ @click.option(
304
+ "--ignore",
305
+ multiple=True,
306
+ help="Folder(s) to ignore. Can be repeated or comma-separated.",
307
+ )
308
+ @click.option(
309
+ "--severity",
310
+ "min_severity",
311
+ type=click.Choice(SEVERITY_CHOICES, case_sensitive=False),
312
+ default="LOW",
313
+ show_default=True,
314
+ help="Minimum severity level to include.",
315
+ )
316
+ @click.option(
317
+ "--format",
318
+ "fmt",
319
+ type=click.Choice(FORMAT_CHOICES, case_sensitive=False),
320
+ default="table",
321
+ show_default=True,
322
+ help="Output format: table, json, or markdown.",
323
+ )
324
+ @click.option(
325
+ "--watch",
326
+ is_flag=True,
327
+ default=False,
328
+ help="Re-scan automatically when source files change.",
329
+ )
330
+ def report(
331
+ target: str,
332
+ output: str,
333
+ ignore: Sequence[str],
334
+ min_severity: str,
335
+ fmt: str,
336
+ watch: bool,
337
+ ) -> None:
338
+ if watch:
339
+ _watch_loop(target=target, ignore=ignore, min_severity=min_severity, output=output, fmt=fmt)
340
+ else:
341
+ _run_scan(
342
+ target=target,
343
+ ignore=ignore,
344
+ min_severity=min_severity.upper(),
345
+ output=output,
346
+ fmt=fmt,
347
+ )
348
+
349
+
350
+ @main.command()
351
+ @click.argument("filepath", type=click.Path(exists=True, dir_okay=False))
352
+ def fix(filepath: str) -> None:
353
+ try:
354
+ code = _read_file_for_fix(filepath)
355
+ except click.ClickException:
356
+ raise
357
+ except OSError as exc:
358
+ raise click.ClickException(f"Unable to access file: {exc}") from exc
359
+
360
+ try:
361
+ ai_client = AIClient()
362
+ except RuntimeError as exc:
363
+ raise click.ClickException(str(exc)) from exc
364
+
365
+ filename = os.path.basename(filepath)
366
+ response = ai_client.suggest_fixes(filename, code)
367
+
368
+ if not response.strip():
369
+ console.print("[yellow]AI returned no fix suggestions.[/yellow]")
370
+ return
371
+
372
+ console.rule(f"Fix Suggestions: {filename}")
373
+ console.print(response)
374
+
375
+
376
+ def _watch_loop(
377
+ *,
378
+ target: str,
379
+ ignore: Sequence[str],
380
+ min_severity: str,
381
+ output: str | None,
382
+ fmt: str,
383
+ ) -> None:
384
+ target_path = Path(target).resolve()
385
+ if not target_path.exists() or not target_path.is_dir():
386
+ raise click.ClickException(f"Directory not found: {target}")
387
+
388
+ click.echo(
389
+ f"{Fore.CYAN}Watching {target_path} for changes "
390
+ f"(Ctrl+C to stop)...{Style.RESET_ALL}\n"
391
+ )
392
+
393
+ _run_scan(
394
+ target=target,
395
+ ignore=ignore,
396
+ min_severity=min_severity.upper(),
397
+ output=output,
398
+ fmt=fmt,
399
+ )
400
+
401
+ try:
402
+ _watch_with_watchdog(target_path, ignore, min_severity, output, fmt)
403
+ except ImportError:
404
+ _watch_with_polling(target_path, ignore, min_severity, output, fmt)
405
+
406
+
407
+ def _watch_with_watchdog(
408
+ target_path: Path,
409
+ ignore: Sequence[str],
410
+ min_severity: str,
411
+ output: str | None,
412
+ fmt: str,
413
+ ) -> None:
414
+ from watchdog.observers import Observer
415
+ from watchdog.events import FileSystemEventHandler
416
+ import threading
417
+
418
+ from .scanner import SUPPORTED_EXTENSIONS
419
+
420
+ debounce_timer: threading.Timer | None = None
421
+ lock = threading.Lock()
422
+
423
+ def _do_rescan() -> None:
424
+ click.echo(f"\n{Fore.CYAN}Change detected - rescanning...{Style.RESET_ALL}\n")
425
+ try:
426
+ _run_scan(
427
+ target=str(target_path),
428
+ ignore=ignore,
429
+ min_severity=min_severity.upper(),
430
+ output=output,
431
+ fmt=fmt,
432
+ )
433
+ except click.ClickException as exc:
434
+ click.echo(f"{Fore.RED}Error: {exc.message}{Style.RESET_ALL}")
435
+
436
+ class _Handler(FileSystemEventHandler):
437
+ def on_any_event(self, event) -> None:
438
+ if event.is_directory:
439
+ return
440
+ src = getattr(event, "src_path", "")
441
+ ext = os.path.splitext(src)[1].lower()
442
+ if ext not in SUPPORTED_EXTENSIONS:
443
+ return
444
+
445
+ nonlocal debounce_timer
446
+ with lock:
447
+ if debounce_timer is not None:
448
+ debounce_timer.cancel()
449
+ debounce_timer = threading.Timer(1.5, _do_rescan)
450
+ debounce_timer.daemon = True
451
+ debounce_timer.start()
452
+
453
+ observer = Observer()
454
+ observer.schedule(_Handler(), str(target_path), recursive=True)
455
+ observer.start()
456
+
457
+ try:
458
+ while True:
459
+ time.sleep(1)
460
+ except KeyboardInterrupt:
461
+ click.echo(f"\n{Fore.YELLOW}Stopped watching.{Style.RESET_ALL}")
462
+ finally:
463
+ observer.stop()
464
+ observer.join()
465
+
466
+
467
+ def _watch_with_polling(
468
+ target_path: Path,
469
+ ignore: Sequence[str],
470
+ min_severity: str,
471
+ output: str | None,
472
+ fmt: str,
473
+ ) -> None:
474
+ from .scanner import iter_source_files
475
+
476
+ click.echo(
477
+ f"{Fore.YELLOW}(watchdog not installed - using polling, "
478
+ f"install watchdog for better performance){Style.RESET_ALL}\n"
479
+ )
480
+
481
+ def _snapshot() -> dict[str, float]:
482
+ result: dict[str, float] = {}
483
+ for fp in iter_source_files(str(target_path), ignore_dirs=ignore):
484
+ try:
485
+ result[fp] = os.path.getmtime(fp)
486
+ except OSError:
487
+ pass
488
+ return result
489
+
490
+ prev = _snapshot()
491
+
492
+ try:
493
+ while True:
494
+ time.sleep(3)
495
+ current = _snapshot()
496
+ if current != prev:
497
+ prev = current
498
+ click.echo(
499
+ f"\n{Fore.CYAN}Change detected - rescanning...{Style.RESET_ALL}\n"
500
+ )
501
+ try:
502
+ _run_scan(
503
+ target=str(target_path),
504
+ ignore=ignore,
505
+ min_severity=min_severity.upper(),
506
+ output=output,
507
+ fmt=fmt,
508
+ )
509
+ except click.ClickException as exc:
510
+ click.echo(f"{Fore.RED}Error: {exc.message}{Style.RESET_ALL}")
511
+ except KeyboardInterrupt:
512
+ click.echo(f"\n{Fore.YELLOW}Stopped watching.{Style.RESET_ALL}")
513
+
514
+
515
+ if __name__ == "__main__":
516
+ main()
@@ -0,0 +1,17 @@
1
+ SCAN_PROMPT = """You are a senior code reviewer. Analyze this code for technical
2
+ debt. For each issue found, output EXACTLY in this format:
3
+ SEVERITY: [CRITICAL/HIGH/MEDIUM/LOW]
4
+ FILE: [filename]
5
+ LINE: [line number]
6
+ ISSUE: [one line description]
7
+ FIX: [one line suggested fix]
8
+ ---
9
+ Be specific. Only report real issues. Maximum 10 issues per file."""
10
+
11
+ FIX_PROMPT = """You are a senior developer. Given this code, show me exactly
12
+ how to fix the most critical technical debt issues.
13
+ For each fix show:
14
+ BEFORE: [the problematic code]
15
+ AFTER: [the fixed code]
16
+ WHY: [one line explanation]
17
+ Maximum 3 fixes. Be specific and actionable."""