Files
Video-Tree-TRM5/research-wiki/plans/2026-07-07-core-evolution.md

1146 lines
44 KiB
Markdown
Raw Permalink 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.
# core/evolution/ Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use subagent-driven-development to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Migrate TRM4 evolution engine into a Clean Architecture extractable kernel (`core/evolution/`).
**Architecture:** 7 files, dependency-ordered. Pure decision logic only — no DB writes, no filesystem versioning, no inference orchestration. All external I/O through 3 Protocols + 1 existing LLMProvider. Async-first (asyncio.gather + Semaphore).
**Tech Stack:** Python 3.11, asyncio, scipy.special, json_repair, loguru, pluggy (already in project)
**Design doc:** `research-wiki/designs/2026-07-07-core-evolution-design.md`
**TRM4 source:** `/home/iomgaa/Projects/Video-Tree-TRM4/core/harness/`
---
## File Structure
| File | Lines (est.) | Creates | Depends on |
|------|-------------|---------|-----------|
| `core/evolution/protocols.py` | 60 | New | `core/evolution/types.py` (TYPE_CHECKING) |
| `core/evolution/types.py` | 280 | New | — |
| `core/evolution/gate.py` | 170 | New | `types.py` |
| `core/evolution/patch.py` | 440 | New | — (loguru only) |
| `core/evolution/validate.py` | 70 | New | `gate.py`, `types.py` |
| `core/evolution/diagnose.py` | 1200 | New | `types.py`, `protocols.py`, `core/protocols.py` |
| `core/evolution/evolve.py` | 900 | New | `types.py`, `protocols.py`, `patch.py`, `core/protocols.py` |
| `core/evolution/__init__.py` | 30 | Modify | all above |
| `tests/unit/test_gate.py` | 200 | New | — |
| `tests/unit/test_patch.py` | 350 | New | — |
| `tests/unit/test_validate.py` | 120 | New | — |
| `tests/unit/test_diagnose.py` | 400 | New | — |
| `tests/unit/test_evolve.py` | 400 | New | — |
---
### Task 1: protocols.py + types.py(基础层)
**Files:**
- Create: `core/evolution/protocols.py`
- Create: `core/evolution/types.py`
- Test: `tests/unit/test_evolution_types.py`
- [ ] **Step 1: Write type construction tests**
```python
"""tests/unit/test_evolution_types.py"""
from core.evolution.types import (
GateParams, GateVerdict, SpanMetrics, SkillStepAdherence,
QuestionMetrics, ErrorAttribution, CaseSample,
SkillCasePack, SystemCasePack, ToolCasePack, DiagnosisResult,
EvolutionRecord, RejectedEdit, EvolutionResult,
PairResult, QuadrantClassification,
DiagnosePrompts, EvolvePrompts,
)
def test_gate_params_frozen():
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
import pytest
with pytest.raises(AttributeError):
p.e_confirm = 1.0
def test_evolution_record_mutable():
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_diagnose_prompts_frozen():
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"
```
- [ ] **Step 2: Run test — expect FAIL (ImportError)**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_evolution_types.py -v`
- [ ] **Step 3: Implement types.py**
从 TRM4 迁移所有 dataclass。关键变更:
| TRM4 位置 | TRM5 变更 |
|-----------|---------|
| `eprocess.py::GateParams/GateVerdict` | 原样迁移 |
| `diagnose.py::SpanMetrics` 等 9 个 | 全部标 `frozen=True`TRM4 中 QuestionMetrics 非 frozenTRM5 一次性构造) |
| `evolve.py::EvolutionRecord` | 保持 mutable,新增 `result_version: str | None = None` 字段 |
| `evolve.py::RejectedEdit` | 原样迁移,frozen |
| `evolve.py::EvolutionResult` | 移除 `skills_version`/`prompts_version`app/ 职责) |
| `validate.py::ValidationOutcome/Probation/InferenceRunConfig` | 不迁——属 app/harness/ |
| 新增 `PairResult`/`QuadrantClassification` | 块验证纯决策输出 |
| 新增 `DiagnosePrompts`/`EvolvePrompts` | 模板束 |
**保真校验点**:逐字段对比 TRM4 dataclass,确保无遗漏字段。特别注意 `DiagnosisResult` 的完整字段列表(约 20 个字段)。
- [ ] **Step 4: Implement protocols.py**
```python
"""core/evolution/protocols.py — 3 个只读 Protocol"""
from __future__ import annotations
from typing import Any, Protocol, runtime_checkable
@runtime_checkable
class SkillStore(Protocol):
def read_skill(self, filename: str) -> str: ...
def list_skill_files(self) -> list[str]: ...
@runtime_checkable
class PromptStore(Protocol):
def read_prompt(self, filename: str) -> str: ...
def list_prompt_files(self) -> list[str]: ...
@runtime_checkable
class RunLog(Protocol):
async def get_predictions(
self, run_id: str, *, question_ids: list[str] | None = None,
) -> list[dict[str, Any]]: ...
async def get_traces(
self, run_id: str, *, question_ids: list[str] | None = None,
) -> list[dict[str, Any]]: ...
```
- [ ] **Step 5: Run test — expect PASS**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_evolution_types.py -v`
- [ ] **Step 6: Commit**
```
git add core/evolution/types.py core/evolution/protocols.py tests/unit/test_evolution_types.py
git commit -m "feat(evolution): types.py + protocols.py — foundation dataclasses and Protocol ports"
```
---
### Task 2: gate.py — CE-Gate e-process(算法 #5
**Files:**
- Create: `core/evolution/gate.py`
- Test: `tests/unit/test_gate.py`
- Source: TRM4 `eprocess.py` (163 行,原样迁移,零有意变更)
- [ ] **Step 1: Write gate tests**
```python
"""tests/unit/test_gate.py"""
import math
import pytest
from core.evolution.gate import compute_e_value, gate_decision, probation_verdict
from core.evolution.types import GateParams, GateVerdict
_PARAMS = 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,
)
class TestComputeEValue:
def test_zero_zero_returns_one(self):
assert compute_e_value(0, 0) == pytest.approx(1.0)
def test_negative_w_raises(self):
with pytest.raises(ValueError):
compute_e_value(-1, 0)
def test_negative_l_raises(self):
with pytest.raises(ValueError):
compute_e_value(0, -1)
def test_heavy_loss_returns_near_zero(self):
assert compute_e_value(0, 20) < 0.01
def test_heavy_win_returns_large(self):
assert compute_e_value(10, 0) > 100
def test_symmetric(self):
e_5_3 = compute_e_value(5, 3)
e_3_5 = compute_e_value(3, 5)
assert e_5_3 > e_3_5
class TestGateDecision:
def test_confirmed_needs_both_e_and_delta(self):
v = gate_decision(10, 0, 10, 10, params=_PARAMS)
assert v.decision == "accept_confirmed"
def test_continue_on_balanced(self):
v = gate_decision(3, 3, 6, 20, params=_PARAMS)
assert v.decision == "continue"
def test_reject_inertia_on_exhaustion(self):
v = gate_decision(1, 1, 2, 0, params=_PARAMS)
assert v.decision == "reject_inertia"
def test_n_used_zero_raises(self):
with pytest.raises(ValueError):
gate_decision(0, 0, 0, 10, params=_PARAMS)
def test_n_remaining_negative_raises(self):
with pytest.raises(ValueError):
gate_decision(1, 0, 1, -1, params=_PARAMS)
class TestProbationVerdict:
def test_strong_win_confirmed(self):
assert probation_verdict(10, 0, params=_PARAMS) == "confirmed"
def test_strong_loss_rollback(self):
assert probation_verdict(0, 10, params=_PARAMS) == "rollback"
def test_balanced_unverified(self):
assert probation_verdict(3, 3, params=_PARAMS) == "unverified"
```
- [ ] **Step 2: Run test — expect FAIL**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_gate.py -v`
- [ ] **Step 3: Implement gate.py**
从 TRM4 `eprocess.py` 原样迁移全部代码(163 行)。变更仅限:
- import 路径:`core.harness.eprocess``core.evolution.gate`
- `GateParams`/`GateVerdict``core.evolution.types` 导入(不在 gate.py 定义)
**保真校验**:逐行比对 TRM4 `eprocess.py`,确保 `_WALD_WIN`/`_WALD_LOSS`/`_SHRINK_PSEUDO` 常量值、log 空间公式、对称性技巧、四出口优先级链、futility best-case 检查全部保留。
- [ ] **Step 4: Run test — expect PASS**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_gate.py -v`
- [ ] **Step 5: Commit**
```
git add core/evolution/gate.py tests/unit/test_gate.py
git commit -m "feat(evolution): gate.py — CE-Gate e-process (#5)"
```
---
### Task 3: patch.py — 补丁引擎(算法 #9 局部)
**Files:**
- Create: `core/evolution/patch.py`
- Test: `tests/unit/test_patch.py`
- Source: TRM4 `patch.py` (427 行,原样迁移,零有意变更)
- [ ] **Step 1: Write patch tests**
```python
"""tests/unit/test_patch.py"""
import pytest
from core.evolution.patch import (
APPENDIX_START, APPENDIX_END, MOMENTUM_START, MOMENTUM_END,
appendix_region_bounds, momentum_region_bounds, momentum_inner,
append_to_appendix, extract_appendix_notes, replace_appendix_notes,
replace_momentum, apply_patch_with_report,
)
class TestRegionBounds:
def test_no_markers_returns_none(self):
assert appendix_region_bounds("hello") is None
assert momentum_region_bounds("hello") is None
def test_both_markers_returns_range(self):
text = f"head\n{APPENDIX_START}\nbody\n{APPENDIX_END}\ntail"
start, end = appendix_region_bounds(text)
assert text[start:end].startswith(APPENDIX_START)
assert text[start:end].endswith(APPENDIX_END)
def test_single_marker_raises(self):
with pytest.raises(ValueError):
appendix_region_bounds(f"head\n{APPENDIX_START}\nbody")
with pytest.raises(ValueError):
momentum_region_bounds(f"head\n{MOMENTUM_END}\nbody")
class TestAppendix:
def test_append_creates_region(self):
result = append_to_appendix("content", ["note1"])
assert APPENDIX_START in result
assert "- note1" in result
def test_extract_notes(self):
text = f"{APPENDIX_START}\n- a\n- b\n{APPENDIX_END}"
assert extract_appendix_notes(text) == ["a", "b"]
def test_replace_empty_deletes_region(self):
text = f"head\n{APPENDIX_START}\n- old\n{APPENDIX_END}\ntail"
result = replace_appendix_notes(text, [])
assert APPENDIX_START not in result
class TestMomentum:
def test_replace_creates_region(self):
result = replace_momentum("content", "guidance text")
assert MOMENTUM_START in result
assert "guidance text" in result
def test_marker_injection_raises(self):
with pytest.raises(ValueError):
replace_momentum("content", f"evil {MOMENTUM_START}")
def test_empty_guidance_clears(self):
text = replace_momentum("content", "old")
result = replace_momentum(text, "")
inner = momentum_inner(result)
assert inner == ""
class TestApplyPatch:
def test_append_before_protected(self):
content = f"body\n{APPENDIX_START}\nprotected\n{APPENDIX_END}"
edits = [{"op": "append", "target": "", "content": "new line"}]
new, report = apply_patch_with_report(content, edits, [f"{APPENDIX_START}\nprotected\n{APPENDIX_END}"])
assert report[0]["status"].startswith("applied")
assert new.index("new line") < new.index(APPENDIX_START)
def test_replace_in_protected_skipped(self):
protected = f"{APPENDIX_START}\nprotected\n{APPENDIX_END}"
content = f"body\n{protected}"
edits = [{"op": "replace", "target": "protected", "content": "replaced"}]
new, report = apply_patch_with_report(content, edits, [protected])
assert report[0]["status"] == "skipped_protected"
assert "protected" in new
def test_insert_after_fallback(self):
content = "line1\nline2"
edits = [{"op": "insert_after", "target": "nonexistent", "content": "new"}]
new, report = apply_patch_with_report(content, edits)
assert "applied_insert_after_fallback" in report[0]["status"]
def test_delete_first_occurrence(self):
content = "a\nb\na\nc"
edits = [{"op": "delete", "target": "a", "content": ""}]
new, report = apply_patch_with_report(content, edits)
assert new.count("a") == 1
```
- [ ] **Step 2: Run test — expect FAIL**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_patch.py -v`
- [ ] **Step 3: Implement patch.py**
从 TRM4 `patch.py` 原样迁移全部代码(427 行)。零有意变更——import 路径调整除外。
**保真校验**
- 7 个常量值完全一致
- `_protected_ranges` 半开区间语义 `[start, end)`
- `_in_ranges``start <= pos < end`
- append op 的 `start > 0` 过滤(跳过 frontmatter
- insert_after 三结果(成功 / 降级 append / skip
- target 不 strippayload strip
- 每条 edit 前重算 ranges
- report 字段 truncationtarget[:200], content[:200]
- [ ] **Step 4: Run test — expect PASS**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_patch.py -v`
- [ ] **Step 5: Commit**
```
git add core/evolution/patch.py tests/unit/test_patch.py
git commit -m "feat(evolution): patch.py — patch engine with protected regions (#9)"
```
---
### Task 4: validate.py — 纯决策函数(算法 #7 局部)
**Files:**
- Create: `core/evolution/validate.py`
- Test: `tests/unit/test_validate.py`
- [ ] **Step 1: Write validate tests**
```python
"""tests/unit/test_validate.py"""
from core.evolution.validate import pair_block, classify_quadrants, compute_accuracy
class TestPairBlock:
def test_basic_flips(self):
baseline = {"q1": False, "q2": True, "q3": True}
candidate = {"q1": True, "q2": False, "q3": True}
result = pair_block(baseline, candidate, ["q1", "q2", "q3"])
assert result.w == 1 # q1: wrong→right
assert result.l == 1 # q2: right→wrong
assert result.observed == {
"q1": (False, True), "q2": (True, False), "q3": (True, True)
}
def test_empty(self):
result = pair_block({}, {}, [])
assert result.w == 0 and result.l == 0
class TestClassifyQuadrants:
def test_all_four(self):
observed = {
"q1": (False, True), # improved
"q2": (True, False), # regressed
"q3": (False, False), # persistent_fail
"q4": (True, True), # stable_success
}
qc = classify_quadrants(observed)
assert qc.improvements == ["q1"]
assert qc.regressions == ["q2"]
assert qc.persistent_fails == ["q3"]
assert qc.stable_successes == ["q4"]
def test_sorted_within_quadrant(self):
observed = {"z": (False, True), "a": (False, True)}
qc = classify_quadrants(observed)
assert qc.improvements == ["a", "z"]
class TestComputeAccuracy:
def test_basic(self):
assert compute_accuracy({"q1": True, "q2": False}, ["q1", "q2"]) == 0.5
def test_empty_raises(self):
import pytest
with pytest.raises(ZeroDivisionError):
compute_accuracy({}, [])
```
- [ ] **Step 2: Run test — expect FAIL**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_validate.py -v`
- [ ] **Step 3: Implement validate.py**
三个纯函数,约 70 行。从 TRM4 `validate.py``_pair_block``_classify_quadrants` 提取纯逻辑。
```python
"""core/evolution/validate.py — 块验证纯决策函数。"""
from core.evolution.types import PairResult, QuadrantClassification
def pair_block(
baseline: dict[str, bool],
candidate: dict[str, bool],
question_ids: list[str],
) -> PairResult:
"""逐题比对基线与候选对错,统计翻转。"""
w = l = 0
observed: dict[str, tuple[bool, bool]] = {}
for qid in question_ids:
b, c = baseline[qid], candidate[qid]
observed[qid] = (b, c)
if not b and c:
w += 1
elif b and not c:
l += 1
return PairResult(w=w, l=l, observed=observed)
def classify_quadrants(
observed: dict[str, tuple[bool, bool]],
) -> QuadrantClassification:
"""按 (baseline, candidate) 四组分类,各组内 sorted。"""
improvements, regressions, persistent_fails, stable_successes = [], [], [], []
for qid, (prev, curr) in observed.items():
if not prev and curr:
improvements.append(qid)
elif prev and not curr:
regressions.append(qid)
elif not prev and not curr:
persistent_fails.append(qid)
else:
stable_successes.append(qid)
return QuadrantClassification(
improvements=sorted(improvements),
regressions=sorted(regressions),
persistent_fails=sorted(persistent_fails),
stable_successes=sorted(stable_successes),
)
def compute_accuracy(
correctness: dict[str, bool],
question_ids: list[str],
) -> float:
"""纯算术:sum(correct) / len(ids)。"""
return sum(correctness[qid] for qid in question_ids) / len(question_ids)
```
- [ ] **Step 4: Run test — expect PASS**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_validate.py -v`
- [ ] **Step 5: Commit**
```
git add core/evolution/validate.py tests/unit/test_validate.py
git commit -m "feat(evolution): validate.py — pure block validation decision functions (#7)"
```
---
### Task 5: diagnose.py — 指标计算与 judge 辅助函数
**Files:**
- Create: `core/evolution/diagnose.py`(本 Task 写 metrics 部分,约 500 行)
- Test: `tests/unit/test_diagnose.py`(本 Task 写 metrics 测试)
- Source: TRM4 `metrics.py` + `diagnose.py``attribute_error`/`classify_defect_vs_lapse`
- [ ] **Step 1: Write metrics + attribution tests**
```python
"""tests/unit/test_diagnose.py"""
import pytest
from core.evolution.diagnose import (
calc_format_compliance, calc_budget_usage,
calc_confidence_calibration, calc_repeat_visit_rate,
calc_search_keyword_repetition, calc_level_jump_pattern,
calc_tool_usage, extract_json_from_response,
attribute_error, question_soft_score, aggregate_soft,
)
class TestRuleMetrics:
def test_format_compliance_empty_returns_one(self):
assert calc_format_compliance([]) == 1.0
def test_budget_usage(self):
assert calc_budget_usage(5, 15) == pytest.approx(1/3)
def test_confidence_calibration(self):
assert calc_confidence_calibration(0.9, False) == "high_conf_wrong"
assert calc_confidence_calibration(0.3, True) == "low_conf_right"
assert calc_confidence_calibration(0.6, True) == "calibrated"
def test_repeat_visit_empty(self):
assert calc_repeat_visit_rate([]) == 0.0
def test_repeat_visit_all_unique(self):
assert calc_repeat_visit_rate(["a", "b", "c"]) == 0.0
def test_repeat_visit_all_same(self):
assert calc_repeat_visit_rate(["a", "a", "a"]) == pytest.approx(2/3)
def test_keyword_repetition_lt2(self):
assert calc_search_keyword_repetition(["one"]) == 0.0
def test_keyword_repetition_max_jaccard(self):
val = calc_search_keyword_repetition(["abcdef", "abcxyz"])
assert 0.0 < val < 1.0
def test_level_jump_pattern(self):
assert "L1" in calc_level_jump_pattern(["seg_L1_000", "seg_L2_001"])
def test_tool_usage_counts(self):
assert calc_tool_usage(["view_node", "view_node", "search_similar"]) == {
"view_node": 2, "search_similar": 1,
}
class TestJsonExtraction:
def test_fenced_block(self):
raw = '```json\n{"key": "val"}\n```'
assert extract_json_from_response(raw) == {"key": "val"}
def test_outermost_braces(self):
raw = 'prefix {"key": 1} suffix'
assert extract_json_from_response(raw) == {"key": 1}
def test_non_dict_raises(self):
with pytest.raises(ValueError):
extract_json_from_response("[1,2,3]")
def test_garbage_raises(self):
with pytest.raises(ValueError):
extract_json_from_response("not json at all")
class TestAttributeError:
def test_extraction_failure(self):
from core.evolution.types import QuestionMetrics, SpanMetrics
span = SpanMetrics(step=1, tool_name="view_node",
extraction_completeness=0.3, hallucination_rate=0.0,
missed_info_tags=[], hallucination_tags=[])
qm = _make_qm(correct=False, span_metrics=[span], missed_nodes=[],
evidence_sufficient=True)
ea = attribute_error(qm)
assert ea.error_type == "extraction_failure"
def test_search_failure(self):
qm = _make_qm(correct=False, span_metrics=[], missed_nodes=["L2_001"],
evidence_sufficient=False)
ea = attribute_error(qm)
assert ea.error_type == "search_failure"
def test_reasoning_failure(self):
qm = _make_qm(correct=False, span_metrics=[], missed_nodes=[],
evidence_sufficient=True)
ea = attribute_error(qm)
assert ea.error_type == "reasoning_failure"
def test_mixed_fallback(self):
qm = _make_qm(correct=False, span_metrics=[], missed_nodes=[],
evidence_sufficient=False)
ea = attribute_error(qm)
assert ea.error_type == "mixed"
class TestSoftScore:
def test_no_spans_returns_none(self):
assert question_soft_score([]) is None
def test_aggregate_skips_none(self):
assert aggregate_soft([0.8, None, 0.6]) == pytest.approx(0.7)
def test_aggregate_all_none(self):
assert aggregate_soft([None, None]) is None
```
注:`_make_qm` 是测试辅助工厂函数,构造 `QuestionMetrics` 并为非关键字段填充合理默认值。
- [ ] **Step 2: Run test — expect FAIL**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_diagnose.py -v`
- [ ] **Step 3: Implement diagnose.py metrics 部分**
从 TRM4 `metrics.py` 迁移以下函数组:
- `extract_rule_metrics`(从 prediction dict + raw_contents 提取 7 个规则指标;confidence 优先取末步 JSON 的 `reflect.confidence`,否则 `prediction["answer_confidence"]` 默认 0.5
- 7 个规则指标函数(`calc_format_compliance` 等)+ `_trigrams` + `_parse_json_object` + `_extract_last_confidence`
- `extract_json_from_response`(三级解析:fenced → outermost `{}``json_repair`
- `_call_judge`async 化,max_retries=2 即共 3 次,仅 ValueError 重试,API 错误直传)
- `question_soft_score` + `aggregate_soft`
- 5 个 judge 函数(`evaluate_span` 等,async 化),prompt 文件名:`diagnose_span.md`/`diagnose_missed_nodes.md`/`diagnose_skill_adherence.md`/`diagnose_confirmation_bias.md`/`diagnose_evidence_sufficiency.md`
- `compute_question_metrics`async 化)
- `_format_trace_text`metrics 版:thought[:100], output[:200]
从 TRM4 `diagnose.py` 迁移:
- `attribute_error`(归因瀑布,纯函数)
- `classify_defect_vs_lapse`async 化,LLMProvider 替代 LLMClient
- `_make_degraded_metrics`worker 抛 ValueError 时生成 degraded=True 的 QuestionMetricsjudge 字段 None/空列表;其他异常直传)
**关键变更**
- `LLMClient``LLMProvider``response.choices[0].message.content``response.content`
- `_call_judge` 变 async`await llm.chat(messages)`
- judge 函数均变 async
- `load_diagnose_prompt(prompts_dir, filename)` → 直接从 `DiagnosePrompts` 束取属性
**保真校验**
- `_SPAN_EVAL_TOOLS = {"view_node", "search_similar", "observe_frame"}`
- trigram 是字符级,取 MAX(非 mean
- `calc_format_compliance` 空返回 1.0`calc_budget_usage` 无除零 guardP5
- confidence 阈值:`>=0.7` 且错 → high_conf_wrong`<0.5` 且对 → low_conf_right
- `calc_level_jump_pattern` regex `r"_L(\d+)_"`,用 `→` 连接
- `_call_judge` max_retries=2(共 3 次),API 错误直传
- 归因瀑布精确顺序:extraction → search → reasoning → mixed
- defect_vs_lapse 解析失败降级 "lapse"
- `_extract_last_confidence` 任意异常返回 0.5
- judge 返回值默认:span completeness/hallucination 默认 0.0tags 用 `list()`missed_nodes 非 list 返 `[]`
- [ ] **Step 4: Run test — expect PASS**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_diagnose.py -v`
- [ ] **Step 5: Commit**
```
git add core/evolution/diagnose.py tests/unit/test_diagnose.py
git commit -m "feat(evolution): diagnose.py metrics + attribution (Stage 1)"
```
---
### Task 6: diagnose.py — 聚合 + 案例包 + 入口
**Files:**
- Modify: `core/evolution/diagnose.py`(追加 ~700 行)
- Modify: `tests/unit/test_diagnose.py`(追加聚合 + 入口测试)
- [ ] **Step 1: Write aggregation + case pack tests**
```python
# 追加到 tests/unit/test_diagnose.py
from unittest.mock import AsyncMock
from core.evolution.types import (
QuestionMetrics, ErrorAttribution, SpanMetrics,
SkillCasePack, SystemCasePack, ToolCasePack, DiagnosisResult,
)
from core.evolution.diagnose import (
aggregate_d2, aggregate_d3, aggregate_d4, aggregate_d5,
merge_system_packs, merge_tool_packs, run_diagnosis,
)
class TestAggregation:
def test_d2_empty(self):
assert aggregate_d2([]) == {}
def test_d5_empty_returns_zero_structure(self):
result = aggregate_d5([])
assert "early_submit_rate" in result
assert result["early_submit_rate"] == 0.0
class TestMerge:
def test_merge_system_packs_none_on_empty(self):
assert merge_system_packs([]) is None
def test_merge_system_packs_wraps_stats(self):
pack = SystemCasePack(
stats={"a": 1}, failure_cases=[], success_cases=[],
)
merged = merge_system_packs([pack, pack])
assert "per_step" in merged.stats
assert len(merged.stats["per_step"]) == 2
class TestRunDiagnosis:
def test_empty_predictions_returns_empty_result(self):
import asyncio
from core.evolution.types import DiagnosePrompts
mock_log = AsyncMock()
mock_log.get_predictions.return_value = []
mock_log.get_traces.return_value = []
mock_llm = AsyncMock()
mock_store = MagicMock()
mock_store.list_skill_files.return_value = []
prompts = DiagnosePrompts(
defect_vs_lapse="", reasoning_sub="",
span_eval_system="", span_eval_user="",
missed_nodes="", skill_adherence="",
confirmation_bias="", evidence_sufficiency="",
)
result = asyncio.run(run_diagnosis(
"run1", [], {}, mock_llm, mock_log, mock_store, prompts,
concurrency=1,
))
assert isinstance(result, DiagnosisResult)
```
- [ ] **Step 2: Run test — expect FAIL**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_diagnose.py::TestAggregation tests/unit/test_diagnose.py::TestMerge tests/unit/test_diagnose.py::TestRunDiagnosis -v`
- [ ] **Step 3: Implement aggregation + case packs + run_diagnosis**
从 TRM4 `diagnose.py` 迁移:
- `_mean`, `_percentile` 辅助函数
- `aggregate_d2/d3/d4/d5`
- `_build_skill_case_packs`(含 severity 函数、C3 lapse routing、single-failure fallback;成功案例 `n_success=max(2, len(failures)//2)`acc≤0.3 按 budget 升序否则按 adherence 降序)
- `_build_system_case_pack``_MIN_PATTERN_COUNT=3`,3 种行为模式;成功案例要求 correct+calibrated+no_bias+0.3≤budget≤0.8,按 abs(budget-0.5) 排序)
- `_build_tool_case_packs``_TOOL_TARGET_FILES` 映射;低 completeness 先选最多 4 条,高 hallucination 补到总数 4 上限;成功 span 要求 completeness≥0.9 且 hallucination==0.0
- `merge_system_packs`/`merge_tool_packs`stats 用 `{"per_step": [...]}` 包裹)
- `_classify_reasoning_failure`(串行 passprompt `diagnose_reasoning_failure.md`JSON key `type`,解析失败 → `reasoning_failure_type=None` 不中断)
- `run_diagnosis` 入口(asyncSemaphore 限并发,reasoning_failure 串行 pass
注:`resolve_skill_file` 定义在 evolve.pyTask 7),diagnose.py 从 evolve 导入。
**关键变更**
- `ThreadPoolExecutor``asyncio.gather` + `Semaphore(concurrency)`
- `HarnessLog``RunLog` Protocol`get_predictions`/`get_traces`
- 不写 DB`_ensure_diagnosis_tables`/`_clear_existing`/`_insert_*` 全部移除)
- 不写 JSON 文件(`write analyses/...` 移除)
- 树数据从参数传入(非 `_load_tree_cache` 文件读)
- skill 内容从 `SkillStore`
- INFRA 统计按 task/video/question 过滤范围重算,不受 stop_reason 过滤影响
**保真校验**
- `_INFRA_STOP_REASONS = frozenset({"error", "parse_error"})`
- 案例包选择规则(见上述各函数描述)
- single-failure fallback1 个 defect → lapse_notefallback 文本 `"复核该类已有规则,避免重复此类单例失败"`
- lapse_note 空白过滤(strip 后空则丢弃)
- `_format_trace_text`diagnose 版不截断,与 metrics 版不同!)
- D3 `avg_steps` key 实际存 budget_usage meanTRM4 命名不一致,保留)
- `_make_case_sample` metrics 子字典固定 keycorrect/error_type/budget_usage/confidence_calibration/repeat_visit_rate/tool_usage/missed_nodes/adherence_rate/confirmation_bias/evidence_sufficient
- [ ] **Step 4: Run test — expect PASS**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_diagnose.py -v`
- [ ] **Step 5: Commit**
```
git add core/evolution/diagnose.py tests/unit/test_diagnose.py
git commit -m "feat(evolution): diagnose.py aggregation + case packs + run_diagnosis (#8)"
```
---
### Task 7: evolve.py — 验证 + 辅助函数
**Files:**
- Create: `core/evolution/evolve.py`(本 Task 写验证 + 辅助部分,约 400 行)
- Test: `tests/unit/test_evolve.py`
- Source: TRM4 `evolve.py``validate_*`/`rank_and_clip`/`edit_budget_at`/`_resolve_skill_file`
- [ ] **Step 1: Write evolve validation + helpers tests**
```python
"""tests/unit/test_evolve.py"""
import pytest
from core.evolution.evolve import (
validate_skill, validate_system, validate_tool,
edit_budget_at, resolve_skill_file,
)
class TestValidateSkill:
def test_identical_passes(self):
content = "---\nname: test\ndescription: d\ntask_type: t\n---\nbody"
result = validate_skill(content, content)
assert result.passed
def test_changed_frontmatter_fails(self):
orig = "---\nname: a\ndescription: d\ntask_type: t\n---\nbody"
evol = "---\nname: b\ndescription: d\ntask_type: t\n---\nbody"
result = validate_skill(orig, evol)
assert not result.passed
def test_length_ratio_too_short_fails(self):
orig = "---\nname: a\ndescription: d\ntask_type: t\n---\n" + "x" * 1000
evol = "---\nname: a\ndescription: d\ntask_type: t\n---\nshort"
result = validate_skill(orig, evol)
assert not result.passed
class TestValidateSystem:
def test_identical_passes(self):
content = "intro\n## 能力边界\nfrozen\n## 输出格式\nfrozen2\n## other\nbody"
result = validate_system(content, content)
assert result.passed
def test_changed_frozen_section_fails(self):
orig = "intro\n## 能力边界\noriginal\n## other\nbody"
evol = "intro\n## 能力边界\nchanged\n## other\nbody"
result = validate_system(orig, evol)
assert not result.passed
class TestValidateTool:
def test_identical_passes(self):
extract = "## 输出格式\nfixed\n## other\nbody"
verify = "## 输出格式\nfixed2\n## other\nbody2"
result = validate_tool(extract, extract, verify, verify)
assert result.passed
def test_no_code_block_check(self):
extract = "## 输出格式\nfixed\n```\nunclosed"
result = validate_tool(extract, extract, "v", "v")
assert result.passed # tool 不检查代码块闭合
class TestEditBudget:
def test_start_at_zero(self):
assert edit_budget_at(0, 100, 5, 2) == 5
def test_end_at_total(self):
assert edit_budget_at(100, 100, 5, 2) == 2
def test_total_steps_one(self):
assert edit_budget_at(0, 1, 5, 2) == 5
def test_start_less_than_end_asserts(self):
with pytest.raises(AssertionError):
edit_budget_at(0, 100, 2, 5)
class TestResolveSkillFile:
def test_direct_match(self):
class FakeStore:
def list_skill_files(self): return ["action-reasoning.md", "default-strategy.md"]
def read_skill(self, f): return ""
assert resolve_skill_file(FakeStore(), "Action Reasoning") == "action-reasoning.md"
def test_fallback_to_default(self):
class FakeStore:
def list_skill_files(self): return ["default-strategy.md"]
def read_skill(self, f): return ""
assert resolve_skill_file(FakeStore(), "Unknown Type") == "default-strategy.md"
```
- [ ] **Step 2: Run test — expect FAIL**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_evolve.py -v`
- [ ] **Step 3: Implement evolve.py validation + helpers**
从 TRM4 `evolve.py` 迁移:
- `_parse_frontmatter``_strip_appendix_region`(宽容版)、`_strip_momentum_region`(严格版)、`_strip_protected_regions`
- `_check_length`(去 appendix+momentum 后比较,ratio [0.3, 2.0]
- `_check_code_blocks`
- `_extract_section`
- `_skill_protected_spans`/`_system_protected_spans`/`_tool_protected_spans`
- `validate_skill`/`validate_system`/`validate_tool` → 返回 `ValidationResult`(定义在此文件内部,非 types.py)
- `edit_budget_at`(纯数学,保持 TRM4 断言和 banker's rounding
- `rank_and_clip`async 化,`type(idx) is int` 排除 bool
- `_select_top_edits`
- `_parse_llm_json`(两级:fenced → json.loads,失败返回 None
- `resolve_skill_file`(接受 `SkillStore` 而非 `Path`
- `_format_case_samples`tool_output[:500] 截断)
- `_format_spans`tool_output[:500] 截断)
- `_format_rejected_edits`
**关键变更**
- `LLMClient``LLMProvider`
- `_resolve_skill_file(skills_dir: Path, ...)``resolve_skill_file(skill_store: SkillStore, ...)`
- `rank_and_clip` 变 async
**保真校验**
- 冻结区配置:Skill(frontmatter+appendix+momentum)、System(3 sections+appendix)、Tool(输出格式+appendix)
- frontmatter 三字段:name/description/task_type
- `_parse_frontmatter` regex 必须从文件开头匹配 `^---\n...\n---``yaml.safe_load` 失败返回 None
- `_strip_appendix_region` 宽容 vs `_strip_momentum_region` 严格(不对称保留)
- `_parse_llm_json`:只匹配 ` ```json ` fenced block(非 ``` 不带 json)→ `json.loads`,失败返回 None(与 metrics 的三级不同!)
- validate_tool 不检查代码块闭合(与 skill/system 不同)
- `type(idx) is int`(非 isinstance
- `rank_and_clip`/`_request_rank_indices`rank LLM ValueError 降级,API 异常不捕获
- `rewrite_from_suggestions`prompt `evolve_rewrite.md`JSON key `rewritten`,重写不得长于原文,只捕 ValueError/KeyError/TypeError/AttributeError
- `_format_case_samples` tool_output[:500] 截断
- `_format_rejected_edits` gate 证据格式 `W=... L=... E={:.2f} δ̂={:+.3f}`
- [ ] **Step 4: Run test — expect PASS**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_evolve.py -v`
- [ ] **Step 5: Commit**
```
git add core/evolution/evolve.py tests/unit/test_evolve.py
git commit -m "feat(evolution): evolve.py validation + helpers"
```
---
### Task 8: evolve.py — per-target 进化
**Files:**
- Modify: `core/evolution/evolve.py`(追加 ~500 行)
- Modify: `tests/unit/test_evolve.py`(追加进化测试)
- [ ] **Step 1: Write evolution loop tests**
```python
# 追加到 tests/unit/test_evolve.py
import asyncio
from unittest.mock import AsyncMock, MagicMock
from core.evolution.types import (
SkillCasePack, SystemCasePack, ToolCasePack,
EvolutionRecord, EvolvePrompts,
)
from core.evolution.evolve import (
evolve_single_skill, evolve_system_prompt, evolve_single_tool,
consolidate_appendix,
)
_PROMPTS = EvolvePrompts(
evolve_skill="sk", evolve_system="sys", evolve_tool="tool",
evolve_rank="rank", consolidate_system="cons",
)
def _make_fake_llm(response_content: str):
"""构造返回固定内容的假 LLMProvider。"""
from core.types import LLMResponse
mock = AsyncMock()
mock.chat.return_value = LLMResponse(
content=response_content, thinking="", model="test",
provider="test", prompt_tokens=0, completion_tokens=0,
latency_ms=0, ttft_ms=None, max_inter_token_ms=None,
cache_hit=False, call_id="test-id",
)
return mock
class TestEvolveSingleSkill:
def test_empty_pack_skipped(self):
pack = SkillCasePack(
task_type="test", target_file="test.md",
stats={}, failure_cases=[], success_cases=[], lapse_notes=[],
)
store = MagicMock()
store.read_skill.return_value = "---\nname: t\ndescription: d\ntask_type: t\n---\nbody"
store.list_skill_files.return_value = ["test.md"]
llm = _make_fake_llm('{"suggestions":[],"edits":[]}')
record = asyncio.run(evolve_single_skill(
llm, pack, store, _PROMPTS, "v1", 5, 6,
))
assert record.status in ("rejected", "skipped")
class TestEvolveSystemPrompt:
def test_no_failures_returns_skipped(self):
pack = SystemCasePack(stats={}, failure_cases=[], success_cases=[])
store = MagicMock()
store.read_prompt.return_value = "## 能力边界\nfixed\n## 输出格式\nfixed\n## 视频树结构\nfixed\nbody"
llm = _make_fake_llm('{"suggestions":[],"edits":[]}')
record = asyncio.run(evolve_system_prompt(
llm, pack, store, _PROMPTS, "v1", 5,
))
assert record.status in ("rejected", "skipped")
class TestEvolveSingleTool:
def test_evolved_content_is_json(self):
pack = ToolCasePack(
tool_name="view_node",
target_files=["view_node_extract.md", "view_node_verify.md"],
stats={}, failure_spans=[], success_spans=[],
)
store = MagicMock()
store.read_prompt.return_value = "## 输出格式\nfixed\nbody"
llm = _make_fake_llm('{"suggestions":[],"edits":[]}')
record = asyncio.run(evolve_single_tool(
llm, pack, store, _PROMPTS, "v1", 5,
))
import json
parsed = json.loads(record.evolved_content)
assert "extract" in parsed and "verify" in parsed
class TestConsolidateAppendix:
def test_single_note_passthrough(self):
llm = _make_fake_llm("")
result = asyncio.run(consolidate_appendix(llm, ["note1"]))
assert result == ["note1"]
def test_exception_returns_original(self):
llm = AsyncMock()
llm.chat.side_effect = RuntimeError("boom")
result = asyncio.run(consolidate_appendix(llm, ["a", "b", "c"]))
assert result == ["a", "b", "c"]
```
- [ ] **Step 2: Run test — expect FAIL**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_evolve.py::TestEvolveSingleSkill tests/unit/test_evolve.py::TestEvolveSystemPrompt tests/unit/test_evolve.py::TestEvolveSingleTool tests/unit/test_evolve.py::TestConsolidateAppendix -v`
- [ ] **Step 3: Implement per-target evolution**
从 TRM4 `evolve.py` 迁移:
- `_run_patch_evolution_loop`async 化,`range(2)` 两轮,三种失败反馈)
- `_build_lapse_only_attempt`(合成 `applied_append` report
- `evolve_single_skill`(三分支:lapse-only/rewrite/patch + appendix 追加 + consolidation
- `evolve_system_prompt`(无 lapse、无 rewrite、无 appendix
- `evolve_single_tool`extract+verify 合池 `_src` 标记、shared budget、JSON evolved_content
- `consolidate_appendix`async 化,四守卫)
- `rewrite_from_suggestions`async 化,3 个拒绝条件)
- `_append_lapse_with_consolidation``>= threshold` 触发、G4 `>=` 拒绝等长)
**关键变更**
- 全部 `client.chat()``await llm.chat(messages)`
- `response.choices[0].message.content``response.content`
- `skills_dir / target_file``skill_store.read_skill(target_file)`
- `prompts_dir / filename``prompt_store.read_prompt(filename)`
- 版本写入(`advance_version`/copytree)全部移除——返回 `EvolutionRecord`
- `run_evolution` 编排移除——per-target 函数是最高粒度
**保真校验**
- Skill 三分支精确条件和行为
- `_build_lapse_only_attempt` 合成 `applied_append` 状态
- rank_and_clip 三级降级
- Tool `_src` 标记合池/拆回
- consolidate 四守卫(G4 在调用方)
- rewrite 长度限制(重写不得长于原文)、异常类型(仅捕 ValueError/KeyError/TypeError/AttributeError
- 两轮重试反馈文本
- [ ] **Step 4: Run test — expect PASS**
Run: `conda activate Video-Tree-TRM & pytest tests/unit/test_evolve.py -v`
- [ ] **Step 5: Commit**
```
git add core/evolution/evolve.py tests/unit/test_evolve.py
git commit -m "feat(evolution): evolve.py per-target evolution — skill/system/tool (#9)"
```
---
### Task 9: __init__.py + 集成 + lint
**Files:**
- Modify: `core/evolution/__init__.py`
- Run: lint + dependency check
- [ ] **Step 1: Write __init__.py public API**
```python
"""core/evolution/ — 自进化循环决策内核。"""
from core.evolution.gate import compute_e_value, gate_decision, probation_verdict
from core.evolution.patch import (
apply_patch_with_report, append_to_appendix,
extract_appendix_notes, replace_appendix_notes,
replace_momentum, momentum_inner,
)
from core.evolution.validate import pair_block, classify_quadrants, compute_accuracy
from core.evolution.diagnose import run_diagnosis
from core.evolution.evolve import (
evolve_single_skill, evolve_system_prompt, evolve_single_tool,
edit_budget_at, resolve_skill_file,
)
__all__ = [
"compute_e_value", "gate_decision", "probation_verdict",
"apply_patch_with_report", "append_to_appendix",
"extract_appendix_notes", "replace_appendix_notes",
"replace_momentum", "momentum_inner",
"pair_block", "classify_quadrants", "compute_accuracy",
"run_diagnosis",
"evolve_single_skill", "evolve_system_prompt", "evolve_single_tool",
"edit_budget_at", "resolve_skill_file",
]
```
- [ ] **Step 2: Run lint**
```bash
conda activate Video-Tree-TRM & ruff check core/evolution/ --fix
conda activate Video-Tree-TRM & ruff format core/evolution/
```
- [ ] **Step 3: Dependency direction check**
```bash
# core/evolution/ 不得 import app/ 或 adapters/
grep -rn "from app\." core/evolution/ && echo "VIOLATION" || echo "OK"
grep -rn "from adapters\." core/evolution/ && echo "VIOLATION" || echo "OK"
grep -rn "import app\." core/evolution/ && echo "VIOLATION" || echo "OK"
grep -rn "import adapters\." core/evolution/ && echo "VIOLATION" || echo "OK"
```
Expected: 全部 OK
- [ ] **Step 4: Update ARCHITECTURE.md §3.1**
将 SkillStore/PromptStore/RunLog 的 Protocol 定义从"含写方法"修订为"core/ 只读,写方法在 app/ 实现类"。同步设计文档 §3 的修订说明。
- [ ] **Step 5: Run full test suite**
```bash
conda activate Video-Tree-TRM & pytest tests/unit/test_gate.py tests/unit/test_patch.py tests/unit/test_validate.py tests/unit/test_diagnose.py tests/unit/test_evolve.py tests/unit/test_evolution_types.py -v --tb=short
```
- [ ] **Step 6: Commit**
```
git add core/evolution/__init__.py research-wiki/ARCHITECTURE.md
git commit -m "feat(evolution): __init__.py public API + ARCHITECTURE.md Protocol update"
```
---
## Algorithm Fidelity Check
本计划涉及 4 项核心算法迁移:
| # | 算法 | 计划 Task | 保真措施 |
|---|------|----------|---------|
| 5 | CE-Gate e-process | Task 2 | 原样迁移 163 行,零有意变更;测试覆盖边界(W=L=0、负值、重 win/loss |
| 7 | 块顺序验证 | Task 4 | 纯决策函数提取;编排留 app/harness/Design B 约定) |
| 8 | 诊断瀑布 | Task 5-6 | 归因瀑布顺序、defect/lapse 分类、案例包选择规则逐条保真 |
| 9 | 进化 patch 引擎 | Task 3, 7-8 | patch.py 原样迁移;evolve.py 三分支/rank_clip/consolidate 四守卫逐条保真 |
不涉及的算法:#1-4(建树/检索器)、#6(信息阶梯,app/harness/)、#10mini-batchapp/harness/)、#11Agent Loop,已迁移)、#12(树环境语义搜索,已迁移)、#13(训练循环编排,app/harness/)。