"""逐条承接引文行为;不执行旧Skill脚本、不规范化正文。""" from dataclasses import asdict, replace import pytest from pydantic import TypeAdapter, ValidationError from muse.元数据.接口 import ( 值规则, 元数据错误, 合成结构, 字段定义, 引用规则, 类型定义, 结构定义, 读取内置种子, ) from muse.故事世界.接口 import ( 事实条目, 事实错误, 保留名称提及, 叙事窗口, 定位字面引文, 引用对象, 校验事实批次, 核对原文引文, 解析类型选择, ) from muse.正文写作.接口 import 换行节点, 文本节点, 正文草稿, 段落 原增量正文 = "林深把黑纹缠上手臂,异种核心在胸腔里低鸣。何岚站在舱门口没有说话。" 原抽取正文 = "林深走进舰桥,何岚把黑色钥匙交给他。" def 单段(内容): return 正文草稿((段落("p1", (文本节点(内容),)),)) @pytest.mark.case_id( "TC-3554e51a0d7a", environment="离线,固定原文与引用", when="解析事实或关系提案并解析其证据和端点。", contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md", ) def test_quote_not_found_fails_closed__3554e5(): with pytest.raises(事实错误) as 错误: 定位字面引文(单段(原增量正文), "正文里不存在的句子") assert 错误.value.错误码 == "FACT_QUOTE_NOT_FOUND" @pytest.mark.case_id( "TC-5058648ccd62", environment="离线,固定原文与引用", given="原增量BODY追加字面反斜杠n与原quote真实换行;另给格式1显式换行正文作对照", when="对固定格式1正文定位并核对引文", then=["字面反斜杠输入不改写,错误换行引文拒绝;真实换行正文可定位且码点区间准确"], contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md", ) def test_quote_is_normalized_before_matching__505864(): # 当前合成样本:格式1不允许把字面反斜杠改成真正换行。 escaped_body = 原增量正文 + "\\n舱灯闪了一下" quote = "\n舱灯闪了一下" with pytest.raises(事实错误) as 错误: 定位字面引文(单段(escaped_body), quote) assert 错误.value.错误码 == "FACT_QUOTE_NOT_FOUND" 字面证据 = 定位字面引文(单段(escaped_body), "\\n舱灯闪了一下") assert asdict(字面证据) == { "paragraph_id": "p1", "start": len(原增量正文), "end": len(escaped_body), "quote": "\\n舱灯闪了一下", } assert 核对原文引文(单段(escaped_body), 字面证据) == "\\n舱灯闪了一下" assert 单段(escaped_body).format_version == 1 原文 = 正文草稿((段落("p1", (文本节点(原增量正文), 换行节点(), 文本节点("舱灯闪了一下"))),)) 证据 = 定位字面引文(原文, quote) assert 核对原文引文(原文, 证据) == quote assert 证据.start == len(原增量正文) and 证据.end == len(原增量正文 + quote) @pytest.mark.case_id( "TC-9e18584a8a92", environment="离线,固定原文与引用", when="解析事实或关系提案并解析其证据和端点。", contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md", ) def test_normalize_rejects_fabricated_evidence__9e1858(): 原输入 = { "type": "item", "name": "黑钥匙", "brief": "钥匙", "fields": {}, "evidence": "正文没有这句", } with pytest.raises(事实错误) as 错误: 定位字面引文(单段(原抽取正文), 原输入["evidence"]) assert 错误.value.错误码 == "FACT_QUOTE_NOT_FOUND" @pytest.mark.case_id( "TC-1b9888594fd1", environment="离线,固定原文与引用", when="解析事实或关系提案并解析其证据和端点。", contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md", ) def test_normalize_binds_evidence_and_aliases__1b9888(): 原输入 = { "type": "人物", "name": "林深", "brief": "机师", "fields": {}, "evidence": "林深走进舰桥", } 种子 = next(s for s in 读取内置种子() if s.类型.type_id == "character") # 此旧场景中的中文类型名在输入目录登记;解析代码没有固定中文别名表。 类型 = replace(种子.类型, 名称="人物") 选中 = 解析类型选择(原输入["type"], (类型,)) 正文 = 单段(原抽取正文) 提案 = 事实条目( "proposal-lin", "character-lin", 选中.type_id, {"名称": 原输入["name"], "一句话摘要": 原输入["brief"], **原输入["fields"]}, (定位字面引文(正文, 原输入["evidence"]),), ) 结果 = 校验事实批次( (提案,), 作品ID="synthetic-work", 结构目录={"character": (类型, 合成结构(种子.结构))}, 正文=正文, 可用对象={}, ) assert 结果[0].type_id == "character" assert asdict(结果[0].evidence[0]) == { "paragraph_id": "p1", "start": 0, "end": len("林深走进舰桥"), "quote": "林深走进舰桥", } assert 核对原文引文(正文, 结果[0].evidence[0]) == "林深走进舰桥" assert 正文.format_version == 1 assert 结果[0].content["名称"] == "林深" 旧增量样本 = { "character_location_changed": ( {"characterName": "林深", "toLocation": "舱室底层"}, "异种核心在胸腔里低鸣", ), "character_knowledge_added": ( {"characterName": "何岚", "knowledge": "林深体内有异种核心"}, "何岚站在舱门口没有说话", ), "relationship_changed": ( {"fromName": "林深", "toName": "何岚", "relation": "互相提防"}, "何岚站在舱门口没有说话", ), "hook_advanced": ( { "hookId": "hook-voice", "action": "advanced", "dueWindow": {"fromChapter": 5, "toChapter": 8}, }, "异种核心在胸腔里低鸣", ), "timeline_event_added": ({"event": "隔离舱首次审讯结束"}, "何岚站在舱门口没有说话"), "setting_added": ({"factType": "污染规则", "text": "黑纹扩散不可逆"}, "林深把黑纹缠上手臂"), } 名称映射 = { "林深": "character-lin", "何岚": "character-he", "舱室底层": "location-bottom", "指挥舱": "location-command", "hook-voice": "hook-voice", "h1": "hook-one", } @pytest.fixture def 旧增量环境(): # 六行保留旧输入种类;它们仅是本用例的动态登记,不是生产类型白名单。 字符 = 值规则("text") def 引用(type_id): return 值规则("reference", 引用=引用规则((type_id,))) 角色, 地点, 伏笔 = 引用("character"), 引用("location"), 引用("foreshadowing") 字段表 = { "character_location_changed": [ ("角色", "characterName", 角色), ("目标地点", "toLocation", 地点), ], "character_knowledge_added": [ ("角色", "characterName", 角色), ("新增认知", "knowledge", 字符), ], "relationship_changed": [ ("甲方", "fromName", 角色), ("乙方", "toName", 角色), ("关系", "relation", 字符), ], "hook_advanced": [ ("伏笔", "hookId", 伏笔), ("动作", "action", 值规则("enum", 枚举=("planted", "advanced", "paid"))), ], "timeline_event_added": [("事件", "event", 字符)], "setting_added": [("设定类型", "factType", 字符), ("内容", "text", 字符)], } 结构目录 = {} for id_, fields in 字段表.items(): 家族 = "relation" if id_ == "relationship_changed" else "entity" 类型 = 类型定义(id_, id_, "world", 家族, (家族,)) 结构 = 结构定义( id_, id_, 1, tuple( 字段定义(id_ + "." + key, key, key, rule, 必填=True, 别名=(alias,)) for key, alias, rule in fields ), ) 结构目录[id_] = (类型, 合成结构(结构)) 对象 = { id_: 引用对象( id_, "synthetic-work", "character" if id_.startswith("character-") else "location" if id_.startswith("location-") else "foreshadowing", ) for id_ in 名称映射.values() } return { "作品ID": "synthetic-work", "结构目录": 结构目录, "正文": 单段(原增量正文), "可用对象": 对象, "章节顺序": {f"chapter-{i}": i for i in range(1, 13)}, } def 旧样本提案( 类型名="character_location_changed", *, payload=None, quote=None, delta_id="delta-1" ): 原字段, 原引文 = 旧增量样本.get(类型名, 旧增量样本["character_location_changed"]) 内容 = dict(原字段 if payload is None else payload) 窗口 = 内容.pop("dueWindow", None) # 已知旧名称在输入边界转换为固定对象ID;生产事实不由显示名拼接身份。 for key in ("characterName", "fromName", "toName", "toLocation", "hookId"): if key in 内容: 内容[key] = 名称映射[内容[key]] return 事实条目( delta_id, 内容.get("hookId", "object-" + delta_id), 类型名, 内容, (定位字面引文(单段(原增量正文), 原引文 if quote is None else quote),), instance_family="relation" if 类型名 == "relationship_changed" else "entity", planned_window=叙事窗口(f"chapter-{窗口['fromChapter']}", f"chapter-{窗口['toChapter']}") if 窗口 else None, ) @pytest.mark.case_id( "TC-d0e17d43037f", environment="离线,固定原文与引用", when="解析事实或关系提案并解析其证据和端点。", contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md", ) @pytest.mark.parametrize("delta_type", tuple(旧增量样本), ids=tuple(旧增量样本)) def test_each_delta_type_has_working_contract__d0e17d(旧增量环境, delta_type): 原字段, 引文 = 旧增量样本[delta_type] 提案 = 旧样本提案(delta_type, payload=原字段, quote=引文) 结果 = 校验事实批次((提案,), **旧增量环境) assert 结果[0].type_id == delta_type and 结果[0].object_id assert 结果[0].evidence[0].quote == 引文 @pytest.mark.case_id( "TC-e42f4bc89b7b", environment="离线,固定原文与引用", when="解析事实或关系提案并解析其证据和端点。", contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md", ) def test_subject_key_derivation__e42f4b(旧增量环境): 提案 = 旧样本提案( "relationship_changed", payload={"fromName": "林深", "toName": "何岚", "relation": "同盟"}, quote="何岚站在舱门口没有说话", ) 结果 = 校验事实批次((提案,), **旧增量环境)[0] assert (结果.content["甲方"], 结果.content["乙方"]) == ("character-lin", "character-he") hook = 旧样本提案( "hook_advanced", payload={"hookId": "hook-voice", "action": "planted"}, quote="异种核心在胸腔里低鸣", ) assert 校验事实批次((hook,), **旧增量环境)[0].object_id == "hook-voice" @pytest.mark.case_id( "TC-42dd3be8765a", environment="离线,固定原文与引用", when="解析事实或关系提案并解析其证据和端点。", contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md", ) def test_closed_field_set_and_type_enum__42dd3b(旧增量环境): extra = asdict(旧样本提案()) extra["extraField"] = 1 with pytest.raises(ValidationError): TypeAdapter(事实条目).validate_python(extra) bad_type = 旧样本提案("character_resurrected") with pytest.raises(事实错误) as 错误: 校验事实批次((bad_type,), **旧增量环境) assert 错误.value.错误码 == "FACT_TYPE_UNKNOWN" @pytest.mark.case_id( "TC-a30962eff4a0", environment="离线,固定原文与引用", when="解析事实或关系提案并解析其证据和端点。", contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md", ) def test_payload_contract_enforced_per_type__a30962(旧增量环境): missing = 旧样本提案(payload={"characterName": "林深"}) with pytest.raises(元数据错误) as 错误: 校验事实批次((missing,), **旧增量环境) assert 错误.value.错误码 == "FIELD_INVALID" assert 错误.value.上下文 == {"字段": ["目标地点"], "路径": "$"} action = 旧样本提案( "hook_advanced", payload={"hookId": "h1", "action": "detonated"}, quote="异种核心在胸腔里低鸣", ) with pytest.raises(元数据错误) as 错误: 校验事实批次((action,), **旧增量环境) assert 错误.value.错误码 == "FIELD_INVALID" assert 错误.value.上下文 == {"路径": "$.动作", "类型": "enum"} window = 旧样本提案( "hook_advanced", payload={ "hookId": "h1", "action": "planted", "dueWindow": {"fromChapter": 9, "toChapter": 3}, }, quote="异种核心在胸腔里低鸣", ) with pytest.raises(事实错误) as 错误: 校验事实批次((window,), **旧增量环境) assert 错误.value.错误码 == "FACT_TIME_INVALID" @pytest.mark.case_id( "TC-b430df49f1e4", environment="离线,固定原文与引用", when="解析事实或关系提案并解析其证据和端点。", contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md", ) def test_batch_rejects_duplicate_ids__b430df(旧增量环境): 首 = 旧样本提案(delta_id="delta-1") 后 = 旧样本提案(delta_id="delta-1", payload={"characterName": "何岚", "toLocation": "指挥舱"}) with pytest.raises(事实错误) as 错误: 校验事实批次((首, 后), **旧增量环境) assert 错误.value.错误码 == "FACT_DUPLICATE_ID" assert [ x.proposal_id for x in 校验事实批次((首, replace(后, proposal_id="delta-2")), **旧增量环境) ] == ["delta-1", "delta-2"] @pytest.mark.case_id( "TC-8101bb76825a", environment="离线,固定原文与引用", when="解析事实或关系提案并解析其证据和端点。", contract="docs/系统架构/新版设计/数据模型/状态与来源有效性.md", ) def test_salvage_drops_unbound_relation_and_keeps_exact_name_evidence__8101bb(): 原输出 = { "entities": [ { "type": "item", "name": "黑色钥匙", "brief": "钥匙", "fields": {}, "evidence": "模型编造", }, { "type": "item", "name": "不存在的物件", "brief": "?", "fields": {}, "evidence": "模型编造", }, ], "relations": [ { "source": "林深", "target": "何岚", "type": "伙伴", "description": "?", "evidence": "模型编造", } ], "state": {}, } 结果 = 保留名称提及(原输出, 单段(原抽取正文), tuple(s.类型 for s in 读取内置种子())) assert [x["name"] for x in 结果["mentions"]] == ["黑色钥匙"] assert 结果["mentions"][0]["evidence"].quote == "黑色钥匙" assert 结果["relations"] == [] and 结果["mechanical_drops"] == {"entities": 1, "relations": 1} assert "brief" not in 结果["mentions"][0] and 结果["state"] == "needs_confirmation"