From c9ffada503813fd99204c39c798dff50e26ff9ac Mon Sep 17 00:00:00 2001 From: Yaojia Wang Date: Wed, 8 Jul 2026 13:02:30 +0200 Subject: [PATCH] =?UTF-8?q?feat(orchestrator):=20=E7=BB=AD=E5=86=99?= =?UTF-8?q?=E9=93=BE=E5=89=8D=E4=B8=80=E7=AB=A0=E6=97=A0=E5=B7=B2=E9=AA=8C?= =?UTF-8?q?=E6=94=B6=E7=BB=88=E7=A8=BF=E5=8F=91=20warning=E2=80=94?= =?UTF-8?q?=E2=80=94=E9=9D=99=E9=BB=98=E4=B8=A2=E5=89=8D=E6=96=87=E5=BC=95?= =?UTF-8?q?=E5=AD=90=E5=8F=AF=E8=A7=82=E6=B5=8B=EF=BC=88CR-L1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/tests/test_chain_graph.py | 52 +++++++++++++++++++ .../core/ww_core/orchestrator/chain/nodes.py | 12 ++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/core/tests/test_chain_graph.py b/packages/core/tests/test_chain_graph.py index a89934c..6fd0763 100644 --- a/packages/core/tests/test_chain_graph.py +++ b/packages/core/tests/test_chain_graph.py @@ -13,6 +13,7 @@ from collections.abc import AsyncIterator from contextlib import asynccontextmanager from typing import Any +import structlog from langchain_core.runnables import RunnableConfig from langgraph.checkpoint.memory import MemorySaver from langgraph.types import Command @@ -446,6 +447,57 @@ async def test_write_chapter_continue_volume_injects_prior_accepted_text() -> No assert "前文正文" in write_input # build_continuation_context 的小节标题 +async def test_continue_volume_warns_when_prior_chapter_unaccepted() -> None: + """continue_volume 写第 2 章但第 1 章无已验收终稿 → 静默丢前文引子须发 warning(CR-L1)。""" + h = _make_harness(conflicts=[]) + # accepted_store 故意留空:第 1 章尚无已验收正文 → 续写丢失前文引子(静默降级)。 + state = initial_chain_state( + project_id=PROJECT, + user_id=USER, + start_chapter_no=2, + count=1, + chain_key="continue_volume", + ) + + with structlog.testing.capture_logs() as logs: + await write_chapter( + state, + gateway_builder=h["gateway_builder"], + session_factory=h["session_factory"], + memory_repos_factory=h["memory_repos_factory"], + chapter_repo_factory=h["chapter_repo_factory"], + assemble=h["assemble"], + ) + + warning = next(e for e in logs if e["event"] == "chain_prior_accepted_missing") + assert warning["log_level"] == "warning" + assert warning["chapter_no"] == 2 + + +async def test_continue_volume_first_chapter_does_not_warn() -> None: + """第 1 章前一章号 < 1,无前文属预期(非降级)→ 不应发缺前文 warning。""" + h = _make_harness(conflicts=[]) + state = initial_chain_state( + project_id=PROJECT, + user_id=USER, + start_chapter_no=1, + count=1, + chain_key="continue_volume", + ) + + with structlog.testing.capture_logs() as logs: + await write_chapter( + state, + gateway_builder=h["gateway_builder"], + session_factory=h["session_factory"], + memory_repos_factory=h["memory_repos_factory"], + chapter_repo_factory=h["chapter_repo_factory"], + assemble=h["assemble"], + ) + + assert not [e for e in logs if e["event"] == "chain_prior_accepted_missing"] + + async def test_continue_volume_chain_second_chapter_sees_first_chapter_text() -> None: """全图 E2E:continue_volume 两章——第 2 章请求上下文含第 1 章正文(不变量 #1/#5)。""" h = _make_harness(conflicts=[]) diff --git a/packages/core/ww_core/orchestrator/chain/nodes.py b/packages/core/ww_core/orchestrator/chain/nodes.py index 6efd4d2..66b8d12 100644 --- a/packages/core/ww_core/orchestrator/chain/nodes.py +++ b/packages/core/ww_core/orchestrator/chain/nodes.py @@ -325,7 +325,17 @@ async def _read_prior_accepted( if chapter_no <= 1: return "" view = await chapter_repo.latest_accepted(project_id, chapter_no - 1) - return view.content if view is not None else "" + if view is None: + # 静默降级须可观测(CR-L1):续写式链本章续接的前一章尚无已验收终稿——续写会丢失 + # 前文引子(降级为 build_continuation_context 的占位)。发 warning 便于查错,勿静默吞。 + log.warning( + "chain_prior_accepted_missing", + project_id=str(project_id), + chapter_no=chapter_no, + prior_chapter_no=chapter_no - 1, + ) + return "" + return view.content async def _commit(session: CommitSession) -> None: