Files
gaokao-volunteer-system/data/crowd_db/tests/test_high_trust_thresholds.py
Hermes Agent 4c732eb836
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
fix(crowd_db): 高信任数据系统性修复 — 契约硬化+真相单一化+防漂移
review 发现数据本身真实达标(7 high / 20 usable / 0 skeleton),
但代码/文档/测试/元数据 4 层出现严重脱节,存在静默升级风险与
合规假象回归漏洞。

Phase 1: 契约硬化(P0)
- risk_report.py 新增 _classify_score_bands + _compute_quality_level
- 质量等级判定从"仅看 confidence"升级为综合门槛
  (conf + sr + recs + alts + 三层分数带),对齐 plan §4
- 新增 low 等级区分"已脱离骨架但未达可用"
- _load_provenance_metadata 改为优先 load_province 取完整数据
- finding_to_risk_dict 不再二次规范化已规范化数据
- quality_summary.py 增加 low 等级统计
- SCHEMA.md §6 同步完整门槛定义

Phase 2: 测试加固(P0)
- 新增 test_high_trust_thresholds.py 锁死 high/usable 完整门槛
  (plan §9.2 要求的"防静默升级"测试)
- 修复 test_crowd_db_data_quality.py 等级枚举支持 low
- 修复 test_risk_report.py 硬编码日期脆弱性

Phase 3: 真相源单一化(P0)
- CURRENT_STATE.md §0.5 清除 6/20 旧文案"4 high + 3 usable + 20 skeleton"
- 改为引用顶部状态词 + 历史轨迹仅供审计
- NATIONALIZATION §4 清除"当前 high 已扩展为 5 省"矛盾文案
- 顶部状态词从"Phase 0 收口中"升级为"已完成"

Phase 4: 元数据状态对齐(P1)
- hunan.json / sichuan.json trusted_sources.kind
  province_official_pending_review -> province_official
- 同步更新 quality_note 说明已完成 2025 年度复核
- 消除"状态 high/usable 但 kind=pending_review"的矛盾

Phase 5: 防漂移机制(P1)
- 新增 scripts/check_crowd_db_consistency.py
  跨文档+数据+测试白名单一致性检查(5 项检查)
- dev-verify.sh 接入 crowd_db quality summary 打印

验证:
- ruff: All checks passed
- mypy: Success, no issues in 16 source files
- pytest crowd_db/: 148 passed, 2 skipped
- pytest 全量: 1283 passed, 2 skipped (无回归)
- consistency check: high=7 usable=20 low=0 skeleton=0

Refs: docs/plans/2026-06-23-national-high-trust-crowd-db-plan.md §4/§9
2026-06-25 07:41:11 +08:00

