diff --git a/core/evolution/protocols.py b/core/evolution/protocols.py new file mode 100644 index 0000000..088217f --- /dev/null +++ b/core/evolution/protocols.py @@ -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 过滤列表。 + + 返回: + 轨迹记录字典列表。 + """ + ... diff --git a/core/evolution/types.py b/core/evolution/types.py new file mode 100644 index 0000000..96b5d21 --- /dev/null +++ b/core/evolution/types.py @@ -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/alpha,20 对应 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_used;n_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/L(win/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 diff --git a/tests/unit/test_evolution_types.py b/tests/unit/test_evolution_types.py new file mode 100644 index 0000000..ab48e0c --- /dev/null +++ b/tests/unit/test_evolution_types.py @@ -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"