W14 受限上下文、工具与探索回放:首切片「受限历史正文读取与材料预算」:材料预算纯函数、授权历史范围、read_history_document 具名只读工具接入 S02 并留痕;13 个新 NC 通过(离线 237、非宿主 PG 173)。W14 整体 in_progress,尚未验收。

按 R2 串行阶段整理提交;包内文件为该阶段交付(含后续小增量),状态以工作包清单为准。
This commit is contained in:
zizi 2026-09-10 19:25:41 +08:00
parent 224df00706
commit f7a57f68e6
9 changed files with 829 additions and 0 deletions

View File

@ -0,0 +1 @@
"""上下文模块;公开用例与合同统一经接口.py,材料预算见材料装配.py。"""

View File

@ -0,0 +1,189 @@
"""服务端授权并恢复历史正文读取范围;执行阶段只从已保存任务取回。
合同依据:模块设计/B09-上下文.md、接口契约/任务工具与事件.md。
- 授权只在服务端发生:作者、作品、叙事截止与文稿清单由作者动作传入,
模型侧工具不接受其中任何一项。
- 范围随任务冻结;恢复时校验形状与任务身份一致,缺失截止不是无限,
也不静默改读最新版本。
- 正文与目录只经 B01/B05 公开接口读取;本文件不写跨模块 SQL。
"""
from __future__ import annotations
from typing import Any
from psycopg import Connection
from muse.上下文.模型 import 上下文错误
from muse.任务运行.接口 import 任务快照, 任务服务
from muse.作品规划.接口 import 作品错误, 读取作品范围, 读取章节归属
from muse.共享.调用身份 import 调用身份
from muse.基础设施.数据库.连接 import 数据库工厂
from muse.正文写作.接口 import 读取正文依据
from muse.正文写作.模型 import 正文错误
_文稿必需键 = frozenset(
{"chapter_id", "branch_id", "revision", "document_id", "document_hash", "position"}
)
def 校验截止(值: Any) -> int:
"""叙事截止必须是明确的非负整数;bool 与缺失都不是截止。"""
if isinstance(值, bool) or not isinstance(值, int) or 值 < 0:
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"叙事截止必须是非负整数,收到 {值!r}")
return 值
def _拒绝(错误: 作品错误, 前因: str) -> 上下文错误:
代码 = "SOURCE_NOT_FOUND" if 错误.错误码 == "CHAPTER_NOT_FOUND" else 错误.错误码
return 上下文错误(代码, f"{前因}:{错误.说明}")
class 上下文服务:
"""授权历史范围并从已保存任务恢复冻结范围;读取范围失败即关闭。"""
def __init__(self, 数据库: 数据库工厂, 任务: 任务服务) -> None:
self.数据库 = 数据库
self.任务 = 任务
def 授权历史范围(
self, 身份: 调用身份, 作品ID: str, 截止: int, 文稿查询: tuple[dict, ...]
) -> dict:
"""按作者、作品与叙事截止固定可读文稿、目录版本与正文哈希;只读不写入。"""
if not 作品ID:
raise 上下文错误("CONTEXT_SCOPE_INVALID", "历史范围必须绑定明确作品")
if 身份.作者 is None or 身份.用途 is not self.数据库.用途:
raise 上下文错误("SCOPE_DENIED", "历史范围授权需要与装配用途一致的作者身份")
校验截止(截止)
if not isinstance(文稿查询, tuple) or not 文稿查询:
raise 上下文错误("CONTEXT_SCOPE_INVALID", "历史范围至少声明一个文稿来源")
with self.数据库.连接(只读=True) as 连:
目录 = self._读取目录(连, 身份.作者, 作品ID)
章节 = {c["chapter_id"]: c for c in 目录["chapters"]}
文稿 = [self._授权文稿(连, 身份.作者, 作品ID, 截止, 章节, 查询) for 查询 in 文稿查询]
if len({d["document_id"] for d in 文稿}) != len(文稿):
raise 上下文错误("CONTEXT_SCOPE_INVALID", "同一文稿在历史范围内重复声明")
return {
"work_id": 作品ID,
"as_of": 截止,
"source_ids": [d["document_id"] for d in 文稿],
"documents": 文稿,
"directory_revision": 目录["revision"],
"authorized_by": 身份.作者,
}
def 读取范围(self, 任务ID: str) -> dict:
"""从已保存任务恢复冻结范围;形状或身份不一致即失败关闭。"""
return self.恢复冻结范围(self.任务.读取任务(任务ID))
def 恢复冻结范围(self, 任务: 任务快照) -> dict:
"""校验任务内冻结范围完整且与任务、装配用途一致,然后原样返回。"""
固定 = 任务.冻结输入
范围 = (固定.get("冻结上下文") or {}).get("source_scope")
if not isinstance(范围, dict):
raise 上下文错误("CONTEXT_SCOPE_INVALID", "任务缺少冻结来源范围")
if 范围.get("work_id") != 固定.get("作品ID"):
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结范围与任务作品不一致")
if 范围.get("authorized_by") != 任务.作者:
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结范围与任务作者不一致")
if str(固定.get("执行用途")) != self.数据库.用途.value:
raise 上下文错误("CONTEXT_SCOPE_INVALID", "任务执行用途与装配用途不一致")
校验截止(范围.get("as_of"))
目录版本 = 范围.get("directory_revision")
if isinstance(目录版本, bool) or not isinstance(目录版本, int) or 目录版本 < 0:
raise 上下文错误(
"CONTEXT_SCOPE_INVALID", f"冻结目录版本必须是非负整数,收到 {目录版本!r}"
)
文稿 = 范围.get("documents")
if not isinstance(文稿, list) or not 文稿:
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结范围没有已授权文稿")
for 项 in 文稿:
if not isinstance(项, dict) or not _文稿必需键 <= 项.keys():
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结文稿缺少身份、版本或哈希绑定")
版本 = 项["revision"]
if isinstance(版本, bool) or not isinstance(版本, int) or 版本 < 1:
raise 上下文错误(
"CONTEXT_SCOPE_INVALID", f"冻结文稿版本必须是正整数,收到 {版本!r}"
)
位置 = 项["position"]
if isinstance(位置, bool) or not isinstance(位置, int) or 位置 < 1:
raise 上下文错误(
"CONTEXT_SCOPE_INVALID", f"冻结文稿目录位置必须是正整数,收到 {位置!r}"
)
for 键 in ("chapter_id", "branch_id", "document_id", "document_hash"):
if not isinstance(项[键], str) or not 项[键]:
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"冻结文稿的 {键} 必须是非空字符串")
身份集 = 范围.get("source_ids")
if (
not isinstance(身份集, list)
or not 身份集
or not all(isinstance(i, str) and i for i in 身份集)
or len(set(身份集)) != len(身份集)
or set(身份集) != {项["document_id"] for 项 in 文稿}
):
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结来源身份与文稿清单不一致")
return 范围
@staticmethod
def _读取目录(连: Connection, 作者: str, 作品ID: str) -> dict:
try:
return 读取作品范围(连, 作者, 作品ID)
except 作品错误 as 错误:
raise _拒绝(错误, f"作品 {作品ID} 的历史范围授权被拒绝") from None
@staticmethod
def _授权文稿(
连: Connection, 作者: str, 作品ID: str, 截止: int, 章节: dict, 查询: dict
) -> dict:
if not isinstance(查询, dict):
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"文稿查询必须显式声明:{查询!r}")
chapter_id = 查询.get("chapter_id")
revision = 查询.get("revision")
分支 = 查询.get("branch_id", "main")
if not isinstance(chapter_id, str) or not chapter_id:
raise 上下文错误("CONTEXT_SCOPE_INVALID", "文稿查询缺少 chapter_id")
if isinstance(revision, bool) or not isinstance(revision, int) or revision < 1:
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"文稿版本必须是正整数,收到 {revision!r}")
if not isinstance(分支, str) or not 分支:
raise 上下文错误("CONTEXT_SCOPE_INVALID", "文稿分支必须是非空字符串")
行 = 章节.get(chapter_id)
if 行 is None:
归属 = None
try:
归属 = 读取章节归属(连, 作者, chapter_id)
except 作品错误 as 错误:
raise _拒绝(错误, f"章节 {chapter_id} 无法授权") from None
# 章节真实存在但不在目标作品:明确拒绝,不猜测替代作品。
raise 上下文错误(
"SCOPE_DENIED",
f"章节 {chapter_id} 不属于作品 {作品ID}(实际属于 {归属['work_id']})",
)
if 行["position"] > 截止:
raise 上下文错误(
"SCOPE_DENIED",
f"章节 {chapter_id} 位于叙事截止 {截止} 之后(目录位置 {行['position']})",
)
try:
依据 = 读取正文依据(连, 作者, chapter_id, revision, 分支=分支)
except 正文错误 as 错误:
raise 上下文错误(
"SOURCE_NOT_FOUND", f"章节 {chapter_id} 的正文版本 {revision} 不存在:{错误.说明}"
) from None
if 依据["current_revision"] != revision:
raise 上下文错误(
"SOURCE_STALE",
f"章节 {chapter_id} 当前版本为 {依据['current_revision']},"
f"与指定版本 {revision} 不一致;历史版本不再用于新任务",
)
return {
"chapter_id": chapter_id,
"branch_id": 分支,
"revision": revision,
"document_id": 依据["document_id"],
"document_hash": 依据["document_hash"],
"position": 行["position"],
}
__all__ = ["上下文服务", "校验截止"]

