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:
2026-07-07 09:34:04 -04:00
parent 09100dcbd4
commit 9d4d52dac5
3 changed files with 964 additions and 0 deletions
+374
View File
@@ -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 dataclass8 个模板字段。"""
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 dataclass5 个模板字段。"""
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"