"""目标章细纲与时点事实经 S02 任务以 aiContext 真投影读取;不调用模型。""" from dataclasses import asdict from uuid import uuid4 import pytest from pydantic import TypeAdapter from muse.上下文.接口 import 上下文错误, 装配材料 from muse.任务运行.接口 import ( 事件类型, 任务请求, 工具请求, 步骤处理器, 步骤结果, 步骤计划, ) from muse.作品规划.接口 import 档案保存, 规划保存, 规划定位 from muse.元数据.接口 import ( 值规则, 元数据服务, 字段定义, 字段用途, 类型定义, 结构定义, ) from muse.共享.调用身份 import 内容用途, 用途, 调用身份 from muse.启动 import 构建 from muse.故事世界.接口 import 事实定位, 事实提案请求, 定位字面引文, 正文依据 from muse.正式变更.接口 import 作者动作 from muse.正文写作.接口 import 文本节点, 正文草稿, 段落 from muse.编排.接口 import 流程定义, 流程服务, 流程登记 from muse.配置 import 应用配置 pytestmark = pytest.mark.数据库 def 细纲内容(parent_id: str) -> dict: return { "targetChapter": 3, "sourceRef": {"sourceId": parent_id, "sourceVersion": 1}, "chapterGoal": "邮差在旧码头确认收信人。", "hardConstraints": ["上一章结尾雨还没停。"], "keyEvents": [ { "id": "arrival", "order": 1, "event": "邮差到达旧码头", "participants": ["lin-shen"], "trigger": "收到退回的信", "resultDirection": "确认码头还在使用", } ], "adjustableBeats": [], "declaredNewFacts": [], "mustAppearEntities": ["lin-shen"], "chapterEndHook": "仓库的门后有人应声。", } @pytest.fixture def 投影环境(内置结构测试库): 应用测试库 = 内置结构测试库 with 应用测试库[用途.维护].连接() as 连, 连.transaction(): 元数据 = 元数据服务(连) # 动态类型:名称对两种投影可见,秘密备注只对 uiVisible 可见。 元数据.登记类型(类型定义("landing", "泊位", "world", "entity", ("entity",))) 结构 = 结构定义( "landing", "landing", 1, ( 字段定义( "landing.name", "名称", "名称", 值规则("text"), 必填=True, 用途=字段用途(aiContext=True), ), 字段定义( "landing.note", "秘密备注", "秘密备注", 值规则("text"), 用途=字段用途(uiVisible=True, userEditable=True, aiContext=False), ), ), ) 元数据.登记结构候选(结构) 元数据.发布结构("landing", 1, 结构.内容哈希) 登记 = 流程登记() 装配 = 构建(应用配置(应用测试库[用途.生产].引用, "test"), 流程=登记) with 装配.生命周期(): 作者 = 调用身份("ctx-author", None, 用途.生产, 内容用途.生成) 作品, 正文 = 装配.要求作品(), 装配.要求正文() 结构选择 = 作品.新档案结构(作者, "ctx-work", "work_core", 1) 作品.保存档案( 作者, "create-ctx-work", 档案保存( "ctx-work", 0, {"名称": "旧码头", "题材定位": "现实", "主题立意": "还信"}, 结构选择 ), ) for i in range(1, 5): 作品.添加章节( 作者, f"add-ch-{i}", "ctx-work", f"ch-{i}", f"第{i}章", 预期目录版本=i - 1 ) for 章, 文本 in [ ("ch-1", "林深停在码头,雨还没停。"), ("ch-3", "林深来到北岸。"), ]: 正文.保存人工( 作者, f"body-{章}", 章, 0, 正文草稿((段落(f"p-{章}", (文本节点(文本),)),)) ) 外结构 = 作品.新档案结构(作者, "other-work", "work_core", 1) 作品.保存档案( 作者, "create-other-work", 档案保存( "other-work", 0, {"名称": "别的书", "题材定位": "现实", "主题立意": "无"}, 外结构 ), ) 作品.添加章节(作者, "add-foreign", "other-work", "foreign", "外章", 预期目录版本=0) 规划 = 装配.要求规划() def 保存并确认(请求, 命令: str) -> dict: 回执 = 规划.保存人工候选(作者, 命令 + "-draft", 请求) 候选 = 规划.读取候选(作者, 回执["results"][0]["candidate_id"]) 定位 = 规划定位( 候选["candidate_id"], 候选["revision"], 候选["candidate_hash"], 候选["plan_id"], 请求.expected_revision, ) 审阅 = 规划.打开审阅(作者, 定位) 规划.决定( 作者, 命令 + "-accept", 定位, 作者动作.采纳, 审阅["review_id"], 审阅["review_hash"], ("plan",), ) return 规划.读取规划(作者, 候选["plan_id"]) 世界 = 装配.要求故事世界() def 正文源(章: str, 引文: str) -> 正文依据: 记录 = 正文.读取正文(作者, 章) 草稿 = TypeAdapter(正文草稿).validate_python(记录["document"]) return 正文依据( 章, "main", 记录["revision"], 记录["document_hash"], (定位字面引文(草稿, 引文),) ) def 确认事实(对象, 类型, 内容, 来源=(), 生效="ch-1", 独立=False) -> None: 选择 = 世界.新事实结构(作者, "ctx-work", 对象, 类型, 1) 回执 = 世界.提出事实( 作者, "propose-" + 对象, 事实提案请求("ctx-work", 对象, 0, 选择, 内容, tuple(来源), 生效, independent=独立), ) 提案 = 世界.读取提案(作者, 回执["results"][0]["proposal_id"]) 定位 = 事实定位( 提案["proposal_id"], 提案["revision"], 提案["proposal_hash"], "world:" + 对象, 0 ) 审阅 = 世界.打开事实审阅(作者, 定位) 世界.决定事实( 作者, "confirm-" + 对象, 定位, 作者动作.采纳, 审阅["review_id"], 审阅["review_hash"], ("object",), ) 确认事实("lin-shen", "character", {"名称": "林深"}, (正文源("ch-1", "林深停在码头"),)) 确认事实("dock-yard", "landing", {"名称": "旧码头", "秘密备注": "夜里卸货"}, 独立=True) 确认事实( "secret-cargo", "character", {"名称": "夜间货物"}, (正文源("ch-3", "林深来到北岸"),) ) 父 = 保存并确认( 规划保存( "ctx-work", 0, 规划.新规划结构(作者, "ctx-work", "outline", 1), {"名称": "旧码头"} ), "outline", ) 细纲 = 保存并确认( 规划保存( "ctx-work", 0, 规划.新规划结构(作者, "ctx-work", "fine_outline", 1), 细纲内容(父["plan_id"]), chapter_id="ch-3", ), "fine-outline", ) def 执行(上下文): 工具 = 装配.要求只读工具(上下文.领取) 材料 = {} 计划 = 工具.调用(工具请求("plan", "query_work_plan", {})) 材料["目标章细纲"] = 计划.内容["content"]["chapterGoal"] 事实 = 工具.调用(工具请求("facts", "query_facts_as_of", {})) for 项 in 事实.内容["facts"]: 名称 = 项["content"].get("名称") if 名称: 材料["事实:" + 项["object_id"]] = 名称 历史 = 工具.调用( 工具请求("history", "read_history_document", {"chapter_id": "ch-1", "revision": 1}) ) 材料["历史:ch-1"] = 历史.内容["text"] 上限 = 上下文.任务.冻结输入["冻结上下文"]["budget"]["context_bytes"] return 步骤结果(asdict(装配材料(材料, tuple(材料), 字节上限=上限))) 登记.登记处理器( 步骤处理器( "synthetic-projection", "1", 执行, "projection/v1", "projection/v1", 保护职责="范围核对", 允许工具=("read_history_document", "query_work_plan", "query_facts_as_of"), ) ) 登记.登记类型("synthetic-projection", 必需保护=("范围核对",)) 登记.登记恢复检查( "synthetic-projection", "1", lambda 任务: 装配.要求上下文().读取范围(任务.任务ID) ) 运行 = 装配.任务运行 assert 运行 is not None 流程服务(运行, 登记).发布( 流程定义( "synthetic-projection", "1", ( 步骤计划( "read", "synthetic-projection", "1", 工具=("read_history_document", "query_work_plan", "query_facts_as_of"), ), ), ) ) def 开始(范围=None, 任务内容用途=内容用途.生成): if 范围 is None: 范围 = 装配.要求上下文().授权任务范围( 作者, "ctx-work", 2, ({"chapter_id": "ch-1", "revision": 1},), 目标章ID="ch-3", 含事实=True, ) id_ = 运行.创建任务( 任务请求( "synthetic-projection", str(uuid4()), 作者.作者, 用途.生产, 任务内容用途, {}, "test", "test", { "source_scope": 范围, "schema_versions": {}, "authorization": "synthetic-author-choice", "budget": {"context_bytes": 2048}, "stop_conditions": {"max_reads": 3}, }, 作品ID="ctx-work", ), "synthetic-projection", "1", ) 领取 = 运行.领取步骤("projection-test", ["synthetic-projection"], 租期秒=120) assert 领取 is not None and 领取.任务ID == id_ return id_, 领取, 装配.要求只读工具(领取) yield { "装配": 装配, "作者": 作者, "开始": 开始, "保存并确认": 保存并确认, "父": 父, "细纲": 细纲, "确认事实": 确认事实, "正文源": 正文源, "规划": 规划, } @pytest.mark.case_id( "NC-ctx-projection-s02-trace", environment="真实隔离PostgreSQL", given="已确认目标章细纲、多条时点事实与历史正文的真实作品", when="经 S02 任务执行装配处理器依次调用三种具名工具并装配材料", then=["三种工具读取均留工具读取事件与来源引用;细纲/事实/历史全部进入材料预算入选清单"], contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) def test_细纲与事实经真实任务读取留痕并进入预算__b09201(投影环境): 环境 = 投影环境 装配, 开始, 细纲 = 环境["装配"], 环境["开始"], 环境["细纲"] task_id, 领取, _ = 开始() 结果 = 装配.任务运行.执行一步(领取) assert set(结果.输出["入选来源"]) == { "目标章细纲", "事实:dock-yard", "事实:lin-shen", "事实:secret-cargo", "历史:ch-1", } assert len(结果.输出["入选来源"]) == 5 读取 = [e for e in 装配.任务运行.续接事件(task_id).事件 if e.类型 == 事件类型.工具读取] assert [e.载荷["tool"] for e in 读取] == [ "query_work_plan", "query_facts_as_of", "read_history_document", ] 计划引用 = 读取[0].载荷["source_refs"][0] assert 计划引用["source_id"] == 细纲["plan_id"] assert 计划引用["revision"] == "1" and 计划引用["schema_hash"] 事实身份 = {r["source_id"] for e in 读取 for r in e.载荷["source_refs"]} assert {"lin-shen", "dock-yard", "secret-cargo"} <= 事实身份 @pytest.mark.case_id( "NC-ctx-plan-projection", environment="真实隔离PostgreSQL", given="任务冻结范围为 generation 用途", when="调用 query_work_plan 工具", then=["返回细纲 aiContext 投影内容、剔除说明与两种用途;来源版本、结构哈希与投影版本完整"], contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) def test_目标细纲按任务用途返回真投影__b09202(投影环境): _, _, 工具 = 投影环境["开始"]() 结果 = 工具.调用(工具请求("plan", "query_work_plan", {})) 内容 = 结果.内容 assert 内容["chapter_id"] == "ch-3" assert 内容["content"]["chapterGoal"] == "邮差在旧码头确认收信人。" assert 内容["content"]["targetChapter"] == 3 assert 内容["content"]["sourceRef"]["sourceVersion"] == 1 assert 内容["run_purpose"] == "production" assert 内容["content_purpose"] == "generation" assert 结果.来源[0].数据版本 == "1" assert 结果.来源[0].投影版本 @pytest.mark.case_id( "NC-ctx-facts-view-narrowing", environment="真实隔离PostgreSQL", given="冻结事实系统版本;一条事实的正文依据位于截止之后;一个不存在的知情主体", when="分别以作者、读者与角色视角调用 query_facts_as_of", then=[ "作者视角含全部事实;读者视角省略未来依据并给出 FUTURE_EVIDENCE;未知知情主体 SCOPE_DENIED" ], contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) def test_时点事实按冻结系统版本与视角收窄返回__b09203(投影环境): _, _, 工具 = 投影环境["开始"]() 作者视角 = 工具.调用(工具请求("facts", "query_facts_as_of", {})) 对象 = {项["object_id"] for 项 in 作者视角.内容["facts"]} assert {"lin-shen", "dock-yard", "secret-cargo"} <= 对象 assert 作者视角.内容["run_purpose"] == "production" assert 作者视角.内容["content_purpose"] == "generation" 读者视角 = 工具.调用(工具请求("facts", "query_facts_as_of", {"view": "reader"})) 读者对象 = {项["object_id"] for 项 in 读者视角.内容["facts"]} assert "secret-cargo" not in 读者对象 and "lin-shen" in 读者对象 省略 = {项["object_id"]: 项["reason"] for 项 in 读者视角.内容["omitted"]} assert 省略["secret-cargo"] == "FUTURE_EVIDENCE" with pytest.raises(上下文错误) as 异常: 工具.调用( 工具请求( "facts", "query_facts_as_of", {"view": "character", "knower_id": "missing-actor"}, ) ) assert 异常.value.错误码 == "SCOPE_DENIED" @pytest.mark.case_id( "NC-ctx-projection-drift", environment="真实隔离PostgreSQL", given="任务范围已冻结细纲身份与事实系统版本", when="重新确认细纲新版本或确认新事实后再次调用工具", then=["SOURCE_STALE;漂移后不得沿用原任务范围"], contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) @pytest.mark.parametrize("漂移源", ["plan", "facts"], ids=["plan-revised", "facts-advanced"]) def test_细纲或事实漂移后拒绝沿用原任务__b09204(投影环境, 漂移源): 环境 = 投影环境 _, _, 工具 = 环境["开始"]() if 漂移源 == "plan": 内容 = 细纲内容(环境["父"]["plan_id"]) 内容["chapterGoal"] = "作者改过的目标。" 环境["保存并确认"]( 规划保存( "ctx-work", 1, 环境["规划"].新规划结构(环境["作者"], "ctx-work", "fine_outline", 1), 内容, chapter_id="ch-3", ), "revise-outline", ) else: 环境["确认事实"]("late-comer", "character", {"名称": "后来的事实"}, 独立=True) with pytest.raises(上下文错误) as 异常: 工具.调用( 工具请求( "stale", "query_work_plan" if 漂移源 == "plan" else "query_facts_as_of", {}, ) ) assert 异常.value.错误码 == "SOURCE_STALE" @pytest.mark.case_id( "NC-ctx-aicontext-differs", environment="真实隔离PostgreSQL", given="一个字段只声明 uiVisible、另一个字段两种用途都可见的动态类型事实", when="同一对象分别经工作台查询与 query_facts_as_of 读取", then=["工作台可见秘密备注;工具投影剔除该字段并给出 FIELD_NOT_ALLOWED 剔除原因"], contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) def test_aiContext投影与uiVisible差异真实生效__b09205(投影环境): 环境 = 投影环境 装配, 作者, 开始 = 环境["装配"], 环境["作者"], 环境["开始"] _, _, 工具 = 开始() 界面 = 装配.要求故事世界().查询世界(作者, "ctx-work", "ch-2") 泊位 = next(项 for 项 in 界面["facts"] if 项["object_id"] == "dock-yard") assert 泊位["content"]["秘密备注"] == "夜里卸货" 投影 = 工具.调用(工具请求("facts", "query_facts_as_of", {})) 工具泊位 = next(项 for 项 in 投影.内容["facts"] if 项["object_id"] == "dock-yard") assert "秘密备注" not in 工具泊位["content"] assert 工具泊位["omitted_fields"]["秘密备注"] == "FIELD_NOT_ALLOWED" assert 工具泊位["content"]["名称"] == "旧码头" @pytest.mark.case_id( "NC-ctx-plan-target-boundary", environment="真实隔离PostgreSQL", given="历史章、截止位置章、跨作品章或无已确认细纲的紧邻章", when="以目标章授权任务范围", then=["前三种 SCOPE_DENIED(含实际所属作品);无细纲 PRECONDITION_PENDING"], contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) @pytest.mark.parametrize( ("目标章", "截止", "错误码"), [ ("ch-1", 2, "SCOPE_DENIED"), ("ch-2", 2, "SCOPE_DENIED"), ("foreign", 2, "SCOPE_DENIED"), ("ch-4", 3, "PRECONDITION_PENDING"), ], ids=["past-chapter", "cutoff-chapter", "cross-work", "unconfirmed-outline"], ) def test_目标章不在截止之后紧邻或无细纲拒绝授权__b09206(投影环境, 目标章, 截止, 错误码): 环境 = 投影环境 with pytest.raises(上下文错误) as 异常: 环境["装配"].要求上下文().授权任务范围( 环境["作者"], "ctx-work", 截止, (), 目标章ID=目标章, 含事实=True ) assert 异常.value.错误码 == 错误码 @pytest.mark.case_id( "NC-ctx-purpose-mismatch", environment="真实隔离PostgreSQL", given="范围按 generation 授权,任务以 planning 内容用途创建", when="领取只读工具装配", then=["CONTEXT_SCOPE_INVALID;任务用途与冻结范围用途不一致即失败关闭"], contract="docs/系统架构/新版设计/模块设计/B09-上下文.md", ) def test_任务用途与冻结范围不一致拒绝__b09207(投影环境): with pytest.raises(上下文错误) as 异常: 投影环境["开始"](任务内容用途=内容用途.规划) assert 异常.value.错误码 == "CONTEXT_SCOPE_INVALID"