View File

@ -0,0 +1,25 @@
"""上下文模块唯一公开入口;跨模块只经本文件导入。"""
from muse.上下文.任务范围 import 上下文服务, 校验截止
from muse.上下文.材料装配 import 装配材料
from muse.上下文.来源读取 import (
历史正文参数合同,
历史正文工具,
投影合同,
投影版本,
读取历史正文,
)
from muse.上下文.模型 import 上下文错误, 材料装配结果
__all__ = [
"上下文错误",
"上下文服务",
"材料装配结果",
"装配材料",
"历史正文参数合同",
"历史正文工具",
"投影合同",
"投影版本",
"读取历史正文",
"校验截止",
]

View File

@ -0,0 +1,71 @@
"""材料按必读优先与 UTF-8 字节预算取舍;省略有原因,不补造材料。
边界:纯函数,不访问数据库、模型、文件或网络,也不承担来源授权。
字节数为材料渲染后的 UTF-8 字节(含 ``\\n\\n`` 分隔符),不是 tokenizer 或计费估算。
"""
from __future__ import annotations
from collections.abc import Mapping
from muse.上下文.模型 import 上下文错误, 材料装配结果
分隔符 = "\n\n"
def 装配材料(
材料: Mapping[str, str],
必读: tuple[str, ...],
*,
字节上限: int,
) -> 材料装配结果:
"""按必读优先、输入顺序整项加入可选,返回实际入选与省略条目。
必读按声明顺序先放入;缺失或空白必读拒绝,任一必读放不下即整体拒绝,不返回部分材料。
可选材料按输入顺序整项加入,空白与超预算分别记入省略原因;不截断中文字节,保留原文空白。
字节上限必须是真正的 int:True 是 bool 不是预算,与零、负数、浮点一并拒绝。
"""
if isinstance(字节上限, bool) or not isinstance(字节上限, int) or 字节上限 <= 0:
raise 上下文错误("CONTEXT_INPUT_INVALID", f"字节上限必须是正整数,收到 {字节上限!r}")
if len(set(必读)) != len(必读):
raise 上下文错误("CONTEXT_INPUT_INVALID", f"必读来源身份重复:{tuple(必读)}")
分隔字节 = len(分隔符.encode("utf-8"))
片段: list[str] = []
入选: list[str] = []
字节数 = 0
for 来源 in 必读:
文本 = 材料.get(来源)
if 文本 is None or not 文本.strip():
raise 上下文错误("CONTEXT_REQUIRED_MISSING", f"必读来源缺失或为空:{来源}")
必读总字节 = 字节数 + len(文本.encode("utf-8")) + (分隔字节 if 片段 else 0)
if 必读总字节 > 字节上限:
raise 上下文错误(
"CONTEXT_BUDGET_EXCEEDED",
f"必读材料需要 {必读总字节} 字节,超出上限 {字节上限}",
)
片段.append(文本)
入选.append(来源)
字节数 = 必读总字节
省略: list[tuple[str, str]] = []
for 来源, 文本 in 材料.items():
if 来源 in 入选:
continue
if not 文本.strip():
省略.append((来源, "EMPTY"))
continue
增量 = len(文本.encode("utf-8")) + (分隔字节 if 片段 else 0)
if 字节数 + 增量 > 字节上限:
省略.append((来源, "BUDGET_EXCEEDED"))
continue
片段.append(文本)
入选.append(来源)
字节数 += 增量
return 材料装配结果(
入选来源=tuple(入选), 文本=分隔符.join(片段), 字节数=字节数, 省略=tuple(省略)
)
__all__ = ["装配材料"]

