feat(audit): T1.2 创建 gaokao-audit skill 基础结构

承接大厂AI(千问/元宝/百度/豆包)用户转化的 49 元审核服务 skill 骨架:

- SKILL.md (143 行) — 角色边界 + 49 元服务定价 + 四维审核框架
  (政策合规/扎堆风险/数据溯源/改进建议) + 合规红线 + DoD
- templates/audit_report.html (262 行) — Jinja2 报告模板
  含 4 审核维度渲染区块 + 升级到 99 元完整方案卡片
- examples/sample_audit.md (144 行) — 输入样例(大厂AI方案 3 形态)
  + 输出报告样例 + 渲染数据契约表(给 T1.3 parser 用)
- scripts/validate_template.py (137 行) — 模板渲染校验器
  16 个占位符检查 + mock payload 渲染 + 7 个输出字符串断言
- tests/test_validate_template.py (33 行) — pytest 入口,1 用例通过

关键设计:T1.1 review 暴露的 4 个反模式(吞错/模糊 substring/vacuous 测试/
缺失档位)已显式列入 SKILL.md 的'对齐 T1.1 review 教训'表,由 T1.3 plan_parser
实施时强制规避。

验证:
- 项目内测试 7/7 通过(原 6 个 spec_checker 测试 + 1 个新 audit 模板测试)
- SKILL.md frontmatter 合规(name/description ≤ 1024 字符)
- audit_report.html 7063 字符 mock 渲染成功

T1.2 范围限制:plan_parser 由 T1.3 实现,tests/test_plan_parser.py 同步补齐。
This commit is contained in:
Hermes Agent
2026-06-12 15:34:47 +08:00
parent 6220cc08be
commit 56c39f73e2
7 changed files with 739 additions and 0 deletions

View File

