"""续审 Agent 的结构化输出 schema（ARCH §5.4 continuity 行 / §6.1 冲突分类）。

snake_case，Pydantic v2。网关经 instructor 保证产出符合这些 schema（C1/§4.4）。

不变量 #3：四审只读——审稿期只产**冲突清单**，不产 digest（digest 在验收时
从终稿另提，见不变量 #4 / ARCH §5.5）。
"""

from __future__ import annotations

from typing import Literal

from pydantic import BaseModel, Field

# ARCH §6.1 五类冲突；受限枚举，便于前端就地裁决分类与统计。
ConflictType = Literal[
    "性格漂移",
    "能力不符",
    "设定违例",
    "地理矛盾",
    "时间线倒错",
]


class Conflict(BaseModel):
    """单条一致性冲突（ARCH §6.1）。"""

    type: ConflictType = Field(description="冲突分类（五类之一）")
    where: str = Field(description="本章定位：哪一段/哪句触发冲突")
    refs: list[str] = Field(
        default_factory=list,
        description="冲突来源引用：相关章节号、设定项或人物卡条目",
    )
    suggestion: str = Field(description="改法建议")
    # 一键采纳补丁对（可缺）：可由局部改写修复时给出，供前端把 original→replacement
    # find-replace 进终稿（作者点「采纳改法」触发，仍只读——审稿只提议、不改稿）。
    # 不可定位到单一连续片段（跨段/缺设定等）则两者留空，回退到作者手改。
    original: str | None = Field(
        default=None,
        description="可一键采纳时：从本章草稿**逐字摘录**的精确原文最小句段（含标点，不得改写）；不可定位则留空",
    )
    replacement: str | None = Field(
        default=None,
        description="可一键采纳时：对应 original 的改后文本（同粒度最小句段）；不可定位则留空",
    )


class ContinuityReview(BaseModel):
    """续审结构化产出：仅冲突清单（无 digest）。

    digest 不在审稿期产——那是验收时从**终稿**另提（不变量 #4）。
    """

    conflicts: list[Conflict] = Field(
        default_factory=list,
        description="检出的一致性冲突；无冲突时为空列表",
    )


# ---- 大纲 Agent 结构化输出（ARCH §5.4 outliner 行 / §6.2 伏笔窗口）----


class ForeshadowWindow(BaseModel):
    """伏笔回收窗口：关联本章与某条伏笔（ARCH §6.2）。

    排大纲时把伏笔的「埋设章 → 期望回收区间」绑到具体章节；接近回收窗口的章节
    提示作者推进/收束。下界/上界可缺省（仅埋设、尚未排定回收时间）。
    """

    code: str = Field(description="伏笔编码（关联 foreshadow.code）")
    plant_chapter: int | None = Field(
        default=None, description="埋设章号（伏笔在本章/此章首次出现）"
    )
    expected_close_from: int | None = Field(default=None, description="期望回收区间下界（章号）")
    expected_close_to: int | None = Field(default=None, description="期望回收区间上界（章号）")


class OutlineChapter(BaseModel):
    """单章大纲：章号 + 节拍清单 + 关联伏笔窗口。"""

    no: int = Field(description="章号")
    beats: list[str] = Field(default_factory=list, description="本章节拍/情节要点（有序）")
    foreshadow_windows: list[ForeshadowWindow] = Field(
        default_factory=list,
        description="本章关联的伏笔回收窗口；无则空列表",
    )


class OutlineResult(BaseModel):
    """大纲结构化产出：分卷分章的章节大纲清单（ARCH §5.4 writes=outline）。

    声明式 writes（不变量 #3）：本结构是排大纲的纯产物，落 `outline` 表经验收/
    端点（T3.5），不在 agent 节点直接写库。
    """

    chapters: list[OutlineChapter] = Field(
        default_factory=list,
        description="分章大纲；无则空列表",
    )


# ---- 伏笔续审 Agent 结构化输出（ARCH §5.4 foreshadow-analyst 行 / §6.2）----


