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.
- scitex/_dev/__init__.py +122 -0
- scitex/_dev/_config.py +391 -0
- scitex/_dev/_dashboard/__init__.py +11 -0
- scitex/_dev/_dashboard/_app.py +89 -0
- scitex/_dev/_dashboard/_routes.py +169 -0
- scitex/_dev/_dashboard/_scripts.py +301 -0
- scitex/_dev/_dashboard/_styles.py +205 -0
- scitex/_dev/_dashboard/_templates.py +117 -0
- scitex/_dev/_dashboard/static/version-dashboard-favicon.svg +12 -0
- scitex/_dev/_ecosystem.py +109 -0
- scitex/_dev/_github.py +360 -0
- scitex/_dev/_mcp/__init__.py +11 -0
- scitex/_dev/_mcp/handlers.py +182 -0
- scitex/_dev/_ssh.py +332 -0
- scitex/_dev/_versions.py +272 -0
- scitex/_mcp_resources/_cheatsheet.py +1 -1
- scitex/_mcp_resources/_modules.py +1 -1
- scitex/_mcp_tools/__init__.py +4 -0
- scitex/_mcp_tools/dev.py +186 -0
- scitex/_mcp_tools/verify.py +256 -0
- scitex/audio/_audio_check.py +84 -41
- scitex/cli/capture.py +45 -22
- scitex/cli/dev.py +494 -0
- scitex/cli/main.py +4 -0
- scitex/cli/stats.py +48 -20
- scitex/cli/verify.py +473 -0
- scitex/dev/plt/__init__.py +1 -1
- scitex/dev/plt/mpl/get_dir_ax.py +1 -1
- scitex/dev/plt/mpl/get_signatures.py +1 -1
- scitex/dev/plt/mpl/get_signatures_details.py +1 -1
- scitex/io/_load.py +8 -1
- scitex/io/_save.py +12 -0
- scitex/plt/__init__.py +16 -6
- scitex/session/README.md +2 -2
- scitex/session/__init__.py +1 -0
- scitex/session/_decorator.py +57 -33
- scitex/session/_lifecycle/__init__.py +23 -0
- scitex/session/_lifecycle/_close.py +225 -0
- scitex/session/_lifecycle/_config.py +112 -0
- scitex/session/_lifecycle/_matplotlib.py +83 -0
- scitex/session/_lifecycle/_start.py +246 -0
- scitex/session/_lifecycle/_utils.py +186 -0
- scitex/session/_manager.py +40 -3
- scitex/session/template.py +1 -1
- scitex/template/__init__.py +18 -1
- scitex/template/_templates/plt.py +1 -1
- scitex/template/_templates/session.py +1 -1
- scitex/template/clone_research_minimal.py +111 -0
- scitex/verify/README.md +300 -0
- scitex/verify/__init__.py +208 -0
- scitex/verify/_chain.py +369 -0
- scitex/verify/_db.py +600 -0
- scitex/verify/_hash.py +187 -0
- scitex/verify/_integration.py +127 -0
- scitex/verify/_rerun.py +253 -0
- scitex/verify/_tracker.py +330 -0
- scitex/verify/_visualize.py +44 -0
- scitex/verify/_viz/__init__.py +38 -0
- scitex/verify/_viz/_colors.py +84 -0
- scitex/verify/_viz/_format.py +302 -0
- scitex/verify/_viz/_json.py +192 -0
- scitex/verify/_viz/_mermaid.py +440 -0
- scitex/verify/_viz/_templates.py +246 -0
- scitex/verify/_viz/_utils.py +56 -0
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/METADATA +2 -1
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/RECORD +69 -28
- scitex/session/_lifecycle.py +0 -827
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/WHEEL +0 -0
- {scitex-2.16.2.dist-info → scitex-2.17.3.dist-info}/entry_points.txt +0 -0
- {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
|