feat(cli): add order delegation + doctor self-check (phase 3)
Some checks failed
CI / pytest (Python 3.10) (push) Has been cancelled
CI / pytest (Python 3.11) (push) Has been cancelled
CI / pytest (Python 3.12) (push) Has been cancelled

docs-only + cli: extend scripts/gaokao-cli with two thin wrappers
without changing data/rules, data/majors_catalog, or rules/_truth.

- gaokao-cli order <subcommand> delegates argv to data.orders.cli.main
  with the 'order' prefix stripped, so callers can use the unified
  command face without changing the existing orders parser.
- gaokao-cli doctor <json?> returns a single self-check payload that
  reuses RuleLoader + MajorsCatalogLoader (no new dependencies, no
  gaokato/ scaffolding). Truth-root or catalog-root failures degrade
  gracefully with structured {error, ok=false} sections instead of
  raising.
- tests/test_cli_doctor_phase3.py covers: help text, order delegation
  to list subcommand, doctor ok-path, doctor failure-path.

Also re-aligns docs/ACTIVE_EXECUTION_BOARD_2026-06-17.md (new) +
docs/CURRENT_STATE.md (single execution口径) + rules/majors source-
of-truth index files so design-phase numbering no longer drifts from
execution-phase numbering. Pre-existing 2026-06-16 optimization board
and 2026-06-17 phase2 plan are demoted to historical snapshots.
P0_P1_P2 remediation plan §4 historical cards get a red warning
banner so they are not re-read as current pending.

Verification:
- focused: 4 passed
- dev-verify: 770 passed, 1 failed
  (alipay_sim E2E failure pre-existed this commit; caused by an
  uncommitted upstream edit in admin/routes/web_public.py that
  rewires payment_return -> /portal/{token}/payment-success while
  the E2E still asserts /portal/{token}/status; out of scope for
  this commit and intentionally not touched.)
This commit is contained in:
Hermes
2026-06-17 18:05:16 +08:00
parent edc5b11230
commit 7d31d75145
10 changed files with 514 additions and 153 deletions

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from data.majors_catalog.cli import (
@@ -87,6 +88,15 @@ def _build_parser() -> argparse.ArgumentParser:
audit_run.add_argument("--catalog-root", default=str(DEFAULT_CATALOG_ROOT))
audit_run.add_argument("--json", action="store_true")
subparsers.add_parser("order", help="delegate to data.orders.cli (orders commands)")
doctor_parser = subparsers.add_parser(
"doctor", help="self-check rules/majors/audit/catalog wiring"
)
doctor_parser.add_argument("--truth-root", default=str(DEFAULT_RULES_TRUTH_ROOT))
doctor_parser.add_argument("--catalog-root", default=str(DEFAULT_CATALOG_ROOT))
doctor_parser.add_argument("--json", action="store_true")
return parser
@@ -99,8 +109,19 @@ def _emit(payload: dict[str, object], json_output: bool) -> int:
def main(argv: list[str] | None = None) -> int:
# `order` is a pure delegation marker. Handle it before argparse so
# the orders parser can see its native subcommand + flags directly
# (including `--help`).
tokens = list(sys.argv[1:]) if argv is None else list(argv)
if tokens and tokens[0] == "order":
from data.orders.cli import main as orders_main
# data.orders.cli.main expects subcommands without the `order`
# prefix (its parser exposes `create` / `list` / `show` / ...).
return orders_main(tokens[1:])
parser = _build_parser()
args = parser.parse_args(argv)
args = parser.parse_args(tokens)
if args.command == "rules" and args.rules_command == "status":
loader = RuleLoader.from_truth_root(Path(args.truth_root))
@@ -167,6 +188,48 @@ def main(argv: list[str] | None = None) -> int:
)
return _emit(payload, args.json)
if args.command == "doctor":
truth_root = Path(args.truth_root)
catalog_root = Path(args.catalog_root)
rules_section: dict[str, object]
rules_ok: bool
try:
rule_status = RuleLoader.from_truth_root(truth_root).build_status()
rules_section = {
"province_count": rule_status.province_count,
"national_rule_count": rule_status.national_rule_count,
}
rules_ok = rule_status.province_count > 0
except (FileNotFoundError, KeyError, ValueError) as exc:
rules_section = {"error": f"{type(exc).__name__}: {exc}"}
rules_ok = False
def _safe_build(builder: object, root: Path) -> tuple[dict[str, object], bool]:
try:
payload = builder(root) # type: ignore[operator]
return dict(payload), bool(payload.get("ok", True)) # type: ignore[union-attr]
except (FileNotFoundError, KeyError, ValueError) as exc:
return (
{"ok": False, "error": f"{type(exc).__name__}: {exc}"},
False,
)
majors_status, majors_status_ok = _safe_build(
build_majors_status_payload, catalog_root
)
majors_verify, majors_verify_ok = _safe_build(
build_majors_verify_payload, catalog_root
)
majors_ok = majors_status_ok and majors_verify_ok
overall_ok = rules_ok and majors_ok
payload = {
"ok": overall_ok,
"rules": rules_section,
"majors": majors_status,
"majors_verify": majors_verify,
}
return _emit(payload, args.json)
parser.error("unsupported command")
return 2

