133 lines
6.7 KiB
Python
133 lines
6.7 KiB
Python
"""镜树域模型(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)
|