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.
- kernhell/__init__.py +8 -0
- kernhell/__main__.py +6 -0
- kernhell/analyzer/analyzer.py +178 -0
- kernhell/analyzer/healer.py +249 -0
- kernhell/analyzer/prompts.py +89 -0
- kernhell/api/__init__.py +5 -0
- kernhell/api/server.py +367 -0
- kernhell/cache/__init__.py +13 -0
- kernhell/cache/cache_manager.py +221 -0
- kernhell/cache/embedder.py +104 -0
- kernhell/cache/schemas.py +42 -0
- kernhell/cli/__init__.py +1 -0
- kernhell/cli/app.py +62 -0
- kernhell/cli/commands/__init__.py +1 -0
- kernhell/cli/commands/cache.py +96 -0
- kernhell/cli/commands/config.py +111 -0
- kernhell/cli/commands/dashboard.py +55 -0
- kernhell/cli/commands/heal.py +141 -0
- kernhell/cli/commands/scan.py +87 -0
- kernhell/cli/ui.py +176 -0
- kernhell/core/__init__.py +1 -0
- kernhell/core/config.py +219 -0
- kernhell/core/exceptions.py +104 -0
- kernhell/core/models.py +174 -0
- kernhell/core/orchestrator.py +245 -0
- kernhell/patcher/patcher.py +215 -0
- kernhell/patcher/verifier.py +82 -0
- kernhell/pruner/__init__.py +1 -0
- kernhell/pruner/dom_pruner.py +240 -0
- kernhell/reporter/__init__.py +5 -0
- kernhell/reporter/reporter.py +124 -0
- kernhell/scanner/__init__.py +1 -0
- kernhell/scanner/parser.py +276 -0
- kernhell/scanner/runner.py +242 -0
- kernhell/utils/__init__.py +1 -0
- kernhell/utils/file_utils.py +117 -0
- kernhell/utils/logger.py +78 -0
- kernhell-0.1.0.dist-info/METADATA +245 -0
- kernhell-0.1.0.dist-info/RECORD +42 -0
- kernhell-0.1.0.dist-info/WHEEL +4 -0
- kernhell-0.1.0.dist-info/entry_points.txt +2 -0
- kernhell-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Scanner runner β executes Playwright tests and captures failures + live DOM.
|
|
2
|
+
|
|
3
|
+
Weekend 1 strategy: **Subprocess-based** execution.
|
|
4
|
+
1. Run the test file via ``pytest`` in a subprocess.
|
|
5
|
+
2. If it fails, parse the error output for broken selectors.
|
|
6
|
+
3. Re-launch a browser, navigate to the page URL extracted from the test,
|
|
7
|
+
and capture ``page.content()`` for the live DOM snapshot.
|
|
8
|
+
|
|
9
|
+
This approach is simpler but less precise than the step-by-step interceptor
|
|
10
|
+
(planned for Weekend 3). It works reliably for the common case where the
|
|
11
|
+
test navigates to a URL before interacting with elements.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import subprocess
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import structlog
|
|
23
|
+
|
|
24
|
+
from kernhell.core.config import KernHellConfig
|
|
25
|
+
from kernhell.core.exceptions import (
|
|
26
|
+
NoFailuresDetectedError,
|
|
27
|
+
PlaywrightNotInstalledError,
|
|
28
|
+
ScannerError,
|
|
29
|
+
TestFileNotFoundError,
|
|
30
|
+
)
|
|
31
|
+
from kernhell.core.models import FailureContext
|
|
32
|
+
from kernhell.scanner.parser import parse_playwright_errors
|
|
33
|
+
|
|
34
|
+
logger = structlog.get_logger()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _extract_url_from_test(test_file: Path) -> str | None:
|
|
38
|
+
"""Extract the first ``page.goto(...)`` URL from a test file.
|
|
39
|
+
|
|
40
|
+
This is a best-effort heuristic used to re-open the page for DOM capture.
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
content = test_file.read_text(encoding="utf-8")
|
|
44
|
+
except Exception:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
# Match: page.goto("https://example.com") or page.goto('...')
|
|
48
|
+
m = re.search(r"""page\.goto\(\s*["'](.+?)["']""", content)
|
|
49
|
+
return m.group(1) if m else None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class ScannerRunner:
|
|
53
|
+
"""Executes a Playwright test file and captures failure context.
|
|
54
|
+
|
|
55
|
+
Parameters
|
|
56
|
+
----------
|
|
57
|
+
config:
|
|
58
|
+
The KernHell configuration (scanner timeout, browser, headless, etc.).
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, config: KernHellConfig) -> None:
|
|
62
|
+
self._config = config
|
|
63
|
+
self._scanner_cfg = config.scanner
|
|
64
|
+
|
|
65
|
+
def run(self, test_file: Path) -> list[FailureContext]:
|
|
66
|
+
"""Run the test file and return structured failures.
|
|
67
|
+
|
|
68
|
+
Parameters
|
|
69
|
+
----------
|
|
70
|
+
test_file:
|
|
71
|
+
Path to the Playwright test file (e.g., ``tests/test_checkout.py``).
|
|
72
|
+
|
|
73
|
+
Returns
|
|
74
|
+
-------
|
|
75
|
+
list[FailureContext]
|
|
76
|
+
One entry per broken selector detected. The ``live_dom`` field
|
|
77
|
+
will be populated if we can successfully re-open the page.
|
|
78
|
+
|
|
79
|
+
Raises
|
|
80
|
+
------
|
|
81
|
+
TestFileNotFoundError
|
|
82
|
+
If *test_file* does not exist.
|
|
83
|
+
ScannerError
|
|
84
|
+
If the test runner itself fails to execute.
|
|
85
|
+
NoFailuresDetectedError
|
|
86
|
+
If the test passes with zero selector failures.
|
|
87
|
+
"""
|
|
88
|
+
test_file = test_file.resolve()
|
|
89
|
+
if not test_file.is_file():
|
|
90
|
+
raise TestFileNotFoundError(f"Test file not found: {test_file}")
|
|
91
|
+
|
|
92
|
+
logger.info("scanner.run.start", test_file=str(test_file))
|
|
93
|
+
|
|
94
|
+
# Step 1: Run the test via pytest subprocess
|
|
95
|
+
error_output = self._run_pytest(test_file)
|
|
96
|
+
|
|
97
|
+
if error_output is None:
|
|
98
|
+
raise NoFailuresDetectedError(
|
|
99
|
+
f"All tests passed in {test_file.name} β nothing to heal!"
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Step 2: Parse the error output
|
|
103
|
+
failures = parse_playwright_errors(error_output, test_file)
|
|
104
|
+
|
|
105
|
+
if not failures:
|
|
106
|
+
raise NoFailuresDetectedError(
|
|
107
|
+
f"Test failed but no Playwright selector errors detected in {test_file.name}. "
|
|
108
|
+
"The failure may not be selector-related."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Step 3: Capture live DOM for each failure
|
|
112
|
+
page_url = _extract_url_from_test(test_file)
|
|
113
|
+
if page_url:
|
|
114
|
+
live_dom = self._capture_live_dom(page_url)
|
|
115
|
+
if live_dom:
|
|
116
|
+
for f in failures:
|
|
117
|
+
f.live_dom = live_dom
|
|
118
|
+
f.page_url = page_url
|
|
119
|
+
|
|
120
|
+
logger.info(
|
|
121
|
+
"scanner.run.done",
|
|
122
|
+
test_file=str(test_file),
|
|
123
|
+
failures=len(failures),
|
|
124
|
+
has_dom=bool(failures[0].live_dom) if failures else False,
|
|
125
|
+
)
|
|
126
|
+
return failures
|
|
127
|
+
|
|
128
|
+
def _run_pytest(self, test_file: Path) -> str | None:
|
|
129
|
+
"""Execute pytest on the test file and return error output.
|
|
130
|
+
|
|
131
|
+
Returns *None* if the test passes (exit code 0).
|
|
132
|
+
"""
|
|
133
|
+
cmd = [
|
|
134
|
+
sys.executable,
|
|
135
|
+
"-m",
|
|
136
|
+
"pytest",
|
|
137
|
+
str(test_file),
|
|
138
|
+
"-v",
|
|
139
|
+
"--tb=long",
|
|
140
|
+
"--no-header",
|
|
141
|
+
f"--timeout={self._scanner_cfg.timeout_ms // 1000}",
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
logger.debug("scanner.pytest.exec", cmd=" ".join(cmd))
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
result = subprocess.run(
|
|
148
|
+
cmd,
|
|
149
|
+
capture_output=True,
|
|
150
|
+
text=True,
|
|
151
|
+
timeout=self._scanner_cfg.timeout_ms * 3 / 1000, # 3x the action timeout
|
|
152
|
+
cwd=str(test_file.parent),
|
|
153
|
+
)
|
|
154
|
+
except FileNotFoundError:
|
|
155
|
+
raise PlaywrightNotInstalledError(
|
|
156
|
+
"pytest is not installed or not on PATH. "
|
|
157
|
+
"Run: pip install pytest pytest-playwright"
|
|
158
|
+
)
|
|
159
|
+
except subprocess.TimeoutExpired:
|
|
160
|
+
raise ScannerError(
|
|
161
|
+
f"Test execution timed out after {self._scanner_cfg.timeout_ms * 3}ms"
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
if result.returncode == 0:
|
|
165
|
+
logger.info("scanner.pytest.passed", test_file=str(test_file))
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
# Combine stdout and stderr for full error context
|
|
169
|
+
combined = result.stdout + "\n" + result.stderr
|
|
170
|
+
logger.debug(
|
|
171
|
+
"scanner.pytest.failed",
|
|
172
|
+
exit_code=result.returncode,
|
|
173
|
+
output_len=len(combined),
|
|
174
|
+
)
|
|
175
|
+
return combined
|
|
176
|
+
|
|
177
|
+
def _capture_live_dom(self, url: str) -> str:
|
|
178
|
+
"""Re-open the page URL and capture the live DOM HTML.
|
|
179
|
+
|
|
180
|
+
Uses Playwright's sync API to launch a browser, navigate to the URL,
|
|
181
|
+
and grab ``page.content()``. This gives us the actual DOM state
|
|
182
|
+
the developer sees, even if selectors changed.
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
186
|
+
self._async_capture_dom(url)
|
|
187
|
+
)
|
|
188
|
+
except RuntimeError:
|
|
189
|
+
# No event loop running β create one
|
|
190
|
+
return asyncio.run(self._async_capture_dom(url))
|
|
191
|
+
|
|
192
|
+
async def _async_capture_dom(self, url: str) -> str:
|
|
193
|
+
"""Async implementation of DOM capture."""
|
|
194
|
+
try:
|
|
195
|
+
from playwright.async_api import async_playwright
|
|
196
|
+
except ImportError:
|
|
197
|
+
raise PlaywrightNotInstalledError(
|
|
198
|
+
"Playwright is not installed. Run: pip install playwright && playwright install"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
logger.debug("scanner.dom_capture.start", url=url)
|
|
202
|
+
|
|
203
|
+
try:
|
|
204
|
+
async with async_playwright() as p:
|
|
205
|
+
browser_type = getattr(p, self._scanner_cfg.browser, p.chromium)
|
|
206
|
+
browser = await browser_type.launch(headless=self._scanner_cfg.headless)
|
|
207
|
+
page = await browser.new_page()
|
|
208
|
+
|
|
209
|
+
try:
|
|
210
|
+
await page.goto(
|
|
211
|
+
url,
|
|
212
|
+
wait_until="domcontentloaded",
|
|
213
|
+
timeout=self._scanner_cfg.timeout_ms,
|
|
214
|
+
)
|
|
215
|
+
# Wait a bit for dynamic content
|
|
216
|
+
await page.wait_for_timeout(1000)
|
|
217
|
+
dom = await page.content()
|
|
218
|
+
|
|
219
|
+
# Optional screenshot
|
|
220
|
+
if self._scanner_cfg.capture_screenshot:
|
|
221
|
+
screenshot = await page.screenshot(full_page=True)
|
|
222
|
+
logger.debug(
|
|
223
|
+
"scanner.screenshot.captured",
|
|
224
|
+
size=len(screenshot),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
logger.info(
|
|
228
|
+
"scanner.dom_capture.done",
|
|
229
|
+
url=url,
|
|
230
|
+
dom_size=len(dom),
|
|
231
|
+
)
|
|
232
|
+
return dom
|
|
233
|
+
finally:
|
|
234
|
+
await browser.close()
|
|
235
|
+
|
|
236
|
+
except Exception as exc:
|
|
237
|
+
logger.warning(
|
|
238
|
+
"scanner.dom_capture.failed",
|
|
239
|
+
url=url,
|
|
240
|
+
error=str(exc),
|
|
241
|
+
)
|
|
242
|
+
return ""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility package β logging, file I/O, and git helpers."""
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""Safe file I/O utilities for KernHell.
|
|
2
|
+
|
|
3
|
+
Every file write goes through these helpers so we can:
|
|
4
|
+
- Create automatic backups before patching.
|
|
5
|
+
- Handle encoding consistently (UTF-8 everywhere).
|
|
6
|
+
- Provide atomic writes to prevent half-written files on crash.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
import shutil
|
|
13
|
+
import tempfile
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import structlog
|
|
17
|
+
|
|
18
|
+
logger = structlog.get_logger()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def read_file(path: Path) -> str:
|
|
22
|
+
"""Read a file as UTF-8 text.
|
|
23
|
+
|
|
24
|
+
Raises
|
|
25
|
+
------
|
|
26
|
+
FileNotFoundError
|
|
27
|
+
If the file does not exist.
|
|
28
|
+
"""
|
|
29
|
+
resolved = path.resolve()
|
|
30
|
+
logger.debug("file.read", path=str(resolved))
|
|
31
|
+
return resolved.read_text(encoding="utf-8")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def write_file(path: Path, content: str) -> None:
|
|
35
|
+
"""Write content to a file atomically.
|
|
36
|
+
|
|
37
|
+
Writes to a temporary file in the same directory first, then
|
|
38
|
+
atomically replaces the target. This prevents data loss if the
|
|
39
|
+
process crashes mid-write.
|
|
40
|
+
"""
|
|
41
|
+
resolved = path.resolve()
|
|
42
|
+
resolved.parent.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
|
|
44
|
+
# Write to a temp file in the same directory so os.replace is atomic.
|
|
45
|
+
fd, tmp_path_str = tempfile.mkstemp(
|
|
46
|
+
dir=str(resolved.parent),
|
|
47
|
+
prefix=".kernhell_",
|
|
48
|
+
suffix=".tmp",
|
|
49
|
+
)
|
|
50
|
+
# Important on Windows: close file descriptor immediately so file isn't locked
|
|
51
|
+
os.close(fd)
|
|
52
|
+
tmp_path = Path(tmp_path_str)
|
|
53
|
+
try:
|
|
54
|
+
tmp_path.write_text(content, encoding="utf-8")
|
|
55
|
+
os.replace(str(tmp_path), str(resolved))
|
|
56
|
+
logger.debug("file.write", path=str(resolved), size=len(content))
|
|
57
|
+
except Exception:
|
|
58
|
+
# Clean up the temp file on failure.
|
|
59
|
+
tmp_path.unlink(missing_ok=True)
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def backup_file(path: Path, suffix: str = ".kernhell.bak") -> Path:
|
|
64
|
+
"""Create a backup copy of a file before patching.
|
|
65
|
+
|
|
66
|
+
Returns the path to the backup file.
|
|
67
|
+
"""
|
|
68
|
+
resolved = path.resolve()
|
|
69
|
+
backup_path = resolved.with_suffix(resolved.suffix + suffix)
|
|
70
|
+
shutil.copy2(str(resolved), str(backup_path))
|
|
71
|
+
logger.info("file.backup", original=str(resolved), backup=str(backup_path))
|
|
72
|
+
return backup_path
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def restore_from_backup(original: Path, suffix: str = ".kernhell.bak") -> bool:
|
|
76
|
+
"""Restore a file from its backup. Returns True if successful."""
|
|
77
|
+
resolved = original.resolve()
|
|
78
|
+
backup_path = resolved.with_suffix(resolved.suffix + suffix)
|
|
79
|
+
if backup_path.is_file():
|
|
80
|
+
shutil.copy2(str(backup_path), str(resolved))
|
|
81
|
+
logger.info("file.restore", path=str(resolved))
|
|
82
|
+
return True
|
|
83
|
+
logger.warning("file.restore.missing", backup=str(backup_path))
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def extract_surrounding_lines(
|
|
88
|
+
content: str,
|
|
89
|
+
line_number: int,
|
|
90
|
+
context: int = 10,
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Extract Β±context lines around a specific line number.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
content:
|
|
97
|
+
Full file content.
|
|
98
|
+
line_number:
|
|
99
|
+
1-indexed line number to centre on.
|
|
100
|
+
context:
|
|
101
|
+
Number of lines to include above and below.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
str
|
|
106
|
+
The extracted lines with line numbers prefixed.
|
|
107
|
+
"""
|
|
108
|
+
lines = content.splitlines()
|
|
109
|
+
start = max(0, line_number - 1 - context)
|
|
110
|
+
end = min(len(lines), line_number + context)
|
|
111
|
+
|
|
112
|
+
numbered: list[str] = []
|
|
113
|
+
for i, line in enumerate(lines[start:end], start=start + 1):
|
|
114
|
+
marker = " >>> " if i == line_number else " "
|
|
115
|
+
numbered.append(f"{i:4d}{marker}{line}")
|
|
116
|
+
|
|
117
|
+
return "\n".join(numbered)
|
kernhell/utils/logger.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Structured logging configuration for KernHell.
|
|
2
|
+
|
|
3
|
+
Uses ``structlog`` with two output modes:
|
|
4
|
+
- **Console** (default): Rich-coloured, human-friendly output via
|
|
5
|
+
``structlog.dev.ConsoleRenderer``.
|
|
6
|
+
- **JSON** (``--json-log`` or CI): Machine-parseable JSON lines, ideal
|
|
7
|
+
for log aggregation.
|
|
8
|
+
|
|
9
|
+
Call :func:`setup_logging` once at startup (done automatically by the CLI).
|
|
10
|
+
After that, obtain loggers anywhere with ``structlog.get_logger()``.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
import structlog
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def setup_logging(*, level: str = "INFO", json_output: bool = False) -> None:
|
|
22
|
+
"""Configure structlog and stdlib logging for the application.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
level:
|
|
27
|
+
Minimum log level (``DEBUG``, ``INFO``, ``WARNING``, ``ERROR``).
|
|
28
|
+
json_output:
|
|
29
|
+
When *True*, emit JSON lines instead of coloured console output.
|
|
30
|
+
Recommended for CI/CD environments.
|
|
31
|
+
"""
|
|
32
|
+
log_level = getattr(logging, level.upper(), logging.INFO)
|
|
33
|
+
|
|
34
|
+
# Shared processors run before the final renderer.
|
|
35
|
+
shared_processors: list[structlog.types.Processor] = [
|
|
36
|
+
structlog.contextvars.merge_contextvars,
|
|
37
|
+
structlog.stdlib.add_log_level,
|
|
38
|
+
structlog.stdlib.add_logger_name,
|
|
39
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
40
|
+
structlog.processors.StackInfoRenderer(),
|
|
41
|
+
structlog.processors.UnicodeDecoder(),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
if json_output:
|
|
45
|
+
renderer: structlog.types.Processor = structlog.processors.JSONRenderer()
|
|
46
|
+
else:
|
|
47
|
+
renderer = structlog.dev.ConsoleRenderer(
|
|
48
|
+
colors=sys.stderr.isatty(),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
structlog.configure(
|
|
52
|
+
processors=[
|
|
53
|
+
*shared_processors,
|
|
54
|
+
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
|
|
55
|
+
],
|
|
56
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
57
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
58
|
+
cache_logger_on_first_use=True,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
formatter = structlog.stdlib.ProcessorFormatter(
|
|
62
|
+
processors=[
|
|
63
|
+
structlog.stdlib.ProcessorFormatter.remove_processors_meta,
|
|
64
|
+
renderer,
|
|
65
|
+
],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
handler = logging.StreamHandler(sys.stderr)
|
|
69
|
+
handler.setFormatter(formatter)
|
|
70
|
+
|
|
71
|
+
root = logging.getLogger()
|
|
72
|
+
root.handlers.clear()
|
|
73
|
+
root.addHandler(handler)
|
|
74
|
+
root.setLevel(log_level)
|
|
75
|
+
|
|
76
|
+
# Silence noisy third-party loggers.
|
|
77
|
+
for noisy in ("playwright", "urllib3", "httpx", "chromadb"):
|
|
78
|
+
logging.getLogger(noisy).setLevel(logging.WARNING)
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: kernhell
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: AI-Powered Autonomous Self-Healing Testing Engine β The Heart of Your System Where Innovation Meets Precision
|
|
5
|
+
Project-URL: Homepage, https://github.com/riteshkumar072/KernHell
|
|
6
|
+
Project-URL: Documentation, https://github.com/riteshkumar072/KernHell#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/riteshkumar072/KernHell
|
|
8
|
+
Project-URL: Issues, https://github.com/riteshkumar072/KernHell/issues
|
|
9
|
+
Author-email: Ritesh <ritesh@kernhell.dev>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: ai,cli,playwright,self-healing,testing
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Environment :: Console
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
20
|
+
Classifier: Topic :: Software Development :: Testing
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: libcst>=1.1.0
|
|
23
|
+
Requires-Dist: litellm<2.0,>=1.30
|
|
24
|
+
Requires-Dist: playwright<2.0,>=1.40
|
|
25
|
+
Requires-Dist: pydantic-settings<3.0,>=2.1
|
|
26
|
+
Requires-Dist: pydantic<3.0,>=2.5
|
|
27
|
+
Requires-Dist: rich<16.0,>=13.0
|
|
28
|
+
Requires-Dist: selectolax<1.0,>=0.3
|
|
29
|
+
Requires-Dist: structlog>=24.0
|
|
30
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
31
|
+
Requires-Dist: typer<1.0,>=0.12
|
|
32
|
+
Provides-Extra: all
|
|
33
|
+
Requires-Dist: aiosqlite<1.0,>=0.19; extra == 'all'
|
|
34
|
+
Requires-Dist: build>=1.0.0; extra == 'all'
|
|
35
|
+
Requires-Dist: chromadb<1.0,>=0.4; extra == 'all'
|
|
36
|
+
Requires-Dist: fastapi<1.0,>=0.110; extra == 'all'
|
|
37
|
+
Requires-Dist: mypy<2.0,>=1.8; extra == 'all'
|
|
38
|
+
Requires-Dist: pytest-asyncio<1.0,>=0.23; extra == 'all'
|
|
39
|
+
Requires-Dist: pytest-cov<6.0,>=5.0; extra == 'all'
|
|
40
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == 'all'
|
|
41
|
+
Requires-Dist: ruff<1.0,>=0.3; extra == 'all'
|
|
42
|
+
Requires-Dist: sentence-transformers<3.0,>=2.2; extra == 'all'
|
|
43
|
+
Requires-Dist: twine>=5.0.0; extra == 'all'
|
|
44
|
+
Requires-Dist: uvicorn[standard]<1.0,>=0.27; extra == 'all'
|
|
45
|
+
Provides-Extra: cache
|
|
46
|
+
Requires-Dist: chromadb<1.0,>=0.4; extra == 'cache'
|
|
47
|
+
Requires-Dist: sentence-transformers<3.0,>=2.2; extra == 'cache'
|
|
48
|
+
Provides-Extra: dashboard
|
|
49
|
+
Requires-Dist: aiosqlite<1.0,>=0.19; extra == 'dashboard'
|
|
50
|
+
Requires-Dist: fastapi<1.0,>=0.110; extra == 'dashboard'
|
|
51
|
+
Requires-Dist: uvicorn[standard]<1.0,>=0.27; extra == 'dashboard'
|
|
52
|
+
Provides-Extra: dev
|
|
53
|
+
Requires-Dist: build>=1.0.0; extra == 'dev'
|
|
54
|
+
Requires-Dist: mypy<2.0,>=1.8; extra == 'dev'
|
|
55
|
+
Requires-Dist: pytest-asyncio<1.0,>=0.23; extra == 'dev'
|
|
56
|
+
Requires-Dist: pytest-cov<6.0,>=5.0; extra == 'dev'
|
|
57
|
+
Requires-Dist: pytest<9.0,>=8.0; extra == 'dev'
|
|
58
|
+
Requires-Dist: ruff<1.0,>=0.3; extra == 'dev'
|
|
59
|
+
Requires-Dist: twine>=5.0.0; extra == 'dev'
|
|
60
|
+
Description-Content-Type: text/markdown
|
|
61
|
+
|
|
62
|
+
# π₯ KernHell
|
|
63
|
+
|
|
64
|
+
> *"The Heart of Your System Where Innovation Meets Precision"*
|
|
65
|
+
|
|
66
|
+
**AI-Powered Autonomous Self-Healing Testing Engine & CLI tool for Playwright.**
|
|
67
|
+
|
|
68
|
+
[](https://www.python.org/)
|
|
69
|
+
[](https://nextjs.org/)
|
|
70
|
+
[](https://opensource.org/licenses/MIT)
|
|
71
|
+
[-brightgreen.svg)](https://github.com/riteshkumar072/KernHell)
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## β‘ The Problem
|
|
76
|
+
|
|
77
|
+
Modern frontend engineering moves fast. When developers refactor classes, IDs, or DOM wrappers (e.g., `#buy-now` becomes `#purchase-btn`), automated Playwright and Selenium test suites fail even though the business functionality works seamlessly. QA teams waste 30β40% of their time manually maintaining broken selectors.
|
|
78
|
+
|
|
79
|
+
**KernHell autonomously resolves broken selectors in real-time, preserving 100% of your code formatting, comments, and QA sanity.**
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## ποΈ Autonomous 6-Stage Healing Architecture
|
|
84
|
+
|
|
85
|
+
```mermaid
|
|
86
|
+
flowchart TD
|
|
87
|
+
A[Playwright Locator Timeout] -->|1. Intercept| B(Scanner & Live DOM Capture)
|
|
88
|
+
B -->|2. Prune| C(Selectolax Lexbor HTML5 Pruner)
|
|
89
|
+
C -->|85% Token Reduction| D{3. Dense Vector Cache}
|
|
90
|
+
D -->|Cache Hit <5ms| G[Strict DOM Verification]
|
|
91
|
+
D -->|Cache Miss| E[4. AI Brain: LiteLLM Gateway]
|
|
92
|
+
E -->|Intent Diagnosis| F{RENAME vs MISSING?}
|
|
93
|
+
F -->|MISSING: Real Bug| H[Anti-Hallucination Guardrail: Abort Healing]
|
|
94
|
+
F -->|RENAME: Safe Refactor| G
|
|
95
|
+
G -->|5. Verify Unambiguous Element| I[Selector Fix: role / data-testid / text]
|
|
96
|
+
I -->|Store Embedding| D
|
|
97
|
+
I -->|6. Surgical Patch| J[LibCST AST Patcher]
|
|
98
|
+
J -->|Preserve Comments & Formatting| K[Automatic Verification Runner]
|
|
99
|
+
K -->|Pass| L[Commit Fix & Log Run]
|
|
100
|
+
K -->|Fail| M[Immediate Rollback from .kernhell.bak]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## π Key Features
|
|
106
|
+
|
|
107
|
+
- **Failure Interception & Live DOM Capture (`Scanner`)**: Intercepts Playwright selector timeouts and crashes, capturing the error stack trace, line number, broken selector, and live DOM snapshot (`page.content()`).
|
|
108
|
+
- **High-Speed Semantic DOM Pruning (`DOMPruner`)**: Strips scripts, styles, SVGs, base64 data, and non-semantic wrappers using `selectolax` to slash token usage by 80β90% while keeping semantic landmarks and accessibility roles.
|
|
109
|
+
- **Semantic Vector Memory (`Vector Cache`)**: 384-dimensional dense cosine index matching broken selectors against historical fixes. Resolves recurring failures in **<5ms** with zero LLM API cost.
|
|
110
|
+
- **Strict Anti-Hallucination Guardrails**: Distinguishes between renamed selectors (`RENAME`) and genuine missing features (`MISSING`) to prevent false-positive heals on actual product regressions.
|
|
111
|
+
- **Surgical LibCST AST Patching (`Patcher`)**: Surgically transforms only the target locator expression in the AST without reformatting unrelated code or stripping inline comments.
|
|
112
|
+
- **Automated Verification & Zero-Risk Rollback (`Verifier`)**: Re-runs the patched test in a sandbox; if it fails, immediately restores the code from `.kernhell.bak`.
|
|
113
|
+
- **Mission Control Dashboard (`Next.js App Router`)**: High-tech, dark-mode glassmorphic web dashboard providing live stats, real-time healing stream, semantic memory inspector, and interactive simulation playground.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## π¦ Installation
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Clone the repository
|
|
121
|
+
git clone https://github.com/riteshkumar072/KernHell.git
|
|
122
|
+
cd KernHell
|
|
123
|
+
|
|
124
|
+
# Install Python package in editable mode with all dependencies
|
|
125
|
+
pip install -e ".[all]"
|
|
126
|
+
|
|
127
|
+
# Install Mission Control dashboard dependencies
|
|
128
|
+
cd dashboard
|
|
129
|
+
npm install
|
|
130
|
+
cd ..
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## π οΈ CLI Usage
|
|
136
|
+
|
|
137
|
+
```text
|
|
138
|
+
Usage: kernhell [OPTIONS] COMMAND [ARGS]...
|
|
139
|
+
|
|
140
|
+
ββ Commands βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
141
|
+
β scan Scan a test file to detect broken selectors and capture DOM β
|
|
142
|
+
β snapshots. β
|
|
143
|
+
β heal Autonomously heal broken Playwright selectors in a test file. β
|
|
144
|
+
β dashboard Launch the KernHell Mission Control Dashboard and REST API. β
|
|
145
|
+
β ui Alias for 'dashboard' command. β
|
|
146
|
+
β cache Inspect, query, and manage KernHell's Semantic Vector Cache. β
|
|
147
|
+
β config View and configure KernHell engine parameters. β
|
|
148
|
+
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### 1. Scan a Test File for Broken Selectors
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
kernhell scan tests/test_checkout.py
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 2. Autonomously Heal a Test File or Suite
|
|
158
|
+
|
|
159
|
+
```bash
|
|
160
|
+
# Preview changes without modifying code (dry run)
|
|
161
|
+
kernhell heal tests/test_checkout.py --dry-run
|
|
162
|
+
|
|
163
|
+
# Surgically patch and verify fix with automatic backup
|
|
164
|
+
kernhell heal tests/test_checkout.py
|
|
165
|
+
|
|
166
|
+
# Batch heal all test files in a folder
|
|
167
|
+
kernhell heal tests/
|
|
168
|
+
|
|
169
|
+
# Export machine-readable report for CI/CD pipelines
|
|
170
|
+
kernhell heal tests/ --output json
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 3. Launch the Mission Control Dashboard
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
# Start FastAPI REST API and open dashboard in browser
|
|
177
|
+
kernhell dashboard
|
|
178
|
+
|
|
179
|
+
# Or run Next.js frontend directly:
|
|
180
|
+
cd dashboard
|
|
181
|
+
npm run dev
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Visit **`http://localhost:3000`** to view the live dashboard and interactive playground.
|
|
185
|
+
|
|
186
|
+
### 4. Semantic Vector Memory & Cache
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
# List all verified cached selector mappings
|
|
190
|
+
kernhell cache list
|
|
191
|
+
|
|
192
|
+
# View cache hit/miss ratio and efficiency metrics
|
|
193
|
+
kernhell cache stats
|
|
194
|
+
|
|
195
|
+
# Purge semantic memory
|
|
196
|
+
kernhell cache clear
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 5. Engine Configuration
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
# Display active settings
|
|
203
|
+
kernhell config show
|
|
204
|
+
|
|
205
|
+
# Run the interactive configuration wizard
|
|
206
|
+
kernhell config init
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## π₯οΈ Mission Control Dashboard
|
|
212
|
+
|
|
213
|
+
The dashboard provides a production-grade operations center:
|
|
214
|
+
|
|
215
|
+
- **Overview (`/`)**: Real-time healing metrics (autonomous fixes, cache efficiency, real bugs guarded, dollar savings), live 6-stage pipeline status, and recent activity stream.
|
|
216
|
+
- **Interactive Playground (`/playground`)**: Test healer sandbox with pre-configured refactor scenarios and out-of-stock guardrail alerts.
|
|
217
|
+
- **Semantic Memory Explorer (`/cache`)**: Searchable browser for 384-dimensional dense vector embeddings with hit counts and verification badges.
|
|
218
|
+
- **Run History & Diffs (`/history`)**: Audit trail with side-by-side syntax-highlighted unified diffs of AST code patches.
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## π§ͺ Running the Test Suite
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
# Run all 59 unit and integration tests
|
|
226
|
+
python -m pytest -v
|
|
227
|
+
|
|
228
|
+
# Run with test coverage
|
|
229
|
+
python -m pytest tests/unit -v --cov=src/kernhell
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## πΊοΈ Execution Roadmap
|
|
235
|
+
|
|
236
|
+
- [x] **Weekend 1: Foundation (Scanner, Pruner, Core Models & CLI)**
|
|
237
|
+
- [x] **Weekend 2: AI Brain (Analyzer, Healer, CST Surgical Patcher & Verifier)**
|
|
238
|
+
- [x] **Weekend 3: Semantic Memory (Vector Cache, Embedder, Batch Healing & CI Export)**
|
|
239
|
+
- [x] **Weekend 4: Mission Control Dashboard (Next.js & Production Release)**
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## π License
|
|
244
|
+
|
|
245
|
+
MIT © 2026 Ritesh Kumar β’ KernHell Systems
|