class ForeshadowSuggestion(BaseModel):
    """单条伏笔建议：本章新埋线 或 疑似回收（ARCH §6.2）。

    只读建议——作者在验收裁决里确认后才登记/改状态（不变量 #3）；本审不写库。
    `code` 可缺（新埋线尚无编码时由作者命名）；`where` 给本章定位。
    """

    code: str | None = Field(
        default=None, description="伏笔编码（关联已登记 foreshadow.code；新埋可缺）"
    )
    title: str = Field(description="伏笔标题/一句话描述")
    where: str | None = Field(default=None, description="本章定位：哪一段埋下/疑似回收")
    note: str | None = Field(default=None, description="建议说明（埋设要点 / 回收理由）")


class ForeshadowReview(BaseModel):
    """伏笔续审结构化产出：本章新埋 + 疑似回收两组建议（ARCH §5.4 / §6.2）。

    只读建议清单（不变量 #3）；登记/状态变更经验收裁决（作者确认）。
    """

    planted: list[ForeshadowSuggestion] = Field(
        default_factory=list,
        description="本章疑似新埋的伏笔；无则空列表",
    )
    resolved: list[ForeshadowSuggestion] = Field(
        default_factory=list,
        description="本章疑似回收/收束的伏笔；无则空列表",
    )


# ---- 节奏续审 Agent 结构化输出（ARCH §5.4 pace-checker 行 / §6.4）----


class PaceIssue(BaseModel):
    """单条节奏问题：注水段定位 + 原因（ARCH §6.4）。"""

    where: str = Field(description="本章定位：哪一段疑似注水/拖沓")
    reason: str = Field(description="判定理由（信息密度低 / 重复 / 偏题等）")


class PaceReview(BaseModel):
    """节奏续审结构化产出：注水段 + 章末钩子 + 爽点节拍图（ARCH §5.4 / §6.4）。

    只读（不变量 #3）。`beat_map` 是逐段节拍强度序列（整数），供前端 ▁▃▅ 可视化。
    """

    water: list[PaceIssue] = Field(
        default_factory=list,
        description="疑似注水/拖沓段；无则空列表",
    )
    hook: bool = Field(default=False, description="章末是否有有效钩子（悬念/反转/期待）")
    beat_map: list[int] = Field(
        default_factory=list,
        description="逐段爽点节拍强度序列（整数，供前端节拍图可视化）",
    )


# ---- 文风提取轨结构化输出（ARCH §5.4 style-auditor「提取」轨 / §6.9）----


class StyleDimension(BaseModel):
    """单维文风指纹：维度名 + 取值 + 原文证据引用（ARCH §6.9）。

    每维必须带原文证据（quotes），便于作者核验「这条文风结论来自哪句」。
    """

    name: str = Field(description="文风维度名（如句长节奏 / 叙事人称 / 用词风格）")
    value: str = Field(description="该维度在样本中的取值/概括")
    evidence: list[str] = Field(
        default_factory=list,
        description="原文证据：支撑该维度判定的样本原句摘录",
    )


class StyleFingerprintResult(BaseModel):
    """文风指纹提取产出：16 维文风指纹清单（ARCH §5.4 提取轨）。

    声明式 writes（不变量 #3）：本结构是从样本提取的纯产物，落 `style_fingerprint` 表
    经端点（T4.3），不在 agent 节点直接写库。16 维 = 9 通用 + 7 中文网文专有。
    """

    dimensions: list[StyleDimension] = Field(
        default_factory=list,
        description="文风维度清单（每维带原文证据）；无则空列表",
    )


# ---- 文风漂移轨结构化输出（ARCH §5.4 style-auditor「打分」轨 = 第四审）----


