scitex 2.16.2__py3-none-any.whl → 2.17.3__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 (70) hide show
  1. scitex/_dev/__init__.py +122 -0
  2. scitex/_dev/_config.py +391 -0
  3. scitex/_dev/_dashboard/__init__.py +11 -0
  4. scitex/_dev/_dashboard/_app.py +89 -0
  5. scitex/_dev/_dashboard/_routes.py +169 -0
  6. scitex/_dev/_dashboard/_scripts.py +301 -0
  7. scitex/_dev/_dashboard/_styles.py +205 -0
  8. scitex/_dev/_dashboard/_templates.py +117 -0
  9. scitex/_dev/_dashboard/static/version-dashboard-favicon.svg +12 -0
  10. scitex/_dev/_ecosystem.py +109 -0
  11. scitex/_dev/_github.py +360 -0
  12. scitex/_dev/_mcp/__init__.py +11 -0
  13. scitex/_dev/_mcp/handlers.py +182 -0
  14. scitex/_dev/_ssh.py +332 -0
  15. scitex/_dev/_versions.py +272 -0
  16. scitex/_mcp_resources/_cheatsheet.py +1 -1
  17. scitex/_mcp_resources/_modules.py +1 -1
  18. scitex/_mcp_tools/__init__.py +4 -0
  19. scitex/_mcp_tools/dev.py +186 -0
  20. scitex/_mcp_tools/verify.py +256 -0
  21. scitex/audio/_audio_check.py +84 -41
  22. scitex/cli/capture.py +45 -22
  23. scitex/cli/dev.py +494 -0
  24. scitex/cli/main.py +4 -0
  25. scitex/cli/stats.py +48 -20
  26. scitex/cli/verify.py +473 -0
  27. scitex/dev/plt/__init__.py +1 -1
  28. scitex/dev/plt/mpl/get_dir_ax.py +1 -1
  29. scitex/dev/plt/mpl/get_signatures.py +1 -1
  30. scitex/dev/plt/mpl/get_signatures_details.py +1 -1
  31. scitex/io/_load.py +8 -1
  32. scitex/io/_save.py +12 -0
  33. scitex/plt/__init__.py +16 -6
  34. scitex/session/README.md +2 -2
  35. scitex/session/__init__.py +1 -0
  36. scitex/session/_decorator.py +57 -33
  37. scitex/session/_lifecycle/__init__.py +23 -0
  38. scitex/session/_lifecycle/_close.py +225 -0
  39. scitex/session/_lifecycle/_config.py +112 -0
  40. scitex/session/_lifecycle/_matplotlib.py +83 -0
  41. scitex/session/_lifecycle/_start.py +246 -0
  42. scitex/session/_lifecycle/_utils.py +186 -0
  43. scitex/session/_manager.py +40 -3
  44. scitex/session/template.py +1 -1
  45. scitex/template/__init__.py +18 -1
  46. scitex/template/_templates/plt.py +1 -1
  47. scitex/template/_templates/session.py +1 -1
  48. scitex/template/clone_research_minimal.py +111 -0
  49. scitex/verify/README.md +300 -0
  50. scitex/verify/__init__.py +208 -0
  51. scitex/verify/_chain.py +369 -0
  52. scitex/verify/_db.py +600 -0
  53. scitex/verify/_hash.py +187 -0
  54. scitex/verify/_integration.py +127 -0
  55. scitex/verify/_rerun.py +253 -0
  56. scitex/verify/_tracker.py +330 -0
  57. scitex/verify/_visualize.py +44 -0
  58. scitex/verify/_viz/__init__.py +38 -0
  59. scitex/verify/_viz/_colors.py +84 -0
  60. scitex/verify/_viz/_format.py +302 -0
  61. scitex/verify/_viz/_json.py +192 -0
  62. scitex/verify/_viz/_mermaid.py +440 -0
  63. scitex/verify/_viz/_templates.py +246 -0
  64. scitex/verify/_viz/_utils.py +56 -0
  65. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/METADATA +2 -1
  66. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/RECORD +69 -28
  67. scitex/session/_lifecycle.py +0 -827
  68. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/WHEEL +0 -0
  69. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/entry_points.txt +0 -0
  70. {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,440 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-01 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/verify/_viz/_mermaid.py
4
+ """Mermaid diagram generation for verification DAG."""
5
+
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ from pathlib import Path
10
+ from typing import Literal, Optional, Union
11
+
12
+ from .._chain import VerificationLevel, verify_chain, verify_run
13
+ from .._db import get_db
14
+ from ._json import file_to_node_id, format_path, generate_dag_json, verify_file_hash
15
+ from ._templates import get_html_template
16
+
17
+ PathMode = Literal["name", "relative", "absolute"]
18
+
19
+
20
+ def generate_mermaid_dag(
21
+ session_id: Optional[str] = None,
22
+ target_file: Optional[str] = None,
23
+ max_depth: int = 10,
24
+ show_files: bool = True,
25
+ show_hashes: bool = False,
26
+ path_mode: PathMode = "name",
27
+ ) -> str:
28
+ """
29
+ Generate Mermaid diagram for verification DAG.
30
+
31
+ Parameters
32
+ ----------
33
+ session_id : str, optional
34
+ Start from this session
35
+ target_file : str, optional
36
+ Start from session that produced this file
37
+ max_depth : int, optional
38
+ Maximum chain depth
39
+ show_files : bool, optional
40
+ Whether to show input/output files as nodes (default: True)
41
+ show_hashes : bool, optional
42
+ Whether to show truncated file hashes (default: False)
43
+ path_mode : str, optional
44
+ How to display file paths: "name", "relative", or "absolute"
45
+
46
+ Returns
47
+ -------
48
+ str
49
+ Mermaid diagram code
50
+ """
51
+ db = get_db()
52
+ lines = ["graph TD"]
53
+
54
+ if target_file:
55
+ chain = verify_chain(target_file)
56
+ chain_ids = [run.session_id for run in chain.runs]
57
+ elif session_id:
58
+ chain_ids = db.get_chain(session_id)
59
+ else:
60
+ chain_ids = []
61
+
62
+ if not chain_ids:
63
+ lines.append(' empty["No runs found"]')
64
+ return "\n".join(lines)
65
+
66
+ runs_data = _collect_runs_data(chain_ids, db)
67
+
68
+ if show_files:
69
+ _generate_detailed_dag(lines, runs_data, show_hashes, path_mode)
70
+ else:
71
+ _generate_simple_dag(lines, runs_data, chain_ids, path_mode)
72
+
73
+ _append_class_definitions(lines)
74
+ return "\n".join(lines)
75
+
76
+
77
+ def _collect_runs_data(chain_ids: list, db) -> list:
78
+ """Collect run data for all sessions in chain."""
79
+ runs_data = []
80
+ for sid in chain_ids:
81
+ run = db.get_run(sid)
82
+ verification = verify_run(sid)
83
+
84
+ # Check if there's a stored from-scratch verification result
85
+ latest_verification = db.get_latest_verification(sid)
86
+ if (
87
+ latest_verification
88
+ and latest_verification.get("level") == "rerun"
89
+ and latest_verification.get("status") == "verified"
90
+ ):
91
+ # Apply from-scratch level to the verification
92
+ verification.level = VerificationLevel.RERUN
93
+
94
+ inputs = db.get_file_hashes(sid, role="input")
95
+ outputs = db.get_file_hashes(sid, role="output")
96
+ runs_data.append(
97
+ {
98
+ "session_id": sid,
99
+ "run": run,
100
+ "verification": verification,
101
+ "inputs": inputs,
102
+ "outputs": outputs,
103
+ }
104
+ )
105
+ return runs_data
106
+
107
+
108
+ def _append_class_definitions(lines: list) -> None:
109
+ """Append Mermaid class definitions for styling."""
110
+ lines.append("")
111
+ lines.append(" classDef script fill:#87CEEB,stroke:#4169E1,stroke-width:2px")
112
+ lines.append(" classDef verified fill:#90EE90,stroke:#228B22")
113
+ lines.append(
114
+ " classDef verified_scratch fill:#90EE90,stroke:#228B22,stroke-width:4px"
115
+ )
116
+ lines.append(" classDef failed fill:#FFB6C1,stroke:#DC143C")
117
+ lines.append(" classDef file fill:#FFF8DC,stroke:#DAA520")
118
+ lines.append(" classDef file_ok fill:#90EE90,stroke:#228B22")
119
+ lines.append(" classDef file_rerun fill:#90EE90,stroke:#228B22,stroke-width:4px")
120
+ lines.append(" classDef file_bad fill:#FFB6C1,stroke:#DC143C")
121
+
122
+
123
+ def _generate_simple_dag(
124
+ lines: list, runs_data: list, chain_ids: list, path_mode: PathMode = "name"
125
+ ) -> None:
126
+ """Generate simple script-only DAG."""
127
+ for data in runs_data:
128
+ sid = data["session_id"]
129
+ run = data["run"]
130
+ verification = data["verification"]
131
+ node_id = sid.replace("-", "_").replace(".", "_")
132
+ status_class = "verified" if verification.is_verified else "failed"
133
+ script_name = format_path(
134
+ run.get("script_path", "unknown") if run else "unknown", path_mode
135
+ )
136
+ lines.append(f' {node_id}["{script_name}"]:::{status_class}')
137
+
138
+ for i in range(len(chain_ids) - 1):
139
+ curr = chain_ids[i].replace("-", "_").replace(".", "_")
140
+ parent = chain_ids[i + 1].replace("-", "_").replace(".", "_")
141
+ lines.append(f" {parent} --> {curr}")
142
+
143
+
144
+ def _generate_detailed_dag(
145
+ lines: list,
146
+ runs_data: list,
147
+ show_hashes: bool = False,
148
+ path_mode: PathMode = "name",
149
+ ) -> None:
150
+ """Generate detailed DAG with input/output files and verification status."""
151
+ file_nodes = {}
152
+ failed_files = set() # Track failed files for propagation
153
+ runs_data = list(reversed(runs_data))
154
+
155
+ # First pass: identify all failed files
156
+ for data in runs_data:
157
+ inputs = data["inputs"]
158
+ outputs = data["outputs"]
159
+ for fpath, stored_hash in {**inputs, **outputs}.items():
160
+ if not verify_file_hash(fpath, stored_hash):
161
+ failed_files.add(fpath)
162
+
163
+ # Second pass: propagate failures through chain
164
+ for data in runs_data:
165
+ inputs = data["inputs"]
166
+ outputs = data["outputs"]
167
+ # If any input is failed, all outputs are also failed
168
+ has_failed_input = any(fpath in failed_files for fpath in inputs.keys())
169
+ if has_failed_input:
170
+ for fpath in outputs.keys():
171
+ failed_files.add(fpath)
172
+
173
+ for i, data in enumerate(runs_data):
174
+ sid = data["session_id"]
175
+ run = data["run"]
176
+ verification = data["verification"]
177
+ inputs = data["inputs"]
178
+ outputs = data["outputs"]
179
+
180
+ # Check if this script has failed inputs (propagated failure)
181
+ has_failed_input = any(fpath in failed_files for fpath in inputs.keys())
182
+
183
+ _add_script_node(
184
+ lines, i, sid, run, verification, path_mode, show_hashes, has_failed_input
185
+ )
186
+ is_rerun = verification.is_verified_from_scratch
187
+ _add_file_nodes(
188
+ lines,
189
+ f"script_{i}",
190
+ inputs,
191
+ file_nodes,
192
+ show_hashes,
193
+ path_mode,
194
+ "input",
195
+ False,
196
+ failed_files,
197
+ )
198
+ _add_file_nodes(
199
+ lines,
200
+ f"script_{i}",
201
+ outputs,
202
+ file_nodes,
203
+ show_hashes,
204
+ path_mode,
205
+ "output",
206
+ is_rerun,
207
+ failed_files,
208
+ )
209
+
210
+
211
+ def _get_file_icon(filename: str) -> str:
212
+ """Get icon emoji for file type."""
213
+ ext = Path(filename).suffix.lower()
214
+ icons = {
215
+ ".py": "🐍",
216
+ ".csv": "📊",
217
+ ".json": "📋",
218
+ ".yaml": "⚙️",
219
+ ".yml": "⚙️",
220
+ ".png": "🖼️",
221
+ ".jpg": "🖼️",
222
+ ".jpeg": "🖼️",
223
+ ".svg": "🖼️",
224
+ ".pdf": "📄",
225
+ ".html": "🌐",
226
+ ".txt": "📝",
227
+ ".md": "📝",
228
+ ".npy": "🔢",
229
+ ".npz": "🔢",
230
+ ".pkl": "📦",
231
+ ".pickle": "📦",
232
+ ".h5": "💾",
233
+ ".hdf5": "💾",
234
+ ".mat": "🔬",
235
+ ".sh": "🖥️",
236
+ }
237
+ return icons.get(ext, "📄")
238
+
239
+
240
+ def _add_script_node(
241
+ lines: list,
242
+ idx: int,
243
+ sid: str,
244
+ run: dict,
245
+ verification,
246
+ path_mode: PathMode,
247
+ show_hashes: bool = False,
248
+ has_failed_input: bool = False,
249
+ ) -> None:
250
+ """Add a script node to the diagram."""
251
+ node_id = f"script_{idx}"
252
+ script_verified = verification.is_verified and not has_failed_input
253
+ is_from_scratch = verification.is_verified_from_scratch and not has_failed_input
254
+
255
+ # Determine status class with from-scratch distinction
256
+ if has_failed_input:
257
+ status_class = "failed"
258
+ elif is_from_scratch:
259
+ status_class = "verified_scratch"
260
+ elif script_verified:
261
+ status_class = "verified"
262
+ else:
263
+ status_class = "failed"
264
+
265
+ script_path = run.get("script_path", "unknown") if run else "unknown"
266
+ script_name = format_path(script_path, path_mode)
267
+ icon = _get_file_icon(script_path)
268
+ short_id = sid.split("_")[-1][:4] if "_" in sid else sid[:8]
269
+ badge = "✓✓" if is_from_scratch else ("✓" if script_verified else "✗")
270
+ # Show script hash if requested
271
+ script_hash = run.get("script_hash", "") if run else ""
272
+ hash_display = f"<br/>{script_hash[:8]}..." if show_hashes and script_hash else ""
273
+ lines.append(
274
+ f' {node_id}["{badge} {icon} {script_name}<br/>({short_id}){hash_display}"]:::{status_class}'
275
+ )
276
+
277
+
278
+ def _add_file_nodes(
279
+ lines: list,
280
+ script_id: str,
281
+ files: dict,
282
+ file_nodes: dict,
283
+ show_hashes: bool,
284
+ path_mode: PathMode,
285
+ role: str,
286
+ is_script_rerun_verified: bool = False,
287
+ failed_files: set = None,
288
+ ) -> None:
289
+ """Add file nodes and connections to the diagram."""
290
+ failed_files = failed_files or set()
291
+
292
+ for fpath, stored_hash in files.items():
293
+ display_name = format_path(fpath, path_mode)
294
+ file_id = file_to_node_id(Path(fpath).name)
295
+ icon = _get_file_icon(fpath)
296
+
297
+ if file_id not in file_nodes:
298
+ file_status = verify_file_hash(fpath, stored_hash)
299
+ is_failed = fpath in failed_files or not file_status
300
+
301
+ # Determine badge and class
302
+ if is_failed:
303
+ file_class = "file_bad"
304
+ badge = "✗"
305
+ elif role == "output" and is_script_rerun_verified:
306
+ file_class = "file_rerun"
307
+ badge = "✓✓"
308
+ else:
309
+ file_class = "file_ok"
310
+ badge = "✓"
311
+
312
+ hash_display = f"<br/>{stored_hash[:8]}..." if show_hashes else ""
313
+ lines.append(
314
+ f' {file_id}[("{badge} {icon} {display_name}{hash_display}")]:::{file_class}'
315
+ )
316
+ file_nodes[file_id] = (fpath, stored_hash)
317
+
318
+ if role == "input":
319
+ lines.append(f" {file_id} --> {script_id}")
320
+ else:
321
+ lines.append(f" {script_id} --> {file_id}")
322
+
323
+
324
+ def generate_html_dag(
325
+ session_id: Optional[str] = None,
326
+ target_file: Optional[str] = None,
327
+ title: str = "Verification DAG",
328
+ show_hashes: bool = False,
329
+ path_mode: PathMode = "name",
330
+ ) -> str:
331
+ """Generate interactive HTML visualization for verification DAG."""
332
+ mermaid_code = generate_mermaid_dag(
333
+ session_id=session_id,
334
+ target_file=target_file,
335
+ show_hashes=show_hashes,
336
+ path_mode=path_mode,
337
+ )
338
+ return get_html_template(title, mermaid_code)
339
+
340
+
341
+ def render_dag(
342
+ output_path: Union[str, Path],
343
+ session_id: Optional[str] = None,
344
+ target_file: Optional[str] = None,
345
+ title: str = "Verification DAG",
346
+ show_hashes: bool = False,
347
+ path_mode: PathMode = "name",
348
+ ) -> Path:
349
+ """
350
+ Render verification DAG to file (HTML, PNG, SVG, JSON, or MMD).
351
+
352
+ Parameters
353
+ ----------
354
+ output_path : str or Path
355
+ Output file path. Extension determines format.
356
+ session_id : str, optional
357
+ Start from this session
358
+ target_file : str, optional
359
+ Start from session that produced this file
360
+ title : str, optional
361
+ Title for the visualization
362
+ show_hashes : bool, optional
363
+ Whether to show file hashes
364
+ path_mode : str, optional
365
+ Path display mode
366
+
367
+ Returns
368
+ -------
369
+ Path
370
+ Path to the generated file
371
+ """
372
+ output_path = Path(output_path)
373
+ output_path.parent.mkdir(parents=True, exist_ok=True)
374
+ ext = output_path.suffix.lower()
375
+
376
+ if ext == ".html":
377
+ content = generate_html_dag(
378
+ session_id=session_id,
379
+ target_file=target_file,
380
+ title=title,
381
+ show_hashes=show_hashes,
382
+ path_mode=path_mode,
383
+ )
384
+ output_path.write_text(content)
385
+
386
+ elif ext == ".mmd":
387
+ content = generate_mermaid_dag(
388
+ session_id=session_id,
389
+ target_file=target_file,
390
+ show_hashes=show_hashes,
391
+ path_mode=path_mode,
392
+ )
393
+ output_path.write_text(content)
394
+
395
+ elif ext == ".json":
396
+ graph_json = generate_dag_json(
397
+ session_id=session_id,
398
+ target_file=target_file,
399
+ path_mode=path_mode,
400
+ )
401
+ output_path.write_text(json.dumps(graph_json, indent=2))
402
+
403
+ elif ext in [".png", ".svg"]:
404
+ mermaid = generate_mermaid_dag(
405
+ session_id=session_id,
406
+ target_file=target_file,
407
+ show_hashes=show_hashes,
408
+ path_mode=path_mode,
409
+ )
410
+ # Write mermaid to temp file and compile with mmdc
411
+ import subprocess
412
+ import tempfile
413
+
414
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".mmd", delete=False) as f:
415
+ f.write(mermaid)
416
+ mmd_path = f.name
417
+
418
+ try:
419
+ subprocess.run(
420
+ ["mmdc", "-i", mmd_path, "-o", str(output_path)],
421
+ check=True,
422
+ capture_output=True,
423
+ )
424
+ except (subprocess.CalledProcessError, FileNotFoundError):
425
+ # Fallback to mmd file if mmdc fails
426
+ fallback_path = output_path.with_suffix(".mmd")
427
+ fallback_path.write_text(mermaid)
428
+ return fallback_path
429
+ finally:
430
+ Path(mmd_path).unlink(missing_ok=True)
431
+
432
+ else:
433
+ raise ValueError(
434
+ f"Unsupported format: {ext}. Use .html, .png, .svg, .json, or .mmd"
435
+ )
436
+
437
+ return output_path
438
+
439
+
440
+ # EOF
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env python3
2
+ # Timestamp: "2026-02-01 (ywatanabe)"
3
+ # File: /home/ywatanabe/proj/scitex-python/src/scitex/verify/_viz/_templates.py
4
+ """HTML templates for verification DAG visualization."""
5
+
6
+ from __future__ import annotations
7
+
8
+ from datetime import datetime
9
+
10
+
11
+ def get_timestamp() -> str:
12
+ """Get current timestamp for footer."""
13
+ return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
14
+
15
+
16
+ def get_html_template(title: str, mermaid_code: str) -> str:
17
+ """
18
+ Generate sleek HTML template with icons for verification DAG.
19
+
20
+ Parameters
21
+ ----------
22
+ title : str
23
+ Page title
24
+ mermaid_code : str
25
+ Mermaid diagram code
26
+
27
+ Returns
28
+ -------
29
+ str
30
+ Complete HTML document
31
+ """
32
+ timestamp = get_timestamp()
33
+
34
+ return f"""<!DOCTYPE html>
35
+ <html>
36
+ <head>
37
+ <meta charset="UTF-8">
38
+ <title>{title}</title>
39
+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
40
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
41
+ <style>
42
+ :root {{
43
+ --verified-bg: #d4edda;
44
+ --verified-border: #28a745;
45
+ --verified-text: #155724;
46
+ --failed-bg: #f8d7da;
47
+ --failed-border: #dc3545;
48
+ --failed-text: #721c24;
49
+ --file-bg: #fff3cd;
50
+ --file-border: #ffc107;
51
+ --script-bg: #cce5ff;
52
+ --script-border: #4a6baf;
53
+ }}
54
+ body {{
55
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
56
+ margin: 0;
57
+ padding: 20px;
58
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
59
+ min-height: 100vh;
60
+ }}
61
+ .container {{
62
+ max-width: 1400px;
63
+ margin: 0 auto;
64
+ background: white;
65
+ padding: 30px;
66
+ border-radius: 16px;
67
+ box-shadow: 0 10px 40px rgba(0,0,0,0.2);
68
+ }}
69
+ .header {{
70
+ display: flex;
71
+ align-items: center;
72
+ gap: 15px;
73
+ margin-bottom: 25px;
74
+ padding-bottom: 15px;
75
+ border-bottom: 3px solid var(--script-border);
76
+ }}
77
+ .header-icon {{
78
+ width: 48px;
79
+ height: 48px;
80
+ background: var(--script-border);
81
+ border-radius: 12px;
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ color: white;
86
+ font-size: 24px;
87
+ }}
88
+ h1 {{
89
+ color: #333;
90
+ margin: 0;
91
+ font-size: 1.8rem;
92
+ }}
93
+ .legend {{
94
+ display: flex;
95
+ flex-wrap: wrap;
96
+ gap: 20px;
97
+ margin: 20px 0;
98
+ padding: 15px;
99
+ background: linear-gradient(to right, #f8f9fa, #e9ecef);
100
+ border-radius: 12px;
101
+ border: 1px solid #dee2e6;
102
+ }}
103
+ .legend-item {{
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 10px;
107
+ padding: 8px 15px;
108
+ background: white;
109
+ border-radius: 8px;
110
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
111
+ }}
112
+ .legend-icon {{
113
+ width: 28px;
114
+ height: 28px;
115
+ border-radius: 6px;
116
+ display: flex;
117
+ align-items: center;
118
+ justify-content: center;
119
+ font-size: 14px;
120
+ }}
121
+ .legend-icon.verified {{
122
+ background: var(--verified-bg);
123
+ color: var(--verified-text);
124
+ border: 2px solid var(--verified-border);
125
+ }}
126
+ .legend-icon.from-scratch {{
127
+ background: var(--verified-bg);
128
+ color: var(--verified-text);
129
+ border: 3px solid var(--verified-border);
130
+ box-shadow: 0 0 0 2px var(--verified-bg);
131
+ }}
132
+ .legend-icon.failed {{
133
+ background: var(--failed-bg);
134
+ color: var(--failed-text);
135
+ border: 2px solid var(--failed-border);
136
+ }}
137
+ .legend-icon.file {{
138
+ background: var(--file-bg);
139
+ color: #856404;
140
+ border: 2px solid var(--file-border);
141
+ }}
142
+ .legend-icon.script {{
143
+ background: var(--script-bg);
144
+ color: #004085;
145
+ border: 2px solid var(--script-border);
146
+ }}
147
+ .legend-text {{
148
+ font-size: 0.9rem;
149
+ color: #495057;
150
+ }}
151
+ .mermaid {{
152
+ margin: 25px 0;
153
+ padding: 20px;
154
+ background: #fafafa;
155
+ border-radius: 12px;
156
+ border: 1px solid #e9ecef;
157
+ overflow-x: auto;
158
+ }}
159
+ .footer {{
160
+ margin-top: 25px;
161
+ padding-top: 15px;
162
+ border-top: 1px solid #e9ecef;
163
+ display: flex;
164
+ justify-content: space-between;
165
+ align-items: center;
166
+ color: #6c757d;
167
+ font-size: 0.85rem;
168
+ }}
169
+ .footer-brand {{
170
+ display: flex;
171
+ align-items: center;
172
+ gap: 8px;
173
+ }}
174
+ .footer-brand i {{
175
+ color: var(--script-border);
176
+ }}
177
+ </style>
178
+ </head>
179
+ <body>
180
+ <div class="container">
181
+ <div class="header">
182
+ <div class="header-icon">
183
+ <i class="fas fa-project-diagram"></i>
184
+ </div>
185
+ <h1>{title}</h1>
186
+ </div>
187
+ <div class="legend">
188
+ <div class="legend-item">
189
+ <span style="font-size:1.2em">🐍</span>
190
+ <span class="legend-text">Python</span>
191
+ </div>
192
+ <div class="legend-item">
193
+ <span style="font-size:1.2em">📊</span>
194
+ <span class="legend-text">CSV</span>
195
+ </div>
196
+ <div class="legend-item">
197
+ <span style="font-size:1.2em">📋</span>
198
+ <span class="legend-text">JSON</span>
199
+ </div>
200
+ <div class="legend-item">
201
+ <span style="font-size:1.2em">⚙️</span>
202
+ <span class="legend-text">Config</span>
203
+ </div>
204
+ <div class="legend-item">
205
+ <div class="legend-icon verified"><i class="fas fa-check"></i></div>
206
+ <span class="legend-text">Verified</span>
207
+ </div>
208
+ <div class="legend-item">
209
+ <div class="legend-icon from-scratch"><i class="fas fa-check-double"></i></div>
210
+ <span class="legend-text">From-scratch</span>
211
+ </div>
212
+ <div class="legend-item">
213
+ <div class="legend-icon failed"><i class="fas fa-times"></i></div>
214
+ <span class="legend-text">Failed</span>
215
+ </div>
216
+ </div>
217
+ <div class="mermaid">
218
+ {mermaid_code}
219
+ </div>
220
+ <div class="footer">
221
+ <div class="footer-brand">
222
+ <i class="fas fa-flask"></i>
223
+ <span>Generated by SciTeX Verify</span>
224
+ </div>
225
+ <div>
226
+ <i class="far fa-clock"></i> Generated at: {timestamp}
227
+ </div>
228
+ </div>
229
+ </div>
230
+ <script>
231
+ mermaid.initialize({{
232
+ startOnLoad: true,
233
+ theme: 'default',
234
+ flowchart: {{
235
+ curve: 'basis',
236
+ padding: 20,
237
+ nodeSpacing: 50,
238
+ rankSpacing: 60
239
+ }}
240
+ }});
241
+ </script>
242
+ </body>
243
+ </html>"""
244
+
245
+
246
+ # EOF