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.
- package/.claude-plugin/plugin.json +2 -2
- package/README.md +9 -2
- package/commands/annotate.md +23 -0
- package/commands/archive.md +23 -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 +14 -0
- package/package.json +1 -1
- package/src/install.js +1 -0
- package/src/verify.js +7 -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__/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 +12 -0
- package/templates/scripts/session_flashback.py +461 -0
- 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()
|