class StyleDriftSegment(BaseModel):
    """单个漂移段：原文锚 + 段索引 + 相似度分 + 可选标签（ARCH §5.4 打分轨）。

    `text` 是从本章草稿**逐字摘录**的该漂移段原文（含标点，不得改写）——前端据此用
    内容匹配在终稿里定位回炉目标（**内容锚**，不靠位置 idx，避免分段方式不一致导致定位
    到错段 / 越界静默失败）。`idx` 仅供前端展示排序（0 起）；`score` 是该段相对文风指纹
    的相似度（0–100，越低越偏）；`label` 可缺（如「机翻腔」「叙述拖沓」等漂移类型说明）。
    """

    idx: int = Field(description="本章段索引（0 起，仅供展示排序）")
    text: str = Field(
        default="",
        description="该漂移段从本章草稿**逐字摘录**的原文（含标点，不得改写）；供前端内容锚定位回炉目标",
    )
    score: int = Field(description="该段相对文风指纹的相似度（0–100，越低越偏离）")
    label: str | None = Field(default=None, description="漂移类型标签（如「机翻腔」）；可缺")


class StyleDriftReview(BaseModel):
    """文风漂移续审结构化产出：整体相似度 + 低相似段（ARCH §5.4 / 第四审）。

    只读（不变量 #3）。`score` 是整章相对文风指纹的整体相似度（0–100）。
    **无指纹优雅降级**：材料中无文风指纹时返回 `score=100, segments=[]`（默认值），
    不报错、不阻塞其余审（§5.2 失败隔离之外的内容性兜底）。
    """

    score: int = Field(default=100, description="整章相对文风指纹的整体相似度（0–100）")
    segments: list[StyleDriftSegment] = Field(
        default_factory=list,
        description="低相似度（疑似漂移）段清单；无则空列表（含无指纹降级）",
    )


# ---- 人物塑造续审结构化输出（灵感③ · advisory 单维；仅建议、不阻断验收）----


class CharacterizationIssue(BaseModel):
    """单条人物塑造问题：角色 + 维度 + 定位 + 逐字引用 + 诊断 + 建议 + 置信度。

    以人物卡 `motive`/`appearance` 为客观锚点。`quote` 是从本章草稿**逐字摘录**的支撑
    片段（内容锚，不得改写）；`confidence` 供作者过滤主观/边缘诊断（对抗 LLM-judge 偏见）。
    全字段给默认值守解析韧性（畸形/缺字段不炸）。
    """

    character: str = Field(
        default="", description="涉及的角色名（尽量对齐人物卡 name；无法归属留空）"
    )
    aspect: str = Field(
        default="",
        description="塑造维度（动机一致性 / 声音辨识度 / 行为逻辑 / 外貌一致性 / 扁平化 等）",
    )
    where: str = Field(default="", description="本章定位：哪一段/哪个场景体现该问题")
    quote: str = Field(
        default="",
        description="从本章草稿**逐字摘录**的支撑片段（含标点，不得改写）；供作者内容锚定位",
    )
    diagnosis: str = Field(default="", description="诊断说明：为何此处人物塑造偏弱/失真")
    suggestion: str = Field(default="", description="改进建议（方向性，不含改写后的正文）")
    confidence: float = Field(
        default=0.0, description="置信度 0.0–1.0（越确信越高，主观/边缘给低值，供作者过滤）"
    )


class CharacterizationReview(BaseModel):
    """人物塑造续审结构化产出：只读建议清单（advisory，不阻断验收，不变量 #3）。

    **带默认值降级**：材料无人物卡 / 无明显问题时返回 `issues=[]`（默认值），不报错、
    不阻塞其余审（§5.2 失败隔离之外的内容性兜底，仿 `StyleDriftReview`）。仅建议——
    不进 conflicts、不碰验收 gate（灵感 D2 advisory）。
    """

    issues: list[CharacterizationIssue] = Field(
        default_factory=list,
        description="人物塑造问题清单（每条带角色/维度/定位/逐字引用/置信度）；无则空列表",
    )


# ---- worldbuilder 结构化输出（ARCH §5.4 worldbuilder 行 / §6.5 / §4.5）----


