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.
Files changed (62) hide show
  1. agents/editor.py +455 -0
  2. agents/researcher.py +405 -0
  3. agents/reviewer.py +374 -0
  4. cli/model_cli.py +430 -0
  5. config.py +557 -0
  6. differix-0.1.0a1.dist-info/METADATA +123 -0
  7. differix-0.1.0a1.dist-info/RECORD +62 -0
  8. differix-0.1.0a1.dist-info/WHEEL +5 -0
  9. differix-0.1.0a1.dist-info/entry_points.txt +2 -0
  10. differix-0.1.0a1.dist-info/top_level.txt +6 -0
  11. main.py +2796 -0
  12. memory/__init__.py +59 -0
  13. memory/embeddings/__init__.py +29 -0
  14. memory/embeddings/factory.py +178 -0
  15. memory/embeddings/providers.py +259 -0
  16. memory/manager.py +186 -0
  17. memory/migrator.py +253 -0
  18. memory/ranker.py +203 -0
  19. memory/schemas.py +127 -0
  20. memory/storage.py +552 -0
  21. tools/agents_chat.py +152 -0
  22. tools/asset_extractor.py +545 -0
  23. tools/asset_manager.py +115 -0
  24. tools/atomic_changeset.py +264 -0
  25. tools/backup_manager.py +122 -0
  26. tools/changelog_engine.py +261 -0
  27. tools/coherence_checker.py +771 -0
  28. tools/compiler.py +531 -0
  29. tools/component_normalizer.py +130 -0
  30. tools/context_assembler.py +631 -0
  31. tools/contract_validator.py +478 -0
  32. tools/cost_predictor.py +141 -0
  33. tools/drift_guard.py +196 -0
  34. tools/file_manager.py +119 -0
  35. tools/handshake.py +351 -0
  36. tools/lesson_manager.py +570 -0
  37. tools/lesson_memory_bridge.py +398 -0
  38. tools/llm_client.py +103 -0
  39. tools/llm_selector.py +362 -0
  40. tools/model_ranker.py +535 -0
  41. tools/palette_extractor.py +1043 -0
  42. tools/plugin_manager.py +204 -0
  43. tools/project_context_gatherer.py +404 -0
  44. tools/project_scanner.py +41 -0
  45. tools/project_structure.py +244 -0
  46. tools/reasoning_logger.py +78 -0
  47. tools/reflection.py +452 -0
  48. tools/sandbox_utils.py +56 -0
  49. tools/security/__init__.py +0 -0
  50. tools/security/behavior_guard.py +83 -0
  51. tools/security/verification_firewall.py +113 -0
  52. tools/security_filter.py +213 -0
  53. tools/session_adr.py +311 -0
  54. tools/skill_manager.py +129 -0
  55. tools/state_manager.py +383 -0
  56. tools/structure_index.py +570 -0
  57. tools/test_schema_consistency.py +19 -0
  58. tools/verification_firewall.py +113 -0
  59. tools/visual_capture.py +65 -0
  60. tools/visual_diff.py +220 -0
  61. tools/visual_reviewer.py +79 -0
  62. tools/web_search.py +371 -0
@@ -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}
@@ -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
+ }