M2a:镜树域后端核心——树/节点/引文/归真/移栽全量 API 与 Kernel 完成回调(芽体固化/残答封存),8 用例钉死 PRD 场景 1/2/3/5/6/7/8

This commit is contained in:
chenxin committed 2026-08-21 23:00:07 +08:00
1 parent 1cd0069d65
commit e6a3b7121f
16 files changed
+2002 -74

No files matched your search

+7
View File
@@ -19,6 +19,13 @@ from app.models.imports import CharacterImportJob, ImportItem
from app.models.jingsheng import Custodianship, EmbryoDraft, IdentityRecord, RuntimeSeat
from app.models.lorebook import Lorebook, LorebookEntry
from app.models.mcp_servers import McpServer
from app.models.mirror_tree import (
Canonization,
Citation,
MirrorTree,
TreeNode,
TreeTransplant,
)
from app.models.memory import Memory, MemoryLink
from app.models.personality import PersonalityProposal, PersonalityVersion
from app.models.personality_emotion import CharacterEmotionState
+132
View File
@@ -0,0 +1,132 @@
"""镜树域模型(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)