claude-turing 2.5.0 → 3.1.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 +7 -2
- package/commands/audit.md +56 -0
- package/commands/baseline.md +45 -0
- package/commands/leak.md +47 -0
- package/commands/sanity.md +48 -0
- package/commands/transfer.md +54 -0
- package/commands/turing.md +10 -0
- package/package.json +1 -1
- package/src/install.js +2 -0
- package/src/verify.js +5 -0
- package/templates/scripts/__pycache__/generate_baselines.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/generate_brief.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/knowledge_transfer.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/leakage_detector.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/methodology_audit.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/sanity_checks.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/templates/scripts/generate_baselines.py +423 -0
- package/templates/scripts/generate_brief.py +41 -0
- package/templates/scripts/knowledge_transfer.py +618 -0
- package/templates/scripts/leakage_detector.py +402 -0
- package/templates/scripts/methodology_audit.py +451 -0
- package/templates/scripts/sanity_checks.py +503 -0
- package/templates/scripts/scaffold.py +10 -0
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Targeted leakage detection for the autoresearch pipeline.
|
|
3
|
+
|
|
4
|
+
Probes for data leakage by training on single features, checking
|
|
5
|
+
feature-target correlations, detecting train/test overlap, and flagging
|
|
6
|
+
suspiciously predictive features.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python scripts/leakage_detector.py
|
|
10
|
+
python scripts/leakage_detector.py --deep
|
|
11
|
+
python scripts/leakage_detector.py --features "feature_1,feature_2"
|
|
12
|
+
python scripts/leakage_detector.py --json
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
import yaml
|
|
26
|
+
|
|
27
|
+
from scripts.turing_io import load_config
|
|
28
|
+
|
|
29
|
+
DEFAULT_CORRELATION_THRESHOLD = 0.95
|
|
30
|
+
DEFAULT_SINGLE_FEATURE_RATIO = 0.80 # Flag if single feature achieves >80% of full model
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --- Leakage Checks ---
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def check_feature_target_correlation(
|
|
37
|
+
X: np.ndarray,
|
|
38
|
+
y: np.ndarray,
|
|
39
|
+
feature_names: list[str] | None = None,
|
|
40
|
+
threshold: float = DEFAULT_CORRELATION_THRESHOLD,
|
|
41
|
+
) -> list[dict]:
|
|
42
|
+
"""Check for features with very high correlation to the target.
|
|
43
|
+
|
|
44
|
+
Returns list of flagged features.
|
|
45
|
+
"""
|
|
46
|
+
if X.ndim == 1:
|
|
47
|
+
X = X.reshape(-1, 1)
|
|
48
|
+
|
|
49
|
+
n_features = X.shape[1]
|
|
50
|
+
if feature_names is None:
|
|
51
|
+
feature_names = [f"feature_{i}" for i in range(n_features)]
|
|
52
|
+
|
|
53
|
+
flags = []
|
|
54
|
+
for i in range(n_features):
|
|
55
|
+
feature = X[:, i].astype(float)
|
|
56
|
+
target = y.astype(float)
|
|
57
|
+
|
|
58
|
+
# Skip non-numeric
|
|
59
|
+
if np.any(np.isnan(feature)) or np.any(np.isnan(target)):
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if np.std(feature) == 0 or np.std(target) == 0:
|
|
63
|
+
continue
|
|
64
|
+
|
|
65
|
+
corr = abs(float(np.corrcoef(feature, target)[0, 1]))
|
|
66
|
+
|
|
67
|
+
if corr > threshold:
|
|
68
|
+
flags.append({
|
|
69
|
+
"feature": feature_names[i] if i < len(feature_names) else f"feature_{i}",
|
|
70
|
+
"correlation": round(corr, 4),
|
|
71
|
+
"severity": "critical" if corr > 0.99 else "high",
|
|
72
|
+
"reason": f"Correlation {corr:.4f} with target — likely derived from target",
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
return flags
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def check_single_feature_predictiveness(
|
|
79
|
+
X: np.ndarray,
|
|
80
|
+
y: np.ndarray,
|
|
81
|
+
full_model_score: float,
|
|
82
|
+
feature_names: list[str] | None = None,
|
|
83
|
+
ratio_threshold: float = DEFAULT_SINGLE_FEATURE_RATIO,
|
|
84
|
+
task_type: str = "classification",
|
|
85
|
+
) -> list[dict]:
|
|
86
|
+
"""Train a simple model on each feature individually.
|
|
87
|
+
|
|
88
|
+
Flags features where single-feature accuracy > ratio_threshold * full_model_score.
|
|
89
|
+
"""
|
|
90
|
+
if X.ndim == 1:
|
|
91
|
+
X = X.reshape(-1, 1)
|
|
92
|
+
|
|
93
|
+
n_features = X.shape[1]
|
|
94
|
+
if feature_names is None:
|
|
95
|
+
feature_names = [f"feature_{i}" for i in range(n_features)]
|
|
96
|
+
|
|
97
|
+
flags = []
|
|
98
|
+
threshold_value = ratio_threshold * full_model_score
|
|
99
|
+
|
|
100
|
+
for i in range(n_features):
|
|
101
|
+
feature = X[:, i].reshape(-1, 1)
|
|
102
|
+
|
|
103
|
+
# Simple threshold-based classifier / linear regression
|
|
104
|
+
score = _simple_single_feature_score(feature, y, task_type)
|
|
105
|
+
|
|
106
|
+
name = feature_names[i] if i < len(feature_names) else f"feature_{i}"
|
|
107
|
+
|
|
108
|
+
if score > threshold_value:
|
|
109
|
+
flags.append({
|
|
110
|
+
"feature": name,
|
|
111
|
+
"single_feature_score": round(score, 4),
|
|
112
|
+
"full_model_score": round(full_model_score, 4),
|
|
113
|
+
"ratio": round(score / full_model_score, 4) if full_model_score > 0 else None,
|
|
114
|
+
"severity": "critical" if score > full_model_score else "high",
|
|
115
|
+
"reason": (
|
|
116
|
+
f"Single feature achieves {score:.4f} "
|
|
117
|
+
f"({'more than' if score > full_model_score else f'{score/full_model_score:.0%} of'} "
|
|
118
|
+
f"full model {full_model_score:.4f}) — investigate leakage"
|
|
119
|
+
),
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return flags
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _simple_single_feature_score(
|
|
126
|
+
feature: np.ndarray,
|
|
127
|
+
y: np.ndarray,
|
|
128
|
+
task_type: str,
|
|
129
|
+
) -> float:
|
|
130
|
+
"""Quick score for a single feature using threshold-based prediction."""
|
|
131
|
+
feature = feature.ravel()
|
|
132
|
+
|
|
133
|
+
if task_type == "classification":
|
|
134
|
+
# Find best threshold (accuracy maximizing)
|
|
135
|
+
thresholds = np.percentile(feature, [25, 50, 75])
|
|
136
|
+
best_acc = 0.0
|
|
137
|
+
classes = np.unique(y)
|
|
138
|
+
if len(classes) <= 1:
|
|
139
|
+
return 0.0
|
|
140
|
+
|
|
141
|
+
for t in thresholds:
|
|
142
|
+
pred = (feature > t).astype(int)
|
|
143
|
+
# Map to actual classes
|
|
144
|
+
if len(classes) == 2:
|
|
145
|
+
pred_mapped = np.where(pred, classes[1], classes[0])
|
|
146
|
+
else:
|
|
147
|
+
pred_mapped = pred
|
|
148
|
+
acc = float(np.mean(pred_mapped == y))
|
|
149
|
+
best_acc = max(best_acc, acc, 1 - acc) # Try both directions
|
|
150
|
+
|
|
151
|
+
return best_acc
|
|
152
|
+
else:
|
|
153
|
+
# Correlation-based R² approximation
|
|
154
|
+
if np.std(feature) == 0:
|
|
155
|
+
return 0.0
|
|
156
|
+
corr = np.corrcoef(feature, y.astype(float))[0, 1]
|
|
157
|
+
return float(corr ** 2) # R²
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def check_train_test_overlap(
|
|
161
|
+
X_train: np.ndarray,
|
|
162
|
+
X_test: np.ndarray,
|
|
163
|
+
) -> dict:
|
|
164
|
+
"""Check for identical or near-identical samples across splits.
|
|
165
|
+
|
|
166
|
+
Uses hash-based deduplication.
|
|
167
|
+
"""
|
|
168
|
+
def hash_row(row):
|
|
169
|
+
return hashlib.md5(row.tobytes()).hexdigest()
|
|
170
|
+
|
|
171
|
+
train_hashes = set()
|
|
172
|
+
for row in X_train:
|
|
173
|
+
train_hashes.add(hash_row(np.asarray(row)))
|
|
174
|
+
|
|
175
|
+
overlapping = 0
|
|
176
|
+
overlap_indices = []
|
|
177
|
+
for idx, row in enumerate(X_test):
|
|
178
|
+
h = hash_row(np.asarray(row))
|
|
179
|
+
if h in train_hashes:
|
|
180
|
+
overlapping += 1
|
|
181
|
+
if len(overlap_indices) < 10:
|
|
182
|
+
overlap_indices.append(idx)
|
|
183
|
+
|
|
184
|
+
n_test = len(X_test)
|
|
185
|
+
overlap_pct = overlapping / n_test if n_test > 0 else 0
|
|
186
|
+
|
|
187
|
+
if overlapping == 0:
|
|
188
|
+
status = "pass"
|
|
189
|
+
reason = "No overlapping samples between train and test"
|
|
190
|
+
elif overlap_pct < 0.01:
|
|
191
|
+
status = "warn"
|
|
192
|
+
reason = f"{overlapping} overlapping samples ({overlap_pct:.2%}) — minor but investigate"
|
|
193
|
+
else:
|
|
194
|
+
status = "fail"
|
|
195
|
+
reason = f"{overlapping} overlapping samples ({overlap_pct:.2%}) — significant leakage"
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
"check": "train_test_overlap",
|
|
199
|
+
"status": status,
|
|
200
|
+
"overlapping_samples": overlapping,
|
|
201
|
+
"overlap_percentage": round(overlap_pct, 4),
|
|
202
|
+
"test_size": n_test,
|
|
203
|
+
"reason": reason,
|
|
204
|
+
"severity": "critical",
|
|
205
|
+
"sample_overlap_indices": overlap_indices,
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# --- Full Leakage Scan ---
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def run_leakage_scan(
|
|
213
|
+
X: np.ndarray | None = None,
|
|
214
|
+
y: np.ndarray | None = None,
|
|
215
|
+
X_train: np.ndarray | None = None,
|
|
216
|
+
X_test: np.ndarray | None = None,
|
|
217
|
+
full_model_score: float | None = None,
|
|
218
|
+
feature_names: list[str] | None = None,
|
|
219
|
+
deep: bool = False,
|
|
220
|
+
task_type: str = "classification",
|
|
221
|
+
config_path: str = "config.yaml",
|
|
222
|
+
) -> dict:
|
|
223
|
+
"""Run a complete leakage scan.
|
|
224
|
+
|
|
225
|
+
Args:
|
|
226
|
+
X: Feature matrix (for correlation and single-feature tests).
|
|
227
|
+
y: Target array.
|
|
228
|
+
X_train: Training features (for overlap check).
|
|
229
|
+
X_test: Test features (for overlap check).
|
|
230
|
+
full_model_score: Best model's primary metric (for single-feature comparison).
|
|
231
|
+
feature_names: Names of features.
|
|
232
|
+
deep: Run full single-feature analysis (slower).
|
|
233
|
+
task_type: classification or regression.
|
|
234
|
+
config_path: Path to config.yaml.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Complete leakage report.
|
|
238
|
+
"""
|
|
239
|
+
config = load_config(config_path)
|
|
240
|
+
if not task_type:
|
|
241
|
+
task_type = config.get("task", {}).get("type", "classification")
|
|
242
|
+
|
|
243
|
+
checks = []
|
|
244
|
+
|
|
245
|
+
# Feature-target correlation
|
|
246
|
+
if X is not None and y is not None:
|
|
247
|
+
corr_flags = check_feature_target_correlation(X, y, feature_names)
|
|
248
|
+
checks.append({
|
|
249
|
+
"check": "feature_target_correlation",
|
|
250
|
+
"status": "fail" if corr_flags else "pass",
|
|
251
|
+
"flags": corr_flags,
|
|
252
|
+
"n_flagged": len(corr_flags),
|
|
253
|
+
"severity": "critical",
|
|
254
|
+
"reason": f"{len(corr_flags)} feature(s) with >{DEFAULT_CORRELATION_THRESHOLD} target correlation" if corr_flags else "No suspicious correlations",
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
# Single-feature predictiveness (deep mode)
|
|
258
|
+
if deep and X is not None and y is not None and full_model_score is not None:
|
|
259
|
+
sf_flags = check_single_feature_predictiveness(
|
|
260
|
+
X, y, full_model_score, feature_names, task_type=task_type,
|
|
261
|
+
)
|
|
262
|
+
checks.append({
|
|
263
|
+
"check": "single_feature_predictiveness",
|
|
264
|
+
"status": "fail" if sf_flags else "pass",
|
|
265
|
+
"flags": sf_flags,
|
|
266
|
+
"n_flagged": len(sf_flags),
|
|
267
|
+
"severity": "critical",
|
|
268
|
+
"reason": f"{len(sf_flags)} feature(s) suspiciously predictive alone" if sf_flags else "No single feature is suspiciously predictive",
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
# Train/test overlap
|
|
272
|
+
if X_train is not None and X_test is not None:
|
|
273
|
+
overlap = check_train_test_overlap(X_train, X_test)
|
|
274
|
+
checks.append(overlap)
|
|
275
|
+
|
|
276
|
+
if not checks:
|
|
277
|
+
return {
|
|
278
|
+
"error": "No data provided for leakage scan. Provide X, y, or X_train/X_test arrays.",
|
|
279
|
+
"note": "Run with --data train.npz --test test.npz to scan for leakage.",
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
# Compute verdict
|
|
283
|
+
n_fail = sum(1 for c in checks if c.get("status") == "fail")
|
|
284
|
+
n_warn = sum(1 for c in checks if c.get("status") == "warn")
|
|
285
|
+
|
|
286
|
+
if n_fail > 0:
|
|
287
|
+
verdict = "leakage_detected"
|
|
288
|
+
elif n_warn > 0:
|
|
289
|
+
verdict = "suspicious"
|
|
290
|
+
else:
|
|
291
|
+
verdict = "clean"
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
"scanned_at": datetime.now(timezone.utc).isoformat(),
|
|
295
|
+
"task_type": task_type,
|
|
296
|
+
"deep_mode": deep,
|
|
297
|
+
"checks": checks,
|
|
298
|
+
"verdict": verdict,
|
|
299
|
+
"n_checks": len(checks),
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
# --- Report Formatting ---
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def save_leakage_report(report: dict, output_dir: str = "experiments/leakage") -> Path:
|
|
307
|
+
"""Save leakage report to YAML."""
|
|
308
|
+
out_path = Path(output_dir)
|
|
309
|
+
out_path.mkdir(parents=True, exist_ok=True)
|
|
310
|
+
|
|
311
|
+
date = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
312
|
+
filepath = out_path / f"leak-{date}.yaml"
|
|
313
|
+
|
|
314
|
+
clean = json.loads(json.dumps(report, default=str))
|
|
315
|
+
with open(filepath, "w") as f:
|
|
316
|
+
yaml.dump(clean, f, default_flow_style=False, sort_keys=False)
|
|
317
|
+
|
|
318
|
+
return filepath
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def format_leakage_report(report: dict) -> str:
|
|
322
|
+
"""Format leakage report as markdown."""
|
|
323
|
+
if "error" in report:
|
|
324
|
+
return f"ERROR: {report['error']}\n{report.get('note', '')}"
|
|
325
|
+
|
|
326
|
+
verdict = report.get("verdict", "?")
|
|
327
|
+
verdict_labels = {
|
|
328
|
+
"leakage_detected": "LEAKAGE DETECTED — investigate flagged features",
|
|
329
|
+
"suspicious": "SUSPICIOUS — review warnings",
|
|
330
|
+
"clean": "CLEAN — no leakage detected",
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
lines = [
|
|
334
|
+
"# Leakage Scan",
|
|
335
|
+
"",
|
|
336
|
+
f"*Scanned {report.get('scanned_at', 'N/A')[:19]}*",
|
|
337
|
+
f"*Mode: {'deep' if report.get('deep_mode') else 'standard'}*",
|
|
338
|
+
"",
|
|
339
|
+
f"**{verdict_labels.get(verdict, verdict.upper())}**",
|
|
340
|
+
"",
|
|
341
|
+
]
|
|
342
|
+
|
|
343
|
+
for c in report.get("checks", []):
|
|
344
|
+
check_name = c.get("check", "?")
|
|
345
|
+
status = c.get("status", "?")
|
|
346
|
+
marker = {"pass": "OK", "fail": "FLAG", "warn": "WARN"}.get(status, status.upper())
|
|
347
|
+
|
|
348
|
+
lines.append(f"### {check_name}")
|
|
349
|
+
lines.append(f"**[{marker}]** {c.get('reason', 'N/A')}")
|
|
350
|
+
lines.append("")
|
|
351
|
+
|
|
352
|
+
flags = c.get("flags", [])
|
|
353
|
+
for f in flags[:5]:
|
|
354
|
+
lines.append(f"- **{f.get('feature', '?')}**: {f.get('reason', 'N/A')}")
|
|
355
|
+
if len(flags) > 5:
|
|
356
|
+
lines.append(f" *...and {len(flags) - 5} more*")
|
|
357
|
+
lines.append("")
|
|
358
|
+
|
|
359
|
+
return "\n".join(lines)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def main() -> None:
|
|
363
|
+
"""CLI entry point."""
|
|
364
|
+
parser = argparse.ArgumentParser(
|
|
365
|
+
description="Targeted leakage detection",
|
|
366
|
+
)
|
|
367
|
+
parser.add_argument(
|
|
368
|
+
"--deep", action="store_true",
|
|
369
|
+
help="Run full single-feature analysis (slower but thorough)",
|
|
370
|
+
)
|
|
371
|
+
parser.add_argument(
|
|
372
|
+
"--features",
|
|
373
|
+
help="Specific features to check (comma-separated)",
|
|
374
|
+
)
|
|
375
|
+
parser.add_argument(
|
|
376
|
+
"--config", default="config.yaml",
|
|
377
|
+
help="Path to config.yaml",
|
|
378
|
+
)
|
|
379
|
+
parser.add_argument(
|
|
380
|
+
"--json", action="store_true",
|
|
381
|
+
help="Output raw JSON instead of formatted report",
|
|
382
|
+
)
|
|
383
|
+
args = parser.parse_args()
|
|
384
|
+
|
|
385
|
+
# In CLI mode without data args, produce a plan
|
|
386
|
+
report = run_leakage_scan(config_path=args.config, deep=args.deep)
|
|
387
|
+
|
|
388
|
+
if "error" not in report:
|
|
389
|
+
filepath = save_leakage_report(report)
|
|
390
|
+
print(f"Saved to {filepath}", file=sys.stderr)
|
|
391
|
+
|
|
392
|
+
if args.json:
|
|
393
|
+
print(json.dumps(report, indent=2, default=str))
|
|
394
|
+
else:
|
|
395
|
+
print(format_leakage_report(report))
|
|
396
|
+
|
|
397
|
+
if report.get("verdict") == "leakage_detected":
|
|
398
|
+
sys.exit(1)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
if __name__ == "__main__":
|
|
402
|
+
main()
|