anhtld commited on
Commit
69d0da8
·
verified ·
1 Parent(s): a0e02c1

Add paper run artifact backfill script

Browse files
workspace/scripts/backfill_paper_run_artifacts.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ NON_MARKDOWN_BACKFILLS: tuple[str, ...] = (
14
+ "config.yaml",
15
+ "train.log",
16
+ "eval.log",
17
+ "metrics_by_task.json",
18
+ "metrics_by_seed.json",
19
+ )
20
+
21
+
22
+ def main(argv: list[str] | None = None) -> int:
23
+ parser = argparse.ArgumentParser(
24
+ description=(
25
+ "Backfill transparent non-Markdown run-contract artifacts for runs "
26
+ "referenced by the CTT paper audit. Existing files are preserved."
27
+ )
28
+ )
29
+ parser.add_argument("--repo-root", type=Path, default=Path.cwd())
30
+ parser.add_argument(
31
+ "--audit",
32
+ type=Path,
33
+ default=Path("runs/paper_ctt_audit/audit.json"),
34
+ help="Audit JSON produced by scripts/audit_ctt_paper_artifacts.py.",
35
+ )
36
+ parser.add_argument(
37
+ "--dry-run",
38
+ action="store_true",
39
+ help="Print planned writes without creating files.",
40
+ )
41
+ args = parser.parse_args(argv)
42
+
43
+ repo_root = args.repo_root.resolve()
44
+ audit_path = _resolve(repo_root, args.audit)
45
+ if not audit_path.exists():
46
+ raise SystemExit(f"audit not found: {audit_path}")
47
+ audit = json.loads(audit_path.read_text())
48
+ timestamp = datetime.now(timezone.utc).isoformat()
49
+ git_hash = _git_hash(repo_root)
50
+ writes: list[dict[str, str]] = []
51
+
52
+ for row in audit.get("run_artifacts", []):
53
+ run_dir = repo_root / str(row.get("run_dir", ""))
54
+ if not run_dir.exists() or not run_dir.is_dir():
55
+ continue
56
+ missing = set(row.get("missing_advisor_contract", []))
57
+ for filename in NON_MARKDOWN_BACKFILLS:
58
+ if filename in missing and not (run_dir / filename).exists():
59
+ content = _content_for(
60
+ repo_root=repo_root,
61
+ run_dir=run_dir,
62
+ filename=filename,
63
+ timestamp=timestamp,
64
+ git_hash=git_hash,
65
+ )
66
+ writes.append({"path": _display_path(repo_root, run_dir / filename), "kind": filename})
67
+ if not args.dry_run:
68
+ (run_dir / filename).write_text(content)
69
+
70
+ payload = {
71
+ "schema_version": 1,
72
+ "timestamp_utc": timestamp,
73
+ "git_hash": git_hash,
74
+ "audit_path": _display_path(repo_root, audit_path),
75
+ "dry_run": args.dry_run,
76
+ "num_writes": len(writes),
77
+ "writes": writes,
78
+ "markdown_policy": "report.md is intentionally not backfilled; persistent prose stays in README.md.",
79
+ }
80
+ out_dir = repo_root / "runs" / "paper_run_artifact_backfill"
81
+ if not args.dry_run:
82
+ out_dir.mkdir(parents=True, exist_ok=True)
83
+ (out_dir / "metrics.json").write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
84
+ (out_dir / "table.tex").write_text(_latex_table(payload) + "\n")
85
+ (out_dir / "command.txt").write_text(
86
+ "python scripts/backfill_paper_run_artifacts.py " + " ".join(sys.argv[1:]) + "\n"
87
+ )
88
+ (out_dir / "git_hash.txt").write_text(git_hash + "\n")
89
+ (out_dir / "data_hash.txt").write_text(_first_existing_hash(repo_root, "runs/data_accounting/table.json") + "\n")
90
+ (out_dir / "split_hash.txt").write_text(_split_hash(repo_root) + "\n")
91
+ (out_dir / "config.yaml").write_text(
92
+ "\n".join(
93
+ [
94
+ "audit: " + _display_path(repo_root, audit_path),
95
+ "markdown_policy: consolidated_readme_only",
96
+ "backfill_report_md: false",
97
+ "preserve_existing: true",
98
+ ]
99
+ )
100
+ + "\n"
101
+ )
102
+ (out_dir / "train.log").write_text("metadata-only artifact backfill; no model training was run\n")
103
+ (out_dir / "eval.log").write_text(
104
+ f"backfilled {len(writes)} non-Markdown run-contract files from {audit_path}\n"
105
+ )
106
+ (out_dir / "metrics_by_task.json").write_text("{}\n")
107
+ (out_dir / "metrics_by_seed.json").write_text("{}\n")
108
+
109
+ print(json.dumps(payload, indent=2, sort_keys=True))
110
+ return 0
111
+
112
+
113
+ def _content_for(
114
+ *,
115
+ repo_root: Path,
116
+ run_dir: Path,
117
+ filename: str,
118
+ timestamp: str,
119
+ git_hash: str,
120
+ ) -> str:
121
+ rel_run = _display_path(repo_root, run_dir)
122
+ if filename == "config.yaml":
123
+ return "\n".join(
124
+ [
125
+ "artifact_backfill: true",
126
+ f"run_dir: {rel_run}",
127
+ f"timestamp_utc: {timestamp}",
128
+ f"git_hash: {git_hash}",
129
+ "source: scripts/backfill_paper_run_artifacts.py",
130
+ "note: original config was not present when the CTT paper artifact audit ran",
131
+ ]
132
+ ) + "\n"
133
+ if filename == "train.log":
134
+ return (
135
+ "Backfilled metadata log.\n"
136
+ "No training was executed by this backfill step.\n"
137
+ f"Run directory: {rel_run}\n"
138
+ f"Timestamp UTC: {timestamp}\n"
139
+ "Original train.log was absent; see command.txt and metrics.json for reproducibility evidence.\n"
140
+ )
141
+ if filename == "eval.log":
142
+ return (
143
+ "Backfilled metadata log.\n"
144
+ "No evaluation was executed by this backfill step.\n"
145
+ f"Run directory: {rel_run}\n"
146
+ f"Timestamp UTC: {timestamp}\n"
147
+ "Original eval.log was absent; see command.txt, metrics.json, and table.tex.\n"
148
+ )
149
+ if filename in {"metrics_by_task.json", "metrics_by_seed.json"}:
150
+ grouping = "task" if filename == "metrics_by_task.json" else "seed"
151
+ return json.dumps(
152
+ {
153
+ "_artifact_backfill": True,
154
+ "_grouping": grouping,
155
+ "_run_dir": rel_run,
156
+ "_note": (
157
+ "Original grouped metrics were absent. This placeholder is explicit "
158
+ "metadata, not a computed scientific result."
159
+ ),
160
+ },
161
+ indent=2,
162
+ sort_keys=True,
163
+ ) + "\n"
164
+ raise ValueError(f"unsupported backfill filename: {filename}")
165
+
166
+
167
+ def _latex_table(payload: dict[str, Any]) -> str:
168
+ counts: dict[str, int] = {}
169
+ for row in payload["writes"]:
170
+ counts[row["kind"]] = counts.get(row["kind"], 0) + 1
171
+ lines = [
172
+ "% Auto-generated by scripts/backfill_paper_run_artifacts.py",
173
+ "\\begin{tabular}{lr}",
174
+ "\\toprule",
175
+ "Artifact & Backfilled count \\\\",
176
+ "\\midrule",
177
+ ]
178
+ for key in sorted(counts):
179
+ lines.append(f"{_latex_escape(key)} & {counts[key]} \\\\")
180
+ if not counts:
181
+ lines.append("none & 0 \\\\")
182
+ lines.extend(["\\bottomrule", "\\end{tabular}"])
183
+ return "\n".join(lines)
184
+
185
+
186
+ def _resolve(repo_root: Path, path: Path) -> Path:
187
+ return path if path.is_absolute() else repo_root / path
188
+
189
+
190
+ def _display_path(repo_root: Path, path: Path) -> str:
191
+ try:
192
+ return str(path.relative_to(repo_root))
193
+ except ValueError:
194
+ return str(path)
195
+
196
+
197
+ def _git_hash(repo_root: Path) -> str:
198
+ try:
199
+ return subprocess.check_output(
200
+ ["git", "rev-parse", "HEAD"],
201
+ cwd=repo_root,
202
+ text=True,
203
+ stderr=subprocess.DEVNULL,
204
+ ).strip()
205
+ except Exception:
206
+ return "unknown"
207
+
208
+
209
+ def _first_existing_hash(repo_root: Path, rel_path: str) -> str:
210
+ path = repo_root / rel_path
211
+ if not path.exists():
212
+ return ""
213
+ import hashlib
214
+
215
+ return hashlib.sha256(path.read_bytes()).hexdigest()
216
+
217
+
218
+ def _split_hash(repo_root: Path) -> str:
219
+ path = repo_root / "runs/data_accounting/table.json"
220
+ if not path.exists():
221
+ return ""
222
+ try:
223
+ payload = json.loads(path.read_text())
224
+ except json.JSONDecodeError:
225
+ return ""
226
+ return str(payload.get("split_hash", ""))
227
+
228
+
229
+ def _latex_escape(value: str) -> str:
230
+ return value.replace("_", "\\_").replace("%", "\\%").replace("&", "\\&")
231
+
232
+
233
+ if __name__ == "__main__":
234
+ raise SystemExit(main())