176 lines
7.1 KiB
Python
176 lines
7.1 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from data.majors_catalog.cli import (
|
|
DEFAULT_CATALOG_ROOT,
|
|
build_changes_payload,
|
|
build_lookup_payload,
|
|
build_school_status_payload,
|
|
build_school_verify_payload,
|
|
build_status_payload as build_majors_status_payload,
|
|
build_verify_payload as build_majors_verify_payload,
|
|
)
|
|
from data.majors_catalog.loader import MajorsCatalogLoader
|
|
from data.rules.audit_engine import AuditEngine
|
|
from data.rules.loader import RuleLoader
|
|
|
|
|
|
DEFAULT_RULES_TRUTH_ROOT = Path(__file__).resolve().parents[2] / "rules" / "_truth"
|
|
|
|
|
|
def _build_parser() -> argparse.ArgumentParser:
|
|
parser = argparse.ArgumentParser(prog="gaokao-cli")
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
rules_parser = subparsers.add_parser("rules", help="rules truth source commands")
|
|
rules_sub = rules_parser.add_subparsers(dest="rules_command", required=True)
|
|
|
|
status_parser = rules_sub.add_parser("status", help="show rules truth status")
|
|
status_parser.add_argument("--truth-root", default=str(DEFAULT_RULES_TRUTH_ROOT))
|
|
status_parser.add_argument("--json", action="store_true")
|
|
|
|
verify_parser = rules_sub.add_parser("verify", help="verify rules truth tree")
|
|
verify_parser.add_argument("--truth-root", default=str(DEFAULT_RULES_TRUTH_ROOT))
|
|
verify_parser.add_argument("--json", action="store_true")
|
|
|
|
majors_parser = subparsers.add_parser(
|
|
"majors", help="national majors catalog commands"
|
|
)
|
|
majors_sub = majors_parser.add_subparsers(dest="majors_command", required=True)
|
|
|
|
majors_status = majors_sub.add_parser("status", help="show majors catalog status")
|
|
majors_status.add_argument("--catalog-root", default=str(DEFAULT_CATALOG_ROOT))
|
|
majors_status.add_argument("--json", action="store_true")
|
|
|
|
majors_lookup = majors_sub.add_parser(
|
|
"lookup", help="lookup a major by code or name"
|
|
)
|
|
majors_lookup.add_argument("name_or_code")
|
|
majors_lookup.add_argument("--catalog-root", default=str(DEFAULT_CATALOG_ROOT))
|
|
majors_lookup.add_argument("--json", action="store_true")
|
|
|
|
majors_verify = majors_sub.add_parser(
|
|
"verify", help="verify majors catalog structure"
|
|
)
|
|
majors_verify.add_argument("--catalog-root", default=str(DEFAULT_CATALOG_ROOT))
|
|
majors_verify.add_argument("--json", action="store_true")
|
|
|
|
majors_changes = majors_sub.add_parser(
|
|
"changes", help="list non-active major records"
|
|
)
|
|
majors_changes.add_argument("--catalog-root", default=str(DEFAULT_CATALOG_ROOT))
|
|
majors_changes.add_argument("--json", action="store_true")
|
|
|
|
school_status = majors_sub.add_parser(
|
|
"school-status", help="show school catalog summary for a year"
|
|
)
|
|
school_status.add_argument("--catalog-root", default=str(DEFAULT_CATALOG_ROOT))
|
|
school_status.add_argument("--year", required=True, type=int)
|
|
school_status.add_argument("--json", action="store_true")
|
|
|
|
school_verify = majors_sub.add_parser(
|
|
"school-verify", help="verify school catalog structure for a year"
|
|
)
|
|
school_verify.add_argument("--catalog-root", default=str(DEFAULT_CATALOG_ROOT))
|
|
school_verify.add_argument("--year", required=True, type=int)
|
|
school_verify.add_argument("--json", action="store_true")
|
|
|
|
audit_parser = subparsers.add_parser("audit", help="structured audit commands")
|
|
audit_sub = audit_parser.add_subparsers(dest="audit_command", required=True)
|
|
audit_run = audit_sub.add_parser("run", help="run structured audit on JSON plan")
|
|
audit_run.add_argument("--province", required=True)
|
|
audit_run.add_argument("--plan", required=True)
|
|
audit_run.add_argument("--truth-root", default=str(DEFAULT_RULES_TRUTH_ROOT))
|
|
audit_run.add_argument("--catalog-root", default=str(DEFAULT_CATALOG_ROOT))
|
|
audit_run.add_argument("--json", action="store_true")
|
|
|
|
return parser
|
|
|
|
|
|
def _emit(payload: dict[str, object], json_output: bool) -> int:
|
|
if json_output:
|
|
print(json.dumps(payload, ensure_ascii=False))
|
|
else:
|
|
print(payload)
|
|
return 0 if bool(payload.get("ok", payload.get("overall_pass", True))) else 1
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = _build_parser()
|
|
args = parser.parse_args(argv)
|
|
|
|
if args.command == "rules" and args.rules_command == "status":
|
|
loader = RuleLoader.from_truth_root(Path(args.truth_root))
|
|
status = loader.build_status()
|
|
return _emit(
|
|
{
|
|
"ok": True,
|
|
"province_count": status.province_count,
|
|
"national_rule_count": status.national_rule_count,
|
|
"active_provinces": status.active_provinces,
|
|
},
|
|
args.json,
|
|
)
|
|
|
|
if args.command == "rules" and args.rules_command == "verify":
|
|
loader = RuleLoader.from_truth_root(Path(args.truth_root))
|
|
status = loader.build_status()
|
|
missing_required_files: list[str] = []
|
|
truth_root = Path(args.truth_root)
|
|
if not (truth_root / "national.yaml").is_file():
|
|
missing_required_files.append("national.yaml")
|
|
if not (truth_root / "province").is_dir():
|
|
missing_required_files.append("province/")
|
|
return _emit(
|
|
{
|
|
"ok": not missing_required_files,
|
|
"province_count": status.province_count,
|
|
"national_rule_count": status.national_rule_count,
|
|
"missing_required_files": missing_required_files,
|
|
},
|
|
args.json,
|
|
)
|
|
|
|
if args.command == "majors" and args.majors_command == "status":
|
|
payload = build_majors_status_payload(Path(args.catalog_root))
|
|
return _emit(payload, args.json)
|
|
|
|
if args.command == "majors" and args.majors_command == "lookup":
|
|
payload = build_lookup_payload(Path(args.catalog_root), args.name_or_code)
|
|
return _emit(payload, args.json)
|
|
|
|
if args.command == "majors" and args.majors_command == "verify":
|
|
payload = build_majors_verify_payload(Path(args.catalog_root))
|
|
return _emit(payload, args.json)
|
|
|
|
if args.command == "majors" and args.majors_command == "changes":
|
|
payload = build_changes_payload(Path(args.catalog_root))
|
|
return _emit(payload, args.json)
|
|
|
|
if args.command == "majors" and args.majors_command == "school-status":
|
|
payload = build_school_status_payload(Path(args.catalog_root), args.year)
|
|
return _emit(payload, args.json)
|
|
|
|
if args.command == "majors" and args.majors_command == "school-verify":
|
|
payload = build_school_verify_payload(Path(args.catalog_root), args.year)
|
|
return _emit(payload, args.json)
|
|
|
|
if args.command == "audit" and args.audit_command == "run":
|
|
truth_loader = RuleLoader.from_truth_root(Path(args.truth_root))
|
|
majors_loader = MajorsCatalogLoader.from_catalog_root(Path(args.catalog_root))
|
|
plan = json.loads(Path(args.plan).read_text(encoding="utf-8"))
|
|
payload = AuditEngine(truth_loader, majors_loader=majors_loader).audit_plan(
|
|
args.province, plan
|
|
)
|
|
return _emit(payload, args.json)
|
|
|
|
parser.error("unsupported command")
|
|
return 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|