mirror of
https://github.com/harvard-edge/cs249r_book.git
synced 2026-11-09 10:39:41 -06:00
interviews/ held StaffML and nothing else, so the directory now carries the product's name. Three names inside it repeated that prefix, and now line up with vault/, vault-cli/, and paper/: interviews/staffml/ -> staffml/app/ interviews/staffml-vault-worker/ -> staffml/vault-worker/ interviews/staffml-vault-types/ -> staffml/vault-types/ Every repository path moved with it: workflow paths: triggers and variable fallbacks, pre-commit hooks, .gitignore, contributor tooling, vault-cli path joins, the app's tsconfig mapping to the shared types, and the docs. Left alone on purpose: - the published /interviews/ redirect to /staffml/ and the gh-pages subsite lists that protect it; those are URLs, not repository paths - package and worker names (staffml-vault-worker, @staffml/vault-types) and the vitest job name, which branch protection may key on - question YAMLs and generated track JSON, which are content-addressed. Three questions carry an older corruption, "recall @interviews/vault/ questions/...", that predates this change - the applied D1 migration 0001 and the vault CHANGELOG, which are history codegen-hashes.txt is re-recorded; the three pinned artifacts changed only in path strings. vault-cli: 97 passed, codegen --check clean. interviewer.py's __main__ loader was one parent short and pointed at a corpus.json that never existed; it now resolves staffml/app/src/data/ corpus.json. tools/validate_playbook.py and generate_interview_flashcard.py still expect per-track directories that no longer exist; they point at the new root but remain dead. The GitHub repository variables STAFFML_ROOT, VAULT_DIR, and VAULT_CLI_DIR still name the old paths and must change when this reaches origin.
102 lines
3.1 KiB
Python
102 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Add explicit status / provenance / deletion_reason where the YAML is
|
|
silently relying on Pydantic defaults or violating soft-delete pairing.
|
|
|
|
Three classes of fix:
|
|
|
|
A. status field missing entirely
|
|
Pydantic defaults to 'draft', but the YAML on disk lacks the field.
|
|
Add explicit `status: draft` + `provenance: imported` so the file
|
|
no longer relies on a silent default.
|
|
|
|
B. deleted but no deletion_reason
|
|
Soft-delete pairing rule (Question.status='deleted' must carry
|
|
deletion_reason). Add placeholder text.
|
|
|
|
C. flagged with no human_reviewed
|
|
Reported but not auto-fixed; needs human disposition.
|
|
|
|
Usage:
|
|
|
|
python3 staffml/vault-cli/scripts/fix_missing_metadata.py [--dry-run]
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
from collections import Counter
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
sys.path.insert(0, str(REPO_ROOT / "staffml" / "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 / "staffml" / "vault" / "questions"
|
|
PLACEHOLDER_REASON = (
|
|
"imported as deleted; original deletion reason was not preserved on import"
|
|
)
|
|
|
|
|
|
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("--dry-run", action="store_true")
|
|
args = ap.parse_args()
|
|
|
|
counters: Counter[str] = Counter()
|
|
|
|
for yp in QUESTIONS_DIR.rglob("*.yaml"):
|
|
body = load_file(yp)
|
|
if not isinstance(body, dict):
|
|
continue
|
|
changed = False
|
|
|
|
# Class A: status field missing → set draft + imported
|
|
if "status" not in body:
|
|
body["status"] = "draft"
|
|
if "provenance" not in body:
|
|
body["provenance"] = "imported"
|
|
counters["status-added"] += 1
|
|
changed = True
|
|
|
|
# Class B: deleted without deletion_reason
|
|
if body.get("status") == "deleted" and not body.get("deletion_reason"):
|
|
body["deletion_reason"] = PLACEHOLDER_REASON
|
|
counters["deletion-reason-added"] += 1
|
|
changed = True
|
|
|
|
if not changed:
|
|
continue
|
|
|
|
try:
|
|
Question.model_validate(body)
|
|
except Exception as e:
|
|
counters["pydantic-fail"] += 1
|
|
print(f" pydantic-fail {body.get('id')}: {str(e)[:200]}", file=sys.stderr)
|
|
continue
|
|
|
|
if args.dry_run:
|
|
print(f" [dry] {body.get('id')}: status={body.get('status')!r}, "
|
|
f"provenance={body.get('provenance')!r}, "
|
|
f"deletion_reason={'yes' if body.get('deletion_reason') else 'no'}")
|
|
else:
|
|
write_yaml(yp, body)
|
|
|
|
print(f"\ncounters: {dict(counters)}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|