class WorldEntityCard(BaseModel):
    """单个世界观实体（ARCH §5.4：`{type,name,rules}` 硬规则显式）。

    `type`（势力/地理/力量体系/物品…）+ `name` + `rules`（**显式硬规则**清单：
    不可违背的设定，供后续 continuity 续审引用比对）。映射 `world_entities` 表
    （type/name 列 + rules 入 JSONB）；落库经入库端点（T5.2，不变量 #3）。
    """

    type: str = Field(description="实体类型（势力 / 地理 / 力量体系 / 物品 / 概念 等）")
    name: str = Field(description="实体名")
    rules: list[str] = Field(
        default_factory=list,
        description="该实体的硬规则清单（显式不可违背设定，供 continuity 校验引用）",
    )


class WorldGenResult(BaseModel):
    """worldbuilder 结构化产出：世界观实体清单（ARCH §5.4 writes=world_entities）。

    声明式 writes（不变量 #3）：本结构是 worldbuilder 的纯产物，落 `world_entities` 表
    经入库端点（T5.2），不在 agent 节点直接写库。
    """

    entities: list[WorldEntityCard] = Field(
        default_factory=list,
        description="生成的世界观实体清单；无则空列表",
    )


# ---- character-gen 结构化输出（ARCH §5.4 character-gen 行 / §6.5 / §4.5）----


class CharacterRelation(BaseModel):
    """单条人物关系（写入 `characters.relations` JSONB 冗余）。

    `name`（关系对象角色名）+ `kind`（宿敌 / 师徒 / CP / 同盟 …）+ 可选 `note`。
    双向边由编排器在入库时统一维护（§4.5 三关键能力之三），本 schema 仅声明本侧。
    """

    name: str = Field(description="关系对象的角色名")
    kind: str = Field(description="关系类型（宿敌 / 师徒 / CP / 同盟 / 工具人 等）")
    note: str | None = Field(default=None, description="关系说明（可缺）")


class CharacterCard(BaseModel):
    """单张结构化角色卡（ARCH §5.4 character-gen 行：name/role/traits/backstory/arc/...）。

    映射 `characters` 表列（name/role/backstory + traits/arc/speech_tics/tags/relations
    入 JSONB）；落库经入库端点（T5.2，不变量 #3）。`role` = 角色定位（主角/CP/对手/导师/
    工具人，§4.5 网文专属）。
    """

    name: str = Field(description="角色名（取名风格契合世界观）")
    role: str = Field(description="角色定位（主角 / CP / 对手 / 导师 / 工具人 等）")
    traits: list[str] = Field(
        default_factory=list,
        description="性格特质清单（仅核心-表层-阴影三层；动机/欲望/恐惧落 motive）",
    )
    appearance: str = Field(
        default="",
        description="外貌形象（身形/五官/气质/标志性外观/常着装扮）；缺则空串（守解析韧性）",
    )
    motive: str = Field(
        default="",
        description="核心动机/欲望/恐惧（驱动行动的唯一内驱，arc 由此推动）；缺则空串",
    )
    backstory: str = Field(description="背景故事（出身、关键经历、创伤/转折点）")
    arc: str = Field(description="人物弧光（起点 → 转变 → 终点，与剧情挂钩，由 motive 推动）")
    speech_tics: list[str] = Field(
        default_factory=list,
        description="口癖/语言风格（用词偏好、口头禅，喂给文风一致性）",
    )
    tags: list[str] = Field(
        default_factory=list,
        description="人设标签/萌点（网文专属，辅助检索与差异化）",
    )
    relations: list[CharacterRelation] = Field(
        default_factory=list,
        description="关系网：与已有/同批角色建边；无则空列表",
    )


class CharacterGenResult(BaseModel):
    """character-gen 结构化产出：一组角色卡（ARCH §5.4 writes=characters / §4.5 群像）。

    声明式 writes（不变量 #3）：本结构是 character-gen 的纯产物，落 `characters` 表
    经入库端点（T5.2），且**入库前过编排器 continuity 校验**（§6.5）。单生成 → 1 张卡；
    批量群像 → 多张（防雷同：生成时注入「已生成卡 + 已有角色」要求差异化）。
    """

    cards: list[CharacterCard] = Field(
        default_factory=list,
        description="生成的角色卡清单；无则空列表",
    )


