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.
Files changed (38) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/README.md +12 -2
  3. package/commands/annotate.md +23 -0
  4. package/commands/archive.md +23 -0
  5. package/commands/changelog.md +22 -0
  6. package/commands/cite.md +23 -0
  7. package/commands/flashback.md +22 -0
  8. package/commands/present.md +23 -0
  9. package/commands/replay.md +23 -0
  10. package/commands/search.md +22 -0
  11. package/commands/template.md +22 -0
  12. package/commands/trend.md +21 -0
  13. package/commands/turing.md +20 -0
  14. package/package.json +1 -1
  15. package/src/install.js +2 -0
  16. package/src/verify.js +10 -0
  17. package/templates/scripts/__pycache__/citation_manager.cpython-314.pyc +0 -0
  18. package/templates/scripts/__pycache__/experiment_annotations.cpython-314.pyc +0 -0
  19. package/templates/scripts/__pycache__/experiment_archive.cpython-314.pyc +0 -0
  20. package/templates/scripts/__pycache__/experiment_replay.cpython-314.pyc +0 -0
  21. package/templates/scripts/__pycache__/experiment_search.cpython-314.pyc +0 -0
  22. package/templates/scripts/__pycache__/experiment_templates.cpython-314.pyc +0 -0
  23. package/templates/scripts/__pycache__/generate_changelog.cpython-314.pyc +0 -0
  24. package/templates/scripts/__pycache__/generate_figures.cpython-314.pyc +0 -0
  25. package/templates/scripts/__pycache__/scaffold.cpython-314.pyc +0 -0
  26. package/templates/scripts/__pycache__/session_flashback.cpython-314.pyc +0 -0
  27. package/templates/scripts/__pycache__/trend_analysis.cpython-314.pyc +0 -0
  28. package/templates/scripts/citation_manager.py +436 -0
  29. package/templates/scripts/experiment_annotations.py +392 -0
  30. package/templates/scripts/experiment_archive.py +534 -0
  31. package/templates/scripts/experiment_replay.py +592 -0
  32. package/templates/scripts/experiment_search.py +451 -0
  33. package/templates/scripts/experiment_templates.py +501 -0
  34. package/templates/scripts/generate_changelog.py +464 -0
  35. package/templates/scripts/generate_figures.py +597 -0
  36. package/templates/scripts/scaffold.py +17 -0
  37. package/templates/scripts/session_flashback.py +461 -0
  38. 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()