claude-turing 3.4.0 → 3.5.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 (34) hide show
  1. package/.claude-plugin/plugin.json +2 -2
  2. package/README.md +9 -2
  3. package/commands/annotate.md +23 -0
  4. package/commands/archive.md +23 -0
  5. package/commands/cite.md +23 -0
  6. package/commands/flashback.md +22 -0
  7. package/commands/present.md +23 -0
  8. package/commands/replay.md +23 -0
  9. package/commands/search.md +22 -0
  10. package/commands/template.md +22 -0
  11. package/commands/trend.md +21 -0
  12. package/commands/turing.md +14 -0
  13. package/package.json +1 -1
  14. package/src/install.js +1 -0
  15. package/src/verify.js +7 -0
  16. package/templates/scripts/__pycache__/experiment_annotations.cpython-314.pyc +0 -0
  17. package/templates/scripts/__pycache__/experiment_archive.cpython-314.pyc +0 -0
  18. package/templates/scripts/__pycache__/experiment_replay.cpython-314.pyc +0 -0
  19. package/templates/scripts/__pycache__/experiment_search.cpython-314.pyc +0 -0
  20. package/templates/scripts/__pycache__/experiment_templates.cpython-314.pyc +0 -0
  21. package/templates/scripts/__pycache__/scaffold.cpython-314.pyc +0 -0
  22. package/templates/scripts/__pycache__/session_flashback.cpython-314.pyc +0 -0
  23. package/templates/scripts/__pycache__/trend_analysis.cpython-314.pyc +0 -0
  24. package/templates/scripts/citation_manager.py +436 -0
  25. package/templates/scripts/experiment_annotations.py +392 -0
  26. package/templates/scripts/experiment_archive.py +534 -0
  27. package/templates/scripts/experiment_replay.py +592 -0
  28. package/templates/scripts/experiment_search.py +451 -0
  29. package/templates/scripts/experiment_templates.py +501 -0
  30. package/templates/scripts/generate_changelog.py +464 -0
  31. package/templates/scripts/generate_figures.py +597 -0
  32. package/templates/scripts/scaffold.py +12 -0
  33. package/templates/scripts/session_flashback.py +461 -0
  34. package/templates/scripts/trend_analysis.py +503 -0
