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
+106
View File
@@ -0,0 +1,106 @@
"""core/evolution/ 子包的只读 Protocol 定义。
三个 Protocol 均为只读——core/ 返回结果 dataclass,写入由 app/ 持久化。
SkillStore / PromptStore 为同步(文件读取量小且快),RunLog 为异步
(隔离 SQLite 查询,core/ 不写 SQL)。
"""
from __future__ import annotations
from typing import Any, Protocol, runtime_checkable
@runtime_checkable
class SkillStore(Protocol):
"""版本化技能读取端口。
实现方解析 manifest 指针,core/ 不感知版本号。
"""
def read_skill(self, filename: str) -> str:
"""读取指定 skill 文件的全文内容。
参数:
filename: skill 文件名,如 'temporal-reasoning.md'
返回:
文件全文内容。
"""
...
def list_skill_files(self) -> list[str]:
"""列出当前版本所有 skill 文件名。
返回:
文件名列表。
"""
...
@runtime_checkable
class PromptStore(Protocol):
"""版本化提示词读取端口。
覆盖 system.md 和 tool extract/verify 文件。
"""
def read_prompt(self, filename: str) -> str:
"""读取指定 prompt 文件的全文内容。
参数:
filename: prompt 文件名,如 'system.md'
返回:
文件全文内容。
"""
...
def list_prompt_files(self) -> list[str]:
"""列出当前版本所有 prompt 文件名。
返回:
文件名列表。
"""
...
@runtime_checkable
class RunLog(Protocol):
"""实验日志查询端口。
隔离 SQLite 实现细节,core/ 不写 SQL。
"""
async def get_predictions(
self,
run_id: str,
*,
question_ids: list[str] | None = None,
) -> list[dict[str, Any]]:
"""查询指定 run 的预测记录。
参数:
run_id: 运行标识。
question_ids: 可选的题目 ID 过滤列表。
返回:
预测记录字典列表。
"""
...
async def get_traces(
self,
run_id: str,
*,
question_ids: list[str] | None = None,
) -> list[dict[str, Any]]:
"""查询指定 run 的推理轨迹。
参数:
run_id: 运行标识。
question_ids: 可选的题目 ID 过滤列表。
返回:
轨迹记录字典列表。
"""
...
+484
View File
@@ -0,0 +1,484 @@
"""core/evolution 子包的数据类型定义。
自进化循环中 gate、diagnose、evolve、validate 共用的 dataclass。
所有输出类型默认 frozen=True(一次性构造、不可变),唯一例外是
EvolutionRecord(构建过程中需要多次修改状态)。
不依赖 app/ 或 adapters/。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
# =========================================================================
# 1. Gate 决策类型
# =========================================================================
@dataclass(frozen=True)
class GateParams:
"""CE-Gate 判据阈值组(从实验配置构造)。
属性:
e_confirm: CONFIRMED 接受的 e 值门槛(1/alpha20 对应 alpha=5%)。
e_provisional: 题尽暂定接受门槛,同时是 futility 出口的代数界。
w_net_min: 题尽暂定接受要求的最小净胜 W-L。
delta_min: 接受要求的最小点估计效应量 (W-L)/n_used。
lambda_dir: Wald 方向游走的拒绝阈值(负数)。
e_rollback: 试用期结算的对称回滚 e 值门槛(1/alpha'10 对应 10%)。
"""
e_confirm: float
e_provisional: float
w_net_min: int
delta_min: float
lambda_dir: float
e_rollback: float
@dataclass(frozen=True)
class GateVerdict:
"""一次块间判定的完整结果(判定 + 全部诊断量)。
属性:
decision: 判定结果,取值为 continue / accept_confirmed /
reject_directional / reject_futility / accept_provisional /
reject_inertia 之一。
e_value: 当前 e 值。
wald_lambda: 当前 Wald 方向游走值。
delta_hat: 点估计效应量 (W-L)/n_usedn_used=0 时为 0。
delta_shrunk: 收缩点估计 (W-L)/(n_used+4),仅观测用。
"""
decision: str
e_value: float
wald_lambda: float
delta_hat: float
delta_shrunk: float
# =========================================================================
# 2. 诊断类型
# =========================================================================
@dataclass(frozen=True)
class SpanMetrics:
"""单次工具调用的输出质量指标。
属性:
step: 工具调用所在的步骤编号。
tool_name: 本次调用使用的工具名称。
extraction_completeness: 信息提取完整度。
hallucination_rate: 幻觉内容占比。
missed_info_tags: 未提取信息的标签列表。
hallucination_tags: 幻觉内容的标签列表。
"""
step: int
tool_name: str
extraction_completeness: float
hallucination_rate: float
missed_info_tags: list[str] = field(default_factory=list)
hallucination_tags: list[str] = field(default_factory=list)
@dataclass(frozen=True)
class SkillStepAdherence:
"""单个 skill step 的遵循判定。
属性:
step_label: 被判定的步骤标签。
adhered: 该步骤是否被遵循。
description: 对遵循情况的文字说明。
"""
step_label: str
adhered: bool
description: str
@dataclass(frozen=True)
class QuestionMetrics:
"""单题的完整指标,即 Stage 1 输出。
包含 7 个规则指标和 5 类 judge 指标(span / missed / adherence /
bias / sufficiency)。frozen=True 保证构造后不可变。
属性:
question_id: 题目唯一标识。
video_id: 对应视频唯一标识。
task_type: 题目任务类型。
correct: 该题最终是否答对。
format_compliance: 输出格式遵循程度。
budget_usage: 预算使用比例。
confidence_calibration: 置信度校准结论。
repeat_visit_rate: 重复访问节点的比例。
search_keyword_repetition: 搜索关键词重复率。
level_jump_pattern: 层级跳转模式描述。
tool_usage: 各工具的调用次数统计。
span_metrics: 该题全部工具调用的片段级质量指标。
missed_nodes: 该题遗漏的节点列表。
skill_adherence: 该题对 skill 步骤的遵循情况。
confirmation_bias: 是否出现确认偏误。None 表示 judge 不可用。
evidence_sufficient: 当前证据是否充足。None 表示 judge 不可用。
degraded: 是否为降级指标(judge 解析失败时生成)。
"""
question_id: str
video_id: str
task_type: str
correct: bool
format_compliance: float
budget_usage: float
confidence_calibration: str
repeat_visit_rate: float
search_keyword_repetition: float
level_jump_pattern: str
tool_usage: dict[str, int]
span_metrics: list[SpanMetrics]
missed_nodes: list[str]
skill_adherence: list[SkillStepAdherence]
confirmation_bias: bool | None
evidence_sufficient: bool | None
degraded: bool = False
@dataclass(frozen=True)
class ErrorAttribution:
"""D1 错误归因。
属性:
question_id: 发生错误归因的题目唯一标识。
error_type: 错误的主要类别。
reasoning_failure_type: 推理失败类型;若不适用则为 None。
cause_category: C3 病因:'defect'/'lapse';正确题/INFRA/未判为 None。
lapse_note: LAPSE 提醒文本(供 appendix 路由);非 LAPSE 为 None。
"""
question_id: str
error_type: str
reasoning_failure_type: str | None
cause_category: str | None = None
lapse_note: str | None = None
@dataclass(frozen=True)
class CaseSample:
"""单个案例样本,进化模块的最小输入单元。
属性:
question_id: 题目唯一标识。
video_id: 对应视频唯一标识。
task_type: 题目任务类型。
question: 题目文本。
options: 选项列表。
answer: 正确答案。
prediction: Agent 预测答案。
correct: 是否答对。
error_type: 错误类型;正确题为 None。
selection_reason: 被选为案例的原因说明。
metrics: QuestionMetrics 的关键字段子集。
trace: 完整推理轨迹,不截断。
"""
question_id: str
video_id: str
task_type: str
question: str
options: list[str]
answer: str
prediction: str | None
correct: bool
error_type: str | None
selection_reason: str
metrics: dict[str, Any]
trace: list[dict[str, Any]]
@dataclass(frozen=True)
class SkillCasePack:
"""单个 task_type 的案例包,服务于 Skill 进化。
属性:
task_type: 题目任务类型。
target_file: 对应 skill 文件名,如 'temporal-reasoning.md'
stats: 从 D3/D4 提取的该题型统计。
failure_cases: 失败案例列表。
success_cases: 成功案例列表。
lapse_notes: C3 LAPSE 提醒文本列表(路由进 appendix 受保护区)。
"""
task_type: str
target_file: str
stats: dict[str, Any]
failure_cases: list[CaseSample] = field(default_factory=list)
success_cases: list[CaseSample] = field(default_factory=list)
lapse_notes: list[str] = field(default_factory=list)
@dataclass(frozen=True)
class SystemCasePack:
"""跨题型行为模式案例包,服务于 System Prompt 进化。
属性:
stats: 从 D5 提取的行为模式统计。
failure_cases: 失败案例列表。
success_cases: 成功案例列表。
"""
stats: dict[str, Any]
failure_cases: list[CaseSample] = field(default_factory=list)
success_cases: list[CaseSample] = field(default_factory=list)
@dataclass(frozen=True)
class ToolCasePack:
"""单个 tool_name 的案例包,服务于 Tool Prompt 进化。
属性:
tool_name: 工具名称。
target_files: 对应 prompt 文件名列表。
stats: 从 D2 提取的工具质量统计。
failure_spans: 失败 span 案例列表。
success_spans: 成功 span 案例列表。
"""
tool_name: str
target_files: list[str]
stats: dict[str, Any]
failure_spans: list[dict[str, Any]] = field(default_factory=list)
success_spans: list[dict[str, Any]] = field(default_factory=list)
@dataclass(frozen=True)
class DiagnosisResult:
"""完整诊断报告,即两阶段诊断管线的最终输出。
属性:
run_id: 本次诊断运行的唯一标识。
filter_summary: 筛选条件与筛选结果摘要。
error_attributions: 错误归因结果列表。
attribution_distribution: 各归因类别的分布统计。
reasoning_failure_types: 各推理失败类型的分布统计。
tool_quality: 按工具聚合的质量分析结果。
search_effectiveness: 搜索有效性的聚合统计。
skill_compliance: 技能遵循情况的聚合统计。
decision_patterns: 决策模式与行为模式摘要。
skill_case_packs: 按题型组织的 Skill 进化案例包。
system_case_pack: 跨题型行为模式案例包;无系统性问题时为 None。
tool_case_packs: 按工具名组织的 Tool Prompt 进化案例包。
infra_excluded_count: C3:被 stop_reason 排除的题数。
infra_excluded_ratio: INFRA 占总题数比例。
infra_question_ids: 被排除题 question_id 列表。
defect_count: 进入诊断池错题中判为 DEFECT 的数量。
lapse_count: 进入诊断池错题中判为 LAPSE 的数量。
degraded_count: judge 解析失败而降级的题数。
degraded_question_ids: 降级题的 question_id 列表。
"""
run_id: str
filter_summary: dict[str, Any] = field(default_factory=dict)
error_attributions: list[ErrorAttribution] = field(default_factory=list)
attribution_distribution: dict[str, int] = field(default_factory=dict)
reasoning_failure_types: dict[str, int] = field(default_factory=dict)
tool_quality: dict[str, dict[str, Any]] = field(default_factory=dict)
search_effectiveness: dict[str, dict[str, Any]] = field(default_factory=dict)
skill_compliance: dict[str, dict[str, Any]] = field(default_factory=dict)
decision_patterns: dict[str, Any] = field(default_factory=dict)
skill_case_packs: dict[str, SkillCasePack] = field(default_factory=dict)
system_case_pack: SystemCasePack | None = None
tool_case_packs: dict[str, ToolCasePack] = field(default_factory=dict)
infra_excluded_count: int = 0
infra_excluded_ratio: float = 0.0
infra_question_ids: list[str] = field(default_factory=list)
defect_count: int = 0
lapse_count: int = 0
degraded_count: int = 0
degraded_question_ids: list[str] = field(default_factory=list)
# =========================================================================
# 3. 进化类型
# =========================================================================
@dataclass
class EvolutionRecord:
"""单个目标文件的一次进化记录。
构建过程中需要多次修改状态(如 status、result_version),
因此是唯一不使用 frozen=True 的类型。
属性:
target_file: 目标文件名,如 'temporal-reasoning.md'
target_type: 目标类型: 'skill' / 'system' / 'tool'
original_content: 改写前原文。
evolved_content: 改写后内容;rejected 时与 original_content 相同。
reason: 状态说明。
status: 'accepted' / 'rejected' / 'skipped'
source_version: 改写前版本号,如 'v1'
result_version: 改写后版本号;rejected/skipped 时为 None。
suggestions: LLM 输出的改动建议列表。
attempts: 每次 LLM 调用的原始响应摘要。
validation_errors: 验证失败的具体原因。
edits: LLM 输出的补丁列表。
apply_report: 补丁逐条应用状态。
clip_info: 超预算裁剪信息。
"""
target_file: str
target_type: str
original_content: str
evolved_content: str
reason: str
status: str
source_version: str
result_version: str | None = None
suggestions: list[dict[str, Any]] = field(default_factory=list)
attempts: list[dict[str, Any]] = field(default_factory=list)
validation_errors: list[str] = field(default_factory=list)
edits: list[dict[str, Any]] = field(default_factory=list)
apply_report: list[dict[str, Any]] = field(default_factory=list)
clip_info: dict[str, Any] = field(default_factory=lambda: {"triggered": False, "clipped": 0})
@dataclass(frozen=True)
class RejectedEdit:
"""已在验证阶段证明无效的历史改法摘要。
属性:
target_file: 目标文件名,如 'temporal-reasoning.md'
target_type: 目标类型: 'skill' / 'system' / 'tool'
change_summary: 被验证为无效的改法摘要。
delta: 该改法对应候选相对基线的准确率变化。
source_version: 该改法来源的版本号,如 'v2'
epoch: 该改法所属的进化轮次。
gate_w: CE-Gate 证据:配对翻转 W(基线错到候选对)。
gate_l: CE-Gate 证据:配对翻转 L(基线对到候选错)。
gate_e_value: CE-Gate 证据:终态 e 值。
gate_delta_shrunk: CE-Gate 证据:收缩效应量(观测用)。
"""
target_file: str
target_type: str
change_summary: str
delta: float
source_version: str
epoch: int
gate_w: int | None = None
gate_l: int | None = None
gate_e_value: float | None = None
gate_delta_shrunk: float | None = None
@dataclass(frozen=True)
class EvolutionResult:
"""一次整体进化流程的汇总结果。
由 app/harness/ 编排层组装。不含 skills_version / prompts_version
(版本管理是 app/ 职责,不属于 core/ 决策内核)。
属性:
records: 所有目标的进化记录。
accepted_count: 通过验证的改写数。
rejected_count: 未通过验证的改写数。
skipped_count: 因无失败案例而跳过的目标数。
"""
records: list[EvolutionRecord] = field(default_factory=list)
accepted_count: int = 0
rejected_count: int = 0
skipped_count: int = 0
# =========================================================================
# 4. 验证辅助类型
# =========================================================================
@dataclass(frozen=True)
class PairResult:
"""块验证配对比对结果。
属性:
w: 基线错、候选对的翻转数。
l: 基线对、候选错的翻转数。
observed: 每题的 (基线是否正确, 候选是否正确) 记录。
"""
w: int
l: int # noqa: E741 — 数学记号 W/Lwin/loss),与 gate.py 一致
observed: dict[str, tuple[bool, bool]]
@dataclass(frozen=True)
class QuadrantClassification:
"""块验证四象限分类。
属性:
improvements: 基线错、候选对的题目 ID 列表。
regressions: 基线对、候选错的题目 ID 列表。
persistent_fails: 两臂均错的题目 ID 列表。
stable_successes: 两臂均对的题目 ID 列表。
"""
improvements: list[str]
regressions: list[str]
persistent_fails: list[str]
stable_successes: list[str]
# =========================================================================
# 5. Prompt 模板束
# =========================================================================
@dataclass(frozen=True)
class DiagnosePrompts:
"""诊断管线所需的全部固定模板束。
由调用方加载后以 frozen dataclass 传入,避免 core/ 依赖文件系统。
属性:
defect_vs_lapse: defect/lapse 病因判别模板。
reasoning_sub: 推理失败子分类模板。
span_eval_system: span 评估系统提示模板。
span_eval_user: span 评估用户提示模板。
missed_nodes: 遗漏节点检测模板。
skill_adherence: 技能遵循判定模板。
confirmation_bias: 确认偏误检测模板。
evidence_sufficiency: 证据充足性判定模板。
"""
defect_vs_lapse: str
reasoning_sub: str
span_eval_system: str
span_eval_user: str
missed_nodes: str
skill_adherence: str
confirmation_bias: str
evidence_sufficiency: str
@dataclass(frozen=True)
class EvolvePrompts:
"""进化引擎所需的全部固定模板束。
由调用方加载后以 frozen dataclass 传入,避免 core/ 依赖文件系统。
属性:
evolve_skill: Skill 进化提示模板。
evolve_system: System Prompt 进化提示模板。
evolve_tool: Tool Prompt 进化提示模板。
evolve_rank: 编辑排序提示模板。
consolidate_system: appendix 压缩系统提示。
"""
evolve_skill: str
evolve_system: str
evolve_tool: str
evolve_rank: str
consolidate_system: str
+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"