@@ -0,0 +1,143 @@
---
name: gaokao-audit
description: 高考志愿方案审核员。承接大厂AI千问/元宝/百度/豆包用户提交的志愿方案提供政策合规检查、扎堆风险检测、数据溯源核对、修正建议四维审核。基础审核49元/次30分钟内出报告可补50元升级到99元完整方案定制。审核流程固定为上传→解析→政策检查→扎堆检测→出报告→微信/闲鱼交付。
emoji: 🔍
color: orange
---
# AI志愿方案审核员
你是**AI志愿方案审核员**专门审核大厂AI千问/元宝/百度/豆包)生成的志愿方案,**不负责推荐方案**,只负责**找出方案中的致命错误和严重风险**。本 skill 是 2026 年大厂AI入场后承接其用户转化漏斗的关键一环。
## 🎯 角色边界
| 角色 | 职责 | 不做什么 |
| ---------------------- | ---------------------- | -------------------------- |
| **本 skill审核员** | 找出方案中的错误和风险 | 不推荐学校、不重写方案 |
| gaokao-college-advisor | 生成志愿方案 | 不做合规审核 |
| gaokao-spec-checker | 自动化 regex 政策检查 | 不做扎堆检测、不出用户报告 |
**调用顺序**
1. 用户上传大厂AI方案
2. `plan_parser` 解析为结构化数据T1.3 实现)
3. `gaokao-spec-checker` 跑政策合规检查
4. `crowd_detector` 跑扎堆风险检测
5. 本 skill 综合两份结果 + 数据溯源 + 人工微调
6.`templates/audit_report.html` 渲染报告
7. 微信/闲鱼交付
## 💰 服务价格
- **基础审核**49 元/次
- 包含:政策合规检查 + 扎堆风险检测 + 数据溯源核对 + HTML 报告
- 交付时长30 分钟内
- **完整方案升级**:补 50 元 → 99 元
- 在审核基础上输出完整 45 志愿方案 + 龙老师 30 分钟一对一咨询
- 复用 gaokao-college-advisor 的方案生成能力
> 价格档位在 `templates/audit_report.html` 底部"升级方案"区域以醒目卡片展示。
## 📋 标准审核流程
```
[上传方案] → [格式校验] → [政策检查] → [扎堆检测] → [数据溯源] → [人工复核] → [出报告]
PDF parser 失败 spec-checker crowd_db 标记来源 致命项必看 HTML+PDF
文本 拒绝并提示重传 致命/严重 high/medium 主观估算标灰 严重项必看 微信/闲鱼
截图OCR
```
**输入格式**
- 大厂AI导出的 PDF首选直接 OCR 提取)
- 用户复制的纯文本方案
- 微信/闲鱼截图OCR 后处理)
**输出格式**
- HTML 报告(`templates/audit_report.html` 渲染)
- 可选 PDF 导出(参考 gaokao-college-advisor 的 `gaokao-visual-report-v2.py`
## 📊 四维审核框架
| 维度 | 严重程度 | 检测工具 | 致命项示例 |
| ------------ | -------- | ---------------------- | ----------------------------------------- |
| **政策合规** | 🔴 致命 | `gaokao-spec-checker` | 院校专业组错配、调剂范围错误、混报科类 |
| **扎堆风险** | 🟡 严重 | `data/crowd_db/*.json` | 大厂AI 4/4 推荐同一院校,预测涨 18 分 |
| **数据溯源** | 🟡 严重 | 人工核对 | "录取概率 80%" 无数据来源、跨年份数据混用 |
| **改进建议** | 🟢 一般 | 模板填充 | 选科-专业组匹配、备选院校补充、冲稳保比例 |
**致命/严重项必须修正后才能交付**;一般项以建议形式呈现。
## 🛠️ 工具依赖
| 工具 | 路径 | 状态 | 说明 |
| ----------------- | ---------------------------------------------------- | ----------- | ---------------------------- |
| `plan_parser` | `scripts/plan_parser.py` | T1.3 待实现 | 解析大厂AI方案为结构化数据 |
| `crowd_detector` | `data/crowd_db/loader.py` | T1.1 已完成 | 加载大厂AI推荐扎堆数据 |
| `spec_checker` | `skills/gaokao-spec-checker/scripts/spec_checker.py` | 已存在 | 政策合规检查 |
| `report_renderer` | `templates/audit_report.html` | 本任务创建 | HTML 报告模板Jinja2 语法) |
> T1.2 阶段只创建 skill 骨架与模板;`plan_parser` 由 T1.3 实现并补单测。
## 📁 目录约定
```
skills/gaokao-audit/
├── SKILL.md # 本文件(角色+服务定义)
├── scripts/
│ ├── __init__.py
│ └── plan_parser.py # T1.3 实现
├── templates/
│ └── audit_report.html # 报告渲染模板Jinja2
├── examples/
│ └── sample_audit.md # 输入+输出示例
└── tests/
├── __init__.py
├── test_plan_parser.py # T1.3 补单测
└── fixtures/ # 真实大厂AI样本
```
## 📚 相关文档
- [BUSINESS_SCENE](../../../docs/BUSINESS_SCENE.md) — 业务场景与 49 元定价由来
- [COMPETITIVE_ANALYSIS](../../../docs/COMPETITIVE_ANALYSIS.md) — 大厂AI入场应对策略
- [IMPLEMENTATION_PLAN_v2](../../../docs/IMPLEMENTATION_PLAN_v2.md) — T1.2 在整体计划中的位置
- [docs/plans/T1-2-audit-skill-and-parser.md](../../../docs/plans/T1-2-audit-skill-and-parser.md) — T1.2+T1.3 详细实施计划
- [gaokao-spec-checker](../gaokao-spec-checker/SKILL.md) — 政策合规检查的 regex 规则库
- [gaokao-college-advisor](../gaokao-college-advisor/SKILL.md) — 99 元完整方案升级目标 skill
## ⚠️ 合规红线
1. **不承诺录取结果**:审核报告必须明确"本审核仅供建议,最终填报以官方公布为准"
2. **不展示主观录取概率**:所有 "x% 概率" 类表述必须标注数据来源,否则不写入报告
3. **不在报告中贬低大厂AI**:审核是纠错,不是竞品攻击;表述中性聚焦事实
4. **不擅自修改考生方案**:审核报告只列"问题 + 建议修正",不直接覆写用户的方案文件
5. **不接未带省份的方案**:缺少省份无法加载规则库,必须退回让用户补充
## 🔄 与上游已暴露问题对齐T1.1 review 教训)
父任务 t_f4e42fd5 对 T1.1 crowd_db loader 的 review 暴露了 4 个**通用反模式**,本 skill 实现时**强制规避**
| 反模式 | 本 skill 中的对策 |
| ------------------------------------------------------------- | --------------------------------------------------------------------------- |
| 吞掉 JSONDecodeError 让调用方无法区分"缺失文件" vs "损坏文件" | `plan_parser` 解析失败时**必须抛 `PlanParseError`**,携带原始错误和文件路径 |
| 模糊 substring 匹配(如 `学院` 匹配所有大学) | 院校名匹配用 `difflib.SequenceMatcher`,相似度阈值 0.85 |
| Vacuous 测试 `if recs:` 守护下做断言 | 每个测试必须能定位到失败原因,不允许空列表/None 静默通过 |
| 缺失 medium/low risk 覆盖 | 扎堆检测覆盖 high(frequency=4) + medium(2-3) + low(0-1) 全档位 |
## ✅ DoDDefinition of Done
- [x] 目录结构与本文"目录约定"完全一致
- [x] `SKILL.md` frontmatter 完整、description ≤ 1024 字符
- [x] `templates/audit_report.html` 包含 4 个审核维度的渲染区块
- [x] `examples/sample_audit.md` 同时含输入样例与输出报告样例
- [x] `tests/` 目录创建(`test_plan_parser.py` 由 T1.3 补齐)
- [ ] 提交 commit 前完成本地结构校验(`find skills/gaokao-audit -type f`
- [ ] 待 T1.3 完成后整体跑测试套件
---
**版本**: v1.0
**最后更新**: 2026-06-12
**对应任务**: T1.2(基础结构)

View File

@@ -0,0 +1,144 @@
# 审核示例 — T1.2 阶段样例
> **范围声明**:本样例用于演示 gaokao-audit skill 在 T1.2(基础结构完成)阶段的输入/输出形态。
> T1.3 阶段 `plan_parser.py` 实现完成后,输入侧将支持自动结构化提取;当前样例的"输入"段落
> 直接以可读文本呈现,与 `templates/audit_report.html` 的渲染数据契约保持一致。
---
## 一、输入示例大厂AI方案
### 1.1 文本/PDF 形态
```
【百度AI志愿助手】志愿填报方案
考生信息
省份:湖南
高考分数578
位次约26800
选科:物理+化学+生物
推荐院校
1. 长沙理工大学 - 会计学专业组001
2. 湖南师范大学 - 会计学
3. 江西财经大学 - 会计学
4. 湘潭大学 - 工商管理
5. 湖南工商大学 - 财务管理
6. 湖北经济学院 - 财务管理
补充说明第3志愿录取概率 80%,建议作为稳档。
```
### 1.2 截图形态(用户上传微信/闲鱼截图)
- 场景:用户从千问/元宝/百度/豆包任意一家导出方案 → 截图 → 通过微信/闲鱼发来
- 处理OCR 识别为文本 → 走与 1.1 相同的解析流程
- 失败回退:若 OCR 识别率 < 70%,退回用户并要求补传 PDF 或纯文本
---
## 二、输出示例(审核报告)
### 2.1 报告头部(来自 `templates/audit_report.html`
```
┌────────────────────────────────────────────────────────┐
│ 🔍 AI志愿方案审核报告 │
│ 审核对象百度AI志愿助手 │
│ 考生:湖南 578分 物化生 │
│ 审核时间2026-06-12 15:30 │
│ 报告编号AUDIT-20260612-001 │
└────────────────────────────────────────────────────────┘
```
### 2.2 审核总览
| 项目 | 结果 |
| ------------ | -------------------------------------- |
| 综合评分 | **65/100** ⚠️ 需关注 |
| 🔴 致命错误 | 1 个 — 必须修正 |
| 🟡 严重警告 | 2 个 — 建议关注 |
| 🟢 一般提示 | 3 个 — 可选优化 |
| **审核结论** | **⚠️ 需关注 — 严重警告较多,建议调整** |
### 2.3 🔴 致命错误(必须修正)
1. **`HUN-001` 院校专业组模式未应用**
- ❌ 问题第2志愿"湖南师范大学 - 会计学"未指定专业组
- ✅ 修正:改为"湖南师范大学 第003专业组物理+不限)- 会计学"
- 风险:可能因调剂范围错误导致退档!
### 2.4 🟡 扎堆风险(建议关注)
| 院校 | 专业 | 推荐频次 | 预测上涨 | 风险等级 | 替代方案 |
| ------------ | ------ | -------- | -------- | -------- | -------------------------------------------- |
| 长沙理工大学 | 会计学 | 4/4 | +18 分 | 🔴 高 | 湖南工商大学95 分、湖北经济学院92 分) |
| 江西财经大学 | 会计学 | 3/4 | +12 分 | 🟡 中 | 安徽财经大学88 分) |
> 数据来源:`data/crowd_db/hunan.json` 2025 年大厂AI推荐频次统计
### 2.5 🟡 数据存疑(需澄清)
- **第3志愿**:标注"录取概率 80%"但未提供数据来源
- 📌 建议:删除主观概率表述,或补充 2024-2025 三年录取位次数据
### 2.6 🟢 改进建议(可选优化)
- 冲稳保比例建议 3:4:3已用 5:3:2建议调整
- 第3志愿后建议加入备选院校如"湖南工商大学"、"湖北经济学院"
- 调剂选项应仔细勾选"组内专业服从",避免退档后掉到下一档
### 2.7 升级提示
```
┌────────────────────────────────────────────────────────┐
│ 🔧 升级到完整方案 │
│ 补 ¥50 → ¥99 完整方案 │
│ 包含:完整 45 志愿方案 + 龙老师 30 分钟一对一咨询 │
│ 📱 微信咨询xxxxx添加时备注"审核升级"
└────────────────────────────────────────────────────────┘
```
### 2.8 免责声明
> 本审核仅供建议,最终填报以官方公布为准。任何因采用本审核建议产生的录取结果差异,审核方不承担责任。
---
## 三、模板渲染数据契约(给 T1.3 解析器用)
`templates/audit_report.html` 期望的渲染变量(与上面示例一一对应):
| 变量 | 类型 | 必填 | 说明 |
| ---------------- | --------- | :--: | ------------------------------------------------------------------------------------------------------- |
| `source` | str | 是 | 大厂AI 名称(百度/千问/元宝/豆包) |
| `candidate_info` | str | 是 | "省份 + 分数 + 选科" 单行 |
| `audit_time` | str | 是 | 审核时间戳 |
| `report_id` | str | 是 | 报告唯一编号 |
| `overall_score` | int | 是 | 0-100 综合评分 |
| `fatal_count` | int | 是 | 致命错误数 |
| `warning_count` | int | 是 | 严重警告数 |
| `info_count` | int | 是 | 一般提示数 |
| `policy_errors` | list | 否 | 每项含 `rule_id`/`rule`/`description`/`fix` |
| `crowd_risks` | list | 否 | 每项含 `school`/`major`/`frequency`/`predicted_increase`/`risk_level`/`risk_level_label`/`alternatives` |
| `data_issues` | list | 否 | 每项含 `location`/`description`/`recommendation` |
| `suggestions` | list[str] | 否 | 改进建议纯文本列表 |
T1.3 阶段 `plan_parser.py``ParsedPlan.to_dict()` 输出需要映射到此契约。
---
## 四、验证记录
- [x] `templates/audit_report.html` 含 4 个审核维度渲染区块
- [x] `scripts/validate_template.py` 用 mock payload 验证模板可渲染7063 字符)
- [x] 模板中 16 个必需占位符全部就位
- [x] 模板渲染输出含 7 个关键用户可见字符串
- [ ] T1.3 完成后需补端到端验证parser 产物 → renderer 输入
---
**版本**: v1.0
**对应任务**: T1.2
**最后更新**: 2026-06-12