196 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""高信任门槛约束测试(防静默升级)。
目的:锁死 high/usable 判定的完整门槛,防止只改 confidence 值就升级 quality_level。
门槛来源docs/plans/2026-06-23-national-high-trust-crowd-db-plan.md §4
关键约束:
- high 必须同时满足 conf>=0.8 + sr>=8 + recs>=40 + alts>=60 + 3层分数带
- usable 必须同时满足 conf>=0.65 + sr>=6 + recs>=24 + alts>=24
"""
from __future__ import annotations
import pytest
from data.crowd_db.loader import CrowdDBLoader
from data.crowd_db.quality_summary import build_quality_summary
def _classify_score_bands(score_ranges):
"""复用 risk_report.py 的分数带分类逻辑(防重复)。"""
bands = set()
for sr in score_ranges:
rng = sr.get("range", [0, 0])
if not rng or len(rng) < 2:
continue
mid = (rng[0] + rng[1]) / 2
if mid >= 580:
bands.add("high")
elif mid >= 480:
bands.add("mid")
else:
bands.add("low")
return bands
# 加载满数据版本(含 score_ranges
loader = CrowdDBLoader(warn_low_confidence=False)
@pytest.fixture(scope="module")
def quality_data():
"""返回各省份的质量元数据,含完整内容。"""
provinces = []
for province in loader.list_supported_provinces():
full_data = loader.load_province(province)
if not full_data:
continue
conf = full_data.get("confidence", 0)
sr = full_data.get("score_ranges", [])
# 统计 recs / alts
recs = sum(len(r.get("recommendations", [])) for r in sr)
alts = sum(
len(rec.get("alternatives", []))
for r in sr
for rec in r.get("recommendations", [])
)
bands = _classify_score_bands(sr)
provinces.append({
"province": province,
"confidence": conf,
"score_ranges": sr,
"recs": recs,
"alts": alts,
"bands": bands,
"sr_count": len(sr),
})
return provinces
def test_high_province_must_meet_all_thresholds(quality_data):
"""high 省必须同时满足 conf + sr + recs + alts + 分数带(防静默升级)。
用例:所有 quality_level=high 的省份,必须完全达标。
"""
high_provinces = [p for p in quality_data if p["confidence"] >= 0.80]
assert high_provinces, "应有 high 省"
for p in high_provinces:
assert p["confidence"] >= 0.8, (
f"{p['province']}: high 门槛要求 conf >= 0.80 (实际 {p['confidence']})"
)
assert p["sr_count"] >= 8, (
f"{p['province']}: high 门槛要求 >= 8 个分数段 (实际 {p['sr_count']})"
)
assert p["recs"] >= 40, (
f"{p['province']}: high 门槛要求 >= 40 条 recommendations "
f"(实际 {p['recs']})"
)
assert p["alts"] >= 60, (
f"{p['province']}: high 门槛要求 >= 60 条 alternatives (实际 {p['alts']})"
)
assert len(p["bands"]) >= 3, (
f"{p['province']}: high 门槛要求覆盖至少 3 层分数带 "
f"(实际 {sorted(p['bands'])})"
)
def test_usable_province_must_meet_all_thresholds(quality_data):
"""usable 省必须同时满足 conf + sr + recs + alts。
用例:所有 quality_level=usable 的省份,必须完全达标。
"""
usable_provinces = [p for p in quality_data if 0.65 <= p["confidence"] < 0.8]
# 当前实地运行无 usable均为 high但保留门槛测试
if not usable_provinces:
pytest.skip("当前无 usable 省份")
for p in usable_provinces:
assert p["confidence"] >= 0.65, (
f"{p['province']}: usable 门槛要求 conf >= 0.65 (实际 {p['confidence']})"
)
assert p["sr_count"] >= 6, (
f"{p['province']}: usable 门槛要求 >= 6 个分数段 (实际 {p['sr_count']})"
)
assert p["recs"] >= 24, (
f"{p['province']}: usable 门槛要求 >= 24 条 recommendations "
f"(实际 {p['recs']})"
)
assert p["alts"] >= 24, (
f"{p['province']}: usable 门槛要求 >= 24 条 alternatives (实际 {p['alts']})"
)
def test_low_province_meets_confidence_half(quality_data):
"""low 省应满足 conf >= 0.5 但未达 usable 门槛(新旧分层边界)。"""
low_provinces = [p for p in quality_data if 0.5 <= p["confidence"] < 0.65]
if not low_provinces:
pytest.skip("当前无 low 省份")
for p in low_provinces:
assert p["confidence"] >= 0.5, (
f"{p['province']}: low 要求 conf >= 0.5 (实际 {p['confidence']})"
)
# 未达 usable 门槛即为 low具体缺什么在详细质量说明中
# 此处不做细分断言,保持"低但仍可用"的模糊边界
def test_skeleton_province_confidence_below_half(quality_data):
"""skeleton 省应 conf < 0.5(骨架门槛)。"""
skeleton_provinces = [
p for p in quality_data if p["confidence"] >= 0.5 and p["recs"] < 24
] # 低 recs 实际仍为 skeleton
if not skeleton_provinces:
pytest.skip("当前无 skeleton 省份")
for p in skeleton_provinces:
# 结构上应该为 skeleton但当前实在数据无 skeleton
# 持留接口:未来迁移数据到真正的 skeleton 样板后可激活
assert p["confidence"] < 0.5 or p["recs"] < 24
def test_only_high_is_approved_by_quality_summary():
"""quality_summary 与门槛测试不应存在"只看 conf"的漏洞。
门槛测试已覆盖需求,此测试相当于双重验证。
"""
summary = build_quality_summary()
high_provinces = [p for p in summary["provinces"] if p["quality_level"] == "high"]
assert high_provinces, "quality_summary 应统计到 high 省份"
# 手动复核一次门槛(防质量问题被测试隐藏)
loader = CrowdDBLoader(warn_low_confidence=False)
for p_dict in high_provinces:
metadata = loader.load_province(p_dict["province"])
if not metadata:
continue
conf = metadata.get("confidence", 0)
sr = metadata.get("score_ranges", [])
recs = sum(len(r.get("recommendations", [])) for r in sr)
alts = sum(
len(rec.get("alternatives", []))
for r in sr
for rec in r.get("recommendations", [])
)
bands = _classify_score_bands(sr)
assert conf >= 0.8, (
f"{p_dict['province']}: 质量摘要判 high但 conf {conf} 不满足 high 门槛"
)
assert len(sr) >= 8, (
f"{p_dict['province']}: 质量摘要判 high但 score_ranges 数量 {len(sr)} 不满足 >= 8"
)
assert recs >= 40, (
f"{p_dict['province']}: 质量摘要判 high但 recs {recs} 不满足 >= 40"
)
assert alts >= 60, (
f"{p_dict['province']}: 质量摘要判 high但 alts {alts} 不满足 >= 60"
)
assert len(bands) >= 3, (
f"{p_dict['province']}: 质量摘要判 high但分数带 {sorted(bands)} 不满层"
)