View File

@ -0,0 +1,136 @@
"""按 B05/B01 公开接口读取冻结范围内的历史正文;码点切片不做规范化。
合同依据:模块设计/B09-上下文.md、接口契约/正文结构与中文选区.md。
- 只返回冻结文稿、目录版本、正文版本与哈希全部匹配的可见文本;
任一漂移返回 SOURCE_STALE,不静默改读最新版。
- 投影版本由固定 visible-text-codepoints/v1 合同与两种用途确定性派生,
不伪造 S04 动态结构绑定。
"""
from __future__ import annotations
from psycopg import Connection
from muse.上下文.模型 import 上下文错误
from muse.任务运行.接口 import 工具定义, 工具来源, 工具结果, 工具范围
from muse.作品规划.接口 import 作品错误, 读取作品范围
from muse.基础设施.数据库.连接 import 数据库工厂
from muse.正文写作.接口 import 可见文本, 可见文本哈希, 读取正文依据
from muse.正文写作.模型 import 正文错误
投影合同 = "visible-text-codepoints/v1"
历史正文参数合同 = {
"type": "object",
"additionalProperties": False,
"required": ["chapter_id", "revision"],
"properties": {
"chapter_id": {"type": "string", "minLength": 1},
"revision": {"type": "integer", "minimum": 1},
"branch_id": {"type": "string", "minLength": 1},
"start": {"type": "integer", "minimum": 0},
"end": {"type": "integer", "minimum": 0},
},
}
def 投影版本(运行用途: str, 内容用途: str) -> str:
"""由固定投影合同与两种用途确定性派生;同一输入永远同一版本。"""
return f"{投影合同}:{运行用途}:{内容用途}"
def 读取历史正文(数据库: 数据库工厂, 冻结范围: dict, 范围: 工具范围, 参数: dict) -> 工具结果:
"""只读冻结文稿匹配且目录、版本与哈希一致的可见文本码点片段。"""
键 = (参数["chapter_id"], 参数.get("branch_id", "main"), 参数["revision"])
目标 = next(
(
d
for d in 冻结范围["documents"]
if (d["chapter_id"], d["branch_id"], d["revision"]) == 键
),
None,
)
if 目标 is None:
raise 上下文错误(
"SCOPE_DENIED", f"文稿(章 {键[0]}、分支 {键[1]}、版本 {键[2]})不在冻结范围内"
)
作者 = 冻结范围["authorized_by"]
with 数据库.连接(只读=True) as 连:
依据 = _核对并读取(连, 作者, 冻结范围, 键, 目标)
文本 = 可见文本(依据["document"])
start = 参数.get("start", 0)
end = 参数.get("end", len(文本))
if not 0 <= start <= end <= len(文本):
raise 上下文错误(
"CONTEXT_INPUT_INVALID",
f"码点片段 [{start}:{end}) 超出可见文本长度 {len(文本)}",
)
return 工具结果(
内容={
"chapter_id": 键[0],
"document_id": 依据["document_id"],
"revision": 键[2],
"text": 文本[start:end],
"start": start,
"end": end,
"total_codepoints": len(文本),
"visible_text_hash": 可见文本哈希(依据["document"]),
"run_purpose": 范围.运行用途,
"content_purpose": 范围.内容用途,
},
来源=(
工具来源(
来源ID=依据["document_id"],
数据版本=str(键[2]),
结构哈希=依据["document_hash"],
投影版本=投影版本(范围.运行用途, 范围.内容用途),
),
),
)
def _核对并读取(
连: Connection, 作者: str, 冻结范围: dict, 键: tuple[str, str, int], 目标: dict
) -> dict:
try:
目录 = 读取作品范围(连, 作者, 冻结范围["work_id"])
except 作品错误 as 错误:
raise 上下文错误("SOURCE_STALE", f"作品目录已不可用:{错误.说明}") from None
当前 = next((c for c in 目录["chapters"] if c["chapter_id"] == 键[0]), None)
if (
目录["revision"] != 冻结范围["directory_revision"]
or 当前 is None
or 当前["position"] != 目标["position"]
):
raise 上下文错误(
"SOURCE_STALE",
f"作品目录已从版本 {冻结范围['directory_revision']} 漂移;任务范围失效,需重新授权",
)
try:
依据 = 读取正文依据(连, 作者, 键[0], 键[2], 分支=键[1])
except 正文错误 as 错误:
raise 上下文错误("SOURCE_NOT_FOUND", f"冻结正文版本已不存在:{错误.说明}") from None
if 依据["current_revision"] != 键[2] or 依据["document_hash"] != 目标["document_hash"]:
raise 上下文错误(
"SOURCE_STALE",
f"文稿 {键[0]} 已漂移(当前版本 {依据['current_revision']});任务范围失效",
)
return 依据
def 历史正文工具(数据库: 数据库工厂, 冻结范围: dict) -> 工具定义:
"""为已冻结范围构建本轮唯一具名只读工具;参数只含查询条件。"""
def 读取(范围: 工具范围, 参数: dict) -> 工具结果:
return 读取历史正文(数据库, 冻结范围, 范围, 参数)
return 工具定义(
"read_history_document",
"读取任务冻结范围内的历史正文可见文本;start/end 为 Unicode 码点左闭右开片段",
历史正文参数合同,
读取,
)
__all__ = ["历史正文参数合同", "历史正文工具", "投影合同", "投影版本", "读取历史正文"]

