feat(core/evolution): protocols.py + types.py 基础层 — 18 个 dataclass + 3 个 Protocol
TRM4 诊断/进化/门控数据类型迁移至 TRM5 Clean Architecture 内核。 逐字段比对 TRM4 的 eprocess.py、diagnose.py、evolve.py 保真迁移。 types.py (18 个 dataclass): - Gate: GateParams, GateVerdict (frozen, 原样迁移) - 诊断: SpanMetrics, SkillStepAdherence, QuestionMetrics, ErrorAttribution, CaseSample, SkillCasePack, SystemCasePack, ToolCasePack, DiagnosisResult (全部 frozen=True) - 进化: EvolutionRecord (mutable), RejectedEdit (frozen), EvolutionResult (frozen, 移除 skills_version/prompts_version) - 新增: PairResult, QuadrantClassification (块验证纯决策输出) - 新增: DiagnosePrompts, EvolvePrompts (模板束, frozen) protocols.py (3 个只读 Protocol): - SkillStore, PromptStore (同步文件读取) - RunLog (异步日志查询, 隔离 SQL) 变更理由: - QuestionMetrics 由 TRM4 mutable 改为 frozen (一次性构造) - ErrorAttribution 由 TRM4 mutable 改为 frozen (构造时填入全部字段) - EvolutionResult 移除版本管理字段 (app/ 职责) 涉及算法: #5(CE-Gate), #8(诊断瀑布), #9(进化引擎) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,374 @@
|
||||
"""core/evolution/types.py 的类型构造与约束测试。
|
||||
|
||||
验证:
|
||||
- frozen 类型不可变性
|
||||
- mutable 类型可修改
|
||||
- 全部 18 个类型可正确构造
|
||||
- 默认值正确性
|
||||
- 字段完整性
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from core.evolution.types import (
|
||||
CaseSample,
|
||||
DiagnosePrompts,
|
||||
DiagnosisResult,
|
||||
ErrorAttribution,
|
||||
EvolutionRecord,
|
||||
EvolutionResult,
|
||||
EvolvePrompts,
|
||||
GateParams,
|
||||
GateVerdict,
|
||||
PairResult,
|
||||
QuadrantClassification,
|
||||
QuestionMetrics,
|
||||
RejectedEdit,
|
||||
SkillCasePack,
|
||||
SkillStepAdherence,
|
||||
SpanMetrics,
|
||||
SystemCasePack,
|
||||
ToolCasePack,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Gate 类型
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_gate_params_frozen():
|
||||
"""GateParams 是 frozen dataclass,构造后不可修改。"""
|
||||
p = GateParams(
|
||||
e_confirm=20.0,
|
||||
e_provisional=3.0,
|
||||
w_net_min=2,
|
||||
delta_min=0.02,
|
||||
lambda_dir=-0.642,
|
||||
e_rollback=10.0,
|
||||
)
|
||||
assert p.e_confirm == 20.0
|
||||
with pytest.raises(AttributeError):
|
||||
p.e_confirm = 1.0
|
||||
|
||||
|
||||
def test_gate_verdict_frozen():
|
||||
"""GateVerdict 是 frozen dataclass。"""
|
||||
v = GateVerdict(
|
||||
decision="accept_confirmed",
|
||||
e_value=25.0,
|
||||
wald_lambda=1.2,
|
||||
delta_hat=0.15,
|
||||
delta_shrunk=0.12,
|
||||
)
|
||||
assert v.decision == "accept_confirmed"
|
||||
with pytest.raises(AttributeError):
|
||||
v.decision = "reject"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 诊断类型
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_span_metrics_frozen():
|
||||
"""SpanMetrics 是 frozen dataclass,含默认空列表。"""
|
||||
sm = SpanMetrics(
|
||||
step=1,
|
||||
tool_name="view_node",
|
||||
extraction_completeness=0.9,
|
||||
hallucination_rate=0.05,
|
||||
)
|
||||
assert sm.step == 1
|
||||
assert sm.missed_info_tags == []
|
||||
assert sm.hallucination_tags == []
|
||||
with pytest.raises(AttributeError):
|
||||
sm.step = 2
|
||||
|
||||
|
||||
def test_skill_step_adherence_frozen():
|
||||
"""SkillStepAdherence 是 frozen dataclass。"""
|
||||
sa = SkillStepAdherence(
|
||||
step_label="定位目标层级",
|
||||
adhered=True,
|
||||
description="正确遵循了定位步骤",
|
||||
)
|
||||
assert sa.adhered is True
|
||||
with pytest.raises(AttributeError):
|
||||
sa.adhered = False
|
||||
|
||||
|
||||
def test_question_metrics_frozen():
|
||||
"""QuestionMetrics 是 frozen dataclass,约 17 个字段。"""
|
||||
qm = QuestionMetrics(
|
||||
question_id="q001",
|
||||
video_id="v001",
|
||||
task_type="Action Reasoning",
|
||||
correct=False,
|
||||
format_compliance=1.0,
|
||||
budget_usage=0.6,
|
||||
confidence_calibration="calibrated",
|
||||
repeat_visit_rate=0.1,
|
||||
search_keyword_repetition=0.0,
|
||||
level_jump_pattern="L1→L2→L3",
|
||||
tool_usage={"view_node": 3, "search_similar": 1},
|
||||
span_metrics=[],
|
||||
missed_nodes=["L2_seg_01"],
|
||||
skill_adherence=[],
|
||||
confirmation_bias=None,
|
||||
evidence_sufficient=True,
|
||||
)
|
||||
assert qm.question_id == "q001"
|
||||
assert qm.degraded is False # 默认值
|
||||
with pytest.raises(AttributeError):
|
||||
qm.correct = True
|
||||
|
||||
|
||||
def test_error_attribution_frozen():
|
||||
"""ErrorAttribution 是 frozen dataclass,含可选字段。"""
|
||||
ea = ErrorAttribution(
|
||||
question_id="q001",
|
||||
error_type="search_failure",
|
||||
reasoning_failure_type=None,
|
||||
cause_category="defect",
|
||||
lapse_note=None,
|
||||
)
|
||||
assert ea.cause_category == "defect"
|
||||
with pytest.raises(AttributeError):
|
||||
ea.cause_category = "lapse"
|
||||
|
||||
|
||||
def test_case_sample_frozen():
|
||||
"""CaseSample 是 frozen dataclass,含完整推理轨迹。"""
|
||||
cs = CaseSample(
|
||||
question_id="q001",
|
||||
video_id="v001",
|
||||
task_type="Temporal Reasoning",
|
||||
question="视频中发生了什么?",
|
||||
options=["A. 跑步", "B. 走路"],
|
||||
answer="A",
|
||||
prediction="B",
|
||||
correct=False,
|
||||
error_type="reasoning_failure",
|
||||
selection_reason="error_type=reasoning_failure, severity=(1, 0.6)",
|
||||
metrics={"correct": False, "budget_usage": 0.6},
|
||||
trace=[{"step": 1, "tool_name": "view_node", "tool_output": "..."}],
|
||||
)
|
||||
assert cs.prediction == "B"
|
||||
with pytest.raises(AttributeError):
|
||||
cs.prediction = "A"
|
||||
|
||||
|
||||
def test_skill_case_pack_frozen():
|
||||
"""SkillCasePack 是 frozen dataclass,含默认空列表。"""
|
||||
pack = SkillCasePack(
|
||||
task_type="Action Reasoning",
|
||||
target_file="action-reasoning.md",
|
||||
stats={"n_total": 10, "accuracy": 0.7},
|
||||
)
|
||||
assert pack.failure_cases == []
|
||||
assert pack.success_cases == []
|
||||
assert pack.lapse_notes == []
|
||||
with pytest.raises(AttributeError):
|
||||
pack.task_type = "other"
|
||||
|
||||
|
||||
def test_system_case_pack_frozen():
|
||||
"""SystemCasePack 是 frozen dataclass。"""
|
||||
pack = SystemCasePack(stats={"early_submit_count": 5})
|
||||
assert pack.failure_cases == []
|
||||
assert pack.success_cases == []
|
||||
with pytest.raises(AttributeError):
|
||||
pack.stats = {}
|
||||
|
||||
|
||||
def test_tool_case_pack_frozen():
|
||||
"""ToolCasePack 是 frozen dataclass。"""
|
||||
pack = ToolCasePack(
|
||||
tool_name="view_node",
|
||||
target_files=["view_node_extract.md", "view_node_verify.md"],
|
||||
stats={"avg_completeness": 0.85},
|
||||
)
|
||||
assert pack.failure_spans == []
|
||||
assert pack.success_spans == []
|
||||
with pytest.raises(AttributeError):
|
||||
pack.tool_name = "other"
|
||||
|
||||
|
||||
def test_diagnosis_result_frozen():
|
||||
"""DiagnosisResult 是 frozen dataclass,约 18 个字段。"""
|
||||
dr = DiagnosisResult(run_id="run_001")
|
||||
assert dr.run_id == "run_001"
|
||||
assert dr.filter_summary == {}
|
||||
assert dr.error_attributions == []
|
||||
assert dr.system_case_pack is None
|
||||
assert dr.infra_excluded_count == 0
|
||||
assert dr.infra_excluded_ratio == 0.0
|
||||
assert dr.defect_count == 0
|
||||
assert dr.lapse_count == 0
|
||||
assert dr.degraded_count == 0
|
||||
assert dr.degraded_question_ids == []
|
||||
with pytest.raises(AttributeError):
|
||||
dr.run_id = "other"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 进化类型
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_evolution_record_mutable():
|
||||
"""EvolutionRecord 是 mutable dataclass,构建过程中需修改。"""
|
||||
r = EvolutionRecord(
|
||||
target_file="test.md",
|
||||
target_type="skill",
|
||||
original_content="a",
|
||||
evolved_content="b",
|
||||
reason="test",
|
||||
status="accepted",
|
||||
source_version="v1",
|
||||
suggestions=[],
|
||||
edits=[],
|
||||
apply_report=[],
|
||||
clip_info={},
|
||||
)
|
||||
r.status = "rejected"
|
||||
assert r.status == "rejected"
|
||||
|
||||
|
||||
def test_evolution_record_defaults():
|
||||
"""EvolutionRecord 各默认字段值正确。"""
|
||||
r = EvolutionRecord(
|
||||
target_file="x.md",
|
||||
target_type="skill",
|
||||
original_content="orig",
|
||||
evolved_content="new",
|
||||
reason="pass",
|
||||
status="accepted",
|
||||
source_version="v1",
|
||||
)
|
||||
assert r.result_version is None
|
||||
assert r.suggestions == []
|
||||
assert r.attempts == []
|
||||
assert r.validation_errors == []
|
||||
assert r.edits == []
|
||||
assert r.apply_report == []
|
||||
assert r.clip_info == {"triggered": False, "clipped": 0}
|
||||
|
||||
|
||||
def test_rejected_edit_frozen():
|
||||
"""RejectedEdit 是 frozen dataclass,含 gate 证据可选字段。"""
|
||||
re_ = RejectedEdit(
|
||||
target_file="temporal-reasoning.md",
|
||||
target_type="skill",
|
||||
change_summary="增加了时序推理步骤",
|
||||
delta=-0.05,
|
||||
source_version="v2",
|
||||
epoch=3,
|
||||
gate_w=5,
|
||||
gate_l=8,
|
||||
gate_e_value=0.3,
|
||||
gate_delta_shrunk=-0.02,
|
||||
)
|
||||
assert re_.gate_w == 5
|
||||
with pytest.raises(AttributeError):
|
||||
re_.delta = 0.0
|
||||
|
||||
|
||||
def test_rejected_edit_optional_gate_fields():
|
||||
"""RejectedEdit gate 字段默认为 None。"""
|
||||
re_ = RejectedEdit(
|
||||
target_file="x.md",
|
||||
target_type="skill",
|
||||
change_summary="test",
|
||||
delta=0.0,
|
||||
source_version="v1",
|
||||
epoch=1,
|
||||
)
|
||||
assert re_.gate_w is None
|
||||
assert re_.gate_l is None
|
||||
assert re_.gate_e_value is None
|
||||
assert re_.gate_delta_shrunk is None
|
||||
|
||||
|
||||
def test_evolution_result_frozen():
|
||||
"""EvolutionResult 是 frozen dataclass,不含 skills_version/prompts_version。"""
|
||||
result = EvolutionResult(
|
||||
records=[],
|
||||
accepted_count=2,
|
||||
rejected_count=1,
|
||||
skipped_count=0,
|
||||
)
|
||||
assert result.accepted_count == 2
|
||||
with pytest.raises(AttributeError):
|
||||
result.accepted_count = 0
|
||||
# 确认不含 TRM4 的 skills_version/prompts_version
|
||||
assert not hasattr(result, "skills_version")
|
||||
assert not hasattr(result, "prompts_version")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 验证辅助类型
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_pair_result_frozen():
|
||||
"""PairResult 是 frozen dataclass。"""
|
||||
pr = PairResult(
|
||||
w=3,
|
||||
l=1,
|
||||
observed={"q1": (False, True), "q2": (True, False)},
|
||||
)
|
||||
assert pr.w == 3
|
||||
with pytest.raises(AttributeError):
|
||||
pr.w = 0
|
||||
|
||||
|
||||
def test_quadrant_classification_frozen():
|
||||
"""QuadrantClassification 是 frozen dataclass,四象限分类。"""
|
||||
qc = QuadrantClassification(
|
||||
improvements=["q1", "q3"],
|
||||
regressions=["q2"],
|
||||
persistent_fails=["q4"],
|
||||
stable_successes=["q5", "q6"],
|
||||
)
|
||||
assert len(qc.improvements) == 2
|
||||
with pytest.raises(AttributeError):
|
||||
qc.improvements = []
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Prompt 模板束
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_diagnose_prompts_frozen():
|
||||
"""DiagnosePrompts 是 frozen dataclass,8 个模板字段。"""
|
||||
dp = DiagnosePrompts(
|
||||
defect_vs_lapse="p1",
|
||||
reasoning_sub="p2",
|
||||
span_eval_system="p3",
|
||||
span_eval_user="p4",
|
||||
missed_nodes="p5",
|
||||
skill_adherence="p6",
|
||||
confirmation_bias="p7",
|
||||
evidence_sufficiency="p8",
|
||||
)
|
||||
assert dp.defect_vs_lapse == "p1"
|
||||
with pytest.raises(AttributeError):
|
||||
dp.defect_vs_lapse = "other"
|
||||
|
||||
|
||||
def test_evolve_prompts_frozen():
|
||||
"""EvolvePrompts 是 frozen dataclass,5 个模板字段。"""
|
||||
ep = EvolvePrompts(
|
||||
evolve_skill="skill_tmpl",
|
||||
evolve_system="system_tmpl",
|
||||
evolve_tool="tool_tmpl",
|
||||
evolve_rank="rank_tmpl",
|
||||
consolidate_system="consolidate_tmpl",
|
||||
)
|
||||
assert ep.evolve_rank == "rank_tmpl"
|
||||
with pytest.raises(AttributeError):
|
||||
ep.evolve_skill = "other"
|
||||
Reference in New Issue
Block a user