differix 0.1.0a1__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.
- agents/editor.py +455 -0
- agents/researcher.py +405 -0
- agents/reviewer.py +374 -0
- cli/model_cli.py +430 -0
- config.py +557 -0
- differix-0.1.0a1.dist-info/METADATA +123 -0
- differix-0.1.0a1.dist-info/RECORD +62 -0
- differix-0.1.0a1.dist-info/WHEEL +5 -0
- differix-0.1.0a1.dist-info/entry_points.txt +2 -0
- differix-0.1.0a1.dist-info/top_level.txt +6 -0
- main.py +2796 -0
- memory/__init__.py +59 -0
- memory/embeddings/__init__.py +29 -0
- memory/embeddings/factory.py +178 -0
- memory/embeddings/providers.py +259 -0
- memory/manager.py +186 -0
- memory/migrator.py +253 -0
- memory/ranker.py +203 -0
- memory/schemas.py +127 -0
- memory/storage.py +552 -0
- tools/agents_chat.py +152 -0
- tools/asset_extractor.py +545 -0
- tools/asset_manager.py +115 -0
- tools/atomic_changeset.py +264 -0
- tools/backup_manager.py +122 -0
- tools/changelog_engine.py +261 -0
- tools/coherence_checker.py +771 -0
- tools/compiler.py +531 -0
- tools/component_normalizer.py +130 -0
- tools/context_assembler.py +631 -0
- tools/contract_validator.py +478 -0
- tools/cost_predictor.py +141 -0
- tools/drift_guard.py +196 -0
- tools/file_manager.py +119 -0
- tools/handshake.py +351 -0
- tools/lesson_manager.py +570 -0
- tools/lesson_memory_bridge.py +398 -0
- tools/llm_client.py +103 -0
- tools/llm_selector.py +362 -0
- tools/model_ranker.py +535 -0
- tools/palette_extractor.py +1043 -0
- tools/plugin_manager.py +204 -0
- tools/project_context_gatherer.py +404 -0
- tools/project_scanner.py +41 -0
- tools/project_structure.py +244 -0
- tools/reasoning_logger.py +78 -0
- tools/reflection.py +452 -0
- tools/sandbox_utils.py +56 -0
- tools/security/__init__.py +0 -0
- tools/security/behavior_guard.py +83 -0
- tools/security/verification_firewall.py +113 -0
- tools/security_filter.py +213 -0
- tools/session_adr.py +311 -0
- tools/skill_manager.py +129 -0
- tools/state_manager.py +383 -0
- tools/structure_index.py +570 -0
- tools/test_schema_consistency.py +19 -0
- tools/verification_firewall.py +113 -0
- tools/visual_capture.py +65 -0
- tools/visual_diff.py +220 -0
- tools/visual_reviewer.py +79 -0
- tools/web_search.py +371 -0
tools/visual_capture.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from memory.schemas import MemoryStorageError
|
|
5
|
+
|
|
6
|
+
def capture_page(url_or_path: str, output_dir: str, viewport: tuple = (1440, 900)) -> str:
|
|
7
|
+
"""Capture a full-page screenshot of a webpage or local HTML file.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
url_or_path: URL or path to HTML file
|
|
11
|
+
output_dir: Directory to save screenshot
|
|
12
|
+
viewport: Viewport size (width, height)
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Absolute path to saved PNG file
|
|
16
|
+
|
|
17
|
+
Raises:
|
|
18
|
+
MemoryStorageError: If page loading fails
|
|
19
|
+
"""
|
|
20
|
+
try:
|
|
21
|
+
from playwright.sync_api import sync_playwright
|
|
22
|
+
|
|
23
|
+
# Ensure output directory exists
|
|
24
|
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
with sync_playwright() as p:
|
|
27
|
+
browser = p.chromium.launch(headless=True)
|
|
28
|
+
page = browser.new_page()
|
|
29
|
+
|
|
30
|
+
# Set viewport size
|
|
31
|
+
page.set_viewport_size({"width": viewport[0], "height": viewport[1]})
|
|
32
|
+
|
|
33
|
+
# Inject styles to pause animations
|
|
34
|
+
page.add_style_tag(content="""
|
|
35
|
+
*, *::before, *::after {
|
|
36
|
+
animation-play-state: paused !important;
|
|
37
|
+
transition: none !important;
|
|
38
|
+
}
|
|
39
|
+
""")
|
|
40
|
+
|
|
41
|
+
# Load the page
|
|
42
|
+
if os.path.exists(url_or_path):
|
|
43
|
+
page.goto(f"file://{os.path.abspath(url_or_path)}")
|
|
44
|
+
else:
|
|
45
|
+
page.goto(url_or_path)
|
|
46
|
+
|
|
47
|
+
# Wait for fonts to load and add small delay
|
|
48
|
+
page.evaluate("""async () => {
|
|
49
|
+
await document.fonts.ready;
|
|
50
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
51
|
+
}""")
|
|
52
|
+
|
|
53
|
+
# Take screenshot
|
|
54
|
+
output_path = os.path.join(
|
|
55
|
+
output_dir,
|
|
56
|
+
f"{viewport[0]}x{viewport[1]}.png"
|
|
57
|
+
)
|
|
58
|
+
page.screenshot(path=output_path, full_page=True)
|
|
59
|
+
|
|
60
|
+
browser.close()
|
|
61
|
+
|
|
62
|
+
return os.path.abspath(output_path)
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
raise MemoryStorageError(f"Failed to capture page: {str(e)}")
|
tools/visual_diff.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
tools/visual_diff.py
|
|
3
|
+
--------------------
|
|
4
|
+
Visual regression diffing pipeline for the VegeX web-dev plugin.
|
|
5
|
+
|
|
6
|
+
Given a reference screenshot and a freshly captured screenshot, this
|
|
7
|
+
module computes a structural-similarity (SSIM) score, locates the
|
|
8
|
+
regions where the two images diverge, and writes side-by-side crops of
|
|
9
|
+
each divergent region for human inspection.
|
|
10
|
+
|
|
11
|
+
Public API:
|
|
12
|
+
compare(reference_png, current_png, output_dir) -> dict
|
|
13
|
+
|
|
14
|
+
Pipeline:
|
|
15
|
+
1. Load both PNGs with PIL and force RGB.
|
|
16
|
+
2. Resize the current image to match the reference dimensions.
|
|
17
|
+
3. Convert to grayscale with skimage.color.rgb2gray.
|
|
18
|
+
4. Compute SSIM (full=True) with skimage.metrics.structural_similarity.
|
|
19
|
+
5. Binarize the diff map: (1 - ssim_map) > 0.35.
|
|
20
|
+
6. Dilate the mask with scipy.ndimage.binary_dilation(iterations=8).
|
|
21
|
+
7. Label connected components and extract regionprops.
|
|
22
|
+
8. Sort regions by area, keep the top 5 with area > 500.
|
|
23
|
+
9. For each kept region, crop the padded bounding box from both
|
|
24
|
+
images and save them side-by-side to {output_dir}/region_{i}.png.
|
|
25
|
+
10. Return {"ssim": float, "regions": [...]}.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
import os
|
|
32
|
+
from typing import Dict, List
|
|
33
|
+
|
|
34
|
+
import numpy as np
|
|
35
|
+
from PIL import Image
|
|
36
|
+
|
|
37
|
+
from scipy import ndimage
|
|
38
|
+
|
|
39
|
+
from skimage import color
|
|
40
|
+
from skimage.metrics import structural_similarity
|
|
41
|
+
|
|
42
|
+
logger = logging.getLogger(__name__)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Tunable constants (kept module-level so callers/tests can introspect them)
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
DIFF_THRESHOLD: float = 0.35
|
|
49
|
+
DILATION_ITERATIONS: int = 8
|
|
50
|
+
MAX_REGIONS: int = 5
|
|
51
|
+
MIN_REGION_AREA: int = 500
|
|
52
|
+
CROP_PADDING: int = 12
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _load_rgb(path: str) -> Image.Image:
|
|
56
|
+
"""Load an image from disk and force it into RGB mode."""
|
|
57
|
+
if not os.path.isfile(path):
|
|
58
|
+
raise FileNotFoundError(f"Image not found: {path}")
|
|
59
|
+
with Image.open(path) as img:
|
|
60
|
+
return img.convert("RGB")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _pad_bbox(
|
|
64
|
+
x: int,
|
|
65
|
+
y: int,
|
|
66
|
+
w: int,
|
|
67
|
+
h: int,
|
|
68
|
+
width: int,
|
|
69
|
+
height: int,
|
|
70
|
+
padding: int = CROP_PADDING,
|
|
71
|
+
) -> tuple:
|
|
72
|
+
"""Clamp a padded bounding box to the image bounds."""
|
|
73
|
+
x0 = max(0, int(x) - padding)
|
|
74
|
+
y0 = max(0, int(y) - padding)
|
|
75
|
+
x1 = min(width, int(x + w) + padding)
|
|
76
|
+
y1 = min(height, int(y + h) + padding)
|
|
77
|
+
return x0, y0, x1, y1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _side_by_side(reference_crop: Image.Image, current_crop: Image.Image) -> Image.Image:
|
|
81
|
+
"""Combine two crops into a single horizontal strip with a divider."""
|
|
82
|
+
ref_w, ref_h = reference_crop.size
|
|
83
|
+
cur_w, cur_h = current_crop.size
|
|
84
|
+
height = max(ref_h, cur_h)
|
|
85
|
+
gap = 8
|
|
86
|
+
|
|
87
|
+
canvas = Image.new("RGB", (ref_w + gap + cur_w, height), (0, 0, 0))
|
|
88
|
+
canvas.paste(reference_crop, (0, 0))
|
|
89
|
+
canvas.paste(current_crop, (ref_w + gap, 0))
|
|
90
|
+
return canvas
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def compare(reference_png: str, current_png: str, output_dir: str) -> Dict:
|
|
94
|
+
"""
|
|
95
|
+
Compare two PNG screenshots and report the divergent regions.
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
reference_png : str
|
|
100
|
+
Path to the baseline (reference) screenshot.
|
|
101
|
+
current_png : str
|
|
102
|
+
Path to the freshly captured screenshot.
|
|
103
|
+
output_dir : str
|
|
104
|
+
Directory where per-region side-by-side crops are written.
|
|
105
|
+
|
|
106
|
+
Returns
|
|
107
|
+
-------
|
|
108
|
+
dict
|
|
109
|
+
{
|
|
110
|
+
"ssim": float,
|
|
111
|
+
"regions": [
|
|
112
|
+
{"x": int, "y": int, "w": int, "h": int, "score": float},
|
|
113
|
+
...
|
|
114
|
+
],
|
|
115
|
+
}
|
|
116
|
+
"""
|
|
117
|
+
reference_img = _load_rgb(reference_png)
|
|
118
|
+
current_img = _load_rgb(current_png)
|
|
119
|
+
|
|
120
|
+
# Resize the current image to match the reference dimensions.
|
|
121
|
+
if current_img.size != reference_img.size:
|
|
122
|
+
logger.debug(
|
|
123
|
+
"Resizing current image %s -> %s to match reference",
|
|
124
|
+
current_img.size,
|
|
125
|
+
reference_img.size,
|
|
126
|
+
)
|
|
127
|
+
current_img = current_img.resize(reference_img.size, Image.LANCZOS)
|
|
128
|
+
|
|
129
|
+
ref_array = np.asarray(reference_img, dtype=np.float64)
|
|
130
|
+
cur_array = np.asarray(current_img, dtype=np.float64)
|
|
131
|
+
|
|
132
|
+
# Convert to grayscale (skimage expects float in [0, 1]).
|
|
133
|
+
ref_gray = color.rgb2gray(ref_array / 255.0)
|
|
134
|
+
cur_gray = color.rgb2gray(cur_array / 255.0)
|
|
135
|
+
|
|
136
|
+
# Structural similarity — full=True returns the per-pixel SSIM map.
|
|
137
|
+
ssim_score, ssim_map = structural_similarity(
|
|
138
|
+
ref_gray,
|
|
139
|
+
cur_gray,
|
|
140
|
+
full=True,
|
|
141
|
+
data_range=1.0,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Binarize the diff map: where the images disagree strongly.
|
|
145
|
+
diff_map = 1.0 - ssim_map
|
|
146
|
+
binary_mask = diff_map > DIFF_THRESHOLD
|
|
147
|
+
|
|
148
|
+
# Merge nearby pixels into coherent blobs.
|
|
149
|
+
dilated_mask = ndimage.binary_dilation(
|
|
150
|
+
binary_mask,
|
|
151
|
+
iterations=DILATION_ITERATIONS,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Label connected components.
|
|
155
|
+
labeled, num_labels = ndimage.label(dilated_mask)
|
|
156
|
+
|
|
157
|
+
height, width = ref_gray.shape
|
|
158
|
+
regions: List[Dict] = []
|
|
159
|
+
|
|
160
|
+
if num_labels > 0:
|
|
161
|
+
props = ndimage.find_objects(labeled)
|
|
162
|
+
candidates: List[Dict] = []
|
|
163
|
+
for label_index, slice_tuple in enumerate(props, start=1):
|
|
164
|
+
if slice_tuple is None:
|
|
165
|
+
continue
|
|
166
|
+
ys, xs = slice_tuple
|
|
167
|
+
y0, y1 = int(ys.start), int(ys.stop)
|
|
168
|
+
x0, x1 = int(xs.start), int(xs.stop)
|
|
169
|
+
w = x1 - x0
|
|
170
|
+
h = y1 - y0
|
|
171
|
+
area = int(np.count_nonzero(labeled[slice_tuple] == label_index))
|
|
172
|
+
if area <= MIN_REGION_AREA:
|
|
173
|
+
continue
|
|
174
|
+
region_diff = diff_map[slice_tuple]
|
|
175
|
+
score = float(np.mean(region_diff)) if region_diff.size else 0.0
|
|
176
|
+
candidates.append(
|
|
177
|
+
{
|
|
178
|
+
"x": x0,
|
|
179
|
+
"y": y0,
|
|
180
|
+
"w": w,
|
|
181
|
+
"h": h,
|
|
182
|
+
"area": area,
|
|
183
|
+
"score": score,
|
|
184
|
+
}
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
candidates.sort(key=lambda r: r["area"], reverse=True)
|
|
188
|
+
candidates = candidates[:MAX_REGIONS]
|
|
189
|
+
|
|
190
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
191
|
+
|
|
192
|
+
for index, region in enumerate(candidates):
|
|
193
|
+
px0, py0, px1, py1 = _pad_bbox(
|
|
194
|
+
region["x"],
|
|
195
|
+
region["y"],
|
|
196
|
+
region["w"],
|
|
197
|
+
region["h"],
|
|
198
|
+
width,
|
|
199
|
+
height,
|
|
200
|
+
)
|
|
201
|
+
ref_crop = reference_img.crop((px0, py0, px1, py1))
|
|
202
|
+
cur_crop = current_img.crop((px0, py0, px1, py1))
|
|
203
|
+
composite = _side_by_side(ref_crop, cur_crop)
|
|
204
|
+
out_path = os.path.join(output_dir, f"region_{index}.png")
|
|
205
|
+
composite.save(out_path)
|
|
206
|
+
logger.debug("Wrote diff region crop: %s", out_path)
|
|
207
|
+
|
|
208
|
+
regions.append(
|
|
209
|
+
{
|
|
210
|
+
"x": region["x"],
|
|
211
|
+
"y": region["y"],
|
|
212
|
+
"w": region["w"],
|
|
213
|
+
"h": region["h"],
|
|
214
|
+
"score": region["score"],
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
else:
|
|
218
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
219
|
+
|
|
220
|
+
return {"ssim": float(ssim_score), "regions": regions}
|
tools/visual_reviewer.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import Optional, Dict
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
def review_page(html_path: str, reference_png: Optional[str], output_dir: str) -> Dict:
|
|
6
|
+
"""Review an HTML page by comparing it visually to a reference image.
|
|
7
|
+
|
|
8
|
+
Args:
|
|
9
|
+
html_path: Path to HTML file to review
|
|
10
|
+
reference_png: Optional path to reference PNG for comparison
|
|
11
|
+
output_dir: Directory to save diff results and artifacts
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
Dict with review results containing:
|
|
15
|
+
- ok: bool - True if visual match passes threshold or no reference
|
|
16
|
+
- ssim: Optional[float] - Structural similarity score (0-1)
|
|
17
|
+
- regions: Optional[list] - Diff regions if ssim < 0.85
|
|
18
|
+
- feedback: str - Human-readable summary of results
|
|
19
|
+
"""
|
|
20
|
+
from tools.visual_capture import capture_page
|
|
21
|
+
from tools.visual_diff import compare
|
|
22
|
+
|
|
23
|
+
# Ensure output dir exists
|
|
24
|
+
Path(output_dir).mkdir(parents=True, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
# Capture current page
|
|
27
|
+
try:
|
|
28
|
+
captured_png = os.path.join(output_dir, "current.png")
|
|
29
|
+
capture_page(html_path, captured_png, viewport=(1440, 900))
|
|
30
|
+
except Exception as e:
|
|
31
|
+
return {
|
|
32
|
+
"ok": False,
|
|
33
|
+
"ssim": None,
|
|
34
|
+
"feedback": f"Failed to capture page: {str(e)}"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
# Handle missing reference case
|
|
38
|
+
if not reference_png or not os.path.exists(reference_png):
|
|
39
|
+
return {
|
|
40
|
+
"ok": True,
|
|
41
|
+
"ssim": None,
|
|
42
|
+
"feedback": "no reference"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Compare to reference
|
|
46
|
+
try:
|
|
47
|
+
diff_result = compare(reference_png, captured_png, output_dir)
|
|
48
|
+
ssim = diff_result.get("ssim", 0)
|
|
49
|
+
|
|
50
|
+
if ssim >= 0.85:
|
|
51
|
+
return {
|
|
52
|
+
"ok": True,
|
|
53
|
+
"ssim": ssim,
|
|
54
|
+
"feedback": ""
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Generate feedback for failed diff
|
|
58
|
+
regions = diff_result.get("regions", [])
|
|
59
|
+
top_regions = sorted(regions, key=lambda r: r["score"])[:3]
|
|
60
|
+
|
|
61
|
+
feedback_lines = ["Visual differences detected:"]
|
|
62
|
+
for i, region in enumerate(top_regions, 1):
|
|
63
|
+
x, y = region["x"], region["y"]
|
|
64
|
+
score = region["score"]
|
|
65
|
+
feedback_lines.append(f"{i}. Diff at region {x},{y} (score {score:.2f})")
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
"ok": False,
|
|
69
|
+
"ssim": ssim,
|
|
70
|
+
"regions": regions,
|
|
71
|
+
"feedback": "\n".join(feedback_lines)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
return {
|
|
76
|
+
"ok": False,
|
|
77
|
+
"ssim": None,
|
|
78
|
+
"feedback": f"Diff failed: {str(e)}"
|
|
79
|
+
}
|