mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-05-07 10:08:50 -05:00
apply_math_skip_level.py is a Phase 5 cleanup helper. For the small set of qids whose math fix carries a level relabel that's chain-blocked or relabel-up, the math correction is independently verified and applies cleanly — only the level relabel is the chain-team / authoring decision. This script applies napkin_math/realistic_solution/common_mistake while leaving level untouched, writing a 05_math_skip_level.json sidecar. verify_math_corrections.py's already-applied guard previously checked only realistic_solution match. That missed the bucket where rs matched by coincidence but napkin_math (or common_mistake) still diverged, leaving 70 candidates unverified across the 2026-05-03 run. The guard now considers all three math fields.
149 lines
5.2 KiB
Python
149 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Apply math-only corrections for the 13 qids whose level relabel was blocked.
|
|
|
|
Phase 5 verify_math_corrections.py applied 204 of 217 Gemini-verified math
|
|
fixes. The remaining 13 were skipped because their accompanying level
|
|
relabel violated chain monotonicity or was a relabel-up (against §10 Q3
|
|
policy). The math fix itself was independently verified by Gemini-2.
|
|
|
|
This script applies napkin_math + realistic_solution + common_mistake
|
|
for those 13 qids while LEAVING the level field untouched. The level
|
|
relabel question stays in PHASE_5_UNRESOLVED.md for human review (it's
|
|
chain-team / authoring territory).
|
|
|
|
Usage:
|
|
|
|
python3 interviews/vault-cli/scripts/apply_math_skip_level.py \\
|
|
--merged-dir interviews/vault/_pipeline/runs/full-corpus-20260503-merged
|
|
|
|
CORPUS_HARDENING_PLAN.md Phase 5 — math-skip-level cleanup leg.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import UTC, datetime
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
sys.path.insert(0, str(REPO_ROOT / "interviews" / "vault-cli" / "src"))
|
|
|
|
from vault_cli.models import Question # noqa: E402
|
|
from vault_cli.yaml_io import dump_str, load_file # noqa: E402
|
|
|
|
QUESTIONS_DIR = REPO_ROOT / "interviews" / "vault" / "questions"
|
|
|
|
|
|
def find_question_file(qid: str) -> Path | None:
|
|
for p in QUESTIONS_DIR.rglob(f"{qid}.yaml"):
|
|
return p
|
|
return None
|
|
|
|
|
|
def apply_math_only(body: dict, correction: dict) -> dict:
|
|
out = json.loads(json.dumps(body))
|
|
details = out.setdefault("details", {})
|
|
if correction.get("napkin_math"):
|
|
details["napkin_math"] = correction["napkin_math"]
|
|
if correction.get("realistic_solution"):
|
|
details["realistic_solution"] = correction["realistic_solution"]
|
|
if correction.get("common_mistake"):
|
|
details["common_mistake"] = correction["common_mistake"]
|
|
return out
|
|
|
|
|
|
def write_yaml(path: Path, body: dict) -> None:
|
|
text = dump_str(body)
|
|
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
tmp.write_text(text, encoding="utf-8")
|
|
os.replace(tmp, path)
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--merged-dir", type=Path, required=True,
|
|
help="dir holding 01_audit.json + 04_math_applied.json")
|
|
ap.add_argument("--dry-run", action="store_true",
|
|
help="show plan without writing")
|
|
args = ap.parse_args()
|
|
|
|
audit = json.loads((args.merged_dir / "01_audit.json").read_text(encoding="utf-8"))
|
|
math_applied = json.loads((args.merged_dir / "04_math_applied.json").read_text(encoding="utf-8"))
|
|
|
|
rows_by_qid = {r["qid"]: r for r in audit["rows"]}
|
|
target_qids = [d["qid"] for d in math_applied["dispositions"]
|
|
if d["result"] == "level-block"]
|
|
print(f"target qids: {len(target_qids)}")
|
|
|
|
dispositions: list[dict] = []
|
|
counters = {"applied": 0, "yaml-missing": 0, "pydantic-fail": 0,
|
|
"no-correction": 0, "no-change": 0}
|
|
|
|
for qid in target_qids:
|
|
row = rows_by_qid.get(qid)
|
|
if not row or not row.get("suggested_corrections"):
|
|
counters["no-correction"] += 1
|
|
dispositions.append({"qid": qid, "result": "no-correction"})
|
|
continue
|
|
correction = row["suggested_corrections"]
|
|
if not (correction.get("napkin_math")
|
|
or correction.get("realistic_solution")
|
|
or correction.get("common_mistake")):
|
|
counters["no-change"] += 1
|
|
dispositions.append({"qid": qid, "result": "no-change"})
|
|
continue
|
|
|
|
yp = find_question_file(qid)
|
|
if not yp:
|
|
counters["yaml-missing"] += 1
|
|
dispositions.append({"qid": qid, "result": "yaml-missing"})
|
|
continue
|
|
body = load_file(yp)
|
|
if not isinstance(body, dict):
|
|
continue
|
|
|
|
proposed = apply_math_only(body, correction)
|
|
if proposed == body:
|
|
counters["no-change"] += 1
|
|
dispositions.append({"qid": qid, "result": "no-change"})
|
|
continue
|
|
|
|
try:
|
|
Question.model_validate(proposed)
|
|
except Exception as e:
|
|
counters["pydantic-fail"] += 1
|
|
dispositions.append({"qid": qid, "result": "pydantic-fail",
|
|
"error": str(e)[:300]})
|
|
continue
|
|
|
|
if args.dry_run:
|
|
print(f" [dry] {qid}: would update math fields, level "
|
|
f"{body.get('level')!r} unchanged")
|
|
else:
|
|
write_yaml(yp, proposed)
|
|
|
|
counters["applied"] += 1
|
|
dispositions.append({"qid": qid, "result": "applied",
|
|
"kept_level": body.get("level")})
|
|
|
|
print(f"\ncounters: {counters}")
|
|
|
|
out_path = args.merged_dir / "05_math_skip_level.json"
|
|
if not args.dry_run:
|
|
out_path.write_text(json.dumps({
|
|
"generated_at": datetime.now(UTC).isoformat(timespec="seconds"),
|
|
"summary": counters,
|
|
"dispositions": dispositions,
|
|
}, indent=2) + "\n", encoding="utf-8")
|
|
print(f"wrote {out_path}")
|
|
|
|
return 0 if counters["applied"] > 0 else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|