- 伏笔账本:纯函数状态机(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
91 lines
3.8 KiB
Python
91 lines
3.8 KiB
Python
"""验收后伏笔到期扫描(M3-b/d;ARCH §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
|