Files
hunjing/backend/app/models/mirror_tree.py
T

133 lines
6.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""镜树域模型(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)