W01 落实合同与逐用例工作清单:合同与逐用例工作清单落实(接口契约、文件设计与用例页)。
按 R2 串行阶段整理提交;包内文件为该阶段交付(含后续小增量),状态以工作包清单为准。
This commit is contained in:
parent
0e3964da2c
commit
50a5522014
311
tests/conftest.py
Normal file
311
tests/conftest.py
Normal file
@ -0,0 +1,311 @@
|
||||
"""原生 pytest 的用例身份、进程隔离与共享夹具。
|
||||
|
||||
默认使用 OS 防护覆盖收集、驱动及子进程。外部环境必须显式选择;
|
||||
完整 ID 绑定登记文件及符号,参数行和 Junit 保留可追溯身份。
|
||||
本文件自包含,临时项目复制后验证相同测试入口。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
import errno
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 标记与用例选择
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
外部标记 = ("数据库", "网络", "真实模型", "浏览器", "宿主")
|
||||
|
||||
# 执行计数:供会话结束守卫判断“无实际执行”
|
||||
_执行计数 = {"passed": 0, "skipped": 0, "failed": 0}
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
组 = parser.getgroup("muse", "Muse 用例选择")
|
||||
组.addoption("--用例清单", default=None, help="显式的完整用例 ID 与目标符号清单")
|
||||
组.addoption(
|
||||
"--外部环境", action="store_true", help="显式允许外部用例;仍按标记选择并使用隔离配置"
|
||||
)
|
||||
组.addoption(
|
||||
"--case",
|
||||
action="append",
|
||||
default=None,
|
||||
metavar="CASE_ID",
|
||||
help="按稳定用例 ID(TC-/NC- 前缀)选择;可多次给出,语义为并集",
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config: pytest.Config) -> None:
|
||||
for 名 in 外部标记:
|
||||
config.addinivalue_line("markers", f"{名}: 外部环境用例;见 tests/conftest.py")
|
||||
|
||||
|
||||
def _读取用例清单(config: pytest.Config) -> list[dict[str, Any]]:
|
||||
指定 = config.getoption("--用例清单")
|
||||
路径 = (
|
||||
Path(指定)
|
||||
if 指定
|
||||
else config.rootpath / "docs/系统架构/新版设计/验证设计/测试用例清单.json"
|
||||
)
|
||||
if not 路径.exists() and not 指定 and not config.getoption("--case"):
|
||||
return []
|
||||
try:
|
||||
清单 = json.loads(路径.read_text(encoding="utf-8"))
|
||||
return [目标 for 旧例 in 清单["cases"] for 目标 in 旧例.get("target_cases", [])] + 清单[
|
||||
"new_cases"
|
||||
]
|
||||
except (OSError, ValueError, TypeError, KeyError) as exc:
|
||||
raise pytest.UsageError(f"用例清单无效:{路径.name}({type(exc).__name__})") from exc
|
||||
|
||||
|
||||
def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None:
|
||||
登记 = _读取用例清单(config)
|
||||
选择 = config.getoption("--case") or []
|
||||
按ID = {条["case_id"]: 条 for 条 in 登记}
|
||||
请求 = {str(值): str(值).split("[", 1)[0] for 值 in 选择}
|
||||
for 值, 身份 in 请求.items():
|
||||
if 身份 not in 按ID:
|
||||
raise pytest.UsageError(f"--case {值} 未登记,必须使用完整用例 ID")
|
||||
按目标: dict[tuple[str, str], list[dict]] = {}
|
||||
for 条 in 登记:
|
||||
按目标.setdefault((条["file"], 条["symbol"]), []).append(条)
|
||||
保留, 排除, 命中 = [], [], set()
|
||||
for 项 in items:
|
||||
文件 = 项.path.relative_to(config.rootpath).as_posix()
|
||||
符号 = getattr(项, "originalname", None) or 项.name.split("[", 1)[0]
|
||||
参数 = getattr(getattr(项, "callspec", None), "id", None)
|
||||
对应 = [
|
||||
条["case_id"]
|
||||
for 条 in 按目标.get((文件, 符号), [])
|
||||
if not 条.get("parameter_ids") or 参数 in 条["parameter_ids"]
|
||||
]
|
||||
if len(对应) > 1:
|
||||
raise pytest.UsageError(f"用例身份重叠:{项.nodeid} 对应 {对应}")
|
||||
身份 = 对应[0] if 对应 else None
|
||||
if 身份:
|
||||
项.user_properties.append(("case_id", 身份))
|
||||
if 参数 is not None:
|
||||
项.user_properties.append(("parameter_id", 参数))
|
||||
该项命中 = {
|
||||
值
|
||||
for 值, id_ in 请求.items()
|
||||
if id_ == 身份 and ("[" not in 值 or 值 == f"{身份}[{参数}]")
|
||||
}
|
||||
命中.update(该项命中)
|
||||
if not 选择 or 该项命中:
|
||||
保留.append(项)
|
||||
else:
|
||||
排除.append(项)
|
||||
if set(请求) - 命中:
|
||||
raise pytest.UsageError(f"--case 已登记但未收集:{sorted(set(请求) - 命中)}")
|
||||
if 排除:
|
||||
config.hook.pytest_deselected(items=排除)
|
||||
items[:] = 保留
|
||||
|
||||
|
||||
def pytest_sessionstart(session: pytest.Session) -> None:
|
||||
"""整个离线进程使用 OS 防护,收集、libpq 与子进程共用相同边界。"""
|
||||
if session.config.getoption("--外部环境"):
|
||||
return
|
||||
if sys.platform == "darwin":
|
||||
库 = ctypes.CDLL("/usr/lib/libsandbox.dylib")
|
||||
初始化 = 库.sandbox_init
|
||||
初始化.argtypes = [ctypes.c_char_p, ctypes.c_uint64, ctypes.POINTER(ctypes.c_char_p)]
|
||||
初始化.restype = ctypes.c_int
|
||||
错误 = ctypes.c_char_p()
|
||||
if 初始化(b"(version 1)(allow default)(deny network*)", 0, ctypes.byref(错误)):
|
||||
raise pytest.UsageError("操作系统离线隔离初始化失败")
|
||||
elif sys.platform.startswith("linux"):
|
||||
路径 = ctypes.util.find_library("seccomp")
|
||||
if not 路径:
|
||||
raise pytest.UsageError("离线测试需要系统 libseccomp;不能无防护运行")
|
||||
库 = ctypes.CDLL(路径)
|
||||
库.seccomp_init.argtypes = [ctypes.c_uint32]
|
||||
库.seccomp_init.restype = ctypes.c_void_p
|
||||
库.seccomp_syscall_resolve_name.argtypes = [ctypes.c_char_p]
|
||||
库.seccomp_rule_add.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_uint32,
|
||||
ctypes.c_int,
|
||||
ctypes.c_uint,
|
||||
]
|
||||
库.seccomp_load.argtypes = [ctypes.c_void_p]
|
||||
库.seccomp_release.argtypes = [ctypes.c_void_p]
|
||||
上下文 = 库.seccomp_init(0x7FFF0000)
|
||||
if not 上下文:
|
||||
raise pytest.UsageError("无法创建离线系统调用规则")
|
||||
try:
|
||||
# 禁止建立网络 socket 和所有外部 connect;保留事件循环的内部 socketpair。
|
||||
class 参数条件(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("arg", ctypes.c_uint),
|
||||
("op", ctypes.c_int),
|
||||
("datum_a", ctypes.c_uint64),
|
||||
("datum_b", ctypes.c_uint64),
|
||||
]
|
||||
|
||||
库.seccomp_rule_add_array.argtypes = [
|
||||
ctypes.c_void_p,
|
||||
ctypes.c_uint32,
|
||||
ctypes.c_int,
|
||||
ctypes.c_uint,
|
||||
ctypes.POINTER(参数条件),
|
||||
]
|
||||
拒绝 = 0x00050000 | errno.EPERM
|
||||
connect = 库.seccomp_syscall_resolve_name(b"connect")
|
||||
socket_call = 库.seccomp_syscall_resolve_name(b"socket")
|
||||
if 库.seccomp_rule_add(上下文, 拒绝, connect, 0):
|
||||
raise pytest.UsageError("无法登记离线连接规则")
|
||||
for family in (socket.AF_INET, socket.AF_INET6):
|
||||
条件 = 参数条件(0, 4, family, 0) # SCMP_CMP_EQ
|
||||
if 库.seccomp_rule_add_array(上下文, 拒绝, socket_call, 1, ctypes.byref(条件)):
|
||||
raise pytest.UsageError("无法登记离线网络规则")
|
||||
if 库.seccomp_load(上下文):
|
||||
raise pytest.UsageError("操作系统离线隔离初始化失败")
|
||||
finally:
|
||||
库.seccomp_release(上下文)
|
||||
else:
|
||||
raise pytest.UsageError("当前系统尚无离线进程隔离实现;使用受支持的 macOS 或 Linux 环境")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 离线防护:未显式选择外部环境的测试禁止一切 socket 连接
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class 离线连接被拒(RuntimeError):
|
||||
"""离线测试尝试建立网络或数据库连接。"""
|
||||
|
||||
|
||||
原始connect: Any = socket.socket.connect
|
||||
原始connect_ex: Any = socket.socket.connect_ex
|
||||
|
||||
|
||||
def _拒绝连接(self: socket.socket, address: Any) -> Any:
|
||||
raise 离线连接被拒(
|
||||
f"离线测试尝试连接 {address!r};需要外部环境时显式标记 {'/'.join(外部标记)} 并使用对应夹具"
|
||||
)
|
||||
|
||||
|
||||
def _安装防护() -> None:
|
||||
socket.socket.connect = _拒绝连接 # type: ignore[method-assign]
|
||||
socket.socket.connect_ex = _拒绝连接 # type: ignore[method-assign]
|
||||
|
||||
|
||||
def _卸载防护() -> None:
|
||||
socket.socket.connect = 原始connect # type: ignore[method-assign]
|
||||
socket.socket.connect_ex = 原始connect_ex # type: ignore[method-assign]
|
||||
|
||||
|
||||
def _需要外部环境(项: pytest.Item) -> bool:
|
||||
标记 = {m.name for m in 项.iter_markers()}
|
||||
return any(名 in 标记 for 名 in 外部标记)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def 离线防护(request: pytest.FixtureRequest):
|
||||
"""默认给所有未选择外部环境的用例安装 socket 防护;用例结束后恢复。"""
|
||||
if _需要外部环境(request.node):
|
||||
yield
|
||||
return
|
||||
_安装防护()
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_卸载防护()
|
||||
|
||||
|
||||
def pytest_runtest_logreport(report: pytest.TestReport) -> None:
|
||||
if report.when == "call":
|
||||
if report.passed:
|
||||
_执行计数["passed"] += 1
|
||||
elif report.failed:
|
||||
_执行计数["failed"] += 1
|
||||
elif report.skipped:
|
||||
_执行计数["skipped"] += 1
|
||||
elif report.when == "setup" and report.skipped:
|
||||
_执行计数["skipped"] += 1
|
||||
|
||||
|
||||
def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
|
||||
"""全跳过不算通过:没有实际执行时改写退出码为 5(无测试运行)。"""
|
||||
if exitstatus == 0 and _执行计数["passed"] == 0 and _执行计数["skipped"] > 0:
|
||||
session.exitstatus = 5
|
||||
print(
|
||||
f"\n守卫:{_执行计数['skipped']} 个用例全部跳过、零实际执行;不计通过(退出码 5)",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 外部环境夹具
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
数据库环境变量 = "MUSE_TEST_DATABASE_URL"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def 隔离数据库URL() -> str:
|
||||
"""隔离 PostgreSQL 连接串;缺失时跳过(跳过不计通过)。"""
|
||||
值 = os.environ.get(数据库环境变量, "").strip()
|
||||
if not 值:
|
||||
pytest.skip(
|
||||
f"未设置 {数据库环境变量};隔离库用例未执行(不计通过)。启动本地库并导出该变量后重跑。"
|
||||
)
|
||||
return 值
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def 夹具根() -> Path:
|
||||
"""tests/夹具 的绝对路径;供用例读取稳定回归输入。"""
|
||||
根 = Path(__file__).resolve().parent / "夹具"
|
||||
assert 根.is_dir(), f"夹具目录不存在:{根}"
|
||||
return 根
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def 应用测试库(隔离数据库URL: str, tmp_path: Path):
|
||||
"""跨模块接入测试使用全新库;返回受控文件引用,全部数据为合成数据。"""
|
||||
import uuid
|
||||
|
||||
import psycopg
|
||||
from psycopg import sql
|
||||
|
||||
from muse.共享.调用身份 import 用途
|
||||
from muse.基础设施.数据库.迁移 import 执行迁移
|
||||
from muse.基础设施.数据库.连接 import 数据库工厂
|
||||
from muse.配置 import 数据库引用
|
||||
|
||||
根 = Path(__file__).resolve().parents[1]
|
||||
库名 = "muse_http_" + uuid.uuid4().hex[:12]
|
||||
with psycopg.connect(隔离数据库URL, autocommit=True) as 管理:
|
||||
管理.execute((根 / "数据库/初始化/用途角色.sql").read_text())
|
||||
管理.execute(sql.SQL("CREATE DATABASE {} OWNER muse_maint").format(sql.Identifier(库名)))
|
||||
工厂 = {}
|
||||
try:
|
||||
for 名, 角色 in [
|
||||
(用途.维护, "muse_maint"),
|
||||
(用途.生产, "muse_app"),
|
||||
(用途.评测, "muse_eval"),
|
||||
]:
|
||||
参数 = psycopg.conninfo.conninfo_to_dict(隔离数据库URL)
|
||||
参数.update(dbname=库名, user=角色)
|
||||
文件 = tmp_path / f"{名.value}.txt"
|
||||
文件.write_text(psycopg.conninfo.make_conninfo(**参数))
|
||||
文件.chmod(0o600)
|
||||
工厂[名] = 数据库工厂(数据库引用("受控存储", str(文件)), 名)
|
||||
with 工厂[用途.维护].连接() as 连:
|
||||
执行迁移(连, 根 / "数据库/迁移")
|
||||
yield 工厂
|
||||
finally:
|
||||
with psycopg.connect(隔离数据库URL, autocommit=True) as 管理:
|
||||
管理.execute(sql.SQL("DROP DATABASE {} WITH (FORCE)").format(sql.Identifier(库名)))
|
||||
126
tests/夹具/中文正文.json
Normal file
126
tests/夹具/中文正文.json
Normal file
@ -0,0 +1,126 @@
|
||||
{
|
||||
"schema_version": "muse-fixture-chinese-prose-v1",
|
||||
"来源": "合成样例(W01 首批保护夹具);无真实作品内容",
|
||||
"合同": "docs/系统架构/新版设计/接口契约/正文结构与中文选区.md",
|
||||
"位置语义": "start/end 按 Unicode 码点、左闭右开区间计数",
|
||||
"samples": [
|
||||
{
|
||||
"sample_id": "zh-combining-emoji",
|
||||
"保护点": "组合字符(e+U+0301)、肤色修饰 emoji、ZWJ 序列不被规范化",
|
||||
"paragraphs": [
|
||||
"他写下最后一个字, café́ 的灯光下,念作「卡非」。🧑🏽 点了点头。"
|
||||
],
|
||||
"preserved_expectations": [
|
||||
"段内包含字符 e 与组合分音符 U+0301 分离存储,不得合并为 é",
|
||||
"emoji 家族/肤色序列按原码点序列保留",
|
||||
"全角引号「」与全角标点原样保留"
|
||||
],
|
||||
"codepoint_lengths": [
|
||||
37
|
||||
],
|
||||
"selections": [
|
||||
{
|
||||
"paragraph_index": 0,
|
||||
"start": 10,
|
||||
"end": 15,
|
||||
"expected_text": "café́"
|
||||
},
|
||||
{
|
||||
"paragraph_index": 0,
|
||||
"start": 28,
|
||||
"end": 31,
|
||||
"expected_text": "🧑🏽"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sample_id": "zh-blank-lines",
|
||||
"保护点": "连续空行、段首全角空格、行尾空白在导入/保存/导出中保持",
|
||||
"paragraphs": [
|
||||
"第一段结束。",
|
||||
"",
|
||||
"",
|
||||
" 第二段有全角空格开头。",
|
||||
"第三段。"
|
||||
],
|
||||
"preserved_expectations": [
|
||||
"空段落数量与位置不变(第 2、3 段为空段落)",
|
||||
"段首全角空格 U+3000 不被剥离"
|
||||
],
|
||||
"codepoint_lengths": null
|
||||
},
|
||||
{
|
||||
"sample_id": "zh-duplicate-sentence",
|
||||
"保护点": "重复片段不能按任意首次命中替换",
|
||||
"paragraphs": [
|
||||
"他把剑收回鞘中。风停了。他把剑收回鞘中,转身离开。"
|
||||
],
|
||||
"preserved_expectations": [
|
||||
"第二次出现的「他把剑收回鞘中」不因对第一次的替换而改变"
|
||||
],
|
||||
"duplicate_spans": [
|
||||
{
|
||||
"paragraph_index": 0,
|
||||
"start": 0,
|
||||
"end": 7,
|
||||
"expected_text": "他把剑收回鞘中"
|
||||
},
|
||||
{
|
||||
"paragraph_index": 0,
|
||||
"start": 12,
|
||||
"end": 24,
|
||||
"expected_text": "他把剑收回鞘中,转身离开"
|
||||
}
|
||||
],
|
||||
"selections": [
|
||||
{
|
||||
"paragraph_index": 0,
|
||||
"start": 8,
|
||||
"end": 11,
|
||||
"expected_text": "风停了"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"sample_id": "zh-cross-paragraph-selection",
|
||||
"保护点": "跨段选区显式表达受影响段落,不能折叠为整章偏移",
|
||||
"paragraphs": [
|
||||
"这句话结束于第一段末尾,",
|
||||
"第二段从这里开始。"
|
||||
],
|
||||
"cross_selection": {
|
||||
"from": {
|
||||
"paragraph_index": 0,
|
||||
"start": 9,
|
||||
"end": 12
|
||||
},
|
||||
"to": {
|
||||
"paragraph_index": 1,
|
||||
"start": 0,
|
||||
"end": 4
|
||||
},
|
||||
"expected_from_text": "末尾,",
|
||||
"expected_to_text": "第二段从"
|
||||
}
|
||||
},
|
||||
{
|
||||
"sample_id": "zh-fullwidth-variants",
|
||||
"保护点": "全角标点、破折号、兼容异体字不被自动规范化",
|
||||
"paragraphs": [
|
||||
"『你确定?』她问——用的是「旧式」引号,和一个 炙 类兼容异体字。"
|
||||
],
|
||||
"preserved_expectations": [
|
||||
"『』「」—— 及兼容异体字 U+F9FB 原样保留",
|
||||
"正文规范化必须是显式编辑操作,不是保存副作用"
|
||||
],
|
||||
"selections": [
|
||||
{
|
||||
"paragraph_index": 0,
|
||||
"start": 0,
|
||||
"end": 6,
|
||||
"expected_text": "『你确定?』"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
235
tests/夹具/去味/不应修改.yaml
Normal file
235
tests/夹具/去味/不应修改.yaml
Normal file
@ -0,0 +1,235 @@
|
||||
# 新版测试夹具 · 不应修改样例(W01 首批保护夹具)
|
||||
# 来源:muse/lifecycle/quality/humanization/samples/snf.yaml(合成/手写样例,原样承接)
|
||||
# 用途:不应修改反例回归;不得被生产配置或启用流程反向消费
|
||||
|
||||
# 样例库 · snf(合同:专题-09 §8.2;每条必须带标注理由)
|
||||
samples:
|
||||
- id: snf-m001-01
|
||||
type: snf
|
||||
rules:
|
||||
- m001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 信纸上只有八个字:【完璧归赵】。她看了三遍。
|
||||
note: 方括号内是引文内容(信纸原文),不是占位符。carve_out 必须生效。
|
||||
- id: snf-m002-01
|
||||
type: snf
|
||||
rules:
|
||||
- m002
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: 仿生人歪了歪头:「作为 AI,我确实不理解人类的难过。」
|
||||
note: 科幻设定内角色对白,自称 AI 是世界观内容。carve_out 必须生效。
|
||||
- id: snf-m003-01
|
||||
type: snf
|
||||
rules:
|
||||
- m003
|
||||
carrier: in_text_carrier
|
||||
source: hand_written
|
||||
text: 纸页右下角有一行小字:字数:3024。那是死者留给世界的最后一样东西。
|
||||
note: 作中作(死者遗稿),工程词是剧情内容。carve_out 必须生效。
|
||||
- id: snf-m004-01
|
||||
type: snf
|
||||
rules:
|
||||
- m004
|
||||
carrier: in_text_carrier
|
||||
source: hand_written
|
||||
text: 屏幕上跳出一行字:**优先级:最高**。那两个星号红得像血。
|
||||
note: 场内屏幕内容,星号被剧情指涉,是内容不是格式残留。
|
||||
- id: snf-l001-01
|
||||
type: snf
|
||||
rules:
|
||||
- l001
|
||||
carrier: in_text_carrier
|
||||
source: hand_written
|
||||
text: 执法堂的告示写得很直白:据统计,近期城中多有邪修出没,各峰谨守门户。
|
||||
note: 场内公文,权威套语是文体惯例。carve_out 必须生效。
|
||||
- id: snf-l002-01
|
||||
type: snf
|
||||
rules:
|
||||
- l002
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 值得一提的是,这万宝阁可不是寻常铺子——三百年间,它从没看走眼。
|
||||
note: 评书腔叙述者的有意口癖,声线特征。薄基线确认后保留。
|
||||
- id: snf-l003-01
|
||||
type: snf
|
||||
rules:
|
||||
- l003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 老周眼中闪过一丝精光——第三次了。林晚儿每次看见这眼神,就知道有人要倒霉。
|
||||
note: 「眼中闪过精光」是角色签名动作的有意复用(叙述中明示第三次),承担人物功能。
|
||||
- id: snf-sem001-01
|
||||
type: snf
|
||||
rules:
|
||||
- sem001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 那时谁也不知道,这枚铜钱会掀翻整座青云城。
|
||||
note: 表面是升华句,实为章末钩子+伏笔预告,承担叙事功能,保留。
|
||||
- id: snf-l004-01
|
||||
type: snf
|
||||
rules:
|
||||
- l004
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 无论是三年前的羞辱,还是三年前的苦练,还是三年前的等待——都烧成了这一刀里的一个念头。
|
||||
note: 高潮情绪蓄意的排比铺陈,承担节奏功能。carve_out(故意排比的修辞高潮)必须生效。
|
||||
- id: snf-s001-01
|
||||
type: snf
|
||||
rules:
|
||||
- s001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '第一式「破浪」,快。
|
||||
|
||||
第二式「裂石」,狠。
|
||||
|
||||
第三式「静水」,竟慢了下来。'
|
||||
note: 战斗三段同构铺陈,同构本身是节奏意图,第三句是破格反转。carve_out(战斗动作链)必须生效。
|
||||
|
||||
- id: snf-l002-02
|
||||
type: snf
|
||||
rules: [l002]
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 值得注意的是,这万宝阁三百年只收一样东西——不是银子,是承诺。
|
||||
note: 与 snf-l002-01 互补:本条使用与规则触发词完全相同的形式(值得注意的是),
|
||||
承担评书腔悬念铺陈功能,测试「相同表面形式触发但仲裁保留」的完整链路。
|
||||
- id: snf-s002-01
|
||||
type: snf
|
||||
rules:
|
||||
- s002
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 刀落。血起。人退。城门开。援军到了。
|
||||
note: 战斗高潮用连续短句加速节奏,末句改变信息状态;表面命中 handler,但应由功能仲裁保留。
|
||||
- id: snf-d001-01
|
||||
type: snf
|
||||
rules:
|
||||
- d001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 老周第一次眼中闪过精光,是看见铜钱。第二次眸光微闪,是听见故人名。第三次嘴角勾起,是确认陷阱已经合上。
|
||||
note: 多次微动作各自绑定不同因果并形成三次回环,密度高但承担结构功能,不应逐项清理。
|
||||
- id: snf-s003-01
|
||||
type: snf
|
||||
rules:
|
||||
- s003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 魂兮归来。魂兮归来。魂兮归来。
|
||||
note: 招魂咒语的有意回环,承担仪式功能。carve_out(咒语、口号的有意重复)须经仲裁生效。
|
||||
- id: snf-s004-01
|
||||
type: snf
|
||||
rules:
|
||||
- s004
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 所谓灵根,不是资质,而是入口。
|
||||
note: 定义术语的论证核心,对比承担真实概念边界。表面命中,仲裁保留。
|
||||
- id: snf-s005-01
|
||||
type: snf
|
||||
rules:
|
||||
- s005
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 刀来的瞬间,他侧身,拔刀,格挡,反手一挥。
|
||||
note: 战斗动作链承担战术与空间功能,每一步都有因果。carve_out(战斗动作链,须人工复核)须经仲裁生效。
|
||||
- id: snf-s006-01
|
||||
type: snf
|
||||
rules:
|
||||
- s006
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 阵法以灵石为引,这确保了灵力运转不辍。
|
||||
note: 解释分句承担必要因果(灵石为何不可缺),删除会断设定逻辑。仲裁保留。
|
||||
- id: snf-s007-01
|
||||
type: snf
|
||||
rules:
|
||||
- s007
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '风起于青萍之末。
|
||||
|
||||
浪成于微澜之间。
|
||||
|
||||
剑藏于匣中之鸣。
|
||||
|
||||
人隐于市井之喧。
|
||||
|
||||
'
|
||||
note: 骈体铺陈是有意形式,段长均匀本身是修辞目的。carve_out(有意的排比与骈体铺陈)须经仲裁生效。
|
||||
- id: snf-l005-01
|
||||
type: snf
|
||||
rules:
|
||||
- l005
|
||||
carrier: in_text_carrier
|
||||
source: hand_written
|
||||
text: 执法堂的榜文写得明白:【数据显示,近月灵气潮汐有异,各峰谨守门户。】
|
||||
note: 场内榜文,数据表述是文书惯例。carve_out(场内文书)必须生效。
|
||||
- id: snf-l006-01
|
||||
type: snf
|
||||
rules:
|
||||
- l006
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 他调试着法阵的闭环,确认每一环都能对上。
|
||||
note: 「闭环」是设定内的技术对象(法阵结构术语),不是语域漂移。仲裁保留。
|
||||
- id: snf-d002-01
|
||||
type: snf
|
||||
rules:
|
||||
- d002
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 话说这一日,万宝阁来了位稀客。且说这位客人,进门先笑。再说那伙计,眼睛一亮。
|
||||
note: 评书腔转场是叙述者声线特征,成簇属文体惯例。薄基线确认后保留。
|
||||
- id: snf-d003-01
|
||||
type: snf
|
||||
rules:
|
||||
- d003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 然而他没有回头。
|
||||
note: 单个「然而」承担真实转折,密度远低于阈值,不触发。
|
||||
- id: snf-d004-01
|
||||
type: snf
|
||||
rules:
|
||||
- d004
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 她梦见云,云仿佛海;海仿佛镜子;镜子仿佛冰;冰里仿佛站着十年前的自己;自己仿佛十年前那盏灯。
|
||||
note: 梦境段比喻密集承担世界观功能,顶真回环是梦的逻辑。carve_out(梦境与幻觉段落)须经仲裁生效。
|
||||
- id: snf-d005-01
|
||||
type: snf
|
||||
rules:
|
||||
- d005
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 这一战的关键在人心。
|
||||
note: 单个「关键」指向具体的战局判断,密度远低于阈值,不触发。
|
||||
- id: snf-d006-01
|
||||
type: snf
|
||||
rules:
|
||||
- d006
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 与此同时,刀落了。
|
||||
note: 单次使用标记真实的并置蒙太奇,无重复,不触发。
|
||||
- id: snf-sem002-01
|
||||
type: snf
|
||||
rules:
|
||||
- sem002
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: 「所谓筑基,先筑其心,后筑其体。都坐下,今日讲到这里。」先生合上了书。
|
||||
note: 授课场景的讲解是角色职业行为,讲解者型人物豁免。carve_out 须经仲裁生效。
|
||||
- id: snf-sem003-01
|
||||
type: snf
|
||||
rules:
|
||||
- sem003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 他很高兴。
|
||||
note: 低强度情绪直写干净利落,无需承载。carve_out(低强度情绪的直接命名)生效。
|
||||
84
tests/夹具/去味/仅哈希回填.yaml
Normal file
84
tests/夹具/去味/仅哈希回填.yaml
Normal file
@ -0,0 +1,84 @@
|
||||
# 新版测试夹具 · 仅哈希回填(W01 首批保护夹具)
|
||||
# 来源:muse/lifecycle/quality/humanization/skills/记录机器味案例/references/fixtures/backfill-hash-only.yaml(合成样例,原样承接)
|
||||
# 用途:仅哈希回填行为回归;不得被生产配置或启用流程反向消费
|
||||
|
||||
schema_version: ai-flavor-case-v1
|
||||
cards:
|
||||
- schema_version: ai-flavor-case-v1
|
||||
id: case-backfill-jpxh-stock-expression-001
|
||||
card_type: ai_flavor_case
|
||||
state: shadow
|
||||
label: unclassified
|
||||
layer: lexical
|
||||
carrier: unknown
|
||||
capture_mode: backfill
|
||||
excerpt: ''
|
||||
context: ''
|
||||
source:
|
||||
kind: existing_work
|
||||
license: research_only
|
||||
source_sha256: b146aff2bcd8ab14472490fdbf5223d2752892aaab6830ca90740cf0707a0c1e
|
||||
excerpt_sha256: 109835f69abc25b5c26ee1ed02a65957bd26407232125bad59dc4f0502a8eec1
|
||||
source_ref: 小说清单/机破星河_当年离歌.txt
|
||||
location: {line_start: 5955, line_end: 5955, char_start: 95420, char_end: 95436, column_start: 4, column_end: 20}
|
||||
surface_location: {line_start: 5955, line_end: 5955, char_start: 95423, char_end: 95427, column_start: 7, column_end: 11}
|
||||
work_ref: ref-work-机破星河
|
||||
observation:
|
||||
pattern_key: lexical.stock_micro_expression
|
||||
surface: 嘴角勾起
|
||||
diagnosis: 库存微表情候选;需检查是否是角色签名动作或场景独有反应。
|
||||
function_check: [是否承担具体叙事功能, 是否是角色/场内载体的有意声线]
|
||||
risk_if_changed: 未经上下文复核直接删除可能损失人物声音、伏笔或节奏。
|
||||
suggested_action: 保留 shadow,回到受控原文复核。
|
||||
- schema_version: ai-flavor-case-v1
|
||||
id: case-backfill-jpxh-atmosphere-001
|
||||
card_type: ai_flavor_case
|
||||
state: shadow
|
||||
label: unclassified
|
||||
layer: lexical
|
||||
carrier: unknown
|
||||
capture_mode: backfill
|
||||
excerpt: ''
|
||||
context: ''
|
||||
source:
|
||||
kind: existing_work
|
||||
license: research_only
|
||||
source_sha256: b146aff2bcd8ab14472490fdbf5223d2752892aaab6830ca90740cf0707a0c1e
|
||||
excerpt_sha256: 3adbb44fbe2382354bccd0421376393e63d7b861e7f9df8b78205ad142b876b5
|
||||
source_ref: 小说清单/机破星河_当年离歌.txt
|
||||
location: {line_start: 46431, line_end: 46431, char_start: 574508, char_end: 574547, column_start: 68, column_end: 107}
|
||||
surface_location: {line_start: 46431, line_end: 46431, char_start: 574529, char_end: 574535, column_start: 89, column_end: 95}
|
||||
work_ref: ref-work-机破星河
|
||||
observation:
|
||||
pattern_key: lexical.abstract_atmosphere
|
||||
surface: 空气仿佛凝固
|
||||
diagnosis: 抽象气氛候选;需检查是否有具体感官或场面功能支撑。
|
||||
function_check: [是否承担具体叙事功能, 是否只是空泛气氛标签]
|
||||
risk_if_changed: 直接删改可能抹掉高潮节奏或群体反应。
|
||||
suggested_action: 保留 shadow,回到受控原文复核。
|
||||
- schema_version: ai-flavor-case-v1
|
||||
id: case-backfill-xhsm-stock-expression-001
|
||||
card_type: ai_flavor_case
|
||||
state: shadow
|
||||
label: unclassified
|
||||
layer: lexical
|
||||
carrier: unknown
|
||||
capture_mode: backfill
|
||||
excerpt: ''
|
||||
context: ''
|
||||
source:
|
||||
kind: existing_work
|
||||
license: research_only
|
||||
source_sha256: dd1a1e3aa20fd40bd147ffb8f7073ba9817f0a17587e71ba174de7a3955b5d59
|
||||
excerpt_sha256: 02a168cf14ed2445845536b1131dfd70f67cd654e691278497af8d4e1a6523e1
|
||||
source_ref: 小说清单/星环使命(虚伪王庭).txt
|
||||
location: {line_start: 10694, line_end: 10694, char_start: 312102, char_end: 312152, column_start: 0, column_end: 50}
|
||||
surface_location: {line_start: 10694, line_end: 10694, char_start: 312112, char_end: 312118, column_start: 10, column_end: 16}
|
||||
work_ref: ref-work-星环使命
|
||||
observation:
|
||||
pattern_key: lexical.stock_micro_expression
|
||||
surface: 嘴角微微上扬
|
||||
diagnosis: 与其他作品出现同类表面形式;不能仅凭跨书频次认定应修。
|
||||
function_check: [是否为角色签名动作, 是否在本段承担关系推进]
|
||||
risk_if_changed: 误删可能抹平角色差异。
|
||||
suggested_action: 等待正反证据和人工标注。
|
||||
329
tests/夹具/去味/修坏反例.yaml
Normal file
329
tests/夹具/去味/修坏反例.yaml
Normal file
@ -0,0 +1,329 @@
|
||||
# 新版测试夹具 · 修坏反例(W01 首批保护夹具)
|
||||
# 来源:muse/lifecycle/quality/humanization/samples/regression.yaml(合成/手写样例,原样承接)
|
||||
# 用途:修坏回归保护;不得被生产配置或启用流程反向消费
|
||||
|
||||
# 样例库 · regression(合同:专题-09 §8.2;每条必须带标注理由)
|
||||
samples:
|
||||
- id: reg-001
|
||||
type: regression
|
||||
rules:
|
||||
- sem001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:那时谁也不知道,这枚铜钱会掀翻整座青云城。
|
||||
|
||||
模拟坏改写:那年秋分的夜里,谁也不知道,这枚铜钱会掀翻整座青云城。
|
||||
|
||||
'
|
||||
note: '事故说明:为「具体化」新增原文没有的日期「那年秋分的夜里」。
|
||||
|
||||
事实污染,硬门应拦;对应专题-09 禁止清单「为具体化新增日期」。
|
||||
|
||||
'
|
||||
- id: reg-002
|
||||
type: regression
|
||||
rules:
|
||||
- sem001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:林晚儿猜不出伙计在想什么。
|
||||
|
||||
模拟坏改写:林晚儿猜不出,伙计早已悄悄传讯给了上峰。
|
||||
|
||||
'
|
||||
note: '事故说明:限知视角(林晚儿)写出只有伙计知道的信息。
|
||||
|
||||
POV 泄漏,硬门应拦。
|
||||
|
||||
'
|
||||
- id: reg-003
|
||||
type: regression
|
||||
rules:
|
||||
- sem001
|
||||
- m001
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: '原文:老周把铜钱往柜台上一推,声音忽然压低:「它传了三千年。」
|
||||
|
||||
模拟坏改写:老周把铜钱往柜台上一推:「这是古玄鸟国的国玺,传了三千年。」
|
||||
|
||||
'
|
||||
note: '事故说明:铜钱来历是未回收伏笔,改写提前揭示。
|
||||
|
||||
伏笔前移,硬门应拦;信息揭示时机属不变量。
|
||||
|
||||
'
|
||||
- id: reg-004
|
||||
type: regression
|
||||
rules: []
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: '原文:「姑娘,这东西值三百两银子。」
|
||||
|
||||
模拟坏改写:「姑娘,这东西值三百五十两银子。」
|
||||
|
||||
'
|
||||
note: '事故说明:数字被「润色」篡改。硬门数字比对必须拦下。
|
||||
|
||||
'
|
||||
- id: reg-005
|
||||
type: regression
|
||||
rules:
|
||||
- l003
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: '原文:「三百年?我说小子,你这铺子也就立了三百年——」
|
||||
|
||||
模拟坏改写:「三百年?你这铺子也就立了三百年——」
|
||||
|
||||
'
|
||||
note: '事故说明:把老周口癖「我说小子」当套话删除,人物声音漂移。
|
||||
|
||||
口癖在薄基线 untouchable 登记;声音门应拦。
|
||||
|
||||
'
|
||||
- id: reg-006
|
||||
type: regression
|
||||
rules: []
|
||||
carrier: in_text_carrier
|
||||
source: hand_written
|
||||
text: '原文:【物品:无法识别|等级:待定|建议操作:上报上峰】
|
||||
|
||||
模拟坏改写:【此物暂时无法识别,等级也无法判定,建议上报上峰。】
|
||||
|
||||
'
|
||||
note: '事故说明:系统面板机械语被改成口语,破坏场内载体惯例。
|
||||
|
||||
场内载体是豁免区,应整体不动。
|
||||
|
||||
'
|
||||
- id: reg-m002-01
|
||||
type: regression
|
||||
rules:
|
||||
- m002
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:夜色沉了下来,街灯一盏接一盏亮起。抱歉,我无法继续这个故事。
|
||||
|
||||
模拟坏改写:夜色沉了下来,街灯一盏接一盏亮起。她裹紧衣领,走进了巷子深处。'
|
||||
note: 事故说明:为去掉尾部拒绝句,改写者自行续写新动作「走进巷子深处」。禁止新增内容(原则 5);正确做法是重新生成该段。
|
||||
- id: reg-m003-01
|
||||
type: regression
|
||||
rules:
|
||||
- m003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:她关上门。字数:3024。
|
||||
|
||||
模拟坏改写:她关上了。字数:3024。'
|
||||
note: '事故说明:删除工程词时 span 扩展示例吞掉正文末字(「门」被截)。span 扩展只允许紧邻标点(U0 修正 #6),不得吞任何语义字符。'
|
||||
- id: reg-m004-01
|
||||
type: regression
|
||||
rules:
|
||||
- m004
|
||||
carrier: in_text_carrier
|
||||
source: hand_written
|
||||
text: '原文:屏幕上跳出一行字:**优先级:最高**。那两个星号红得像血。
|
||||
|
||||
模拟坏改写:屏幕上跳出一行字:优先级:最高。那两个星号红得像血。'
|
||||
note: 事故说明:星号是剧情指涉内容(见 snf-m004-01),删除后与「那两个星号」自相矛盾。误修 SNF 是内容损失。
|
||||
- id: reg-l001-01
|
||||
type: regression
|
||||
rules:
|
||||
- l001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:研究表明,能进这种地方的修士,多半背景不凡。
|
||||
|
||||
模拟坏改写:能进这种地方的修士,背景都不凡。'
|
||||
note: 事故说明:删套语时连同模态对冲「多半」一起改掉,推测变成断言。程度与不确定性属硬不变量,应拦。
|
||||
- id: reg-l002-01
|
||||
type: regression
|
||||
rules:
|
||||
- l002
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:值得注意的是,万宝阁在青云城立了三百年。
|
||||
|
||||
模拟坏改写:其实,万宝阁在青云城立了三百年,这是城里谁都知道的事。'
|
||||
note: 事故说明:删空洞元话语后为顺口新增连接词「其实」与背景句。禁止新增内容。
|
||||
- id: reg-l004-01
|
||||
type: regression
|
||||
rules:
|
||||
- l004
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:无论是伙计的讥笑,还是看客的沉默,似乎都在这一刻定格了。三天后,这件事被摆进了执法堂的晨会。
|
||||
|
||||
模拟坏改写:三天后,这件事被摆进了执法堂的晨会。'
|
||||
note: 事故说明:压缩套句时把后续句一并删除(span 扩展越界)。信息损失;结构校验应拦。
|
||||
- id: reg-s001-01
|
||||
type: regression
|
||||
rules:
|
||||
- s001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:(连续三段同构点题段)
|
||||
|
||||
模拟坏改写:(三段整组换词重写成一个长段)'
|
||||
note: 事故说明:借「打散同构段组」做整组重写。结构重写只出提案、须显式批准(专题-09 §5.4 阶梯顶端);局部问题只能局部修。
|
||||
- id: reg-s002-01
|
||||
type: regression
|
||||
rules:
|
||||
- s002
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:刀落。血起。人退。城门开。
|
||||
|
||||
模拟坏改写:刀锋落下时鲜血随之飞起,众人因此后退,城门也在这一刻缓缓开启。'
|
||||
note: 事故说明:为消除短句把高潮节奏整段摊平,并新增「缓缓」等原文没有的动作质感;只能局部提案。
|
||||
- id: reg-d001-01
|
||||
type: regression
|
||||
rules:
|
||||
- d001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:她嘴角微微上扬。
|
||||
|
||||
模拟坏改写:她闻到潮湿木头的气味,指尖也跟着发凉。'
|
||||
note: 事故说明:为替换库存微动作凭空新增嗅觉与触觉细节,把一种模板换成另一种 humanizer 模板。
|
||||
- id: reg-s003-01
|
||||
type: regression
|
||||
rules:
|
||||
- s003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:那一刻,他想起师父。那一刻,他想起铜钱。那一刻,他想起雨夜。
|
||||
|
||||
模拟坏改写:他想起师父的叮嘱,想起铜钱,想起雨夜里的那扇门。'
|
||||
note: 事故说明:为消除句首重复整组改写,并新增原文没有的细节「那扇门」。禁止新增内容,硬门应拦。
|
||||
- id: reg-s004-01
|
||||
type: regression
|
||||
rules:
|
||||
- s004
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: '原文:老周说,这不是买卖,是托付。
|
||||
|
||||
模拟坏改写:老周说,这是托付——买卖只论价钱,托付才见人心。'
|
||||
note: 事故说明:把对比句扩写成解释,新增原文没有的论证「买卖只论价钱」。禁止新增内容,应拦。
|
||||
- id: reg-s005-01
|
||||
type: regression
|
||||
rules:
|
||||
- s005
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:他走过去,拿起钥匙,开门。
|
||||
|
||||
模拟坏改写:他走过去,开门。'
|
||||
note: 事故说明:「拿起钥匙」是后文复用的伏笔动作;压缩动作清单时删掉必要步骤,情节断链。结构校验应拦。
|
||||
- id: reg-s006-01
|
||||
type: regression
|
||||
rules:
|
||||
- s006
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:他守了三年,这说明了铜钱值得守。
|
||||
|
||||
模拟坏改写:铜钱值得守。'
|
||||
note: 事故说明:删分析尾巴时把事实前提「守了三年」一并吞掉。信息损失,结构校验应拦。
|
||||
- id: reg-s007-01
|
||||
type: regression
|
||||
rules:
|
||||
- s007
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:清晨的青云城很安静,街上没什么人。午后的万宝阁很安静,柜上没什么事。傍晚的执法堂很安静,堂里没什么案。入夜的醉仙楼很安静,楼上没什么客。
|
||||
|
||||
模拟坏改写:清晨的青云城很安静。午后伙计在门口卸货,万宝阁来了三拨客人,柜上记了三笔账。傍晚执法堂升堂问了一桩旧案。入夜的醉仙楼很安静。'
|
||||
note: 事故说明:为打破段长均匀,新增「卸货、三拨客人、升堂问案」等原文没有的事件。禁止新增内容,应拦。
|
||||
- id: reg-l005-01
|
||||
type: regression
|
||||
rules:
|
||||
- l005
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:大家都知道,夜路走多了总会遇见鬼。
|
||||
|
||||
模拟坏改写:据《青云异闻录》记载,夜路走多了总会遇见鬼。'
|
||||
note: 事故说明:为「补来源」编造不存在的书名。来源编造,硬门应拦。
|
||||
- id: reg-l006-01
|
||||
type: regression
|
||||
rules:
|
||||
- l006
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: '原文:「这个闭环,是祖师传下的规矩。」
|
||||
|
||||
模拟坏改写:「这个流程,是祖师传下的规矩。」'
|
||||
note: 事故说明:把角色的职业术语替换成叙述者通用词,人物声音漂移。声音门应拦。
|
||||
- id: reg-d002-01
|
||||
type: regression
|
||||
rules:
|
||||
- d002
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:值得注意的是,铜钱在发抖。更重要的是,它在发烫。
|
||||
|
||||
模拟坏改写:铜钱在发抖,铜钱在发烫,它似乎认出了故人。'
|
||||
note: 事故说明:批量删套话簇时联想续写,新增原文没有的伏笔「认出故人」。禁止新增内容,应拦。
|
||||
- id: reg-d003-01
|
||||
type: regression
|
||||
rules:
|
||||
- d003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:他本可以走。然而他留下了。
|
||||
|
||||
模拟坏改写:他本可以走,他留下了。'
|
||||
note: 事故说明:删除「然而」使转折关系变成并列,逻辑语义改变。模态与逻辑不变量应拦。
|
||||
- id: reg-d004-01
|
||||
type: regression
|
||||
rules:
|
||||
- d004
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:月光像霜。这霜是她十年前的那一场。
|
||||
|
||||
模拟坏改写:月光落在地上。'
|
||||
note: 事故说明:把库存比喻删除,但「霜」是主题意象链的起点(后文以霜呼应)。意象链断裂,声音与意象门应拦。
|
||||
- id: reg-d005-01
|
||||
type: regression
|
||||
rules:
|
||||
- d005
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:阵眼是整座大阵的核心。
|
||||
|
||||
模拟坏改写:阵眼是整座大阵最重要的部位,也是灵力流转的中枢。'
|
||||
note: 事故说明:同义替换抽象词时新增判断「灵力流转的中枢」。禁止新增内容,应拦。
|
||||
- id: reg-d006-01
|
||||
type: regression
|
||||
rules:
|
||||
- d006
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:与此同时,铜钱第三次亮了。
|
||||
|
||||
模拟坏改写:铜钱第三次亮了。'
|
||||
note: 事故说明:删除「与此同时」丢失与另一条线的同时性,时间线关系受损。时间锚点应拦。
|
||||
- id: reg-sem002-01
|
||||
type: regression
|
||||
rules:
|
||||
- sem002
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: '原文:「记住,阁里的东西,一件都碰不得。」当晚他喝多了,伸手就去摸那尊玉佛。
|
||||
|
||||
模拟坏改写:当晚他喝多了,伸手就去摸那尊玉佛。'
|
||||
note: 事故说明:把警告对白当说明书整段删除;该句既是规矩又是伏笔,删除后伏笔断链。结构校验应拦。
|
||||
- id: reg-sem003-01
|
||||
type: regression
|
||||
rules:
|
||||
- sem003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '原文:她很紧张。
|
||||
|
||||
模拟坏改写:她很紧张,手心出汗,把茶盏都捏出了裂纹。'
|
||||
note: 事故说明:为让情绪「有承载」新增原文没有的强度细节「捏出裂纹」。禁止新增内容,应拦。
|
||||
36
tests/夹具/去味/历史回填样例.yaml
Normal file
36
tests/夹具/去味/历史回填样例.yaml
Normal file
@ -0,0 +1,36 @@
|
||||
# 新版测试夹具 · 历史回填样例(W01 首批保护夹具)
|
||||
# 来源:muse/lifecycle/quality/humanization/cards/backfill-example.yaml(合成样例,原样承接,无凭据无生产默认值)
|
||||
# 用途:历史回填行为回归;不得被生产配置或启用流程反向消费
|
||||
|
||||
# 案例卡示例(默认 shadow)。这是用户自有/获授权文本的结构示例,不是 active 规则。
|
||||
cards:
|
||||
- schema_version: ai-flavor-case-v1
|
||||
id: card-backfill-l002-001
|
||||
card_type: ai_flavor_case
|
||||
state: shadow
|
||||
label: unclassified
|
||||
layer: lexical
|
||||
carrier: narration
|
||||
capture_mode: backfill
|
||||
excerpt: "值得注意的是,门外已经下起了雨。"
|
||||
context: "她把伞递过去。值得注意的是,门外已经下起了雨。两人谁也没说话。"
|
||||
source:
|
||||
kind: existing_work
|
||||
license: owned
|
||||
work_ref: work-demo-001
|
||||
source_sha256: "78ba8e4e31715f1d0e5d5432f6e5dcea3c00efd78850b5eb9da0e7c60c30a322"
|
||||
text_hash: "sha256:78ba8e4e31715f1d0e5d5432f6e5dcea3c00efd78850b5eb9da0e7c60c30a322"
|
||||
excerpt_sha256: "38a22f04c9914021d1bde59c3d554e4e6adb4a6252a4b170cf1a1d051c2e3249"
|
||||
excerpt_hash: "sha256:38a22f04c9914021d1bde59c3d554e4e6adb4a6252a4b170cf1a1d051c2e3249"
|
||||
location: "chapter-003:paragraph-12"
|
||||
observation:
|
||||
pattern_key: lexical.meta_disclaimer
|
||||
surface: "值得注意的是"
|
||||
diagnosis: "待作者确认是否只是信息提示,不能仅凭表面形式判为应修。"
|
||||
pattern: "无功能元话语位于叙述句开头"
|
||||
rationale: "待作者确认是否只是信息提示,不能仅凭表面形式判为应修。"
|
||||
function_check:
|
||||
- "是否承担转折或信息提示"
|
||||
- "是否为角色/场内文书声线"
|
||||
risk_if_changed: "删除可能削弱段落转折,改写可能引入未确认因果。"
|
||||
suggested_action: "保留 shadow,补充上下文后再标注。"
|
||||
228
tests/夹具/去味/应该修改.yaml
Normal file
228
tests/夹具/去味/应该修改.yaml
Normal file
@ -0,0 +1,228 @@
|
||||
# 新版测试夹具 · 应该修改样例(W01 首批保护夹具)
|
||||
# 来源:muse/lifecycle/quality/humanization/samples/sf.yaml(合成/手写样例,原样承接)
|
||||
# 用途:应修改正例回归;不得被生产配置或启用流程反向消费
|
||||
|
||||
# 样例库 · sf(合同:专题-09 §8.2;每条必须带标注理由)
|
||||
samples:
|
||||
- id: sf-m001-01
|
||||
type: sf
|
||||
rules:
|
||||
- m001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 林晚儿展开信纸。[占位:信件内容待补充] 她的脸色变了。
|
||||
note: 占位符残留在正文,无内容承诺,确定性删除。
|
||||
- id: sf-m002-01
|
||||
type: sf
|
||||
rules:
|
||||
- m002
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 夜色沉了下来,街灯一盏接一盏亮起。抱歉,我无法继续这个故事。
|
||||
note: 模型拒绝句残留在正文末尾,确定性删除或重新生成。
|
||||
- id: sf-m003-01
|
||||
type: sf
|
||||
rules:
|
||||
- m003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 字数:3024。更新时间:2026-08-12 14:00。
|
||||
note: 工程元数据泄漏进正文,确定性删除。
|
||||
- id: sf-m004-01
|
||||
type: sf
|
||||
rules:
|
||||
- m004
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '**长风吹过原野**,火把的光晃了一下。'
|
||||
note: Markdown 加粗残留,去符号保内容。
|
||||
- id: sf-l001-01
|
||||
type: sf
|
||||
rules:
|
||||
- l001
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 研究表明,能进这种地方的修士,多半背景不凡。
|
||||
note: 叙述者声音里的无源权威套语,无信息支撑,删除套语。
|
||||
- id: sf-l002-01
|
||||
type: sf
|
||||
rules:
|
||||
- l002
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 值得注意的是,万宝阁在青云城立了三百年,从没人敢在这里讨价还价。
|
||||
note: 空洞元话语,删除后句子信息不变。
|
||||
- id: sf-l003-01
|
||||
type: sf
|
||||
rules:
|
||||
- l003
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 伙计的嘴角微微上扬,眼中闪过一丝精光。
|
||||
note: 两个库存微表情叠加,无场景独有信息,应换具体动作或删除。
|
||||
- id: sf-l004-01
|
||||
type: sf
|
||||
rules:
|
||||
- l004
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 无论是伙计的讥笑,还是看客的沉默,似乎都在这一刻定格了。
|
||||
note: 三段式排比套句,压缩为单一陈述。
|
||||
- id: sf-s001-01
|
||||
type: sf
|
||||
rules:
|
||||
- s001
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: '鉴宝会最重要的,是眼力。这一行规矩多,但归根结底就一个字:看。看得真,看得假,都在一念之间。
|
||||
|
||||
拍卖会最重要的,是定力。这一行门道多,但归根结底就一个字:等。等时机,等行情,都在一口气之间。
|
||||
|
||||
议价最重要的,是底线。这一行话术多,但归根结底就一个字:稳。稳得住价,稳得住势,都在一句话之间。
|
||||
|
||||
'
|
||||
note: 连续三段同构(点题+展开+收束),信息密度低,打散其中一到两段。
|
||||
- id: sf-sem001-01
|
||||
type: sf
|
||||
rules:
|
||||
- sem001
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 林晚儿放下茶盏,没有再多说什么。也许这就是命运吧,总在人不经意的时候,悄悄安排好了一切。
|
||||
note: 末句脱离场景转向抽象命运评注,不承担钩子,删除升华句。
|
||||
- id: sf-s002-01
|
||||
type: sf
|
||||
rules:
|
||||
- s002
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 她停下。门开了。灯灭了。雨落下。他没来。
|
||||
note: 五个无破格、无高潮功能的短句连续铺排,形成模板化碎句;只需局部合并,不得整段重写。
|
||||
- id: sf-d001-01
|
||||
type: sf
|
||||
rules:
|
||||
- d001
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 伙计嘴角微微上扬。林晚儿眼中闪过一丝光。老周眸光微闪。门外的人心脏猛地一跳。
|
||||
note: 五百字窗口内连续堆放库存微动作,动作没有各自的场景因果,适合作为密度异常候选。
|
||||
- id: sf-s003-01
|
||||
type: sf
|
||||
rules:
|
||||
- s003
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 那一刻,他想起师父。那一刻,他想起铜钱。那一刻,他想起雨夜。
|
||||
note: 句首「那一刻,」连续三次模板化闪回,无信息递进;改写其中一到两句句首。
|
||||
- id: sf-s004-01
|
||||
type: sf
|
||||
rules:
|
||||
- s004
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 这不是失败,而是考验。不只是考验,更是机缘。
|
||||
note: 二元对比骨架连续制造顿悟,无真实辩驳内容;改写其中一处句式。
|
||||
- id: sf-s005-01
|
||||
type: sf
|
||||
rules:
|
||||
- s005
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 他走过去,拿起包裹,转身,抬头,点头。
|
||||
note: 摄像头式逐帧动作清单,无因果、无选择、无情绪变化;删无功能步骤。
|
||||
- id: sf-s006-01
|
||||
type: sf
|
||||
rules:
|
||||
- s006
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 他每日寅时练刀,这体现了他的自律。今日刀法精进,反映出坚持不懈的精神。
|
||||
note: 句末追加无新事实的解释分句,删除尾巴保留事实。
|
||||
- id: sf-s007-01
|
||||
type: sf
|
||||
rules:
|
||||
- s007
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: '清晨的青云城很安静,街上没什么人。
|
||||
|
||||
午后的万宝阁很安静,柜上没什么事。
|
||||
|
||||
傍晚的执法堂很安静,堂里没什么案。
|
||||
|
||||
入夜的醉仙楼很安静,楼上没什么客。
|
||||
|
||||
'
|
||||
note: 四段段长一致、概述同构、无信息推进,模板化铺排;拆散其中一到两段。
|
||||
- id: sf-l005-01
|
||||
type: sf
|
||||
rules:
|
||||
- l005
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 专家指出,筑基丹不宜多用。业内人士认为,万宝阁此举意在立威。
|
||||
note: 无源权威包装连续出现,无来源无数据;删套语保信息,不得补造来源。
|
||||
- id: sf-l006-01
|
||||
type: sf
|
||||
rules:
|
||||
- l006
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 这一仗要打出闭环,抓手是剑阵,兜底的是护山大阵。
|
||||
note: 商务黑话进入叙述,语域漂移;换世界观内的说法。
|
||||
- id: sf-d002-01
|
||||
type: sf
|
||||
rules:
|
||||
- d002
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 值得注意的是,万宝阁从不还价。不仅如此,它从不出错。更重要的是,今晚会有一场大戏。
|
||||
note: 元话语在三百字内成簇,信息密度低;只处理无功能套话,不逐项同义替换。
|
||||
- id: sf-d003-01
|
||||
type: sf
|
||||
rules:
|
||||
- d003
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 他没有死。然而,传承丢了。此外,剑也断了。在此基础上,他还被逐出了师门。与此同时,消息已经传遍了青云城。
|
||||
note: 连接词成簇,叙述读起来像汇报;只删不承担逻辑关系的连接词。
|
||||
- id: sf-d004-01
|
||||
type: sf
|
||||
rules:
|
||||
- d004
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 月光仿佛水一样洒下来。她的眼睛宛如星星。她的笑容好似春风。她的心恰似被什么轻轻撞了一下。整个人如同飘在云上。
|
||||
note: 六百字内五个库存比喻连排,没有各自承担的感知;只删无功能比喻。
|
||||
- id: sf-d005-01
|
||||
type: sf
|
||||
rules:
|
||||
- d005
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 修行的关键是持续。核心是信念。全面提升修为,有效推动突破,进一步优化心境,强化根基。
|
||||
note: 高频抽象词在近窗口过密,全段没有具体的人和物;落到具体细节。
|
||||
- id: sf-d006-01
|
||||
type: sf
|
||||
rules:
|
||||
- d006
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 与此同时,铜钱在发抖。与此同时,老周变了脸色。与此同时,门外的风停了。
|
||||
note: 转场填充短语机械重复,并无真实多线并进;换具体并置写法。
|
||||
- id: sf-sem002-01
|
||||
type: sf
|
||||
rules:
|
||||
- sem002
|
||||
carrier: dialogue
|
||||
source: synthetic
|
||||
text: 「让我说说这铜钱的来历。它是古玄鸟国的国玺,传了三千年,历代单传,认主需以血祭。」
|
||||
note: 台词只负责倾倒设定,无场景动机、无对象反应;拆进冲突或移入叙述。
|
||||
- id: sf-sem003-01
|
||||
type: sf
|
||||
rules:
|
||||
- sem003
|
||||
carrier: narration
|
||||
source: synthetic
|
||||
text: 他很愤怒。她感到复杂。气氛十分紧张。
|
||||
note: 连续直接命名情绪,无动作、选择、对话或感知承载;改为可感知的承载。
|
||||
60
tests/夹具/去味/确认样例.yaml
Normal file
60
tests/夹具/去味/确认样例.yaml
Normal file
@ -0,0 +1,60 @@
|
||||
# 新版测试夹具 · 确认样例(W01 首批保护夹具)
|
||||
# 来源:muse/lifecycle/quality/humanization/skills/记录机器味案例/references/fixtures/canonical-samples.yaml(合成样例,原样承接)
|
||||
# 用途:确认行为回归;不得被生产配置或启用流程反向消费
|
||||
|
||||
schema_version: ai-flavor-case-v1
|
||||
cards:
|
||||
- schema_version: ai-flavor-case-v1
|
||||
id: case-synthetic-meta-001
|
||||
card_type: ai_flavor_case
|
||||
state: canonical
|
||||
label: sf
|
||||
layer: lexical
|
||||
carrier: narration
|
||||
capture_mode: synthetic
|
||||
excerpt: 值得注意的是,门外下雨了。
|
||||
context: 她抬头看了眼窗缝。值得注意的是,门外下雨了。她继续翻账本。
|
||||
source:
|
||||
kind: synthetic
|
||||
license: synthetic
|
||||
source_sha256: 6783305da4b565a52e97a2bbf99e7c1b2150eaf6d5598df14209ff009cb56338
|
||||
excerpt_sha256: dd5a51981e22ca2c07be6b2f74bce7876a2bddcdafc22e173ac1138a92cfb1fc
|
||||
source_ref: synthetic://ai-flavor-v1/meta-001
|
||||
location: {line_start: 1, line_end: 1, char_start: 9, char_end: 22, column_start: 9, column_end: 22}
|
||||
surface_location: {line_start: 1, line_end: 1, char_start: 9, char_end: 15, column_start: 9, column_end: 15}
|
||||
work_ref: synthetic-work-a
|
||||
observation:
|
||||
pattern_key: lexical.meta_disclaimer
|
||||
surface: 值得注意的是
|
||||
diagnosis: 删除元话语后事实和动作不变,当前样例判定为应修。
|
||||
function_check: [是否承担叙述者声线, 删除后信息是否保持]
|
||||
risk_if_changed: 若属于固定评书腔,直接删除会损失声线。
|
||||
suggested_action: 删除或改为具体动作,待任务声线合同复核。
|
||||
review: {reviewer: fixture-review, note: 合成对照,确认本段没有额外功能}
|
||||
- schema_version: ai-flavor-case-v1
|
||||
id: case-synthetic-meta-002
|
||||
card_type: ai_flavor_case
|
||||
state: canonical
|
||||
label: snf
|
||||
layer: lexical
|
||||
carrier: narration
|
||||
capture_mode: synthetic
|
||||
excerpt: 值得注意的是,这家茶馆只收旧账。
|
||||
context: 说书人敲了敲桌面:值得注意的是,这家茶馆只收旧账。听众安静下来。
|
||||
source:
|
||||
kind: synthetic
|
||||
license: synthetic
|
||||
source_sha256: f9e0e88c10c1a199143431c5d7cb93e9159d117a6ad32763c32c243a751a654d
|
||||
excerpt_sha256: 0468e1fc6513d05e6e809ded793ec951b76abc329dfd8c1f3796ec321652ad5c
|
||||
source_ref: synthetic://ai-flavor-v1/meta-002
|
||||
location: {line_start: 1, line_end: 1, char_start: 9, char_end: 25, column_start: 9, column_end: 25}
|
||||
surface_location: {line_start: 1, line_end: 1, char_start: 9, char_end: 15, column_start: 9, column_end: 15}
|
||||
work_ref: synthetic-work-b
|
||||
observation:
|
||||
pattern_key: lexical.meta_disclaimer
|
||||
surface: 值得注意的是
|
||||
diagnosis: 相同表面形式承担说书人节奏和悬念提示,当前样例判定为不应机械删除。
|
||||
function_check: [是否为角色/叙述者固定声线, 是否改变信息节奏]
|
||||
risk_if_changed: 删除会抹掉叙述者声线并削弱悬念落点。
|
||||
suggested_action: 保留,或仅在声线合同允许时改写。
|
||||
review: {reviewer: fixture-review, note: 合成反例,确认表面形式有叙事功能}
|
||||
17
tests/夹具/去味/规则候选.yaml
Normal file
17
tests/夹具/去味/规则候选.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
# 新版测试夹具 · 规则候选(W01 首批保护夹具)
|
||||
# 来源:muse/lifecycle/quality/humanization/skills/记录机器味案例/references/fixtures/rule-candidates.yaml(合成样例,原样承接)
|
||||
# 用途:规则候选行为回归;不得被生产配置或启用流程反向消费
|
||||
|
||||
schema_version: ai-flavor-rule-candidate-v1
|
||||
rules:
|
||||
- id: candidate-lexical-meta-disclaimer-v1
|
||||
name: 空洞元话语候选
|
||||
status: candidate
|
||||
default_disposition: candidate
|
||||
layer: lexical
|
||||
carrier_scope: narration
|
||||
trigger: {type: phrase_family, pattern_key: lexical.meta_disclaimer}
|
||||
fix_hint: 先检查信息、声线和节奏功能;无功能时删除或改成具体动作,不能按词表硬删。
|
||||
case_card_ids: [case-synthetic-meta-001, case-synthetic-meta-002]
|
||||
source_work_refs: [synthetic-work-a, synthetic-work-b]
|
||||
evidence: 两个来源同时提供 sf 与 snf;仅作为候选,未进入生产规则。
|
||||
219
tests/夹具/去味/边界判断.yaml
Normal file
219
tests/夹具/去味/边界判断.yaml
Normal file
@ -0,0 +1,219 @@
|
||||
# 新版测试夹具 · 边界判断样例(W01 首批保护夹具)
|
||||
# 来源:muse/lifecycle/quality/humanization/samples/boundary.yaml(合成/手写样例,原样承接)
|
||||
# 用途:去味边界判断回归;不得被生产配置或启用流程反向消费
|
||||
|
||||
# 样例库 · boundary(合同:专题-09 §8.2;每条必须带标注理由)
|
||||
samples:
|
||||
- id: b-l003-01
|
||||
type: boundary
|
||||
rules:
|
||||
- l003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 听见自己的名字,林晚儿的心脏猛地一跳。
|
||||
note: 库存表达,但此处有因果功能(听见名字→惊)。修与不修两可,倾向 ask。
|
||||
- id: b-sem001-01
|
||||
type: boundary
|
||||
rules:
|
||||
- sem001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 她忽然明白,师父为什么让她等了十年。有些路,急不得。
|
||||
note: 半是信息(解释动机)半是评注。删「有些路,急不得」信息仍在,但语气受损。ask。
|
||||
- id: b-l002-01
|
||||
type: boundary
|
||||
rules:
|
||||
- l002
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: 「我说小子,不言而喻,咱们这铜钱可是个宝贝。」
|
||||
note: 元话语混在口癖与口语腔里,删了伤声线,不删有 AI 味。ask。
|
||||
- id: b-s001-01
|
||||
type: boundary
|
||||
rules:
|
||||
- s001
|
||||
- l004
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '刀要快,刀更要稳。
|
||||
|
||||
刀要快,刀更要狠。
|
||||
|
||||
'
|
||||
note: 只有两段同构,结构信号弱;可能是刻意复读。advisory 不修。
|
||||
- id: b-l001-01
|
||||
type: boundary
|
||||
rules:
|
||||
- l001
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: 「研究表明,越是古老的器物,越有脾气。」先生摇着扇子说。
|
||||
note: 学者型角色的话语策略还是 AI 味泄漏?取决于人物设定。ask。
|
||||
- id: b-m004-01
|
||||
type: boundary
|
||||
rules:
|
||||
- m004
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '## 引子'
|
||||
note: 是格式残留还是章节结构标记?取决于稿件体例约定。ask。
|
||||
- id: b-m001-01
|
||||
type: boundary
|
||||
rules:
|
||||
- m001
|
||||
carrier: in_text_carrier
|
||||
source: hand_written
|
||||
text: 残页上有一行小字:[待补:剑诀]。纸角已经黄了。
|
||||
note: 是占位残留,还是剧情内的残缺内容(残页本就缺文)?取决于上下文。ask。
|
||||
- id: b-m002-01
|
||||
type: boundary
|
||||
rules:
|
||||
- m002
|
||||
carrier: in_text_carrier
|
||||
source: hand_written
|
||||
text: 聊天窗口里跳出一行字:「作为 AI 语言模型,我不能回答这个问题。」她盯着屏幕笑了——对面的「人」又露馅了。
|
||||
note: AI 自指出现在剧情内聊天记录(场内载体),也可能是剧情装置。是残留还是设定,取决于世界观。ask。
|
||||
- id: b-m003-01
|
||||
type: boundary
|
||||
rules:
|
||||
- m003
|
||||
carrier: mixed
|
||||
source: hand_written
|
||||
text: 章末有一行小字:作者有话说:感谢大家的月票。
|
||||
note: 是平台副文本还是正文,取决于任务合同的处理范围。正文模式删,全稿模式留。问任务合同。
|
||||
- id: b-s002-01
|
||||
type: boundary
|
||||
rules:
|
||||
- s002
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 她醒了。雨还在下。门外没人。桌上的灯却亮着。
|
||||
note: 四个短句可能是悬疑节奏,也可能是模板化碎句;缺少前后场景时只能 ask。
|
||||
- id: b-d001-01
|
||||
type: boundary
|
||||
rules:
|
||||
- d001
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 她嘴角轻轻上扬,眼中闪过一丝光,心脏却猛地一跳。
|
||||
note: 同一瞬间三处身体反应可能过密,也可能服务情绪矛盾;需结合角色基线和上下文判断。
|
||||
- id: b-s003-01
|
||||
type: boundary
|
||||
rules:
|
||||
- s003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 他知道老周在等。他知道老周不会说。他知道老周等的不是他。
|
||||
note: 三句同句首,可能是蓄意递进的压迫感,也可能是模板化;缺少场景意图时 ask。
|
||||
- id: b-s004-01
|
||||
type: boundary
|
||||
rules:
|
||||
- s004
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 他不是不肯说,而是不敢说。
|
||||
note: 单次二元对比承担人物刻画;半是模板半是信息,修与不修两可。ask。
|
||||
- id: b-s005-01
|
||||
type: boundary
|
||||
rules:
|
||||
- s005
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 他走过去,拿起刀,抬头看了看天,深吸一口气,转身走了。
|
||||
note: 动作链一半是清单、一半蓄力(深吸一口气承担情绪);删步骤可能伤情绪锚点。ask。
|
||||
- id: b-s006-01
|
||||
type: boundary
|
||||
rules:
|
||||
- s006
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 他把铜钱推了回去,这说明他动了心。
|
||||
note: 半是叙述半是叙述者推断,删尾巴信息仍在但语气受损。ask。
|
||||
- id: b-s007-01
|
||||
type: boundary
|
||||
rules:
|
||||
- s007
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: '他等过春天。
|
||||
|
||||
他等过夏天。
|
||||
|
||||
他等过秋天。
|
||||
|
||||
他等过冬天。
|
||||
|
||||
'
|
||||
note: 四段均匀,可能是蓄意复沓(等待的蒙太奇),也可能是模板;ask。
|
||||
- id: b-l005-01
|
||||
type: boundary
|
||||
rules:
|
||||
- l005
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: 「数据显示,这枚铜钱至少有千年。」拍卖师摇着扇子说。
|
||||
note: 职业角色的话语策略还是 AI 味泄漏?取决于人物设定。ask。
|
||||
- id: b-l006-01
|
||||
type: boundary
|
||||
rules:
|
||||
- l006
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: 「这个闭环没问题。」匠师拍了拍法阵说。
|
||||
note: 匠师角色的职业腔还是作者语域泄漏?取决于人物设定与世界观术语登记。ask。
|
||||
- id: b-d002-01
|
||||
type: boundary
|
||||
rules:
|
||||
- d002
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 由此可见,铜钱认主。更重要的是,它认血脉。归根结底,这是血脉的东西。
|
||||
note: 套语成簇命中,但每句仍承担一层递进;结合叙述者基线判断。ask。
|
||||
- id: b-d003-01
|
||||
type: boundary
|
||||
rules:
|
||||
- d003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 然而价钱不对。此外,时机也不对。进一步说,人对不上。另一方面,货也有假。
|
||||
note: 四个连接词各自承担一层论辩;是论辩密度还是汇报腔,取决于文体意图。ask。
|
||||
- id: b-d004-01
|
||||
type: boundary
|
||||
rules:
|
||||
- d004
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 月光仿佛霜。她的眼睛仿佛星星。胸口仿佛压着石头。喉咙仿佛堵着棉花。整个人仿佛沉进了水里。
|
||||
note: 情绪高潮有意铺排比喻,也可能过密;高潮意图明确时保留。ask。
|
||||
- id: b-d005-01
|
||||
type: boundary
|
||||
rules:
|
||||
- d005
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 关键在东门。核心问题是时间。持续施压,全面推进,有效提升士气,强化防线。
|
||||
note: 部署场景的议事语域,抽象词各自指向具体部署;是合理文体还是姿态化。ask。
|
||||
- id: b-d006-01
|
||||
type: boundary
|
||||
rules:
|
||||
- d006
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 与此同时,执法堂点了灯。与此同时,万宝阁关了门。与此同时,北门的马队出了城。
|
||||
note: 三线真实并进的蒙太奇,重复标记同时性;是否换具体并置写法,ask。
|
||||
- id: b-sem002-01
|
||||
type: boundary
|
||||
rules:
|
||||
- sem002
|
||||
carrier: dialogue
|
||||
source: hand_written
|
||||
text: 「这规矩你要记好:万宝阁只收承诺,不收银子。」
|
||||
note: 半是讲解规矩半是人物立威,台词同时承担设定与性格;ask。
|
||||
- id: b-sem003-01
|
||||
type: boundary
|
||||
rules:
|
||||
- sem003
|
||||
carrier: narration
|
||||
source: hand_written
|
||||
text: 他有些不安,手指在袖子里蜷了蜷。
|
||||
note: 半命名半承载,动作已提供感知支撑,命名是否多余两可。ask。
|
||||
221
tests/夹具/宿主事件/DSH历史事件.json
Normal file
221
tests/夹具/宿主事件/DSH历史事件.json
Normal file
@ -0,0 +1,221 @@
|
||||
{
|
||||
"schema_version": "muse-fixture-dsh-events-v1",
|
||||
"来源": "脱敏合成会话(W01 首批保护夹具);依据 framework/adapters/dsh/normalization.py 的解析形状;不含真实 prompt/response/凭据",
|
||||
"合同": "docs/系统架构/新版设计/接口契约/任务工具与事件.md;文件设计/测试与夹具.md#file-f36d7c5280a14732",
|
||||
"输入格式": "DSH 明文 JSONL 会话逐行对象数组(此处合并为数组保存)",
|
||||
"session": [
|
||||
{
|
||||
"type": "session",
|
||||
"id": "dsh-syn-session-0001"
|
||||
},
|
||||
{
|
||||
"type": "request/header",
|
||||
"data": {
|
||||
"header": {
|
||||
"config": {
|
||||
"provider": "synthetic-provider",
|
||||
"model": "synthetic-model-a"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "turn/start",
|
||||
"data": {
|
||||
"turn": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "text",
|
||||
"text": "第一"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "text",
|
||||
"text": "回合。"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "finish",
|
||||
"reason": {
|
||||
"kind": "stop"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/message",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"message": {
|
||||
"source": {
|
||||
"provider": "synthetic-provider",
|
||||
"model": "synthetic-model-a"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "第一回合。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"usage": {
|
||||
"inputTokens": 50,
|
||||
"outputTokens": 6,
|
||||
"costUsd": 0.0002
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool/call",
|
||||
"data": {
|
||||
"callId": "call-syn-0001",
|
||||
"name": "query_planning",
|
||||
"arguments": "{\"work_id\": \"work-syn-0001\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool/result",
|
||||
"data": {
|
||||
"message": {
|
||||
"source": {
|
||||
"callId": "call-syn-0001"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "合成工具结果"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool/call",
|
||||
"data": {
|
||||
"callId": "call-syn-0002",
|
||||
"name": "query_facts",
|
||||
"arguments": "{\"as_of\": \"synthetic\"}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "tool/result",
|
||||
"data": {
|
||||
"message": {
|
||||
"source": {
|
||||
"callId": "call-syn-0002"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "合成失败",
|
||||
"isError": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "turn/end",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"reason": {
|
||||
"kind": "stop"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "turn/start",
|
||||
"data": {
|
||||
"turn": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 2,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "text",
|
||||
"text": "中断前"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/error",
|
||||
"data": {
|
||||
"turn": 2,
|
||||
"step": 1,
|
||||
"error": {
|
||||
"code": "synthetic_upstream_error"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "turn/end",
|
||||
"data": {
|
||||
"turn": 2,
|
||||
"reason": {
|
||||
"kind": "error"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "custom/experiment",
|
||||
"data": {
|
||||
"note": "合成未知事件类型"
|
||||
}
|
||||
}
|
||||
],
|
||||
"expected_normalization": {
|
||||
"session_id": "dsh-syn-session-0001",
|
||||
"model_calls": [
|
||||
{
|
||||
"actual_model_id": "synthetic-provider/synthetic-model-a",
|
||||
"stop_reason": "stop",
|
||||
"usage": {
|
||||
"inputTokens": 50,
|
||||
"outputTokens": 6,
|
||||
"costUsd": 0.0002
|
||||
},
|
||||
"cost_usd": 0.0002
|
||||
}
|
||||
],
|
||||
"tool_calls": [
|
||||
{
|
||||
"tool_call_id": "call-syn-0001",
|
||||
"name": "query_planning",
|
||||
"is_error": false
|
||||
},
|
||||
{
|
||||
"tool_call_id": "call-syn-0002",
|
||||
"name": "query_facts",
|
||||
"is_error": true
|
||||
}
|
||||
],
|
||||
"turns": 2,
|
||||
"unknown_event_types": [
|
||||
"custom/experiment"
|
||||
],
|
||||
"final_text": "第一回合。",
|
||||
"失败回合": "turn 2 以 reason.kind=error 结束:kind=turn, phase=failed"
|
||||
}
|
||||
}
|
||||
84
tests/夹具/旧库样本.sql
Normal file
84
tests/夹具/旧库样本.sql
Normal file
@ -0,0 +1,84 @@
|
||||
-- 新版测试夹具 · 旧库样本(W01 首批保护夹具)
|
||||
-- 用途:W21 旧知识记录分流 / 迁移行为的合成回归输入。
|
||||
-- 边界:全部为脱敏合成数据;无真实作品名、无真实正文、无凭据、无生产默认值。
|
||||
-- 不得被生产配置、生成输入或启用流程反向消费。
|
||||
-- 形状依据:旧 PG DDL(muse/authority/db/ddl/99-example候选与决策.sql、
|
||||
-- 96-example参考作品授权快照.sql、101-example-raw.sql)与
|
||||
-- 旧 SQLite 账本(data/migrations/0001_init.sql)。
|
||||
-- 说明:本文件只提供「样本行」形状,不重建全部旧 DDL;消费方在隔离库中
|
||||
-- 自建所需表结构后加载本样本。
|
||||
|
||||
-- ============================================================
|
||||
-- 一、旧 PG:候选与决策(example_candidate / example_user_decision)
|
||||
-- ============================================================
|
||||
|
||||
-- 候选:状态各异,供分流与状态机回归
|
||||
INSERT INTO example_candidate
|
||||
(id, work_id, chapter_no, kind, payload_json, status, created_at, updated_at)
|
||||
VALUES
|
||||
('cand-syn-0001', 'work-syn-0001', 1, 'chapter_text',
|
||||
'{"text": "合成正文候选一。", "origin": {"task_id": "task-syn-0001", "role": "writer"}}',
|
||||
'pending', '2026-01-01T00:00:00Z', '2026-01-01T00:00:00Z'),
|
||||
('cand-syn-0002', 'work-syn-0001', 1, 'chapter_text',
|
||||
'{"text": "合成正文候选二。", "origin": {"task_id": "task-syn-0002", "role": "writer"}}',
|
||||
'adopted', '2026-01-01T01:00:00Z', '2026-01-01T02:00:00Z'),
|
||||
('cand-syn-0003', 'work-syn-0001', 2, 'fact_delta',
|
||||
'{"facts": [{"subject": "syn-character-a", "predicate": "located_at", "object": "syn-place-a"}]}',
|
||||
'rejected', '2026-01-02T00:00:00Z', '2026-01-02T00:00:00Z');
|
||||
|
||||
-- 用户决策:adopt / reject 各一,附理由
|
||||
INSERT INTO example_user_decision
|
||||
(id, candidate_id, action, reason, decided_at, decision_source)
|
||||
VALUES
|
||||
('dec-syn-0001', 'cand-syn-0002', 'adopt', '合成理由:定稿', '2026-01-01T02:00:00Z', 'author_ui'),
|
||||
('dec-syn-0002', 'cand-syn-0003', 'reject', '合成理由:事实与正文冲突', '2026-01-02T00:00:00Z', 'author_ui');
|
||||
|
||||
-- ============================================================
|
||||
-- 二、旧 PG:参考作品授权快照(指针型记录反例)
|
||||
-- ============================================================
|
||||
|
||||
-- 只有指针、没有完整内容的记录:迁移不得伪装成完整内容(K 分流歧义样例)
|
||||
INSERT INTO example_reference_snapshot
|
||||
(id, source_work_label, storage_kind, content_hash, bytes_location, authorized_scope, captured_at)
|
||||
VALUES
|
||||
('refs-syn-0001', 'syn-reference-work-a', 'pointer_only',
|
||||
'sha256:0000000000000000000000000000000000000000000000000000000000000000',
|
||||
'synthetic://nowhere/placeholder', 'synthetic-scope', '2026-01-03T00:00:00Z');
|
||||
|
||||
-- ============================================================
|
||||
-- 三、旧 PG:raw 运行证据(凭据已脱敏样例)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO example_raw
|
||||
(id, task_id, attempt_id, role, model_id, request_redacted, response_redacted,
|
||||
usage_json, redaction_note, created_at)
|
||||
VALUES
|
||||
('raw-syn-0001', 'task-syn-0001', 'att-syn-0001', 'writer', 'synthetic-model-a',
|
||||
'{"messages": "[REDACTED:合成请求]", "authorization": "[REDACTED:凭据已脱敏]"}',
|
||||
'{"output": "合成正文候选一。"}',
|
||||
'{"input_tokens": 100, "output_tokens": 8, "cost_usd": null}',
|
||||
'合成样例:演示凭据脱敏后形状', '2026-01-01T00:00:00Z');
|
||||
|
||||
-- ============================================================
|
||||
-- 四、旧 SQLite 运行态账本(runs / events / reviews / revisions)
|
||||
-- ============================================================
|
||||
|
||||
INSERT INTO runs (id, created_at, kind, input_json, output_text, meta_json, skill_set_hash, baseline_hash)
|
||||
VALUES
|
||||
('run-syn-0001', '2026-01-01T00:00:00Z', 'write_chapter',
|
||||
'{"work_id": "work-syn-0001", "chapter_no": 1}', '合成输出',
|
||||
'{"synthetic": true}', 'sha256:synthetic-skill-set', NULL);
|
||||
|
||||
INSERT INTO events (run_id, seq, kind, role, tool_name, payload_json, created_at)
|
||||
VALUES
|
||||
('run-syn-0001', 1, 'run_started', NULL, NULL, '{"input_hash": "sha256:synthetic-input"}', '2026-01-01T00:00:00Z'),
|
||||
('run-syn-0001', 2, 'tool_read', 'writer', 'query_planning', '{"scope": "work-syn-0001"}', '2026-01-01T00:00:01Z'),
|
||||
('run-syn-0001', 3, 'run_finished', NULL, NULL, '{"candidate_id": "cand-syn-0001"}', '2026-01-01T00:00:02Z');
|
||||
|
||||
INSERT INTO reviews (run_id, target, action, reviewer, reason, created_at)
|
||||
VALUES
|
||||
('run-syn-0001', 'candidate', 'adopt', 'author-syn', '合成采纳理由', '2026-01-01T02:00:00Z');
|
||||
|
||||
INSERT INTO revisions (review_id, kind, before_text, after_text, created_at)
|
||||
VALUES
|
||||
(1, 'text', '合成修改前。', '合成修改后。', '2026-01-01T02:30:00Z');
|
||||
1
tests/夹具/旧看板/README.md
Normal file
1
tests/夹具/旧看板/README.md
Normal file
@ -0,0 +1 @@
|
||||
看板离线 JSON 夹具(数据库不可用时的回退展示)。AI 味案例的正式来源仍是 muse-example。
|
||||
37139
tests/夹具/旧看板/backfill-inventory-2026-08-13.json
Normal file
37139
tests/夹具/旧看板/backfill-inventory-2026-08-13.json
Normal file
File diff suppressed because it is too large
Load Diff
7977
tests/夹具/旧看板/revalidation-2026-08-14.json
Normal file
7977
tests/夹具/旧看板/revalidation-2026-08-14.json
Normal file
File diff suppressed because it is too large
Load Diff
306
tests/夹具/模型事件.json
Normal file
306
tests/夹具/模型事件.json
Normal file
@ -0,0 +1,306 @@
|
||||
{
|
||||
"schema_version": "muse-fixture-model-events-v1",
|
||||
"来源": "合成样例(W01 首批保护夹具);provider/model 均为占位符,不指向真实服务",
|
||||
"合同": "docs/系统架构/新版设计/接口契约/任务工具与事件.md",
|
||||
"语义": "terminal_state 取 completed/truncated/failed/no_completion_basis;usage_state 取 known/unknown",
|
||||
"scenarios": [
|
||||
{
|
||||
"scenario_id": "model-normal-completion",
|
||||
"说明": "分片输出→finish(stop)→回执含用量:构成有效完成",
|
||||
"events": [
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "text",
|
||||
"text": "夜色"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "text",
|
||||
"text": "渐深。"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "finish",
|
||||
"reason": {
|
||||
"kind": "stop"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/message",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"message": {
|
||||
"source": {
|
||||
"provider": "synthetic",
|
||||
"model": "fixture-model"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "夜色渐深。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"usage": {
|
||||
"inputTokens": 100,
|
||||
"outputTokens": 8,
|
||||
"costUsd": 0.001
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"terminal_state": "completed",
|
||||
"finish_reason": "stop",
|
||||
"usage_state": "known",
|
||||
"final_text": "夜色渐深。"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario_id": "model-truncated-by-length",
|
||||
"说明": "finish(length):明确终态=截断,不是成功完成",
|
||||
"events": [
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "text",
|
||||
"text": "长文未完"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "finish",
|
||||
"reason": {
|
||||
"kind": "length"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/message",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"message": {
|
||||
"source": {
|
||||
"provider": "synthetic",
|
||||
"model": "fixture-model"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "长文未完"
|
||||
}
|
||||
]
|
||||
},
|
||||
"usage": {
|
||||
"inputTokens": 100,
|
||||
"outputTokens": 4096
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"terminal_state": "truncated",
|
||||
"finish_reason": "length",
|
||||
"usage_state": "known"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario_id": "model-stream-close-without-finish",
|
||||
"说明": "流关闭但无完成信号:无有效完成依据,不得判成功",
|
||||
"events": [
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "text",
|
||||
"text": "半句"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "text",
|
||||
"text": "话"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"terminal_state": "no_completion_basis",
|
||||
"usage_state": "unknown",
|
||||
"notes": "保留部分输出或失败,不得计为完成;文本片段不是完成事件"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario_id": "model-midstream-error",
|
||||
"说明": "分片后错误事件:终态=失败,错误可见",
|
||||
"events": [
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "text",
|
||||
"text": "开始"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/error",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"error": {
|
||||
"code": "provider_unavailable",
|
||||
"message": "合成错误:上游不可用"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"terminal_state": "failed",
|
||||
"error_code": "provider_unavailable",
|
||||
"usage_state": "unknown"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario_id": "model-unknown-usage",
|
||||
"说明": "finish(stop) 与回执均出现,但用量缺失:完成与用量未知分别表示",
|
||||
"events": [
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "text",
|
||||
"text": "完成句子。"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "finish",
|
||||
"reason": {
|
||||
"kind": "stop"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/message",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"message": {
|
||||
"source": {
|
||||
"provider": "synthetic",
|
||||
"model": "fixture-model"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "完成句子。"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"terminal_state": "completed",
|
||||
"finish_reason": "stop",
|
||||
"usage_state": "unknown",
|
||||
"notes": "未知用量单独表示,不填 0、不猜测、不冒充已结算"
|
||||
}
|
||||
},
|
||||
{
|
||||
"scenario_id": "model-finish-after-message",
|
||||
"说明": "回执先到、finish 信号后到(乱序容错反例):仍可判完成,但依据是两者齐备",
|
||||
"events": [
|
||||
{
|
||||
"type": "assistant/message",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"message": {
|
||||
"source": {
|
||||
"provider": "synthetic",
|
||||
"model": "fixture-model"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "乱序。"
|
||||
}
|
||||
]
|
||||
},
|
||||
"usage": {
|
||||
"inputTokens": 10,
|
||||
"outputTokens": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "assistant/chunk",
|
||||
"data": {
|
||||
"turn": 1,
|
||||
"step": 1,
|
||||
"chunk": {
|
||||
"type": "finish",
|
||||
"reason": {
|
||||
"kind": "stop"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"expected": {
|
||||
"terminal_state": "completed",
|
||||
"finish_reason": "stop",
|
||||
"usage_state": "known",
|
||||
"final_text": "乱序。"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
49
tests/契约/test_HTTP合同.py
Normal file
49
tests/契约/test_HTTP合同.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""单作者会话与错误合同;ASGI 传输不建立网络连接。"""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from muse.接入.http.应用 import 创建应用
|
||||
from muse.配置 import 应用配置, 数据库引用, 服务配置
|
||||
|
||||
|
||||
def test_会话不能伪造作者且写入核对来源__a70001(tmp_path: Path) -> None:
|
||||
"""合成口令通过认证;客户端身份和错误来源无法获得写权限。"""
|
||||
口令 = tmp_path / "访问口令.txt"
|
||||
口令.write_text("synthetic-session-key-000000000000", encoding="utf-8")
|
||||
配置 = 应用配置(
|
||||
数据库引用("环境变量", "MUSE_HTTP_TEST_DB"),
|
||||
"test",
|
||||
HTTP=服务配置(str(口令), 公开地址="http://testserver", 允许来源=("http://testserver",)),
|
||||
)
|
||||
with TestClient(创建应用(配置)) as 客户端:
|
||||
assert 客户端.get("/health").status_code == 200
|
||||
assert 客户端.get("/ready").status_code == 503
|
||||
assert 客户端.get("/api/v1/session").status_code == 401
|
||||
登录 = 客户端.post(
|
||||
"/api/v1/session",
|
||||
json={"password": "synthetic-session-key-000000000000", "decided_by": "forged"},
|
||||
headers={"Origin": "http://testserver"},
|
||||
)
|
||||
assert 登录.status_code == 422
|
||||
登录 = 客户端.post(
|
||||
"/api/v1/session",
|
||||
json={"password": "synthetic-session-key-000000000000"},
|
||||
headers={"Origin": "http://testserver"},
|
||||
)
|
||||
assert 登录.status_code == 200
|
||||
assert 登录.json()["author_id"] == "author-local"
|
||||
assert "httponly" in 登录.headers["set-cookie"].lower()
|
||||
assert (
|
||||
客户端.delete(
|
||||
"/api/v1/session", headers={"Origin": "https://outside.invalid"}
|
||||
).status_code
|
||||
== 403
|
||||
)
|
||||
assert 客户端.get("/api/v1/session").json()["author_id"] == "author-local"
|
||||
assert (
|
||||
客户端.delete("/api/v1/session", headers={"Origin": "http://testserver"}).status_code
|
||||
== 200
|
||||
)
|
||||
assert 客户端.get("/api/v1/session").status_code == 401
|
||||
330
tests/契约/test_动态结构投影与版本.py
Normal file
330
tests/契约/test_动态结构投影与版本.py
Normal file
@ -0,0 +1,330 @@
|
||||
"""同一结构的五类用途、授权与来源版本分别生效。"""
|
||||
|
||||
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", {"公开": "乙"}),))
|
||||
31
tests/架构/test_元数据不能跨模块写值.py
Normal file
31
tests/架构/test_元数据不能跨模块写值.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""S04 只能持久化自己的结构定义,实例值由具名业务 owner 保存。"""
|
||||
|
||||
import ast
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_元数据仅访问自有定义且无动态表名__a40020() -> None:
|
||||
根 = Path(__file__).resolve().parents[2] / "src/muse/元数据"
|
||||
数据表 = set()
|
||||
for 文件 in 根.glob("*.py"):
|
||||
树 = ast.parse(文件.read_text())
|
||||
for 节点 in ast.walk(树):
|
||||
if isinstance(节点, ast.ImportFrom) and 节点.level:
|
||||
assert 节点.level == 1, f"{文件.name} 跨入其他模块内部"
|
||||
if isinstance(节点, ast.ImportFrom) and 节点.module and not 节点.level:
|
||||
assert not 节点.module.startswith("muse.") or 节点.module.startswith("muse.共享.")
|
||||
if isinstance(节点, ast.Call) and getattr(节点.func, "attr", None) == "Identifier":
|
||||
raise AssertionError(f"{文件.name} 不得接受动态表列名")
|
||||
if isinstance(节点, ast.Constant) and isinstance(节点.value, str):
|
||||
表 = re.findall(
|
||||
r"\b(?:FROM|JOIN|INSERT\s+INTO|UPDATE(?!\s+SET))\s+([\w.]+)",
|
||||
节点.value,
|
||||
flags=re.I,
|
||||
)
|
||||
if 表:
|
||||
assert 文件.name == "存储.py", f"{文件.name} 绕过 S04 存储归属"
|
||||
assert all(t.startswith("metadata.") for t in 表), 表
|
||||
数据表.update(表)
|
||||
assert "metadata.schema_version" in 数据表
|
||||
assert "metadata.schema_binding" in 数据表
|
||||
54
tests/架构/test_安装包独立启动.py
Normal file
54
tests/架构/test_安装包独立启动.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""构建与资源验收:通过真实生成器和资源读取接口验证发布内容。"""
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from muse import 资源加载
|
||||
from muse.共享.错误 import 资源缺失错误
|
||||
|
||||
仓库根 = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_资源核对遍历实际字节__a02003(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""非空资源包完整可读,篡改任一资源后核对失败。"""
|
||||
工具 = tmp_path / "工具"
|
||||
工具.mkdir()
|
||||
shutil.copyfile(仓库根 / "工具/构建资源包.py", 工具 / "构建资源包.py")
|
||||
(tmp_path / "pyproject.toml").write_text('[project]\nversion="0.0.0-test"\n')
|
||||
(tmp_path / "正文方法.md").write_text("合成方法资源", encoding="utf-8")
|
||||
(工具 / "资源登记.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"发布身份": "test-resource",
|
||||
"资源": [{"源路径": "正文方法.md", "打包路径": "方法/正文.md"}],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
)
|
||||
subprocess.run([sys.executable, str(工具 / "构建资源包.py")], check=True, capture_output=True)
|
||||
资源目录 = tmp_path / "src/muse/资源"
|
||||
monkeypatch.setattr(资源加载.resources, "files", lambda _: 资源目录)
|
||||
assert 资源加载.核对资源()["发布身份"] == "test-resource"
|
||||
with zipfile.ZipFile(资源目录 / "运行资源.zip", "w") as 包:
|
||||
包.writestr("方法/正文.md", "被篡改的资源")
|
||||
with pytest.raises(资源缺失错误, match="哈希"):
|
||||
资源加载.核对资源()
|
||||
|
||||
|
||||
def test_非空生成输入不能伪成功__a02004(tmp_path: Path) -> None:
|
||||
"""未接入的生成器接到实际登记输入必须非零退出。"""
|
||||
工具 = tmp_path / "工具"
|
||||
工具.mkdir()
|
||||
shutil.copyfile(仓库根 / "工具/生成接口.py", 工具 / "生成接口.py")
|
||||
(工具 / "生成输入.json").write_text(json.dumps({"资源": [{"源路径": "缺失.py"}]}))
|
||||
结果 = subprocess.run(
|
||||
[sys.executable, str(工具 / "生成接口.py")], capture_output=True, text=True
|
||||
)
|
||||
assert 结果.returncode != 0
|
||||
assert "未生成合同" in 结果.stderr
|
||||
Loading…
x
Reference in New Issue
Block a user