"""镜树域模型(data-model.md §2):主题镜树、节点、引文锚、归真记录、移栽谱系。 不变量(应用层强制 + 测试钉死): - 候答重生成=插入新行,旧节点永不 UPDATE 内容或状态回退; - 后代节点绑定用户实际采用的候答(问节默认挂工作入口); - working_entry 变更不触碰 canonizations(采用与归真解耦); - canonizations 不可变,正影=新版本节点 + 新归真记录。 """ from datetime import datetime from sqlalchemy import DateTime, ForeignKey, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column from app.core.ids import ( new_canonization_id, new_citation_id, new_mirror_tree_id, new_tree_node_id, new_transplant_id, ) from app.core.time import utc_now from app.db.base import Base class MirrorTree(Base): """主题镜树:一条归真主脉 + 多条推演枝 + 一个工作入口。""" __tablename__ = "mirror_trees" id: Mapped[str] = mapped_column(String(80), primary_key=True, default=new_mirror_tree_id) character_id: Mapped[str] = mapped_column( String(80), ForeignKey("characters.id"), nullable=False, index=True ) title: Mapped[str] = mapped_column(String(300), nullable=False) # active / archived status: Mapped[str] = mapped_column(String(40), nullable=False, default="active", index=True) # 默认界域(reality / persistent_world / simulation),问节未显式指定时继承 default_realm: Mapped[str] = mapped_column(String(40), nullable=False, default="reality") # 采用:当前阅读与继续生长的工作入口(answer/continuation 节点) working_entry_node_id: Mapped[str | None] = mapped_column(String(80)) # 移栽来源谱系:{source_tree_id, source_node_id};原生树为空 transplanted_from_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}") created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utc_now, onupdate=utc_now ) class TreeNode(Base): """统一节点表:问节 / 候答 / 续思。 拓扑:answer.parent=question;question.parent=所依据的候答(默认工作入口); continuation.parent=被延展的候答或续思。编辑已发送内容=新建分叉节点(supersedes 指旧节点)。 """ __tablename__ = "tree_nodes" __table_args__ = ( UniqueConstraint("tree_id", "supersedes_node_id", name="uq_tree_node_supersedes"), ) id: Mapped[str] = mapped_column(String(80), primary_key=True, default=new_tree_node_id) tree_id: Mapped[str] = mapped_column(String(80), ForeignKey("mirror_trees.id"), index=True) parent_node_id: Mapped[str | None] = mapped_column( String(80), ForeignKey("tree_nodes.id"), index=True ) # question / answer / continuation(契约词典 §9.1) node_type: Mapped[str] = mapped_column(String(40), nullable=False, index=True) # budding / sealed_residual / solidified(契约词典 §9.2) status: Mapped[str] = mapped_column(String(40), nullable=False, default="budding", index=True) # user / character(问节作者为 user;候答为 character) author: Mapped[str] = mapped_column(String(40), nullable=False, default="user") content: Mapped[str] = mapped_column(Text, nullable=False, default="") # 节点界域(契约词典 §12);默认继承树默认值 realm: Mapped[str] = mapped_column(String(40), nullable=False, default="reality") # 生成元数据:{model, run_id, trace_id, instructions} generation_meta_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}") # 正影/编辑分叉:新节点指向被替代的旧节点(旧节点原样保留) supersedes_node_id: Mapped[str | None] = mapped_column(String(80)) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), default=utc_now, onupdate=utc_now ) class Citation(Base): """引文锚:稳定引用 + 原文快照。来源正影时旧锚不静默替换,仅提示后继版本。""" __tablename__ = "citations" id: Mapped[str] = mapped_column(String(80), primary_key=True, default=new_citation_id) node_id: Mapped[str] = mapped_column(String(80), ForeignKey("tree_nodes.id"), index=True) # text_range / image_region / pdf_region / attachment / av_segment(契约词典 §9.5) target_type: Mapped[str] = mapped_column(String(40), nullable=False) # 定位信息:{文本区间 / 页码区域 / 时间段等} locator_json: Mapped[str] = mapped_column(Text, nullable=False, default="{}") # 引用时的原文/媒体快照(来源变化不被覆盖) snapshot: Mapped[str] = mapped_column(Text, nullable=False, default="") source_id: Mapped[str] = mapped_column(String(200), nullable=False) source_version: Mapped[int] = mapped_column(nullable=False, default=1) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) class Canonization(Base): """归真记录(不可变):路径进入明确的 canonical_scope;主脉=同作用域记录的路径并集。""" __tablename__ = "canonizations" id: Mapped[str] = mapped_column(String(80), primary_key=True, default=new_canonization_id) tree_id: Mapped[str] = mapped_column(String(80), ForeignKey("mirror_trees.id"), index=True) character_id: Mapped[str] = mapped_column(String(80), index=True) # reality / persistent_world / simulation(契约词典 §9.4) scope: Mapped[str] = mapped_column(String(40), nullable=False, index=True) world_id: Mapped[str | None] = mapped_column(String(80)) # 路径端点(answer/continuation 节点) endpoint_node_id: Mapped[str] = mapped_column(String(80), index=True) # 端点祖先链快照(root→endpoint 的节点 ID 序列) path_nodes_json: Mapped[str] = mapped_column(Text, nullable=False, default="[]") confirmed_by: Mapped[str] = mapped_column(String(80), nullable=False) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now) class TreeTransplant(Base): """移栽谱系:分支复制为新树,保留来源引用;树间只引用不物理合并。""" __tablename__ = "tree_transplants" id: Mapped[str] = mapped_column(String(80), primary_key=True, default=new_transplant_id) source_tree_id: Mapped[str] = mapped_column(String(80), index=True) source_node_id: Mapped[str] = mapped_column(String(80)) new_tree_id: Mapped[str] = mapped_column(String(80), index=True) created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)