"""同一结构的五类用途、授权与来源版本分别生效。""" from __future__ import annotations from dataclasses import replace import pytest from muse.元数据.接口 import ( 值规则, 元数据错误, 合成结构, 字段定义, 字段用途, 字段限制, 投影值, 投影字段, 授权摘要, 策略快照, 结构定义, ) @pytest.mark.parametrize( "用途", ["uiVisible", "userEditable", "userSearchable", "exportable", "aiContext"], ids=["display", "edit", "search", "export", "generation"], ) def test_五类用途分别投影并解释剔除__a41001(用途: str) -> None: """given 作者备忘仅可看可编辑;when 选择五类用途;then 不扩大 AI、检索或导出权限。""" 结构 = 合成结构( 结构定义( "character", "character", 1, ( 字段定义("name", "姓名", "姓名", 值规则("text"), 用途=字段用途(aiContext=True)), 字段定义( "memo", "创作备忘", "创作备忘", 值规则("text"), 用途=字段用途(userSearchable=False, exportable=False), ), ), ) ) 投影 = 投影字段( 结构, 策略快照("character", 1), 授权摘要("a1", "s1"), 字段用途名=用途, 内容用途="generation", 运行用途="production", ) assert 投影值(投影, {"姓名": "青棠", "创作备忘": "作者私记"}) == ( {"姓名": "青棠", "创作备忘": "作者私记"} if 用途 in {"uiVisible", "userEditable"} else {"姓名": "青棠"} ) if 用途 not in {"uiVisible", "userEditable"}: assert 投影.剔除原因["创作备忘"] == "FIELD_NOT_ALLOWED" def test_投影身份包含授权来源及当前策略__a41002() -> None: """given 历史结构;when 撤回用途或改变授权来源;then 投影版本变化且旧值不泄露。""" 结构 = 合成结构( 结构定义( "character", "character", 1, (字段定义("secret", "秘密", "秘密", 值规则("text"), 用途=字段用途(aiContext=True)),), ) ) 策略 = 策略快照("character", 1) 授权 = 授权摘要("author-a1", "source-s1") def 生成(策略=策略, 授权=授权): return 投影字段( 结构, 策略, 授权, 字段用途名="aiContext", 内容用途="generation", 运行用途="production" ) 旧 = 生成() assert ( 旧.projection_version != 生成(授权=replace(授权, 授权指纹="author-a2")).projection_version ) assert ( 旧.projection_version != 生成(授权=replace(授权, 来源指纹="source-s2")).projection_version ) 新 = 生成(策略=策略快照("character", 2, (字段限制("secret", ("aiContext",)),))) assert 新.projection_version != 旧.projection_version assert 投影值(新, {"秘密": "暗门"}) == {} with pytest.raises(元数据错误): 投影字段( 结构, 策略, 授权, 字段用途名="aiContext", 内容用途="unknown", 运行用途="production" ) def test_原结构字段语义用途别名逐项承接__a41003() -> None: """given 冻结原定义;when 检查发布种子;then 逐型身份、别名和 AI 用途原样承接。""" import hashlib from pathlib import Path import yaml from muse.元数据.接口 import 读取内置种子 根 = Path(__file__).resolve().parents[2] 源 = 根 / "docs/系统架构/新版设计/研究依据/原结构快照" 总字段 = 0 for 种子 in 读取内置种子(): 文件 = 源 / f"{种子.类型.type_id}.yaml" 原 = yaml.safe_load(文件.read_text(encoding="utf-8")) assert 种子.原定义哈希 == hashlib.sha256(文件.read_bytes()).hexdigest() 按键 = {项.key: 项 for 项 in 种子.结构.字段 if not 项.field_id.startswith("common.")} assert set(按键) == {项["key"] for 项 in 原.get("特有字段", [])} for 项 in 原.get("特有字段", []): 新 = 按键[项["key"]] ai = 项.get("aiContext", False) assert 新.用途.aiContext == (tuple(ai) if isinstance(ai, list) else ai) assert not 项.get("alias") or 项["alias"] in 新.别名 assert 项.get("说明", "") in 新.说明 总字段 += len(按键) assert 总字段 == 216 def test_公共内容字段参加同源校验投影且不混入运行外壳__a41009() -> None: """名称等公共内容与逐型字段合成;来源和状态仍由业务骨架维护。""" from muse.元数据.接口 import 校验值, 读取内置结构 结构 = 合成结构(读取内置结构("character")) 值 = {"名称": "青棠", "别名": ["阿棠"], "一句话摘要": "守住旧约的人", "标签": ["守信"]} assert 校验值(结构, 值).值 == 值 投影 = 投影字段( 结构, 策略快照("character", 1), 授权摘要("a1", "s1"), 字段用途名="aiContext", 内容用途="generation", 运行用途="production", ) assert 投影值(投影, 值) == 值 with pytest.raises(元数据错误): 校验值(结构, {"标签": "不能把列表写成字符串"}) with pytest.raises(元数据错误): 校验值(结构, {"状态": "已确认"}) for schema_id in ("generation_context", "fine_outline"): assert not any(f.field_id.startswith("common.") for f in 读取内置结构(schema_id).字段) def test_联合类型按已验证分支裁剪隐藏值__a41010() -> None: """两个分支都是列表时,不能因外形相同把私记按文本列表原样送出。""" from muse.元数据.接口 import 校验值 分支 = 值规则( "object", 字段=( 字段定义("public", "公开", "公开", 值规则("text"), 用途=字段用途(aiContext=True)), 字段定义("private", "私记", "私记", 值规则("text")), ), ) 规则 = 值规则( "union", 分支=( 值规则("list", 元素=值规则("text")), 值规则("list", 元素=分支), ), ) 结构 = 合成结构( 结构定义( "t", "s", 1, (字段定义("data", "资料", "资料", 规则, 用途=字段用途(aiContext=True)),) ) ) 值 = 校验值(结构, {"资料": [{"公开": "公开信息", "私记": "私人想法"}]}).值 投影 = 投影字段( 结构, 策略快照("t", 1), 授权摘要("a1", "s1"), 字段用途名="aiContext", 内容用途="generation", 运行用途="production", ) assert 投影值(投影, 值) == {"资料": [{"公开": "公开信息"}]} def test_嵌套隐藏字段不会流入模型合同或值__a41004() -> None: """given 嵌套动态对象含私人备注;when 模型投影;then 输出合同和值均删除隐藏项。""" from muse.元数据.接口 import 生成字段合同 对象 = 值规则( "object", 字段=( 字段定义( "nested.public", "公开", "公开", 值规则("text"), 用途=字段用途(aiContext=True) ), 字段定义("nested.private", "私记", "私记", 值规则("text")), ), ) 结构 = 合成结构( 结构定义( "new_type", "new_schema", 1, ( 字段定义( "outer", "观察", "观察", 值规则("list", 元素=对象), 用途=字段用途(aiContext=True), ), ), ) ) 投影 = 投影字段( 结构, 策略快照("new_type", 1), 授权摘要("a1", "s1"), 字段用途名="aiContext", 内容用途="generation", 运行用途="production", ) assert 投影值(投影, {"观察": [{"公开": "桥下有人", "私记": "后续身份计划"}]}) == { "观察": [{"公开": "桥下有人"}] } 合同 = 生成字段合同(投影.允许字段) assert set(合同["properties"]["观察"]["items"]["properties"]) == {"公开"} def test_开放类型只选择已登记实例维护者__a41005() -> None: """given 新题材类型;when 选择实体生命周期;then 可归 B02,不能声明表名或脚本。""" from muse.元数据.接口 import 类型定义, 选择实例维护者 类型 = 类型定义("weather_spirit", "天气精灵", "world", "entity", ("entity",)) assert 选择实例维护者(类型, "entity") == "B02" with pytest.raises(元数据错误): 选择实例维护者(replace(类型, 实例族=("public.character",)), "public.character") def test_结构升级要求转换且旧结构可解释__a41006() -> None: """given 原枚举与可选字段;when 缩窄枚举新增必填;then 明确转换对象并保留旧解释。""" from muse.元数据.接口 import 分析变更, 校验值 字段 = 字段定义("mood", "情绪", "情绪", 值规则("enum", 枚举=("平静", "焦虑"))) 旧 = 合成结构(结构定义("character", "character", 1, (字段,))) 新 = 合成结构( 结构定义( "character", "character", 2, (replace(字段, 值=值规则("enum", 枚举=("平静",)), 必填=True),), 父版本=1, ) ) 影响 = 分析变更(旧, 新, ("entity-a",)) assert 影响.分类 == "incompatible" assert 影响.转换字段 == ("mood",) assert 影响.影响对象 == ("entity-a",) assert 校验值(旧, {"情绪": "焦虑"}).值 == {"情绪": "焦虑"} with pytest.raises(元数据错误): 校验值(新, {"情绪": "焦虑"}) def test_表单修改不能重发只读字段__a41007() -> None: """given 作品动态表单;when 重发计算字数;then 只允许编辑字段且条件来自同源合同。""" from muse.元数据.接口 import 校验字段修改, 生成结构合同, 读取内置结构 作品 = 合成结构(读取内置结构("novel_work")) 投影 = 投影字段( 作品, 策略快照("novel_work", 1), 授权摘要("author-v1", "source-v1"), 字段用途名="userEditable", 内容用途="planning", 运行用途="production", ) assert 校验字段修改(投影, {"书名": "候雨"}).值 == {"书名": "候雨"} with pytest.raises(元数据错误): 校验字段修改(投影, {"总字数": 999999}) 合同 = 生成结构合同(合成结构(读取内置结构("craft"))) 条件 = 合同["allOf"] assert 条件[0]["then"]["required"] == ["埋设手法", "回收点", "记忆维持", "间隔纪律"] assert 条件[1]["then"]["properties"] == {"间隔纪律": False} assert 合同["properties"]["读者收益"]["maxItems"] == 2 def test_逐字段修改保留未展示子字段__aa1201(): from muse.元数据.接口 import 字段变更, 校验字段变更 结构 = 合成结构( 结构定义( "profile", "profile", 1, ( 字段定义( "p.info", "资料", "资料", 值规则( "object", 字段=( 字段定义("p.public", "公开", "公开", 值规则("text")), 字段定义( "p.private", "私记", "私记", 值规则("text"), 用途=字段用途(userEditable=False), ), ), ), ), ), ) ) 投影 = 投影字段( 结构, 策略快照("profile", 0), 授权摘要("author", "source"), 字段用途名="userEditable", 内容用途="planning", 运行用途="production", ) 原 = {"资料": {"公开": "甲", "私记": "不改这里"}} 结果 = 校验字段变更(结构, 投影, 原, (字段变更(("资料", "公开"), "set", "乙"),)) assert 结果.值 == {"资料": {"公开": "乙", "私记": "不改这里"}} assert 原["资料"]["公开"] == "甲" with pytest.raises(元数据错误): 校验字段变更(结构, 投影, 原, (字段变更(("资料",), "set", {"公开": "乙"}),))