# ---- 创作工具箱 · 脑洞生成器结构化输出（T6 通用生成器框架 · brainstorm）----


class Idea(BaseModel):
    """单条脑洞：前提 + 钩子 + 适配题材（创作工具箱最简生成器产物）。

    纯预览产物——不映射任何业务表、不入库（`brainstorm_spec.writes=[]`，不变量 #3）。
    `genre_fit` 可缺（作者未限定赛道时由模型留空）。
    """

    premise: str = Field(description="一句话脑洞/设定前提")
    hook: str = Field(description="抓人钩子：为何让读者想追读")
    genre_fit: str | None = Field(default=None, description="适配题材/赛道（可缺）")


class IdeaListResult(BaseModel):
    """脑洞生成器结构化产出：一组脑洞（T6 · 通用生成器框架首个最简生成器）。

    声明式只读（不变量 #3）：纯预览，端点同步返回，不写任何业务表（仅记 ledger）。
    """

    ideas: list[Idea] = Field(
        default_factory=list,
        description="生成的脑洞清单；无则空列表",
    )


# ---- 创作工具箱 · 书名生成器（book-title；light；纯预览 writes=[]）----


class Title(BaseModel):
    """单个书名候选：书名 + 取名理由（创作工具箱 book-title 产物）。

    纯预览产物——不映射任何业务表、不入库（`book_title_spec.writes=[]`，不变量 #3）。
    """

    title: str = Field(description="书名候选")
    rationale: str = Field(description="取名理由：为何抓人/契合题材")


class TitleListResult(BaseModel):
    """书名生成器结构化产出：一组书名候选（T6 创作工具箱）。

    声明式只读（不变量 #3）：纯预览，端点同步返回，不写任何业务表。
    """

    titles: list[Title] = Field(
        default_factory=list,
        description="生成的书名候选清单；无则空列表",
    )


# ---- 创作工具箱 · 简介生成器（blurb；analyst；纯预览 writes=[]）----


class Blurb(BaseModel):
    """单条作品简介：正文 + 切入角度（创作工具箱 blurb 产物）。

    纯预览产物——不映射任何业务表、不入库（`blurb_spec.writes=[]`，不变量 #3）。
    """

    text: str = Field(description="简介正文（一段抓人文案）")
    angle: str = Field(description="切入角度：从哪个卖点/钩子切入")


class BlurbResult(BaseModel):
    """简介生成器结构化产出：多版差异化简介（T6 创作工具箱）。

    声明式只读（不变量 #3）：纯预览，端点同步返回，不写任何业务表。
    """

    variants: list[Blurb] = Field(
        default_factory=list,
        description="生成的简介变体清单；无则空列表",
    )


# ---- 创作工具箱 · 取名生成器（name；light；纯预览 writes=[]）----


class NameSuggestion(BaseModel):
    """单条命名建议：名字 + 类别 + 可选说明（创作工具箱 name 产物）。

    纯预览产物——不映射任何业务表、不入库（`name_spec.writes=[]`，不变量 #3）。
    `kind` 标注命名对象类别（人物/势力/地点/功法/物品…）。
    """

    name: str = Field(description="建议的名字")
    kind: str = Field(description="命名对象类别（人物 / 势力 / 地点 / 功法 / 物品 等）")
    note: str | None = Field(default=None, description="命名说明（取意/出处，可缺）")


class NameListResult(BaseModel):
    """取名生成器结构化产出：一组命名建议（T6 创作工具箱）。

    声明式只读（不变量 #3）：纯预览，端点同步返回，不写任何业务表。
    """

    names: list[NameSuggestion] = Field(
        default_factory=list,
        description="生成的命名建议清单；无则空列表",
    )