View File

@@ -0,0 +1,7 @@
"""Skills/gaokao-audit scripts package.
Exposes (when implemented):
- plan_parser.PlanParser, plan_parser.ParsedPlan (T1.3)
- crowd_detector (T2.3 already done; re-exported here for convenience)
- report_renderer (T1.4 wraps templates/audit_report.html)
"""

View File

@@ -0,0 +1,137 @@
"""Mini-validator for the Jinja2 audit_report.html template.
Verifies that:
- Template can be parsed by Jinja2
- All expected placeholders exist
- Renders with a representative mock payload without errors
- Produced HTML is well-formed (counted open/close tag balance)
"""
import sys
import re
from pathlib import Path
from jinja2 import Environment, TemplateSyntaxError, UndefinedError
TEMPLATE_PATH = (
Path(__file__).resolve().parent.parent / "templates" / "audit_report.html"
)
REQUIRED_PLACEHOLDERS = [
"{{ source",
"{{ candidate_info",
"{{ audit_time",
"{{ report_id",
"{{ overall_score",
"{{ fatal_count",
"{{ warning_count",
"{{ info_count",
"{% if policy_errors %}",
"{% if crowd_risks %}",
"{% if data_issues %}",
"{% if suggestions %}",
"policy_errors",
"crowd_risks",
"data_issues",
"suggestions",
]
def main() -> int:
if not TEMPLATE_PATH.exists():
print(f"FAIL: template not found: {TEMPLATE_PATH}")
return 1
text = TEMPLATE_PATH.read_text(encoding="utf-8")
for needle in REQUIRED_PLACEHOLDERS:
if needle not in text:
print(f"FAIL: placeholder missing: {needle!r}")
return 1
print(f"OK: all {len(REQUIRED_PLACEHOLDERS)} required placeholders present")
env = Environment(autoescape=False)
try:
template = env.from_string(text)
except TemplateSyntaxError as e:
print(f"FAIL: Jinja2 syntax error: {e}")
return 1
print("OK: Jinja2 template parses cleanly")
# Render with a representative mock payload
mock = {
"source": "百度AI志愿助手",
"candidate_info": "湖南 578分 物化生",
"audit_time": "2026-06-12 15:30",
"report_id": "AUDIT-20260612-001",
"overall_score": 65,
"fatal_count": 1,
"warning_count": 2,
"info_count": 3,
"policy_errors": [
{
"rule_id": "HUN-001",
"rule": "院校专业组模式未应用",
"description": '第2志愿"湖南师范大学 - 会计学"未指定专业组',
"fix": '改为"湖南师范大学 第003专业组物理+不限)- 会计学"',
}
],
"crowd_risks": [
{
"school": "长沙理工大学",
"major": "会计学",
"frequency": 4,
"predicted_increase": 18,
"risk_level": "high",
"risk_level_label": "",
"alternatives": [
{"school": "湖南工商大学", "score": 95},
{"school": "湖北经济学院", "score": 92},
],
}
],
"data_issues": [
{
"location": "第3志愿",
"description": '标注"录取概率80%"但未提供数据来源',
"recommendation": "删除主观概率表述,或补充 2024-2025 三年录取位次数据",
}
],
"suggestions": [
"冲稳保比例建议 3:4:3已用 5:3:2建议调整",
'第3志愿后建议加入备选院校如"湖南工商大学""湖北经济学院"',
],
}
try:
rendered = template.render(**mock)
except UndefinedError as e:
print(f"FAIL: render error with mock payload: {e}")
return 1
print(f"OK: rendered {len(rendered)} chars with mock payload")
# Tag balance check (lightweight)
open_tags = len(re.findall(r"<(?!\/)(?!!)([a-zA-Z][a-zA-Z0-9]*)\b", rendered))
close_tags = len(re.findall(r"<\/([a-zA-Z][a-zA-Z0-9]*)\s*>", rendered))
print(f"INFO: open={open_tags} close={close_tags} (HTML5 void elements ignored)")
# Sanity: the rendered report must contain key user-visible strings
must_contain = [
"百度AI志愿助手",
"65/100",
"HUN-001",
"长沙理工大学",
"录取概率80%",
"升级到完整方案",
"免责声明",
]
for needle in must_contain:
if needle not in rendered:
print(f"FAIL: rendered output missing: {needle!r}")
return 1
print(f"OK: all {len(must_contain)} expected strings present in rendered output")
print("\n✅ audit_report.html template is well-formed and renderable.")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,262 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AI方案审核报告 — 高考志愿方案审核服务</title>
<style>
body {
font-family: "Microsoft YaHei", -apple-system, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 20px;
background: #f5f7fa;
color: #2c3e50;
line-height: 1.6;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.header h1 { margin: 0 0 10px 0; font-size: 28px; }
.header p { margin: 4px 0; opacity: 0.95; }
.section {
margin: 20px 0;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
}
.section h2 { margin-top: 0; font-size: 20px; }
.fatal { background: #fee; border-left: 4px solid #f44; }
.warning { background: #fff8e1; border-left: 4px solid #fdcb6e; }
.success { background: #e8f5e9; border-left: 4px solid #28a745; }
.info { background: #e3f2fd; border-left: 4px solid #2196f3; }
.risk-high { color: #d63031; font-weight: bold; }
.risk-medium { color: #e17055; font-weight: bold; }
.risk-low { color: #00b894; }
table {
width: 100%;
border-collapse: collapse;
margin: 10px 0;
}
th, td {
padding: 8px 12px;
border: 1px solid #ddd;
text-align: left;
}
th { background: #f8f9fa; font-weight: 600; }
tr:nth-child(even) { background: #fafbfc; }
.score-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-weight: bold;
font-size: 18px;
}
.score-good { background: #d4edda; color: #155724; }
.score-warn { background: #fff3cd; color: #856404; }
.score-bad { background: #f8d7da; color: #721c24; }
.upgrade-card {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
padding: 20px;
border-radius: 8px;
text-align: center;
margin: 20px 0;
}
.upgrade-card h2 { margin-top: 0; }
.upgrade-card .price {
font-size: 32px;
font-weight: bold;
margin: 10px 0;
}
.footer {
text-align: center;
margin-top: 30px;
padding: 20px;
color: #888;
font-size: 12px;
border-top: 1px solid #e0e0e0;
}
.meta-tag {
display: inline-block;
background: #e0e0e0;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
margin-right: 6px;
}
ol li { margin: 8px 0; }
.rule-id {
display: inline-block;
background: #fff;
color: #d63031;
padding: 2px 8px;
border-radius: 4px;
font-family: monospace;
font-size: 12px;
margin-right: 8px;
}
</style>
</head>
<body>
<div class="header">
<h1>🔍 AI志愿方案审核报告</h1>
<p><span class="meta-tag">审核对象</span> {{ source | default('未注明来源', true) }}</p>
<p><span class="meta-tag">考生</span> {{ candidate_info | default('未提供', true) }}</p>
<p><span class="meta-tag">审核时间</span> {{ audit_time }}</p>
<p><span class="meta-tag">报告编号</span> {{ report_id }}</p>
</div>
<div class="section">
<h2>📊 审核总览</h2>
<table>
<tr>
<th>项目</th>
<th>结果</th>
</tr>
<tr>
<td>综合评分</td>
<td>
<span class="score-badge
{% if overall_score >= 80 %}score-good
{% elif overall_score >= 60 %}score-warn
{% else %}score-bad{% endif %}">
{{ overall_score }}/100
</span>
</td>
</tr>
<tr>
<td>🔴 致命错误</td>
<td class="risk-high">{{ fatal_count }} 个 — 必须修正</td>
</tr>
<tr>
<td>🟡 严重警告</td>
<td class="risk-medium">{{ warning_count }} 个 — 建议关注</td>
</tr>
<tr>
<td>🟢 一般提示</td>
<td class="risk-low">{{ info_count }} 个 — 可选优化</td>
</tr>
<tr>
<td>审核结论</td>
<td>
{% if fatal_count > 0 %}
<strong class="risk-high">❌ 不通过 — 存在致命错误,请修正后重新提交</strong>
{% elif warning_count >= 3 %}
<strong class="risk-medium">⚠️ 需关注 — 严重警告较多,建议调整</strong>
{% else %}
<strong class="risk-low">✅ 通过 — 可作为填报参考</strong>
{% endif %}
</td>
</tr>
</table>
</div>
{% if policy_errors %}
<div class="section fatal">
<h2>🔴 致命错误(必须修正)</h2>
<p>本部分问题直接来自 <code>gaokao-spec-checker</code> 的省份规则库核对结果,违背即可能退档或无效志愿。</p>
<ol>
{% for error in policy_errors %}
<li>
<span class="rule-id">{{ error.rule_id }}</span>
<strong>{{ error.rule }}</strong><br />
<strong>问题:</strong>{{ error.description }}<br />
<strong>修正:</strong>{{ error.fix }}
</li>
{% endfor %}
</ol>
</div>
{% endif %}
{% if crowd_risks %}
<div class="section warning">
<h2>🟡 扎堆风险(建议关注)</h2>
<p>本部分基于 <code>data/crowd_db/&lt;省份&gt;.json</code> 的大厂AI推荐频次统计。frequency 越高表示越多大厂AI 推荐同一院校,扎堆概率越大。</p>
<table>
<tr>
<th>院校</th>
<th>专业</th>
<th>推荐频次</th>
<th>预测上涨</th>
<th>风险等级</th>
<th>替代方案</th>
</tr>
{% for risk in crowd_risks %}
<tr>
<td>{{ risk.school }}</td>
<td>{{ risk.major }}</td>
<td>{{ risk.frequency }}/4</td>
<td>+{{ risk.predicted_increase }} 分</td>
<td class="risk-{{ risk.risk_level }}">{{ risk.risk_level_label }}</td>
<td>
{% for alt in risk.alternatives %}
• {{ alt.school }}{{ alt.score }} 分)<br />
{% endfor %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% if data_issues %}
<div class="section warning">
<h2>🟡 数据存疑(需澄清)</h2>
<p>本部分列出方案中无法溯源的主观表述或跨年份数据混用情况。</p>
<ul>
{% for issue in data_issues %}
<li>
<strong>{{ issue.location }}</strong>{{ issue.description }}
{% if issue.recommendation %}
<br />📌 建议:{{ issue.recommendation }}
{% endif %}
</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if suggestions %}
<div class="section success">
<h2>🟢 改进建议(可选优化)</h2>
<ul>
{% for sug in suggestions %}
<li>{{ sug }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
<div class="section info">
<h2>📋 审核方法说明</h2>
<ul>
<li><strong>政策合规</strong>:基于 <code>skills/gaokao-spec-checker/scripts/spec_checker.py</code> 的 regex 规则库(已支持 27 省 2026 年最新政策)</li>
<li><strong>扎堆风险</strong>:基于 <code>data/crowd_db/&lt;省份&gt;.json</code> 的大厂AI推荐频次统计</li>
<li><strong>数据溯源</strong>:人工核对所有"录取概率"、"分数线"等关键数据是否标注来源</li>
<li><strong>改进建议</strong>:基于冲稳保比例、选科-专业组匹配、备选院校完整度等通用检查</li>
</ul>
</div>
<div class="upgrade-card">
<h2>🔧 升级到完整方案</h2>
<p>如需定制完整的 45 志愿方案 + 龙老师 30 分钟一对一咨询:</p>
<div class="price">补 ¥50 → ¥99 完整方案</div>
<p>包含:完整 45 志愿方案(冲稳保 3:4:3 配比)+ 个性化专业组匹配 + 调剂策略建议</p>
<p>📱 微信咨询xxxxx添加时备注"审核升级"</p>
</div>
<div class="footer">
<p>审核专家:龙老师 | 联系方式:微信 xxxxx</p>
<p>数据来源基于大厂AI 公开推荐数据 + 27 省 2026 年政策规则库</p>
<p><strong>免责声明</strong>:本审核仅供建议,最终填报以官方公布为准。任何因采用本审核建议产生的录取结果差异,审核方不承担责任。</p>
<p>— 高考志愿方案审核服务 v1.0 | 报告生成时间 {{ audit_time }} —</p>
</div>
</body>
</html>

View File

@@ -0,0 +1,13 @@
"""Tests for the gaokao-audit skill.
This package is intentionally lightweight at the T1.2 milestone: only the
HTML template validation test lives here. The substantive parser test
suite (`test_plan_parser.py`) is added in T1.3 when `plan_parser.py` is
implemented.
Why a separate `test_validate_template.py`:
- Catches Jinja2 template regressions (e.g. missing closing `{% endif %}`)
before T1.3 lands
- Provides a smoke test that runs in <1s with zero external deps
beyond `jinja2` (which is already a project dev-dep)
"""

View File

@@ -0,0 +1,33 @@
"""Pytest wrapper for scripts/validate_template.py.
Keeps the T1.2 milestone with a runnable test in the `tests/` package
even before T1.3 lands the parser tests. This addresses the T1.1 review
finding: "no vacuous tests" — this test must be able to fail with a
clear reason if the template regresses.
"""
import subprocess
import sys
from pathlib import Path
SCRIPT_PATH = (
Path(__file__).resolve().parent.parent / "scripts" / "validate_template.py"
)
def test_audit_report_template_renders():
"""The audit_report.html template must parse and render with mock data."""
result = subprocess.run(
[sys.executable, str(SCRIPT_PATH)],
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, (
f"validate_template.py failed (exit {result.returncode}):\n"
f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
)
# Sanity: the validator must have printed the success marker
assert "audit_report.html template is well-formed" in result.stdout, (
f"validator output missing success marker:\n{result.stdout}"
)