kernhell 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kernhell might be problematic. Click here for more details.

Files changed (42) hide show
  1. kernhell/__init__.py +8 -0
  2. kernhell/__main__.py +6 -0
  3. kernhell/analyzer/analyzer.py +178 -0
  4. kernhell/analyzer/healer.py +249 -0
  5. kernhell/analyzer/prompts.py +89 -0
  6. kernhell/api/__init__.py +5 -0
  7. kernhell/api/server.py +367 -0
  8. kernhell/cache/__init__.py +13 -0
  9. kernhell/cache/cache_manager.py +221 -0
  10. kernhell/cache/embedder.py +104 -0
  11. kernhell/cache/schemas.py +42 -0
  12. kernhell/cli/__init__.py +1 -0
  13. kernhell/cli/app.py +62 -0
  14. kernhell/cli/commands/__init__.py +1 -0
  15. kernhell/cli/commands/cache.py +96 -0
  16. kernhell/cli/commands/config.py +111 -0
  17. kernhell/cli/commands/dashboard.py +55 -0
  18. kernhell/cli/commands/heal.py +141 -0
  19. kernhell/cli/commands/scan.py +87 -0
  20. kernhell/cli/ui.py +176 -0
  21. kernhell/core/__init__.py +1 -0
  22. kernhell/core/config.py +219 -0
  23. kernhell/core/exceptions.py +104 -0
  24. kernhell/core/models.py +174 -0
  25. kernhell/core/orchestrator.py +245 -0
  26. kernhell/patcher/patcher.py +215 -0
  27. kernhell/patcher/verifier.py +82 -0
  28. kernhell/pruner/__init__.py +1 -0
  29. kernhell/pruner/dom_pruner.py +240 -0
  30. kernhell/reporter/__init__.py +5 -0
  31. kernhell/reporter/reporter.py +124 -0
  32. kernhell/scanner/__init__.py +1 -0
  33. kernhell/scanner/parser.py +276 -0
  34. kernhell/scanner/runner.py +242 -0
  35. kernhell/utils/__init__.py +1 -0
  36. kernhell/utils/file_utils.py +117 -0
  37. kernhell/utils/logger.py +78 -0
  38. kernhell-0.1.0.dist-info/METADATA +245 -0
  39. kernhell-0.1.0.dist-info/RECORD +42 -0
  40. kernhell-0.1.0.dist-info/WHEEL +4 -0
  41. kernhell-0.1.0.dist-info/entry_points.txt +2 -0
  42. kernhell-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,42 @@