View File

@ -0,0 +1,32 @@
"""上下文材料的稳定对象;错误带实例错误码与中文原因。"""
from __future__ import annotations
from dataclasses import dataclass
from muse.共享.错误 import Muse错误
class 上下文错误(Muse错误):
"""上下文输入非法、必读缺项或必读超预算的稳定错误。
:param 错误码: CONTEXT_INPUT_INVALID、CONTEXT_REQUIRED_MISSING 或 CONTEXT_BUDGET_EXCEEDED。
:param 原因: 面向作者的中文说明,直陈缺项、越界值或超出量。
"""
def __init__(self, 错误码: str, 原因: str) -> None:
self.错误码 = 错误码
super().__init__(原因)
@dataclass(frozen=True, slots=True)
class 材料装配结果:
"""一次材料装配的实际入选与省略;不补造未入选材料。"""
入选来源: tuple[str, ...]
文本: str
字节数: int
省略: tuple[tuple[str, str], ...]
__all__ = ["上下文错误", "材料装配结果"]

View File

@ -0,0 +1,41 @@
"""从已领取步骤装配 S02 只读工具集;接入层只转交,不写来源规则。
合同依据:文件设计/后端-装配与接入.md、接口契约/任务工具与事件.md。
- 工具范围完全取自任务冻结输入;本文件不重新解释作者、作品或截止。
- 只安装已实现的历史正文工具;未实现的方法、资料等工具不伪返回成功。
"""
from __future__ import annotations
from uuid import uuid4
from muse.上下文.接口 import 上下文服务, 历史正文工具
from muse.任务运行.接口 import 事件类型, 任务服务, 只读工具集, 工具范围, 领取凭证
def 领取只读工具(上下文: 上下文服务, 运行: 任务服务, 领取: 领取凭证) -> 只读工具集:
"""按任务冻结范围安装已实现工具;成功读取复用 S02 工具读取事件留痕。"""
任务 = 运行.读取任务(领取.任务ID)
冻结 = 上下文.恢复冻结范围(任务)
步骤 = next(s for s in 任务.流程.步骤 if s.步骤ID == 领取.步骤ID)
固定 = 任务.冻结输入
def 记录(载荷: dict) -> None:
运行.追加事件(领取, 事件类型.工具读取, 载荷, 事件ID=str(uuid4()))
return 只读工具集(
(历史正文工具(上下文.数据库, 冻结),),
步骤.工具,
工具范围(
任务.任务ID,
冻结["work_id"],
tuple(冻结["source_ids"]),
冻结["as_of"],
固定["执行用途"],
固定["内容用途"],
),
记录,
)
__all__ = ["领取只读工具"]

