claude-turing 3.4.0 → 4.0.0
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.
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +12 -2
- package/commands/annotate.md +23 -0
- package/commands/archive.md +23 -0
- package/commands/changelog.md +22 -0
- package/commands/cite.md +23 -0
- package/commands/flashback.md +22 -0
- package/commands/present.md +23 -0
- package/commands/replay.md +23 -0
- package/commands/search.md +22 -0
- package/commands/template.md +22 -0
- package/commands/trend.md +21 -0
- package/commands/turing.md +20 -0
- package/package.json +1 -1
- package/src/install.js +2 -0
- package/src/verify.js +10 -0
- package/templates/scripts/__pycache__/citation_manager.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/experiment_annotations.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/experiment_archive.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/experiment_replay.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/experiment_search.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/experiment_templates.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/generate_changelog.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/generate_figures.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/session_flashback.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/trend_analysis.cpython-314.pyc +0 -0
- package/templates/scripts/citation_manager.py +436 -0
- package/templates/scripts/experiment_annotations.py +392 -0
- package/templates/scripts/experiment_archive.py +534 -0
- package/templates/scripts/experiment_replay.py +592 -0
- package/templates/scripts/experiment_search.py +451 -0
- package/templates/scripts/experiment_templates.py +501 -0
- package/templates/scripts/generate_changelog.py +464 -0
- package/templates/scripts/generate_figures.py +597 -0
- package/templates/scripts/scaffold.py +17 -0
- package/templates/scripts/session_flashback.py +461 -0
- package/templates/scripts/trend_analysis.py +503 -0
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Save, list, and apply reusable experiment configuration templates.
|
|
3
|
+
|
|
4
|
+
Extract proven configurations from successful experiments, strip
|
|
5
|
+
project-specific paths, and save as portable templates. Apply them
|
|
6
|
+
to new projects to bootstrap experiments without rediscovering
|
|
7
|
+
hyperparameters.
|
|
8
|
+
|
|
9
|
+
Templates are stored in ~/.turing/templates/ for cross-project reuse.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
python scripts/experiment_templates.py save exp-042 "gradient-boost-tuned"
|
|
13
|
+
python scripts/experiment_templates.py save exp-042 "gb-tuned" --description "Tuned GBM config"
|
|
14
|
+
python scripts/experiment_templates.py list
|
|
15
|
+
python scripts/experiment_templates.py show gradient-boost-tuned
|
|
16
|
+
python scripts/experiment_templates.py apply gradient-boost-tuned
|
|
17
|
+
python scripts/experiment_templates.py apply gradient-boost-tuned --output custom-config.yaml
|
|
18
|
+
python scripts/experiment_templates.py delete gradient-boost-tuned
|
|
19
|
+
python scripts/experiment_templates.py --json
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
from datetime import datetime, timezone
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
import yaml
|
|
32
|
+
|
|
33
|
+
from scripts.turing_io import load_config, load_experiments
|
|
34
|
+
|
|
35
|
+
DEFAULT_LOG_PATH = "experiments/log.jsonl"
|
|
36
|
+
DEFAULT_TEMPLATE_DIR = Path.home() / ".turing" / "templates"
|
|
37
|
+
|
|
38
|
+
# Keys that are project-specific and should be stripped from templates
|
|
39
|
+
STRIP_KEYS = {
|
|
40
|
+
"data_path", "data_dir", "output_dir", "output_path", "log_path",
|
|
41
|
+
"checkpoint_dir", "cache_dir", "model_path", "save_path",
|
|
42
|
+
"experiment_id", "timestamp", "run_id",
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Keys that contain path-like values
|
|
46
|
+
PATH_PATTERNS = [
|
|
47
|
+
re.compile(r"^(/|~|\.\.?/)"), # Absolute or relative paths
|
|
48
|
+
re.compile(r"\.(csv|parquet|jsonl|pkl|h5|pt|pth|ckpt)$"), # File extensions
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# --- Template Operations ---
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def extract_template(
|
|
56
|
+
experiment: dict,
|
|
57
|
+
strip_paths: bool = True,
|
|
58
|
+
) -> dict:
|
|
59
|
+
"""Extract a portable template from an experiment config.
|
|
60
|
+
|
|
61
|
+
Strips project-specific paths and identifiers to make the config
|
|
62
|
+
reusable across projects.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
experiment: Full experiment dict from log.
|
|
66
|
+
strip_paths: Whether to remove path-like values.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Cleaned config dict suitable for a template.
|
|
70
|
+
"""
|
|
71
|
+
config = dict(experiment.get("config", {}))
|
|
72
|
+
|
|
73
|
+
# Strip known project-specific keys
|
|
74
|
+
for key in STRIP_KEYS:
|
|
75
|
+
config.pop(key, None)
|
|
76
|
+
|
|
77
|
+
# Strip values that look like file paths
|
|
78
|
+
if strip_paths:
|
|
79
|
+
config = _strip_path_values(config)
|
|
80
|
+
|
|
81
|
+
return config
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _strip_path_values(d: dict) -> dict:
|
|
85
|
+
"""Recursively remove values that look like file paths."""
|
|
86
|
+
cleaned = {}
|
|
87
|
+
for k, v in d.items():
|
|
88
|
+
if isinstance(v, str):
|
|
89
|
+
is_path = any(p.search(v) for p in PATH_PATTERNS)
|
|
90
|
+
if is_path:
|
|
91
|
+
cleaned[k] = f"<{k}>" # Placeholder
|
|
92
|
+
else:
|
|
93
|
+
cleaned[k] = v
|
|
94
|
+
elif isinstance(v, dict):
|
|
95
|
+
cleaned[k] = _strip_path_values(v)
|
|
96
|
+
else:
|
|
97
|
+
cleaned[k] = v
|
|
98
|
+
return cleaned
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def save_template(
|
|
102
|
+
name: str,
|
|
103
|
+
config: dict,
|
|
104
|
+
description: str = "",
|
|
105
|
+
source_experiment: str | None = None,
|
|
106
|
+
source_metrics: dict | None = None,
|
|
107
|
+
template_dir: str | None = None,
|
|
108
|
+
) -> dict:
|
|
109
|
+
"""Save a template to the templates directory.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
name: Template name (used as filename).
|
|
113
|
+
config: Extracted configuration dict.
|
|
114
|
+
description: Human-readable description.
|
|
115
|
+
source_experiment: ID of the source experiment.
|
|
116
|
+
source_metrics: Metrics from the source experiment.
|
|
117
|
+
template_dir: Override template directory.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Result dict with path and metadata.
|
|
121
|
+
"""
|
|
122
|
+
tdir = Path(template_dir) if template_dir else DEFAULT_TEMPLATE_DIR
|
|
123
|
+
tdir.mkdir(parents=True, exist_ok=True)
|
|
124
|
+
|
|
125
|
+
# Sanitize name for filename
|
|
126
|
+
safe_name = re.sub(r"[^a-zA-Z0-9_-]", "-", name)
|
|
127
|
+
path = tdir / f"{safe_name}.yaml"
|
|
128
|
+
|
|
129
|
+
template = {
|
|
130
|
+
"name": name,
|
|
131
|
+
"description": description,
|
|
132
|
+
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
133
|
+
"source": {
|
|
134
|
+
"experiment_id": source_experiment,
|
|
135
|
+
"metrics": source_metrics or {},
|
|
136
|
+
"project": Path.cwd().name,
|
|
137
|
+
},
|
|
138
|
+
"config": config,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
with open(path, "w") as f:
|
|
142
|
+
yaml.dump(template, f, default_flow_style=False, sort_keys=False)
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
"name": name,
|
|
146
|
+
"path": str(path),
|
|
147
|
+
"config_keys": list(config.keys()),
|
|
148
|
+
"source_experiment": source_experiment,
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def list_templates(template_dir: str | None = None) -> list[dict]:
|
|
153
|
+
"""List all saved templates.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
template_dir: Override template directory.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
List of template summary dicts.
|
|
160
|
+
"""
|
|
161
|
+
tdir = Path(template_dir) if template_dir else DEFAULT_TEMPLATE_DIR
|
|
162
|
+
if not tdir.exists():
|
|
163
|
+
return []
|
|
164
|
+
|
|
165
|
+
templates = []
|
|
166
|
+
for path in sorted(tdir.glob("*.yaml")):
|
|
167
|
+
try:
|
|
168
|
+
with open(path) as f:
|
|
169
|
+
data = yaml.safe_load(f)
|
|
170
|
+
if not isinstance(data, dict):
|
|
171
|
+
continue
|
|
172
|
+
templates.append({
|
|
173
|
+
"name": data.get("name", path.stem),
|
|
174
|
+
"description": data.get("description", ""),
|
|
175
|
+
"created_at": data.get("created_at", ""),
|
|
176
|
+
"source": data.get("source", {}),
|
|
177
|
+
"config_keys": list(data.get("config", {}).keys()),
|
|
178
|
+
"path": str(path),
|
|
179
|
+
})
|
|
180
|
+
except (yaml.YAMLError, OSError):
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
return templates
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def show_template(name: str, template_dir: str | None = None) -> dict | None:
|
|
187
|
+
"""Load and return a template by name.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
name: Template name.
|
|
191
|
+
template_dir: Override template directory.
|
|
192
|
+
|
|
193
|
+
Returns:
|
|
194
|
+
Full template dict, or None if not found.
|
|
195
|
+
"""
|
|
196
|
+
tdir = Path(template_dir) if template_dir else DEFAULT_TEMPLATE_DIR
|
|
197
|
+
safe_name = re.sub(r"[^a-zA-Z0-9_-]", "-", name)
|
|
198
|
+
path = tdir / f"{safe_name}.yaml"
|
|
199
|
+
|
|
200
|
+
if not path.exists():
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
with open(path) as f:
|
|
204
|
+
return yaml.safe_load(f)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def apply_template(
|
|
208
|
+
name: str,
|
|
209
|
+
output_path: str = "config.yaml",
|
|
210
|
+
overrides: dict | None = None,
|
|
211
|
+
template_dir: str | None = None,
|
|
212
|
+
) -> dict:
|
|
213
|
+
"""Apply a template to generate a project config.
|
|
214
|
+
|
|
215
|
+
Loads the template, merges with any existing config, applies
|
|
216
|
+
overrides, and fills in project-specific placeholders.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
name: Template name to apply.
|
|
220
|
+
output_path: Where to write the generated config.
|
|
221
|
+
overrides: Dict of key=value overrides to apply on top.
|
|
222
|
+
template_dir: Override template directory.
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Result dict with generated config and path.
|
|
226
|
+
"""
|
|
227
|
+
template = show_template(name, template_dir)
|
|
228
|
+
if template is None:
|
|
229
|
+
return {"error": f"Template '{name}' not found"}
|
|
230
|
+
|
|
231
|
+
template_config = template.get("config", {})
|
|
232
|
+
|
|
233
|
+
# Load existing config and merge (template values take precedence)
|
|
234
|
+
existing = load_config(output_path)
|
|
235
|
+
merged = _deep_merge(existing, template_config)
|
|
236
|
+
|
|
237
|
+
# Apply overrides
|
|
238
|
+
if overrides:
|
|
239
|
+
merged = _deep_merge(merged, overrides)
|
|
240
|
+
|
|
241
|
+
# Replace placeholders with sensible defaults
|
|
242
|
+
merged = _fill_placeholders(merged)
|
|
243
|
+
|
|
244
|
+
# Write config
|
|
245
|
+
out = Path(output_path)
|
|
246
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
247
|
+
with open(out, "w") as f:
|
|
248
|
+
yaml.dump(merged, f, default_flow_style=False, sort_keys=False)
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
"template": name,
|
|
252
|
+
"output_path": str(out),
|
|
253
|
+
"config_keys": list(merged.keys()),
|
|
254
|
+
"source_description": template.get("description", ""),
|
|
255
|
+
"applied_overrides": list((overrides or {}).keys()),
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def delete_template(name: str, template_dir: str | None = None) -> dict:
|
|
260
|
+
"""Delete a template by name.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
name: Template name.
|
|
264
|
+
template_dir: Override template directory.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
Result dict.
|
|
268
|
+
"""
|
|
269
|
+
tdir = Path(template_dir) if template_dir else DEFAULT_TEMPLATE_DIR
|
|
270
|
+
safe_name = re.sub(r"[^a-zA-Z0-9_-]", "-", name)
|
|
271
|
+
path = tdir / f"{safe_name}.yaml"
|
|
272
|
+
|
|
273
|
+
if not path.exists():
|
|
274
|
+
return {"error": f"Template '{name}' not found"}
|
|
275
|
+
|
|
276
|
+
path.unlink()
|
|
277
|
+
return {"deleted": name, "path": str(path)}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _deep_merge(base: dict, override: dict) -> dict:
|
|
281
|
+
"""Deep merge two dicts. Override values take precedence."""
|
|
282
|
+
result = dict(base)
|
|
283
|
+
for k, v in override.items():
|
|
284
|
+
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
|
|
285
|
+
result[k] = _deep_merge(result[k], v)
|
|
286
|
+
else:
|
|
287
|
+
result[k] = v
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _fill_placeholders(config: dict) -> dict:
|
|
292
|
+
"""Replace <key> placeholders with reasonable defaults."""
|
|
293
|
+
filled = {}
|
|
294
|
+
for k, v in config.items():
|
|
295
|
+
if isinstance(v, str) and v.startswith("<") and v.endswith(">"):
|
|
296
|
+
# Leave placeholder — user should fill these
|
|
297
|
+
filled[k] = v
|
|
298
|
+
elif isinstance(v, dict):
|
|
299
|
+
filled[k] = _fill_placeholders(v)
|
|
300
|
+
else:
|
|
301
|
+
filled[k] = v
|
|
302
|
+
return filled
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# --- Report ---
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def format_templates_report(templates: list[dict]) -> str:
|
|
309
|
+
"""Format template list as a readable markdown table."""
|
|
310
|
+
if not templates:
|
|
311
|
+
return "No templates found.\n\nSave one: `experiment_templates.py save <exp-id> <name>`"
|
|
312
|
+
|
|
313
|
+
lines = [
|
|
314
|
+
"# Experiment Templates",
|
|
315
|
+
"",
|
|
316
|
+
f"*{len(templates)} template(s) in `{DEFAULT_TEMPLATE_DIR}`*",
|
|
317
|
+
"",
|
|
318
|
+
"| Name | Description | Source | Created | Config Keys |",
|
|
319
|
+
"|------|-------------|--------|---------|-------------|",
|
|
320
|
+
]
|
|
321
|
+
|
|
322
|
+
for t in templates:
|
|
323
|
+
name = t.get("name", "?")
|
|
324
|
+
desc = t.get("description", "—") or "—"
|
|
325
|
+
source = t.get("source", {})
|
|
326
|
+
src_exp = source.get("experiment_id", "—") or "—"
|
|
327
|
+
src_proj = source.get("project", "") or ""
|
|
328
|
+
source_display = f"{src_exp}"
|
|
329
|
+
if src_proj:
|
|
330
|
+
source_display += f" ({src_proj})"
|
|
331
|
+
created = t.get("created_at", "?")[:10]
|
|
332
|
+
n_keys = len(t.get("config_keys", []))
|
|
333
|
+
lines.append(f"| {name} | {desc[:40]} | {source_display} | {created} | {n_keys} |")
|
|
334
|
+
|
|
335
|
+
return "\n".join(lines)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def format_template_detail(template: dict) -> str:
|
|
339
|
+
"""Format a single template in detail."""
|
|
340
|
+
lines = [
|
|
341
|
+
f"# Template: {template.get('name', '?')}",
|
|
342
|
+
"",
|
|
343
|
+
f"**Description:** {template.get('description', '—')}",
|
|
344
|
+
f"**Created:** {template.get('created_at', '?')[:19]} UTC",
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
source = template.get("source", {})
|
|
348
|
+
if source.get("experiment_id"):
|
|
349
|
+
lines.append(f"**Source:** {source['experiment_id']} from {source.get('project', '?')}")
|
|
350
|
+
if source.get("metrics"):
|
|
351
|
+
metrics_str = ", ".join(f"{k}={v}" for k, v in source["metrics"].items())
|
|
352
|
+
lines.append(f"**Source Metrics:** {metrics_str}")
|
|
353
|
+
|
|
354
|
+
lines.extend(["", "## Configuration", "", "```yaml"])
|
|
355
|
+
config = template.get("config", {})
|
|
356
|
+
lines.append(yaml.dump(config, default_flow_style=False, sort_keys=False).strip())
|
|
357
|
+
lines.extend(["```", ""])
|
|
358
|
+
|
|
359
|
+
return "\n".join(lines)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def save_templates_report(report: dict, path: str = "experiments/templates") -> Path:
|
|
363
|
+
"""Save template operation report to YAML."""
|
|
364
|
+
p = Path(path)
|
|
365
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
366
|
+
out = p / f"template-op-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}.yaml"
|
|
367
|
+
with open(out, "w") as f:
|
|
368
|
+
yaml.dump(report, f, default_flow_style=False, sort_keys=False)
|
|
369
|
+
return out
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# --- Orchestration ---
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def run_template(
|
|
376
|
+
action: str,
|
|
377
|
+
name: str | None = None,
|
|
378
|
+
experiment_id: str | None = None,
|
|
379
|
+
description: str = "",
|
|
380
|
+
output_path: str = "config.yaml",
|
|
381
|
+
template_dir: str | None = None,
|
|
382
|
+
log_path: str = DEFAULT_LOG_PATH,
|
|
383
|
+
config_path: str = "config.yaml",
|
|
384
|
+
) -> dict:
|
|
385
|
+
"""Run template operation.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
Result dict.
|
|
389
|
+
"""
|
|
390
|
+
timestamp = datetime.now(timezone.utc).isoformat()
|
|
391
|
+
|
|
392
|
+
if action == "save":
|
|
393
|
+
if not experiment_id or not name:
|
|
394
|
+
return {"error": "Both experiment_id and name are required for save"}
|
|
395
|
+
|
|
396
|
+
experiments = load_experiments(log_path)
|
|
397
|
+
exp = next((e for e in experiments
|
|
398
|
+
if e.get("experiment_id") == experiment_id), None)
|
|
399
|
+
if exp is None:
|
|
400
|
+
return {"error": f"Experiment '{experiment_id}' not found"}
|
|
401
|
+
|
|
402
|
+
config = extract_template(exp)
|
|
403
|
+
result = save_template(
|
|
404
|
+
name, config, description,
|
|
405
|
+
source_experiment=experiment_id,
|
|
406
|
+
source_metrics=exp.get("metrics"),
|
|
407
|
+
template_dir=template_dir,
|
|
408
|
+
)
|
|
409
|
+
return {"timestamp": timestamp, "action": "save", **result}
|
|
410
|
+
|
|
411
|
+
elif action == "list":
|
|
412
|
+
templates = list_templates(template_dir)
|
|
413
|
+
return {
|
|
414
|
+
"timestamp": timestamp,
|
|
415
|
+
"action": "list",
|
|
416
|
+
"count": len(templates),
|
|
417
|
+
"templates": templates,
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
elif action == "show":
|
|
421
|
+
if not name:
|
|
422
|
+
return {"error": "Template name required for show"}
|
|
423
|
+
template = show_template(name, template_dir)
|
|
424
|
+
if template is None:
|
|
425
|
+
return {"error": f"Template '{name}' not found"}
|
|
426
|
+
return {"timestamp": timestamp, "action": "show", "template": template}
|
|
427
|
+
|
|
428
|
+
elif action == "apply":
|
|
429
|
+
if not name:
|
|
430
|
+
return {"error": "Template name required for apply"}
|
|
431
|
+
result = apply_template(name, output_path, template_dir=template_dir)
|
|
432
|
+
if "error" in result:
|
|
433
|
+
return {"timestamp": timestamp, **result}
|
|
434
|
+
return {"timestamp": timestamp, "action": "apply", **result}
|
|
435
|
+
|
|
436
|
+
elif action == "delete":
|
|
437
|
+
if not name:
|
|
438
|
+
return {"error": "Template name required for delete"}
|
|
439
|
+
result = delete_template(name, template_dir)
|
|
440
|
+
return {"timestamp": timestamp, "action": "delete", **result}
|
|
441
|
+
|
|
442
|
+
return {"error": f"Unknown action: {action}"}
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def main() -> None:
|
|
446
|
+
"""CLI entry point."""
|
|
447
|
+
parser = argparse.ArgumentParser(description="Experiment configuration templates")
|
|
448
|
+
parser.add_argument("action", choices=["save", "list", "show", "apply", "delete"],
|
|
449
|
+
help="Template action")
|
|
450
|
+
parser.add_argument("name", nargs="?", default=None,
|
|
451
|
+
help="Template name (for save/show/apply/delete)")
|
|
452
|
+
parser.add_argument("--experiment", default=None,
|
|
453
|
+
help="Source experiment ID (for save)")
|
|
454
|
+
parser.add_argument("--description", default="",
|
|
455
|
+
help="Template description (for save)")
|
|
456
|
+
parser.add_argument("--output", default="config.yaml",
|
|
457
|
+
help="Output config path (for apply)")
|
|
458
|
+
parser.add_argument("--template-dir", default=None,
|
|
459
|
+
help="Override template directory")
|
|
460
|
+
parser.add_argument("--config", default="config.yaml", help="Path to config.yaml")
|
|
461
|
+
parser.add_argument("--log", default=DEFAULT_LOG_PATH, help="Path to experiment log")
|
|
462
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
463
|
+
args = parser.parse_args()
|
|
464
|
+
|
|
465
|
+
report = run_template(
|
|
466
|
+
action=args.action,
|
|
467
|
+
name=args.name,
|
|
468
|
+
experiment_id=args.experiment,
|
|
469
|
+
description=args.description,
|
|
470
|
+
output_path=args.output,
|
|
471
|
+
template_dir=args.template_dir,
|
|
472
|
+
log_path=args.log,
|
|
473
|
+
config_path=args.config,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
if args.json:
|
|
477
|
+
print(json.dumps(report, indent=2, default=str))
|
|
478
|
+
else:
|
|
479
|
+
if "error" in report:
|
|
480
|
+
print(f"ERROR: {report['error']}", file=sys.stderr)
|
|
481
|
+
sys.exit(1)
|
|
482
|
+
|
|
483
|
+
action = report.get("action")
|
|
484
|
+
if action == "save":
|
|
485
|
+
print(f"Saved template '{report['name']}' to {report['path']}")
|
|
486
|
+
print(f" Source: {report.get('source_experiment', '?')}")
|
|
487
|
+
print(f" Config keys: {', '.join(report.get('config_keys', []))}")
|
|
488
|
+
elif action == "list":
|
|
489
|
+
print(format_templates_report(report.get("templates", [])))
|
|
490
|
+
elif action == "show":
|
|
491
|
+
print(format_template_detail(report.get("template", {})))
|
|
492
|
+
elif action == "apply":
|
|
493
|
+
print(f"Applied template '{report['template']}' to {report['output_path']}")
|
|
494
|
+
if report.get("applied_overrides"):
|
|
495
|
+
print(f" Overrides: {', '.join(report['applied_overrides'])}")
|
|
496
|
+
elif action == "delete":
|
|
497
|
+
print(f"Deleted template '{report.get('deleted', '?')}'")
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
if __name__ == "__main__":
|
|
501
|
+
main()
|