"""大纲生成的注入上下文组装(确定性序列化文本;ARCH §5.4 outliner reads / §6.2)。 把 projects 设定 + 已登记伏笔 + 人物 + 世界观确定性序列化成纯文本喂给 outliner—— **无时间戳/无 UUID**(缓存前缀稳定,不变量 #9),逐项排序保证同输入同输出(可单测)。 outliner 据此排出分章节拍 + 伏笔回收窗口(关联本章与伏笔,§6.2)。 注:reads=["projects","foreshadow","characters","world_entities"](C6 outliner_spec), 本组装恰好覆盖这四源,不读 digest/outline(排大纲是从设定生成、非续写既有章)。 """ from __future__ import annotations from collections.abc import Sequence from ww_core.domain import ForeshadowLedgerView, ProjectView from ww_core.domain.repositories import CharacterView, WorldEntityView def _project_block(project: ProjectView) -> str: lines = [f"标题:{project.title}"] if project.genre: lines.append(f"类型:{project.genre}") if project.logline: lines.append(f"一句话简介:{project.logline}") if project.premise: lines.append(f"前提:{project.premise}") if project.theme: lines.append(f"主题:{project.theme}") if project.selling_points: lines.append("卖点:" + "、".join(project.selling_points)) if project.structure: lines.append(f"结构:{project.structure}") return "【作品设定】\n" + "\n".join(lines) def _foreshadow_block(foreshadow: Sequence[ForeshadowLedgerView]) -> str: if not foreshadow: return "【已登记伏笔】\n(无)" rows = sorted(foreshadow, key=lambda f: f.code) lines = [] for f in rows: window = "" if f.expected_close_from is not None or f.expected_close_to is not None: lo = f.expected_close_from if f.expected_close_from is not None else "?" hi = f.expected_close_to if f.expected_close_to is not None else "?" window = f"(回收窗口 {lo}-{hi})" plant = f"埋设第{f.planted_at}章 " if f.planted_at is not None else "" lines.append(f"- [{f.code}] {f.title} {plant}{window}".rstrip()) return "【已登记伏笔】\n" + "\n".join(lines) def _characters_block(characters: Sequence[CharacterView]) -> str: if not characters: return "【人物】\n(无)" rows = sorted(characters, key=lambda c: c.name) lines = [] for c in rows: role = f"({c.role})" if c.role else "" motive = f" 动机:{c.motive}" if c.motive else "" lines.append(f"- {c.name}{role}{motive}".rstrip()) return "【人物】\n" + "\n".join(lines) def _world_block(entities: Sequence[WorldEntityView]) -> str: if not entities: return "【世界观】\n(无)" rows = sorted(entities, key=lambda e: (e.type, e.name)) lines = [f"- [{e.type}] {e.name}" for e in rows] return "【世界观】\n" + "\n".join(lines) def build_outline_context( *, project: ProjectView, foreshadow: Sequence[ForeshadowLedgerView], characters: Sequence[CharacterView], world_entities: Sequence[WorldEntityView], ) -> str: """组装 outliner 注入上下文(确定性文本,纯函数)。""" return "\n\n".join( [ _project_block(project), _foreshadow_block(foreshadow), _characters_block(characters), _world_block(world_entities), ] )