W01 落实合同与逐用例工作清单:合同与逐用例工作清单落实(接口契约、文件设计与用例页)。

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

311
tests/conftest.py Normal file
View 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(库名)))

View 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": "『你确定?』"
}
]
}
]
}

View 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(低强度情绪的直接命名)生效。

View 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: 等待正反证据和人工标注。

View 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: 事故说明:为让情绪「有承载」新增原文没有的强度细节「捏出裂纹」。禁止新增内容,应拦。

View 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,补充上下文后再标注。"

View 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: 连续直接命名情绪,无动作、选择、对话或感知承载;改为可感知的承载。

View 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: 合成反例,确认表面形式有叙事功能}

View 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;仅作为候选,未进入生产规则。

View 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。

View 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"
}
}

View 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');

View File

@ -0,0 +1 @@
看板离线 JSON 夹具(数据库不可用时的回退展示)。AI 味案例的正式来源仍是 muse-example。

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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": "乱序。"
}
}
]
}

View 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

View 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", {"公开": "乙"}),))

View 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 数据表

View 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