Files
writer-work-flow/apps/api/ww_api/services/foreshadow_scan.py
Yaojia Wang 5fb7bfb1de feat: M3 — 伏笔账本 + 节奏引擎 + 大纲(含并发记账 bugfix)
- 伏笔账本:纯函数状态机(OPEN/PARTIAL/CLOSED/OVERDUE) + ForeshadowLedger repo;验收后到期扫描(BackgroundTask 自建 session 置 OVERDUE);登记/状态变更端点
- 节奏 + 三审齐:foreshadow-analyst + pace-checker 并入 LangGraph 并行审(REVIEW_SPECS),collect 分列落 chapter_reviews(conflicts/foreshadow_sug/pace),review SSE 加 foreshadow/pace 事件
- 大纲:outliner Agent 产 OutlineResult(含 foreshadow_windows),POST /outline 逐章 upsert outline 表;GET /foreshadow?status= 看板
- 前端:伏笔四泳道看板(OVERDUE 琥珀) + 大纲编辑器(窗口徽标) + 节奏节拍图(▁▃▅) + 审稿页消费 foreshadow/pace SSE
- bugfix(T3.8):并行三审共用请求 session 记账触发 'Session is already flushing' → foreshadow/pace 静默丢失;SqlAlchemyLedgerSink.record 改 add-only(靠端点/事务 commit),加并发回归测试
- M3 E2E:真实 DB + mock 网关零 token 走通 埋设→进展→验收后扫描 OVERDUE→看板 + 大纲含窗口 + 三审齐 SSE/留痕;E2E 暴露并钉住上述 bug
- 门禁绿:mypy 111 / pytest 228(0 xfailed) / alembic 无漂移;前端 gen:api/lint/tsc/vitest 69/build
2026-06-18 14:21:17 +02:00

91 lines
3.8 KiB
Python
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.

"""验收后伏笔到期扫描M3-b/dARCH §6.2, §7.4,不变量 #3
接 §5.5 验收事务后的 `TODO(M3)` 占位:验收**提交成功后**触发一次伏笔到期扫描,把
`current_ch > expected_close_to AND status≠CLOSED` 的伏笔确定性置 `OVERDUE`M3-d 纯
函数,非 Agent不变量 #3状态变更是确定性扫描而非 AI 静默写库)。
挂载方式端点FastAPI `BackgroundTasks` 在 accept 端点登记 `run_overdue_scan`。
BackgroundTask 在请求-response 发回、请求 session 关闭**之后**才跑——故本函数**自建新
session**(不复用请求 session并自己 `commit()`。
**持久性局限§7.4**BackgroundTasks 在 API 进程内跑,进程重启/崩溃会丢失未跑完的
任务——原型可接受,这里把触发/完成/失败都记结构化日志便于查错;**不引入 `jobs` 表**
(那是 M4/T4.1 的工作,本任务不越界)。
可测性:扫描逻辑抽成纯 async 函数 `run_overdue_scan(session_factory, ...)`,端点用
BackgroundTasks 调它;单测直接 `await` 它(注入 fake session 工厂)断言行被置 OVERDUE
**绝不真起不可控后台线程**。
"""
from __future__ import annotations
import uuid
from collections.abc import Callable
from contextlib import AbstractAsyncContextManager
from typing import Protocol
import structlog
from sqlalchemy.ext.asyncio import AsyncSession
from ww_core.domain import ForeshadowLedgerView, SqlForeshadowLedgerRepo
log = structlog.get_logger(__name__)
# 新建独立 session 的工厂:调用得到一个 async 上下文管理器(`async with` → AsyncSession
SessionFactory = Callable[[], AbstractAsyncContextManager[AsyncSession]]
class OverdueScanRepo(Protocol):
"""到期扫描对账本 repo 的最小依赖(仅 `scan_overdue`)。"""
async def scan_overdue(
self, project_id: uuid.UUID, *, current_chapter: int
) -> list[ForeshadowLedgerView]: ...
# repo 工厂:从新 session 造账本 repo。默认建 SQL 实现;测试注入 fake避免真连 DB
RepoFactory = Callable[[AsyncSession], OverdueScanRepo]
def _default_repo_factory(session: AsyncSession) -> OverdueScanRepo:
return SqlForeshadowLedgerRepo(session)
async def run_overdue_scan(
session_factory: SessionFactory,
*,
project_id: uuid.UUID,
chapter_no: int,
request_id: str | None = None,
repo_factory: RepoFactory = _default_repo_factory,
) -> int:
"""验收后伏笔到期扫描:新建 session → `scan_overdue` 置 OVERDUE → `commit()`。
返回被置 OVERDUE 的伏笔条数(供日志/测试断言)。任何异常被捕获并记错误日志后
吞掉——后台任务失败不应冒泡崩进程也不影响已成功的验收事务§7.4 局限可接受)。
`repo_factory` 是可注入缝:默认建 SQL 账本 repo测试注 fake直接 await、不联网
"""
try:
async with session_factory() as session:
repo = repo_factory(session)
changed = await repo.scan_overdue(project_id, current_chapter=chapter_no)
if changed:
await session.commit()
log.info(
"foreshadow_overdue_scan",
project_id=str(project_id),
chapter_no=chapter_no,
request_id=request_id,
overdue_count=len(changed),
overdue_codes=[v.code for v in changed],
)
return len(changed)
except Exception as exc: # noqa: BLE001 — 后台任务边界记错误、不冒泡崩进程§7.4)。
log.error(
"foreshadow_overdue_scan_failed",
project_id=str(project_id),
chapter_no=chapter_no,
request_id=request_id,
error=str(exc),
)
return 0