@@ -0,0 +1,392 @@
1
+ #!/usr/bin/env python3
2
+ """Add human notes and annotations to experiments.
3
+
4
+ Attach contextual observations, insights, and tagged notes to any
5
+ experiment. Annotations are the institutional memory that metrics
6
+ alone cannot capture — "this ran during a data migration" or
7
+ "suspiciously high accuracy, check for leakage".
8
+
9
+ Usage:
10
+ python scripts/experiment_annotations.py add exp-042 "Suspiciously high accuracy"
11
+ python scripts/experiment_annotations.py add exp-042 "Check for leakage" --tags leakage,investigate
12
+ python scripts/experiment_annotations.py list
13
+ python scripts/experiment_annotations.py list exp-042
14
+ python scripts/experiment_annotations.py search "leakage"
15
+ python scripts/experiment_annotations.py search --tag investigate
16
+ python scripts/experiment_annotations.py --json
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import argparse
22
+ import json
23
+ import os
24
+ import sys
25
+ from datetime import datetime, timezone
26
+ from pathlib import Path
27
+
28
+ import yaml
29
+
30
+ from scripts.turing_io import load_config, load_experiments
31
+
32
+ DEFAULT_LOG_PATH = "experiments/log.jsonl"
33
+ DEFAULT_ANNOTATIONS_PATH = "experiments/annotations.yaml"
34
+
35
+
36
+ # --- Storage ---
37
+
38
+
39
+ def load_annotations(path: str = DEFAULT_ANNOTATIONS_PATH) -> list[dict]:
40
+ """Load annotations from YAML file."""
41
+ p = Path(path)
42
+ if not p.exists() or p.stat().st_size == 0:
43
+ return []
44
+ with open(p) as f:
45
+ data = yaml.safe_load(f)
46
+ return data if isinstance(data, list) else []
47
+
48
+
49
+ def save_annotations(annotations: list[dict], path: str = DEFAULT_ANNOTATIONS_PATH) -> Path:
50
+ """Save annotations list to YAML."""
51
+ p = Path(path)
52
+ p.parent.mkdir(parents=True, exist_ok=True)
53
+ with open(p, "w") as f:
54
+ yaml.dump(annotations, f, default_flow_style=False, sort_keys=False)
55
+ return p
56
+
57
+
58
+ def get_next_annotation_id(annotations: list[dict]) -> str:
59
+ """Generate next sequential annotation ID."""
60
+ max_id = 0
61
+ for ann in annotations:
62
+ aid = ann.get("id", "")
63
+ if aid.startswith("ann-"):
64
+ try:
65
+ num = int(aid.split("-")[1])
66
+ max_id = max(max_id, num)
67
+ except (ValueError, IndexError):
68
+ pass
69
+ return f"ann-{max_id + 1:03d}"
70
+
71
+
72
+ # --- Operations ---
73
+
74
+
75
+ def add_annotation(
76
+ experiment_id: str,
77
+ text: str,
78
+ author: str | None = None,
79
+ tags: list[str] | None = None,
80
+ annotations_path: str = DEFAULT_ANNOTATIONS_PATH,
81
+ log_path: str = DEFAULT_LOG_PATH,
82
+ ) -> dict:
83
+ """Add an annotation to an experiment.
84
+
85
+ Args:
86
+ experiment_id: Target experiment ID (e.g., "exp-042").
87
+ text: Annotation text.
88
+ author: Who wrote the annotation. Defaults to $USER.
89
+ tags: Optional list of tags for categorization.
90
+ annotations_path: Path to annotations YAML.
91
+ log_path: Path to experiment log for validation.
92
+
93
+ Returns:
94
+ The created annotation dict.
95
+ """
96
+ experiments = load_experiments(log_path)
97
+ known_ids = {e.get("experiment_id") for e in experiments}
98
+ if experiment_id not in known_ids:
99
+ return {"error": f"Experiment '{experiment_id}' not found in log"}
100
+
101
+ annotations = load_annotations(annotations_path)
102
+ aid = get_next_annotation_id(annotations)
103
+
104
+ annotation = {
105
+ "id": aid,
106
+ "experiment_id": experiment_id,
107
+ "text": text,
108
+ "author": author or os.environ.get("USER", "unknown"),
109
+ "date": datetime.now(timezone.utc).isoformat(),
110
+ "tags": tags or [],
111
+ }
112
+
113
+ annotations.append(annotation)
114
+ save_annotations(annotations, annotations_path)
115
+ return annotation
116
+
117
+
118
+ def list_annotations(
119
+ experiment_id: str | None = None,
120
+ annotations_path: str = DEFAULT_ANNOTATIONS_PATH,
121
+ ) -> list[dict]:
122
+ """List annotations, optionally filtered by experiment.
123
+
124
+ Args:
125
+ experiment_id: If given, filter to this experiment only.
126
+ annotations_path: Path to annotations YAML.
127
+
128
+ Returns:
129
+ Filtered list of annotations.
130
+ """
131
+ annotations = load_annotations(annotations_path)
132
+ if experiment_id:
133
+ return [a for a in annotations if a.get("experiment_id") == experiment_id]
134
+ return annotations
135
+
136
+
137
+ def search_annotations(
138
+ keyword: str | None = None,
139
+ tag: str | None = None,
140
+ annotations_path: str = DEFAULT_ANNOTATIONS_PATH,
141
+ ) -> list[dict]:
142
+ """Search annotations by keyword in text or by tag.
143
+
144
+ Args:
145
+ keyword: Search string to match against annotation text.
146
+ tag: Tag to filter by.
147
+ annotations_path: Path to annotations YAML.
148
+
149
+ Returns:
150
+ List of matching annotations.
151
+ """
152
+ annotations = load_annotations(annotations_path)
153
+ results = []
154
+
155
+ for ann in annotations:
156
+ if keyword:
157
+ text_lower = ann.get("text", "").lower()
158
+ if keyword.lower() not in text_lower:
159
+ continue
160
+ if tag:
161
+ ann_tags = [t.lower() for t in ann.get("tags", [])]
162
+ if tag.lower() not in ann_tags:
163
+ continue
164
+ results.append(ann)
165
+
166
+ return results
167
+
168
+
169
+ def delete_annotation(
170
+ annotation_id: str,
171
+ annotations_path: str = DEFAULT_ANNOTATIONS_PATH,
172
+ ) -> dict:
173
+ """Delete an annotation by ID.
174
+
175
+ Args:
176
+ annotation_id: Annotation ID to remove.
177
+ annotations_path: Path to annotations YAML.
178
+
179
+ Returns:
180
+ Result dict with deleted annotation or error.
181
+ """
182
+ annotations = load_annotations(annotations_path)
183
+ updated = [a for a in annotations if a.get("id") != annotation_id]
184
+
185
+ if len(updated) == len(annotations):
186
+ return {"error": f"Annotation '{annotation_id}' not found"}
187
+
188
+ save_annotations(updated, annotations_path)
189
+ return {"deleted": annotation_id, "remaining": len(updated)}
190
+
191
+
192
+ # --- Report ---
193
+
194
+
195
+ def format_annotations_report(annotations: list[dict], title: str = "Experiment Annotations") -> str:
196
+ """Format annotations as a readable markdown report."""
197
+ if not annotations:
198
+ return "No annotations found."
199
+
200
+ lines = [
201
+ f"# {title}",
202
+ "",
203
+ f"*{len(annotations)} annotation(s)*",
204
+ "",
205
+ "| ID | Experiment | Date | Tags | Text |",
206
+ "|----|-----------|------|------|------|",
207
+ ]
208
+
209
+ for ann in annotations:
210
+ aid = ann.get("id", "?")
211
+ eid = ann.get("experiment_id", "?")
212
+ date = ann.get("date", "?")[:10]
213
+ tags = ", ".join(ann.get("tags", [])) or "—"
214
+ text = ann.get("text", "")
215
+ # Truncate long text for table display
216
+ display_text = text[:60] + "..." if len(text) > 60 else text
217
+ lines.append(f"| {aid} | {eid} | {date} | {tags} | {display_text} |")
218
+
219
+ # Group summary by experiment
220
+ exp_counts: dict[str, int] = {}
221
+ for ann in annotations:
222
+ eid = ann.get("experiment_id", "?")
223
+ exp_counts[eid] = exp_counts.get(eid, 0) + 1
224
+
225
+ if len(exp_counts) > 1:
226
+ lines.extend(["", "## By Experiment", ""])
227
+ for eid, count in sorted(exp_counts.items(), key=lambda x: -x[1]):
228
+ lines.append(f"- **{eid}**: {count} annotation(s)")
229
+
230
+ # Tag summary
231
+ tag_counts: dict[str, int] = {}
232
+ for ann in annotations:
233
+ for tag in ann.get("tags", []):
234
+ tag_counts[tag] = tag_counts.get(tag, 0) + 1
235
+
236
+ if tag_counts:
237
+ lines.extend(["", "## By Tag", ""])
238
+ for tag, count in sorted(tag_counts.items(), key=lambda x: -x[1]):
239
+ lines.append(f"- `{tag}`: {count}")
240
+
241
+ return "\n".join(lines)
242
+
243
+
244
+ def save_annotations_report(report: dict, path: str = "experiments/annotations") -> Path:
245
+ """Save annotations report to YAML."""
246
+ p = Path(path)
247
+ p.mkdir(parents=True, exist_ok=True)
248
+ out = p / f"report-{datetime.now(timezone.utc).strftime('%Y%m%d-%H%M%S')}.yaml"
249
+ with open(out, "w") as f:
250
+ yaml.dump(report, f, default_flow_style=False, sort_keys=False)
251
+ return out
252
+
253
+
254
+ # --- Orchestration ---
255
+
256
+
257
+ def run_annotations(
258
+ action: str,
259
+ experiment_id: str | None = None,
260
+ text: str | None = None,
261
+ author: str | None = None,
262
+ tags: list[str] | None = None,
263
+ keyword: str | None = None,
264
+ tag: str | None = None,
265
+ annotation_id: str | None = None,
266
+ annotations_path: str = DEFAULT_ANNOTATIONS_PATH,
267
+ log_path: str = DEFAULT_LOG_PATH,
268
+ ) -> dict:
269
+ """Run annotation operation.
270
+
271
+ Args:
272
+ action: One of add, list, search, delete.
273
+ experiment_id: Target experiment (for add/list).
274
+ text: Annotation text (for add).
275
+ author: Annotation author (for add).
276
+ tags: Tags list (for add).
277
+ keyword: Search keyword (for search).
278
+ tag: Search tag (for search).
279
+ annotation_id: Annotation ID (for delete).
280
+ annotations_path: Path to annotations YAML.
281
+ log_path: Path to experiment log.
282
+
283
+ Returns:
284
+ Result dict.
285
+ """
286
+ timestamp = datetime.now(timezone.utc).isoformat()
287
+
288
+ if action == "add":
289
+ if not experiment_id or not text:
290
+ return {"error": "Both experiment_id and text are required for add"}
291
+ result = add_annotation(experiment_id, text, author, tags,
292
+ annotations_path, log_path)
293
+ if "error" in result:
294
+ return {"timestamp": timestamp, **result}
295
+ return {"timestamp": timestamp, "action": "add", "annotation": result}
296
+
297
+ elif action == "list":
298
+ results = list_annotations(experiment_id, annotations_path)
299
+ return {
300
+ "timestamp": timestamp,
301
+ "action": "list",
302
+ "filter": {"experiment_id": experiment_id},
303
+ "count": len(results),
304
+ "annotations": results,
305
+ }
306
+
307
+ elif action == "search":
308
+ if not keyword and not tag:
309
+ return {"error": "Provide --search keyword or --tag for search"}
310
+ results = search_annotations(keyword, tag, annotations_path)
311
+ return {
312
+ "timestamp": timestamp,
313
+ "action": "search",
314
+ "filter": {"keyword": keyword, "tag": tag},
315
+ "count": len(results),
316
+ "annotations": results,
317
+ }
318
+
319
+ elif action == "delete":
320
+ if not annotation_id:
321
+ return {"error": "Provide annotation ID for delete"}
322
+ result = delete_annotation(annotation_id, annotations_path)
323
+ return {"timestamp": timestamp, "action": "delete", **result}
324
+
325
+ return {"error": f"Unknown action: {action}"}
326
+
327
+
328
+ def main() -> None:
329
+ """CLI entry point."""
330
+ parser = argparse.ArgumentParser(description="Add human notes to experiments")
331
+ parser.add_argument("action", choices=["add", "list", "search", "delete"],
332
+ help="Annotation action")
333
+ parser.add_argument("experiment_id", nargs="?", default=None,
334
+ help="Experiment ID (for add/list)")
335
+ parser.add_argument("text", nargs="?", default=None,
336
+ help="Annotation text (for add)")
337
+ parser.add_argument("--author", default=None, help="Annotation author")
338
+ parser.add_argument("--tags", default=None,
339
+ help="Comma-separated tags (e.g., leakage,investigate)")
340
+ parser.add_argument("--search", dest="keyword", default=None,
341
+ help="Search keyword in annotation text")
342
+ parser.add_argument("--tag", default=None,
343
+ help="Filter by tag")
344
+ parser.add_argument("--annotation-id", default=None,
345
+ help="Annotation ID (for delete)")
346
+ parser.add_argument("--config", default="config.yaml", help="Path to config.yaml")
347
+ parser.add_argument("--log", default=DEFAULT_LOG_PATH, help="Path to experiment log")
348
+ parser.add_argument("--annotations-path", default=DEFAULT_ANNOTATIONS_PATH,
349
+ help="Path to annotations YAML")
350
+ parser.add_argument("--json", action="store_true", help="Output raw JSON")
351
+ args = parser.parse_args()
352
+
353
+ tags = [t.strip() for t in args.tags.split(",")] if args.tags else None
354
+
355
+ report = run_annotations(
356
+ action=args.action,
357
+ experiment_id=args.experiment_id,
358
+ text=args.text,
359
+ author=args.author,
360
+ tags=tags,
361
+ keyword=args.keyword,
362
+ tag=args.tag,
363
+ annotation_id=args.annotation_id,
364
+ annotations_path=args.annotations_path,
365
+ log_path=args.log,
366
+ )
367
+
368
+ if args.json:
369
+ print(json.dumps(report, indent=2, default=str))
370
+ else:
371
+ if "error" in report:
372
+ print(f"ERROR: {report['error']}", file=sys.stderr)
373
+ sys.exit(1)
374
+ annotations = report.get("annotations", [])
375
+ if report.get("action") == "add":
376
+ ann = report["annotation"]
377
+ tags_str = f" [{', '.join(ann['tags'])}]" if ann.get("tags") else ""
378
+ print(f"Added {ann['id']} to {ann['experiment_id']}: "
379
+ f"{ann['text']}{tags_str}")
380
+ elif annotations or report.get("action") in ("list", "search"):
381
+ title = "Experiment Annotations"
382
+ if report.get("action") == "search":
383
+ title = "Search Results"
384
+ print(format_annotations_report(annotations, title))
385
+ elif report.get("action") == "delete":
386
+ print(f"Deleted annotation {report.get('deleted', '?')}")
387
+ else:
388
+ print(json.dumps(report, indent=2, default=str))
389
+
390
+
391
+ if __name__ == "__main__":
392
+ main()