cursorflow 2.6.2__py3-none-any.whl → 2.7.1__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.
- cursorflow/cli.py +371 -38
- cursorflow/core/cursorflow.py +8 -1
- cursorflow/core/data_presenter.py +518 -0
- cursorflow/core/output_manager.py +523 -0
- cursorflow/core/query_engine.py +1345 -0
- cursorflow/rules/cursorflow-usage.mdc +245 -6
- {cursorflow-2.6.2.dist-info → cursorflow-2.7.1.dist-info}/METADATA +59 -1
- {cursorflow-2.6.2.dist-info → cursorflow-2.7.1.dist-info}/RECORD +12 -9
- {cursorflow-2.6.2.dist-info → cursorflow-2.7.1.dist-info}/WHEEL +0 -0
- {cursorflow-2.6.2.dist-info → cursorflow-2.7.1.dist-info}/entry_points.txt +0 -0
- {cursorflow-2.6.2.dist-info → cursorflow-2.7.1.dist-info}/licenses/LICENSE +0 -0
- {cursorflow-2.6.2.dist-info → cursorflow-2.7.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,518 @@
|
|
1
|
+
"""
|
2
|
+
Data Presenter - AI-Optimized Data Organization
|
3
|
+
|
4
|
+
Presents test data in structured markdown format optimized for AI consumption.
|
5
|
+
Pure data organization without analysis or recommendations.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from pathlib import Path
|
9
|
+
from typing import Dict, Any
|
10
|
+
import json
|
11
|
+
from datetime import datetime
|
12
|
+
|
13
|
+
|
14
|
+
class DataPresenter:
|
15
|
+
"""
|
16
|
+
Generates AI-optimized markdown presentation of test data.
|
17
|
+
|
18
|
+
Philosophy: Organize and present raw data clearly for AI analysis.
|
19
|
+
No subjective recommendations, no analysis - just structured information.
|
20
|
+
"""
|
21
|
+
|
22
|
+
def generate_data_digest(
|
23
|
+
self,
|
24
|
+
session_dir: Path,
|
25
|
+
results: Dict[str, Any]
|
26
|
+
) -> str:
|
27
|
+
"""
|
28
|
+
Generate AI-optimized data digest from test results.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
session_dir: Path to session directory with split data files
|
32
|
+
results: Original complete results dictionary
|
33
|
+
|
34
|
+
Returns:
|
35
|
+
Markdown string with organized data presentation
|
36
|
+
"""
|
37
|
+
# Load split data files
|
38
|
+
summary = self._load_json(session_dir / "summary.json")
|
39
|
+
errors = self._load_json(session_dir / "errors.json")
|
40
|
+
network = self._load_json(session_dir / "network.json")
|
41
|
+
console = self._load_json(session_dir / "console.json")
|
42
|
+
performance = self._load_json(session_dir / "performance.json")
|
43
|
+
server_logs = self._load_json(session_dir / "server_logs.json")
|
44
|
+
screenshots = self._load_json(session_dir / "screenshots.json")
|
45
|
+
|
46
|
+
# Optional data files
|
47
|
+
mockup = self._load_json(session_dir / "mockup_comparison.json")
|
48
|
+
responsive = self._load_json(session_dir / "responsive_results.json")
|
49
|
+
css_iterations = self._load_json(session_dir / "css_iterations.json")
|
50
|
+
|
51
|
+
# Build markdown digest
|
52
|
+
digest = self._build_header(summary)
|
53
|
+
digest += self._build_quick_stats(summary, errors, network, console, server_logs)
|
54
|
+
digest += self._build_errors_section(errors, session_dir)
|
55
|
+
digest += self._build_network_section(network, session_dir)
|
56
|
+
digest += self._build_console_section(console, session_dir)
|
57
|
+
digest += self._build_server_logs_section(server_logs, session_dir)
|
58
|
+
digest += self._build_screenshots_section(screenshots, session_dir)
|
59
|
+
|
60
|
+
# Optional sections
|
61
|
+
if mockup:
|
62
|
+
digest += self._build_mockup_section(mockup, session_dir)
|
63
|
+
if responsive:
|
64
|
+
digest += self._build_responsive_section(responsive, session_dir)
|
65
|
+
if css_iterations and css_iterations.get('total_iterations', 0) > 0:
|
66
|
+
digest += self._build_css_iterations_section(css_iterations, session_dir)
|
67
|
+
|
68
|
+
digest += self._build_performance_section(performance, session_dir)
|
69
|
+
digest += self._build_data_references(session_dir)
|
70
|
+
digest += self._build_metadata(summary)
|
71
|
+
|
72
|
+
return digest
|
73
|
+
|
74
|
+
def _build_header(self, summary: Dict) -> str:
|
75
|
+
"""Build document header with basic info"""
|
76
|
+
session_id = summary.get('session_id', 'unknown')
|
77
|
+
timestamp = summary.get('timestamp', '')
|
78
|
+
success = summary.get('success', False)
|
79
|
+
|
80
|
+
status_emoji = "✅" if success else "⚠️"
|
81
|
+
status_text = "Completed" if success else "Needs Attention"
|
82
|
+
|
83
|
+
return f"""# CursorFlow Test Data Digest
|
84
|
+
|
85
|
+
**Session**: `{session_id}`
|
86
|
+
**Timestamp**: {timestamp}
|
87
|
+
**Status**: {status_emoji} {status_text}
|
88
|
+
**Execution Time**: {summary.get('execution_time', 0):.2f}s
|
89
|
+
|
90
|
+
---
|
91
|
+
|
92
|
+
"""
|
93
|
+
|
94
|
+
def _build_quick_stats(
|
95
|
+
self,
|
96
|
+
summary: Dict,
|
97
|
+
errors: Dict,
|
98
|
+
network: Dict,
|
99
|
+
console: Dict,
|
100
|
+
server_logs: Dict
|
101
|
+
) -> str:
|
102
|
+
"""Build quick statistics table"""
|
103
|
+
metrics = summary.get('metrics', {})
|
104
|
+
|
105
|
+
return f"""## Quick Statistics
|
106
|
+
|
107
|
+
| Metric | Value | Status |
|
108
|
+
|--------|-------|--------|
|
109
|
+
| DOM Elements | {metrics.get('total_dom_elements', 0)} | ℹ️ Data |
|
110
|
+
| Network Requests | {metrics.get('total_network_requests', 0)} | ℹ️ Data |
|
111
|
+
| Failed Requests | {metrics.get('failed_network_requests', 0)} | {"⚠️ Review" if metrics.get('failed_network_requests', 0) > 0 else "✅ OK"} |
|
112
|
+
| Console Errors | {metrics.get('total_errors', 0)} | {"🚨 Review" if metrics.get('total_errors', 0) > 0 else "✅ OK"} |
|
113
|
+
| Console Warnings | {metrics.get('total_warnings', 0)} | {"⚠️ Review" if metrics.get('total_warnings', 0) > 0 else "✅ OK"} |
|
114
|
+
| Server Logs | {server_logs.get('total_logs', 0)} | ℹ️ Data |
|
115
|
+
| Server Errors | {len(server_logs.get('logs_by_severity', {}).get('error', []))} | {"🚨 Review" if len(server_logs.get('logs_by_severity', {}).get('error', [])) > 0 else "✅ OK"} |
|
116
|
+
| Total Messages | {console.get('total_messages', 0)} | ℹ️ Data |
|
117
|
+
| Screenshots | {metrics.get('total_screenshots', 0)} | ℹ️ Data |
|
118
|
+
| Timeline Events | {metrics.get('total_timeline_events', 0)} | ℹ️ Data |
|
119
|
+
|
120
|
+
---
|
121
|
+
|
122
|
+
"""
|
123
|
+
|
124
|
+
def _build_errors_section(self, errors: Dict, session_dir: Path) -> str:
|
125
|
+
"""Build errors section with categorization"""
|
126
|
+
total_errors = errors.get('total_errors', 0)
|
127
|
+
|
128
|
+
if total_errors == 0:
|
129
|
+
return """## Console Errors
|
130
|
+
|
131
|
+
✅ **No console errors detected**
|
132
|
+
|
133
|
+
---
|
134
|
+
|
135
|
+
"""
|
136
|
+
|
137
|
+
section = f"""## Console Errors
|
138
|
+
|
139
|
+
**Total Errors**: {total_errors}
|
140
|
+
**Unique Error Types**: {errors.get('summary', {}).get('unique_error_types', 0)}
|
141
|
+
|
142
|
+
### Errors by Type
|
143
|
+
|
144
|
+
"""
|
145
|
+
|
146
|
+
# Organize by error type
|
147
|
+
errors_by_type = errors.get('errors_by_type', {})
|
148
|
+
for error_type, error_list in errors_by_type.items():
|
149
|
+
section += f"""#### {error_type.replace('_', ' ').title()} ({len(error_list)})\n\n"""
|
150
|
+
|
151
|
+
# Show first 3 errors of each type
|
152
|
+
for i, error in enumerate(error_list[:3], 1):
|
153
|
+
section += f"""**Error #{i}**
|
154
|
+
- **Message**: `{error.get('message', 'Unknown')[:200]}`
|
155
|
+
- **Source**: `{error.get('source', 'Unknown')}`
|
156
|
+
- **Location**: Line {error.get('line', '?')}, Column {error.get('column', '?')}
|
157
|
+
- **Screenshot**: `{error.get('screenshot_name', 'unknown')}`
|
158
|
+
- **URL**: `{error.get('url', '')[:100]}`
|
159
|
+
|
160
|
+
"""
|
161
|
+
|
162
|
+
if len(error_list) > 3:
|
163
|
+
section += f"*...and {len(error_list) - 3} more errors of this type*\n\n"
|
164
|
+
|
165
|
+
section += f"""**Full Error Data**: See `{session_dir.name}/errors.json` for complete error details and stack traces.
|
166
|
+
|
167
|
+
---
|
168
|
+
|
169
|
+
"""
|
170
|
+
return section
|
171
|
+
|
172
|
+
def _build_network_section(self, network: Dict, session_dir: Path) -> str:
|
173
|
+
"""Build network requests section"""
|
174
|
+
total_requests = network.get('total_requests', 0)
|
175
|
+
failed_requests = network.get('failed_requests', [])
|
176
|
+
|
177
|
+
section = f"""## Network Activity
|
178
|
+
|
179
|
+
**Total Requests**: {total_requests}
|
180
|
+
**Failed Requests**: {len(failed_requests)}
|
181
|
+
**Success Rate**: {network.get('summary', {}).get('success_rate', 100):.1f}%
|
182
|
+
|
183
|
+
"""
|
184
|
+
|
185
|
+
if failed_requests:
|
186
|
+
section += f"""### Failed Requests ({len(failed_requests)})
|
187
|
+
|
188
|
+
"""
|
189
|
+
# Group by status code
|
190
|
+
by_status = {}
|
191
|
+
for req in failed_requests:
|
192
|
+
status = req.get('status_code', 0)
|
193
|
+
if status not in by_status:
|
194
|
+
by_status[status] = []
|
195
|
+
by_status[status].append(req)
|
196
|
+
|
197
|
+
for status_code in sorted(by_status.keys()):
|
198
|
+
requests = by_status[status_code]
|
199
|
+
section += f"""#### HTTP {status_code} ({len(requests)} requests)
|
200
|
+
|
201
|
+
"""
|
202
|
+
for i, req in enumerate(requests[:5], 1):
|
203
|
+
section += f"""**Request #{i}**
|
204
|
+
- **URL**: `{req.get('url', 'Unknown')[:100]}`
|
205
|
+
- **Method**: {req.get('method', 'GET')}
|
206
|
+
- **Status**: {req.get('status_code', 0)}
|
207
|
+
- **Screenshot**: `{req.get('screenshot_name', 'unknown')}`
|
208
|
+
|
209
|
+
"""
|
210
|
+
|
211
|
+
if len(requests) > 5:
|
212
|
+
section += f"*...and {len(requests) - 5} more {status_code} errors*\n\n"
|
213
|
+
else:
|
214
|
+
section += "✅ **All network requests successful**\n\n"
|
215
|
+
|
216
|
+
section += f"""**Full Network Data**: See `{session_dir.name}/network.json` for complete request/response details.
|
217
|
+
|
218
|
+
---
|
219
|
+
|
220
|
+
"""
|
221
|
+
return section
|
222
|
+
|
223
|
+
def _build_console_section(self, console: Dict, session_dir: Path) -> str:
|
224
|
+
"""Build console messages section"""
|
225
|
+
total_messages = console.get('total_messages', 0)
|
226
|
+
messages_by_type = console.get('messages_by_type', {})
|
227
|
+
|
228
|
+
section = f"""## Console Messages
|
229
|
+
|
230
|
+
**Total Messages**: {total_messages}
|
231
|
+
|
232
|
+
"""
|
233
|
+
|
234
|
+
if total_messages == 0:
|
235
|
+
return section + "No console messages captured.\n\n---\n\n"
|
236
|
+
|
237
|
+
# Show counts by type
|
238
|
+
section += "### Message Breakdown\n\n"
|
239
|
+
for msg_type, messages in messages_by_type.items():
|
240
|
+
emoji = {
|
241
|
+
'errors': '🚨',
|
242
|
+
'warnings': '⚠️',
|
243
|
+
'logs': '📝',
|
244
|
+
'info': 'ℹ️'
|
245
|
+
}.get(msg_type, '📋')
|
246
|
+
|
247
|
+
section += f"- {emoji} **{msg_type.title()}**: {len(messages)}\n"
|
248
|
+
|
249
|
+
section += f"""\n**Full Console Data**: See `{session_dir.name}/console.json` for all console messages.
|
250
|
+
|
251
|
+
---
|
252
|
+
|
253
|
+
"""
|
254
|
+
return section
|
255
|
+
|
256
|
+
def _build_performance_section(self, performance: Dict, session_dir: Path) -> str:
|
257
|
+
"""Build performance metrics section"""
|
258
|
+
summary = performance.get('summary', {})
|
259
|
+
execution_time = performance.get('execution_time', 0)
|
260
|
+
|
261
|
+
section = f"""## Performance Metrics
|
262
|
+
|
263
|
+
**Test Execution Time**: {execution_time:.2f}s
|
264
|
+
|
265
|
+
"""
|
266
|
+
|
267
|
+
if summary:
|
268
|
+
avg_load = summary.get('average_page_load_time', 0)
|
269
|
+
max_memory = summary.get('max_memory_usage', 0)
|
270
|
+
|
271
|
+
section += f"""### Page Performance
|
272
|
+
|
273
|
+
- **Average Load Time**: {avg_load:.1f}ms
|
274
|
+
- **Max Memory Usage**: {max_memory:.1f}MB
|
275
|
+
- **Min Memory Usage**: {summary.get('min_memory_usage', 0):.1f}MB
|
276
|
+
|
277
|
+
"""
|
278
|
+
|
279
|
+
section += f"""**Full Performance Data**: See `{session_dir.name}/performance.json` for detailed metrics.
|
280
|
+
|
281
|
+
---
|
282
|
+
|
283
|
+
"""
|
284
|
+
return section
|
285
|
+
|
286
|
+
def _build_data_references(self, session_dir: Path) -> str:
|
287
|
+
"""Build section with references to all data files"""
|
288
|
+
return f"""## Complete Data Files
|
289
|
+
|
290
|
+
All comprehensive data available in: `{session_dir.name}/`
|
291
|
+
|
292
|
+
### Structured Data Files
|
293
|
+
|
294
|
+
| File | Description | Use Case |
|
295
|
+
|------|-------------|----------|
|
296
|
+
| `summary.json` | High-level metrics and counts | Quick overview, status checking |
|
297
|
+
| `errors.json` | All console errors with context | Error analysis, debugging |
|
298
|
+
| `network.json` | Complete network request/response data | API debugging, performance analysis |
|
299
|
+
| `console.json` | All console messages (errors, warnings, logs) | Application flow analysis |
|
300
|
+
| `server_logs.json` | Server-side logs (SSH/local/Docker) | Backend correlation, server debugging |
|
301
|
+
| `dom_analysis.json` | Complete DOM structure and elements | UI analysis, element inspection |
|
302
|
+
| `performance.json` | Performance metrics and timing | Performance optimization |
|
303
|
+
| `timeline.json` | Chronological event timeline | Understanding test flow, correlation |
|
304
|
+
| `screenshots.json` | Screenshot metadata and index | Screenshot navigation, filtering |
|
305
|
+
|
306
|
+
### Optional Data Files
|
307
|
+
|
308
|
+
| File | Description | When Present |
|
309
|
+
|------|-------------|--------------|
|
310
|
+
| `mockup_comparison.json` | Mockup vs implementation comparison | When using `compare-mockup` |
|
311
|
+
| `responsive_results.json` | Multi-viewport testing results | When using `--responsive` flag |
|
312
|
+
| `css_iterations.json` | CSS iteration history | When using `css_iteration_session()` |
|
313
|
+
|
314
|
+
### Artifact Directories
|
315
|
+
|
316
|
+
| Directory | Contents |
|
317
|
+
|-----------|----------|
|
318
|
+
| `screenshots/` | Visual captures at key moments |
|
319
|
+
| `traces/` | Playwright trace files (open with: `playwright show-trace`) |
|
320
|
+
|
321
|
+
---
|
322
|
+
|
323
|
+
"""
|
324
|
+
|
325
|
+
def _build_metadata(self, summary: Dict) -> str:
|
326
|
+
"""Build metadata section"""
|
327
|
+
return f"""## Metadata
|
328
|
+
|
329
|
+
```json
|
330
|
+
{{
|
331
|
+
"session_id": "{summary.get('session_id', 'unknown')}",
|
332
|
+
"timestamp": "{summary.get('timestamp', '')}",
|
333
|
+
"success": {str(summary.get('success', False)).lower()},
|
334
|
+
"execution_time": {summary.get('execution_time', 0)},
|
335
|
+
"has_errors": {str(summary.get('status', {}).get('has_errors', False)).lower()},
|
336
|
+
"has_network_failures": {str(summary.get('status', {}).get('has_network_failures', False)).lower()},
|
337
|
+
"has_warnings": {str(summary.get('status', {}).get('has_warnings', False)).lower()}
|
338
|
+
}}
|
339
|
+
```
|
340
|
+
|
341
|
+
---
|
342
|
+
|
343
|
+
**Generated by CursorFlow v2.7.0** - AI-Optimized Data Collection
|
344
|
+
**Format**: Multi-file structured output for AI consumption
|
345
|
+
**Philosophy**: Pure data organization, no analysis - AI does the thinking
|
346
|
+
"""
|
347
|
+
|
348
|
+
def _build_server_logs_section(self, server_logs: Dict, session_dir: Path) -> str:
|
349
|
+
"""Build server logs section"""
|
350
|
+
total_logs = server_logs.get('total_logs', 0)
|
351
|
+
|
352
|
+
if total_logs == 0:
|
353
|
+
return """## Server Logs
|
354
|
+
|
355
|
+
✅ **No server logs captured** (log monitoring may not be configured)
|
356
|
+
|
357
|
+
---
|
358
|
+
|
359
|
+
"""
|
360
|
+
|
361
|
+
section = f"""## Server Logs
|
362
|
+
|
363
|
+
**Total Server Logs**: {total_logs}
|
364
|
+
|
365
|
+
### Server Logs by Severity
|
366
|
+
|
367
|
+
"""
|
368
|
+
|
369
|
+
logs_by_severity = server_logs.get('logs_by_severity', {})
|
370
|
+
for severity, logs in logs_by_severity.items():
|
371
|
+
emoji = {
|
372
|
+
'error': '🚨',
|
373
|
+
'warning': '⚠️',
|
374
|
+
'info': 'ℹ️',
|
375
|
+
'debug': '🔍'
|
376
|
+
}.get(severity.lower(), '📝')
|
377
|
+
|
378
|
+
section += f"- {emoji} **{severity.title()}**: {len(logs)}\n"
|
379
|
+
|
380
|
+
# Show error logs if present
|
381
|
+
error_logs = logs_by_severity.get('error', [])
|
382
|
+
if error_logs:
|
383
|
+
section += f"""\n### Server Error Logs ({len(error_logs)})
|
384
|
+
|
385
|
+
"""
|
386
|
+
for i, log in enumerate(error_logs[:5], 1):
|
387
|
+
section += f"""**Log #{i}**
|
388
|
+
- **Content**: `{log.get('content', 'Unknown')[:150]}`
|
389
|
+
- **Source**: {log.get('source', 'unknown')}
|
390
|
+
- **File**: `{log.get('file', 'unknown')}`
|
391
|
+
- **Timestamp**: {log.get('timestamp', 0)}
|
392
|
+
|
393
|
+
"""
|
394
|
+
|
395
|
+
if len(error_logs) > 5:
|
396
|
+
section += f"*...and {len(error_logs) - 5} more server errors*\n\n"
|
397
|
+
|
398
|
+
section += f"""**Full Server Log Data**: See `{session_dir.name}/server_logs.json` for all server logs.
|
399
|
+
|
400
|
+
---
|
401
|
+
|
402
|
+
"""
|
403
|
+
return section
|
404
|
+
|
405
|
+
def _build_screenshots_section(self, screenshots: Dict, session_dir: Path) -> str:
|
406
|
+
"""Build screenshots section"""
|
407
|
+
total = screenshots.get('total_screenshots', 0)
|
408
|
+
|
409
|
+
if total == 0:
|
410
|
+
return ""
|
411
|
+
|
412
|
+
section = f"""## Screenshots
|
413
|
+
|
414
|
+
**Total Screenshots**: {total}
|
415
|
+
|
416
|
+
"""
|
417
|
+
|
418
|
+
screenshot_list = screenshots.get('screenshots', [])
|
419
|
+
screenshots_with_errors = [s for s in screenshot_list if s.get('has_errors')]
|
420
|
+
screenshots_with_network_failures = [s for s in screenshot_list if s.get('has_network_failures')]
|
421
|
+
|
422
|
+
if screenshots_with_errors:
|
423
|
+
section += f"- 🚨 **With Console Errors**: {len(screenshots_with_errors)}\n"
|
424
|
+
if screenshots_with_network_failures:
|
425
|
+
section += f"- ⚠️ **With Network Failures**: {len(screenshots_with_network_failures)}\n"
|
426
|
+
|
427
|
+
section += f"""\n**Screenshot Index**: See `{session_dir.name}/screenshots.json` for complete metadata.
|
428
|
+
**Screenshot Files**: See `{session_dir.name}/screenshots/` directory.
|
429
|
+
|
430
|
+
---
|
431
|
+
|
432
|
+
"""
|
433
|
+
return section
|
434
|
+
|
435
|
+
def _build_mockup_section(self, mockup: Dict, session_dir: Path) -> str:
|
436
|
+
"""Build mockup comparison section"""
|
437
|
+
similarity = mockup.get('similarity_score', 0)
|
438
|
+
|
439
|
+
section = f"""## Mockup Comparison
|
440
|
+
|
441
|
+
**Mockup URL**: `{mockup.get('mockup_url', 'N/A')}`
|
442
|
+
**Implementation URL**: `{mockup.get('implementation_url', 'N/A')}`
|
443
|
+
**Similarity Score**: {similarity:.1f}%
|
444
|
+
|
445
|
+
"""
|
446
|
+
|
447
|
+
differences = mockup.get('differences', [])
|
448
|
+
if differences:
|
449
|
+
section += f"**Differences Detected**: {len(differences)}\n\n"
|
450
|
+
|
451
|
+
section += f"""**Full Mockup Data**: See `{session_dir.name}/mockup_comparison.json` for detailed comparison.
|
452
|
+
|
453
|
+
---
|
454
|
+
|
455
|
+
"""
|
456
|
+
return section
|
457
|
+
|
458
|
+
def _build_responsive_section(self, responsive: Dict, session_dir: Path) -> str:
|
459
|
+
"""Build responsive testing section"""
|
460
|
+
viewports = responsive.get('viewports', {})
|
461
|
+
|
462
|
+
section = f"""## Responsive Testing
|
463
|
+
|
464
|
+
**Viewports Tested**: {len(viewports)}
|
465
|
+
|
466
|
+
"""
|
467
|
+
|
468
|
+
for viewport_name, viewport_data in viewports.items():
|
469
|
+
errors = viewport_data.get('errors', 0)
|
470
|
+
network_failures = viewport_data.get('network_failures', 0)
|
471
|
+
|
472
|
+
section += f"- **{viewport_name.title()}**: "
|
473
|
+
if errors > 0 or network_failures > 0:
|
474
|
+
section += f"{errors} errors, {network_failures} network failures\n"
|
475
|
+
else:
|
476
|
+
section += "✅ OK\n"
|
477
|
+
|
478
|
+
section += f"""\n**Full Responsive Data**: See `{session_dir.name}/responsive_results.json` for all viewport results.
|
479
|
+
|
480
|
+
---
|
481
|
+
|
482
|
+
"""
|
483
|
+
return section
|
484
|
+
|
485
|
+
def _build_css_iterations_section(self, css_iterations: Dict, session_dir: Path) -> str:
|
486
|
+
"""Build CSS iterations section"""
|
487
|
+
total = css_iterations.get('total_iterations', 0)
|
488
|
+
|
489
|
+
section = f"""## CSS Iterations
|
490
|
+
|
491
|
+
**Total Iterations**: {total}
|
492
|
+
|
493
|
+
"""
|
494
|
+
|
495
|
+
iterations = css_iterations.get('iterations', [])
|
496
|
+
for i, iteration in enumerate(iterations[:5], 1):
|
497
|
+
section += f"- **Iteration {i}**: {iteration.get('name', 'unnamed')}\n"
|
498
|
+
|
499
|
+
if len(iterations) > 5:
|
500
|
+
section += f"*...and {len(iterations) - 5} more iterations*\n"
|
501
|
+
|
502
|
+
section += f"""\n**Full CSS Iteration Data**: See `{session_dir.name}/css_iterations.json` for all iterations.
|
503
|
+
|
504
|
+
---
|
505
|
+
|
506
|
+
"""
|
507
|
+
return section
|
508
|
+
|
509
|
+
def _load_json(self, path: Path) -> Dict:
|
510
|
+
"""Load JSON file, return empty dict if not found"""
|
511
|
+
try:
|
512
|
+
with open(path, 'r', encoding='utf-8') as f:
|
513
|
+
return json.load(f)
|
514
|
+
except FileNotFoundError:
|
515
|
+
return {}
|
516
|
+
except json.JSONDecodeError:
|
517
|
+
return {}
|
518
|
+
|