From 0a2dd17b0bb19f3204e8e0d2d9199d942dadfbbb Mon Sep 17 00:00:00 2001 From: lhl Date: Thu, 13 Aug 2026 09:46:38 +0800 Subject: [PATCH] feat(writer): add WriterState (cross-chapter regeneration tracking) --- src/genesis/eval/scorer.py | 3 ++- src/genesis/writer/writer_state.py | 28 ++++++++++++++++++++++++++ tests/test_phase5_writer_state.py | 32 ++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 src/genesis/writer/writer_state.py create mode 100644 tests/test_phase5_writer_state.py diff --git a/src/genesis/eval/scorer.py b/src/genesis/eval/scorer.py index b77433c..d6b46df 100644 --- a/src/genesis/eval/scorer.py +++ b/src/genesis/eval/scorer.py @@ -12,7 +12,7 @@ LLM 语义维度(内容准确性/幻觉/规则遵守)通过 llm_evaluators from __future__ import annotations import re -from dataclasses import dataclass +from dataclasses import dataclass, field from genesis.data_models import StructuredSource from genesis.parsers.resolver import validate_source_uris @@ -41,6 +41,7 @@ class EvalReport: dimensions: list[DimensionScore] total_score: float passed: bool + failed_chapters: list[str] = field(default_factory=list) # 维度默认通过阈值 diff --git a/src/genesis/writer/writer_state.py b/src/genesis/writer/writer_state.py new file mode 100644 index 0000000..3a6630f --- /dev/null +++ b/src/genesis/writer/writer_state.py @@ -0,0 +1,28 @@ +"""Writer 跨章状态(Phase 5)。追踪各章版本/内容/最近评估结果。""" +from __future__ import annotations + +from genesis.writer.models import ChapterContent +from genesis.eval.scorer import EvalReport + + +class WriterState: + def __init__(self, chapter_order: list[str]) -> None: + self.versions: dict[str, int] = {cid: 0 for cid in chapter_order} + self.contents: dict[str, ChapterContent | None] = {cid: None for cid in chapter_order} + self.last_eval: dict[str, EvalReport | None] = {cid: None for cid in chapter_order} + + def record_success(self, content: ChapterContent) -> None: + self.versions[content.chapter_id] += 1 + self.contents[content.chapter_id] = content + + def record_eval(self, cid: str, report: EvalReport) -> None: + self.last_eval[cid] = report + + def needs_regeneration(self) -> list[str]: + out: list[str] = [] + for cid, content in self.contents.items(): + if content is None: + out.append(cid) + elif self.last_eval[cid] is not None and cid in self.last_eval[cid].failed_chapters: + out.append(cid) + return out diff --git a/tests/test_phase5_writer_state.py b/tests/test_phase5_writer_state.py new file mode 100644 index 0000000..dbbb0fb --- /dev/null +++ b/tests/test_phase5_writer_state.py @@ -0,0 +1,32 @@ +"""WriterState 测试(P5-T5)。""" +from genesis.writer.writer_state import WriterState +from genesis.writer.models import ChapterContent +from genesis.eval.scorer import EvalReport + + +def _content(cid, version=1): + return ChapterContent(chapter_id=cid, version=version, title="x", blocks=[]) + + +def _report(failed): + return EvalReport( + dimensions=[], total_score=0.0, passed=False, failed_chapters=failed, + ) + + +def test_initial_state_empty(): + st = WriterState(["a", "b"]) + assert st.needs_regeneration() == ["a", "b"] + + +def test_record_success_clears_need(): + st = WriterState(["a"]) + st.record_success(_content("a")) + assert st.needs_regeneration() == [] + + +def test_failed_eval_marks_regeneration(): + st = WriterState(["a"]) + st.record_success(_content("a")) + st.record_eval("a", _report(["a"])) + assert st.needs_regeneration() == ["a"]