View File

@@ -1,5 +1,8 @@
# Batch 1 审计收敛执行板2026-06-16
> **状态****已收口(历史快照)**。当前执行真相请看 `docs/ACTIVE_EXECUTION_BOARD_2026-06-17.md` + `docs/CURRENT_STATE.md` §0。
> 本文件仅作历史审计记录保留,不再作为当前执行口径源。
真相源输入:
1. `docs/plans/2026-06-16-optimization-program.md`

View File

@@ -0,0 +1,97 @@
# ACTIVE_EXECUTION_BOARD_2026-06-17
> 真相源: `docs/CURRENT_STATE.md` §0
> 设计快照: `docs/DESIGN_RULES_TRUSTED_CLI_2026-06-16.md`
> 优化计划: `docs/plans/2026-06-16-optimization-program.md`
> 状态: 已收口 / 未启动 两段式
---
## 1. 已收口(执行 Phase 1 / 1.5 / 2
### 1.1 执行 Phase 1 + 1.5 — 规则真相源化
- 已落地:
- `rules/_truth/national.yaml`(国家级规则)
- `rules/_truth/province/*.yaml`27 省2026 版)
- `data/rules/loader.py` `RuleLoader.from_truth_root`
- `data/rules/audit_engine.py` `AuditEngine.audit_plan`
- `data/rules/cli.py` `rules {status,verify}`
- `scripts/migrate_province_rules_to_truth.py`(从 `PROVINCE_RULES` 一次性迁移)
- `tests/test_rules_truth_phase1.py` + `tests/test_audit_cli_major_validation_phase2.py`
- 提交链:
- `ae4835e` rules truth source loader + rules CLI
- `8a61b8f` phase1.5 verify 收口 + 旧 checker 迁移
- 三仓: gitea / origin / tksea 同步至 `edc5b11`
### 1.2 执行 Phase 2 Batch 1 — 国家级专业目录 MVP
- 已落地:
- `data/majors_catalog/models.py` `NationalMajor`
- `data/majors_catalog/loader.py` `MajorsCatalogLoader`
- `data/majors_catalog/national/2024.json` + `national/latest.json`13 个 curated subset
- `data/majors_catalog/cli.py` `majors {status,lookup,verify,changes}`
- `tests/test_majors_catalog_phase2.py` + `tests/test_majors_cli_phase2.py`
- 提交: `36ad58a feat(majors): add phase2 national catalog mvp`
### 1.3 执行 Phase 2 Batch 2 — 校级招生目录骨架
- 已落地:
- `data/majors_catalog/schools/2026/10001.json`1 所校级骨架样本)
- `data/majors_catalog/cli.py` `majors school-status / school-verify --year`
- `tests/test_majors_school_catalog_phase2.py` + `tests/test_majors_school_cli_phase2.py`
- 提交: `6b1157f feat(majors): add school catalog skeleton and verification`
### 1.4 执行 Phase 2 Batch 3 — audit_engine 接入 major_validation
- 已落地:
- `data/rules/audit_engine.py` `_validate_majors` 接入 `audit_plan`
- `data/rules/cli.py` `audit run --province --plan --truth-root --catalog-root --json`
- 输出口径: `MAJORS.not_found` / `MAJORS.non_active`
- `tests/test_audit_engine_major_validation_phase2.py` + `tests/test_audit_cli_major_validation_phase2.py`
- 提交: `edc5b11 feat(audit): validate majors in structured audit flow`
---
## 2. 未启动(执行 Phase 3 = 统一 CLI 命令面)
> 这是当前唯一未启动且**不依赖外部前置**的 P1 主目标。
> Lane D设计的 Phase 4 = Lane E执行的 Phase 3请勿混淆。
### 2.1 范围
按 6/16 优化计划 Phase 4 / 设计层 Phase 4 收敛:
- `gaokao-cli {order,share,delivery,retention,payment,backup,channel,doctor}` 收纳
- `gaokao-cli doctor` 最小自检(已有 loader / engine / CLI 调用面,不依赖 `gaokato/`
-`scripts/gaokao-*` wrapper 的 deprecation warning
- `docs/CLI_API_MAPPING.md` 同步"已落地命令清单"节
### 2.2 暂不进入本轮
- `gaokato/services/*``gaokato/transport/*`(设计层 Phase 5 = 执行层 Phase 4
- `gaokato/capabilities/registry.py`(等 doctor 落地后再用其 JSON 输出作为初版源)
- 退出码统一改写(沿用各子 CLI 现状,不与 Lane D 退出码收口任务冲突)
### 2.3 不可触发的真源改动
- 不改 `data/rules/loader.py` 字段
- 不改 `data/majors_catalog/*` 数据契约
- 不改 `rules/_truth/*` yaml
- 不重写 `data/orders/cli.py` 状态机
---
## 3. 收口规则(防止 4 条线再漂)
1. 状态查询一律走本文件 + `docs/CURRENT_STATE.md` §0
2. 设计层 Phase 编号不等于执行层 Phase 编号;引用必须带"执行/设计"前缀
3. 新写"待做"必须指明属于哪个执行 Phase
4. 历史评审/历史快照/历史整改计划三类文件不再进入本板追踪
5. docs-only commit 必须在 commit message 标注"docs-only",避免与代码 commit 混轨
---
## 4. 与本板同步的当前任务文档
- `/home/long/.hermes/docs/tasks/current/TASK-20260617-gaokao-optimization-batch2.md`

View File

@@ -0,0 +1,94 @@
# CURRENT_RULES_STATE_2026-06-16
最后更新: 2026-06-16
真相源: `docs/RULES_SOURCE_OF_TRUTH.md`
执行上下文: `docs/ACTIVE_EXECUTION_BOARD_2026-06-17.md` §1.1
---
## 1. 国家级规则覆盖矩阵
| scope | rule_key | title | severity | status | source_evidence_id |
| -------- | ------------- | ------------ | -------- | ------ | --------------------------- |
| national | parallel_rule | 平行志愿规则 | info | active | national-2026-parallel-rule |
> 当前国家级仅 1 条(参考用);其余通用规则随各省差异实现。
---
## 2. 省级规则覆盖矩阵2026 版)
| province | slug | rules_count | status | yaml |
| -------- | ------------ | ----------- | ------ | ----------------------------------------- |
| 北京 | beijing | TBD | active | `rules/_truth/province/beijing.yaml` |
| 上海 | shanghai | TBD | active | `rules/_truth/province/shanghai.yaml` |
| 天津 | tianjin | TBD | active | `rules/_truth/province/tianjin.yaml` |
| 重庆 | chongqing | TBD | active | `rules/_truth/province/chongqing.yaml` |
| 河北 | hebei | TBD | active | `rules/_truth/province/hebei.yaml` |
| 山西 | shanxi | TBD | active | `rules/_truth/province/shanxi.yaml` |
| 内蒙古 | (待补) | — | — | — |
| 辽宁 | liaoning | TBD | active | `rules/_truth/province/liaoning.yaml` |
| 吉林 | jilin | TBD | active | `rules/_truth/province/jilin.yaml` |
| 黑龙江 | heilongjiang | TBD | active | `rules/_truth/province/heilongjiang.yaml` |
| 江苏 | jiangsu | TBD | active | `rules/_truth/province/jiangsu.yaml` |
| 浙江 | zhejiang | TBD | active | `rules/_truth/province/zhejiang.yaml` |
| 安徽 | anhui | TBD | active | `rules/_truth/province/anhui.yaml` |
| 福建 | fujian | TBD | active | `rules/_truth/province/fujian.yaml` |
| 江西 | jiangxi | TBD | active | `rules/_truth/province/jiangxi.yaml` |
| 山东 | shandong | TBD | active | `rules/_truth/province/shandong.yaml` |
| 河南 | henan | TBD | active | `rules/_truth/province/henan.yaml` |
| 湖北 | hubei | TBD | active | `rules/_truth/province/hubei.yaml` |
| 湖南 | hunan | TBD | active | `rules/_truth/province/hunan.yaml` |
| 广东 | guangdong | TBD | active | `rules/_truth/province/guangdong.yaml` |
| 广西 | guangxi | TBD | active | `rules/_truth/province/guangxi.yaml` |
| 海南 | hainan | TBD | active | `rules/_truth/province/hainan.yaml` |
| 四川 | sichuan | TBD | active | `rules/_truth/province/sichuan.yaml` |
| 贵州 | guizhou | TBD | active | `rules/_truth/province/guizhou.yaml` |
| 云南 | yunnan | TBD | active | `rules/_truth/province/yunnan.yaml` |
| 西藏 | xizang | TBD | active | `rules/_truth/province/xizang.yaml` |
| 陕西 | (待补) | — | — | — |
| 甘肃 | gansu | TBD | active | `rules/_truth/province/gansu.yaml` |
| 青海 | qinghai | TBD | active | `rules/_truth/province/qinghai.yaml` |
| 宁夏 | (待补) | — | — | — |
| 新疆 | xinjiang | TBD | active | `rules/_truth/province/xinjiang.yaml` |
> 当前已落地 27 省 yaml不含内蒙古/陕西/宁夏3 个待补)。每省 `rules_count` 由本轮 Batch 4 候选任务回填。
---
## 3. 证据链覆盖矩阵
| 类别 | 已落地 | 备注 |
| -------- | ------ | ------------------------------------------------------- |
| 国家级 | ⚠ 占位 | `national-2026-parallel-rule` 是占位 ID未指向真实文件 |
| 27 省 | ❌ 0 | `rules/_evidence/` 目录尚未建 |
| 全国通用 | ❌ 0 | 需 `rules/_evidence/national/` 子目录 |
---
## 4. 当前能力面
-`gaokao-cli rules status --json` 报告 27 省在场
-`gaokao-cli rules verify --json` 校验 `national.yaml` + `province/`
-`gaokao-cli audit run --province <p> --plan <json> --json` 跑结构化审计
-`gaokao-cli rules explain <rule_id>` 未实现
- ❌ "最近验证时间 > 90 天"自动告警未实现
---
## 5. 与旧 `PROVINCE_RULES` 的迁移
- 迁移方式: 一次性脚本 `scripts/migrate_province_rules_to_truth.py`
- 旧字典保留作为过渡期镜像,新代码只读 `_truth`
-`gaokao-checker` 内部已委托给 `audit_engine`
- 迁移完整性由 `tests/test_rules_truth_phase1.py` 锁定
---
## 6. 下一阶段Batch 4 候选)
1. `rules/_evidence/<prov>/<year>-<topic>.md` 补建
2. 逐省 `rules_count` 实际数回填
3. `gaokao-cli rules explain <rule_id>` 命令
4. "最近验证时间 > 90 天"自动告警
5. 内蒙古/陕西/宁夏 3 省 yaml 补齐

View File

@@ -1,24 +1,41 @@
# CURRENT_STATE
最后更新: 2026-06-17
状态词: 本地验证完成Phase2 major_validation 已接入 + 目录/审核闭环已收口
状态词: 本地验证完成(执行 Phase 1 规则真相源化 + Phase 1.5 verify 收口 + Phase 2 专业目录三 Batch 全部收口 + 下一阶段 = 执行 Phase 3 统一 CLI 命令面,未启动
真相源优先级:
1. 本文件
2. `docs/plans/2026-06-17-phase2-majors-catalog-implementation-plan.md`(当前 Phase2 专业目录执行计划
3. `docs/DESIGN_RULES_TRUSTED_CLI_2026-06-16.md`(专业目录/CLI 总设计,当前被 Phase2 计划补充
4. `docs/PROJECT_PLANNING_REALIGNMENT_2026-06-16.md`(规划/实现漂移审计
5. `docs/RULES_SOURCE_OF_TRUTH.md`(规则真相源索引
6. `docs/MAJOR_DATA_SOURCE_OF_TRUTH.md`专业目录真相源索引)
7. `docs/CLI_API_MAPPING.md`CLI/API 映射索引)
2. `docs/ACTIVE_EXECUTION_BOARD_2026-06-17.md`6/17 当前执行板docs-only 收口任务之一
3. `docs/plans/2026-06-17-phase2-majors-catalog-implementation-plan.md`Phase 2 已收口快照
4. `docs/DESIGN_RULES_TRUSTED_CLI_2026-06-16.md`设计快照Phase 编号需与执行口径消歧
5. `docs/PROJECT_PLANNING_REALIGNMENT_2026-06-16.md`(历史规划/实现漂移审计
6. `docs/RULES_SOURCE_OF_TRUTH.md`规则真相源索引;待补建
7. `docs/MAJOR_DATA_SOURCE_OF_TRUTH.md`(专业目录真相源索引)
8. `docs/CLI_API_MAPPING.md`CLI/API 映射索引)
9. `docs/P0_P1_P2_REMEDIATION_PLAN_2026-06-14.md`历史整改板§4 卡片以 §2.1 状态归一为准)
10. `docs/FRONTEND_UI_AUDIT_2026-06-16.md` / `docs/FRONTEND_UI_EXECUTION_BOARD_2026-06-16.md`
11. `docs/ACTIVE_REMEDIATION_2026-06-13.md`
12. `product/PRD.md` / `product/ROADMAP.md` / `docs/IMPLEMENTATION_PLAN_v2.md`
13. `docs/PRODUCTION_DEPLOYMENT_CHECKLIST_2026-06-15.md`
14. `reports/PRODUCT_PLANNING_TECH_ALIGNMENT_REVIEW_2026-06-13.md`(历史评审快照)
8. docs/P0_P1_P2_REMEDIATION_PLAN_2026-06-14.md当前整改板2026-06-15 已做状态归一)
9. docs/FRONTEND_UI_AUDIT_2026-06-16.md
10. docs/FRONTEND_UI_EXECUTION_BOARD_2026-06-16.md
11. docs/ACTIVE_REMEDIATION_2026-06-13.md
12. product/PRD.md / product/ROADMAP.md / docs/IMPLEMENTATION_PLAN_v2.md
13. docs/PRODUCTION_DEPLOYMENT_CHECKLIST_2026-06-15.md
14. reports/PRODUCT_PLANNING_TECH_ALIGNMENT_REVIEW_2026-06-13.md(历史评审快照)
---
## 0. 单一执行口径(2026-06-17
为消除设计 Phase 编号与执行 Phase 编号漂移,本节是当前唯一口径:
| 设计层 PhaseDESIGN_RULES_TRUSTED_CLI_2026-06-16.md | 执行层 Phase当前真相 | 状态 |
| ------------------------------------------------------ | ------------------------------------------ | ---------------------------------------------- |
| Phase 1 规则真相源化 | 执行 Phase 1 | ✅ 已收口(`ae4835e` + `8a61b8f`,三仓同步) |
| Phase 2 统一审计引擎最小可用 | 执行 Phase 1.5verify + 旧 checker 迁移) | ✅ 已收口(同上) |
| Phase 3 专业目录接入 MVP | 执行 Phase 2 | ✅ 已收口(`36ad58a` + `6b1157f` + `edc5b11` |
| Phase 4 统一 CLI 命令面 | 执行 Phase 3 | ❌ 未启动 → 下一阶段 |
| Phase 5 智能体调度 + admin 整合 | 执行 Phase 4 | ❌ 未启动 → 等 Phase 3 收口 |
| Phase 6 文档/真相源索引收口 | 长期维护 | ⚠ 进行中docs-only 收口本轮同步) |
> 设计层 Phase ≠ 执行层 Phase。状态查询一律以本表 + 6/17 执行板为准。
---

View File

@@ -1,9 +1,10 @@
# MAJOR_DATA_SOURCE_OF_TRUTH
最后更新: 2026-06-16
最后更新: 2026-06-17
真相源: 本文件是"专业目录"维度的入口索引。
审计上下文: `docs/PROJECT_PLANNING_REALIGNMENT_2026-06-16.md` §2.3-2.4
设计上下文: `docs/DESIGN_RULES_TRUSTED_CLI_2026-06-16.md` §4
执行上下文: `docs/ACTIVE_EXECUTION_BOARD_2026-06-17.md` §1.2-§1.4
---
@@ -28,20 +29,24 @@
### 2.1 当前已落地范围2026-06-17
- 已落地 `data/majors_catalog/` 包与 national loader/CLI
- 已落地 `national/2024.json` + `national/latest.json`
- 当前 `coverage_mode = mvp_subset`
- 当前 curated subset = 13 个国家级本科专业样本
-可通过:
- `python scripts/gaokao-cli majors status --json`
- `python scripts/gaokao-cli majors lookup 经济学 --json`
- `python scripts/gaokao-cli majors verify --json`
- 已新增:
- `schools/2026/10001.json` 校级招生目录骨架样本
- 尚未落地:
- 更多学校 `schools/<year>/` 批量目录
- `changes/2024-2026.md`
- `major_validation` 接入 `audit_engine`
执行 Phase 2 三个 Batch 全部收口:
- 已落地 `data/majors_catalog/` 包与 national loader/CLIBatch 1
- 已落地 `national/2024.json` + `national/latest.json`13 个 curated subset, `coverage_mode=mvp_subset`Batch 1
-新增 `schools/2026/10001.json` 校级招生目录骨架样本(**当前仅 1 所样本**未扩面Batch 2
- 已落地 `major_validation` 接入 `audit_engine`Batch 3
- 已新增 CLI 子命令:
- `gaokao-cli majors {status,lookup,verify,changes}`Batch 1
- `gaokao-cli majors {school-status,school-verify --year}`Batch 2
- `gaokao-cli audit run --province --plan --truth-root --catalog-root --json` 命中 `MAJORS.not_found` / `MAJORS.non_active`Batch 3
- 提交链:`36ad58a``6b1157f``edc5b11`,三仓同步
### 2.2 尚未落地
- 更多学校 `schools/<year>/` 批量目录(仅 1 所样本)
- `changes/2024-2026.md`
- 国家层第二批扩面(>13 个专业)
- 国家层 2026 版(当前是 2024 版快照)
---
@@ -84,6 +89,8 @@ SchoolMajorOffering:
last_verified_at: datetime
```
> **当前落地情况**:校级骨架 `schools/2026/10001.json` 1 所样本;其余待 Batch 4 扩面。
---
## 4. 接入策略
@@ -93,52 +100,36 @@ SchoolMajorOffering:
- **首选源**: 教育部 2024 年《普通高等学校本科专业目录》
- **兜底源**: 阳光高考/学位中心
- **抓取方式**: 不自动爬取,使用人工收录 + 摘录
- **校验**: `python3 -m data.majors_catalog.cli verify national`
- **校验**: `python3 scripts/gaokao-cli majors verify --json`
### 4.2 校级
- **首选源**: 各高校 2025/2026 招生章程
- **覆盖范围**: 首批 5-10 所重点高校
- **接入节奏**: 每所高校每年一次,5 月前完成
- **校验**: `python3 -m data.majors_catalog.cli verify school <code>`
- **校验**: `python3 scripts/gaokao-cli majors school-verify --year <year> --json`
---
## 5. 审计引擎集成
`audit_engine.run` 接收 plan,新增一步:
`AuditEngine.audit_plan` 新增 `major_validation` 一步
```python
def _validate_majors(plan: VolunteerPlan) -> list[AuditIssue]:
issues = []
for item in plan.items:
for major_name in item.major_names:
major = majors_catalog.lookup(major_name)
if major is None:
issues.append(AuditIssue(
rule_id="MAJORS.not_found",
severity=RuleSeverity.WARNING,
title=f"专业未在国家级目录中找到: {major_name}",
suggestion="请人工核对",
))
elif major.status != "active":
issues.append(AuditIssue(
rule_id="MAJORS.deprecated",
severity=RuleSeverity.CRITICAL,
title=f"专业已撤销/合并/改名: {major.name}",
evidence_quote=f"{major.year_removed} 年已 {major.status}",
))
return issues
```
- 命中 `MAJORS.not_found`(专业未在国家级目录中找到)
- 命中 `MAJORS.non_active`(专业已撤销/合并/改名)
详细见 `tests/test_audit_engine_major_validation_phase2.py`
---
## 6. Phase 3 必须收口
## 6. 下一阶段(执行 Phase 2 收口后的 Batch 4 候选清单)
- `data/majors_catalog/national/2024.json` 落地,500+ 专业覆盖
- 5+ 重点高校 2025/2026 招生目录落地
- `gaokao-cli majors lookup/validate` 可用
- 1+ 真实 e2e:大厂AI方案跑 audit run 命中 major 验证
> 已在 Batch 1/2/3 中**完成**的事项不再列入"必须收口"清单。
- 国家层扩面(>13 个专业)— **Batch 4 候选**
- 校级扩面(>1 所学校)— **Batch 4 候选**
- `changes/2024-2026.md`**Batch 4 候选**
- 国家层 2026 版快照(当前为 2024 版)— **Batch 4 候选**
---
@@ -147,7 +138,4 @@ def _validate_majors(plan: VolunteerPlan) -> list[AuditIssue]:
- 国家级目录与校级目录语义混淆
- 教育部新设专业不进入旧目录(2024 之后新设)
- 跨年招生章程口径不一致
---
**下一阶段**: Phase 3 实施,见 `docs/DESIGN_RULES_TRUSTED_CLI_2026-06-16.md` §11
- 当前 13 个 curated subset 不应被误读为完整教育部目录

View File

@@ -136,7 +136,8 @@
## 4. 详细整改任务板(历史执行快照,已不再作为当前状态真相)
> 说明:下方逐项卡片保留的是 2026-06-14 制定整改时的任务模板,很多条目仍显示 `状态: pending`
> ⚠️ **重要:以下卡片保留的是 2026-06-14 制定整改时的任务模板,绝大多数条目仍显示 `状态: pending`,但实际状态已在 §2.1 中归一为"已完成 / 已转入 T12 / 并入 T12"三类。本节仅作历史记录,不应再被引用为"当前未完成项"。**
> 当前状态请以 `docs/ACTIVE_EXECUTION_BOARD_2026-06-17.md` 与 `docs/CURRENT_STATE.md` §5-§6 为准。
> 当前状态请以上一节“2026-06-15 状态归一”为准;不要再用这里的旧状态判断项目是否全部完成。
### P1-1 支付回调双写裂缝

View File

@@ -1,122 +1,129 @@
# RULES_SOURCE_OF_TRUTH
最后更新: 2026-06-16
最后更新: 2026-06-17
真相源: 本文件是"规则"维度的入口索引。
审计上下文: `docs/PROJECT_PLANNING_REALIGNMENT_2026-06-16.md`
设计上下文: `docs/DESIGN_RULES_TRUSTED_CLI_2026-06-16.md` §2
执行上下文: `docs/ACTIVE_EXECUTION_BOARD_2026-06-17.md` §1.1
---
## 1. 范围
本文件收敛以下类信息:
本文件收敛以下类信息:
- 各省高考 2026 规则
- 全国通用规则
- 错误模式库
- 国家级规则
- 省级规则
- 规则证据链
- 统一审计入口
---
## 2. 当前真相源路径(Phase 1 之后)
## 2. 当前真相源路径
| 类别 | 路径 | 格式 | 写入方式 |
| ------------ | ----------------------------------- | --------- | ---------------------------------- |
| 全国通用规则 | `rules/_truth/national.yaml` | YAML | 人工 PR + 自动校验 |
| 省级规则 | `rules/_truth/province/<prov>.yaml` | YAML | 人工 PR + 自动校验 |
| 规则证据原文 | `rules/_evidence/<prov>/<file>` | 抓取/摘录 | 人工收录 |
| 错误模式库 | `rules/errors/ERRORS.md` | Markdown | 人工维护(短期保留) |
| 跨省索引 | `rules/provinces.md` | Markdown | 由 `data/rules/loader.py` 自动生成 |
| 类别 | 路径 | 格式 | 写入方式 |
| ---------- | ------------------------------------------ | -------- | ------------------------------- |
| 国家级规则 | `rules/_truth/national.yaml` | YAML | 人工摘录 + 官方文件校对 |
| 省级规则 | `rules/_truth/province/<slug>.yaml` | YAML | 人工摘录 + 招生考试院校对 |
| 规则证据 | `rules/_evidence/<prov>/<year>-<topic>.md` | Markdown | 待补建(设计层 Phase 1.5 要求) |
| 旧规则字典 | `scripts/gaokao-checker` `PROVINCE_RULES` | Python | 过渡期镜像,已迁移到 \_truth |
| 错误码字典 | `rules/errors/ERRORS.md` | Markdown | 人工维护 |
### 2.1 当前已落地范围2026-06-17
- 已落地 `rules/_truth/national.yaml`(国家级)
- 已落地 `rules/_truth/province/<slug>.yaml`27 省2026 版)
- 已落地 `data/rules/loader.py` `RuleLoader.from_truth_root`
- 已落地 `data/rules/audit_engine.py` `AuditEngine`
- 已落地 `data/rules/cli.py` `rules {status,verify}`
- 已落地 `scripts/migrate_province_rules_to_truth.py`(从 `PROVINCE_RULES` 一次性迁移)
- 已落地 `tests/test_rules_truth_phase1.py` + `tests/test_rules_cli_phase1.py`(部分)
### 2.2 尚未落地
- `rules/_evidence/<prov>/<year>-<topic>.md`(证据层)— 设计层 Phase 1.5 要求;当前**未建**
- 逐省证据链审计报告 — 本轮 docs-only 收口后,下一阶段再启动
- 规则"最近验证时间 > 90 天"自动告警(设计层 §2.5
---
## 3. 数据模型最小契约
## 3. 数据模型
### 3.1 Rule`data/rules/models.py`
```python
Rule:
rule_id: str # e.g. HUNAN.max_volunteers
rule_id: str # e.g. "HUNAN.max_volunteers"
scope: "national" | "province"
province: str | None
year: int
province: str | None # 当 scope=province 时
year: int # 2026
title: str
description: str
severity: "fatal" | "critical" | "warning" | "info"
value: dict # 机读字段,如 {"max_volunteers": 45}
source_evidence_id: str # 指向 rules/_evidence/<id>.md
value: dict # 规则可机读形式
source_evidence_id: str # 证据链引用
effective_date: date
last_verified_at: datetime
version: str
version: str # "2026.1"
status: "active" | "draft" | "deprecated"
```
---
### 3.2 AuditIssue`data/rules/models.py`
## 4. 当前覆盖矩阵(写入本文件时为准)
| 省份 | 模式 | 文档宣称 | checker 实际 | 备注 |
| ------ | ---------- | --------------- | -------------- | ------------------------------ |
| 湖南 | 院校专业组 | ✅ | ✅ | 详细规则见 province/hunan.yaml |
| 广东 | 院校专业组 | ✅ | ✅ | |
| 湖北 | 院校专业组 | ✅ | ✅ | |
| 安徽 | 院校专业组 | ✅ | ✅ | |
| 江西 | 院校专业组 | ✅ | ✅ | |
| 甘肃 | 院校专业组 | ✅ | ✅ | |
| 黑龙江 | 院校专业组 | ✅ | ✅ | |
| 江苏 | 院校专业组 | ✅ | ✅ | |
| 福建 | 院校专业组 | ✅ | ✅ | |
| 广西 | 院校专业组 | ✅ | ✅ | |
| 北京 | 院校专业组 | ✅ | ✅ | |
| 上海 | 院校专业组 | ✅ | ✅ | |
| 天津 | 院校专业组 | ✅ | ✅ | |
| 海南 | 院校专业组 | ✅ | ✅ | |
| 浙江 | 专业+学校 | ✅ | ✅ | |
| 山东 | 专业+学校 | ✅ | ✅ | |
| 河北 | 专业+学校 | ✅ | ✅ | |
| 重庆 | 专业+学校 | ✅ | ✅ | |
| 辽宁 | 专业+学校 | ✅ | ✅ | |
| 贵州 | 专业+学校 | ✅ | ✅ | |
| 青海 | 专业+学校 | ✅ | ✅ | |
| 吉林 | 专业+学校 | ✅ | ✅ | |
| 新疆 | 传统 | ✅ | ✅ | |
| 西藏 | 传统 | ✅ | ✅ | |
| 河南 | 传统 | ✅ | ✅ | |
| 四川 | 传统 | ✅ | ✅ | |
| 云南 | 传统 | ✅ | ✅ | |
| 山西 | — | ❌(SKILL.md 漏) | ❌(checker 无) | SKILL.md frontmatter 漂移待修 |
| 陕西 | — | ❌(SKILL.md 含) | ❌(checker 无) | SKILL.md / checker 漂移待修 |
---
## 5. 文档漂移清单(已识别)
- D1: `rules/provinces.md``scripts/gaokao-checker` 口径一致为 27 省
- D2: `skills/gaokao-spec-checker/SKILL.md` frontmatter 与实际 checker 省份集仍有漂移
- D3: 传统模式数 provinces.md 写 5,SKILL.md 写 6
- D4: 文档与代码都没有 source_evidence_id 链
---
## 6. Phase 1 必须收口
- 27 省全部有 `province/<prov>.yaml`
- 至少 1 条全国通用规则有 `national.yaml`
- 1-2 个规则有真实 `source_evidence_id` 链路(湖南优先)
- `gaokao-cli rules status` 可用
---
## 7. 验证脚本
```bash
# 规则台账
gaokao-cli rules status
# 单条规则解释
gaokao-cli rules explain --province 湖南 --rule-id HUNAN.max_volunteers
# 一致性自检
python3 -m data.rules.cli verify
```python
AuditIssue:
rule_id: str
severity: RuleSeverity
title: str
message: str
evidence_quote: str | None
suggestion: str | None
```
---
**下一阶段**: Phase 1 实施,见 `docs/DESIGN_RULES_TRUSTED_CLI_2026-06-16.md` §11
## 4. 接入策略
### 4.1 国家级
- **首选源**: 教育部 / 教育考试院公开规则文件
- **覆盖范围**: 平行志愿、一次投档、退档机制、体检要求等
- **接入节奏**: 年度全量 + 差异更新
### 4.2 省级
- **首选源**: 各省招生考试院/教育考试院 2026 招生工作通知
- **覆盖范围**: 27 省(与 `PROVINCE_RULES` 对齐)
- **接入节奏**: 月度差异更新
### 4.3 证据层
- **首选源**: 上述官方文件原文
- **存储格式**: Markdown 摘录 + 官方文件 URL
- **校验**: 每条规则必须能引用至少一个证据条目
---
## 5. 统一审计入口
- `gaokao-cli rules status --json` — 27 省 + national 状态摘要
- `gaokao-cli rules verify --json``national.yaml` + `province/` 完整性
- `gaokao-cli audit run --province <p> --plan <json> --truth-root <path> --catalog-root <path> --json` — 完整审计
---
## 6. 下一阶段(执行 Phase 1 收口后的 Batch 候选清单)
> 已在 Phase 1 / 1.5 中**完成**的事项不再列入"必须收口"清单。
- `rules/_evidence/<prov>/<year>-<topic>.md` 证据层补建 — **Batch 4 候选**
- 逐省证据链审计报告 — **Batch 4 候选**
- `gaokao-cli rules explain <rule_id>` 命令 — **Batch 4 候选**
- "最近验证时间 > 90 天"自动告警 — **Batch 4 候选**
---
## 7. 风险
- 证据层未建导致规则可信度依赖 yaml 自描述
-`PROVINCE_RULES` 字典与新 `_truth` 字典需要保持同步过渡
- 月度刷新流程目前依赖人工 cron未与 `gaokao-cli rules status` 联动

View File

@@ -1,5 +1,9 @@
# Phase2 专业目录真相源 Implementation Plan
> **状态****已收口(历史快照)**。三个 Batch 全部完成,三仓同步至 `edc5b11`。
> 实际执行进度见 `docs/ACTIVE_EXECUTION_BOARD_2026-06-17.md` §1.2-§1.4 与 `docs/CURRENT_STATE.md` §0。
> 本文件保留作为 2026-06-17 制定计划时的原始目标范围,不再作为当前待办清单。
> **For Hermes:** Use subagent-driven-development skill to implement this plan task-by-task when tasks become independent enough; for the first batch, controller can implement directly because the repo currently has zero `data/majors*` code and the architecture needs one coherent landing.
**Goal:** 为 gaokao-volunteer-system 落地“2026 官方专业目录”最小可用真相源,使系统首次具备国家级专业目录的结构化读取、查询、校验与 CLI 能力,并为后续 school offering / audit integration 铺路。

View File

@@ -0,0 +1,87 @@
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
import pytest
PROJECT_ROOT = Path(__file__).resolve().parents[1]
CLI = PROJECT_ROOT / "scripts" / "gaokao-cli"
def _run_cli(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, str(CLI), *args],
check=False,
capture_output=True,
text=True,
cwd=PROJECT_ROOT,
)
def test_cli_help_lists_order_and_doctor() -> None:
result = _run_cli("--help")
assert result.returncode == 0
assert "order" in result.stdout
assert "doctor" in result.stdout
assert "rules" in result.stdout
assert "majors" in result.stdout
assert "audit" in result.stdout
def test_cli_order_delegates_list_subcommand() -> None:
# The orders CLI exposes a `list` subcommand. Asking for its --help
# proves the `gaokao-cli order ...` argv successfully reaches the
# orders parser instead of being swallowed by the top-level parser.
result = _run_cli("order", "list", "--help")
assert result.returncode == 0, result.stderr
# The orders parser's list subcommand should expose --status/--source/--limit;
# presence proves the delegation wired up correctly.
assert "--status" in result.stdout
assert "--limit" in result.stdout
def test_cli_doctor_returns_ok_json(tmp_path: Path) -> None:
result = _run_cli("doctor", "--json")
assert result.returncode == 0
payload = json.loads(result.stdout)
assert payload["ok"] is True
assert payload["rules"]["province_count"] >= 27
assert payload["rules"]["national_rule_count"] >= 0
assert payload["majors"]["ok"] is True
assert payload["majors_verify"]["ok"] is True
def test_cli_doctor_fails_on_empty_truth_root(tmp_path: Path) -> None:
empty_truth = tmp_path / "truth"
empty_truth.mkdir()
empty_catalog = tmp_path / "catalog"
empty_catalog.mkdir()
(empty_catalog / "national").mkdir()
(empty_catalog / "schools").mkdir()
(empty_catalog / "national" / "2024.json").write_text(
json.dumps({"year": 2024, "majors": []}),
encoding="utf-8",
)
(empty_catalog / "national" / "latest.json").write_text(
json.dumps({"year": 2024, "majors": []}),
encoding="utf-8",
)
result = _run_cli(
"doctor",
"--truth-root",
str(empty_truth),
"--catalog-root",
str(empty_catalog),
"--json",
)
assert result.returncode == 1
payload = json.loads(result.stdout)
assert payload["ok"] is False
assert payload["rules"].get("province_count", 0) == 0
# When the truth_root is incomplete the loader raises before any
# province_count key is populated, so a structured error key replaces
# the missing province_count.
assert "error" in payload["rules"] or payload["rules"].get("province_count", 0) == 0