# ---- 创作工具箱 · 金手指生成器（golden-finger；writer；声明式 writes=world_entities）----


class GoldenFinger(BaseModel):
    """单个金手指系统：名称 + 机制 + 成长 + 限制（创作工具箱 golden-finger 产物）。

    映射 `world_entities`（力量体系类）；落库经入库端点（不变量 #3）。`limits` = 不可
    违背的能力边界/代价，供 continuity 续审引用比对（防能力不符）。
    """

    name: str = Field(description="金手指名称")
    mechanism: str = Field(description="核心机制：如何运作、触发条件")
    growth: str = Field(description="成长路径：随剧情如何升级/进阶")
    limits: str = Field(description="限制与代价：能力边界、副作用（防越界，供一致性校验）")


class GoldenFingerResult(BaseModel):
    """金手指生成器结构化产出：一组金手指系统（T6 创作工具箱）。

    声明式 writes（不变量 #3）：本结构是纯产物，落 `world_entities` 表经入库端点，
    不在 agent 节点直接写库。
    """

    systems: list[GoldenFinger] = Field(
        default_factory=list,
        description="生成的金手指系统清单；无则空列表",
    )


# ---- 创作工具箱 · 术语表生成器（glossary；analyst；声明式 writes=world_entities）----


class GlossaryTerm(BaseModel):
    """单条术语：名称 + 类型 + 定义 + 硬规则（创作工具箱 glossary 产物）。

    映射 `world_entities`（concept/术语类）；`rules` = 该术语的硬规则清单，
    对齐 `world_entities.rules`，供 continuity 续审引用比对（不变量 #3）。
    """

    name: str = Field(description="术语名")
    type: str = Field(description="术语类型（境界 / 功法 / 货币 / 度量 / 称谓 等）")
    definition: str = Field(description="术语定义（一句话释义）")
    rules: list[str] = Field(
        default_factory=list,
        description="该术语的硬规则清单（不可违背设定，供 continuity 校验引用）",
    )


class GlossaryResult(BaseModel):
    """术语表生成器结构化产出：一组术语（T6 创作工具箱）。

    声明式 writes（不变量 #3）：本结构是纯产物，落 `world_entities` 表经入库端点，
    不在 agent 节点直接写库。
    """

    terms: list[GlossaryTerm] = Field(
        default_factory=list,
        description="生成的术语清单；无则空列表",
    )


# ---- 创作工具箱 · 开篇生成器（opening；writer；纯预览 writes=[]）----


class Opening(BaseModel):
    """单版开篇正文（创作工具箱 opening 产物）。

    纯预览产物——不映射任何业务表、不入库（`opening_spec.writes=[]`，不变量 #3）。
    """

    text: str = Field(description="开篇正文（一段成稿文本）")


class OpeningResult(BaseModel):
    """开篇生成器结构化产出：多版开篇（T6 创作工具箱）。

    声明式只读（不变量 #3）：纯预览，端点同步返回，不写任何业务表。
    """

    variants: list[Opening] = Field(
        default_factory=list,
        description="生成的开篇变体清单；无则空列表",
    )


# ---- 创作工具箱 · 细纲生成器（fine-outline；analyst；声明式 writes=outline）----


class Scene(BaseModel):
    """单个场景节拍：序号 + 节拍 + 目的 + 冲突 + 钩子（创作工具箱 fine-outline 产物）。

    把某章 outline 的粗节拍展开为细粒度场景；落库经入库端点（不变量 #3）。
    """

    idx: int = Field(description="场景序号（章内有序，0/1 起均可）")
    beat: str = Field(description="场景节拍：本场发生什么")
    purpose: str = Field(description="叙事目的：推动主线/塑造人物/铺垫伏笔")
    conflict: str = Field(description="本场冲突/张力来源")
    hook: str = Field(description="场景钩子：驱动读者读下一场的悬念")


