348 lines
12 KiB
Python
348 lines
12 KiB
Python
from __future__ import annotations
|
||
|
||
import importlib.util
|
||
from importlib.machinery import SourceFileLoader
|
||
from pathlib import Path
|
||
|
||
import yaml # type: ignore[import-untyped]
|
||
|
||
|
||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||
LEGACY_CHECKER_PATH = PROJECT_ROOT / "scripts" / "gaokao-checker"
|
||
|
||
|
||
def _load_python_module(name: str, path: Path):
|
||
loader = SourceFileLoader(name, str(path))
|
||
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||
assert spec is not None and spec.loader is not None
|
||
module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(module)
|
||
return module
|
||
|
||
|
||
def _load_legacy_checker_module():
|
||
loader = SourceFileLoader("legacy_gaokao_checker", str(LEGACY_CHECKER_PATH))
|
||
spec = importlib.util.spec_from_loader(loader.name, loader)
|
||
assert spec is not None and spec.loader is not None
|
||
module = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(module)
|
||
return module
|
||
|
||
|
||
def test_migrate_province_rules_to_truth_exports_all_legacy_provinces(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
legacy = _load_legacy_checker_module()
|
||
|
||
migrate = _load_python_module(
|
||
'migrate_province_rules_to_truth',
|
||
PROJECT_ROOT / 'scripts' / 'migrate_province_rules_to_truth.py',
|
||
)
|
||
|
||
summary = migrate.migrate_legacy_rules_to_truth(output_root=tmp_path)
|
||
|
||
province_dir = tmp_path / "province"
|
||
province_files = sorted(province_dir.glob("*.yaml"))
|
||
|
||
assert summary["province_count"] == len(legacy.PROVINCE_RULES)
|
||
assert len(province_files) == len(legacy.PROVINCE_RULES)
|
||
assert (tmp_path / "national.yaml").is_file()
|
||
|
||
hunan_truth = yaml.safe_load(
|
||
(province_dir / "hunan.yaml").read_text(encoding="utf-8")
|
||
)
|
||
assert hunan_truth["province"] == "湖南"
|
||
assert hunan_truth["year"] == 2026
|
||
assert hunan_truth["status"] == "active"
|
||
assert hunan_truth["rules"]["max_volunteers"]["value"] == {"max_volunteers": 45}
|
||
assert hunan_truth["rules"]["max_volunteers"]["source_evidence_id"]
|
||
|
||
|
||
def test_rule_loader_reads_national_and_province_truth_tree(tmp_path: Path) -> None:
|
||
truth_root = tmp_path
|
||
(truth_root / "province").mkdir(parents=True, exist_ok=True)
|
||
|
||
(truth_root / "national.yaml").write_text(
|
||
yaml.safe_dump(
|
||
{
|
||
"scope": "national",
|
||
"year": 2026,
|
||
"version": "2026.1",
|
||
"last_verified_at": "2026-06-17",
|
||
"rules": {
|
||
"parallel_volunteer_principle": {
|
||
"title": "平行志愿原则",
|
||
"severity": "info",
|
||
"value": {"rule": "分数优先、遵循志愿、一次投档"},
|
||
"source_evidence_id": "national-2026-parallel-rule",
|
||
"effective_date": "2026-01-01",
|
||
"last_verified_at": "2026-06-17",
|
||
"status": "active",
|
||
}
|
||
},
|
||
},
|
||
allow_unicode=True,
|
||
sort_keys=False,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
(truth_root / "province" / "hunan.yaml").write_text(
|
||
yaml.safe_dump(
|
||
{
|
||
"scope": "province",
|
||
"province": "湖南",
|
||
"year": 2026,
|
||
"version": "2026.1",
|
||
"last_verified_at": "2026-06-17",
|
||
"status": "active",
|
||
"rules": {
|
||
"max_volunteers": {
|
||
"title": "本科批志愿上限",
|
||
"severity": "fatal",
|
||
"value": {"max_volunteers": 45},
|
||
"source_evidence_id": "hunan-2026-max-volunteers",
|
||
"effective_date": "2026-01-01",
|
||
"last_verified_at": "2026-06-17",
|
||
"status": "active",
|
||
}
|
||
},
|
||
},
|
||
allow_unicode=True,
|
||
sort_keys=False,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
from data.rules.loader import RuleLoader
|
||
|
||
evidence_root = tmp_path.parent / "_evidence" / "hunan"
|
||
evidence_root.mkdir(parents=True, exist_ok=True)
|
||
(evidence_root / "hunan-2026-max-volunteers.md").write_text(
|
||
"\n".join(
|
||
[
|
||
"# hunan-2026-max-volunteers",
|
||
"",
|
||
"> 对应规则: `HUNAN.max_volunteers`",
|
||
"> 所属: 省级",
|
||
"> 规则版本: `2026.1`",
|
||
"> 摘录时间: 2026-06-17",
|
||
"> 摘录人: pytest",
|
||
"",
|
||
"## 1. 官方原文摘录",
|
||
"",
|
||
'> "湖南本科批院校专业组最多 45 个志愿。" —— 出处: 湖南省招生规则, http://example.com, 2026-06-01',
|
||
"",
|
||
"## 2. 转写为机读规则",
|
||
"",
|
||
"```yaml",
|
||
"HUNAN.max_volunteers:",
|
||
" severity: fatal",
|
||
" value:",
|
||
" max_volunteers: 45",
|
||
" effective_date: 2026-01-01",
|
||
" source_evidence_id: hunan-2026-max-volunteers",
|
||
" status: active",
|
||
"```",
|
||
"",
|
||
"## 3. 关键边界与例外",
|
||
"",
|
||
"- 例 1:示例",
|
||
"- 例 2:示例",
|
||
"",
|
||
"## 4. 后续维护",
|
||
"",
|
||
"- 下次复核时间: 2026-09-01",
|
||
"- 复核来源: http://example.com",
|
||
"- 复核负责人: pytest",
|
||
"",
|
||
]
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
loader = RuleLoader.from_truth_root(truth_root)
|
||
status = loader.build_status()
|
||
rules = loader.list_province_rules("湖南")
|
||
national = loader.list_national_rules()
|
||
|
||
assert status.province_count == 1
|
||
assert status.national_rule_count == 1
|
||
assert status.active_provinces == ["湖南"]
|
||
assert status.national_version == "2026.1"
|
||
assert status.stale_rule_max_age_days == 90
|
||
assert status.stale_rule_count == 0
|
||
assert status.stale_rule_ids == []
|
||
assert rules[0].rule_id == "HUNAN.max_volunteers"
|
||
assert rules[0].value["max_volunteers"] == 45
|
||
assert rules[0].last_verified_at == "2026-06-17"
|
||
assert rules[0].evidence_exists is True
|
||
assert national[0].rule_id == "NATIONAL.parallel_volunteer_principle"
|
||
|
||
|
||
def test_rule_loader_verify_flags_stale_or_missing_evidence(tmp_path: Path) -> None:
|
||
truth_root = tmp_path / "truth"
|
||
province_dir = truth_root / "province"
|
||
province_dir.mkdir(parents=True, exist_ok=True)
|
||
(truth_root / "national.yaml").write_text(
|
||
yaml.safe_dump(
|
||
{
|
||
"scope": "national",
|
||
"year": 2026,
|
||
"version": "2026.1",
|
||
"last_verified_at": "2026-01-01",
|
||
"rules": {
|
||
"parallel_volunteer_principle": {
|
||
"title": "平行志愿原则",
|
||
"severity": "info",
|
||
"value": {"rule": "分数优先、遵循志愿、一次投档"},
|
||
"source_evidence_id": "national-2026-parallel-rule",
|
||
"effective_date": "2026-01-01",
|
||
"last_verified_at": "2026-01-01",
|
||
"status": "active",
|
||
}
|
||
},
|
||
},
|
||
allow_unicode=True,
|
||
sort_keys=False,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
(province_dir / "hunan.yaml").write_text(
|
||
yaml.safe_dump(
|
||
{
|
||
"scope": "province",
|
||
"province": "湖南",
|
||
"year": 2026,
|
||
"version": "2026.1",
|
||
"last_verified_at": "2026-01-01",
|
||
"status": "active",
|
||
"rules": {
|
||
"max_volunteers": {
|
||
"title": "本科批志愿上限",
|
||
"severity": "fatal",
|
||
"value": {"max_volunteers": 45},
|
||
"source_evidence_id": "hunan-2026-max-volunteers",
|
||
"effective_date": "2026-01-01",
|
||
"last_verified_at": "2026-01-01",
|
||
"status": "active",
|
||
}
|
||
},
|
||
},
|
||
allow_unicode=True,
|
||
sort_keys=False,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
from data.rules.loader import RuleLoader
|
||
|
||
loader = RuleLoader.from_truth_root(truth_root)
|
||
payload = loader.verify(strict_evidence=True, max_rule_age_days=90)
|
||
|
||
assert payload["ok"] is False
|
||
assert payload["stale_rule_count"] == 2
|
||
assert "NATIONAL.parallel_volunteer_principle" in payload["stale_rule_ids"]
|
||
assert "HUNAN.max_volunteers" in payload["stale_rule_ids"]
|
||
assert payload["missing_evidence_rule_count"] == 2
|
||
assert "NATIONAL.parallel_volunteer_principle" in payload["missing_evidence_rule_ids"]
|
||
assert "HUNAN.max_volunteers" in payload["missing_evidence_rule_ids"]
|
||
|
||
|
||
def test_rule_loader_scaffold_templates_do_not_count_as_completed_evidence(
|
||
tmp_path: Path,
|
||
) -> None:
|
||
truth_root = tmp_path / "truth"
|
||
province_dir = truth_root / "province"
|
||
province_dir.mkdir(parents=True, exist_ok=True)
|
||
(truth_root / "national.yaml").write_text(
|
||
yaml.safe_dump(
|
||
{
|
||
"scope": "national",
|
||
"year": 2026,
|
||
"version": "2026.1",
|
||
"last_verified_at": "2026-06-17",
|
||
"rules": {},
|
||
},
|
||
allow_unicode=True,
|
||
sort_keys=False,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
(province_dir / "hunan.yaml").write_text(
|
||
yaml.safe_dump(
|
||
{
|
||
"scope": "province",
|
||
"province": "湖南",
|
||
"year": 2026,
|
||
"version": "2026.1",
|
||
"last_verified_at": "2026-06-17",
|
||
"status": "active",
|
||
"rules": {
|
||
"max_volunteers": {
|
||
"title": "本科批志愿上限",
|
||
"severity": "fatal",
|
||
"value": {"max_volunteers": 45},
|
||
"source_evidence_id": "hunan-2026-max-volunteers",
|
||
"effective_date": "2026-01-01",
|
||
"last_verified_at": "2026-06-17",
|
||
"status": "active",
|
||
}
|
||
},
|
||
},
|
||
allow_unicode=True,
|
||
sort_keys=False,
|
||
),
|
||
encoding="utf-8",
|
||
)
|
||
|
||
from data.rules.loader import RuleLoader
|
||
|
||
loader = RuleLoader.from_truth_root(truth_root)
|
||
payload = loader.scaffold_missing_evidence(province="湖南")
|
||
reloaded = RuleLoader.from_truth_root(truth_root)
|
||
rules = reloaded.list_province_rules("湖南")
|
||
|
||
assert payload["created_rule_count"] == 1
|
||
assert payload["touched_index_count"] == 1
|
||
evidence_path = tmp_path / "_evidence" / "hunan" / "hunan-2026-max-volunteers.md"
|
||
assert evidence_path.is_file()
|
||
assert "证据状态: draft_template" in evidence_path.read_text(encoding="utf-8")
|
||
assert rules[0].evidence_exists is False
|
||
|
||
|
||
def test_migrated_truth_set_matches_legacy_checker_provinces(tmp_path: Path) -> None:
|
||
legacy = _load_legacy_checker_module()
|
||
|
||
migrate = _load_python_module(
|
||
'migrate_province_rules_to_truth',
|
||
PROJECT_ROOT / 'scripts' / 'migrate_province_rules_to_truth.py',
|
||
)
|
||
from data.rules.loader import RuleLoader
|
||
|
||
migrate.migrate_legacy_rules_to_truth(output_root=tmp_path)
|
||
loader = RuleLoader.from_truth_root(tmp_path)
|
||
|
||
assert set(loader.active_provinces()) == set(legacy.PROVINCE_RULES.keys())
|
||
|
||
|
||
def test_audit_engine_exposes_province_snapshot_from_truth(tmp_path: Path) -> None:
|
||
from data.rules.audit_engine import AuditEngine
|
||
from data.rules.loader import RuleLoader
|
||
migrate = _load_python_module(
|
||
'migrate_province_rules_to_truth',
|
||
PROJECT_ROOT / 'scripts' / 'migrate_province_rules_to_truth.py',
|
||
)
|
||
|
||
migrate.migrate_legacy_rules_to_truth(output_root=tmp_path)
|
||
loader = RuleLoader.from_truth_root(tmp_path)
|
||
engine = AuditEngine(loader)
|
||
|
||
snapshot = engine.get_province_snapshot("湖南")
|
||
|
||
assert snapshot.province == "湖南"
|
||
assert snapshot.mode == "院校专业组"
|
||
assert snapshot.max_volunteers == 45
|
||
assert snapshot.retrieval_rule == "分数优先、遵循志愿、一次投档"
|
||
assert snapshot.rule_count >= 5
|