cursorflow 2.4.2__tar.gz → 2.5.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.
Files changed (53) hide show
  1. {cursorflow-2.4.2 → cursorflow-2.5.0}/PKG-INFO +8 -6
  2. {cursorflow-2.4.2 → cursorflow-2.5.0}/README.md +7 -5
  3. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/cli.py +13 -22
  4. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/browser_controller.py +5 -31
  5. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/cursorflow.py +3 -13
  6. cursorflow-2.5.0/cursorflow/core/json_utils.py +53 -0
  7. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/mockup_comparator.py +35 -59
  8. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/rules/cursorflow-usage.mdc +25 -0
  9. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow.egg-info/SOURCES.txt +1 -0
  10. {cursorflow-2.4.2 → cursorflow-2.5.0}/docs/user/USAGE_GUIDE.md +198 -0
  11. cursorflow-2.5.0/examples/mockup_comparison_example.py +195 -0
  12. {cursorflow-2.4.2 → cursorflow-2.5.0}/pyproject.toml +2 -2
  13. cursorflow-2.4.2/examples/mockup_comparison_example.py +0 -316
  14. {cursorflow-2.4.2 → cursorflow-2.5.0}/LICENSE +0 -0
  15. {cursorflow-2.4.2 → cursorflow-2.5.0}/MANIFEST.in +0 -0
  16. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/__init__.py +0 -0
  17. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/auto_init.py +0 -0
  18. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/auto_updater.py +0 -0
  19. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/action_validator.py +0 -0
  20. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/agent.py +0 -0
  21. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/auth_handler.py +0 -0
  22. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/browser_engine.py +0 -0
  23. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/config_validator.py +0 -0
  24. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/css_iterator.py +0 -0
  25. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/cursor_integration.py +0 -0
  26. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/error_context_collector.py +0 -0
  27. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/error_correlator.py +0 -0
  28. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/event_correlator.py +0 -0
  29. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/file_change_monitor.py +0 -0
  30. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/hmr_detector.py +0 -0
  31. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/log_collector.py +0 -0
  32. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/log_monitor.py +0 -0
  33. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/persistent_session.py +0 -0
  34. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/report_generator.py +0 -0
  35. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/core/trace_manager.py +0 -0
  36. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/install_cursorflow_rules.py +0 -0
  37. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/log_sources/local_file.py +0 -0
  38. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/log_sources/ssh_remote.py +0 -0
  39. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/post_install.py +0 -0
  40. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/rules/__init__.py +0 -0
  41. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/rules/cursorflow-installation.mdc +0 -0
  42. {cursorflow-2.4.2 → cursorflow-2.5.0}/cursorflow/updater.py +0 -0
  43. {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/comprehensive_screenshot_example.py +0 -0
  44. {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/element_inspection_example.py +0 -0
  45. {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/element_measurement_example.py +0 -0
  46. {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/enhanced_screenshot_example.py +0 -0
  47. {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/hot_reload_css_iteration.py +0 -0
  48. {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/opensas_example.py +0 -0
  49. {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/react_example.py +0 -0
  50. {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/responsive_testing_example.py +0 -0
  51. {cursorflow-2.4.2 → cursorflow-2.5.0}/examples/v2_comprehensive_demo.py +0 -0
  52. {cursorflow-2.4.2 → cursorflow-2.5.0}/setup.cfg +0 -0
  53. {cursorflow-2.4.2 → cursorflow-2.5.0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cursorflow
3
- Version: 2.4.2
3
+ Version: 2.5.0
4
4
  Summary: 🔥 Complete page intelligence for AI-driven development with Hot Reload Intelligence - captures DOM, network, console, performance, HMR events, and comprehensive page analysis
5
5
  Author-email: GeekWarrior Development <rbush@cooltheory.com>
6
6
  License-Expression: MIT
@@ -370,20 +370,22 @@ cursorflow test --base-url http://localhost:3000 --responsive --actions '[
370
370
  cursorflow test --base-url http://localhost:3000 --path "/api" --output "api-test-results.json"
371
371
  ```
372
372
 
373
- ### **Design Comparison**
373
+ ### **Design Comparison** (Pure Measurement)
374
374
  ```bash
375
- # Compare mockup to implementation
375
+ # Compare mockup to implementation - get similarity metrics
376
376
  cursorflow compare-mockup https://mockup.com/design \
377
377
  --base-url http://localhost:3000 \
378
- --mockup-actions '[{"navigate": "/"}]' \
379
378
  --implementation-actions '[{"navigate": "/dashboard"}]'
379
+ # Output: 87.3% similarity, diff images, element measurements
380
380
 
381
- # CSS iteration with HMR intelligence
381
+ # Test CSS variations - observe real rendering
382
382
  cursorflow iterate-mockup https://mockup.com/design \
383
383
  --base-url http://localhost:5173 \
384
384
  --css-improvements '[
385
- {"name": "fix-spacing", "css": ".container { gap: 2rem; }"}
385
+ {"name": "spacing-fix", "css": ".container { gap: 2rem; }"},
386
+ {"name": "tighter-spacing", "css": ".container { gap: 1rem; }"}
386
387
  ]'
388
+ # Output: Similarity data for each variation (Cursor decides which to apply)
387
389
  ```
388
390
 
389
391
  ### **Element Analysis & CSS Debugging**
@@ -325,20 +325,22 @@ cursorflow test --base-url http://localhost:3000 --responsive --actions '[
325
325
  cursorflow test --base-url http://localhost:3000 --path "/api" --output "api-test-results.json"
326
326
  ```
327
327
 
328
- ### **Design Comparison**
328
+ ### **Design Comparison** (Pure Measurement)
329
329
  ```bash
330
- # Compare mockup to implementation
330
+ # Compare mockup to implementation - get similarity metrics
331
331
  cursorflow compare-mockup https://mockup.com/design \
332
332
  --base-url http://localhost:3000 \
333
- --mockup-actions '[{"navigate": "/"}]' \
334
333
  --implementation-actions '[{"navigate": "/dashboard"}]'
334
+ # Output: 87.3% similarity, diff images, element measurements
335
335
 
336
- # CSS iteration with HMR intelligence
336
+ # Test CSS variations - observe real rendering
337
337
  cursorflow iterate-mockup https://mockup.com/design \
338
338
  --base-url http://localhost:5173 \
339
339
  --css-improvements '[
340
- {"name": "fix-spacing", "css": ".container { gap: 2rem; }"}
340
+ {"name": "spacing-fix", "css": ".container { gap: 2rem; }"},
341
+ {"name": "tighter-spacing", "css": ".container { gap: 1rem; }"}
341
342
  ]'
343
+ # Output: Similarity data for each variation (Cursor decides which to apply)
342
344
  ```
343
345
 
344
346
  ### **Element Analysis & CSS Debugging**
@@ -357,13 +357,11 @@ def test(base_url, path, actions, output, logs, config, verbose, headless, timeo
357
357
  help='JSON array of viewports to test: [{"width": 1440, "height": 900, "name": "desktop"}]')
358
358
  @click.option('--diff-threshold', '-t', type=float, default=0.1,
359
359
  help='Visual difference threshold (0.0-1.0)')
360
- @click.option('--block-tracking', is_flag=True,
361
- help='Block tracking scripts (Google Analytics, Facebook Pixel, etc.) to prevent timeout')
362
360
  @click.option('--output', '-o', default='mockup_comparison_results.json',
363
361
  help='Output file for comparison results')
364
362
  @click.option('--verbose', is_flag=True,
365
363
  help='Verbose output')
366
- def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions, viewports, diff_threshold, block_tracking, output, verbose):
364
+ def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions, viewports, diff_threshold, output, verbose):
367
365
  """Compare mockup design to work-in-progress implementation"""
368
366
 
369
367
  console.print(f"🎨 Comparing mockup [blue]{mockup_url}[/blue] to implementation [blue]{base_url}[/blue]")
@@ -407,16 +405,10 @@ def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions,
407
405
  try:
408
406
  from .core.cursorflow import CursorFlow
409
407
 
410
- # Configure browser with tracking blocking if requested
411
- browser_config = {'headless': True}
412
- if block_tracking:
413
- browser_config['block_tracking'] = True
414
- console.print("🚫 Blocking tracking scripts (Google Analytics, Facebook Pixel, etc.)")
415
-
416
408
  flow = CursorFlow(
417
409
  base_url=base_url,
418
410
  log_config={'source': 'local', 'paths': ['logs/app.log']},
419
- browser_config=browser_config
411
+ browser_config={'headless': True}
420
412
  )
421
413
  except Exception as e:
422
414
  console.print(f"[red]Error initializing CursorFlow: {e}[/red]")
@@ -436,22 +428,20 @@ def compare_mockup(mockup_url, base_url, mockup_actions, implementation_actions,
436
428
  console.print(f"[red]❌ Comparison failed: {results['error']}[/red]")
437
429
  return
438
430
 
439
- # Display results summary
431
+ # Display results summary (pure metrics only)
440
432
  summary = results.get('summary', {})
441
433
  console.print(f"✅ Comparison completed: {results.get('comparison_id', 'unknown')}")
442
434
  console.print(f"📊 Average similarity: [bold]{summary.get('average_similarity', 0)}%[/bold]")
443
435
  console.print(f"📱 Viewports tested: {summary.get('viewports_tested', 0)}")
444
436
 
445
- # Show recommendations
446
- recommendations = results.get('recommendations', [])
447
- if recommendations:
448
- console.print(f"💡 Recommendations: {len(recommendations)} improvements suggested")
449
- for i, rec in enumerate(recommendations[:3]): # Show first 3
450
- console.print(f" {i+1}. {rec.get('description', 'No description')}")
437
+ # Show similarity range
438
+ similarity_range = summary.get('similarity_range', {})
439
+ if similarity_range:
440
+ console.print(f"📈 Similarity range: {similarity_range.get('min', 0)}% - {similarity_range.get('max', 0)}%")
451
441
 
452
- # Save results
453
- with open(output, 'w') as f:
454
- json.dump(results, f, indent=2, default=str)
442
+ # Save results with safe serialization
443
+ from .core.json_utils import safe_json_dump
444
+ safe_json_dump(results, output)
455
445
 
456
446
  console.print(f"💾 Full results saved to: [cyan]{output}[/cyan]")
457
447
  console.print(f"📁 Visual diffs stored in: [cyan].cursorflow/artifacts/[/cyan]")
@@ -552,9 +542,10 @@ def iterate_mockup(mockup_url, base_url, css_improvements, base_actions, diff_th
552
542
  for i, rec in enumerate(recommendations[:3]):
553
543
  console.print(f" {i+1}. {rec.get('description', 'No description')}")
554
544
 
555
- # Save results
545
+ # Save results with numpy type handling
546
+ from cursorflow.core.json_utils import safe_json_serialize
556
547
  with open(output, 'w') as f:
557
- json.dump(results, f, indent=2, default=str)
548
+ json.dump(results, f, indent=2, default=safe_json_serialize)
558
549
 
559
550
  console.print(f"💾 Full results saved to: [cyan]{output}[/cyan]")
560
551
  console.print(f"📁 Iteration progress stored in: [cyan].cursorflow/artifacts/[/cyan]")
@@ -131,9 +131,9 @@ class BrowserController:
131
131
  self.context = await self.browser.new_context(**context_config)
132
132
  self.page = await self.context.new_page()
133
133
 
134
- # Block tracking scripts if requested (prevents timeout on pages with analytics)
135
- if self.config.get("block_tracking", False):
136
- await self._setup_tracking_blocker()
134
+ # Note: We do NOT block tracking scripts by default
135
+ # CursorFlow philosophy: "Capture reality, not fiction"
136
+ # Blocking scripts would alter the actual page behavior we're measuring
137
137
 
138
138
  # v2.0 Enhancement: Initialize Error Context Collector
139
139
  self.error_context_collector = ErrorContextCollector(self.page, self.logger)
@@ -173,33 +173,6 @@ class BrowserController:
173
173
 
174
174
  raise
175
175
 
176
- async def _setup_tracking_blocker(self):
177
- """Block common tracking scripts to prevent networkidle timeout"""
178
- tracking_patterns = [
179
- "*google-analytics.com/*",
180
- "*googletagmanager.com/*",
181
- "*facebook.com/tr/*",
182
- "*facebook.net/*",
183
- "*doubleclick.net/*",
184
- "*analytics.google.com/*",
185
- "*hotjar.com/*",
186
- "*mixpanel.com/*",
187
- "*segment.com/*",
188
- "*googleadservices.com/*",
189
- "*connect.facebook.net/*",
190
- "*/analytics/*",
191
- "*/tracking/*"
192
- ]
193
-
194
- async def block_tracking(route):
195
- """Block tracking request"""
196
- await route.abort()
197
-
198
- for pattern in tracking_patterns:
199
- await self.page.route(pattern, block_tracking)
200
-
201
- self.logger.info("🚫 Tracking scripts blocked")
202
-
203
176
  async def _setup_event_listeners(self):
204
177
  """Set up universal event listeners for any framework"""
205
178
 
@@ -388,8 +361,9 @@ class BrowserController:
388
361
  self.logger.info(f"Navigating to: {url}")
389
362
 
390
363
  # Navigate and wait
364
+ # Use 'load' instead of 'networkidle' to avoid timeout on pages with tracking scripts
391
365
  if wait_for_load:
392
- await self.page.goto(url, wait_until="networkidle", timeout=30000)
366
+ await self.page.goto(url, wait_until="load", timeout=30000)
393
367
  else:
394
368
  await self.page.goto(url, timeout=30000)
395
369
 
@@ -575,19 +575,9 @@ class CursorFlow:
575
575
  if "error" in raw_results:
576
576
  return raw_results
577
577
 
578
- # Format results for Cursor analysis
579
- session_id = f"mockup_comparison_{int(time.time())}"
580
- cursor_results = self.cursor_integration.format_mockup_comparison_results(
581
- raw_results=raw_results,
582
- session_id=session_id,
583
- project_context={
584
- "framework": "auto-detected",
585
- "base_url": self.base_url,
586
- "test_type": "mockup_comparison"
587
- }
588
- )
589
-
590
- return cursor_results
578
+ # Return raw measurement data directly (pure observation)
579
+ # No interpretation, no analysis - Cursor makes decisions based on data
580
+ return raw_results
591
581
 
592
582
  except Exception as e:
593
583
  self.logger.error(f"Mockup comparison failed: {e}")
@@ -0,0 +1,53 @@
1
+ """
2
+ JSON Serialization Utilities
3
+
4
+ Handles conversion of numpy and other non-standard types to JSON-safe Python types.
5
+ Essential for mockup comparison and any feature using PIL/numpy for image analysis.
6
+ """
7
+
8
+ import json
9
+ from typing import Any
10
+ from pathlib import Path
11
+
12
+
13
+ def safe_json_serialize(obj: Any) -> Any:
14
+ """
15
+ Convert any Python/numpy type to JSON-safe type
16
+
17
+ Handles:
18
+ - numpy scalars (bool_, int_, float_)
19
+ - numpy arrays (ndarray)
20
+ - Other non-serializable types (converts to string)
21
+
22
+ Args:
23
+ obj: Any object that might not be JSON serializable
24
+
25
+ Returns:
26
+ JSON-safe Python type
27
+ """
28
+ # numpy scalars (bool_, int64, float64, etc.)
29
+ if hasattr(obj, 'item'):
30
+ return obj.item()
31
+
32
+ # numpy arrays
33
+ if hasattr(obj, 'tolist'):
34
+ return obj.tolist()
35
+
36
+ # Fallback for anything else
37
+ return str(obj)
38
+
39
+
40
+ def safe_json_dump(data: Any, file_path: str, indent: int = 2) -> None:
41
+ """
42
+ Safely dump data to JSON file with numpy type handling
43
+
44
+ Args:
45
+ data: Data to serialize
46
+ file_path: Path to output file
47
+ indent: JSON indentation level
48
+ """
49
+ file_path = Path(file_path)
50
+ file_path.parent.mkdir(parents=True, exist_ok=True)
51
+
52
+ with open(file_path, 'w') as f:
53
+ json.dump(data, f, indent=indent, default=safe_json_serialize)
@@ -11,6 +11,8 @@ import json
11
11
  from typing import Dict, List, Optional, Any, Tuple
12
12
  from pathlib import Path
13
13
  import logging
14
+
15
+ from .json_utils import safe_json_dump, safe_json_serialize
14
16
  try:
15
17
  from PIL import Image, ImageDraw, ImageChops
16
18
  import numpy as np
@@ -107,9 +109,14 @@ class MockupComparator:
107
109
  await mockup_browser.set_viewport(viewport["width"], viewport["height"])
108
110
  await implementation_browser.set_viewport(viewport["width"], viewport["height"])
109
111
 
110
- # Navigate to initial pages
112
+ # Navigate to initial pages with stabilization
111
113
  await mockup_browser.navigate("/")
114
+ await asyncio.sleep(1)
115
+ await mockup_browser.page.wait_for_load_state("domcontentloaded")
116
+
112
117
  await implementation_browser.navigate("/")
118
+ await asyncio.sleep(1)
119
+ await implementation_browser.page.wait_for_load_state("domcontentloaded")
113
120
 
114
121
  # Execute any required actions on mockup
115
122
  if mockup_actions:
@@ -119,6 +126,9 @@ class MockupComparator:
119
126
  if implementation_actions:
120
127
  await self._execute_actions_on_browser(implementation_browser, implementation_actions)
121
128
 
129
+ # Final stabilization before capturing
130
+ await asyncio.sleep(0.5)
131
+
122
132
  # Capture screenshots
123
133
  mockup_screenshot = await self._capture_comparison_screenshot(
124
134
  mockup_browser, f"{comparison_name}_mockup_{viewport_name}"
@@ -163,14 +173,12 @@ class MockupComparator:
163
173
  "implementation_url": implementation_url,
164
174
  "viewports_tested": len(viewports),
165
175
  "results": comparison_results,
166
- "summary": self._create_comparison_summary(comparison_results),
167
- "recommendations": self._generate_improvement_recommendations(comparison_results)
176
+ "summary": self._create_comparison_summary(comparison_results)
168
177
  }
169
178
 
170
- # Save comparison report
179
+ # Save comparison report with safe serialization
171
180
  report_path = self.artifacts_base / "mockup_comparisons" / f"{comparison_name}.json"
172
- with open(report_path, 'w') as f:
173
- json.dump(comparison_report, f, indent=2, default=str)
181
+ safe_json_dump(comparison_report, str(report_path))
174
182
 
175
183
  self.logger.info(f"Mockup comparison completed: {comparison_name}")
176
184
  return comparison_report
@@ -288,10 +296,9 @@ class MockupComparator:
288
296
  "final_recommendations": self._generate_final_recommendations(iteration_results)
289
297
  }
290
298
 
291
- # Save iteration report
299
+ # Save iteration report with safe JSON serialization
292
300
  report_path = self.artifacts_base / "iteration_progress" / f"{iteration_session_id}.json"
293
- with open(report_path, 'w') as f:
294
- json.dump(iteration_report, f, indent=2, default=str)
301
+ safe_json_dump(iteration_report, str(report_path))
295
302
 
296
303
  self.logger.info(f"UI matching iteration completed: {iteration_session_id}")
297
304
  return iteration_report
@@ -309,11 +316,15 @@ class MockupComparator:
309
316
  if "navigate" in action:
310
317
  path = action["navigate"]
311
318
  await browser.navigate(path)
319
+ # Wait for page to stabilize after navigation
320
+ await asyncio.sleep(1) # Brief pause for dynamic content rendering
321
+ await browser.page.wait_for_load_state("domcontentloaded")
312
322
  elif "click" in action:
313
323
  selector = action["click"]
314
324
  if isinstance(selector, dict):
315
325
  selector = selector["selector"]
316
326
  await browser.click(selector)
327
+ await asyncio.sleep(0.5) # Brief pause for UI response
317
328
  elif "wait_for" in action:
318
329
  selector = action["wait_for"]
319
330
  if isinstance(selector, dict):
@@ -324,6 +335,7 @@ class MockupComparator:
324
335
  elif "scroll" in action:
325
336
  scroll_config = action["scroll"]
326
337
  await browser.page.evaluate(f"window.scrollTo({scroll_config.get('x', 0)}, {scroll_config.get('y', 0)})")
338
+ await asyncio.sleep(0.3) # Brief pause for scroll rendering
327
339
 
328
340
  async def _capture_comparison_screenshot(self, browser: BrowserController, name: str) -> str:
329
341
  """Capture screenshot for comparison"""
@@ -1194,11 +1206,11 @@ class MockupComparator:
1194
1206
  improvement = improved_similarity - baseline_similarity
1195
1207
 
1196
1208
  return {
1197
- "baseline_similarity": baseline_similarity,
1198
- "improved_similarity": improved_similarity,
1199
- "improvement": improvement,
1200
- "improvement_percentage": round(improvement, 2),
1201
- "is_improvement": improvement > 0
1209
+ "baseline_similarity": float(baseline_similarity),
1210
+ "improved_similarity": float(improved_similarity),
1211
+ "improvement": float(improvement),
1212
+ "improvement_percentage": round(float(improvement), 2),
1213
+ "is_improvement": bool(improvement > 0) # Convert to Python bool
1202
1214
  }
1203
1215
 
1204
1216
  except Exception as e:
@@ -1218,55 +1230,19 @@ class MockupComparator:
1218
1230
 
1219
1231
  avg_similarity = sum(similarities) / len(similarities) if similarities else 0
1220
1232
 
1233
+ # Pure data summary - no interpretation
1221
1234
  return {
1222
- "average_similarity": round(avg_similarity, 2),
1235
+ "average_similarity": round(float(avg_similarity), 2),
1223
1236
  "viewports_tested": len(comparison_results),
1224
- "best_viewport_match": max(comparison_results, key=lambda x: x.get("visual_diff", {}).get("similarity_score", 0)),
1225
- "needs_improvement": avg_similarity < 80 # Threshold for "good enough"
1237
+ "similarity_by_viewport": [
1238
+ {
1239
+ "viewport": result.get("viewport", {}).get("name", "unknown"),
1240
+ "similarity": float(result.get("visual_diff", {}).get("similarity_score", 0))
1241
+ }
1242
+ for result in comparison_results
1243
+ ]
1226
1244
  }
1227
1245
 
1228
- def _generate_improvement_recommendations(self, comparison_results: List[Dict]) -> List[Dict]:
1229
- """Generate recommendations for improving implementation to match mockup"""
1230
- recommendations = []
1231
-
1232
- for result in comparison_results:
1233
- viewport = result.get("viewport", {})
1234
- layout_analysis = result.get("layout_analysis", {})
1235
- visual_diff = result.get("visual_diff", {})
1236
-
1237
- # Analyze layout differences for recommendations
1238
- differences = layout_analysis.get("differences", [])
1239
-
1240
- for diff in differences:
1241
- if diff["type"] == "missing_in_implementation":
1242
- recommendations.append({
1243
- "type": "add_element",
1244
- "priority": "high",
1245
- "description": f"Add missing element: {diff['selector']}",
1246
- "viewport": viewport.get("name", "unknown")
1247
- })
1248
- elif diff["type"] == "property_differences":
1249
- for prop, prop_diff in diff.get("differences", {}).items():
1250
- recommendations.append({
1251
- "type": "adjust_property",
1252
- "priority": "medium",
1253
- "description": f"Adjust {prop} for {diff['selector']}",
1254
- "current_value": prop_diff.get("implementation"),
1255
- "target_value": prop_diff.get("mockup"),
1256
- "viewport": viewport.get("name", "unknown")
1257
- })
1258
-
1259
- # Add visual similarity recommendations
1260
- similarity = visual_diff.get("similarity_score", 0)
1261
- if similarity < 70:
1262
- recommendations.append({
1263
- "type": "major_visual_changes",
1264
- "priority": "high",
1265
- "description": f"Significant visual differences detected (similarity: {similarity}%)",
1266
- "viewport": viewport.get("name", "unknown")
1267
- })
1268
-
1269
- return recommendations
1270
1246
 
1271
1247
  def _create_iteration_summary(self, baseline_comparison: Dict, iteration_results: List[Dict]) -> Dict[str, Any]:
1272
1248
  """Create summary of iteration session"""
@@ -425,6 +425,31 @@ cursorflow measure -u http://localhost:3000 -s "#panel"
425
425
  # Output: 532w × 900h ✅
426
426
  ```
427
427
 
428
+ ### Visual Comparison & Iteration
429
+ ```bash
430
+ # Compare mockup to implementation (pure measurement)
431
+ cursorflow compare-mockup MOCKUP_URL -u BASE_URL -ia '[{"navigate": "/page"}]'
432
+ # Output: Similarity percentage, diff images, element data
433
+
434
+ # Test CSS variations (observe real rendering)
435
+ cursorflow iterate-mockup MOCKUP_URL -u BASE_URL --css-improvements '[
436
+ {"name": "test1", "css": ".header { padding: 2rem; }"},
437
+ {"name": "test2", "css": ".header { padding: 1rem; }"}
438
+ ]'
439
+ # Output: Similarity for each variation - Cursor decides which to apply
440
+ ```
441
+
442
+ **Philosophy**:
443
+ - CursorFlow observes mockup and implementation (both are reality)
444
+ - Provides quantified measurements (similarity %, diff images, element data)
445
+ - Temporarily injects CSS to observe what reality WOULD look like
446
+ - Cursor analyzes the data and makes decisions
447
+
448
+ **When to use**:
449
+ - User has design mockup and needs to match it
450
+ - Testing multiple CSS approaches before applying
451
+ - Measuring progress toward design specifications
452
+
428
453
  ## Analyzing Results
429
454
 
430
455
  ### **Hot Reload CSS Iteration Results**
@@ -24,6 +24,7 @@ cursorflow/core/error_correlator.py
24
24
  cursorflow/core/event_correlator.py
25
25
  cursorflow/core/file_change_monitor.py
26
26
  cursorflow/core/hmr_detector.py
27
+ cursorflow/core/json_utils.py
27
28
  cursorflow/core/log_collector.py
28
29
  cursorflow/core/log_monitor.py
29
30
  cursorflow/core/mockup_comparator.py