claude-turing 4.0.0 → 4.2.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 +8 -2
- package/commands/counterfactual.md +27 -0
- package/commands/onboard.md +20 -0
- package/commands/review.md +20 -0
- package/commands/share.md +20 -0
- package/commands/simulate.md +28 -0
- package/commands/turing.md +12 -0
- package/commands/whatif.md +31 -0
- package/package.json +1 -1
- package/src/install.js +2 -0
- package/src/verify.js +6 -0
- package/templates/scripts/__pycache__/counterfactual_explanation.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/experiment_simulator.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/generate_brief.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/generate_onboarding.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/package_experiments.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/simulate_review.cpython-314.pyc +0 -0
- package/templates/scripts/__pycache__/whatif_engine.cpython-314.pyc +0 -0
- package/templates/scripts/counterfactual_explanation.py +485 -0
- package/templates/scripts/experiment_simulator.py +463 -0
- package/templates/scripts/generate_brief.py +64 -0
- package/templates/scripts/generate_onboarding.py +284 -0
- package/templates/scripts/package_experiments.py +285 -0
- package/templates/scripts/scaffold.py +11 -0
- package/templates/scripts/simulate_review.py +342 -0
- package/templates/scripts/whatif_engine.py +763 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Simulated peer review for ML experiment campaigns.
|
|
3
|
+
|
|
4
|
+
Checks for: missing baselines, missing error bars, missing ablation,
|
|
5
|
+
overclaimed results, missing SOTA comparison, calibration, computational
|
|
6
|
+
cost. Generates structured review with strengths/weaknesses/questions,
|
|
7
|
+
each weakness linked to a /turing: fix command. Scores 1-10.
|
|
8
|
+
|
|
9
|
+
Usage:
|
|
10
|
+
python scripts/simulate_review.py
|
|
11
|
+
python scripts/simulate_review.py --venue neurips --harsh
|
|
12
|
+
python scripts/simulate_review.py --venue icml --json
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import sys
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import yaml
|
|
23
|
+
from scripts.turing_io import load_config, load_experiments
|
|
24
|
+
|
|
25
|
+
DEFAULT_LOG = "experiments/log.jsonl"
|
|
26
|
+
VALID_VENUES = ["neurips", "icml", "general"]
|
|
27
|
+
SEVERITY_WEIGHTS = {"critical": 3.0, "major": 2.0, "minor": 1.0, "nitpick": 0.3}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_yaml_dir(directory: str, glob: str) -> list[dict]:
|
|
31
|
+
path = Path(directory)
|
|
32
|
+
if not path.exists():
|
|
33
|
+
return []
|
|
34
|
+
items = []
|
|
35
|
+
for f in sorted(path.glob(glob)):
|
|
36
|
+
try:
|
|
37
|
+
with open(f) as fh:
|
|
38
|
+
d = yaml.safe_load(fh)
|
|
39
|
+
if d and isinstance(d, dict):
|
|
40
|
+
items.append(d)
|
|
41
|
+
except (yaml.YAMLError, OSError):
|
|
42
|
+
continue
|
|
43
|
+
return items
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_yaml_list(path: str) -> list[dict]:
|
|
47
|
+
p = Path(path)
|
|
48
|
+
if not p.exists() or p.stat().st_size == 0:
|
|
49
|
+
return []
|
|
50
|
+
with open(p) as f:
|
|
51
|
+
data = yaml.safe_load(f)
|
|
52
|
+
return data if isinstance(data, list) else []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _w(id, sev, title, detail, fix, venues=None):
|
|
56
|
+
"""Shorthand weakness constructor."""
|
|
57
|
+
return {"id": id, "severity": sev, "title": title, "detail": detail,
|
|
58
|
+
"fix_command": fix, "venue_relevance": venues or ["neurips", "icml", "general"]}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# --- Review checks ---
|
|
62
|
+
|
|
63
|
+
def check_baselines(experiments, config):
|
|
64
|
+
types = {e.get("config", {}).get("model_type", "") for e in experiments if e.get("status") == "kept"}
|
|
65
|
+
baselines = {"logistic_regression", "linear_regression", "dummy", "majority_class", "random", "baseline"}
|
|
66
|
+
if types and not (types & baselines):
|
|
67
|
+
return _w("missing-baselines", "major", "No simple baseline comparison",
|
|
68
|
+
f"Model types: {', '.join(sorted(types))}. No simple baseline to calibrate expectations.",
|
|
69
|
+
'/turing:try "Add logistic regression baseline"')
|
|
70
|
+
|
|
71
|
+
def check_error_bars(experiments, seeds):
|
|
72
|
+
kept = [e for e in experiments if e.get("status") == "kept"]
|
|
73
|
+
if not kept:
|
|
74
|
+
return None
|
|
75
|
+
studied = {s.get("experiment_id") for s in seeds}
|
|
76
|
+
unstudied = [e for e in kept if e.get("experiment_id") not in studied]
|
|
77
|
+
if len(unstudied) == len(kept):
|
|
78
|
+
return _w("no-error-bars", "critical", "No error bars on any result",
|
|
79
|
+
f"{len(kept)} kept experiment(s) with no seed studies. Single-seed results not publishable.",
|
|
80
|
+
"/turing:seed")
|
|
81
|
+
elif unstudied:
|
|
82
|
+
ids = ", ".join(e.get("experiment_id", "?") for e in unstudied[:5])
|
|
83
|
+
return _w("partial-error-bars", "minor", "Some experiments lack error bars",
|
|
84
|
+
f"{len(unstudied)}/{len(kept)} lack seed studies: {ids}.", "/turing:seed")
|
|
85
|
+
|
|
86
|
+
def check_ablation(experiments, ablations):
|
|
87
|
+
if len([e for e in experiments if e.get("status") == "kept"]) >= 2 and not ablations:
|
|
88
|
+
return _w("no-ablation", "major", "No ablation study",
|
|
89
|
+
"No ablation studies found. Component contributions unclear.", "/turing:ablate")
|
|
90
|
+
|
|
91
|
+
def check_overclaimed(experiments, seeds, metric, lower_is_better):
|
|
92
|
+
sensitive = [s for s in seeds if s.get("seed_sensitive")]
|
|
93
|
+
if not sensitive:
|
|
94
|
+
return None
|
|
95
|
+
details = "; ".join(f"{s.get('experiment_id','?')}: CV={s.get('cv_percent',0):.1f}%" for s in sensitive)
|
|
96
|
+
return _w("overclaimed-results", "major", "Seed-sensitive results may be overclaimed",
|
|
97
|
+
f"{len(sensitive)} experiment(s) show high seed sensitivity: {details}. "
|
|
98
|
+
"Report mean +/- std instead of point estimates.", "/turing:seed")
|
|
99
|
+
|
|
100
|
+
def check_sota(experiments, config, annotations):
|
|
101
|
+
kw = {"sota", "state-of-the-art", "benchmark", "leaderboard", "published"}
|
|
102
|
+
for ann in annotations:
|
|
103
|
+
text = ann.get("text", "").lower()
|
|
104
|
+
if any(k in text for k in kw) or any(k in [t.lower() for t in ann.get("tags", [])] for k in kw):
|
|
105
|
+
return None
|
|
106
|
+
if config.get("evaluation", {}).get("reference_score") or config.get("evaluation", {}).get("sota_score"):
|
|
107
|
+
return None
|
|
108
|
+
return _w("no-sota-comparison", "minor", "No SOTA or external benchmark comparison",
|
|
109
|
+
"No reference to published results. Add reference score or annotate with SOTA values.",
|
|
110
|
+
'/turing:try "Add SOTA comparison from literature"', ["neurips", "icml"])
|
|
111
|
+
|
|
112
|
+
def check_calibration(cal_results, experiments):
|
|
113
|
+
kept = [e for e in experiments if e.get("status") == "kept"]
|
|
114
|
+
if not kept:
|
|
115
|
+
return None
|
|
116
|
+
if not cal_results:
|
|
117
|
+
return _w("no-calibration", "minor", "No calibration analysis",
|
|
118
|
+
"Model calibration not assessed. Reviewers expect ECE or reliability diagrams.",
|
|
119
|
+
'/turing:try "Add calibration analysis (ECE)"', ["neurips", "icml"])
|
|
120
|
+
poor = [r for r in cal_results if r.get("ece", 0) > 0.1]
|
|
121
|
+
if poor:
|
|
122
|
+
return _w("poor-calibration", "minor", "Model poorly calibrated",
|
|
123
|
+
f"{len(poor)} model(s) have ECE > 0.1. Consider temperature scaling.",
|
|
124
|
+
'/turing:try "Apply temperature scaling"')
|
|
125
|
+
|
|
126
|
+
def check_compute_cost(experiments):
|
|
127
|
+
kept = [e for e in experiments if e.get("status") == "kept"]
|
|
128
|
+
if not kept:
|
|
129
|
+
return None
|
|
130
|
+
has_time = any(e.get("metrics", {}).get("train_seconds") is not None for e in kept)
|
|
131
|
+
has_env = any(e.get("environment") for e in kept)
|
|
132
|
+
issues = []
|
|
133
|
+
if not has_time:
|
|
134
|
+
issues.append("No training time reported")
|
|
135
|
+
if not has_env:
|
|
136
|
+
issues.append("No hardware info recorded")
|
|
137
|
+
if issues:
|
|
138
|
+
return _w("no-compute-cost", "major" if not has_time else "minor",
|
|
139
|
+
"Computational cost not reported", "; ".join(issues) + ". "
|
|
140
|
+
"Reporting compute cost is expected at all major venues.",
|
|
141
|
+
'/turing:try "Profile training and report compute cost"')
|
|
142
|
+
|
|
143
|
+
def check_diversity(experiments):
|
|
144
|
+
types = {e.get("config", {}).get("model_type", "") for e in experiments if e.get("status") == "kept"}
|
|
145
|
+
kept_n = sum(1 for e in experiments if e.get("status") == "kept")
|
|
146
|
+
if len(types) == 1 and kept_n >= 3:
|
|
147
|
+
return _w("low-diversity", "minor", "Only one model family explored",
|
|
148
|
+
f"All {kept_n} kept experiments use {list(types)[0]}. Alternatives not explored.",
|
|
149
|
+
'/turing:try "Explore alternative model architecture"')
|
|
150
|
+
|
|
151
|
+
def check_leakage(experiments, annotations):
|
|
152
|
+
kw = {"leakage", "leak", "contamination", "suspicious", "too high", "too good"}
|
|
153
|
+
flagged = [a for a in annotations
|
|
154
|
+
if any(k in a.get("text", "").lower() for k in kw) or "leakage" in [t.lower() for t in a.get("tags", [])]]
|
|
155
|
+
if flagged:
|
|
156
|
+
return _w("leakage-concern", "critical", "Data leakage flagged in annotations",
|
|
157
|
+
f"{len(flagged)} annotation(s) mention potential leakage. Must investigate before submission.",
|
|
158
|
+
'/turing:try "Investigate and rule out data leakage"')
|
|
159
|
+
|
|
160
|
+
def check_reproducibility(experiments, config):
|
|
161
|
+
issues = []
|
|
162
|
+
if config.get("data", {}).get("random_state") is None:
|
|
163
|
+
issues.append("No random state in config")
|
|
164
|
+
rd = Path("experiments/reproductions")
|
|
165
|
+
if len(experiments) >= 5 and not (rd.exists() and any(rd.glob("*.yaml"))):
|
|
166
|
+
issues.append("No reproduction checks run")
|
|
167
|
+
if issues:
|
|
168
|
+
return _w("reproducibility-gaps", "minor", "Reproducibility not fully verified",
|
|
169
|
+
"; ".join(issues) + ".", "/turing:reproduce")
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# --- Strengths & questions ---
|
|
173
|
+
|
|
174
|
+
def identify_strengths(experiments, seeds, ablations, config, metric, lower_is_better):
|
|
175
|
+
S = []
|
|
176
|
+
kept = [e for e in experiments if e.get("status") == "kept"]
|
|
177
|
+
types = set(e.get("config", {}).get("model_type", "") for e in kept)
|
|
178
|
+
if len(kept) >= 5:
|
|
179
|
+
S.append(f"Thorough experimentation: {len(kept)} successful experiments across {len(types)} type(s).")
|
|
180
|
+
stable = [s for s in seeds if not s.get("seed_sensitive")]
|
|
181
|
+
if stable:
|
|
182
|
+
S.append(f"Seed studies: {len(stable)}/{len(seeds)} experiments show stable results.")
|
|
183
|
+
if ablations:
|
|
184
|
+
S.append(f"Ablation analysis provided ({len(ablations)} study/ies).")
|
|
185
|
+
families = set(e.get("family") for e in kept if e.get("family"))
|
|
186
|
+
if len(families) >= 3:
|
|
187
|
+
S.append(f"Systematic exploration of {len(families)} research directions.")
|
|
188
|
+
if len(experiments) >= 5 and len(kept) / len(experiments) >= 0.4:
|
|
189
|
+
S.append(f"High experiment efficiency: {len(kept)/len(experiments):.0%} keep rate.")
|
|
190
|
+
return S or ["Experiments have been initiated on this problem."]
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def generate_questions(weaknesses, experiments, config, venue):
|
|
194
|
+
Q, wids = [], {w["id"] for w in weaknesses}
|
|
195
|
+
qmap = {"missing-baselines": "How does performance compare to a simple baseline?",
|
|
196
|
+
"no-error-bars": "Can you provide confidence intervals over multiple seeds?",
|
|
197
|
+
"overclaimed-results": "Can you provide confidence intervals over multiple seeds?",
|
|
198
|
+
"no-ablation": "What is the contribution of each component?",
|
|
199
|
+
"no-sota-comparison": "How do results compare to published state-of-the-art?",
|
|
200
|
+
"no-compute-cost": "What are the computational requirements (GPU hours)?"}
|
|
201
|
+
for wid, q in qmap.items():
|
|
202
|
+
if wid in wids:
|
|
203
|
+
Q.append(q)
|
|
204
|
+
if venue == "neurips":
|
|
205
|
+
Q.append("What is the broader impact? Are there negative societal implications?")
|
|
206
|
+
elif venue == "icml":
|
|
207
|
+
Q.append("Is there theoretical justification, or is this purely empirical?")
|
|
208
|
+
return Q
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def compute_score(strengths, weaknesses, harsh):
|
|
212
|
+
score = 6.0 + min(len(strengths) * 0.4, 2.0)
|
|
213
|
+
for w in weaknesses:
|
|
214
|
+
score -= SEVERITY_WEIGHTS.get(w["severity"], 1.0)
|
|
215
|
+
if harsh:
|
|
216
|
+
score -= 1.0
|
|
217
|
+
return max(1, min(10, round(score)))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# --- Formatting ---
|
|
221
|
+
|
|
222
|
+
def format_review_report(strengths, weaknesses, questions, score, venue, harsh,
|
|
223
|
+
config, experiments, metric):
|
|
224
|
+
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
|
|
225
|
+
kept = [e for e in experiments if e.get("status") == "kept"]
|
|
226
|
+
types = set(e.get("config", {}).get("model_type", "") for e in kept)
|
|
227
|
+
labels = {1: "Strong Reject", 2: "Reject", 3: "Reject", 4: "Weak Reject",
|
|
228
|
+
5: "Borderline Reject", 6: "Borderline Accept", 7: "Weak Accept",
|
|
229
|
+
8: "Accept", 9: "Strong Accept", 10: "Strong Accept"}
|
|
230
|
+
crit = sum(1 for w in weaknesses if w["severity"] == "critical")
|
|
231
|
+
maj = sum(1 for w in weaknesses if w["severity"] == "major")
|
|
232
|
+
mn = sum(1 for w in weaknesses if w["severity"] == "minor")
|
|
233
|
+
L = ["# Simulated Peer Review", "",
|
|
234
|
+
f"*Generated {now}*",
|
|
235
|
+
f"*Venue: {venue.upper()} | Mode: {'HARSH' if harsh else 'Standard'} | "
|
|
236
|
+
f"Score: {score}/10 ({labels.get(score, '?')})*", "", "---", "",
|
|
237
|
+
"## Summary", "",
|
|
238
|
+
f"This work presents experiments on {config.get('task_description', 'the given task')} "
|
|
239
|
+
f"with {len(kept)} successful experiment(s) across {len(types)} model type(s). "
|
|
240
|
+
f"Primary metric: `{metric}`.", "",
|
|
241
|
+
"## Score", "", f"**{score}/10** — {labels.get(score, '?')}", "",
|
|
242
|
+
f"- Strengths: {len(strengths)}",
|
|
243
|
+
f"- Weaknesses: {len(weaknesses)} ({crit} critical, {maj} major, {mn} minor)", "",
|
|
244
|
+
"## Strengths", ""]
|
|
245
|
+
for i, s in enumerate(strengths, 1):
|
|
246
|
+
L.append(f"**S{i}.** {s}")
|
|
247
|
+
L.extend(["", "## Weaknesses", ""])
|
|
248
|
+
for i, w in enumerate(weaknesses, 1):
|
|
249
|
+
L.extend([f"**W{i}. [{w['severity'].upper()}] {w['title']}**", "",
|
|
250
|
+
w["detail"], "", f"*Fix:* `{w['fix_command']}`", ""])
|
|
251
|
+
if not weaknesses:
|
|
252
|
+
L.extend(["No significant weaknesses identified.", ""])
|
|
253
|
+
L.extend(["## Questions for Authors", ""])
|
|
254
|
+
for i, q in enumerate(questions, 1):
|
|
255
|
+
L.append(f"**Q{i}.** {q}")
|
|
256
|
+
critical_major = [w for w in weaknesses if w["severity"] in ("critical", "major")]
|
|
257
|
+
if critical_major:
|
|
258
|
+
L.extend(["", "## Recommended Action Plan", "", "Address before submission:", ""])
|
|
259
|
+
for p, w in enumerate(critical_major, 1):
|
|
260
|
+
L.append(f"{p}. **[{w['severity'].upper()}]** {w['title']}: `{w['fix_command']}`")
|
|
261
|
+
L.extend(["", "## Verdict", ""])
|
|
262
|
+
if score >= 7:
|
|
263
|
+
L.append("Approaching publication quality. Address minor issues and consider submission.")
|
|
264
|
+
elif score >= 5:
|
|
265
|
+
L.append("Borderline. Significant improvements needed. Follow the action plan.")
|
|
266
|
+
else:
|
|
267
|
+
L.append("Not ready. Major methodology gaps. Focus on critical and major weaknesses.")
|
|
268
|
+
L.extend(["", "---", "*Simulated review by `/turing:review` — not a substitute for actual peer review.*"])
|
|
269
|
+
return "\n".join(L)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def save_review_report(result: dict, output_dir="experiments/reviews") -> Path:
|
|
273
|
+
p = Path(output_dir)
|
|
274
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
276
|
+
out = p / f"review-{ts}.yaml"
|
|
277
|
+
with open(out, "w") as f:
|
|
278
|
+
yaml.dump({"timestamp": result["timestamp"], "venue": result["venue"],
|
|
279
|
+
"harsh": result["harsh"], "score": result["score"],
|
|
280
|
+
"weaknesses": [{"id": w["id"], "severity": w["severity"],
|
|
281
|
+
"title": w["title"], "fix_command": w["fix_command"]}
|
|
282
|
+
for w in result["weaknesses"]]},
|
|
283
|
+
f, default_flow_style=False, sort_keys=False)
|
|
284
|
+
return out
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# --- Orchestration ---
|
|
288
|
+
|
|
289
|
+
def simulate_review(venue="general", harsh=False, config_path="config.yaml",
|
|
290
|
+
log_path=DEFAULT_LOG) -> dict:
|
|
291
|
+
"""Run full simulated review pipeline."""
|
|
292
|
+
config = load_config(config_path)
|
|
293
|
+
metric = config.get("evaluation", {}).get("primary_metric", "accuracy")
|
|
294
|
+
lower = config.get("evaluation", {}).get("lower_is_better", False)
|
|
295
|
+
experiments = load_experiments(log_path)
|
|
296
|
+
if not experiments:
|
|
297
|
+
return {"error": "No experiments found. Run /turing:train first.",
|
|
298
|
+
"timestamp": datetime.now(timezone.utc).isoformat()}
|
|
299
|
+
seeds = _load_yaml_dir("experiments/seed_studies", "*-seeds.yaml")
|
|
300
|
+
ablations = _load_yaml_dir("experiments/ablations", "*-ablation.yaml")
|
|
301
|
+
cal = _load_yaml_dir("experiments/calibration", "*.yaml")
|
|
302
|
+
annotations = _load_yaml_list("experiments/annotations.yaml")
|
|
303
|
+
checks = [check_baselines(experiments, config), check_error_bars(experiments, seeds),
|
|
304
|
+
check_ablation(experiments, ablations), check_overclaimed(experiments, seeds, metric, lower),
|
|
305
|
+
check_sota(experiments, config, annotations), check_calibration(cal, experiments),
|
|
306
|
+
check_compute_cost(experiments), check_diversity(experiments),
|
|
307
|
+
check_leakage(experiments, annotations), check_reproducibility(experiments, config)]
|
|
308
|
+
weaknesses = [c for c in checks if c and venue in c.get("venue_relevance", ["general"])]
|
|
309
|
+
sev_order = {"critical": 0, "major": 1, "minor": 2, "nitpick": 3}
|
|
310
|
+
weaknesses.sort(key=lambda w: sev_order.get(w["severity"], 9))
|
|
311
|
+
strengths = identify_strengths(experiments, seeds, ablations, config, metric, lower)
|
|
312
|
+
questions = generate_questions(weaknesses, experiments, config, venue)
|
|
313
|
+
score = compute_score(strengths, weaknesses, harsh)
|
|
314
|
+
report = format_review_report(strengths, weaknesses, questions, score, venue, harsh,
|
|
315
|
+
config, experiments, metric)
|
|
316
|
+
return {"timestamp": datetime.now(timezone.utc).isoformat(), "venue": venue, "harsh": harsh,
|
|
317
|
+
"score": score, "strengths": strengths, "weaknesses": weaknesses,
|
|
318
|
+
"questions": questions, "report": report}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def main() -> None:
|
|
322
|
+
parser = argparse.ArgumentParser(description="Simulate peer review of experiment campaign")
|
|
323
|
+
parser.add_argument("--venue", default="general", choices=VALID_VENUES)
|
|
324
|
+
parser.add_argument("--harsh", action="store_true", help="Stricter review criteria")
|
|
325
|
+
parser.add_argument("--config", default="config.yaml")
|
|
326
|
+
parser.add_argument("--log", default=DEFAULT_LOG)
|
|
327
|
+
parser.add_argument("--json", action="store_true", help="Output raw JSON")
|
|
328
|
+
args = parser.parse_args()
|
|
329
|
+
result = simulate_review(args.venue, args.harsh, args.config, args.log)
|
|
330
|
+
if "error" in result and "report" not in result:
|
|
331
|
+
print(f"ERROR: {result['error']}", file=sys.stderr)
|
|
332
|
+
sys.exit(1)
|
|
333
|
+
if args.json:
|
|
334
|
+
print(json.dumps(result, indent=2, default=str))
|
|
335
|
+
else:
|
|
336
|
+
print(result["report"])
|
|
337
|
+
saved = save_review_report(result)
|
|
338
|
+
print(f"\nReview saved to {saved}", file=sys.stderr)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if __name__ == "__main__":
|
|
342
|
+
main()
|