class DetailedOutlineResult(BaseModel):
    """细纲生成器结构化产出：某章展开的场景清单（T6 创作工具箱）。

    声明式 writes（不变量 #3）：本结构是纯产物，落 `outline` 表经入库端点，
    不在 agent 节点直接写库。
    """

    scenes: list[Scene] = Field(
        default_factory=list,
        description="展开的场景清单；无则空列表",
    )


# ---- 竞品快赢 · 续写生成器（continue；writer；纯预览 writes=[]）----


class ContinuationResult(BaseModel):
    """续写生成器结构化产出：承接前文的续写正文（Scope B 竞品快赢）。

    纯预览产物——不映射任何业务表、不入库（`continue_spec.writes=[]`，不变量 #3）。
    """

    text: str = Field(description="续写正文（承接前文，可直接作为下文草稿）")


# ---- 竞品快赢 · 扩写生成器（expand；writer；纯预览 writes=[]）----


class PolishResult(BaseModel):
    """扩写生成器结构化产出：在原文基础上扩写/丰富后的正文（Scope B 竞品快赢）。

    纯预览产物——不映射任何业务表、不入库（`expand_spec.writes=[]`，不变量 #3）。
    """

    text: str = Field(description="扩写后的正文（在原文基础上丰富细节/铺陈）")


# ---- 竞品快赢 · 降 AI 率生成器（de-ai；analyst；纯预览 writes=[]）----


class DeAiResult(BaseModel):
    """降 AI 率生成器结构化产出：去 AI 腔/口语化改写后的正文（Scope B 竞品快赢）。

    纯预览产物——不映射任何业务表、不入库（`de_ai_spec.writes=[]`，不变量 #3）。
    """

    text: str = Field(description="降 AI 率改写后的正文（去机翻腔、更自然的人写质感）")


# ---- 竞品快赢 · 拆书生成器（teardown；analyst；纯预览 writes=[]）----


class BookTeardownResult(BaseModel):
    """拆书生成器结构化产出：样本作品的结构化拆解（Scope B 竞品快赢）。

    F1：可经 ingest 端点把结论拍平为项目 `rules` 条目落库（`teardown_spec.writes=["rules"]`）；
    入库仍经白名单 gate（不变量 #3），预览（generate）不写库。
    """

    themes: list[str] = Field(
        default_factory=list,
        description="核心主题/立意清单；无则空列表",
    )
    archetypes: list[str] = Field(
        default_factory=list,
        description="人物原型/角色模板清单；无则空列表",
    )
    structure: str = Field(description="叙事结构概述（开篇/铺垫/高潮/收束的脉络）")
    hooks: list[str] = Field(
        default_factory=list,
        description="抓人钩子/爽点套路清单；无则空列表",
    )


# ---- 立项方案生成器（project-plan；analyst；纯预览 writes=()）----


class ProjectPlanResult(BaseModel):
    """立项方案生成器结构化产出：书级蓝图，供预填立项向导（灵感⑤）。

    纯预览产物——不映射任何业务表、不入库（`project_plan_spec.writes=()`，不变量 #3）。
    **全字段给默认值守解析韧性**（仿 `StyleDriftReview` 降级范式）：LLM 漏产任一字段
    不致整个方案解析失败——缺字段降级为空（书名候选空列表 / 文本字段空串），前端只回填
    非空交集、不覆盖作者已编辑值。
    """

    title_candidates: list[str] = Field(
        default_factory=list,
        description="书名候选清单（3–5 个差异化候选）；无则空列表",
    )
    setting: str = Field(default="", description="时空背景（时代/地域/世界底层规则概述）；缺则空串")
    narrative_structure: str = Field(
        default="", description="叙事结构（主线推进骨架：立钩/升级/转折节拍）；缺则空串"
    )
    story_core: str = Field(
        default="",
        description="故事核心（立意 + 核心冲突：主角要什么、挡路的是什么、代价）；缺则空串",
    )
    ending_design: str = Field(default="", description="结局设计（收束方向与情绪落点）；缺则空串")
    tone: str = Field(default="", description="基调（全书情绪底色与阅读质感）；缺则空串")