View File

@ -0,0 +1,89 @@
"""材料装配的必读优先、UTF-8 字节预算与省略原因;离线纯函数合同。
字节期望值按 UTF-8 逐字写死,不由被测函数自身推算,避免同源计算自证。
"""
import dataclasses
import pytest
from muse.上下文.接口 import 上下文错误, 装配材料
def test_必读优先且可选按原顺序整项加入__1c4a9e() -> None:
材料 = {"可选甲": "甲文", "必读": "必读文", "可选乙": "乙文"}
结果 = 装配材料(材料, ("必读",), 字节上限=64)
assert 结果.入选来源 == ("必读", "可选甲", "可选乙")
assert 结果.文本 == "必读文\n\n甲文\n\n乙文"
assert 结果.字节数 == 25 # 三个片段 9+6+6 字节加两处分隔符
assert 结果.省略 == ()
with pytest.raises(dataclasses.FrozenInstanceError):
结果.文本 = "改写"
def test_中文与emoji按utf8字节计数含分隔符__5d1c47() -> None:
原文 = "第1章:雨。🌧"
仅必读 = 装配材料({"历史": 原文}, ("历史",), 字节上限=20)
assert 仅必读.文本 == 原文
assert 仅必读.字节数 == 20 # 含四字节 emoji
材料 = {"历史": 原文, "旁观": "他站着"}
齐 = 装配材料(材料, ("历史",), 字节上限=31)
assert 齐.入选来源 == ("历史", "旁观")
assert 齐.字节数 == 31 # 20 + 分隔符 2 + 9
assert 齐.文本 == f"{原文}\n\n他站着"
缺一字节 = 装配材料(材料, ("历史",), 字节上限=30)
assert 缺一字节.入选来源 == ("历史",)
assert 缺一字节.文本 == 原文
assert 缺一字节.省略 == (("旁观", "BUDGET_EXCEEDED"),)
@pytest.mark.parametrize(
("材料", "必读"),
[({}, ("孤本第三章",)), ({"孤本第三章": " \n "}, ("孤本第三章",))],
ids=["missing", "blank"],
)
def test_必读缺失或空白拒绝__a7e903(材料: dict[str, str], 必读: tuple[str, ...]) -> None:
with pytest.raises(上下文错误) as 异常:
装配材料(材料, 必读, 字节上限=64)
assert 异常.value.错误码 == "CONTEXT_REQUIRED_MISSING"
assert "孤本第三章" in 异常.value.说明
def test_必读整体超预算拒绝__c9e51f() -> None:
材料 = {"甲": "必读甲", "乙": "必读乙", "可选": "可选丙"}
with pytest.raises(上下文错误) as 异常:
装配材料(材料, ("甲", "乙"), 字节上限=19)
assert 异常.value.错误码 == "CONTEXT_BUDGET_EXCEEDED"
assert "20" in 异常.value.说明
恰好 = 装配材料(材料, ("甲", "乙"), 字节上限=20) # 9 + 分隔符 2 + 9
assert 恰好.入选来源 == ("甲", "乙")
assert 恰好.字节数 == 20
assert 恰好.省略 == (("可选", "BUDGET_EXCEEDED"),)
def test_可选空白与超预算分别给出省略原因__4f8ab2() -> None:
材料 = {"必读": "必读文", "空白": " ", "太长": "长" * 40, "入选": "短文"}
结果 = 装配材料(材料, ("必读",), 字节上限=17) # 9 + 分隔符 2 + 6
assert 结果.入选来源 == ("必读", "入选")
assert 结果.省略 == (("空白", "EMPTY"), ("太长", "BUDGET_EXCEEDED"))
@pytest.mark.parametrize(
("字节上限", "必读"),
[(0, ("必读",)), (-3, ("必读",)), (True, ("必读",)), (64, ("必读", "必读"))],
ids=["zero", "negative", "boolean", "duplicate"],
)
def test_非法预算或重复必读身份拒绝__e2168c(字节上限: int, 必读: tuple[str, ...]) -> None:
with pytest.raises(上下文错误) as 异常:
装配材料({"必读": "必读文"}, 必读, 字节上限=字节上限)
assert 异常.value.错误码 == "CONTEXT_INPUT_INVALID"
def test_原文不裁剪空白且按原字节保留__b4d7a1() -> None:
原文 = " 第1章 \n雨还在下。\t\n"
结果 = 装配材料({"历史": 原文}, ("历史",), 字节上限=29)
assert 结果.文本 == 原文
assert 结果.字节数 == 29

