"""报告流水线的阶段契约(第四批 T15/T16/T17 的共同接口)。 本文件由协调提交只定 **接口**;编排实现(assemble_stages / run_pipeline) 归 T15。T16 提供 vision_stage / diagnosis_stage,T17 提供 copy_stage / validation_stage——三个任务互不改对方文件,靠 Settings.report_pipeline_stages 里的 dotted path 动态装配。 设计动机(为什么动态 import):三个阶段模块由三个并行分支各自创建, 静态 import 会在任一分支缺文件时炸掉编排;按配置动态装配让每个分支 独立可测,合并后自动咬合。代价是 import-linter 看不到这条边——阶段模块 自身的依赖仍受契约约束,编排层注入的 session/gateway 都是既有合法依赖。 """ from __future__ import annotations from typing import Any, Protocol, runtime_checkable from sqlalchemy.ext.asyncio import AsyncSession from packages.llm.gateway import LLMGateway from packages.shared.config import Settings from packages.shared.models import RpInput, RpReport class ReportContext: """一个阶段的执行上下文。 不是 frozen:阶段要写 ``ctx.report.result[stage.name]`` 与 ``ctx.rp_input.derived_features``(中间态落库由编排层 flush)。 """ def __init__( self, *, report: RpReport, report_input: RpInput, session: AsyncSession, settings: Settings, gateway: LLMGateway | None = None, ) -> None: self.report = report self.report_input = report_input self.session = session self.settings = settings self.gateway = gateway @runtime_checkable class Stage(Protocol): """流水线阶段。name 同时是 result JSONB 里的产物键。 约定:阶段把自己的全部产物写进 ``ctx.report.result[self.name]``; 抛异常即整条流水线失败(编排层负责 tk_task→failed + refund)。 """ name: str async def run(self, ctx: ReportContext) -> None: ... #: T15 的 assemble_stages 按此清单 importlib 动态导入;缺失即 CONFIG_ERROR。 DEFAULT_STAGE_PATHS: tuple[str, ...] = ( "packages.report.vision_stage:VisionStage", "packages.report.diagnosis_stage:DiagnosisStage", "packages.report.copy_stage:CopyStage", "packages.report.validation_stage:ValidationStage", ) def stage_names(result: dict[str, Any]) -> list[str]: """result JSONB 里已出现的阶段键(诊断/调试用的小工具)。""" return [k for k in result if isinstance(result[k], dict)]