1
+ """Schemas for the Semantic Vector Cache."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+ from kernhell.core.models import SelectorStrategy
12
+
13
+
14
+ class CacheEntry(BaseModel):
15
+ """A verified selector healing record stored in semantic memory."""
16
+
17
+ id: str # Hash of selector + context
18
+ broken_selector: str
19
+ new_selector: str
20
+ strategy: SelectorStrategy = SelectorStrategy.CSS_FALLBACK
21
+ confidence: float = Field(ge=0.0, le=1.0)
22
+ context_snippet: str = ""
23
+ page_url: str = ""
24
+ test_file: str = ""
25
+ verified: bool = True
26
+ created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
27
+ hit_count: int = 0
28
+
29
+
30
+ class CacheStats(BaseModel):
31
+ """Aggregate statistics for the semantic vector cache."""
32
+
33
+ total_entries: int = 0
34
+ hits: int = 0
35
+ misses: int = 0
36
+
37
+ @property
38
+ def hit_ratio(self) -> float:
39
+ total = self.hits + self.misses
40
+ if total == 0:
41
+ return 0.0
42
+ return round((self.hits / total) * 100, 1)
@@ -0,0 +1 @@
1
+ """CLI package — Typer commands and Rich UI helpers."""
kernhell/cli/app.py ADDED
@@ -0,0 +1,62 @@
1
+ """Main CLI application entrypoint for KernHell."""
2
+
3
+ import sys
4
+
5
+ if sys.platform == "win32":
6
+ if hasattr(sys.stdout, "reconfigure"):
7
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
8
+ if hasattr(sys.stderr, "reconfigure"):
9
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
10
+
11
+ import typer
12
+ from rich.console import Console
13
+
14
+ from kernhell import __app_name__, __version__
15
+ from kernhell.cli.commands.cache import cache_app
16
+ from kernhell.cli.commands.config import config_app
17
+ from kernhell.cli.commands.dashboard import dashboard_command
18
+ from kernhell.cli.commands.heal import heal_command
19
+ from kernhell.cli.commands.scan import scan_command
20
+
21
+ console = Console(stderr=True)
22
+
23
+ app = typer.Typer(
24
+ name=__app_name__,
25
+ help="KernHell — AI-Powered Autonomous Self-Healing Testing Engine.\n\n"
26
+ "'The Heart of Your System Where Innovation Meets Precision'",
27
+ add_completion=False,
28
+ no_args_is_help=True,
29
+ )
30
+
31
+ # Register core commands
32
+ app.command(name="scan", help="Scan a test file to detect broken selectors and capture DOM snapshots.")(scan_command)
33
+ app.command(name="heal", help="Autonomously heal broken Playwright selectors in a test file.")(heal_command)
34
+ app.command(name="dashboard", help="Launch the KernHell Mission Control Dashboard and REST API.")(dashboard_command)
35
+ app.command(name="ui", help="Alias for 'dashboard' command.")(dashboard_command)
36
+ app.add_typer(cache_app, name="cache")
37
+ app.add_typer(config_app, name="config")
38
+
39
+
40
+ def version_callback(value: bool) -> None:
41
+ if value:
42
+ console.print(f"[bold red]KernHell[/bold red] version [cyan]{__version__}[/cyan]")
43
+ raise typer.Exit()
44
+
45
+
46
+ @app.callback()
47
+ def main(
48
+ version: bool = typer.Option(
49
+ False,
50
+ "--version",
51
+ "-V",
52
+ help="Show KernHell version and exit.",
53
+ callback=version_callback,
54
+ is_eager=True,
55
+ ),
56
+ ) -> None:
57
+ """KernHell CLI entrypoint callback."""
58
+ pass
59
+
60
+
61
+ if __name__ == "__main__":
62
+ app()
@@ -0,0 +1 @@
1
+ """CLI commands package."""
@@ -0,0 +1,96 @@
1
+ """CLI commands for managing the Semantic Vector Cache: `kernhell cache <subcommand>`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Optional
6
+ from pathlib import Path
7
+
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.table import Table
11
+ from rich import box
12
+
13
+ from kernhell.cache.cache_manager import CacheManager
14
+ from kernhell.core.config import load_config
15
+ from kernhell.utils.logger import setup_logging
16
+
17
+ console = Console(stderr=True)
18
+
19
+ cache_app = typer.Typer(
20
+ name="cache",
21
+ help="Inspect, query, and manage KernHell's Semantic Vector Cache.",
22
+ no_args_is_help=True,
23
+ )
24
+
25
+
26
+ @cache_app.command(name="list", help="List all verified healed selector mappings in cache.")
27
+ def list_cache(
28
+ config_path: Optional[Path] = typer.Option(None, "--config", "-c"),
29
+ ) -> None:
30
+ cfg = load_config(config_path)
31
+ setup_logging(level=cfg.logging.level)
32
+ mgr = CacheManager(cfg)
33
+
34
+ entries = mgr.list_entries()
35
+ if not entries:
36
+ console.print("[yellow]Semantic Cache is currently empty. Run `kernhell heal` to populate.[/yellow]")
37
+ return
38
+
39
+ table = Table(title="🧠 KernHell Semantic Memory Cache", box=box.ROUNDED)
40
+ table.add_column("Original Selector", style="red")
41
+ table.add_column("Healed Selector", style="bold green")
42
+ table.add_column("Strategy", style="cyan")
43
+ table.add_column("Confidence", justify="right")
44
+ table.add_column("Hits", justify="right")
45
+
46
+ for e in entries:
47
+ table.add_row(
48
+ e.broken_selector,
49
+ e.new_selector,
50
+ e.strategy.value,
51
+ f"{int(e.confidence * 100)}%",
52
+ str(e.hit_count),
53
+ )
54
+
55
+ console.print(table)
56
+
57
+
58
+ @cache_app.command(name="stats", help="Show cache hit/miss metrics and efficiency ratio.")
59
+ def stats_cache(
60
+ config_path: Optional[Path] = typer.Option(None, "--config", "-c"),
61
+ ) -> None:
62
+ cfg = load_config(config_path)
63
+ setup_logging(level=cfg.logging.level)
64
+ mgr = CacheManager(cfg)
65
+
66
+ stats = mgr.get_stats()
67
+ table = Table(title="📊 Semantic Memory Metrics", box=box.ROUNDED)
68
+ table.add_column("Metric", style="bold")
69
+ table.add_column("Value", justify="right")
70
+
71
+ table.add_row("Total Entries", str(stats.total_entries))
72
+ table.add_row("Cache Hits", f"[bold green]{stats.hits}[/bold green]")
73
+ table.add_row("Cache Misses", str(stats.misses))
74
+ table.add_row("Hit Efficiency", f"[bold cyan]{stats.hit_ratio}%[/bold cyan]")
75
+ table.add_row("Storage Path", str(mgr.storage_dir))
76
+
77
+ console.print(table)
78
+
79
+
80
+ @cache_app.command(name="clear", help="Purge all entries from the semantic cache.")
81
+ def clear_cache(
82
+ config_path: Optional[Path] = typer.Option(None, "--config", "-c"),
83
+ force: bool = typer.Option(False, "--force", "-f", help="Bypass confirmation prompt."),
84
+ ) -> None:
85
+ cfg = load_config(config_path)
86
+ setup_logging(level=cfg.logging.level)
87
+ mgr = CacheManager(cfg)
88
+
89
+ if not force:
90
+ confirm = typer.confirm("Are you sure you want to purge the semantic vector cache?")
91
+ if not confirm:
92
+ console.print("[dim]Operation cancelled.[/dim]")
93
+ return
94
+
95
+ count = mgr.clear()
96
+ console.print(f"[bold green]✔ Successfully cleared {count} entries from semantic cache.[/bold green]")
@@ -0,0 +1,111 @@
1
+ """CLI commands for configuration inspection and setup: `kernhell config <subcommand>`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ import typer
9
+ from rich import box
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from kernhell.core.config import KernHellConfig, load_config
14
+ from kernhell.utils.logger import setup_logging
15
+
16
+ console = Console(stderr=True)
17
+
18
+ config_app = typer.Typer(
19
+ name="config",
20
+ help="View and configure KernHell engine parameters.",
21
+ no_args_is_help=True,
22
+ )
23
+
24
+
25
+ @config_app.command(name="show", help="Display active configuration settings and source paths.")
26
+ def show_config(
27
+ config_path: Optional[Path] = typer.Option(None, "--config", "-c"),
28
+ ) -> None:
29
+ cfg = load_config(config_path)
30
+ setup_logging(level=cfg.logging.level)
31
+
32
+ table = Table(title="⚙️ KernHell Active Configuration", box=box.ROUNDED)
33
+ table.add_column("Section", style="bold cyan")
34
+ table.add_column("Setting", style="yellow")
35
+ table.add_column("Value")
36
+
37
+ # LLM
38
+ table.add_row("LLM", "Provider", cfg.llm.provider)
39
+ table.add_row("LLM", "Model", cfg.llm.model)
40
+ table.add_row("LLM", "API Key", "******" if cfg.llm.api_key else "[dim](not set, using local heuristic)[/dim]")
41
+ table.add_row("LLM", "Temperature", str(cfg.llm.temperature))
42
+
43
+ # Healing
44
+ table.add_row("Healing", "Confidence Threshold", f"{int(cfg.healing.confidence_threshold * 100)}%")
45
+ table.add_row("Healing", "Max Heals / File", str(cfg.healing.max_heals_per_file))
46
+ table.add_row("Healing", "Dry Run Default", str(cfg.healing.dry_run))
47
+
48
+ # Cache
49
+ table.add_row("Cache", "Enabled", str(cfg.cache.enabled))
50
+ table.add_row("Cache", "Similarity Threshold", f"{int(cfg.cache.similarity_threshold * 100)}%")
51
+ table.add_row("Cache", "Cache Directory", cfg.cache.cache_dir)
52
+ table.add_row("Cache", "Embedding Model", cfg.cache.embedding_model)
53
+
54
+ # Scanner
55
+ table.add_row("Scanner", "Browser", cfg.scanner.browser)
56
+ table.add_row("Scanner", "Timeout (ms)", str(cfg.scanner.timeout_ms))
57
+ table.add_row("Scanner", "Capture Screenshot", str(cfg.scanner.capture_screenshot))
58
+
59
+ console.print(table)
60
+
61
+
62
+ @config_app.command(name="init", help="Interactive setup wizard to initialize .kernhellrc.toml.")
63
+ def init_config(
64
+ target_path: Path = typer.Option(Path(".kernhellrc.toml"), "--path", "-p", help="Target config file path."),
65
+ ) -> None:
66
+ console.print("[bold red]KernHell Engine Configuration Wizard[/bold red]\n")
67
+
68
+ provider = typer.prompt("Select LLM Provider (groq, openai, anthropic, ollama)", default="groq")
69
+ model = typer.prompt(
70
+ "Default Model Name",
71
+ default="llama-3.3-70b-versatile" if provider == "groq" else "gpt-4o",
72
+ )
73
+ api_key = typer.prompt("LLM API Key (press Enter to skip for local/env)", default="", hide_input=True)
74
+ confidence = typer.prompt("Confidence Threshold for auto-healing (0.0 to 1.0)", default=0.85, type=float)
75
+ browser = typer.prompt("Playwright Browser (chromium, firefox, webkit)", default="chromium")
76
+
77
+ toml_content = f"""# KernHell Configuration File
78
+ # Generated via `kernhell config init`
79
+
80
+ [llm]
81
+ provider = "{provider}"
82
+ model = "{model}"
83
+ {"api_key = \"" + api_key + "\"" if api_key else "# api_key = \"\""}
84
+ temperature = 0.1
85
+ max_tokens = 2048
86
+
87
+ [healing]
88
+ confidence_threshold = {confidence}
89
+ max_heals_per_file = 5
90
+ dry_run = false
91
+ auto_commit = false
92
+
93
+ [cache]
94
+ enabled = true
95
+ similarity_threshold = 0.92
96
+ cache_dir = "~/.kernhell/cache"
97
+ embedding_model = "all-MiniLM-L6-v2"
98
+
99
+ [scanner]
100
+ timeout_ms = 5000
101
+ capture_screenshot = true
102
+ browser = "{browser}"
103
+ headless = true
104
+
105
+ [logging]
106
+ level = "INFO"
107
+ json_output = false
108
+ """
109
+
110
+ target_path.write_text(toml_content.strip() + "\n", encoding="utf-8")
111
+ console.print(f"\n[bold green]✔ Configuration successfully written to {target_path.resolve()}[/bold green]")
@@ -0,0 +1,55 @@
1
+ """CLI command: `kernhell dashboard` (or `kernhell ui`).
2
+
3
+ Launches the local KernHell Mission Control Dashboard and API service.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import webbrowser
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import typer
13
+ import uvicorn
14
+ from rich.console import Console
15
+ from rich.panel import Panel
16
+
17
+ from kernhell.api.server import create_app
18
+ from kernhell.cli.ui import print_banner
19
+ from kernhell.core.config import load_config
20
+ from kernhell.utils.logger import setup_logging
21
+
22
+ console = Console(stderr=True)
23
+
24
+
25
+ def dashboard_command(
26
+ host: str = typer.Option("127.0.0.1", "--host", "-h", help="Bind host address."),
27
+ port: int = typer.Option(8000, "--port", "-p", help="Bind port number."),
28
+ no_open: bool = typer.Option(False, "--no-open", help="Do not automatically open browser."),
29
+ config_path: Optional[Path] = typer.Option(None, "--config", "-c"),
30
+ ) -> None:
31
+ """Launch the KernHell Mission Control Dashboard and REST API service."""
32
+ cfg = load_config(config_path)
33
+ setup_logging(level=cfg.logging.level)
34
+
35
+ print_banner()
36
+
37
+ url = f"http://{host}:{port}"
38
+ web_url = "http://localhost:3000"
39
+
40
+ panel_content = (
41
+ f"[bold cyan]Mission Control API:[/bold cyan] [underline]{url}[/underline]\n"
42
+ f"[bold cyan]Swagger API Docs:[/bold cyan] [underline]{url}/docs[/underline]\n"
43
+ f"[bold cyan]Web Dashboard UI:[/bold cyan] [underline]{web_url}[/underline]\n\n"
44
+ f"[dim]Press Ctrl+C to terminate the dashboard server.[/dim]"
45
+ )
46
+ console.print(Panel(panel_content, title="🚀 Mission Control Active", border_style="cyan"))
47
+
48
+ if not no_open:
49
+ try:
50
+ webbrowser.open(f"{url}/docs")
51
+ except Exception:
52
+ pass
53
+
54
+ app = create_app(cfg)
55
+ uvicorn.run(app, host=host, port=port, log_level="warning")
@@ -0,0 +1,141 @@
1
+ """CLI command: `kernhell heal <test_file_or_directory>`.
2
+
3
+ Executes tests, intercepts selector failures, and runs the self-healing engine.
4
+ Supports single-file healing and directory-wide batch healing.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from typing import Optional
11
+
12
+ import typer
13
+ from rich.console import Console
14
+
15
+ from kernhell.cli.ui import (
16
+ print_banner,
17
+ print_failure_context,
18
+ print_pruning_stats,
19
+ )
20
+ from kernhell.core.config import load_config
21
+ from kernhell.core.exceptions import (
22
+ NoFailuresDetectedError,
23
+ PlaywrightNotInstalledError,
24
+ ScannerError,
25
+ TestFileNotFoundError,
26
+ )
27
+ from kernhell.core.models import HealingReport
28
+ from kernhell.core.orchestrator import HealingOrchestrator
29
+ from kernhell.reporter.reporter import HealingReporter
30
+ from kernhell.utils.file_utils import read_file
31
+ from kernhell.utils.logger import setup_logging
32
+
33
+ console = Console(stderr=True)
34
+
35
+
36
+ def heal_command(
37
+ target: Path = typer.Argument(
38
+ ...,
39
+ help="Path to a Playwright test file or directory of test files to heal.",
40
+ exists=False,
41
+ ),
42
+ dry_run: bool = typer.Option(
43
+ False,
44
+ "--dry-run",
45
+ help="Preview healed selectors without mutating test files on disk.",
46
+ ),
47
+ output_format: str = typer.Option(
48
+ "text",
49
+ "--output",
50
+ "-o",
51
+ help="Output format: 'text' (Rich terminal) or 'json' (machine-parseable for CI).",
52
+ ),
53
+ output_file: Optional[Path] = typer.Option(
54
+ None,
55
+ "--output-file",
56
+ help="Optional path to save JSON healing report.",
57
+ ),
58
+ config_path: Optional[Path] = typer.Option(
59
+ None,
60
+ "--config",
61
+ "-c",
62
+ help="Path to custom .kernhellrc.toml file.",
63
+ ),
64
+ verbose: bool = typer.Option(
65
+ False,
66
+ "--verbose",
67
+ "-v",
68
+ help="Enable detailed debug logs.",
69
+ ),
70
+ json_log: bool = typer.Option(
71
+ False,
72
+ "--json-log",
73
+ help="Output structured logs in JSON format.",
74
+ ),
75
+ ) -> None:
76
+ """Autonomously detect and heal broken Playwright selectors."""
77
+ cfg = load_config(config_path)
78
+ log_level = "DEBUG" if verbose else cfg.logging.level
79
+ setup_logging(level=log_level, json_output=json_log or cfg.logging.json_output)
80
+
81
+ if output_format != "json":
82
+ print_banner()
83
+
84
+ if not target.exists():
85
+ console.print(f"[bold red]Error:[/bold red] Target path not found: {target}")
86
+ raise typer.Exit(code=1)
87
+
88
+ # Discover test files (single file vs batch directory mode)
89
+ if target.is_dir():
90
+ test_files = sorted(target.rglob("test_*.py"))
91
+ if not test_files:
92
+ console.print(f"[yellow]No 'test_*.py' files found in {target}[/yellow]")
93
+ raise typer.Exit(code=0)
94
+ else:
95
+ test_files = [target]
96
+
97
+ orchestrator = HealingOrchestrator(cfg)
98
+ reports: list[HealingReport] = []
99
+
100
+ for test_file in test_files:
101
+ original_code = read_file(test_file) if test_file.is_file() else ""
102
+
103
+ with console.status(f"[bold cyan]Scanning & healing [white]{test_file.name}[/white]..."):
104
+ try:
105
+ report = orchestrator.heal(test_file, dry_run=dry_run)
106
+ reports.append(report)
107
+ except NoFailuresDetectedError as exc:
108
+ if output_format != "json":
109
+ console.print(f"[bold green]✔[/bold green] {test_file.name}: {exc}")
110
+ continue
111
+ except (PlaywrightNotInstalledError, ScannerError, TestFileNotFoundError) as exc:
112
+ console.print(f"[bold red]Failed on {test_file.name}:[/bold red] {exc}")
113
+ continue
114
+
115
+ if output_format != "json" and report.results:
116
+ for i, result in enumerate(report.results, start=1):
117
+ print_failure_context(result.failure, index=i)
118
+ if result.pruned_dom:
119
+ print_pruning_stats(result.pruned_dom)
120
+ console.print()
121
+
122
+ # Render surgical AST diff if file was patched
123
+ if not dry_run and original_code and test_file.is_file():
124
+ patched_code = read_file(test_file)
125
+ if patched_code != original_code:
126
+ HealingReporter.render_diff(
127
+ old_code=original_code,
128
+ new_code=patched_code,
129
+ filename=test_file.name,
130
+ )
131
+
132
+ HealingReporter.print_summary(report)
133
+
134
+ # JSON export handling
135
+ if output_format == "json" or output_file is not None:
136
+ combined_json = [json_str for report in reports for json_str in [HealingReporter.export_json(report)]]
137
+ payload = f"[\n{', '.join(combined_json)}\n]"
138
+ if output_file:
139
+ output_file.write_text(payload, encoding="utf-8")
140
+ if output_format == "json":
141
+ typer.echo(payload)
@@ -0,0 +1,87 @@
1
+ """CLI command: `kernhell scan <test_file>`.
2
+
3
+ Detects broken selectors and captures DOM snapshots without modifying test files.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import typer
12
+ from rich.console import Console
13
+
14
+ from kernhell.cli.ui import (
15
+ print_banner,
16
+ print_failure_context,
17
+ print_pruning_stats,
18
+ print_scan_summary,
19
+ )
20
+ from kernhell.core.config import load_config
21
+ from kernhell.core.exceptions import (
22
+ NoFailuresDetectedError,
23
+ PlaywrightNotInstalledError,
24
+ ScannerError,
25
+ TestFileNotFoundError,
26
+ )
27
+ from kernhell.core.orchestrator import HealingOrchestrator
28
+ from kernhell.utils.logger import setup_logging
29
+
30
+ console = Console(stderr=True)
31
+
32
+
33
+ def scan_command(
34
+ test_file: Path = typer.Argument(
35
+ ...,
36
+ help="Path to the Playwright test file to scan for broken selectors.",
37
+ exists=False,
38
+ ),
39
+ config_path: Optional[Path] = typer.Option(
40
+ None,
41
+ "--config",
42
+ "-c",
43
+ help="Path to custom .kernhellrc.toml file.",
44
+ ),
45
+ verbose: bool = typer.Option(
46
+ False,
47
+ "--verbose",
48
+ "-v",
49
+ help="Enable detailed debug logs.",
50
+ ),
51
+ json_log: bool = typer.Option(
52
+ False,
53
+ "--json-log",
54
+ help="Output structured logs in JSON format.",
55
+ ),
56
+ ) -> None:
57
+ """Scan a Playwright test file, intercepting selector failures and capturing DOM."""
58
+ cfg = load_config(config_path)
59
+ log_level = "DEBUG" if verbose else cfg.logging.level
60
+ setup_logging(level=log_level, json_output=json_log or cfg.logging.json_output)
61
+
62
+ print_banner()
63
+
64
+ if not test_file.exists():
65
+ console.print(f"[bold red]Error:[/bold red] Test file not found: {test_file}")
66
+ raise typer.Exit(code=1)
67
+
68
+ orchestrator = HealingOrchestrator(cfg)
69
+
70
+ with console.status(f"[bold cyan]Scanning [white]{test_file.name}[/white] for selector failures..."):
71
+ try:
72
+ results = orchestrator.scan(test_file)
73
+ except NoFailuresDetectedError as exc:
74
+ console.print(f"[bold green]✔[/bold green] {exc}")
75
+ raise typer.Exit(code=0)
76
+ except (PlaywrightNotInstalledError, ScannerError, TestFileNotFoundError) as exc:
77
+ console.print(f"[bold red]Scan Failed:[/bold red] {exc}")
78
+ raise typer.Exit(code=1)
79
+
80
+ failures = [f for f, _ in results]
81
+ print_scan_summary(failures)
82
+
83
+ for i, (failure, pruned) in enumerate(results, start=1):
84
+ print_failure_context(failure, index=i)
85
+ if pruned:
86
+ print_pruning_stats(pruned)
87
+ console.print()