View File

@ -0,0 +1,245 @@
"""合成流程驱动真实 S02、B01、B05 和隔离 PG;不调用模型、不证明完整创作链。"""
from dataclasses import asdict, replace
from uuid import uuid4
import pytest
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.数据库
原文 = " 雨夜🌧,e\u0301。\n门还开着。"
前段, 后段 = 原文.split("\n")
def 原文草稿(章: str) -> 正文草稿:
"""段内换行走显式换行节点;可见文本仍与原文逐字一致。"""
return 正文草稿((段落("p-" + 章, (文本节点(前段), 换行节点(), 文本节点(后段))),))
@pytest.fixture
def 历史环境(应用测试库):
with 应用测试库[用途.维护].连接() as 连, 连.transaction():
导入内置结构(元数据服务(连))
登记 = 流程登记()
装配 = 构建(应用配置(应用测试库[用途.生产].引用, "test"), 流程=登记)
作者 = 调用身份("history-author", None, 用途.生产, 内容用途.生成)
作品, 正文 = 装配.要求作品(), 装配.要求正文()
for work, chapters in [
("history-work", ("past", "other", "future")),
("another-work", ("foreign",)),
]:
结构 = 作品.新档案结构(作者, work, "work_core", 1)
作品.保存档案(
作者,
"create-" + work,
档案保存(work, 0, {"名称": work, "题材定位": "现实", "主题立意": "选择"}, 结构),
)
for i, chapter in enumerate(chapters):
作品.添加章节(作者, "add-" + chapter, work, chapter, chapter, 预期目录版本=i)
正文.保存人工(作者, "save-" + chapter, chapter, 0, 原文草稿(chapter))
def 执行(上下文):
工具 = 装配.要求只读工具(上下文.领取)
结果 = 工具.调用(
工具请求("history-read", "read_history_document", {"chapter_id": "past", "revision": 1})
)
材料 = {结果.来源[0].来源ID: 结果.内容["text"]}
上限 = 上下文.任务.冻结输入["冻结上下文"]["budget"]["context_bytes"]
return 步骤结果(asdict(装配材料(材料, tuple(材料), 字节上限=上限)))
登记.登记处理器(
步骤处理器(
"synthetic-history",
"1",
执行,
"history/v1",
"history/v1",
保护职责="范围核对",
允许工具=("read_history_document",),
)
)
登记.登记类型("synthetic-history", 必需保护=("范围核对",))
登记.登记恢复检查(
"synthetic-history", "1", lambda 任务: 装配.要求上下文().读取范围(任务.任务ID)
)
运行 = 装配.任务运行
assert 运行 is not None
流程服务(运行, 登记).发布(
流程定义(
"synthetic-history",
"1",
(步骤计划("read", "synthetic-history", "1", 工具=("read_history_document",)),),
)
)
def 开始(范围=None):
if 范围 is None:
范围 = 装配.要求上下文().授权历史范围(
作者, "history-work", 2, ({"chapter_id": "past", "revision": 1},)
)
id_ = 运行.创建任务(
任务请求(
"synthetic-history",
str(uuid4()),
作者.作者,
用途.生产,
内容用途.生成,
{},
"test",
"test",
{
"source_scope": 范围,
"schema_versions": {},
"authorization": "synthetic-author-choice",
"budget": {"context_bytes": 1024},
"stop_conditions": {"max_reads": 1},
},
作品ID="history-work",
),
"synthetic-history",
"1",
)
领取 = 运行.领取步骤("history-test", ["synthetic-history"], 租期秒=120)
assert 领取 is not None and 领取.任务ID == id_
return id_, 领取, 装配.要求只读工具(领取)
return 装配, 作者, 开始
def test_历史正文经真实任务读取留痕并进入材料预算__b09101(历史环境):
装配, _, 开始 = 历史环境
task_id, 领取, _ = 开始()
结果 = 装配.任务运行.执行一步(领取)
assert 结果.输出["文本"] == 原文
assert 结果.输出["字节数"] == len(原文.encode("utf-8"))
事件 = 装配.任务运行.续接事件(task_id).事件
读取 = [e for e in 事件 if e.类型 == 事件类型.工具读取]
assert len(读取) == 1
引用 = 读取[0].载荷["source_refs"][0]
assert 引用["revision"] == "1"
assert 引用["schema_hash"] and 引用["projection_version"]
assert 读取[0].载荷["tool"] == "read_history_document"
def test_正文工具按原文码点返回片段与用途__b09102(历史环境):
_, _, 开始 = 历史环境
_, _, 工具 = 开始()
结果 = 工具.调用(
工具请求(
"slice",
"read_history_document",
{"chapter_id": "past", "revision": 1, "start": 3, "end": 8},
)
)
assert 结果.内容["text"] == "🌧,e\u0301。"
assert (结果.内容["start"], 结果.内容["end"]) == (3, 8)
assert 结果.内容["run_purpose"] == "production"
assert 结果.内容["content_purpose"] == "generation"
@pytest.mark.parametrize(
"章节", ["foreign", "future", "other"], ids=["cross-work", "future", "unauthorized"]
)
def test_正文工具拒绝冻结范围以外的文稿__b09103(历史环境, 章节):
装配, _, 开始 = 历史环境
task_id, _, 工具 = 开始()
with pytest.raises(上下文错误) as 错误:
工具.调用(工具请求("denied", "read_history_document", {"chapter_id": 章节, "revision": 1}))
assert 错误.value.错误码 == "SCOPE_DENIED"
assert not [e for e in 装配.任务运行.续接事件(task_id).事件 if e.类型 == 事件类型.工具读取]
@pytest.mark.parametrize(
"变化", ["document", "directory"], ids=["new-document", "reordered-directory"]
)
def test_正文或目录变化拒绝沿用原任务__b09104(历史环境, 变化):
装配, 作者, 开始 = 历史环境
_, _, 工具 = 开始()
if 变化 == "document":
装配.要求正文().保存人工(
作者, "revision-2", "past", 1, 正文草稿((段落("p-past", (文本节点("改过的正文"),)),))
)
else:
装配.要求作品().重排章节(
作者, "reorder", "history-work", ("future", "other", "past"), 预期目录版本=3
)
with pytest.raises(上下文错误) as 错误:
工具.调用(工具请求("stale", "read_history_document", {"chapter_id": "past", "revision": 1}))
assert 错误.value.错误码 == "SOURCE_STALE"
@pytest.mark.parametrize(
"参数",
[{"chapter_id": "past", "revision": 2}, {"chapter_id": "past", "revision": 1, "end": 1000}],
ids=["unapproved-version", "invalid-span"],
)
def test_未批准版本与越界片段不返回正文__b09105(历史环境, 参数):
_, _, 开始 = 历史环境
_, _, 工具 = 开始()
with pytest.raises(上下文错误) as 错误:
工具.调用(工具请求("invalid", "read_history_document", 参数))
assert 错误.value.错误码 in {"SCOPE_DENIED", "CONTEXT_INPUT_INVALID"}
@pytest.mark.parametrize(
"场景",
["unknown-tool", "work-id", "missing-cutoff", "wrong-author", "missing-version"],
ids=str,
)
def test_未登记工具扩权参数及不完整授权失败关闭__b09106(历史环境, 场景):
装配, 作者, 开始 = 历史环境
if 场景 == "wrong-author":
with pytest.raises(上下文错误) as 错误:
装配.要求上下文().授权历史范围(
replace(作者, 作者="another-author"),
"history-work",
2,
({"chapter_id": "past", "revision": 1},),
)
assert 错误.value.错误码 == "SCOPE_DENIED"
return
if 场景 == "missing-version":
with pytest.raises(上下文错误) as 错误:
装配.要求上下文().授权历史范围(
作者, "history-work", 2, ({"chapter_id": "past", "revision": 99},)
)
assert 错误.value.错误码 == "SOURCE_NOT_FOUND"
return
范围 = 装配.要求上下文().授权历史范围(
作者, "history-work", 2, ({"chapter_id": "past", "revision": 1},)
)
if 场景 == "missing-cutoff":
del 范围["as_of"]
with pytest.raises(上下文错误) as 错误:
开始(范围)
assert 错误.value.错误码 == "CONTEXT_SCOPE_INVALID"
return
_, _, 工具 = 开始(范围)
参数 = {"chapter_id": "past", "revision": 1}
if 场景 == "work-id":
参数["work_id"] = "another-work"
with pytest.raises(模型协议错误):
工具.调用(
工具请求(
"no-tool",
"read_unknown" if 场景 == "unknown-tool" else "read_history_document",
参数,
)
)