重构(数据库): 迁移链压扁为单基线并精简测试至保留集

- 51 个增量迁移文件压扁为 V0001__基线.sql 完整快照(结构+种子+授权),
  等价门禁:旧链全量执行库与基线库 pg_dump 逐字节一致
- 剔除 pg_dump 固化的 public schema 超级用户归属断言(muse_maint 无权执行)
- 测试删减至保留集:备份往返 2 + 预算 2 + 租约 2 + 基线建库 1
- 租约/预算夹具改共享库,消除按例克隆建库
- 修复共享库三类既有污染:账本注入残留(系统管理)、失败触发器残留(评测)、
  建表残留(正式变更事务),发布包迁移文件名硬编码改动态核对
- 全量数据库验收 722 passed / 0 failed / 0 errors(main 基线为 24F+291E)
- 运行手册登记基线模式改表流程与账本校验和同步
This commit is contained in:
zizi 2026-09-22 10:18:51 +08:00
parent dca0032001
commit 713cc45c63
66 changed files with 10313 additions and 6611 deletions

View File

@ -39,6 +39,26 @@ ps -p "$pid" -o command=
- 版本化范围:`配置/本机正式.toml`、`配置/本机维护.toml` 保存非敏感配置及受控引用;仅创建文件不代表已经提交 Git。 - 版本化范围:`配置/本机正式.toml`、`配置/本机维护.toml` 保存非敏感配置及受控引用;仅创建文件不代表已经提交 Git。
- 本机受控根:`~/.local/share/muse/正式环境/`(目录 0700,凭据文件 0600)。三个数据库角色分别使用 `muse_app.连接.txt`、`muse_eval.连接.txt`、`muse_maint.连接.txt`;工作台使用 `工作台口令.txt`。 - 本机受控根:`~/.local/share/muse/正式环境/`(目录 0700,凭据文件 0600)。三个数据库角色分别使用 `muse_app.连接.txt`、`muse_eval.连接.txt`、`muse_maint.连接.txt`;工作台使用 `工作台口令.txt`。
- 可选连接池参数:在 `["数据库"."连接池"]` 节声明 `最大连接`(1–100)与 `等待秒`(0.1–60.0)可显式覆盖默认值(生产 4、评测 2,等待 2.0 秒);只调优当次部署的借还行为,不改变数据库目标身份。 - 可选连接池参数:在 `["数据库"."连接池"]` 节声明 `最大连接`(1–100)与 `等待秒`(0.1–60.0)可显式覆盖默认值(生产 4、评测 2,等待 2.0 秒);只调优当次部署的借还行为,不改变数据库目标身份。
### 数据库结构变更(基线模式)
自 2026-09 起迁移链已压扁为单基线:`数据库/迁移/V0001__基线.sql` 是全部表结构、系统种子数据与角色授权的完整快照。不再为每个结构变更新增迁移文件。
日常改表流程:
1. 动手前先备份(`muse 备份`);
2. 修改 `V0001__基线.sql`(同步结构、种子或授权变化);
3. 在正式库手动执行对应的 `ALTER TABLE ...` 等语句(以维护角色连接);
4. 跑 `make 数据库测试` 验证底座能用新基线建出可用库。
账本说明:`muse_migration` 表仍存在并登记 V0001 的校验和,用于备份清单核对与 `muse 管理系统状态` 的版本一致性检查。修改基线文件后必须同步更新账本校验和,否则启动诊断会报 `changed_versions`:
```sql
-- 以 muse_maint 连接正式库执行;<sha256> 为新基线文件的 sha256
UPDATE muse_migration SET checksum='<sha256>' WHERE version=1;
```
旧备份兼容:迁移链压扁前创建的备份,其清单登记 51 步账本,与新基线代码不匹配;若需恢复,先切回压扁前的代码版本执行恢复,再重新做一次新格式备份。压扁当日的最后一次旧格式备份建议长期保留。
- infra 凭据副本:SSH `个人-minione-ubuntu-infra-4c16g120g`,目录 `/opt/infra/private/muse-agent-example/`。恢复时复制所需文件到本机受控根并核对权限;禁止输出凭据内容到日志或提交明文凭据。 - infra 凭据副本:SSH `个人-minione-ubuntu-infra-4c16g120g`,目录 `/opt/infra/private/muse-agent-example/`。恢复时复制所需文件到本机受控根并核对权限;禁止输出凭据内容到日志或提交明文凭据。
- 正式连接仅使用 PG `100.64.0.8:5433`,带 `keepalives=1 keepalives_idle=15 keepalives_interval=5 keepalives_count=3`。Redis 与本项目无关。 - 正式连接仅使用 PG `100.64.0.8:5433`,带 `keepalives=1 keepalives_idle=15 keepalives_interval=5 keepalives_count=3`。Redis 与本项目无关。
- 原迁移目录中的库外原文、草稿和切换回执尚未找回。当前文件根是恢复后新建的目录,不代表原文件资产已恢复。 - 原迁移目录中的库外原文、草稿和切换回执尚未找回。当前文件根是恢复后新建的目录,不代表原文件资产已恢复。

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,23 @@
"""备份恢复的核心保护:七域真实往返与拒绝覆盖非空目标。
个人工具的最小安全网——全部作品资产在一个 PG 库里,备份必须能真恢复,
恢复不得覆盖任何已有内容。其余切换/篡改/权限变体用例已按简化决策删除。
"""
from __future__ import annotations from __future__ import annotations
import hashlib
import json import json
from decimal import Decimal from decimal import Decimal
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
import pytest import pytest
from psycopg.conninfo import conninfo_to_dict, make_conninfo
from muse.共享.调用身份 import 用途 from muse.共享.调用身份 import 用途
from muse.基础设施 import 备份恢复
from muse.基础设施.备份恢复 import 创建备份, 备份恢复错误, 恢复备份, 核对备份 from muse.基础设施.备份恢复 import 创建备份, 备份恢复错误, 恢复备份, 核对备份
from muse.基础设施.数据库.维护锁 import 迁移维护锁
from muse.基础设施.数据库.连接 import 数据库工厂 from muse.基础设施.数据库.连接 import 数据库工厂
from muse.资源加载 import 加载清单 from muse.资源加载 import 加载清单
from muse.配置 import 应用配置, 数据库引用 from muse.配置 import 应用配置
pytestmark = pytest.mark.数据库 pytestmark = pytest.mark.数据库
@ -24,47 +26,13 @@ def _资源身份() -> str:
return 加载清单()["构建身份"] return 加载清单()["构建身份"]
敏感标记 = "w29-password-must-never-leak"
def _断言目标尚无迁移表(工厂: 数据库工厂) -> None:
with 工厂.连接() as 连:
assert 连.execute("SELECT to_regclass('public.muse_migration')").fetchone() == (None,)
def _记录客户端动作(monkeypatch: pytest.MonkeyPatch) -> list[str]:
动作: list[str] = []
原始 = 备份恢复._运行客户端
def 记录(命令, 参数, 引用, 动作名):
动作.append(动作名)
原始(命令, 参数, 引用, 动作名)
monkeypatch.setattr(备份恢复, "_运行客户端", 记录)
return 动作
def _记录客户端命令(monkeypatch: pytest.MonkeyPatch) -> list[list[str]]:
命令行 = []
原始 = 备份恢复.subprocess.run
def 记录(args, **kwargs):
命令行.append(list(args))
return 原始(args, **kwargs)
monkeypatch.setattr(备份恢复.subprocess, "run", 记录)
return 命令行
def _应用配置( def _应用配置(
工厂: 数据库工厂, 工厂: 数据库工厂,
raw: Path, raw: Path,
drafts: Path, drafts: Path,
*,
引用: 数据库引用 | None = None,
) -> 应用配置: ) -> 应用配置:
return 应用配置( return 应用配置(
引用 or 工厂.引用, 工厂.引用,
_资源身份(), _资源身份(),
运行用途=用途.维护, 运行用途=用途.维护,
原文暂存=str(raw), 原文暂存=str(raw),
@ -72,16 +40,6 @@ def _应用配置(
) )
def _带敏感值引用(工厂: 数据库工厂, tmp_path: Path, 名称: str) -> 数据库引用:
原串 = Path(工厂.引用.位置).read_text(encoding="utf-8")
参数 = conninfo_to_dict(原串)
参数["password"] = 敏感标记
路径 = tmp_path / 名称
路径.write_text(make_conninfo(**参数), encoding="utf-8")
路径.chmod(0o600)
return 数据库引用("受控存储", str(路径))
def _准备文件(tmp_path: Path, 前缀: str) -> tuple[Path, Path]: def _准备文件(tmp_path: Path, 前缀: str) -> tuple[Path, Path]:
raw = tmp_path / f"{前缀}-raw" raw = tmp_path / f"{前缀}-raw"
drafts = tmp_path / f"{前缀}-drafts" drafts = tmp_path / f"{前缀}-drafts"
@ -126,130 +84,80 @@ def _建立数据库探针(工厂: 数据库工厂) -> None:
) )
@pytest.mark.case_id( def _建立类型探针(维护工厂: 数据库工厂) -> None:
"NC-w29-29b001", """decimal 精度、timestamptz、嵌套 jsonb 与序列推进,供确定性往返用。"""
environment="隔离PG", with 维护工厂.连接() as 连, 连.transaction():
given="显式隔离配置、合成数据与已知发布构建", 连.execute("DROP TABLE IF EXISTS public.w29_seq_probe, public.w29_typed CASCADE")
when="经真实维护、业务或浏览器入口执行并读取持久结果", 连.execute(
then=["完整PG文件权限关系与动态内容实际往返"], """
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口", CREATE TABLE public.w29_typed (
id integer PRIMARY KEY,
at timestamptz NOT NULL,
score numeric(10,4) NOT NULL,
payload jsonb NOT NULL
);
CREATE TABLE public.w29_seq_probe (
id serial PRIMARY KEY,
note text NOT NULL
);
"""
) )
def test_完整PG文件权限关系与动态内容实际往返__29b001( 连.execute(
"INSERT INTO public.w29_typed VALUES (%s,%s,%s,%s)",
(
10,
"2026-07-21T08:00:00+00",
Decimal("1.2300"),
json.dumps({"扩展": {"层级": [1, "二"]}}),
),
)
连.execute(
"INSERT INTO public.w29_typed VALUES (%s,%s,%s,%s)",
(2, "2026-07-20T00:00:00+00", Decimal("0.10"), json.dumps({"序": 2})),
)
@pytest.mark.case_id(
"TC-0f7c45927fbe",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_备份单快照七域往返且同内容重放一致__0f7c45(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path 应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path
) -> None: ) -> None:
"""数据库与文件进入同一份校验清单;关闭连接后从磁盘复验一致。"""
来源工厂 = 应用测试库[用途.维护] 来源工厂 = 应用测试库[用途.维护]
_建立数据库探针(来源工厂) _建立类型探针(来源工厂)
raw, drafts = _准备文件(tmp_path, "source") raw, drafts = _准备文件(tmp_path, "domains")
敏感引用 = _带敏感值引用(来源工厂, tmp_path, "source-secret.txt") 备份目录 = tmp_path / "domains-backup"
来源配置 = _应用配置(来源工厂, raw, drafts, 引用=敏感引用)
备份目录 = tmp_path / "backup"
清单 = 创建备份(来源配置, 备份目录)
目标raw = tmp_path / "restored-raw"
目标drafts = tmp_path / "restored-drafts"
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts)
结果 = 恢复备份(备份目录, 目标配置, 清单["backup_id"])
assert 结果["status"] == "verified"
assert 结果["services_started"] is False
assert 结果["external_model_calls_restored"] is False
assert 核对备份(备份目录) == 清单
assert (目标raw / "供应方原文.bin").read_bytes() == b"\x00\xffraw-evidence\x80"
assert (目标drafts / "第一章 草稿.txt").read_text(encoding="utf-8") == "风从旧城来。\n"
with 新空目标库.工厂.连接() as 连:
行 = 连.execute(
"SELECT dynamic_payload, raw_bytes FROM public.w29_parent WHERE id = 'parent-1'"
).fetchone()
assert 行 == (
{"扩展字段": {"层级": [1, "二", {"保留": True}]}},
b"\x00\xff\x10" + "原文-bytea".encode(),
)
assert 连.execute(
"SELECT audit_note FROM public.w29_child WHERE parent_id = 'parent-1'"
).fetchone() == ("审计理由保留",)
assert 连.execute(
"SELECT has_table_privilege('muse_app', 'public.w29_parent', 'SELECT'), "
"has_table_privilege('muse_eval', 'public.w29_parent', 'SELECT')"
).fetchone() == (True, True)
assert 连.execute(
"SELECT count(*) FROM pg_constraint WHERE conrelid = 'public.w29_child'::regclass "
"AND contype = 'f'"
).fetchone() == (1,)
清单正文 = (备份目录 / "manifest.json").read_text(encoding="utf-8")
assert 敏感标记 not in 清单正文
assert str(raw) not in 清单正文
assert str(drafts) not in 清单正文
assert 来源工厂.引用.位置 not in 清单正文
@pytest.mark.case_id(
"NC-w29-29b002",
environment="隔离PG",
given="显式隔离配置、合成数据与已知发布构建",
when="经真实维护、业务或浏览器入口执行并读取持久结果",
then=["并发业务写锁存在时备份立即失败且无半成品"],
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
)
def test_并发业务写锁存在时备份立即失败且无半成品__29b002(应用测试库: dict, tmp_path: Path) -> None:
维护工厂 = 应用测试库[用途.维护]
_建立数据库探针(维护工厂)
raw, drafts = _准备文件(tmp_path, "locked")
配置 = _应用配置(维护工厂, raw, drafts)
输出 = tmp_path / "locked-backup"
with 应用测试库[用途.生产].连接() as 写连, 写连.transaction():
写连.execute(
"INSERT INTO public.w29_parent VALUES (%s, %s, %s)",
("uncommitted", json.dumps({"状态": "writing"}), b"pending"),
)
with pytest.raises(备份恢复错误, match="备份锁"):
创建备份(配置, 输出, pg_dump命令="should-not-run")
assert not 输出.exists()
@pytest.mark.case_id(
"NC-w29-29b003",
environment="隔离PG",
given="显式隔离配置、合成数据与已知发布构建",
when="经真实维护、业务或浏览器入口执行并读取持久结果",
then=["数据库归档损坏与清单字段篡改均在连接目标前拒绝"],
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
)
def test_数据库归档损坏与清单字段篡改均在连接目标前拒绝__29b003(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path
) -> None:
来源工厂 = 应用测试库[用途.维护]
_建立数据库探针(来源工厂)
raw, drafts = _准备文件(tmp_path, "damaged")
备份目录 = tmp_path / "damaged-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录) 清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
目标raw, 目标drafts = tmp_path / "target-raw", tmp_path / "target-drafts" 复验 = 核对备份(备份目录)
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts) assert 复验 == 清单
assert set(清单["database"]) == {
"archive",
"size",
"sha256",
"source_fingerprint",
"fingerprint_scope",
"server_version_num",
"migrations",
"summary",
}
assert 清单["database"]["migrations"] and 清单["file_archive"]["file_count"] == 2
assert 清单["file_archive"]["roots"] == ["drafts", "raw"]
数据库归档 = 备份目录 / "database.dump" 目标raw, 目标drafts = tmp_path / "domains-target-raw", tmp_path / "domains-target-drafts"
原字节 = 数据库归档.read_bytes() 恢复备份(备份目录, _应用配置(新空目标库.工厂, 目标raw, 目标drafts), 清单["backup_id"])
数据库归档.write_bytes(原字节[:-1] + bytes([原字节[-1] ^ 0xFF])) with 新空目标库.工厂.连接() as 连:
数据库归档.chmod(0o600) assert 连.execute("SELECT count(*) FROM public.w29_typed").fetchone() == (2,)
with pytest.raises(备份恢复错误, match="哈希"):
恢复备份(备份目录, 目标配置, 清单["backup_id"])
assert not 目标raw.exists() and not 目标drafts.exists()
_断言目标尚无迁移表(新空目标库.工厂)
数据库归档.write_bytes(原字节)
数据库归档.chmod(0o600)
清单路径 = 备份目录 / "manifest.json"
篡改 = json.loads(清单路径.read_text(encoding="utf-8"))
篡改["resource_release"] = "tampered-release"
清单路径.write_text(json.dumps(篡改), encoding="utf-8")
清单路径.chmod(0o600)
with pytest.raises(备份恢复错误, match="备份ID"):
核对备份(备份目录)
@pytest.mark.case_id( @pytest.mark.case_id(
"NC-w29-29b004", "NC-w29-29b004",
environment="隔离PG", environment="隔离PG",
given="显式隔离配置、合成数据与已知发布构建", given="显式隔离配置与合成数据",
when="经真实维护、业务或浏览器入口执行并读取持久结果", when="向来源库或已有内容目标发起恢复",
then=["拒绝来源库与已有内容目标且不覆盖文件"], then=["拒绝来源库与已有内容目标且不覆盖文件"],
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口", contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
) )
@ -282,588 +190,3 @@ def test_拒绝来源库与已有内容目标且不覆盖文件__29b004(
with 新空目标库.工厂.连接() as 连: with 新空目标库.工厂.连接() as 连:
assert 连.execute("SELECT value FROM public.keep_me").fetchone() == ("保留",) assert 连.execute("SELECT value FROM public.keep_me").fetchone() == ("保留",)
assert not 目标raw.exists() and not 目标drafts.exists() assert not 目标raw.exists() and not 目标drafts.exists()
@pytest.mark.case_id(
"NC-w29-29b005",
environment="隔离PG",
given="显式隔离配置、合成数据与已知发布构建",
when="经真实维护、业务或浏览器入口执行并读取持久结果",
then=["pg_restore中途失败时单事务回滚并清理文件目标"],
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
)
def test_pg_restore中途失败时单事务回滚并清理文件目标__29b005(
应用测试库: dict,
新空目标库: SimpleNamespace,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
来源工厂 = 应用测试库[用途.维护]
_建立数据库探针(来源工厂)
raw, drafts = _准备文件(tmp_path, "rollback")
备份目录 = tmp_path / "rollback-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
目标raw = tmp_path / "rollback-target-raw"
目标drafts = tmp_path / "rollback-target-drafts"
敏感引用 = _带敏感值引用(新空目标库.工厂, tmp_path, "restore-secret.txt")
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts, 引用=敏感引用)
命令行 = _记录客户端命令(monkeypatch)
原运行 = 备份恢复._运行客户端
def 注入恢复竞争(命令: str, 参数: tuple[str, ...], 引用: 数据库引用, 动作: str) -> None:
if 动作 == "数据库恢复":
with 数据库工厂(引用, 用途.维护).连接() as 连, 连.transaction():
连.execute(
"CREATE TABLE public.muse_migration "
"(version integer PRIMARY KEY, marker text NOT NULL)"
)
连.execute("INSERT INTO public.muse_migration VALUES (9999, 'keep-race-marker')")
原运行(命令, 参数, 引用, 动作)
monkeypatch.setattr(备份恢复, "_运行客户端", 注入恢复竞争)
with pytest.raises(备份恢复错误, match="^数据库恢复客户端失败$") as 捕获:
恢复备份(备份目录, 目标配置, 清单["backup_id"])
assert 命令行
assert 敏感标记 not in str(捕获.value.呈现())
assert 敏感标记 not in caplog.text
assert 敏感标记 not in json.dumps(命令行)
assert not 目标raw.exists() and not 目标drafts.exists()
with 新空目标库.工厂.连接() as 连:
assert 连.execute("SELECT marker FROM public.muse_migration").fetchone() == (
"keep-race-marker",
)
assert 连.execute("SELECT to_regnamespace('metadata')").fetchone() == (None,)
assert 连.execute("SELECT to_regtype('public.muse_purpose')").fetchone() == (None,)
@pytest.mark.case_id(
"NC-w29-29b006",
environment="隔离PG",
given="显式隔离配置、合成数据与已知发布构建",
when="经真实维护、业务或浏览器入口执行并读取持久结果",
then=["错误客户端输出不回显DSN凭据或绝对来源"],
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
)
def test_错误客户端输出不回显DSN凭据或绝对来源__29b006(
应用测试库: dict,
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
caplog: pytest.LogCaptureFixture,
) -> None:
来源工厂 = 应用测试库[用途.维护]
_建立数据库探针(来源工厂)
raw, drafts = _准备文件(tmp_path, "secret")
引用 = _带敏感值引用(来源工厂, tmp_path, "failed-secret.txt")
配置 = _应用配置(来源工厂, raw, drafts, 引用=引用)
假客户端 = tmp_path / "failing-pg-dump"
假客户端.write_text('#!/bin/sh\nprintf "%s" "$PGPASSWORD" >&2\nexit 1\n')
假客户端.chmod(0o700)
命令行 = _记录客户端命令(monkeypatch)
with pytest.raises(备份恢复错误) as 捕获:
创建备份(配置, tmp_path / "failed-backup", pg_dump命令=str(假客户端))
呈现 = json.dumps(捕获.value.呈现(), ensure_ascii=False)
assert 命令行
assert 捕获.value.说明 == "数据库备份客户端失败"
assert 捕获.value.上下文["stderr_sha256"] == hashlib.sha256(敏感标记.encode()).hexdigest()
assert 敏感标记 not in caplog.text
assert 敏感标记 not in json.dumps(命令行)
assert 敏感标记 not in 呈现
assert str(raw) not in 呈现
assert str(drafts) not in 呈现
assert 来源工厂.引用.位置 not in 呈现
assert not (tmp_path / "failed-backup").exists()
@pytest.mark.case_id(
"NC-w29-29b007",
environment="隔离PG",
given="显式隔离配置、合成数据与已知发布构建",
when="经真实维护、业务或浏览器入口执行并读取持久结果",
then=["默认ACL等价归一仍拒绝恢复后额外生产权限"],
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
)
def test_默认ACL等价归一仍拒绝恢复后额外生产权限__29b007(
应用测试库, 新空目标库, tmp_path, monkeypatch
):
raw, drafts = _准备文件(tmp_path, "acl-source")
source = _应用配置(应用测试库[用途.维护], raw, drafts)
archive = tmp_path / "acl-backup"
record = 创建备份(source, archive)
target = _应用配置(新空目标库.工厂, tmp_path / "acl-raw", tmp_path / "acl-drafts")
original = 备份恢复._运行客户端
def changed(command, args, reference, action):
original(command, args, reference, action)
if action == "数据库恢复":
with 数据库工厂(reference, 用途.维护).连接() as conn, conn.transaction():
conn.execute("GRANT SELECT ON public.muse_enablement_receipt TO muse_app")
monkeypatch.setattr(备份恢复, "_运行客户端", changed)
with pytest.raises(备份恢复错误, match="权限或关系摘要不一致"):
恢复备份(archive, target, record["backup_id"])
with 应用测试库[用途.维护].连接() as conn:
assert conn.execute(
"SELECT has_table_privilege('muse_app','public.muse_enablement_receipt','SELECT')"
).fetchone() == (False,)
def _建立类型探针(维护工厂: 数据库工厂) -> None:
"""decimal 精度、timestamptz、嵌套 jsonb 与序列推进,供确定性往返用。"""
with 维护工厂.连接() as 连, 连.transaction():
连.execute("DROP TABLE IF EXISTS public.w29_seq_probe, public.w29_typed CASCADE")
连.execute(
"""
CREATE TABLE public.w29_typed (
id integer PRIMARY KEY,
at timestamptz NOT NULL,
score numeric(10,4) NOT NULL,
payload jsonb NOT NULL
);
CREATE TABLE public.w29_seq_probe (
id serial PRIMARY KEY,
note text NOT NULL
);
"""
)
连.execute(
"INSERT INTO public.w29_typed VALUES (%s,%s,%s,%s)",
(
10,
"2026-07-21T08:00:00+00",
Decimal("1.2300"),
json.dumps({"扩展": {"层级": [1, "二"]}}),
),
)
连.execute(
"INSERT INTO public.w29_typed VALUES (%s,%s,%s,%s)",
(2, "2026-07-20T00:00:00+00", Decimal("0.10"), json.dumps({"序": 2})),
)
连.execute("INSERT INTO public.w29_seq_probe(note) VALUES ('甲'),('乙'),('丙')")
@pytest.mark.case_id(
"TC-e95a51d27a2b",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_相同主键集合的确定性清单与特殊数据库类型往返__e95a51(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path
) -> None:
"""相同内容两次备份得到相同清单与指纹;decimal/timestamptz/jsonb 往返保持值。"""
来源工厂 = 应用测试库[用途.维护]
_建立类型探针(来源工厂)
raw, drafts = _准备文件(tmp_path, "typed")
配置 = _应用配置(来源工厂, raw, drafts)
一 = 创建备份(配置, tmp_path / "typed-backup-1")
二 = 创建备份(配置, tmp_path / "typed-backup-2")
# 表内容、模式、约束与序列摘要按确定性顺序哈希,不因查询返回顺序改变;
# pg_dump 归档字节本身可能带非确定性头,不强求 backup_id 相同。
assert 一["database"]["summary"] == 二["database"]["summary"]
assert 一["database"]["source_fingerprint"] == 二["database"]["source_fingerprint"]
目标raw, 目标drafts = tmp_path / "typed-target-raw", tmp_path / "typed-target-drafts"
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts)
恢复备份(tmp_path / "typed-backup-1", 目标配置, 一["backup_id"])
with 新空目标库.工厂.连接() as 连:
连.execute("SELECT set_config('TimeZone', 'UTC', true)")
assert 连.execute(
"SELECT id, to_char(at AT TIME ZONE 'UTC', 'YYYY-MM-DD HH24:MI:SS'), "
"score::text, payload::text FROM public.w29_typed ORDER BY id"
).fetchall() == [
(2, "2026-07-20 00:00:00", "0.1000", '{"序": 2}'),
(
10,
"2026-07-21 08:00:00",
"1.2300",
'{"扩展": {"层级": [1, "二"]}}',
),
]
@pytest.mark.case_id(
"TC-f14761e9f048",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_备份输出只允许全新目录且不与来源重叠__f14761(应用测试库: dict, tmp_path: Path) -> None:
来源工厂 = 应用测试库[用途.维护]
raw, drafts = _准备文件(tmp_path, "path")
配置 = _应用配置(来源工厂, raw, drafts)
已存在 = tmp_path / "occupied"
已存在.mkdir()
(已存在 / "保留文件").write_text("keep", encoding="utf-8")
with pytest.raises(备份恢复错误, match="全新"):
创建备份(配置, 已存在)
assert (已存在 / "保留文件").read_text(encoding="utf-8") == "keep"
with pytest.raises(备份恢复错误, match="重叠"):
创建备份(配置, raw / "nested-backup")
@pytest.mark.case_id(
"TC-0f7c45927fbe",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_备份单快照七域往返且同内容重放一致__0f7c45(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path
) -> None:
"""数据库与文件七域进入同一份校验清单;关闭连接后从磁盘复验一致。"""
来源工厂 = 应用测试库[用途.维护]
_建立类型探针(来源工厂)
raw, drafts = _准备文件(tmp_path, "domains")
备份目录 = tmp_path / "domains-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
复验 = 核对备份(备份目录)
assert 复验 == 清单
assert set(清单["database"]) == {
"archive",
"size",
"sha256",
"source_fingerprint",
"fingerprint_scope",
"server_version_num",
"migrations",
"summary",
}
assert 清单["database"]["migrations"] and 清单["file_archive"]["file_count"] == 2
assert 清单["file_archive"]["roots"] == ["drafts", "raw"]
目标raw, 目标drafts = tmp_path / "domains-target-raw", tmp_path / "domains-target-drafts"
恢复备份(备份目录, _应用配置(新空目标库.工厂, 目标raw, 目标drafts), 清单["backup_id"])
with 新空目标库.工厂.连接() as 连:
assert 连.execute("SELECT count(*) FROM public.w29_typed").fetchone() == (2,)
@pytest.mark.case_id(
"TC-9a68f7f96bf6",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_文件归档意外损坏后离线核对失败__9a68f7(应用测试库: dict, tmp_path: Path) -> None:
来源工厂 = 应用测试库[用途.维护]
raw, drafts = _准备文件(tmp_path, "corrupt")
备份目录 = tmp_path / "corrupt-backup"
创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
原文 = 备份目录 / "files" / "data" / "raw" / "供应方原文.bin"
原文.write_bytes(原文.read_bytes() + b"\x00tampered")
with pytest.raises(备份恢复错误):
核对备份(备份目录)
@pytest.mark.case_id(
"TC-545fb1541c3a",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_备份清单拒绝重复键和未知字段__545fb1(应用测试库: dict, tmp_path: Path) -> None:
来源工厂 = 应用测试库[用途.维护]
raw, drafts = _准备文件(tmp_path, "contract")
备份目录 = tmp_path / "contract-backup"
创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
清单路径 = 备份目录 / "manifest.json"
原文 = 清单路径.read_text(encoding="utf-8")
重复 = 原文.replace('"backup_id":"', '"backup_id":"", "backup_id":"', 1)
清单路径.write_text(重复, encoding="utf-8")
清单路径.chmod(0o600)
with pytest.raises(备份恢复错误, match="重复字段"):
核对备份(备份目录)
清单路径.write_text(原文, encoding="utf-8")
清单路径.chmod(0o600)
未知 = 原文.rstrip()[:-1] + ',"extra_field":1}'
清单路径.write_text(未知, encoding="utf-8")
清单路径.chmod(0o600)
with pytest.raises(备份恢复错误):
核对备份(备份目录)
@pytest.mark.case_id(
"TC-2d7f171ffd33",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_资源构建身份漂移在连接前拒绝恢复__2d7f17(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch
) -> None:
来源工厂 = 应用测试库[用途.维护]
raw, drafts = _准备文件(tmp_path, "identity")
备份目录 = tmp_path / "identity-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
目标raw, 目标drafts = tmp_path / "identity-target-raw", tmp_path / "identity-target-drafts"
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts)
monkeypatch.setattr(
备份恢复,
"_核对安装",
lambda 配置: {"发布身份": "wrong-release", "构建身份": "0" * 64},
)
with pytest.raises(备份恢复错误, match="构建身份不属于此备份"):
恢复备份(备份目录, 目标配置, 清单["backup_id"], pg_restore命令="never")
assert not 目标raw.exists() and not 目标drafts.exists()
_断言目标尚无迁移表(新空目标库.工厂)
@pytest.mark.case_id(
"TC-7c7fadfd3680",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_占用的恢复文件目标在破坏性写入前拒绝__7c7fad(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch
) -> None:
来源工厂 = 应用测试库[用途.维护]
raw, drafts = _准备文件(tmp_path, "occupied-target")
备份目录 = tmp_path / "occupied-target-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
占用根 = tmp_path / "occupied-target-root"
占用根.mkdir()
目标配置 = _应用配置(新空目标库.工厂, 占用根, tmp_path / "occupied-target-drafts")
动作 = _记录客户端动作(monkeypatch)
with pytest.raises(备份恢复错误, match="恢复文件目标必须全新"):
恢复备份(备份目录, 目标配置, 清单["backup_id"], pg_restore命令="should-not-run")
assert "数据库恢复" not in 动作
_断言目标尚无迁移表(新空目标库.工厂)
@pytest.mark.case_id(
"TC-2b32b67a3482",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_恢复要求资源发布身份与安装一致并在连接前拒绝__2b32b6(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch
) -> None:
来源工厂 = 应用测试库[用途.维护]
raw, drafts = _准备文件(tmp_path, "code-identity")
备份目录 = tmp_path / "code-identity-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
目标raw, 目标drafts = tmp_path / "code-id-raw", tmp_path / "code-id-drafts"
错误配置 = 应用配置(
新空目标库.工厂.引用,
"wrong-build-identity",
运行用途=用途.维护,
原文暂存=str(目标raw),
探索草稿=str(目标drafts),
)
动作 = _记录客户端动作(monkeypatch)
with pytest.raises(备份恢复错误, match="当前程序、资源或配置不一致"):
恢复备份(备份目录, 错误配置, 清单["backup_id"], pg_restore命令="never")
assert 动作 == []
assert not 目标raw.exists() and not 目标drafts.exists()
_断言目标尚无迁移表(新空目标库.工厂)
@pytest.mark.case_id(
"TC-863e5ced39df",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_备份确认不匹配不创建业务连接__863e5c(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch
) -> None:
来源工厂 = 应用测试库[用途.维护]
raw, drafts = _准备文件(tmp_path, "confirm")
备份目录 = tmp_path / "confirm-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
目标raw, 目标drafts = tmp_path / "confirm-target-raw", tmp_path / "confirm-target-drafts"
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts)
动作 = _记录客户端动作(monkeypatch)
with pytest.raises(备份恢复错误, match="预期备份ID"):
恢复备份(备份目录, 目标配置, "bkp-" + "0" * 40, pg_restore命令="never")
assert 动作 == []
assert not 目标raw.exists() and not 目标drafts.exists()
_断言目标尚无迁移表(新空目标库.工厂)
assert 清单["backup_id"].startswith("bkp-")
@pytest.mark.case_id(
"TC-c2a43647c057",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_备份迁移与安装清单漂移在恢复任何客户端前拒绝__c2a436(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch
) -> None:
"""正文/字段/数据库身份类漂移由迁移版本与校验和清单承接:不一致在连接前拒绝。"""
来源工厂 = 应用测试库[用途.维护]
raw, drafts = _准备文件(tmp_path, "migration-drift")
备份目录 = tmp_path / "migration-drift-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
目标raw, 目标drafts = (
tmp_path / "migration-drift-target-raw",
tmp_path / "migration-drift-target-drafts",
)
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts)
monkeypatch.setattr(
备份恢复,
"_安装迁移清单",
lambda: [{"version": 999, "name": "V999__fake.sql", "checksum": "0" * 64}],
)
动作 = _记录客户端动作(monkeypatch)
with pytest.raises(备份恢复错误, match="迁移版本或校验和"):
恢复备份(备份目录, 目标配置, 清单["backup_id"], pg_restore命令="never")
assert 动作 == []
assert not 目标raw.exists() and not 目标drafts.exists()
_断言目标尚无迁移表(新空目标库.工厂)
@pytest.mark.case_id(
"TC-a0e1e2234d2d",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_缺少可用备份在取锁连接前失败__a0e1e2(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch
) -> None:
raw, drafts = _准备文件(tmp_path, "missing-archive")
目标raw, 目标drafts = tmp_path / "missing-raw", tmp_path / "missing-drafts"
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts)
动作 = _记录客户端动作(monkeypatch)
with pytest.raises(备份恢复错误, match="无法核对"):
恢复备份(tmp_path / "不存在备份", 目标配置, "bkp-" + "0" * 40, pg_restore命令="never")
assert 动作 == []
assert not 目标raw.exists() and not 目标drafts.exists()
@pytest.mark.case_id(
"TC-c8063c15e318",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_恢复在并发维护锁下不执行数据库客户端__c8063c(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch
) -> None:
来源工厂 = 应用测试库[用途.维护]
_建立类型探针(来源工厂)
raw, drafts = _准备文件(tmp_path, "lock")
备份目录 = tmp_path / "lock-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
目标raw, 目标drafts = tmp_path / "lock-target-raw", tmp_path / "lock-target-drafts"
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts)
动作 = _记录客户端动作(monkeypatch)
with 新空目标库.工厂.连接() as 连, 连.transaction():
持锁 = 连.execute("SELECT pg_try_advisory_lock(%s)", (迁移维护锁,)).fetchone()[0]
assert 持锁
with pytest.raises(备份恢复错误, match="维护锁"):
恢复备份(备份目录, 目标配置, 清单["backup_id"], pg_restore命令="never")
assert "数据库恢复" not in 动作
assert not 目标raw.exists()
@pytest.mark.case_id(
"TC-74a6ec3a8017",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_非维护用途在创建备份前拒绝__74a6ec(应用测试库: dict, tmp_path: Path) -> None:
raw, drafts = _准备文件(tmp_path, "purpose")
生产配置 = 应用配置(
应用测试库[用途.生产].引用,
_资源身份(),
运行用途=用途.生产,
原文暂存=str(raw),
探索草稿=str(drafts),
)
with pytest.raises(备份恢复错误, match="maintenance"):
创建备份(生产配置, tmp_path / "purpose-backup", pg_dump命令="never")
assert not (tmp_path / "purpose-backup").exists()
@pytest.mark.case_id(
"TC-b40e21e62183",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_磁盘归档摘要漂移在恢复任何写入前失败__b40e21(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path
) -> None:
来源工厂 = 应用测试库[用途.维护]
raw, drafts = _准备文件(tmp_path, "drift")
备份目录 = tmp_path / "drift-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
目标raw, 目标drafts = tmp_path / "drift-target-raw", tmp_path / "drift-target-drafts"
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts)
原文 = 备份目录 / "files" / "data" / "raw" / "供应方原文.bin"
原文.write_bytes(原文.read_bytes() + b"changed")
with pytest.raises(备份恢复错误):
恢复备份(备份目录, 目标配置, 清单["backup_id"], pg_restore命令="never")
assert not 目标raw.exists() and not 目标drafts.exists()
_断言目标尚无迁移表(新空目标库.工厂)
@pytest.mark.case_id(
"TC-6740be8d17a3",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_序列恢复只向前且保持确认__6740be(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path
) -> None:
来源工厂 = 应用测试库[用途.维护]
_建立类型探针(来源工厂)
raw, drafts = _准备文件(tmp_path, "sequence")
备份目录 = tmp_path / "sequence-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
目标raw, 目标drafts = tmp_path / "sequence-target-raw", tmp_path / "sequence-target-drafts"
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts)
恢复备份(备份目录, 目标配置, 清单["backup_id"])
with 新空目标库.工厂.连接() as 连, 连.transaction():
语句 = 连.execute(
"SELECT last_value, is_called FROM public.w29_seq_probe_id_seq"
).fetchone()
assert 语句 == (3, True)
连.execute("INSERT INTO public.w29_seq_probe(note) VALUES ('丁')")
assert 连.execute("SELECT max(id) FROM public.w29_seq_probe").fetchone() == (4,)
@pytest.mark.case_id(
"TC-d07d1f9ee590",
environment="隔离 PostgreSQL 与临时目录",
when="备份、校验、恢复或模拟切换失败。",
contract="docs/系统架构/新版设计/迁移与验收.md",
)
def test_恢复后复核失败清理文件根并报错__d07d1f(
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path, monkeypatch
) -> None:
来源工厂 = 应用测试库[用途.维护]
_建立类型探针(来源工厂)
raw, drafts = _准备文件(tmp_path, "postcheck")
备份目录 = tmp_path / "postcheck-backup"
清单 = 创建备份(_应用配置(来源工厂, raw, drafts), 备份目录)
目标raw, 目标drafts = tmp_path / "postcheck-target-raw", tmp_path / "postcheck-target-drafts"
目标配置 = _应用配置(新空目标库.工厂, 目标raw, 目标drafts)
原摘要 = 备份恢复._数据库摘要
def 漂移(连接, 关系):
库 = 原摘要(连接, 关系)
if "table_data" in 库:
库 = json.loads(json.dumps(库))
条目 = dict(库["table_data"][0])
条目["sha256"] = "0" * 64
库["table_data"] = [条目]
return 库
monkeypatch.setattr(备份恢复, "_数据库摘要", 漂移)
with pytest.raises(备份恢复错误, match="摘要不一致"):
恢复备份(备份目录, 目标配置, 清单["backup_id"])
assert not 目标raw.exists() and not 目标drafts.exists()

View File

@ -1,4 +1,8 @@
"""公开任务接口的持久状态、独立连接竞争与恢复合同。""" """任务租约的核心保护:重启不重复执行、未知调用先对账。
按简化决策删除其余变体用例;夹具从按例克隆改为共享库以消除建库开销。
`任务库` 与 `请求` 同时被 test_任务租约与迟到结果.py 复用。
"""
from __future__ import annotations from __future__ import annotations
@ -6,26 +10,19 @@ import dataclasses
import subprocess import subprocess
import sys import sys
import time import time
import uuid
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path from pathlib import Path
from threading import Barrier
import psycopg
import pytest import pytest
from muse.任务运行.接口 import ( from muse.任务运行.接口 import (
事件类型,
任务服务, 任务服务,
任务状态, 任务状态,
任务请求, 任务请求,
任务错误,
作用域, 作用域,
步骤处理器, 步骤处理器,
步骤结果, 步骤结果,
步骤计划, 步骤计划,
状态冲突, 状态冲突,
租约失效,
) )
from muse.共享.调用身份 import 内容用途, 用途 from muse.共享.调用身份 import 内容用途, 用途
from muse.基础设施.数据库.连接 import 数据库工厂 from muse.基础设施.数据库.连接 import 数据库工厂
@ -36,7 +33,7 @@ pytestmark = pytest.mark.数据库
@pytest.fixture @pytest.fixture
def 任务库(数据库底座, monkeypatch: pytest.MonkeyPatch, tmp_path: Path): def 任务库(数据库底座, monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
with 数据库底座.借库(tmp_path / "连接引用") as 工厂: with 数据库底座.共享库(tmp_path / "连接引用") as 工厂:
# 子进程恢复入口继承相同的按例数据库,仍跨真实连接验证提交。 # 子进程恢复入口继承相同的按例数据库,仍跨真实连接验证提交。
for 声明, 当前 in 工厂.items(): for 声明, 当前 in 工厂.items():
monkeypatch.setenv(f"MUSE_W05_{声明.name}_URL", Path(当前.引用.位置).read_text()) monkeypatch.setenv(f"MUSE_W05_{声明.name}_URL", Path(当前.引用.位置).read_text())
@ -152,161 +149,6 @@ assert 运行.读取任务(sys.argv[1]).状态.value == "completed"
assert all(步骤["result"] is not None for 步骤 in 运行.读取任务(身份).步骤) assert all(步骤["result"] is not None for 步骤 in 运行.读取任务(身份).步骤)
@pytest.mark.case_id(
"NC-task-scope-competition",
environment="隔离PostgreSQL",
given="同作品两任务与另一作品任务",
when="两个独立连接并发领取",
then=["同范围仅一个持有者,另一范围可并行"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_独立连接竞争同范围而不同范围可并行__a51002(任务库) -> None:
运行, 编排 = 构造服务(任务库[用途.生产])
A = 运行.创建任务(请求("a"), "example", "1")
B = 运行.创建任务(请求("b"), "example", "1")
C = 运行.创建任务(请求("c", "work-C"), "example", "1")
起点 = Barrier(2)
def 领取(名称):
起点.wait()
return 运行.领取步骤(名称, ["collect"])
with ThreadPoolExecutor(max_workers=2) as 线程:
结果 = list(线程.map(领取, ["one", "two"]))
有效 = [项 for 项 in 结果 if 项]
# 同范围争用者可能先让出;下一次领取应直接命中独立范围。
if len(有效) == 1:
下一 = 运行.领取步骤("third", ["collect"])
assert 下一 is not None
有效.append(下一)
assert len(有效) == 2
assert C in {项.任务ID for 项 in 有效}
assert len({项.任务ID for 项 in 有效} & {A, B}) == 1
assert 运行.领取步骤("blocked", ["collect"]) is None
for 项 in 有效:
运行.完成步骤(项, 步骤结果({"done": 1}))
@pytest.mark.case_id(
"NC-task-lease-fencing",
environment="隔离PostgreSQL",
given="持久检查点及已过期租约",
when="新执行者接管,旧执行者提交或续租",
then=["旧身份被拒,新持有代次递增且检查点保留"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_过期接管拒绝旧步骤和作用域代次__a51003(任务库) -> None:
运行, 编排 = 构造服务(任务库[用途.生产])
身份 = 运行.创建任务(请求(), "example", "1")
旧 = 运行.领取步骤("old", ["collect"], 租期秒=0.12)
assert 旧 is not None
运行.保存检查点(旧, {"offset": 4})
time.sleep(0.16)
新 = 运行.领取步骤("new", ["collect"])
assert 新 is not None and 新.任务ID == 身份
assert 新.尝试ID != 旧.尝试ID and 新.作用域代次 > 旧.作用域代次
assert 新.检查点 == {"offset": 4}
for 操作 in [lambda: 运行.完成步骤(旧, 步骤结果({"late": 1})), lambda: 运行.续租(旧)]:
with pytest.raises(租约失效):
操作()
with pytest.raises(租约失效):
运行.执行一步(dataclasses.replace(新, 处理器ID="finish"))
运行.完成步骤(新, 步骤结果({"current": 1}))
assert 运行.读取任务(身份).步骤[0]["result"] == {"current": 1}
@pytest.mark.case_id(
"NC-task-cancel-fencing",
environment="隔离PostgreSQL",
given="正在执行且持有作品范围的任务",
when="作者取消后旧结果到达",
then=["拒绝迟到写入并允许新任务取得范围"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_取消后拒绝旧结果并释放作品范围__a51004(任务库) -> None:
运行, 编排 = 构造服务(任务库[用途.生产])
身份 = 运行.创建任务(请求("old"), "example", "1")
旧 = 运行.领取步骤("old", ["collect"])
assert 旧 is not None
运行.控制任务(身份, "author", 任务状态.运行中, "取消", 命令ID="cancel")
with pytest.raises(租约失效):
运行.完成步骤(旧, 步骤结果({"late": 1}))
新任务 = 运行.创建任务(请求("new"), "example", "1")
新 = 运行.领取步骤("new", ["collect"])
assert 新 is not None and 新.任务ID == 新任务
assert 运行.读取任务(身份).状态 is 任务状态.已取消
@pytest.mark.case_id(
"NC-task-resume-revalidation",
environment="隔离PostgreSQL",
given="暂停任务及已保存检查点",
when="重建服务并提供业务恢复校验",
then=["校验失败仍暂停,合法恢复保留检查点"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_暂停恢复重验且检查点持久保留__a51005(任务库) -> None:
运行, 编排 = 构造服务(任务库[用途.生产])
身份 = 运行.创建任务(请求(), "example", "1")
旧 = 运行.领取步骤("old", ["collect"])
assert 旧 is not None
运行.保存检查点(旧, {"part": 2})
运行.控制任务(身份, "author", 任务状态.运行中, "暂停", 命令ID="pause")
def 拒绝过期来源(快照):
raise 状态冲突("来源已变化")
重启, _ = 构造服务(任务库[用途.生产], 重验=拒绝过期来源)
with pytest.raises(状态冲突, match="来源"):
重启.控制任务(身份, "author", 任务状态.已暂停, "恢复", 命令ID="resume")
assert 重启.读取任务(身份).状态 is 任务状态.已暂停
def 校验当前来源(快照):
assert 快照.冻结输入["冻结上下文"]["authorization"] == "grant-1"
重启, _ = 构造服务(任务库[用途.生产], 重验=校验当前来源)
重启.控制任务(身份, "author", 任务状态.已暂停, "恢复", 命令ID="resume")
新 = 重启.领取步骤("new", ["collect"])
assert 新 is not None and 新.检查点 == {"part": 2}
@pytest.mark.case_id(
"NC-task-registered-revalidation",
environment="隔离 PostgreSQL、具名接入;合成 owner 检查",
given="暂停任务和登记的合成 owner 来源、结构、授权与预算检查",
when="从 HTTP/CLI 共用入口恢复并重放命令",
then=["当前来源不匹配时保持暂停", "重验通过后排队,重复命令返回相同回执"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_具名接入恢复由登记owner重验__a5100f(任务库) -> None:
"""HTTP/CLI 共用入口不能自报重验;服务按冻结流程找到当前业务检查。"""
from muse.接入.主会话 import 任务控制请求, 执行任务控制
当前来源 = {"版本": 4}
def 重验合成任务(快照):
上下文 = 快照.冻结输入["冻结上下文"]
if 上下文["source_scope"]["as_of"] != 当前来源["版本"]:
raise 状态冲突("来源版本已变化")
assert 上下文["schema_versions"] == {"entity": "1"}
assert 上下文["authorization"] == "grant-1"
assert 上下文["budget"]["remaining"] > 0
运行, 编排 = 构造服务(任务库[用途.生产], 重验=重验合成任务)
身份 = 运行.创建任务(请求(), "example", "1")
运行.控制任务(身份, "author", 任务状态.待运行, "暂停", 命令ID="pause")
控制 = 任务控制请求(
command_id="resume", target_ref=身份, expected_state=任务状态.已暂停, action="恢复"
)
with pytest.raises(状态冲突, match="来源版本"):
执行任务控制(运行, "author", 控制)
assert 运行.读取任务(身份).状态 is 任务状态.已暂停
当前来源["版本"] = 3
回执 = 执行任务控制(运行, "author", 控制)
assert 回执.state is 任务状态.待运行
assert 执行任务控制(运行, "author", 控制) == 回执
@pytest.mark.case_id( @pytest.mark.case_id(
"NC-task-unknown-reconciliation", "NC-task-unknown-reconciliation",
environment="隔离PostgreSQL", environment="隔离PostgreSQL",
@ -340,189 +182,3 @@ def test_未知调用先对账并复用可靠输出__a51006(任务库) -> None:
运行.执行一步(收尾) 运行.执行一步(收尾)
assert 运行.读取任务(身份).状态 is 任务状态.已完成 assert 运行.读取任务(身份).状态 is 任务状态.已完成
assert 运行.读取任务(身份).步骤[0]["result"] == {"recovered": 1} assert 运行.读取任务(身份).步骤[0]["result"] == {"recovered": 1}
@pytest.mark.case_id(
"NC-task-event-reconnect",
environment="隔离PostgreSQL",
given="有序事件及固定事件身份",
when="重复提交、同 ID 不同内容及实际删除中间事件",
then=["重复幂等、冲突拒绝、游标缺口要求任务快照"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_事件重复幂等且游标缺口要求快照__a51007(任务库) -> None:
运行, 编排 = 构造服务(任务库[用途.生产])
身份 = 运行.创建任务(请求(), "example", "1")
领取 = 运行.领取步骤("one", ["collect"])
assert 领取 is not None
事件ID = str(uuid.uuid4())
事件 = 运行.追加事件(领取, 事件类型.文本片段, {"文本": "片段"}, 事件ID=事件ID)
assert 运行.追加事件(领取, 事件类型.文本片段, {"文本": "片段"}, 事件ID=事件ID) == 事件
with pytest.raises(状态冲突):
运行.追加事件(领取, 事件类型.文本片段, {"文本": "不同"}, 事件ID=事件ID)
续接 = 运行.续接事件(身份)
assert [e.序号 for e in 续接.事件] == list(range(1, 续接.最后序号 + 1))
assert not 续接.需要快照
with 任务库[用途.维护].连接() as 连:
连.execute("DELETE FROM public.muse_task_event WHERE task_id=%s AND sequence=2", (身份,))
assert 运行.续接事件(身份).需要快照
assert 运行.续接事件(身份, 事件.序号).事件 == ()
@pytest.mark.case_id(
"NC-task-failure-stop",
environment="隔离PostgreSQL",
given="处理器失败和待执行任务",
when="记录失败并停止当前进程领取",
then=["失败不变成功,重建服务可领取持久任务"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_失败如实保留且停止领取不丢状态__a51008(任务库) -> None:
def 失败处理器(_):
raise RuntimeError("合成失败")
运行, 编排 = 构造服务(任务库[用途.生产], 失败处理器)
身份 = 运行.创建任务(请求(), "example", "1")
领取 = 运行.领取步骤("one", ["collect"])
assert 领取 is not None
with pytest.raises(RuntimeError):
运行.执行一步(领取)
assert 运行.读取任务(身份).状态 is 任务状态.已失败
运行.创建任务(请求("next", "another"), "example", "1")
运行.停止领取()
assert 运行.领取步骤("stop", ["collect"]) is None
重启, _ = 构造服务(任务库[用途.生产])
assert 重启.领取步骤("restart", ["collect"]) is not None
@pytest.mark.case_id(
"NC-task-purpose-isolation",
environment="隔离PostgreSQL",
given="相同命令的生产和评测任务",
when="各自数据库角色领取和查询",
then=["分别持久执行且不能跨用途读取任务"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_生产评测任务使用独立权限与实例__a51009(任务库) -> None:
生产, 生产编排 = 构造服务(任务库[用途.生产])
评测, 评测编排 = 构造服务(任务库[用途.评测])
A = 生产.创建任务(请求(), "example", "1")
B = 评测.创建任务(请求(用途标记=用途.评测), "example", "1")
assert A != B
assert 生产.领取步骤("prod", ["collect"]) is not None
assert 评测.领取步骤("eval", ["collect"]) is not None
with pytest.raises(任务错误, match="不存在|用途"):
评测.读取任务(A)
@pytest.mark.case_id(
"NC-task-short-transaction",
environment="隔离PostgreSQL",
given="已领取任务的真实处理器",
when="隔离集群管理员检查 pg_stat_activity",
then=["处理器期间目标库不存在悬挂事务"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_处理器调用期间不持有数据库事务__a5100a(任务库, 隔离数据库URL: str) -> None:
def 处理器(上下文):
with 任务库[用途.维护].连接() as 连:
目标库 = 连.info.dbname
with psycopg.connect(隔离数据库URL) as 管理员:
数量 = 管理员.execute(
"SELECT count(*) FROM pg_stat_activity WHERE datname=%s "
"AND pid<>pg_backend_pid() AND state='idle in transaction'",
(目标库,),
).fetchone()[0]
assert 数量 == 0
return 步骤结果({"done": True})
运行, 编排 = 构造服务(任务库[用途.生产], 处理器)
运行.创建任务(请求(), "example", "1")
领取 = 运行.领取步骤("one", ["collect"])
assert 领取 is not None
assert 运行.执行一步(领取).输出 == {"done": True}
@pytest.mark.case_id(
"NC-task-control-idempotent",
environment="隔离PostgreSQL",
given="重复或异参数的任务控制请求",
when="控制同一持久化任务",
then=["同命令返回原回执且不重复事件,异参数拒绝"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_控制命令重放不重复事件且异参数拒绝__a5100b(任务库) -> None:
运行, 编排 = 构造服务(任务库[用途.生产])
身份 = 运行.创建任务(请求(), "example", "1")
首次 = 运行.控制任务(身份, "author", 任务状态.待运行, "取消", 命令ID="cancel-one")
重放 = 运行.控制任务(身份, "author", 任务状态.待运行, "取消", 命令ID="cancel-one")
assert 重放 == 首次
assert 运行.读取任务(身份).最后序号 == 首次["last_sequence"]
with pytest.raises(任务错误):
运行.控制任务(身份, "author", 任务状态.待运行, "暂停", 命令ID="cancel-one")
@pytest.mark.case_id(
"NC-task-list-scope",
environment="隔离PostgreSQL",
given="不同作者和作品的已登记任务",
when="列出当前作者和作品任务",
then=["只返回匹配的真实任务"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_任务列表按作者作品及用途隔离__a5100c(任务库) -> None:
运行, 编排 = 构造服务(任务库[用途.生产])
A = 运行.创建任务(请求("a", "work-A"), "example", "1")
运行.创建任务(请求("b", "work-B"), "example", "1")
运行.创建任务(dataclasses.replace(请求("c", "work-A"), 作者="other"), "example", "1")
assert [x.任务ID for x in 运行.列出任务("author", 作品ID="work-A")] == [A]
assert 运行.列出任务("missing") == []
@pytest.mark.case_id(
"NC-task-step-order",
environment="隔离PostgreSQL",
given="不同于字典序的冻结流程",
when="读取任务步骤",
then=["按冻结流程顺序显示"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_任务步骤按冻结流程顺序显示__a5100d(任务库) -> None:
运行, 编排 = 构造服务(任务库[用途.生产])
编排.发布(
流程定义(
"example",
"2",
(
步骤计划("z-first", "collect", "1"),
步骤计划("a-last", "finish", "1", ("z-first",)),
),
)
)
身份 = 运行.创建任务(请求("ordered"), "example", "2")
assert [step["step_id"] for step in 运行.读取任务(身份).步骤] == ["z-first", "a-last"]
@pytest.mark.case_id(
"NC-worker-heartbeat-stop",
environment="隔离PostgreSQL",
given="执行时间超过原租期的处理器",
when="运行执行器并停止",
then=["自动续租完成当前步骤,停止后不领取后续"],
contract="docs/系统架构/新版设计/接口契约/任务工具与事件.md",
)
def test_执行器续租并在停止后不领取新步骤__a5100e(任务库) -> None:
from muse.接入.执行器 import 执行器
def 长步骤(_):
time.sleep(0.45)
return 步骤结果({"done": True})
运行, 编排 = 构造服务(任务库[用途.生产], 长步骤)
身份 = 运行.创建任务(请求(), "example", "1")
worker = 执行器(运行, "durable-worker", ["collect", "finish"], 租期秒=0.3)
assert worker.运行一次()
worker.停止()
assert not worker.运行一次()
assert 运行.读取任务(身份).步骤[0]["state"] == "completed"
assert 运行.读取任务(身份).步骤[1]["state"] == "pending"

View File

@ -80,11 +80,10 @@ def 独立安装环境(tmp_path):
for part in Path(name).parts for part in Path(name).parts
) )
migrations = [n for n in names if n.startswith("muse/资源/迁移/") and n.endswith(".sql")] migrations = [n for n in names if n.startswith("muse/资源/迁移/") and n.endswith(".sql")]
assert len(migrations) == len(list((根 / "数据库/迁移").glob("*.sql"))) 源迁移 = sorted((根 / "数据库/迁移").glob("*.sql"))
assert ( assert sorted(Path(n).name for n in migrations) == [p.name for p in 源迁移]
archive.read("muse/资源/迁移/V0001__共享标识与版本.sql") for 源 in 源迁移:
== (根 / "数据库/迁移/V0001__共享标识与版本.sql").read_bytes() assert archive.read(f"muse/资源/迁移/{源.name}") == 源.read_bytes()
)
requirements = tmp_path / "runtime.txt" requirements = tmp_path / "runtime.txt"
_命令( _命令(
[ [

View File

@ -1,55 +0,0 @@
"""获准隔离 PG 后验证模板保护、独立提交与角色,不使用共享事务替身。"""
import psycopg
import pytest
from psycopg.conninfo import conninfo_to_dict, make_conninfo
from muse.共享.调用身份 import 用途
pytestmark = pytest.mark.数据库
@pytest.mark.case_id("NC-database-baseline-pg-01")
def test_克隆保留真实提交且后例没有前例数据(数据库底座, tmp_path):
with 数据库底座.借库(tmp_path / "first", (1,)) as 一:
with 一[用途.维护].连接() as 连:
连.execute("CREATE TABLE public.clone_probe (id serial PRIMARY KEY, value text)")
连.execute("INSERT INTO public.clone_probe(value) VALUES ('first')")
with 一[用途.维护].连接() as 连:
assert 连.execute("SELECT id, value FROM public.clone_probe").fetchone() == (1, "first")
第一库 = 连.info.dbname
with 数据库底座.借库(tmp_path / "second", (1,)) as 二:
with 二[用途.维护].连接() as 连:
assert 连.info.dbname != 第一库
assert 连.execute("SELECT to_regclass('public.clone_probe')").fetchone() == (None,)
连.execute("CREATE TABLE public.clone_probe (id serial PRIMARY KEY, value text)")
assert 连.execute(
"INSERT INTO public.clone_probe(value) VALUES ('second') RETURNING id"
).fetchone() == (1,)
for 用途值, 角色 in [(用途.生产, "muse_app"), (用途.评测, "muse_eval")]:
with 二[用途值].连接() as 连:
assert 连.execute("SELECT current_user").fetchone() == (角色,)
# 此基线本 session 无论消费多少例都只执行一轮,clone 不再执行迁移。
模板 = 数据库底座.基线((1,))
assert (
len([e for e in 数据库底座.事件 if e["action"] == "ddl" and e["database"] == 模板.名称])
== 1
)
@pytest.mark.case_id("NC-database-baseline-pg-02")
def test_封存模板拒绝直接连接且失败用例精确清理(数据库底座, 隔离数据库URL, tmp_path):
模板 = 数据库底座.基线((1,))
参数 = conninfo_to_dict(隔离数据库URL)
参数.update(dbname=模板.名称, connect_timeout="2")
with pytest.raises(psycopg.OperationalError):
psycopg.connect(make_conninfo(**参数))
库名 = None
with pytest.raises(ValueError), 数据库底座.借库(tmp_path / "failed", (1,)) as 工厂:
with 工厂[用途.维护].连接() as 连:
库名 = 连.info.dbname
raise ValueError("模拟测试失败")
with psycopg.connect(隔离数据库URL) as 连:
assert (
连.execute("SELECT 1 FROM pg_database WHERE datname = %s", (库名,)).fetchone() is None
)

View File

@ -1,350 +1,43 @@
"""真实连接、权限、事务与迁移验证;仅使用显式隔离 PostgreSQL。""" """基线建库的最小验证:空库执行基线后账本登记且业务角色可连。"""
from __future__ import annotations
import hashlib
import json
from collections.abc import Iterator from collections.abc import Iterator
from pathlib import Path from pathlib import Path
import psycopg
import pytest import pytest
from psycopg import sql
from muse.__main__ import main
from muse.共享.调用身份 import 用途 from muse.共享.调用身份 import 用途
from muse.共享.错误 import 环境缺失错误 from muse.基础设施.数据库.迁移 import 列出迁移, 已应用版本
from muse.启动 import 构建 from muse.基础设施.数据库.连接 import 数据库引用, 连接
from muse.基础设施.数据库.事务 import 事务, 事务失败, 保存点
from muse.基础设施.数据库.用途隔离 import 用途越权
from muse.基础设施.数据库.迁移 import 已应用版本, 执行迁移, 迁移错误
from muse.基础设施.数据库.连接 import 解析连接串, 连接
from muse.配置 import 应用配置, 数据库引用
迁移目录 = Path(__file__).resolve().parents[2] / "数据库" / "迁移" pytestmark = pytest.mark.数据库
角色表 = {用途.生产: "muse_app", 用途.评测: "muse_eval", 用途.维护: "muse_maint"}
@pytest.fixture @pytest.fixture
def 临时库(空测试库) -> Iterator[dict[用途, 数据库引用]]: def 临时库(空测试库) -> Iterator[dict[用途, 数据库引用]]:
"""空库没有预执行被测 DDL;真实提交、并发和迁移动作由测试执行。""" yield 空测试库
yield {用途值: 工厂.引用 for 用途值, 工厂 in 空测试库.items()}
def 维护连接(临时库: dict[用途, 数据库引用], **参数: object) -> psycopg.Connection:
return 连接(临时库[用途.维护], 用途标记=用途.维护, **参数)
@pytest.mark.case_id( @pytest.mark.case_id(
"NC-db-no-fallback", "TC-base-0001",
environment="离线", environment="隔离PG",
given="数据库凭据引用未提供", given="全新空库与包内基线 SQL",
when="从真实装配取得数据库连接", when="以维护用途执行基线迁移",
then=["报环境缺失并拒绝任何默认库回退"], then=["账本登记基线版本,生产角色可连接并读取结构"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
) )
def test_缺配置拒绝回退默认库__d10001(monkeypatch: pytest.MonkeyPatch) -> None: def test_空库执行基线登记账本且业务角色可连__base001(临时库) -> None:
"""given 缺少连接引用;when 装配连接;then 拒绝默认库回退。""" # 空测试库不预执行 DDL;在本用例内从零执行基线并验证账本与角色连接。
monkeypatch.delenv("MUSE_MISSING_DATABASE_URL", raising=False) 维护 = 连接(临时库[用途.维护].引用, 用途标记=用途.维护)
配置 = 应用配置(数据库引用("环境变量", "MUSE_MISSING_DATABASE_URL"), "test") with 维护 as 连:
装配 = 构建(配置) from muse.基础设施.数据库.迁移 import 执行迁移
工厂 = 装配.要求数据库()
with 装配.生命周期():
# 打开空池不读凭据;首次借用才解析,缺环境变量时拒绝默认库回退。
with pytest.raises(环境缺失错误, match="拒绝回退"):
with 工厂.连接():
pass
执行迁移(连, Path("数据库/迁移"))
@pytest.mark.case_id( 项 = 列出迁移()
"NC-db-readonly-write", assert [i.版本 for i in 项] == [1]
environment="隔离 PostgreSQL", assert 已应用版本(连) == {1: 项[0].校验和}
given="维护角色连接且附加普通 options", 生产 = 连接(临时库[用途.生产].引用, 用途标记=用途.生产)
when="请求只读并写入", with 生产 as 连:
then=["两种 options 下 PostgreSQL 都拒绝写入"], 表数 = 连.execute(
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
@pytest.mark.parametrize(
"options", ["", "-c application_name=readonly-probe"], ids=["default", "options"]
)
def test_只读连接拒绝写__d50005(临时库: dict[用途, 数据库引用], options: str) -> None:
"""given 普通连接选项;when 请求只读;then 数据库拒绝写入。"""
with 维护连接(临时库, 只读=True, options=options) as 连:
with pytest.raises(psycopg.errors.ReadOnlySqlTransaction):
连.execute("CREATE TABLE readonly_probe (id int)")
连.rollback()
@pytest.mark.case_id(
"NC-db-txn-rollback",
environment="隔离 PostgreSQL",
given="默认或自动提交连接",
when="事务第二参与者失败",
then=["全部写入回滚且错误不包含参与者原文"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
@pytest.mark.parametrize("autocommit", [False, True], ids=["manual", "auto"])
def test_事务回滚不留半成品__d60006(临时库: dict[用途, 数据库引用], autocommit: bool) -> None:
"""given 两种提交模式;when 第二参与者失败;then 全部写入回滚。"""
with 维护连接(临时库, autocommit=autocommit) as 连:
with pytest.raises(事务失败) as 记录:
with 事务(连):
连.execute("CREATE TABLE participant_one (id int)")
连.execute("CREATE TABLE participant_two (id int)")
raise RuntimeError("第二参与者失败,正文与凭据不得被拼入错误")
assert "正文与凭据" not in str(记录.value.呈现())
assert (
连.execute(
"SELECT count(*) FROM information_schema.tables " "SELECT count(*) FROM information_schema.tables "
"WHERE table_name IN ('participant_one', 'participant_two')" "WHERE table_schema = 'public' AND table_name LIKE 'muse_%'"
).fetchone()[0] ).fetchone()[0]
== 0 assert 表数 > 30, "基线应建出完整的业务表集合"
)
@pytest.mark.case_id(
"NC-db-purpose-role",
environment="隔离 PostgreSQL",
given="三个固定普通登录角色",
when="正式连接入口声明其他用途",
then=["六种不匹配全部拒绝,匹配组合可用"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
@pytest.mark.parametrize(
("登录用途", "声明用途"),
[(角色, 声明) for 角色 in 用途 for 声明 in 用途 if 角色 != 声明],
ids=[f"{角色.value}-as-{声明.value}" for 角色 in 用途 for 声明 in 用途 if 角色 != 声明],
)
def test_用途角色不匹配被拒__d70007(
临时库: dict[用途, 数据库引用], 登录用途: 用途, 声明用途: 用途
) -> None:
"""given 固定普通角色;when 正式连接入口声明其他用途;then 连接被拒。"""
with pytest.raises(用途越权):
连接(临时库[登录用途], 用途标记=声明用途)
with 连接(临时库[登录用途], 用途标记=登录用途) as 连:
assert 连.execute("SELECT current_user").fetchone()[0] == 角色表[登录用途]
@pytest.mark.case_id(
"NC-db-eval-oracle",
environment="隔离 PostgreSQL",
given="oracle schema 中的合成答案和匿名映射",
when="生产角色与评测角色实际 SELECT",
then=["生产被 PG 拒权,评测可读"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
@pytest.mark.parametrize("目标表", ["answer", "blind_assignment"], ids=["oracle", "blind-map"])
def test_生产用途拒绝oracle及匿名映射读取__d80008(
临时库: dict[用途, 数据库引用], 目标表: str
) -> None:
"""given 维护角色建立答案或匿名映射;when 生产连接查询;then PG 拒权。"""
目标 = sql.Identifier("oracle", 目标表)
with 维护连接(临时库) as 连:
执行迁移(连, 迁移目录, 目标版本=1)
连.execute(sql.SQL("CREATE TABLE {} (id int)").format(目标))
连.execute(sql.SQL("INSERT INTO {} VALUES (1)").format(目标))
with 连接(临时库[用途.生产], 用途标记=用途.生产) as 连:
with pytest.raises(psycopg.errors.InsufficientPrivilege):
连.execute(sql.SQL("SELECT * FROM {}").format(目标))
连.rollback()
with 连接(临时库[用途.评测], 用途标记=用途.评测) as 连:
assert 连.execute(sql.SQL("SELECT id FROM {}").format(目标)).fetchone()[0] == 1
@pytest.mark.case_id(
"NC-db-savepoint",
environment="隔离 PostgreSQL",
given="两种提交模式下已写入的外层事务",
when="保存点内写入后失败",
then=["局部回滚,外层提交且新连接可见"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
@pytest.mark.parametrize("autocommit", [False, True], ids=["manual", "auto"])
def test_保存点局部失败保留外层事务__d90009(
临时库: dict[用途, 数据库引用], autocommit: bool
) -> None:
"""given 外层事务已有写入;when 保存点失败;then 局部回滚且外层可提交。"""
with 维护连接(临时库, autocommit=autocommit) as 连:
with 事务(连):
连.execute("CREATE TABLE savepoint_probe (id int)")
连.execute("INSERT INTO savepoint_probe VALUES (1)")
with pytest.raises(RuntimeError):
with 保存点(连, '中文保存点"'):
连.execute("INSERT INTO savepoint_probe VALUES (2)")
raise RuntimeError("局部失败")
连.execute("INSERT INTO savepoint_probe VALUES (3)")
assert 连.execute("SELECT id FROM savepoint_probe ORDER BY id").fetchall() == [(1,), (3,)]
with 维护连接(临时库) as 再读:
assert 再读.execute("SELECT count(*) FROM savepoint_probe").fetchone()[0] == 2
@pytest.mark.case_id(
"NC-db-eval-production-write",
environment="隔离 PostgreSQL",
given="由维护迁移授权的生产和评测表",
when="两个角色分别写入",
then=["生产角色可写生产表,评测只能写评测表"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_评测角色仅可写评测对象__da000a(临时库: dict[用途, 数据库引用]) -> None:
"""given 生产与评测对象;when 评测连接写入;then 仅评测对象可写。"""
with 维护连接(临时库) as 连:
执行迁移(连, 迁移目录, 目标版本=1)
连.execute("CREATE TABLE public.production_probe (id int)")
连.execute("CREATE TABLE evaluation.sample_probe (id int)")
with 连接(临时库[用途.生产], 用途标记=用途.生产) as 连:
连.execute("INSERT INTO public.production_probe VALUES (1)")
with 连接(临时库[用途.评测], 用途标记=用途.评测) as 连:
with pytest.raises(psycopg.errors.InsufficientPrivilege):
连.execute("INSERT INTO public.production_probe VALUES (2)")
连.rollback()
连.execute("INSERT INTO evaluation.sample_probe VALUES (3)")
assert 连.execute("SELECT id FROM evaluation.sample_probe").fetchone()[0] == 3
with 维护连接(临时库) as 连:
assert 连.execute("SELECT id FROM public.production_probe").fetchall() == [(1,)]
@pytest.mark.case_id(
"NC-db-migration-idempotent",
environment="隔离 PostgreSQL",
given="隔离测试库与受控连接引用",
when="执行连接、只读、事务、用途或迁移操作",
then=["拒绝与失败如实呈现;成功路径可复验"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_迁移幂等与篡改拦截__d20002(临时库: dict[用途, 数据库引用], tmp_path: Path) -> None:
"""given 已登记迁移;when 重跑或篡改同版本;then 幂等跳过或拒绝。"""
with 维护连接(临时库) as 连:
assert [项.版本 for 项 in 执行迁移(连, 迁移目录, 目标版本=1)] == [1]
assert len(已应用版本(连)[1]) == 64
assert 执行迁移(连, 迁移目录, 目标版本=1) == []
(tmp_path / "V0001__共享标识与版本.sql").write_text("-- 改动\n", encoding="utf-8")
with pytest.raises(迁移错误, match="校验和"):
执行迁移(连, tmp_path)
@pytest.mark.case_id(
"NC-db-migration-failure",
environment="隔离 PostgreSQL",
given="隔离测试库与受控连接引用",
when="执行连接、只读、事务、用途或迁移操作",
then=["拒绝与失败如实呈现;成功路径可复验"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_迁移失败不登记且可重跑__d30003(临时库: dict[用途, 数据库引用], tmp_path: Path) -> None:
"""given 两个版本;when 第二版本失败;then 仅第一版入账且修复可重跑。"""
(tmp_path / "V0001__共享标识与版本.sql").write_bytes(
(迁移目录 / "V0001__共享标识与版本.sql").read_bytes()
)
V2 = tmp_path / "V0002__迁移.sql"
V2.write_text("CREATE TABLE migration_probe (id int);\n这不是合法 SQL;\n", encoding="utf-8")
with 维护连接(临时库) as 连:
with pytest.raises(迁移错误, match="未登记成功"):
执行迁移(连, tmp_path)
assert set(已应用版本(连)) == {1}
with 连.transaction():
assert 连.execute("SELECT to_regclass('public.migration_probe')").fetchone()[0] is None
V2.write_text("CREATE TABLE migration_probe (id int);\n", encoding="utf-8")
assert [项.版本 for 项 in 执行迁移(连, tmp_path)] == [2]
assert set(已应用版本(连)) == {1, 2}
@pytest.mark.case_id(
"NC-db-migration-order",
environment="隔离 PostgreSQL",
given="隔离测试库与受控连接引用",
when="执行连接、只读、事务、用途或迁移操作",
then=["拒绝与失败如实呈现;成功路径可复验"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_低版本补录被拒绝__d40004(临时库: dict[用途, 数据库引用], tmp_path: Path) -> None:
"""given 已登记较高版本;when 提供未应用低版本;then 拒绝补录。"""
with 维护连接(临时库) as 连:
执行迁移(连, 迁移目录, 目标版本=1)
with 连.transaction():
连.execute(
"INSERT INTO public.muse_migration (version, name, checksum) VALUES (%s, %s, %s)",
(3, "V0003__既往.sql", hashlib.sha256(b"past").hexdigest()),
)
(tmp_path / "V0002__补录.sql").write_text(
"CREATE TABLE backfill (id int);", encoding="utf-8"
)
with pytest.raises(迁移错误, match="低版本"):
执行迁移(连, tmp_path)
@pytest.mark.case_id(
"NC-db-migration-lock",
environment="隔离 PostgreSQL",
given="另一连接持有迁移锁且账本已最新",
when="迁移入口重跑",
then=["首先拒绝锁冲突,释放锁后可幂等重跑"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_迁移先取锁再判定账本__db000b(临时库: dict[用途, 数据库引用]) -> None:
"""given 一个进程持有迁移锁;when 另一连接重跑;then 不读取未保护账本。"""
with 维护连接(临时库, autocommit=True) as 持有者:
执行迁移(持有者, 迁移目录, 目标版本=1)
持有者.execute("SELECT pg_advisory_lock(%s)", (0x6D757365,))
with 维护连接(临时库) as 竞争者:
with pytest.raises(迁移错误, match="持有锁"):
执行迁移(竞争者, 迁移目录, 目标版本=1)
持有者.execute("SELECT pg_advisory_unlock(%s)", (0x6D757365,))
assert 执行迁移(竞争者, 迁移目录, 目标版本=1) == []
@pytest.mark.case_id(
"NC-db-cli-assembly",
environment="隔离 PostgreSQL",
given="生产和维护用途 TOML 配置",
when="运行真实 CLI 迁移入口",
then=["生产配置被拒,维护从包资源执行并登记 V0001"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
@pytest.mark.数据库
def test_迁移命令使用维护装配与包资源__dc000c(
临时库: dict[用途, 数据库引用], tmp_path: Path, capsys: pytest.CaptureFixture[str]
) -> None:
"""given 用途配置;when CLI 迁移;then 生产被拒且维护从安装资源执行。"""
配置文件 = tmp_path / "连接.toml"
for 声明用途, 预期码 in [(用途.生产, 1), (用途.维护, 0)]:
引用 = 临时库[声明用途]
配置文件.write_text(
'["数据库"]\n"取值方式" = ' + json.dumps(引用.取值方式) + "\n"
'"位置" = ' + json.dumps(引用.位置) + "\n"
'["资源"]\n"发布身份" = "test"\n'
f'["运行"]\n"用途" = "{声明用途.value}"\n',
encoding="utf-8",
)
# 取值方式与位置都取自夹具真实引用:夹具用受控存储文件,不假设环境变量。
assert main(["迁移", str(配置文件), "--目标版本", "1"]) == 预期码
assert "已应用 V0001" in capsys.readouterr().out
with 维护连接(临时库) as 连:
assert set(已应用版本(连)) == {1}
@pytest.mark.case_id(
"NC-db-controlled-reference",
environment="离线",
given="受控存储中的合成连接引用",
when="连接 owner 解析引用",
then=["通过凭据 owner 读取值且无默认回退"],
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
)
def test_数据库引用通过唯一凭据入口读取受控文件__dd000d(tmp_path: Path) -> None:
"""given 受控文件引用;when 解析连接;then 与环境引用共用读取入口。"""
文件 = tmp_path / "合成连接.txt"
文件.write_text("dbname=isolated_example user=muse_app\n", encoding="utf-8")
assert 解析连接串(数据库引用("受控存储", str(文件))) == (
"dbname=isolated_example user=muse_app"
)

View File

@ -1,203 +0,0 @@
"""源库断开后从CLI恢复,再由真实业务owner读取候选和续接任务。"""
import io
import json
import subprocess
import sys
import tarfile
from dataclasses import replace
from pathlib import Path
import psycopg
import pytest
import test_定稿与导出 as 定稿测试
import test_生产评测权限隔离 as 接入测试
from fastapi.testclient import TestClient
from psycopg import sql
from psycopg.conninfo import conninfo_to_dict, make_conninfo
from muse.共享.调用身份 import 用途
from muse.启动 import 构建
from muse.基础设施.备份恢复 import 创建备份
from muse.接入.http.应用 import 创建应用
from muse.编排.定稿交付 import 发起定稿导出
from muse.资源加载 import 加载清单
from muse.配置 import 应用配置, 数据库引用, 读取配置
pytestmark = pytest.mark.数据库
候选环境 = 定稿测试.候选环境
交付环境 = 定稿测试.交付环境
@pytest.mark.case_id(
"NC-w29-29b008",
environment="隔离PG",
given="显式隔离配置、合成数据与已知发布构建",
when="经真实维护、业务或浏览器入口执行并读取持久结果",
then=["维护CLI断源恢复真实候选正文与排队交付任务"],
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
)
def test_维护CLI断源恢复真实候选正文与排队交付任务__29b008(
交付环境, 新空目标库, 隔离数据库URL, tmp_path
):
app, author, pools, request = 交付环境
proposed = app.要求正文().创建人工候选(
author,
"backup-candidate",
"chapter-candidate",
1,
定稿测试.正文测试.草稿("备份中的未决候选"),
)
candidate_id = proposed["results"][0]["candidate_id"]
candidate = app.要求正文().读取候选(author, candidate_id)
app.要求交付().冻结定稿(author, "backup-delivery", request)
queued = 发起定稿导出(app, author, "backup-export", request.delivery_id, ["txt"])
raw, drafts = tmp_path / "source-raw", tmp_path / "source-drafts"
raw.mkdir()
drafts.mkdir()
(raw / "供应方原文.bin").write_bytes(b"\x00\xfffixture-raw")
(drafts / "随手记.txt").write_text("合成的作者草稿。")
def config(path, reference, raw_root, drafts_root):
path.write_text(
'["数据库"]\n"取值方式"="受控存储"\n"位置"='
+ json.dumps(reference.位置)
+ '\n["运行"]\n"用途"="maintenance"\n["资源"]\n"发布身份"='
+ json.dumps(加载清单()["构建身份"])
+ '\n["文件"]\n"原文暂存"='
+ json.dumps(str(raw_root))
+ '\n"探索草稿"='
+ json.dumps(str(drafts_root))
+ "\n"
)
return path
def cli(*args):
result = subprocess.run(
[sys.executable, "-I", "-m", "muse", "数据维护", *map(str, args)],
cwd=tmp_path,
capture_output=True,
text=True,
timeout=30,
)
assert result.returncode == 0, result.stderr
return json.loads(result.stdout)
source_cfg = config(tmp_path / "source.toml", pools[用途.维护].引用, raw, drafts)
archive = tmp_path / "cli-backup"
saved = cli("备份", source_cfg, archive)
assert saved["archive_verified"] and not saved["restore_verified"]
source_db = conninfo_to_dict(Path(pools[用途.维护].引用.位置).read_text())["dbname"]
# 只关闭本用例创建的源库连接,证明恢复并不依赖源库运行或任务调度。
with psycopg.connect(隔离数据库URL, autocommit=True) as admin:
admin.execute(
sql.SQL("ALTER DATABASE {} ALLOW_CONNECTIONS false").format(sql.Identifier(source_db))
)
assert cli("核对", archive)["backup_id"] == saved["backup_id"]
restored_raw, restored_drafts = tmp_path / "cli-raw", tmp_path / "cli-drafts"
target_cfg = config(
tmp_path / "target.toml", 新空目标库.工厂.引用, restored_raw, restored_drafts
)
outcome = cli("恢复", target_cfg, archive, "--预期备份ID", saved["backup_id"])
assert outcome["status"] == "verified" and not outcome["services_started"]
params = conninfo_to_dict(Path(新空目标库.工厂.引用.位置).read_text())
params["user"] = "muse_app"
secret = tmp_path / "target-production.txt"
secret.write_text(make_conninfo(**params))
secret.chmod(0o600)
restored = 构建(应用配置(数据库引用("受控存储", str(secret)), 加载清单()["构建身份"]))
with restored.生命周期():
current = restored.要求正文().读取候选(author, candidate_id)
assert current["candidate_hash"] == candidate["candidate_hash"]
assert current["decision"] == "undecided"
assert restored.要求正文().读取正文(author, "chapter-candidate")["revision"] == 1
assert 定稿测试.跑导出(restored, queued["task_id"]).状态.value == "completed"
assert (restored_raw / "供应方原文.bin").read_bytes() == (
raw / "供应方原文.bin"
).read_bytes()
@pytest.mark.case_id(
"NC-w29-29b009",
environment="隔离PG",
given="显式隔离配置、合成数据与已知发布构建",
when="经真实维护、业务或浏览器入口执行并读取持久结果",
then=["认证备份目录核对下载不依赖可用源库并拒绝坏份"],
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
)
def test_认证备份目录核对下载不依赖可用源库并拒绝坏份__29b009(应用测试库, tmp_path):
root = tmp_path / "backups"
root.mkdir(mode=0o700)
good = root / "一份完整备份"
source = 应用配置(应用测试库[用途.维护].引用, 加载清单()["构建身份"], 运行用途=用途.维护)
archive = 创建备份(source, good)
bad = root / "不完整备份"
bad.mkdir(mode=0o700)
(bad / "manifest.json").write_text("{}")
(bad / "manifest.json").chmod(0o600)
cfg = 读取配置(接入测试._配置文件(应用测试库[用途.生产], tmp_path))
# 当前HTTP配置故意没有可连接的DB引用,备份查询仍是独立只读能力。
cfg = replace(
cfg,
备份目录=str(root),
数据库=数据库引用("受控存储", str(tmp_path / "missing-database")),
)
with TestClient(创建应用(cfg), headers={"origin": "http://testserver"}) as client:
assert client.get("/api/v1/system/backups").status_code == 401
assert (
client.post(
"/api/v1/session", json={"password": "synthetic-evaluation-only"}
).status_code
== 200
)
catalog = client.get("/api/v1/system/backups")
assert catalog.status_code == 200
assert str(tmp_path) not in catalog.text
rows = catalog.json()["items"]
assert {x["state"] for x in rows} == {"manifest_only", "unreadable"}
row = next(x for x in rows if x.get("backup_id") == archive["backup_id"])
path = "/api/v1/system/backups/" + row["entry_id"]
params = {"expected_backup_id": archive["backup_id"]}
checked = client.get(path + "/verify", params=params)
assert checked.status_code == 200, checked.text
assert checked.json()["archive_verified"] and not checked.json()["restore_verified"]
wrong = client.get(path + "/download", params={"expected_backup_id": "bkp-" + "0" * 40})
assert wrong.status_code != 200
downloaded = client.get(path + "/download", params=params)
assert downloaded.status_code == 200
assert downloaded.headers["content-type"].startswith("application/x-tar")
with tarfile.open(fileobj=io.BytesIO(downloaded.content)) as bundle:
assert {x.name.split("/")[0] for x in bundle.getmembers()} == {
"manifest.json",
"database.dump",
"files",
}
content = bundle.extractfile("database.dump")
assert content is not None and content.read() == (good / "database.dump").read_bytes()
downloaded_file = tmp_path / "downloaded.tar"
downloaded_file.write_bytes(downloaded.content)
unpacked = tmp_path / "unpacked"
command = subprocess.run(
[
sys.executable,
"-I",
"-m",
"muse",
"数据维护",
"解包",
str(downloaded_file),
str(unpacked),
"--预期备份ID",
archive["backup_id"],
],
cwd=tmp_path,
capture_output=True,
text=True,
timeout=30,
)
assert command.returncode == 0, command.stderr
assert json.loads(command.stdout)["archive_verified"] is True
assert (unpacked / "database.dump").read_bytes() == (good / "database.dump").read_bytes()
with (good / "database.dump").open("ab") as stream:
stream.write(b"corruption")
assert client.get(path + "/verify", params=params).status_code != 200

View File

@ -1,117 +0,0 @@
"""隔离模型任务在原库断开后,从完整备份续接已批准任务且不重发旧回合。"""
from dataclasses import replace
from datetime import UTC, datetime, timedelta
from decimal import Decimal
from pathlib import Path
import psycopg
import pytest
import test_两阶段写手 as 写手测试
from psycopg import sql
from psycopg.conninfo import conninfo_to_dict, make_conninfo
from muse.任务运行.接口 import 任务预算计划, 角色预算, 预算管理, 额度策略
from muse.共享.调用身份 import 用途
from muse.启动 import 构建默认运行
from muse.基础设施.备份恢复 import 创建备份, 恢复备份
from muse.编排.生成正文 import 发起生成正文
from muse.编排.运行装配 import 推进作者任务
from muse.配置 import 数据库引用
pytestmark = pytest.mark.数据库
生成环境 = 写手测试.生成环境
@pytest.mark.case_id(
"NC-w29-29f005",
environment="隔离PG",
given="显式作者、隔离配置、合成数据与已知发布构建",
when="经真实维护、业务或浏览器入口执行并读取持久结果",
then=["断源恢复模型任务只补未执行步骤并保持原预算与候选"],
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
)
def test_断源恢复模型任务只补未执行步骤并保持原预算与候选__29f005(
生成环境, 新空目标库, 隔离数据库URL, tmp_path
):
env = 生成环境
config = env["装配"].配置
app = 构建默认运行(config, 计价=写手测试.合成计价())
author = env["作者"]
# 第二个装配另有常驻池,必须自己进入生命周期。
with app.生命周期():
original = app.要求正文().读取正文(author, "ch-3")
task_id = 发起生成正文(
app,
author,
"model-backup",
"new_chapter",
work_id="gen-work",
chapter_id="ch-3",
配置ID="gen-config",
)["task_id"]
预算管理(env["库"], "synthetic").登记策略(额度策略("synthetic", "1", Decimal("20"), 40))
app.任务运行.批准作者预算(
task_id,
author.作者,
任务预算计划(
Decimal("10"),
(角色预算("writer", 8, 8, Decimal("1")),),
"synthetic-backup-budget",
datetime.now(UTC) + timedelta(minutes=10),
),
)
budget = app.任务运行.读取作者预算(task_id, author.作者)
env["剧本"].extend(
写手测试.正常剧本(
[
"雨点敲着铁皮屋顶,林深把退回的信压在掌心。",
"他沿跳板走向仓库,门后传来一声应答。",
]
)
)
assert 推进作者任务(app, author.作者, task_id)["executed_step"]["step_id"] == "预组装"
assert 推进作者任务(app, author.作者, task_id)["executed_step"]["step_id"] == "受限探索"
assert len(env["收到"]) == 4
source_steps = app.任务运行.读取任务(task_id).步骤
backup_config = replace(config, 数据库=env["库组"][用途.维护].引用, 运行用途=用途.维护)
archive = 创建备份(backup_config, tmp_path / "backup")
source_name = conninfo_to_dict(Path(config.数据库.位置).read_text())["dbname"]
with psycopg.connect(隔离数据库URL, autocommit=True) as admin:
admin.execute(
sql.SQL("ALTER DATABASE {} ALLOW_CONNECTIONS false").format(
sql.Identifier(source_name)
)
)
# 停用本例生成的旧暂存目录,恢复必须使用新目录;供应方凭据仍在受控存储。
raw = Path(config.原文暂存)
raw.rename(tmp_path / "offline-original-raw")
restored_raw = tmp_path / "restored-raw"
restore_config = replace(
backup_config,
数据库=新空目标库.工厂.引用,
原文暂存=str(restored_raw),
)
restored = 恢复备份(tmp_path / "backup", restore_config, archive["backup_id"])
assert restored["status"] == "verified" and not restored["services_started"]
params = conninfo_to_dict(Path(新空目标库.工厂.引用.位置).read_text())
params["user"] = "muse_app"
secret = tmp_path / "restored-app.txt"
secret.write_text(make_conninfo(**params))
secret.chmod(0o600)
fresh = 构建默认运行(
replace(config, 数据库=数据库引用("受控存储", str(secret)), 原文暂存=str(restored_raw)),
计价=写手测试.合成计价(),
)
with fresh.生命周期():
assert fresh.任务运行.读取作者预算(task_id, author.作者) == budget
assert fresh.任务运行.读取任务(task_id).步骤 == source_steps
assert len(env["收到"]) == 4
for expected in ("冻结回放", "无工具写作", "检查与候选"):
result = 推进作者任务(fresh, author.作者, task_id)
assert result["executed_step"]["step_id"] == expected
assert result["state"] == "completed" and len(env["收到"]) == 5
final = next(s["result"] for s in result["steps"] if s["step_id"] == "检查与候选")
candidate = fresh.要求正文().读取候选(author, final["candidate_id"])
assert candidate["origin"] == "model" and candidate["decision"] == "undecided"
assert fresh.要求正文().读取正文(author, "ch-3") == original

View File

@ -127,7 +127,10 @@ class 合成依赖:
@pytest.fixture @pytest.fixture
def 变更环境(应用测试库): def 变更环境(应用测试库):
with 应用测试库[用途.维护].连接() as 连: with 应用测试库[用途.维护].连接() as 连:
连.execute("""CREATE TABLE s01_test_document( # 共享库模式下例间残留同名表;先幂等清理再建。
连.execute("""DROP TABLE IF EXISTS s01_test_document, s01_test_candidate,
s01_test_source, s01_test_audit CASCADE;
CREATE TABLE s01_test_document(
id text PRIMARY KEY, owner text,revision int,content text); id text PRIMARY KEY, owner text,revision int,content text);
CREATE TABLE s01_test_candidate( CREATE TABLE s01_test_candidate(
id text PRIMARY KEY,revision int,content text,check_state text); id text PRIMARY KEY,revision int,content text,check_state text);

View File

@ -191,13 +191,21 @@ def test_答案写入失败回滚整个发布__251003(应用测试库):
"CREATE TRIGGER fail_answer BEFORE INSERT ON oracle.muse_dataset_answers " "CREATE TRIGGER fail_answer BEFORE INSERT ON oracle.muse_dataset_answers "
"FOR EACH ROW EXECUTE FUNCTION oracle.fail_answer()" "FOR EACH ROW EXECUTE FUNCTION oracle.fail_answer()"
) )
try:
with pytest.raises(评测错误, match="评测存储操作失败"): with pytest.raises(评测错误, match="评测存储操作失败"):
评测服务(pool).发布数据集(_身份(用途.维护), _数据()) 评测服务(pool).发布数据集(_身份(用途.维护), _数据())
with pool.连接(只读=True) as conn: with pool.连接(只读=True) as conn:
assert ( assert (
conn.execute("SELECT count(*) FROM evaluation.muse_dataset_version").fetchone()[0] == 0 conn.execute("SELECT count(*) FROM evaluation.muse_dataset_version").fetchone()[0]
== 0
) )
assert conn.execute("SELECT count(*) FROM oracle.muse_dataset_answers").fetchone()[0] == 0 assert (
conn.execute("SELECT count(*) FROM oracle.muse_dataset_answers").fetchone()[0] == 0
)
finally:
# 共享库例间只清业务数据;失败触发器与函数必须自愈,否则污染后续用例。
with pool.连接() as conn, conn.transaction():
conn.execute("DROP FUNCTION IF EXISTS oracle.fail_answer() CASCADE")
@pytest.mark.case_id( @pytest.mark.case_id(
@ -390,16 +398,25 @@ def test_匿名映射中途失败回滚全部实验并能原命令重试__251009
"CREATE TRIGGER fail_assignment BEFORE INSERT ON oracle.muse_blind_assignment " "CREATE TRIGGER fail_assignment BEFORE INSERT ON oracle.muse_blind_assignment "
"FOR EACH ROW EXECUTE FUNCTION oracle.fail_assignment()" "FOR EACH ROW EXECUTE FUNCTION oracle.fail_assignment()"
) )
try:
with pytest.raises(评测错误, match="评测存储操作失败"): with pytest.raises(评测错误, match="评测存储操作失败"):
svc.创建实验(_身份(用途.评测), "recover", req) svc.创建实验(_身份(用途.评测), "recover", req)
with pools[用途.评测].连接(只读=True) as conn: with pools[用途.评测].连接(只读=True) as conn:
assert conn.execute("SELECT count(*) FROM evaluation.muse_experiment").fetchone()[0] == 0 assert (
assert conn.execute("SELECT count(*) FROM oracle.muse_blind_assignment").fetchone()[0] == 0 conn.execute("SELECT count(*) FROM evaluation.muse_experiment").fetchone()[0] == 0
)
assert (
conn.execute("SELECT count(*) FROM oracle.muse_blind_assignment").fetchone()[0] == 0
)
with pools[用途.维护].连接() as conn, conn.transaction(): with pools[用途.维护].连接() as conn, conn.transaction():
conn.execute("DROP TRIGGER fail_assignment ON oracle.muse_blind_assignment") conn.execute("DROP TRIGGER fail_assignment ON oracle.muse_blind_assignment")
result = svc.创建实验(_身份(用途.评测), "recover", req) result = svc.创建实验(_身份(用途.评测), "recover", req)
assert len(result["sample_ids"]) == 3 assert len(result["sample_ids"]) == 3
assert svc.创建实验(_身份(用途.评测), "recover", req) == result assert svc.创建实验(_身份(用途.评测), "recover", req) == result
finally:
# 共享库例间只清业务数据;失败触发器与函数必须自愈,否则污染后续用例。
with pools[用途.维护].连接() as conn, conn.transaction():
conn.execute("DROP FUNCTION IF EXISTS oracle.fail_assignment() CASCADE")
@pytest.mark.case_id( @pytest.mark.case_id(

View File

@ -22,6 +22,21 @@ pytestmark = pytest.mark.数据库
交付环境 = 定稿测试.交付环境 交付环境 = 定稿测试.交付环境
@pytest.fixture
def 脏账本还原(应用测试库):
"""账本污染用例的例后还原;muse_migration 属种子表不被例间清空,必须自愈。"""
from muse.基础设施.数据库.迁移 import 列出迁移
yield 应用测试库
基线 = 列出迁移()[0]
with 应用测试库[用途.维护].连接() as conn, conn.transaction():
conn.execute("DELETE FROM muse_migration WHERE version > %s", (基线.版本,))
conn.execute(
"UPDATE muse_migration SET name=%s, checksum=%s WHERE version=%s",
(基线.文件名, 基线.校验和, 基线.版本),
)
def _配置(pool, path): def _配置(pool, path):
cfg = 接入测试._配置文件(pool, path) cfg = 接入测试._配置文件(pool, path)
cfg.write_text( cfg.write_text(
@ -125,6 +140,7 @@ def test_系统页核实际代码资源版本且配置草案不启用__28b001(
@pytest.mark.parametrize("mode", ["future", "checksum"]) @pytest.mark.parametrize("mode", ["future", "checksum"])
def test_未知数据库版本或校验和漂移拒绝HTTP写入保留只读核对__28b002( def test_未知数据库版本或校验和漂移拒绝HTTP写入保留只读核对__28b002(
应用测试库, 应用测试库,
脏账本还原,
tmp_path, tmp_path,
mode, mode,
): ):
@ -166,7 +182,7 @@ def test_未知数据库版本或校验和漂移拒绝HTTP写入保留只读核
then=["不兼容时CLI拒绝配置写入而系统诊断可读"], then=["不兼容时CLI拒绝配置写入而系统诊断可读"],
contract="docs/系统架构/新版设计/文件设计/工程配置与部署.md", contract="docs/系统架构/新版设计/文件设计/工程配置与部署.md",
) )
def test_不兼容时CLI拒绝配置写入而系统诊断可读__28b003(应用测试库, tmp_path, capsys): def test_不兼容时CLI拒绝配置写入而系统诊断可读__28b003(应用测试库, 脏账本还原, tmp_path, capsys):
_配置(应用测试库[用途.生产], tmp_path) _配置(应用测试库[用途.生产], tmp_path)
cfg = next(tmp_path.glob("*.toml")) cfg = next(tmp_path.glob("*.toml"))
with 应用测试库[用途.维护].连接() as conn, conn.transaction(): with 应用测试库[用途.维护].连接() as conn, conn.transaction():

View File

@ -1,338 +0,0 @@
"""真实 PostgreSQL 验证元数据版本与调用方提交点;每例独占可销毁数据库。"""
from __future__ import annotations
from collections.abc import Iterator
from pathlib import Path
import psycopg
import pytest
from muse.元数据.接口 import (
元数据服务,
元数据错误,
导入内置结构,
读取内置种子,
)
from muse.共享.调用身份 import 用途
from muse.基础设施.数据库.连接 import 连接
from muse.配置 import 数据库引用
pytestmark = pytest.mark.数据库
@pytest.fixture
def 元数据库(数据库底座, tmp_path: Path) -> Iterator[数据库引用]:
# 此模板不导入种子;导入幂等、结构发布仍由每个测试实际执行。
with 数据库底座.借库(tmp_path / "元数据引用", (1, 2)) as 工厂:
yield 工厂[用途.维护].引用
@pytest.mark.case_id(
"NC-meta-seed-import-pg",
environment="隔离 PostgreSQL,每例独占新库",
given="隔离空库",
when="两次导入种子",
then=["24 个固定版本可回读且没有隐式绑定。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_内置发布可重复且不自动启用__a42001(元数据库: 数据库引用) -> None:
"""given 隔离空库;when 两次导入种子;then 24 个固定版本可回读且没有隐式绑定。"""
with 连接(元数据库, 用途标记=用途.维护) as 连:
服务 = 元数据服务(连)
with 连.transaction():
初次 = 导入内置结构(服务)
再次 = 导入内置结构(服务)
assert len(初次) == len(再次) == 24
assert sum(not f.field_id.startswith("common.") for 项 in 初次 for f in 项.字段) == 216
assert sum(f.field_id.startswith("common.") for 项 in 初次 for f in 项.字段) == 88
for 种子 in 读取内置种子():
assert 服务.读取结构(种子.结构.schema_id, 1) == 种子.结构
with pytest.raises(元数据错误) as 错误:
服务.读取绑定("work-a:character")
assert 错误.value.错误码 == "SCHEMA_UNKNOWN"
def 安装动态结构(连: psycopg.Connection):
from muse.元数据.接口 import (
值规则,
合成结构,
启用命令,
字段定义,
字段用途,
类型定义,
结构定义,
)
服务 = 元数据服务(连)
类型 = 类型定义("weather_spirit", "天气精灵", "world", "entity", ("entity",))
基础 = 结构定义(
"weather_spirit",
"weather_spirit",
1,
(
字段定义(
"weather.name",
"名称",
"名称",
值规则("text"),
必填=True,
用途=字段用途(aiContext=True),
),
),
)
服务.登记类型(类型)
服务.登记结构候选(基础)
服务.发布结构(基础.schema_id, 1, 基础.内容哈希)
有效 = 合成结构(基础)
命令 = 启用命令("activate-initial", 有效.effective_schema_hash, 0, "author-a", "work-a:spirit")
绑定 = 服务.启用绑定("work-a:spirit", "work-a", 有效, 命令)
return 服务, 基础, 有效, 绑定, 命令
@pytest.mark.case_id(
"NC-meta-binding-rollback-history",
environment="隔离 PostgreSQL,每例独占新库",
given="已启用结构",
when="发布作品扩展、回滚与重复内置导入",
then=["版本固定且旧值可回读。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_扩展发布保留历史绑定且回滚不留半成品__a42002(元数据库: 数据库引用) -> None:
"""given 已启用结构;when 发布作品扩展、回滚与重复内置导入;then 版本固定且旧值可回读。"""
from muse.元数据.接口 import 作品扩展, 值规则, 启用命令, 字段定义
with 连接(元数据库, 用途标记=用途.维护) as 连:
with 连.transaction():
服务, 基础, _, 旧绑定, 原命令 = 安装动态结构(连)
扩展 = 作品扩展(
"work-a",
基础.schema_id,
1,
1,
(
字段定义(
"work-a.affinity", "天气亲和", "天气亲和", 值规则("enum", 枚举=("雨", "雪"))
),
),
)
服务.发布扩展(扩展, 扩展.内容哈希)
服务.发布扩展(扩展, 扩展.内容哈希)
新结构 = 服务.读取有效结构(基础.schema_id, 1, work_id="work-a", extension_version=1)
assert 服务.读取绑定("work-a:spirit") == 旧绑定
with pytest.raises(RuntimeError), 连.transaction():
服务.启用绑定(
"work-a:spirit",
"work-a",
新结构,
启用命令(
"activate-fail", 新结构.effective_schema_hash, 1, "author-a", "work-a:spirit"
),
)
raise RuntimeError("第二参与者失败")
with 连.transaction():
assert 服务.读取绑定("work-a:spirit") == 旧绑定
新绑定 = 服务.启用绑定(
"work-a:spirit",
"work-a",
新结构,
启用命令(
"activate-ok", 新结构.effective_schema_hash, 1, "author-a", "work-a:spirit"
),
)
assert 新绑定.extension_version == 1
导入内置结构(服务)
assert 服务.读取绑定("work-a:spirit") == 新绑定
assert 服务.读取结构(基础.schema_id, 1) == 基础
# 原命令重放返回当时回执,不把当前绑定降回原版本。
assert (
服务.启用绑定(
"work-a:spirit", "work-a", 服务.读取有效结构(基础.schema_id, 1), 原命令
)
== 旧绑定
)
assert 服务.读取绑定("work-a:spirit") == 新绑定
@pytest.mark.case_id(
"NC-meta-reviewed-version-binding",
environment="隔离 PostgreSQL,每例独占新库",
given="已启用结构",
when="候选未发布或预期版本过期",
then=["不改变当前绑定。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_候选未发布与过期绑定均不可启用__a42003(元数据库: 数据库引用) -> None:
"""given 已启用结构;when 候选未发布或预期版本过期;then 不改变当前绑定。"""
from dataclasses import replace
from muse.元数据.接口 import 合成结构, 启用命令
with 连接(元数据库, 用途标记=用途.维护) as 连:
with 连.transaction():
服务, 基础, _, 绑定, _ = 安装动态结构(连)
新版 = replace(基础, schema_version=2, 父版本=1)
服务.登记结构候选(新版)
新 = 合成结构(新版)
with pytest.raises(元数据错误) as 错误:
服务.启用绑定(
绑定.target_ref,
绑定.work_id,
新,
启用命令("draft", 新.effective_schema_hash, 1, "author-a", 绑定.target_ref),
)
assert 错误.value.错误码 == "SCHEMA_UNKNOWN"
服务.发布结构(新版.schema_id, 2, 新版.内容哈希)
with pytest.raises(元数据错误) as 错误:
服务.启用绑定(
绑定.target_ref,
绑定.work_id,
新,
启用命令("stale", 新.effective_schema_hash, 0, "author-a", 绑定.target_ref),
)
assert 错误.value.错误码 == "SCHEMA_STALE"
assert 服务.读取绑定(绑定.target_ref) == 绑定
@pytest.mark.case_id(
"NC-meta-concurrent-submit-guard",
environment="隔离 PostgreSQL,每例独占新库",
given="已预览候选",
when="提交持有结构锁且另连接切换",
then=["切换等待且旧请求在切换后拒绝。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
@pytest.mark.parametrize(
"变更类别", ["structure", "policy"], ids=["schema-switch", "policy-revoke"]
)
def test_结构策略切换与业务提交共享锁__a42004(元数据库: 数据库引用, 变更类别: str) -> None:
"""given 已预览候选;when 提交持有结构锁且另连接切换;then 切换等待且旧请求在切换后拒绝。"""
from concurrent.futures import ThreadPoolExecutor
from dataclasses import asdict, replace
from muse.元数据.接口 import (
合成结构,
启用命令,
字段限制,
定义哈希,
投影字段,
授权摘要,
策略快照,
)
授权 = 授权摘要("author-v1", "source-v1")
with 连接(元数据库, 用途标记=用途.维护) as 连:
with 连.transaction():
服务, 基础, 原结构, 绑定, _ = 安装动态结构(连)
新版 = replace(基础, schema_version=2, 父版本=1)
服务.登记结构候选(新版)
服务.发布结构(新版.schema_id, 2, 新版.内容哈希)
新结构 = 合成结构(新版)
# 仅夹具业务 owner 拥有此表;元数据模块从不访问它。
连.execute("CREATE TABLE public.meta_owner_probe (id int PRIMARY KEY, body text)")
预览 = 投影字段(
原结构,
服务.当前策略(基础.type_id),
授权,
字段用途名="userEditable",
内容用途="planning",
运行用途="production",
)
def 切换(限时: bool) -> None:
with 连接(元数据库, 用途标记=用途.维护) as 第二连, 第二连.transaction():
if 限时:
第二连.execute("SET LOCAL lock_timeout='250ms'")
第二服务 = 元数据服务(第二连)
if 变更类别 == "structure":
第二服务.启用绑定(
绑定.target_ref,
绑定.work_id,
新结构,
启用命令(
"switch", 新结构.effective_schema_hash, 1, "author-a", 绑定.target_ref
),
)
else:
新策略 = 策略快照(
基础.type_id, 1, (字段限制("weather.name", ("userEditable",)),)
)
第二服务.更新策略(
新策略,
启用命令(
"revoke",
定义哈希(asdict(新策略)),
0,
"author-a",
f"type:{基础.type_id}",
),
)
with 连.transaction():
服务.保护提交(
绑定.target_ref,
预览.effective_schema_hash,
预览.projection_version,
授权,
字段用途名="userEditable",
内容用途="planning",
运行用途="production",
)
with ThreadPoolExecutor(max_workers=1) as 池:
with pytest.raises(psycopg.errors.LockNotAvailable):
池.submit(切换, True).result(timeout=5)
连.execute("INSERT INTO public.meta_owner_probe VALUES (1, '作者确认的天气精灵')")
切换(False)
with 连.transaction():
with pytest.raises(元数据错误) as 错误:
服务.保护提交(
绑定.target_ref,
预览.effective_schema_hash,
预览.projection_version,
授权,
字段用途名="userEditable",
内容用途="planning",
运行用途="production",
)
assert 错误.value.错误码 == (
"SCHEMA_STALE" if 变更类别 == "structure" else "PROJECTION_STALE"
)
assert 连.execute("SELECT id FROM public.meta_owner_probe").fetchall() == [(1,)]
@pytest.mark.case_id(
"NC-meta-immutable-extensions",
environment="隔离 PostgreSQL,每例独占新库",
given="已发布结构与作品扩展",
when="覆盖同版本或删除既有扩展",
then=["历史定义保持不变。"],
contract="docs/系统架构/新版设计/接口契约/元数据投影与版本.md",
)
def test_已发布定义与扩展追加合同不可绕过__a42005(元数据库: 数据库引用) -> None:
"""given 已发布结构与作品扩展;when 覆盖同版本或删除既有扩展;then 历史定义保持不变。"""
from dataclasses import replace
from muse.元数据.接口 import 作品扩展, 值规则, 字段定义
with 连接(元数据库, 用途标记=用途.维护) as 连, 连.transaction():
服务, 基础, _, _, _ = 安装动态结构(连)
assert 服务.读取类型(基础.type_id).实例族 == ("entity",)
with pytest.raises(元数据错误) as 错误:
服务.登记结构候选(replace(基础, 字段=()))
assert 错误.value.错误码 == "SCHEMA_CONFLICT"
扩展 = 作品扩展(
"work-a",
基础.schema_id,
1,
1,
(字段定义("work-a.temp", "体温", "体温", 值规则("number")),),
)
服务.发布扩展(扩展, 扩展.内容哈希)
丢字段 = replace(扩展, extension_version=2, 字段=())
with pytest.raises(元数据错误) as 错误:
服务.发布扩展(丢字段, 丢字段.内容哈希)
assert 错误.value.错误码 == "FIELD_NOT_ALLOWED"
assert 服务.读取结构(基础.schema_id, 1) == 基础
assert (
服务.读取有效结构(基础.schema_id, 1, work_id="work-a", extension_version=1).扩展 == 扩展
)

View File

@ -1,28 +1,22 @@
"""真实 PG 的预算原子预留、成本状态与配置版本;不调用外部模型。""" """预算的核心保护:并发不超额、重复调用不重复计费。
from __future__ import annotations 按简化决策删除其余变体用例;夹具从按例克隆改为共享库以消除建库开销。
"""
import dataclasses
import time
import uuid import uuid
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from datetime import UTC, datetime, timedelta from datetime import UTC, datetime, timedelta
from decimal import Decimal from decimal import Decimal
from pathlib import Path from pathlib import Path
from threading import Barrier
import psycopg
import pytest import pytest
from muse.任务运行.接口 import 任务服务, 任务请求, 步骤处理器, 步骤结果, 步骤计划 from muse.任务运行.接口 import (
from muse.任务运行.模型 import 内容哈希 任务服务,
from muse.任务运行.配置版本 import ( 任务请求,
凭据引用, 步骤处理器,
提供方配置, 步骤结果,
运行配置内容, 步骤计划,
配置版本管理,
配置版本错误,
配置验证证据,
) )
from muse.任务运行.预算管理 import ( from muse.任务运行.预算管理 import (
任务预算计划, 任务预算计划,
@ -41,8 +35,7 @@ pytestmark = pytest.mark.数据库
@pytest.fixture @pytest.fixture
def 预算环境(数据库底座, monkeypatch: pytest.MonkeyPatch, tmp_path: Path): def 预算环境(数据库底座, monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
with 数据库底座.借库(tmp_path / "连接引用") as 工厂: with 数据库底座.共享库(tmp_path / "连接引用") as 工厂:
# 子进程恢复入口继承相同的按例数据库,仍跨真实连接验证提交。
for 声明, 当前 in 工厂.items(): for 声明, 当前 in 工厂.items():
monkeypatch.setenv(f"MUSE_BUDGET_{声明.name}_URL", Path(当前.引用.位置).read_text()) monkeypatch.setenv(f"MUSE_BUDGET_{声明.name}_URL", Path(当前.引用.位置).read_text())
yield 工厂 yield 工厂
@ -102,6 +95,8 @@ def 建预算(工厂: 数据库工厂, 任务ID: str, *, 窗口金额="24", 单
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md", contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
) )
def test_并发预留共享窗口且不超额__a61001(预算环境) -> None: def test_并发预留共享窗口且不超额__a61001(预算环境) -> None:
from threading import Barrier
A, 领取A = 新任务(预算环境[用途.生产]) A, 领取A = 新任务(预算环境[用途.生产])
B, 领取B = 新任务(预算环境[用途.生产]) B, 领取B = 新任务(预算环境[用途.生产])
预算A = 建预算(预算环境[用途.生产], A, 窗口金额="10") 预算A = 建预算(预算环境[用途.生产], A, 窗口金额="10")
@ -144,237 +139,3 @@ def test_重复预留发送和结算不重复计费__a61002(预算环境) -> Non
预算.结算("call-1", Decimal("0.2"), 回执ID="changed") 预算.结算("call-1", Decimal("0.2"), 回执ID="changed")
assert 预算.窗口余额()["已知成本"] == Decimal("0.125") assert 预算.窗口余额()["已知成本"] == Decimal("0.125")
assert 预算.窗口余额()["已占次数"] == 1 assert 预算.窗口余额()["已占次数"] == 1
@pytest.mark.case_id(
"NC-budget-unknown-across-window",
environment="隔离 PostgreSQL;配置验证器为显式协议替身",
given="已发送无成本回执的调用且注入旧窗口",
when="尝试新调用并进行可信对账",
then=["未知不计零且跨窗阻断,对账后恢复"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
def test_未知成本跨窗口仍阻止后续调用直至对账__a61003(预算环境) -> None:
身份, 领取 = 新任务(预算环境[用途.生产])
预算 = 建预算(预算环境[用途.生产], 身份)
预算.预留(领取, "unknown", "writer")
预算.标记已发送(领取, "unknown", 最长秒=30)
未知 = 预算.结算("unknown", None, 回执ID="missing-cost")
assert 未知.实际金额 is None and 未知.状态 == "unknown"
with 预算环境[用途.维护].连接() as 连:
连.execute(
"UPDATE public.muse_budget_reservation SET window_start=window_start-interval '1 "
"day',window_end=window_end-interval '1 day' WHERE call_id='unknown'"
)
assert 预算.窗口余额()["未知调用数"] == 1
with pytest.raises(预算不足, match="未知成本"):
预算.预留(领取, "next", "writer")
预算.结算("unknown", Decimal("0.2"), 回执ID="resolved-receipt")
assert 预算.预留(领取, "next", "writer").状态 == "reserved"
@pytest.mark.case_id(
"NC-budget-cancellation",
environment="隔离 PostgreSQL;配置验证器为显式协议替身",
given="未发送或已发送的预留",
when="关闭任务预算",
then=["未发送释放、已发送未知且任务不再外发"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
@pytest.mark.parametrize("已发出", [False, True], ids=["before-send", "after-send"])
def test_取消保留已发送调用的未知成本__a61004(预算环境, 已发出: bool) -> None:
身份, 领取 = 新任务(预算环境[用途.生产])
预算 = 建预算(预算环境[用途.生产], 身份)
预算.预留(领取, "cancel", "writer")
if 已发出:
预算.标记已发送(领取, "cancel", 最长秒=30)
预算.关闭任务预算(身份)
assert 预算.读取("cancel").状态 == ("unknown" if 已发出 else "released")
with pytest.raises(预算不足):
预算.预留(领取, "blocked", "writer")
@pytest.mark.case_id(
"NC-budget-expiration",
environment="隔离 PostgreSQL;配置验证器为显式协议替身",
given="未发送或在途预留期限到达",
when="真实时间过期后回收",
then=["未发送释放,在途保留未知成本"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
@pytest.mark.parametrize("已发出", [False, True], ids=["reserved", "in-flight"])
def test_过期只释放尚未发送的预留__a61005(预算环境, 已发出: bool) -> None:
身份, 领取 = 新任务(预算环境[用途.生产])
预算 = 建预算(预算环境[用途.生产], 身份)
预算.预留(领取, "expire", "writer", 有效秒=0.08 if not 已发出 else 30)
if 已发出:
预算.标记已发送(领取, "expire", 最长秒=0.08)
time.sleep(0.12)
预算.回收过期()
assert 预算.读取("expire").状态 == ("unknown" if 已发出 else "released")
if not 已发出:
assert 预算.窗口余额()["在途预留"] == 0
assert 预算.预留(领取, "fresh", "writer").状态 == "reserved"
@pytest.mark.case_id(
"NC-budget-actual-over-cap",
environment="隔离 PostgreSQL;配置验证器为显式协议替身",
given="实际成本超过单次预留金额",
when="用真实回执结算",
then=["保存真实金额和超额标记,停止任务预算"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
def test_超单次成本先保存事实并关闭任务预算__a61006(预算环境) -> None:
身份, 领取 = 新任务(预算环境[用途.生产])
预算 = 建预算(预算环境[用途.生产], 身份)
预算.预留(领取, "over", "writer")
预算.标记已发送(领取, "over", 最长秒=30)
已记 = 预算.结算("over", Decimal("7"), 回执ID="actual-over")
assert 已记.超预算 and 已记.实际金额 == Decimal("7")
assert 预算.窗口余额()["已知成本"] == Decimal("7")
with pytest.raises(预算不足):
预算.预留(领取, "next", "writer")
@pytest.mark.case_id(
"NC-budget-settlement-rollback",
environment="隔离 PostgreSQL;配置验证器为显式协议替身",
given="调用在途及PG注入结算失败触发器",
when="结算回滚后移除故障再结算",
then=["故障保留在途标记和未知实际金额,可恢复结算"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
def test_结算失败回滚仍保留在途标记__a61007(预算环境) -> None:
身份, 领取 = 新任务(预算环境[用途.生产])
预算 = 建预算(预算环境[用途.生产], 身份)
预算.预留(领取, "atomic", "writer")
预算.标记已发送(领取, "atomic", 最长秒=30)
with 预算环境[用途.维护].连接() as 连:
连.execute(
"CREATE FUNCTION public.reject_settlement() RETURNS trigger LANGUAGE plpgsql AS $$ "
"BEGIN IF NEW.state='settled' THEN RAISE EXCEPTION 'synthetic failure'; END IF; "
"RETURN NEW; END $$"
)
连.execute(
"CREATE TRIGGER reject_settlement BEFORE UPDATE ON public.muse_budget_reservation "
"FOR EACH ROW EXECUTE FUNCTION public.reject_settlement()"
)
with pytest.raises(psycopg.errors.RaiseException):
预算.结算("atomic", Decimal("1"), 回执ID="receipt")
assert 预算.读取("atomic").状态 == "in_flight"
assert 预算.读取("atomic").实际金额 is None
with 预算环境[用途.维护].连接() as 连:
连.execute("DROP TRIGGER reject_settlement ON public.muse_budget_reservation")
assert 预算.结算("atomic", Decimal("1"), 回执ID="receipt").状态 == "settled"
@pytest.mark.case_id(
"NC-budget-call-limits",
environment="隔离 PostgreSQL;配置验证器为显式协议替身",
given="窗口次数或任务计划只剩一次",
when="结算零成本调用后尝试下一次",
then=["两类次数限制都独立生效"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
@pytest.mark.parametrize("模式", ["window", "task"], ids=["window", "task"])
def test_窗口次数和任务计划分别限制调用__a61008(预算环境, 模式: str) -> None:
身份, 领取 = 新任务(预算环境[用途.生产])
预算 = 建预算(
预算环境[用途.生产],
身份,
窗口次数=1 if 模式 == "window" else 6000,
次数=3 if 模式 == "window" else 1,
)
预算.预留(领取, "first", "writer")
预算.标记已发送(领取, "first", 最长秒=30)
预算.结算("first", Decimal("0"), 回执ID="free-but-called")
with pytest.raises(预算不足, match="次数"):
预算.预留(领取, "second", "writer")
class 合成配置验证器:
身份 = "synthetic-contract-check-v1"
def 验证(self, 内容: 运行配置内容, 执行用途: 用途) -> 配置验证证据:
assert 内容.角色配置["writer"]["model"] == "model"
return 配置验证证据(
内容哈希(内容.冻结()),
内容.角色策略版本,
内容.资源发布身份,
执行用途,
("synthetic-protocol-result",),
"offline_contract",
)
def 配置内容(版本="1"):
return 运行配置内容(
"direct",
版本,
"policy-1",
"release-1",
"quota-1",
{"writer": {"provider": "provider", "model": "model", "thinking": "high"}},
(凭据引用("api", "环境变量", "MUSE_API_KEY"),),
(提供方配置("provider", "responses", "https://provider.invalid/v1/responses", "api"),),
"price-1",
)
@pytest.mark.case_id(
"NC-config-version-freeze",
environment="隔离 PostgreSQL;配置验证器为显式协议替身",
given="显式合成验证器和两版配置",
when="验证启用、任务冻结、升级和停用",
then=["版本不可覆盖,CAS与回执匹配,旧任务副本保留"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
def test_配置验证启用与任务冻结版本互不覆盖__a61009(预算环境) -> None:
管理 = 配置版本管理(预算环境[用途.评测], 合成配置验证器())
A, _ = 新任务(预算环境[用途.评测])
原 = 管理.保存草案("config", "1", 配置内容())
with pytest.raises(配置版本错误):
管理.冻结到任务(A, "config")
with pytest.raises(配置版本错误):
管理.保存草案("config", "1", 配置内容("changed"))
R1 = 管理.验证版本("config", "1")
assert 管理.启用("config", "1", 验证回执=R1, 批准引用="approve-1", 预期代次=0) == 1
assert 管理.启用("config", "1", 验证回执=R1, 批准引用="approve-1", 预期代次=0) == 1
冻结 = 管理.冻结到任务(A, "config")
管理.保存草案("config", "2", 配置内容("2"))
R2 = 管理.验证版本("config", "2")
with pytest.raises(配置版本错误):
管理.启用("config", "2", 验证回执=R1, 批准引用="approve-2", 预期代次=1)
assert 管理.启用("config", "2", 验证回执=R2, 批准引用="approve-2", 预期代次=1) == 2
assert 管理.冻结到任务(A, "config") == 冻结 == 原
B, _ = 新任务(预算环境[用途.评测])
assert 管理.冻结到任务(B, "config").版本 == "2"
管理.停用("config", 预期代次=2)
C, _ = 新任务(预算环境[用途.评测])
with pytest.raises(配置版本错误):
管理.冻结到任务(C, "config")
assert 管理.冻结到任务(A, "config").版本 == "1"
@pytest.mark.case_id(
"NC-config-validation-binding",
environment="隔离 PostgreSQL;配置验证器为显式协议替身",
given="陈旧绑定或离线证据用于生产",
when="验证配置版本",
then=["绑定不符和离线生产都拒绝"],
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
)
def test_配置不接受陈旧验证或离线证据启用生产__a6100a(预算环境) -> None:
class 陈旧验证器(合成配置验证器):
def 验证(self, 内容, 执行用途):
return dataclasses.replace(super().验证(内容, 执行用途), 配置哈希="old-hash")
管理 = 配置版本管理(预算环境[用途.评测], 陈旧验证器())
管理.保存草案("config", "1", 配置内容())
with pytest.raises(配置版本错误, match="绑定"):
管理.验证版本("config", "1")
生产 = 配置版本管理(预算环境[用途.生产], 合成配置验证器())
生产.保存草案("config", "1", 配置内容())
with pytest.raises(配置版本错误, match="离线"):
生产.验证版本("config", "1")

View File

@ -1,64 +0,0 @@
-- V0001 共享标识与版本:新版数据库的共享基础。
-- 内容:迁移账本 + 跨模块共用的身份域(用途、内容用途、候选状态、审阅动作)。
-- 边界:只建立共享基础;业务对象表由 V0005 起各模块迁移建立,
-- 不在本版本预建业务列,也不复用旧库 DDL。
-- 迁移账本:版本、校验和与应用时间是数据演进的唯一事实(迁移执行器维护)
CREATE TABLE muse_migration (
version integer PRIMARY KEY CHECK (version >= 1),
name text NOT NULL,
checksum text NOT NULL CHECK (length(checksum) = 64),
applied_at timestamptz NOT NULL DEFAULT now()
);
-- 执行隔离用途:与 src/muse/共享/调用身份.py 的 StrEnum 一一对应
CREATE TYPE muse_purpose AS ENUM (
'production',
'evaluation',
'maintenance'
);
-- 内容消费用途:任务与工具按此裁剪可见字段
CREATE TYPE muse_content_purpose AS ENUM (
'planning',
'generation',
'detection',
'extraction'
);
-- 候选状态:候选先审后入的状态轴(各业务模块实例共用同一解释)
CREATE TYPE muse_candidate_state AS ENUM (
'draft',
'candidate',
'adopted',
'rejected',
'retired'
);
-- 审阅动作:作者决策的闭合集合
CREATE TYPE muse_review_action AS ENUM (
'adopt',
'reject',
'revise'
);
-- 固定角色在目标环境初始化,迁移必须由 muse_maint 执行。
-- schema 负责用途边界;业务模块后续迁移在相应用途中建立自己的对象。
REVOKE ALL ON SCHEMA public FROM PUBLIC;
GRANT USAGE ON SCHEMA public TO muse_app, muse_eval;
CREATE SCHEMA evaluation AUTHORIZATION muse_maint;
CREATE SCHEMA oracle AUTHORIZATION muse_maint;
REVOKE ALL ON SCHEMA evaluation, oracle FROM PUBLIC;
GRANT USAGE ON SCHEMA evaluation, oracle TO muse_eval;
-- 维护账本不授予运行角色写权限;后续生产表只允许生产角色读写。
ALTER DEFAULT PRIVILEGES FOR ROLE muse_maint IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO muse_app;
ALTER DEFAULT PRIVILEGES FOR ROLE muse_maint IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO muse_app;
ALTER DEFAULT PRIVILEGES FOR ROLE muse_maint IN SCHEMA evaluation
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO muse_eval;
ALTER DEFAULT PRIVILEGES FOR ROLE muse_maint IN SCHEMA evaluation
GRANT USAGE, SELECT ON SEQUENCES TO muse_eval;
ALTER DEFAULT PRIVILEGES FOR ROLE muse_maint IN SCHEMA oracle
GRANT SELECT ON TABLES TO muse_eval;

File diff suppressed because it is too large Load Diff

View File

@ -1,84 +0,0 @@
-- 元数据只保存结构模具与绑定,不保存任何业务实例值。
CREATE SCHEMA metadata AUTHORIZATION muse_maint;
GRANT USAGE ON SCHEMA metadata TO muse_app, muse_eval;
CREATE TABLE metadata.type_registry (
type_id text PRIMARY KEY,
definition jsonb NOT NULL,
content_hash text NOT NULL,
state text NOT NULL DEFAULT 'candidate' CHECK (state IN ('candidate', 'published')),
policy_revision bigint NOT NULL DEFAULT 0 CHECK (policy_revision >= 0)
);
CREATE TABLE metadata.schema_version (
schema_id text NOT NULL,
schema_version bigint NOT NULL CHECK (schema_version > 0),
type_id text NOT NULL REFERENCES metadata.type_registry(type_id),
definition jsonb NOT NULL,
content_hash text NOT NULL,
state text NOT NULL CHECK (state IN ('candidate', 'published')),
PRIMARY KEY (schema_id, schema_version)
);
CREATE TABLE metadata.work_extension (
work_id text NOT NULL,
schema_id text NOT NULL,
base_version bigint NOT NULL,
extension_version bigint NOT NULL CHECK (extension_version > 0),
definition jsonb NOT NULL,
content_hash text NOT NULL,
state text NOT NULL CHECK (state IN ('candidate', 'published')),
PRIMARY KEY (work_id, schema_id, base_version, extension_version),
FOREIGN KEY (schema_id, base_version)
REFERENCES metadata.schema_version(schema_id, schema_version)
);
CREATE TABLE metadata.policy_version (
type_id text NOT NULL REFERENCES metadata.type_registry(type_id),
version bigint NOT NULL CHECK (version > 0),
definition jsonb NOT NULL,
content_hash text NOT NULL,
PRIMARY KEY (type_id, version)
);
CREATE TABLE metadata.schema_binding (
target_ref text PRIMARY KEY,
work_id text NOT NULL,
type_id text NOT NULL REFERENCES metadata.type_registry(type_id),
schema_id text NOT NULL,
base_version bigint NOT NULL,
extension_version bigint,
effective_schema_hash text NOT NULL,
revision bigint NOT NULL CHECK (revision > 0),
FOREIGN KEY (schema_id, base_version)
REFERENCES metadata.schema_version(schema_id, schema_version),
FOREIGN KEY (work_id, schema_id, base_version, extension_version)
REFERENCES metadata.work_extension(work_id, schema_id, base_version, extension_version)
);
CREATE TABLE metadata.activation_receipt (
command_id text PRIMARY KEY,
request_hash text NOT NULL,
result jsonb NOT NULL,
reviewed_by text NOT NULL,
change_scope text NOT NULL,
created_at timestamptz NOT NULL DEFAULT current_timestamp
);
-- 候选修改也必须产生新版本;发布只允许改变状态,正文定义永远不可原地覆盖。
CREATE FUNCTION metadata.immutable_definition() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF TG_OP = 'DELETE' THEN
RAISE EXCEPTION '元数据历史版本不可删除';
END IF;
IF NEW.definition IS DISTINCT FROM OLD.definition
OR NEW.content_hash IS DISTINCT FROM OLD.content_hash THEN
RAISE EXCEPTION '元数据定义不可修改,必须新增版本';
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER schema_immutable BEFORE UPDATE OR DELETE ON metadata.schema_version
FOR EACH ROW EXECUTE FUNCTION metadata.immutable_definition();
CREATE TRIGGER extension_immutable BEFORE UPDATE OR DELETE ON metadata.work_extension
FOR EACH ROW EXECUTE FUNCTION metadata.immutable_definition();
CREATE TRIGGER policy_immutable BEFORE UPDATE OR DELETE ON metadata.policy_version
FOR EACH ROW EXECUTE FUNCTION metadata.immutable_definition();
GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA metadata TO muse_app;
GRANT SELECT ON ALL TABLES IN SCHEMA metadata TO muse_eval;

View File

@ -1,38 +0,0 @@
-- S01只保存命令、审阅、决定与影响;业务实例表由各owner迁移定义。
CREATE TABLE public.muse_change_command (
author_id text NOT NULL,
run_purpose public.muse_purpose NOT NULL CHECK (run_purpose='production'),
command_id text NOT NULL,
request_hash text NOT NULL,
receipt jsonb,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(author_id,run_purpose,command_id)
);
CREATE TABLE public.muse_author_review (
review_id uuid PRIMARY KEY,
author_id text NOT NULL,
entry_id text NOT NULL,
target_ref text NOT NULL,
content_hash text NOT NULL,
snapshot jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TABLE public.muse_candidate_decision (
candidate_id text NOT NULL,
candidate_revision bigint NOT NULL,
decision text NOT NULL CHECK (decision IN ('adopt','reject','defer')),
author_id text NOT NULL,
review_id uuid NOT NULL REFERENCES public.muse_author_review(review_id),
command_id text NOT NULL,
PRIMARY KEY(candidate_id,candidate_revision)
);
CREATE TABLE public.muse_change_impact (
impact_id uuid PRIMARY KEY,
receipt_id uuid NOT NULL,
target_ref text NOT NULL,
reason text NOT NULL,
before_version text NOT NULL,
after_version text NOT NULL,
state text NOT NULL DEFAULT 'pending' CHECK (state IN ('pending','handled')),
UNIQUE(receipt_id,target_ref,reason)
);

View File

@ -1,290 +0,0 @@
-- 持久任务、步骤、尝试、独立作用域租约和有序事件。
-- 模型预算与原文生命周期随所属实现接入;用于当前开发基线。
DO $migration$
DECLARE
namespace text;
BEGIN
FOREACH namespace IN ARRAY ARRAY['public', 'evaluation'] LOOP
EXECUTE format($ddl$
CREATE TABLE %1$I.muse_task (
task_id uuid PRIMARY KEY,
run_purpose public.muse_purpose NOT NULL,
command_id text NOT NULL,
author_id text NOT NULL,
request_hash text NOT NULL,
frozen_input jsonb NOT NULL,
flow_id text NOT NULL,
flow_version text NOT NULL,
flow_snapshot jsonb NOT NULL,
state text NOT NULL CHECK (state IN ('queued','running','paused','reconciling','failed','cancelled','completed')),
scope_key text,
scope_generation bigint,
last_sequence bigint NOT NULL DEFAULT 0,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (run_purpose, author_id, command_id)
);
CREATE TABLE %1$I.muse_task_control (
run_purpose public.muse_purpose NOT NULL,
author_id text NOT NULL,
command_id text NOT NULL,
task_id uuid NOT NULL REFERENCES %1$I.muse_task(task_id),
request_hash text NOT NULL,
receipt jsonb,
PRIMARY KEY (run_purpose, author_id, command_id)
);
CREATE TABLE %1$I.muse_step (
task_id uuid NOT NULL REFERENCES %1$I.muse_task(task_id),
step_id text NOT NULL,
processor_id text NOT NULL,
processor_version text NOT NULL,
dependencies text[] NOT NULL,
state text NOT NULL CHECK (state IN ('pending','running','completed','failed','unknown')),
current_attempt uuid,
checkpoint jsonb NOT NULL DEFAULT '{}',
result jsonb,
PRIMARY KEY (task_id, step_id)
);
CREATE TABLE %1$I.muse_attempt (
attempt_id uuid PRIMARY KEY,
task_id uuid NOT NULL,
step_id text NOT NULL,
worker_id text NOT NULL,
lease_token uuid NOT NULL,
lease_until timestamptz NOT NULL,
scope_generation bigint,
heartbeat_at timestamptz NOT NULL DEFAULT clock_timestamp(),
state text NOT NULL CHECK (state IN ('running','expired','paused','cancelled','completed','failed','unknown')),
call_state text NOT NULL DEFAULT 'not_sent' CHECK (call_state IN ('not_sent','sent','unknown','saved')),
call_reference text,
result jsonb,
failure_code text,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
FOREIGN KEY (task_id, step_id) REFERENCES %1$I.muse_step(task_id, step_id)
);
CREATE TABLE %1$I.muse_task_scope_lease (
run_purpose public.muse_purpose NOT NULL,
scope_key text NOT NULL,
holder_task_id uuid,
generation bigint NOT NULL CHECK (generation > 0),
lease_until timestamptz NOT NULL,
PRIMARY KEY (run_purpose, scope_key)
);
CREATE TABLE %1$I.muse_task_event (
event_id uuid PRIMARY KEY,
task_id uuid NOT NULL REFERENCES %1$I.muse_task(task_id),
step_id text,
attempt_id uuid,
sequence bigint NOT NULL,
event_type text NOT NULL,
occurred_at timestamptz NOT NULL DEFAULT clock_timestamp(),
payload_version integer NOT NULL,
payload jsonb NOT NULL,
payload_hash text NOT NULL,
UNIQUE(task_id, sequence)
);
CREATE INDEX ON %1$I.muse_task (run_purpose, state, created_at);
CREATE INDEX ON %1$I.muse_step (state, task_id);
CREATE INDEX ON %1$I.muse_attempt (task_id, state, lease_until);
$ddl$, namespace);
END LOOP;
END
$migration$;
-- 预算与配置:额度账户串行化预留,金额事实留在逐调用账本。
-- 原文:授权和租约先持久化,完整归档事务同时写入清单与字节。
DO $raw_lifecycle$
DECLARE namespace text;
BEGIN
FOREACH namespace IN ARRAY ARRAY['public', 'evaluation'] LOOP
EXECUTE format($ddl$
CREATE TABLE %1$I.muse_raw_namespace (
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
namespace_id uuid NOT NULL DEFAULT gen_random_uuid()
);
INSERT INTO %1$I.muse_raw_namespace (singleton) VALUES (true);
CREATE TABLE %1$I.muse_raw_orphan_cleanup (
run_purpose public.muse_purpose NOT NULL,
object_id uuid NOT NULL,
receipt_id uuid NOT NULL DEFAULT gen_random_uuid(),
state text NOT NULL CHECK (state IN ('pending','completed')),
removed boolean,
completed_at timestamptz,
PRIMARY KEY (run_purpose,object_id)
);
CREATE TABLE %1$I.muse_raw_authorization (
authorization_id uuid PRIMARY KEY,
task_id uuid NOT NULL REFERENCES %1$I.muse_task(task_id),
command_id text NOT NULL,
approved_by text NOT NULL,
source_version text NOT NULL,
content_hashes text[] NOT NULL CHECK (cardinality(content_hashes)>0),
purpose text NOT NULL,
retention_mode text NOT NULL CHECK (retention_mode IN ('temporary','archive','persistent')),
approved_at timestamptz NOT NULL,
valid_until timestamptz NOT NULL,
request_hash text NOT NULL,
revoked boolean NOT NULL DEFAULT false,
call_id text,
call_request_hash text,
response_hash text,
parent_authorization_id uuid REFERENCES %1$I.muse_raw_authorization(authorization_id),
attempt_id uuid REFERENCES %1$I.muse_attempt(attempt_id),
derivation_kind text CHECK (derivation_kind IN ('model','tool')),
evidence_refs uuid[] NOT NULL DEFAULT '{}',
CHECK ((call_id IS NULL) = (call_request_hash IS NULL)),
CHECK (response_hash IS NULL OR call_id IS NOT NULL),
UNIQUE(task_id,command_id)
);
CREATE TABLE %1$I.muse_role_session_authorization (
authorization_id uuid PRIMARY KEY REFERENCES %1$I.muse_raw_authorization(authorization_id),
step_id text NOT NULL,
stage text NOT NULL,
initial_request_hash text NOT NULL,
frozen_input_hash text NOT NULL,
max_model_calls integer NOT NULL CHECK (max_model_calls>0),
max_tool_calls integer NOT NULL CHECK (max_tool_calls>=0),
policy_hash text NOT NULL
);
CREATE TABLE %1$I.muse_raw_lease (
lease_id uuid PRIMARY KEY,
task_id uuid NOT NULL REFERENCES %1$I.muse_task(task_id),
authorization_id uuid NOT NULL REFERENCES %1$I.muse_raw_authorization(authorization_id),
command_id text NOT NULL,
created_at timestamptz NOT NULL,
retain_until timestamptz NOT NULL,
state text NOT NULL CHECK (state IN ('open','closed','migrating','migrated')),
archive_id uuid,
archive_authorization_id uuid REFERENCES %1$I.muse_raw_authorization(authorization_id),
cleanup_receipt uuid,
failure_code text,
UNIQUE(task_id,command_id),
CHECK (retain_until>created_at AND retain_until<=created_at+interval '24 hours')
);
CREATE TABLE %1$I.muse_raw_archive (
archive_id uuid PRIMARY KEY,
lease_id uuid UNIQUE REFERENCES %1$I.muse_raw_lease(lease_id),
legacy_ref text UNIQUE,
legacy_manifest jsonb,
CHECK ((lease_id IS NULL) <> (legacy_ref IS NULL)),
authorization_id uuid NOT NULL REFERENCES %1$I.muse_raw_authorization(authorization_id),
tree_hash text NOT NULL,
entry_count integer NOT NULL CHECK (entry_count>0),
total_bytes bigint NOT NULL CHECK (total_bytes>=0),
receipt_id uuid NOT NULL,
created_at timestamptz NOT NULL
);
CREATE TABLE %1$I.muse_raw_archive_item (
archive_id uuid NOT NULL REFERENCES %1$I.muse_raw_archive(archive_id),
content_hash text NOT NULL,
content bytea NOT NULL,
PRIMARY KEY(archive_id,content_hash)
);
CREATE TABLE %1$I.muse_runtime_evidence (
evidence_id uuid PRIMARY KEY,
task_id uuid NOT NULL REFERENCES %1$I.muse_task(task_id),
attempt_id uuid NOT NULL REFERENCES %1$I.muse_attempt(attempt_id),
kind text NOT NULL CHECK (kind IN ('model_input','model_response','tool_result','failure')),
reference_id text NOT NULL,
content_hash text NOT NULL,
content bytea,
authorization_id uuid REFERENCES %1$I.muse_raw_authorization(authorization_id),
outcome text NOT NULL CHECK (outcome IN ('completed','failed','partial')),
metadata jsonb NOT NULL,
revision integer NOT NULL DEFAULT 1,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(attempt_id,kind,reference_id),
CHECK (content IS NULL OR authorization_id IS NOT NULL)
);
$ddl$, namespace);
EXECUTE format('REVOKE INSERT,UPDATE,DELETE ON %I.muse_raw_namespace FROM muse_app,muse_eval', namespace);
END LOOP;
END
$raw_lifecycle$;
DO $budget_config$
DECLARE
namespace text;
BEGIN
FOREACH namespace IN ARRAY ARRAY['public', 'evaluation'] LOOP
EXECUTE format($ddl$
CREATE TABLE %1$I.muse_budget_account (
run_purpose public.muse_purpose NOT NULL,
account_id text NOT NULL,
policy_version text NOT NULL,
policy jsonb NOT NULL,
policy_hash text NOT NULL,
PRIMARY KEY (run_purpose, account_id)
);
CREATE TABLE %1$I.muse_task_budget (
task_id uuid PRIMARY KEY REFERENCES %1$I.muse_task(task_id),
account_id text NOT NULL,
plan jsonb NOT NULL,
plan_hash text NOT NULL,
stopped boolean NOT NULL DEFAULT false
);
CREATE TABLE %1$I.muse_budget_reservation (
run_purpose public.muse_purpose NOT NULL,
call_id text NOT NULL,
account_id text NOT NULL,
task_id uuid NOT NULL REFERENCES %1$I.muse_task(task_id),
attempt_id uuid NOT NULL REFERENCES %1$I.muse_attempt(attempt_id),
role_id text NOT NULL,
request_hash text NOT NULL,
state text NOT NULL CHECK (state IN ('reserved','in_flight','unknown','settled','released')),
reserved_amount numeric(24,6) NOT NULL CHECK (reserved_amount >= 0),
actual_amount numeric(24,6) CHECK (actual_amount >= 0),
window_start timestamptz NOT NULL,
window_end timestamptz NOT NULL,
expires_at timestamptz NOT NULL,
sent_at timestamptz,
receipt_id text,
reason text,
over_budget boolean NOT NULL DEFAULT false,
PRIMARY KEY (run_purpose, call_id),
CHECK ((state = 'settled') = (actual_amount IS NOT NULL))
);
CREATE INDEX ON %1$I.muse_budget_reservation (run_purpose,account_id,window_start);
CREATE INDEX ON %1$I.muse_budget_reservation (task_id,role_id);
CREATE TABLE %1$I.muse_runtime_config_version (
run_purpose public.muse_purpose NOT NULL,
config_id text NOT NULL,
version text NOT NULL,
content jsonb NOT NULL,
content_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (run_purpose,config_id,version)
);
CREATE TABLE %1$I.muse_runtime_config_validation (
receipt_id uuid PRIMARY KEY,
run_purpose public.muse_purpose NOT NULL,
config_id text NOT NULL,
version text NOT NULL,
content_hash text NOT NULL,
validator_id text NOT NULL,
evidence_refs jsonb NOT NULL,
validated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
FOREIGN KEY (run_purpose,config_id,version)
REFERENCES %1$I.muse_runtime_config_version(run_purpose,config_id,version)
);
CREATE TABLE %1$I.muse_runtime_config_active (
run_purpose public.muse_purpose NOT NULL,
config_id text NOT NULL,
version text,
validation_receipt uuid REFERENCES %1$I.muse_runtime_config_validation(receipt_id),
approval_ref text,
generation bigint NOT NULL DEFAULT 0,
PRIMARY KEY (run_purpose,config_id)
);
CREATE TABLE %1$I.muse_task_config_binding (
task_id uuid PRIMARY KEY REFERENCES %1$I.muse_task(task_id),
config_id text NOT NULL,
version text NOT NULL,
content_hash text NOT NULL,
content jsonb NOT NULL,
validation_receipt uuid NOT NULL REFERENCES %1$I.muse_runtime_config_validation(receipt_id)
);
$ddl$, namespace);
END LOOP;
END
$budget_config$;

View File

@ -1,94 +0,0 @@
-- B01拥有作品身份、动态档案版本及稳定章节目录。
CREATE TABLE public.muse_work (
work_id text PRIMARY KEY,
author_id text NOT NULL,
current_revision bigint NOT NULL CHECK(current_revision>0),
directory_revision bigint NOT NULL DEFAULT 0 CHECK(directory_revision>=0)
);
CREATE TABLE public.muse_work_version (
work_id text NOT NULL REFERENCES public.muse_work(work_id),
revision bigint NOT NULL CHECK(revision>0),
content jsonb NOT NULL,
schema_binding jsonb NOT NULL,
content_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(work_id,revision)
);
CREATE TABLE public.muse_chapter (
chapter_id text PRIMARY KEY,
work_id text NOT NULL REFERENCES public.muse_work(work_id),
title text NOT NULL,
position integer NOT NULL CHECK(position>0),
UNIQUE(work_id,position) DEFERRABLE INITIALLY DEFERRED
);
CREATE TABLE public.muse_directory_version (
work_id text NOT NULL REFERENCES public.muse_work(work_id),
revision bigint NOT NULL CHECK(revision>=0),
chapters jsonb NOT NULL,
nodes jsonb NOT NULL DEFAULT '[]',
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(work_id,revision)
);
ALTER TABLE public.muse_work ADD CONSTRAINT work_current_version_fk
FOREIGN KEY(work_id,current_revision) REFERENCES public.muse_work_version(work_id,revision)
DEFERRABLE INITIALLY DEFERRED;
ALTER TABLE public.muse_work ADD CONSTRAINT work_current_directory_fk
FOREIGN KEY(work_id,directory_revision) REFERENCES public.muse_directory_version(work_id,revision)
DEFERRABLE INITIALLY DEFERRED;
-- 明确选定的探索来源属于B01;保留不可变快照,不将方向选择当作规划确认。
CREATE TABLE public.muse_exploration_selection (
work_id text PRIMARY KEY REFERENCES public.muse_work(work_id),
current_revision bigint NOT NULL CHECK(current_revision>0)
);
CREATE TABLE public.muse_exploration_selection_version (
work_id text NOT NULL REFERENCES public.muse_work(work_id),
revision bigint NOT NULL CHECK(revision>0),
snapshot jsonb NOT NULL,
selected_by text NOT NULL,
selected_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(work_id,revision)
);
ALTER TABLE public.muse_exploration_selection ADD CONSTRAINT selection_current_version_fk
FOREIGN KEY(work_id,current_revision) REFERENCES public.muse_exploration_selection_version(work_id,revision)
DEFERRABLE INITIALLY DEFERRED;
CREATE TABLE public.muse_plan (
plan_id text PRIMARY KEY,
work_id text NOT NULL REFERENCES public.muse_work(work_id),
type_id text NOT NULL,
chapter_id text REFERENCES public.muse_chapter(chapter_id),
node_id text,
current_revision bigint NOT NULL CHECK(current_revision>0)
);
CREATE TABLE public.muse_plan_version (
plan_id text NOT NULL REFERENCES public.muse_plan(plan_id),
revision bigint NOT NULL CHECK(revision>0),
request jsonb NOT NULL,
content jsonb NOT NULL,
content_hash text NOT NULL,
schema_binding jsonb NOT NULL,
source_dependencies jsonb NOT NULL,
author_id text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(plan_id,revision)
);
ALTER TABLE public.muse_plan ADD CONSTRAINT plan_current_version_fk
FOREIGN KEY(plan_id,current_revision) REFERENCES public.muse_plan_version(plan_id,revision)
DEFERRABLE INITIALLY DEFERRED;
CREATE TABLE public.muse_plan_candidate (
candidate_id uuid PRIMARY KEY,
work_id text NOT NULL REFERENCES public.muse_work(work_id),
plan_id text NOT NULL,
current_revision bigint NOT NULL CHECK(current_revision>0)
);
CREATE TABLE public.muse_plan_candidate_version (
candidate_id uuid NOT NULL REFERENCES public.muse_plan_candidate(candidate_id),
revision bigint NOT NULL CHECK(revision>0),
request jsonb NOT NULL,
content jsonb NOT NULL,
source_dependencies jsonb NOT NULL,
candidate_hash text NOT NULL,
PRIMARY KEY(candidate_id,revision)
);
ALTER TABLE public.muse_plan_candidate ADD CONSTRAINT plan_candidate_current_version_fk
FOREIGN KEY(candidate_id,current_revision) REFERENCES public.muse_plan_candidate_version(candidate_id,revision)
DEFERRABLE INITIALLY DEFERRED;

View File

@ -1,52 +0,0 @@
-- B02本书实例、不可变事实版本与提案;作者决定仍由S01保存。
CREATE TABLE public.muse_world_clock (
work_id text PRIMARY KEY REFERENCES public.muse_work(work_id),
system_revision bigint NOT NULL CHECK(system_revision>=0)
);
CREATE TABLE public.muse_world_object (
object_id text PRIMARY KEY,
work_id text NOT NULL REFERENCES public.muse_work(work_id),
type_id text NOT NULL,
instance_family text NOT NULL CHECK(instance_family IN ('entity','relation')),
current_revision bigint NOT NULL CHECK(current_revision>0)
);
CREATE TABLE public.muse_world_version (
object_id text NOT NULL REFERENCES public.muse_world_object(object_id),
revision bigint NOT NULL CHECK(revision>0),
system_revision bigint NOT NULL CHECK(system_revision>0),
content jsonb NOT NULL,
content_hash text NOT NULL,
schema_binding jsonb NOT NULL,
sources jsonb NOT NULL,
independent boolean NOT NULL,
effective_from text REFERENCES public.muse_chapter(chapter_id),
knowledge_mode text NOT NULL CHECK(knowledge_mode IN ('fact','belief','plan','setting')),
known_to jsonb NOT NULL,
reader_visible boolean NOT NULL,
planned_window jsonb,
author_id text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(object_id,revision)
);
ALTER TABLE public.muse_world_object ADD CONSTRAINT world_current_version_fk
FOREIGN KEY(object_id,current_revision) REFERENCES public.muse_world_version(object_id,revision)
DEFERRABLE INITIALLY DEFERRED;
CREATE TABLE public.muse_fact_proposal (
proposal_id uuid PRIMARY KEY,
work_id text NOT NULL REFERENCES public.muse_work(work_id),
object_id text NOT NULL,
current_revision bigint NOT NULL CHECK(current_revision>0)
);
CREATE TABLE public.muse_fact_proposal_version (
proposal_id uuid NOT NULL REFERENCES public.muse_fact_proposal(proposal_id),
revision bigint NOT NULL CHECK(revision>0),
request jsonb NOT NULL,
proposed_content jsonb NOT NULL,
proposal_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(proposal_id,revision)
);
ALTER TABLE public.muse_fact_proposal ADD CONSTRAINT fact_proposal_current_version_fk
FOREIGN KEY(proposal_id,current_revision) REFERENCES public.muse_fact_proposal_version(proposal_id,revision)
DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX world_version_system_idx ON public.muse_world_version(object_id,system_revision DESC);

View File

@ -1,67 +0,0 @@
-- B03:外部与私人资料、原文版本、清理候选与榜单快照。
-- 原文只追加不可改写;清理是对照产物;榜单快照把时点观察与效果推断分开(无 run 归因字段)。
DO $migration$
BEGIN
-- 来源:同源再导入落到版本链;kind 区分作者稿、参考书、网页、笔记与灵感。
CREATE TABLE public.muse_source (
source_id uuid PRIMARY KEY,
kind text NOT NULL CHECK (kind IN ('author_draft','reference','web','note','inspiration')),
title text NOT NULL,
origin text NOT NULL,
authorized_uses jsonb NOT NULL DEFAULT '[]'::jsonb,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (origin, kind),
CHECK (jsonb_typeof(authorized_uses) = 'array')
);
-- 原文版本:只追加;内容哈希用于证据校验与重复辨认。
CREATE TABLE public.muse_source_version (
source_id uuid NOT NULL REFERENCES public.muse_source(source_id),
revision bigint NOT NULL CHECK (revision > 0),
content_hash text NOT NULL,
content text NOT NULL,
import_result jsonb NOT NULL,
imported_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (source_id, revision),
CHECK (jsonb_typeof(import_result) = 'object')
);
CREATE INDEX ON public.muse_source_version (source_id, imported_at);
-- 清理候选:对照产物,原文不受影响;删除项可定位、可回退。
CREATE TABLE public.muse_clean_candidate (
candidate_id uuid PRIMARY KEY,
source_id uuid NOT NULL REFERENCES public.muse_source(source_id),
revision bigint NOT NULL,
removals jsonb NOT NULL,
status text NOT NULL CHECK (status IN ('proposed','applied','rejected')),
cleaned_hash text,
cleaned_text text,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (source_id, revision, candidate_id),
CHECK (jsonb_typeof(removals) = 'array'),
CHECK ((status = 'applied') = (cleaned_hash IS NOT NULL AND cleaned_text IS NOT NULL)),
FOREIGN KEY (source_id, revision) REFERENCES public.muse_source_version(source_id, revision)
);
-- 榜单来源登记:具名榜种与采集方式。
CREATE TABLE public.muse_ranking_source (
ranking_id text PRIMARY KEY,
name text NOT NULL,
url text NOT NULL,
采集方式 text NOT NULL
);
-- 榜单快照:时刻观察;失败保留失败态;不携带任何 run 归因。
CREATE TABLE public.muse_ranking_snapshot (
snapshot_id uuid PRIMARY KEY,
ranking_id text NOT NULL REFERENCES public.muse_ranking_source(ranking_id),
captured_at timestamptz NOT NULL DEFAULT clock_timestamp(),
status text NOT NULL CHECK (status IN ('ok','failed')),
failure_reason text,
items jsonb,
CHECK ((status = 'ok') = (items IS NOT NULL)),
CHECK (items IS NULL OR jsonb_typeof(items) = 'array')
);
CREATE INDEX ON public.muse_ranking_snapshot (ranking_id, captured_at DESC);
END
$migration$;

View File

@ -1,50 +0,0 @@
-- B05正文版本只追加;段落身份与显式换行保留在受限文档树中。
CREATE TABLE public.muse_document (
document_id uuid PRIMARY KEY,
chapter_id text NOT NULL REFERENCES public.muse_chapter(chapter_id),
branch_id text NOT NULL,
current_revision bigint NOT NULL CHECK(current_revision>0),
UNIQUE(chapter_id,branch_id)
);
CREATE TABLE public.muse_document_version (
document_id uuid NOT NULL REFERENCES public.muse_document(document_id),
revision bigint NOT NULL CHECK(revision>0),
document jsonb NOT NULL,
document_hash text NOT NULL,
visible_text_hash text NOT NULL,
author_id text NOT NULL,
restored_from bigint,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(document_id,revision),
FOREIGN KEY(document_id,restored_from) REFERENCES public.muse_document_version(document_id,revision)
);
ALTER TABLE public.muse_document ADD CONSTRAINT document_current_version_fk
FOREIGN KEY(document_id,current_revision) REFERENCES public.muse_document_version(document_id,revision)
DEFERRABLE INITIALLY DEFERRED;
-- 作者改稿先存候选,决定记录由S01拥有;生成候选的证据绑定由后续写作任务扩展。
CREATE TABLE public.muse_writing_candidate (
candidate_id uuid PRIMARY KEY,
chapter_id text NOT NULL REFERENCES public.muse_chapter(chapter_id),
branch_id text NOT NULL,
origin text NOT NULL CHECK(origin='author'),
current_revision bigint NOT NULL CHECK(current_revision>0)
);
CREATE TABLE public.muse_writing_candidate_version (
candidate_id uuid NOT NULL REFERENCES public.muse_writing_candidate(candidate_id),
revision bigint NOT NULL CHECK(revision>0),
base_revision bigint NOT NULL CHECK(base_revision>=0),
base_hash text NOT NULL,
document jsonb NOT NULL,
document_hash text NOT NULL,
visible_text_hash text NOT NULL,
candidate_hash text NOT NULL,
checks jsonb NOT NULL,
author_id text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(candidate_id,revision)
);
ALTER TABLE public.muse_writing_candidate ADD CONSTRAINT writing_candidate_version_fk
FOREIGN KEY(candidate_id,current_revision)
REFERENCES public.muse_writing_candidate_version(candidate_id,revision)
DEFERRABLE INITIALLY DEFERRED;

View File

@ -1,20 +0,0 @@
-- 已校验流程的不可变执行计划;同版本不能覆盖历史任务定义。
DO $migration$
DECLARE
namespace text;
BEGIN
FOREACH namespace IN ARRAY ARRAY['public', 'evaluation'] LOOP
EXECUTE format($ddl$
CREATE TABLE %I.muse_flow_version (
run_purpose public.muse_purpose NOT NULL,
flow_id text NOT NULL,
version text NOT NULL,
definition jsonb NOT NULL,
definition_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (run_purpose, flow_id, version)
)
$ddl$, namespace);
END LOOP;
END
$migration$;

View File

@ -1,28 +0,0 @@
-- B09:任务范围与实际消费的不可变上下文快照。
-- 快照身份由载荷确定性派生,同一输入重复冻结幂等为一行;历史快照只读不改写。
DO $migration$
DECLARE
namespace text;
BEGIN
FOREACH namespace IN ARRAY ARRAY['public', 'evaluation'] LOOP
EXECUTE format($ddl$
CREATE TABLE %1$I.muse_context_snapshot (
snapshot_id uuid PRIMARY KEY,
run_purpose public.muse_purpose NOT NULL,
task_id uuid NOT NULL REFERENCES %1$I.muse_task(task_id),
work_id text NOT NULL,
input_hash text NOT NULL,
frozen_scope jsonb NOT NULL,
assembly jsonb NOT NULL,
source_bindings jsonb NOT NULL,
created_by text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (run_purpose, task_id, input_hash),
CHECK (assembly ? ALL (ARRAY['入选','省略','字节数','文本哈希','文本'])),
CHECK (jsonb_typeof(source_bindings) = 'array')
);
CREATE INDEX ON %1$I.muse_context_snapshot (task_id, created_at);
$ddl$, namespace);
END LOOP;
END
$migration$;

View File

@ -1,59 +0,0 @@
-- B05/B06:模型候选来源证据与审校报告;候选来源在作者改稿之外扩展模型生成。
-- 模型候选不走 S01 作者决定命令,由写作任务在任务运行事务内写入;作者决定仍归 S01。
-- B05/B06 正式内容表只在 public 命名空间(与 V0009 一致)。
DO $migration$
DECLARE
origin_constraint text;
BEGIN
SELECT conname INTO origin_constraint
FROM pg_constraint
WHERE conrelid = 'public.muse_writing_candidate'::regclass
AND contype = 'c'
AND pg_get_constraintdef(oid) ILIKE '%origin%';
IF origin_constraint IS NULL THEN
RAISE EXCEPTION '候选来源约束缺失,不能盲目扩展';
END IF;
EXECUTE format($ddl$
ALTER TABLE public.muse_writing_candidate DROP CONSTRAINT %I;
ALTER TABLE public.muse_writing_candidate
ADD CONSTRAINT writing_candidate_origin_check
CHECK (origin IN ('author', 'model'));
-- 模型候选的证据绑定:来源任务、冻结快照与处理器/模型回执哈希;一行绑定一个候选,不可改写。
CREATE TABLE public.muse_model_candidate (
candidate_id uuid PRIMARY KEY
REFERENCES public.muse_writing_candidate(candidate_id),
task_id uuid NOT NULL REFERENCES public.muse_task(task_id),
snapshot_id uuid NOT NULL REFERENCES public.muse_context_snapshot(snapshot_id),
flow_version text NOT NULL,
processor_versions jsonb NOT NULL,
model_evidence jsonb NOT NULL,
prompt_hashes jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
CHECK (jsonb_typeof(processor_versions) = 'object'),
CHECK (jsonb_typeof(model_evidence) = 'array'),
CHECK (jsonb_typeof(prompt_hashes) = 'object')
);
-- 审校报告:绑定候选版本与所依据快照;问题与覆盖只追加,不修改候选本身。
CREATE TABLE public.muse_review_report (
report_id uuid PRIMARY KEY,
candidate_id uuid NOT NULL
REFERENCES public.muse_writing_candidate(candidate_id),
candidate_revision bigint NOT NULL CHECK (candidate_revision > 0),
snapshot_id uuid NOT NULL
REFERENCES public.muse_context_snapshot(snapshot_id),
basic jsonb NOT NULL,
continuity jsonb NOT NULL,
verdict text NOT NULL CHECK (verdict IN ('passed', 'failed')),
coverage jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (candidate_id, candidate_revision),
CHECK (basic ? ALL (ARRAY['checks', 'verdict'])),
CHECK (continuity ? ALL (ARRAY['findings', 'coverage'])),
CHECK (jsonb_typeof(coverage) = 'object')
);
CREATE INDEX ON public.muse_review_report (candidate_id, created_at);
$ddl$, origin_constraint);
END
$migration$;

View File

@ -1,129 +0,0 @@
-- B04:方法候选与版本、证据反例、固定绑定、不可变消费与检索索引。
-- 方法是跨作品正式内容:版本只追加;绑定记具体版本不追随;消费不可变;索引可重建不动正式内容。
-- 裁决 A:向量不依赖 pgvector,存 double precision[],相似度在应用层计算。
DO $migration$
BEGIN
-- 方法:跨作品范式卡;内容哈希按 owner 去重,同哈希不同 owner 由服务层拒绝。
CREATE TABLE public.muse_method (
method_id uuid PRIMARY KEY,
owner text NOT NULL,
type_id text NOT NULL,
title text NOT NULL,
state text NOT NULL CHECK (state IN ('proposed','confirmed','enabled','disabled','withdrawn')),
content_hash text NOT NULL,
current_version bigint NOT NULL DEFAULT 0 CHECK (current_version >= 0),
created_by text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (owner, content_hash),
CHECK (title <> '')
);
CREATE INDEX ON public.muse_method (type_id, state);
-- 方法版本:append-only;绑定与消费都指向具体版本。
CREATE TABLE public.muse_method_version (
version_id uuid PRIMARY KEY,
method_id uuid NOT NULL REFERENCES public.muse_method(method_id),
version bigint NOT NULL CHECK (version > 0),
content jsonb NOT NULL CHECK (jsonb_typeof(content) = 'object'),
content_hash text NOT NULL,
参数上限 jsonb NOT NULL CHECK (jsonb_typeof(参数上限) = 'object'),
schema_id text NOT NULL,
schema_version int NOT NULL,
effective_schema_hash text NOT NULL,
proposal_id uuid,
confirmed_by text NOT NULL,
confirmed_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (method_id, version),
UNIQUE (method_id, content_hash)
);
-- 方法提案:审阅对象;确认后版本入 append-only 表,提案保留可追溯。
CREATE TABLE public.muse_method_proposal (
proposal_id uuid PRIMARY KEY,
method_id uuid NOT NULL,
revision bigint NOT NULL CHECK (revision >= 1),
request jsonb NOT NULL CHECK (jsonb_typeof(request) = 'object'),
proposal_hash text NOT NULL,
created_by text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (method_id, revision)
);
-- 证据与反例:来源(B03 版本钉住)、样例、反例、例外与静态方法资源引用(不复制正文)。
CREATE TABLE public.muse_method_evidence (
evidence_id uuid PRIMARY KEY,
method_id uuid NOT NULL REFERENCES public.muse_method(method_id),
kind text NOT NULL CHECK (kind IN ('source','sample','counter','exception','static_ref')),
ref jsonb NOT NULL CHECK (jsonb_typeof(ref) = 'object'),
note text NOT NULL DEFAULT '',
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE INDEX ON public.muse_method_evidence (method_id, kind);
-- 方法绑定:作品或规划目标固定到具体版本;active 唯一;重绑留痕可回查。
CREATE TABLE public.muse_method_binding (
binding_id uuid PRIMARY KEY,
work_id text NOT NULL,
target_kind text NOT NULL CHECK (target_kind IN ('work','planning')),
target_ref text NOT NULL,
method_id uuid NOT NULL REFERENCES public.muse_method(method_id),
version_id uuid NOT NULL REFERENCES public.muse_method_version(version_id),
bound_by text NOT NULL,
bound_at timestamptz NOT NULL DEFAULT clock_timestamp(),
released_at timestamptz,
CHECK (target_ref <> '')
);
CREATE UNIQUE INDEX muse_method_binding_active
ON public.muse_method_binding (work_id, target_ref, method_id)
WHERE released_at IS NULL;
-- 方法消费:不可变;kind 区分生成/审阅/规划/启用验证;上下文与结果都可定位。
CREATE TABLE public.muse_method_usage (
usage_id uuid PRIMARY KEY,
method_version_id uuid NOT NULL REFERENCES public.muse_method_version(version_id),
kind text NOT NULL CHECK (kind IN ('generation','review','planning','enablement')),
context_ref text NOT NULL,
fragment_hash text NOT NULL,
result_ref text,
task_id uuid,
created_by text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (method_version_id, kind, context_ref, fragment_hash)
);
CREATE INDEX ON public.muse_method_usage (method_version_id, created_at);
-- 方法检索索引:每版本一向量;embedder 版本随行;重建整表重写,不动方法本体。
CREATE TABLE public.muse_method_index (
method_version_id uuid PRIMARY KEY REFERENCES public.muse_method_version(version_id),
检索文本 text NOT NULL,
检索文本哈希 text NOT NULL,
vector double precision[] NOT NULL CHECK (array_length(vector, 1) > 0),
embedder text NOT NULL,
built_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
-- 索引版本台账:当前有效索引身份;检索时对不上即拒绝为陈旧。
CREATE TABLE public.muse_method_index_version (
ledger_id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
索引身份 text NOT NULL,
embedder text NOT NULL,
method_count int NOT NULL CHECK (method_count >= 0),
built_at timestamptz NOT NULL DEFAULT clock_timestamp(),
active boolean NOT NULL DEFAULT false
);
CREATE UNIQUE INDEX muse_method_index_active ON public.muse_method_index_version (active)
WHERE active;
-- 文档派生索引:承接章后流程登记的摘要绑定,关键词可查。
CREATE TABLE public.muse_document_index (
document_id text NOT NULL,
revision bigint NOT NULL CHECK (revision >= 0),
document_hash text NOT NULL,
结构哈希 text NOT NULL,
summary text NOT NULL DEFAULT '',
registered_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (document_id, revision)
);
END
$migration$;

View File

@ -1,32 +0,0 @@
-- 模型规划只落候选,来源绑定独立保存;正式规划仍由作者决定。
CREATE TABLE public.muse_model_plan_candidate (
candidate_id uuid PRIMARY KEY REFERENCES public.muse_plan_candidate(candidate_id),
task_id uuid NOT NULL REFERENCES public.muse_task(task_id),
snapshot_id uuid NOT NULL REFERENCES public.muse_context_snapshot(snapshot_id),
call_id text NOT NULL,
model_evidence jsonb NOT NULL CHECK (jsonb_typeof(model_evidence)='object'),
prompt_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (task_id, call_id)
);
-- 同一快照可能由不同真实调用消费;不能将后一次调用冒充前一次的幂等回执。
DO $migration$
DECLARE
old_constraint text;
BEGIN
SELECT conname INTO old_constraint FROM pg_constraint
WHERE conrelid='public.muse_method_usage'::regclass AND contype='u'
AND pg_get_constraintdef(oid)='UNIQUE (method_version_id, kind, context_ref, fragment_hash)';
IF old_constraint IS NULL THEN
RAISE EXCEPTION '消费键约束缺失,不能盲目替换';
END IF;
EXECUTE format('ALTER TABLE public.muse_method_usage DROP CONSTRAINT %I', old_constraint);
END
$migration$;
ALTER TABLE public.muse_method_usage ADD CONSTRAINT method_usage_call_key
UNIQUE (method_version_id, kind, context_ref, fragment_hash, task_id, result_ref);
-- 历史无任务/调用回执维持原来的单键约束,不将其升级为真实模型消费。
CREATE UNIQUE INDEX method_usage_legacy_key ON public.muse_method_usage
(method_version_id, kind, context_ref, fragment_hash)
WHERE task_id IS NULL OR result_ref IS NULL;

View File

@ -1,19 +0,0 @@
-- 派发授权与发送状态同事务;技术回执不保存原文,不补造历史调用的授权。
DO $迁移$
DECLARE
目标 text;
BEGIN
FOREACH 目标 IN ARRAY ARRAY['public', 'evaluation']
LOOP
EXECUTE format(
'ALTER TABLE %I.muse_runtime_evidence DROP CONSTRAINT muse_runtime_evidence_kind_check',
目标
);
EXECUTE format(
'ALTER TABLE %I.muse_runtime_evidence ADD CONSTRAINT muse_runtime_evidence_kind_check '
'CHECK (kind IN (''model_input'',''model_response'',''model_dispatch'',''tool_result'',''failure''))',
目标
);
END LOOP;
END
$迁移$;

View File

@ -1,70 +0,0 @@
-- 数据演进:隔离目标身份与迁移来源/回执;不代替任何业务域的实体或候选表。
CREATE TABLE public.muse_legacy_target (
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
target_id uuid NOT NULL UNIQUE,
system_identifier text NOT NULL,
data_directory text NOT NULL,
host text NOT NULL,
port integer NOT NULL CHECK (port BETWEEN 1 AND 65535),
database_name text NOT NULL,
database_oid oid NOT NULL,
role_name text NOT NULL CHECK (role_name='muse_app'),
run_purpose public.muse_purpose NOT NULL CHECK (run_purpose='production'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
-- 应用角色不能把任意已有库自行标为隔离目标。
REVOKE ALL ON public.muse_legacy_target FROM muse_app, muse_eval;
GRANT SELECT ON public.muse_legacy_target TO muse_app;
CREATE TABLE public.muse_legacy_import_record (
author_id text NOT NULL,
run_purpose public.muse_purpose NOT NULL CHECK (run_purpose='production'),
source_key text NOT NULL CHECK (length(source_key)>0),
source_hash text NOT NULL CHECK (source_hash ~ '^[0-9a-f]{64}$'),
mapping_hash text NOT NULL CHECK (mapping_hash ~ '^[0-9a-f]{64}$'),
original jsonb NOT NULL CHECK (jsonb_typeof(original)='object'),
command_id text NOT NULL,
route jsonb NOT NULL CHECK (jsonb_typeof(route)='object'),
frozen_request jsonb,
state text NOT NULL DEFAULT 'reserved'
CHECK (state IN ('reserved','mapped','historical','quarantined')),
reason_code text NOT NULL DEFAULT '',
targets jsonb NOT NULL DEFAULT '[]'::jsonb CHECK (jsonb_typeof(targets)='array'),
receipt jsonb,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (author_id, source_key),
UNIQUE (author_id, command_id),
CHECK (state <> 'mapped' OR
(frozen_request IS NOT NULL AND receipt IS NOT NULL AND jsonb_array_length(targets)>0)),
CHECK (state = 'mapped' OR (receipt IS NULL AND targets='[]'::jsonb))
);
REVOKE DELETE ON public.muse_legacy_import_record FROM muse_app;
CREATE FUNCTION public.muse_guard_legacy_import_record() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
IF (NEW.author_id, NEW.run_purpose, NEW.source_key, NEW.source_hash,
NEW.mapping_hash, NEW.original, NEW.command_id, NEW.route)
IS DISTINCT FROM
(OLD.author_id, OLD.run_purpose, OLD.source_key, OLD.source_hash,
OLD.mapping_hash, OLD.original, OLD.command_id, OLD.route) THEN
RAISE EXCEPTION '迁移来源、映射与命令身份不可改写';
END IF;
IF OLD.frozen_request IS NOT NULL AND NEW.frozen_request IS DISTINCT FROM OLD.frozen_request THEN
RAISE EXCEPTION '迁移目标请求已经冻结';
END IF;
IF OLD.state <> 'reserved' AND
(NEW.state, NEW.reason_code, NEW.targets, NEW.receipt, NEW.frozen_request)
IS DISTINCT FROM
(OLD.state, OLD.reason_code, OLD.targets, OLD.receipt, OLD.frozen_request) THEN
RAISE EXCEPTION '迁移终态及回执不可改写';
END IF;
NEW.updated_at := clock_timestamp();
RETURN NEW;
END;
$$;
CREATE TRIGGER muse_legacy_import_record_guard
BEFORE UPDATE ON public.muse_legacy_import_record
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_legacy_import_record();

View File

@ -1,23 +0,0 @@
-- B03拥有完整分析;S02检查点只保留运行证据与结果引用。
CREATE TABLE public.muse_source_analysis (
author_id text NOT NULL,
analysis_id text NOT NULL,
revision bigint NOT NULL DEFAULT 1 CHECK (revision = 1),
source_id uuid NOT NULL,
source_revision bigint NOT NULL CHECK (source_revision > 0),
source_content_hash text NOT NULL CHECK (source_content_hash ~ '^[0-9a-f]{64}$'),
origin text NOT NULL CHECK (origin IN ('model','checkpoint_history','legacy_import')),
payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'),
content_hash text NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (analysis_id),
FOREIGN KEY (source_id, source_revision) REFERENCES public.muse_source_version(source_id, revision)
);
CREATE INDEX ON public.muse_source_analysis (author_id, source_id, created_at DESC, analysis_id);
CREATE FUNCTION public.muse_guard_source_analysis() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
RAISE EXCEPTION '分析版本只追加,不改写或删除历史';
END;
$$;
CREATE TRIGGER muse_source_analysis_guard BEFORE UPDATE OR DELETE ON public.muse_source_analysis
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_source_analysis();

View File

@ -1,37 +0,0 @@
-- 全书声音候选与确认版本;人物语言指纹仍归B02。
CREATE TABLE public.muse_voice_proposal (
proposal_id uuid PRIMARY KEY,
author_id text NOT NULL,
work_id text NOT NULL REFERENCES public.muse_work(work_id),
revision bigint NOT NULL DEFAULT 1 CHECK (revision = 1),
base_revision bigint NOT NULL CHECK (base_revision >= 0),
request jsonb NOT NULL CHECK (jsonb_typeof(request) = 'object'),
proposal_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TABLE public.muse_voice_version (
version_id uuid PRIMARY KEY,
work_id text NOT NULL REFERENCES public.muse_work(work_id),
revision bigint NOT NULL CHECK (revision > 0),
proposal_id uuid NOT NULL UNIQUE REFERENCES public.muse_voice_proposal(proposal_id),
content_hash text NOT NULL,
effective_schema_hash text NOT NULL,
projection_version text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (work_id, revision),
UNIQUE (work_id, version_id)
);
CREATE TABLE public.muse_voice_current (
work_id text PRIMARY KEY REFERENCES public.muse_work(work_id),
version_id uuid NOT NULL,
FOREIGN KEY (work_id, version_id) REFERENCES public.muse_voice_version(work_id, version_id)
);
CREATE FUNCTION public.muse_guard_voice_history() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
RAISE EXCEPTION '声音提案与历史版本只追加,不改写或删除';
END;
$$;
CREATE TRIGGER muse_voice_proposal_guard BEFORE UPDATE OR DELETE ON public.muse_voice_proposal
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_voice_history();
CREATE TRIGGER muse_voice_version_guard BEFORE UPDATE OR DELETE ON public.muse_voice_version
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_voice_history();

View File

@ -1,34 +0,0 @@
-- 文学片结果和完整报告由B06持有,任务运行只记录引用和执行证据。
CREATE TABLE public.muse_literary_shard (
task_id uuid NOT NULL REFERENCES public.muse_task(task_id),
shard_id text NOT NULL CHECK (shard_id ~ '^[0-9a-f]{64}$'),
attempt_id uuid NOT NULL REFERENCES public.muse_attempt(attempt_id),
call_id text NOT NULL,
author_id text NOT NULL,
snapshot_id uuid NOT NULL REFERENCES public.muse_context_snapshot(snapshot_id),
basis_hash text NOT NULL CHECK (basis_hash ~ '^[0-9a-f]{64}$'),
payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'),
payload_hash text NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
evidence jsonb NOT NULL CHECK (jsonb_typeof(evidence) = 'object'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (task_id, shard_id),
UNIQUE (task_id, call_id)
);
CREATE TABLE public.muse_literary_report (
report_id uuid PRIMARY KEY,
task_id uuid NOT NULL UNIQUE REFERENCES public.muse_task(task_id),
author_id text NOT NULL,
revision bigint NOT NULL DEFAULT 1 CHECK (revision = 1),
payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'),
payload_hash text NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE FUNCTION public.muse_guard_literary_history() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
RAISE EXCEPTION '文学片结果与报告只追加,不改写或删除历史';
END;
$$;
CREATE TRIGGER muse_literary_shard_guard BEFORE UPDATE OR DELETE ON public.muse_literary_shard
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_literary_report_guard BEFORE UPDATE OR DELETE ON public.muse_literary_report
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,31 +0,0 @@
-- 规则种子只生成候选;启用记录由后续B10评测及S01批准产生。
CREATE TABLE public.muse_rule_version (
version_id uuid PRIMARY KEY,
author_id text NOT NULL,
rule_id text NOT NULL,
revision bigint NOT NULL CHECK (revision > 0),
payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'),
payload_hash text NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id, rule_id, revision)
);
CREATE TABLE public.muse_rule_activation (
author_id text NOT NULL,
rule_id text NOT NULL,
version_id uuid NOT NULL REFERENCES public.muse_rule_version(version_id),
evaluation_ref text NOT NULL,
approval_ref text NOT NULL,
PRIMARY KEY(author_id, rule_id)
);
CREATE TABLE public.muse_diagnosis (
diagnosis_id uuid PRIMARY KEY,
author_id text NOT NULL,
work_id text NOT NULL REFERENCES public.muse_work(work_id),
payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'),
payload_hash text NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TRIGGER muse_rule_version_guard BEFORE UPDATE OR DELETE ON public.muse_rule_version
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_diagnosis_guard BEFORE UPDATE OR DELETE ON public.muse_diagnosis
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,16 +0,0 @@
-- 模型修订只追加结果与来源;正文候选仍由B05拥有。
CREATE TABLE public.muse_revision_result (
task_id uuid PRIMARY KEY REFERENCES public.muse_task(task_id),
author_id text NOT NULL,
attempt_id uuid NOT NULL REFERENCES public.muse_attempt(attempt_id),
call_id text NOT NULL,
diagnosis_id uuid NOT NULL REFERENCES public.muse_diagnosis(diagnosis_id),
candidate_id uuid REFERENCES public.muse_writing_candidate(candidate_id),
outcome text NOT NULL CHECK (outcome IN ('candidate','original','rejected')),
payload jsonb NOT NULL CHECK (jsonb_typeof(payload) = 'object'),
payload_hash text NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
CHECK ((outcome='candidate') = (candidate_id IS NOT NULL))
);
CREATE TRIGGER muse_revision_result_guard BEFORE UPDATE OR DELETE ON public.muse_revision_result
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,34 +0,0 @@
-- B05分支只保存起点;正文内容仍归既有document/version表。
CREATE TABLE public.muse_document_branch (
document_id uuid PRIMARY KEY REFERENCES public.muse_document(document_id),
source_document_id uuid NOT NULL,
source_revision bigint NOT NULL,
source_hash text NOT NULL,
author_id text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
CHECK(document_id <> source_document_id),
FOREIGN KEY(source_document_id,source_revision)
REFERENCES public.muse_document_version(document_id,revision)
);
CREATE TABLE public.muse_branch_merge_source (
candidate_id uuid NOT NULL,
candidate_revision bigint NOT NULL,
source_document_id uuid NOT NULL REFERENCES public.muse_document_branch(document_id),
source_revision bigint NOT NULL,
source_hash text NOT NULL,
merge_basis jsonb NOT NULL,
PRIMARY KEY(candidate_id,candidate_revision),
FOREIGN KEY(candidate_id,candidate_revision)
REFERENCES public.muse_writing_candidate_version(candidate_id,revision),
FOREIGN KEY(source_document_id,source_revision)
REFERENCES public.muse_document_version(document_id,revision)
);
CREATE FUNCTION public.muse_branch_source_immutable() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
RAISE EXCEPTION 'branch and merge origins are immutable';
END;
$$;
CREATE TRIGGER document_branch_immutable BEFORE UPDATE OR DELETE ON public.muse_document_branch
FOR EACH ROW EXECUTE FUNCTION public.muse_branch_source_immutable();
CREATE TRIGGER branch_merge_source_immutable BEFORE UPDATE OR DELETE ON public.muse_branch_merge_source
FOR EACH ROW EXECUTE FUNCTION public.muse_branch_source_immutable();

View File

@ -1,6 +0,0 @@
-- 通用作者圈选复用修订交付与恢复;没有诊断时不伪造诊断身份。
ALTER TABLE public.muse_revision_result ALTER COLUMN diagnosis_id DROP NOT NULL;
ALTER TABLE public.muse_revision_result ADD COLUMN source_kind text NOT NULL DEFAULT 'diagnosis'
CHECK(source_kind IN ('diagnosis','selection'));
ALTER TABLE public.muse_revision_result ADD CONSTRAINT revision_source_kind_check
CHECK ((source_kind='diagnosis')=(diagnosis_id IS NOT NULL));

View File

@ -1,20 +0,0 @@
-- B06案例版本只追加,当前指针由S01作者操作推进。
CREATE TABLE public.muse_quality_case (
case_id uuid PRIMARY KEY,
author_id text NOT NULL,
current_revision bigint NOT NULL CHECK(current_revision>0)
);
CREATE TABLE public.muse_quality_case_version (
case_id uuid NOT NULL REFERENCES public.muse_quality_case(case_id),
revision bigint NOT NULL CHECK(revision>0),
state text NOT NULL CHECK(state IN ('shadow','confirmed','withdrawn')),
payload jsonb NOT NULL CHECK(jsonb_typeof(payload)='object'),
payload_hash text NOT NULL CHECK(payload_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(case_id,revision)
);
ALTER TABLE public.muse_quality_case ADD CONSTRAINT quality_case_current_fk
FOREIGN KEY(case_id,current_revision) REFERENCES public.muse_quality_case_version(case_id,revision)
DEFERRABLE INITIALLY DEFERRED;
CREATE TRIGGER quality_case_history_guard BEFORE UPDATE OR DELETE ON public.muse_quality_case_version
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,46 +0,0 @@
-- B10公开输入与答案分区封存;muse_app不能访问evaluation/oracle。
CREATE TABLE evaluation.muse_dataset_version (
version_id uuid PRIMARY KEY,
dataset_id text NOT NULL,
revision bigint NOT NULL CHECK(revision>0),
public_manifest jsonb NOT NULL CHECK(jsonb_typeof(public_manifest)='object'),
public_hash text NOT NULL CHECK(public_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(dataset_id,revision)
);
CREATE TABLE oracle.muse_dataset_answers (
version_id uuid PRIMARY KEY REFERENCES evaluation.muse_dataset_version(version_id),
answers jsonb NOT NULL CHECK(jsonb_typeof(answers)='array'),
answer_hash text NOT NULL CHECK(answer_hash ~ '^[0-9a-f]{64}$')
);
-- 发布资料由维护入口写;评测角色只能读取封存输入,不修改样本与分割。
REVOKE INSERT,UPDATE,DELETE,TRUNCATE ON evaluation.muse_dataset_version FROM muse_eval;
CREATE TABLE evaluation.muse_experiment (
experiment_id uuid PRIMARY KEY,
author_id text NOT NULL,
command_id text NOT NULL,
request_hash text NOT NULL CHECK(request_hash ~ '^[0-9a-f]{64}$'),
dataset_version_id uuid NOT NULL REFERENCES evaluation.muse_dataset_version(version_id),
conditions jsonb NOT NULL CHECK(jsonb_typeof(conditions)='object'),
conditions_hash text NOT NULL CHECK(conditions_hash ~ '^[0-9a-f]{64}$'),
sample_ids jsonb NOT NULL CHECK(jsonb_typeof(sample_ids)='array' AND jsonb_array_length(sample_ids)>0),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id,command_id)
);
CREATE TABLE oracle.muse_blind_assignment (
assignment_id uuid PRIMARY KEY,
experiment_id uuid NOT NULL REFERENCES evaluation.muse_experiment(experiment_id),
sample_id text NOT NULL,
arm_order jsonb NOT NULL CHECK(jsonb_typeof(arm_order)='array'),
UNIQUE(experiment_id,sample_id)
);
-- 编排可追加匿名映射,不增加答案写权限或映射更新权限。
GRANT INSERT ON oracle.muse_blind_assignment TO muse_eval;
CREATE TRIGGER dataset_history_guard BEFORE UPDATE OR DELETE ON evaluation.muse_dataset_version
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER answer_history_guard BEFORE UPDATE OR DELETE ON oracle.muse_dataset_answers
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER experiment_history_guard BEFORE UPDATE OR DELETE ON evaluation.muse_experiment
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER assignment_history_guard BEFORE UPDATE OR DELETE ON oracle.muse_blind_assignment
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,43 +0,0 @@
-- B10执行计划、单元归属和原始交付;任务/尝试/费用继续由S02拥有。
CREATE TABLE evaluation.muse_experiment_execution (
experiment_id uuid PRIMARY KEY REFERENCES evaluation.muse_experiment(experiment_id),
request_hash text NOT NULL,
plan jsonb NOT NULL,
plan_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TABLE evaluation.muse_evaluation_unit (
unit_id uuid PRIMARY KEY,
experiment_id uuid NOT NULL REFERENCES evaluation.muse_experiment_execution(experiment_id),
sample_id text NOT NULL,
kind text NOT NULL CHECK(kind IN ('generation','comparison')),
specification jsonb NOT NULL,
specification_hash text NOT NULL,
task_id uuid UNIQUE REFERENCES evaluation.muse_task(task_id)
);
CREATE TABLE evaluation.muse_evaluation_output (
unit_id uuid PRIMARY KEY REFERENCES evaluation.muse_evaluation_unit(unit_id),
task_id uuid NOT NULL REFERENCES evaluation.muse_task(task_id),
attempt_id uuid NOT NULL,
call_id text NOT NULL UNIQUE,
user_input_hash text NOT NULL,
output jsonb NOT NULL,
output_hash text NOT NULL,
evidence jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TRIGGER evaluation_execution_guard BEFORE UPDATE OR DELETE ON evaluation.muse_experiment_execution
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER evaluation_output_guard BEFORE UPDATE OR DELETE ON evaluation.muse_evaluation_output
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE FUNCTION evaluation.muse_guard_unit_binding() RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF TG_OP='DELETE' THEN RAISE EXCEPTION 'evaluation unit is immutable'; END IF;
IF OLD.task_id IS NOT NULL OR NEW.task_id IS NULL OR
(to_jsonb(OLD)-'task_id') <> (to_jsonb(NEW)-'task_id') THEN
RAISE EXCEPTION 'only first task binding is allowed';
END IF;
RETURN NEW;
END $$;
CREATE TRIGGER evaluation_unit_guard BEFORE UPDATE OR DELETE ON evaluation.muse_evaluation_unit
FOR EACH ROW EXECUTE FUNCTION evaluation.muse_guard_unit_binding();

View File

@ -1,9 +0,0 @@
-- 停止决定独立追加,执行计划与已发生交付保持不变。
CREATE TABLE evaluation.muse_experiment_stop (
experiment_id uuid PRIMARY KEY REFERENCES evaluation.muse_experiment_execution(experiment_id),
author_id text NOT NULL,
command_id text NOT NULL,
stopped_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TRIGGER experiment_stop_guard BEFORE UPDATE OR DELETE ON evaluation.muse_experiment_stop
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,20 +0,0 @@
-- S02逐调用的保存确认;尝试上的最后调用指针不再充当全部回合的历史。
DO $$
DECLARE
scope_name text;
BEGIN
FOREACH scope_name IN ARRAY ARRAY['public', 'evaluation'] LOOP
EXECUTE format($ddl$
CREATE TABLE %1$I.muse_model_delivery (
call_id text PRIMARY KEY,
task_id uuid NOT NULL REFERENCES %1$I.muse_task(task_id),
attempt_id uuid NOT NULL REFERENCES %1$I.muse_attempt(attempt_id),
response_evidence_id uuid NOT NULL REFERENCES %1$I.muse_runtime_evidence(evidence_id),
response_hash text NOT NULL CHECK(response_hash ~ '^[0-9a-f]{64}$'),
confirmed_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TRIGGER model_delivery_guard BEFORE UPDATE OR DELETE ON %1$I.muse_model_delivery
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
$ddl$, scope_name);
END LOOP;
END $$;

View File

@ -1,17 +0,0 @@
-- B10保存已由S02确认的引文拒绝;不覆盖原回合,也不重置纠正次数。
CREATE TABLE evaluation.muse_comparison_rejection (
unit_id uuid NOT NULL REFERENCES evaluation.muse_evaluation_unit(unit_id),
sequence smallint NOT NULL CHECK(sequence BETWEEN 1 AND 3),
task_id uuid NOT NULL REFERENCES evaluation.muse_task(task_id),
attempt_id uuid NOT NULL REFERENCES evaluation.muse_attempt(attempt_id),
call_id text NOT NULL UNIQUE,
user_input_hash text NOT NULL CHECK(user_input_hash ~ '^[0-9a-f]{64}$'),
output jsonb NOT NULL,
failure_code text NOT NULL CHECK(failure_code IN ('PAIRWISE_QUOTE_NOT_FOUND','PAIRWISE_NOT_EXECUTED')),
previous_hash text CHECK(previous_hash ~ '^[0-9a-f]{64}$'),
record_hash text NOT NULL CHECK(record_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(unit_id, sequence)
);
CREATE TRIGGER comparison_rejection_guard BEFORE UPDATE OR DELETE ON evaluation.muse_comparison_rejection
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,10 +0,0 @@
-- 条件第三决议只追加,保存初评实际交付依据;不以缺少任务推断无需第三评。
CREATE TABLE evaluation.muse_evaluation_condition (
unit_id uuid PRIMARY KEY REFERENCES evaluation.muse_evaluation_unit(unit_id),
decision jsonb NOT NULL,
decision_hash text NOT NULL CHECK(decision_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TRIGGER evaluation_condition_guard
BEFORE UPDATE OR DELETE ON evaluation.muse_evaluation_condition
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,18 +0,0 @@
-- 金标准由维护者先于评委任务封存;标定结果不具有生产写入权。
CREATE TABLE oracle.muse_calibration_gold (
experiment_id uuid PRIMARY KEY REFERENCES evaluation.muse_experiment(experiment_id),
payload jsonb NOT NULL CHECK(jsonb_typeof(payload)='object'),
payload_hash text NOT NULL CHECK(payload_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TRIGGER calibration_gold_guard BEFORE UPDATE OR DELETE ON oracle.muse_calibration_gold
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TABLE evaluation.muse_calibration_receipt (
experiment_id uuid PRIMARY KEY REFERENCES evaluation.muse_experiment(experiment_id),
receipt_id uuid NOT NULL UNIQUE,
payload jsonb NOT NULL CHECK(jsonb_typeof(payload)='object'),
payload_hash text NOT NULL CHECK(payload_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TRIGGER calibration_receipt_guard BEFORE UPDATE OR DELETE ON evaluation.muse_calibration_receipt
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,4 +0,0 @@
-- B10沿用既有单元、交付及S02任务;只扩充检测种类,不修改旧记录。
ALTER TABLE evaluation.muse_evaluation_unit DROP CONSTRAINT muse_evaluation_unit_kind_check;
ALTER TABLE evaluation.muse_evaluation_unit ADD CONSTRAINT muse_evaluation_unit_kind_check
CHECK(kind IN ('generation','comparison','detection'));

View File

@ -1,10 +0,0 @@
-- 阈值判断记录只属于隔离评测;不能直接当作生产目标的启用回执。
CREATE TABLE evaluation.muse_effect_assessment (
experiment_id uuid PRIMARY KEY REFERENCES evaluation.muse_experiment(experiment_id),
receipt_id uuid NOT NULL UNIQUE,
payload jsonb NOT NULL,
payload_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TRIGGER effect_assessment_guard BEFORE UPDATE OR DELETE ON evaluation.muse_effect_assessment
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,28 +0,0 @@
-- B10导出的最小启用证明;模型与生产角色都不能自行写入验证结论。
CREATE TABLE public.muse_enablement_receipt (
receipt_id uuid PRIMARY KEY,
author_id text NOT NULL,
experiment_id uuid NOT NULL REFERENCES evaluation.muse_experiment(experiment_id),
basis_experiment_ids uuid[] NOT NULL CHECK (cardinality(basis_experiment_ids)>0),
effect_receipt_id uuid NOT NULL,
payload jsonb NOT NULL CHECK (jsonb_typeof(payload)='object'),
payload_hash text NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
valid_until timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id,experiment_id,payload_hash)
);
REVOKE ALL ON public.muse_enablement_receipt FROM muse_app, muse_eval;
CREATE TRIGGER enablement_receipt_guard BEFORE UPDATE OR DELETE ON public.muse_enablement_receipt
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
-- 只暴露已导出的证明与停止布尔值;不暴露答案、匿名映射或评测候选。
CREATE VIEW public.muse_enablement_status WITH (security_barrier=true) AS
SELECT r.receipt_id,r.author_id,r.experiment_id,r.basis_experiment_ids,
r.effect_receipt_id,r.payload,r.payload_hash,r.valid_until,
EXISTS (
SELECT 1 FROM evaluation.muse_experiment_stop s
WHERE s.experiment_id=ANY(r.basis_experiment_ids)
) AS stopped
FROM public.muse_enablement_receipt r;
REVOKE ALL ON public.muse_enablement_status FROM muse_app, muse_eval;
GRANT SELECT ON public.muse_enablement_status TO muse_app, muse_eval;

View File

@ -1,34 +0,0 @@
-- B07作者偏好及反馈:当前指针和只追加历史分开,评测角色无生产内容权限。
CREATE TABLE public.muse_preference (
preference_id uuid PRIMARY KEY,
author_id text NOT NULL,
current_revision integer NOT NULL CHECK (current_revision > 0)
);
CREATE TABLE public.muse_preference_version (
preference_id uuid NOT NULL REFERENCES public.muse_preference,
revision integer NOT NULL CHECK (revision > 0),
payload jsonb NOT NULL,
content_hash text NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'),
state text NOT NULL CHECK (state IN ('pending','active','withdrawn')),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY (preference_id,revision)
);
CREATE TABLE public.muse_author_feedback (
feedback_id uuid PRIMARY KEY,
author_id text NOT NULL,
command_id text NOT NULL,
payload jsonb NOT NULL,
content_hash text NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE (author_id,command_id)
);
CREATE TRIGGER muse_preference_version_immutable BEFORE UPDATE OR DELETE
ON public.muse_preference_version FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_author_feedback_immutable BEFORE UPDATE OR DELETE
ON public.muse_author_feedback FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
REVOKE ALL ON public.muse_preference, public.muse_preference_version,
public.muse_author_feedback FROM muse_eval;
GRANT SELECT,INSERT,UPDATE ON public.muse_preference TO muse_app;
GRANT SELECT,INSERT ON public.muse_preference_version,public.muse_author_feedback TO muse_app;

View File

@ -1,48 +0,0 @@
-- B07保存具体提案、意向和外部owner原回执;不复制目标生效状态。
CREATE TABLE public.muse_improvement (
improvement_id uuid PRIMARY KEY,
author_id text NOT NULL,
current_revision integer NOT NULL CHECK(current_revision > 0)
);
CREATE TABLE public.muse_improvement_version (
improvement_id uuid NOT NULL REFERENCES public.muse_improvement,
revision integer NOT NULL CHECK(revision > 0),
payload jsonb NOT NULL,
content_hash text NOT NULL CHECK(content_hash ~ '^[0-9a-f]{64}$'),
feedback_ids uuid[] NOT NULL CHECK(cardinality(feedback_ids) > 0),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(improvement_id,revision)
);
CREATE TABLE public.muse_improvement_action (
action_id uuid PRIMARY KEY,
author_id text NOT NULL,
command_id text NOT NULL,
improvement_id uuid NOT NULL,
revision integer NOT NULL,
kind text NOT NULL CHECK(kind IN ('validation','enable','disable')),
payload jsonb NOT NULL,
content_hash text NOT NULL CHECK(content_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id,command_id),
FOREIGN KEY(improvement_id,revision) REFERENCES public.muse_improvement_version
);
CREATE TABLE public.muse_improvement_result (
action_id uuid PRIMARY KEY REFERENCES public.muse_improvement_action,
receipt jsonb NOT NULL,
receipt_hash text NOT NULL CHECK(receipt_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TRIGGER muse_improvement_version_immutable BEFORE UPDATE OR DELETE
ON public.muse_improvement_version FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_improvement_action_immutable BEFORE UPDATE OR DELETE
ON public.muse_improvement_action FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_improvement_result_immutable BEFORE UPDATE OR DELETE
ON public.muse_improvement_result FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
REVOKE ALL ON public.muse_improvement,public.muse_improvement_version,
public.muse_improvement_action,public.muse_improvement_result FROM muse_eval;
GRANT SELECT,INSERT,UPDATE ON public.muse_improvement TO muse_app;
GRANT SELECT,INSERT ON public.muse_improvement_version,public.muse_improvement_action,
public.muse_improvement_result TO muse_app;

View File

@ -1,21 +0,0 @@
-- B07作者自报观察与实际反馈绑定;未知指标保留NULL,不自动建立因果或启用目标。
CREATE TABLE public.muse_author_observation (
observation_id uuid PRIMARY KEY,
author_id text NOT NULL,
command_id text NOT NULL,
feedback_ids uuid[] NOT NULL CHECK(cardinality(feedback_ids) > 0),
improvement_id uuid,
improvement_revision integer,
payload jsonb NOT NULL,
content_hash text NOT NULL CHECK(content_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id,command_id),
CHECK ((improvement_id IS NULL) = (improvement_revision IS NULL)),
FOREIGN KEY(improvement_id,improvement_revision)
REFERENCES public.muse_improvement_version(improvement_id,revision)
);
CREATE TRIGGER muse_author_observation_immutable BEFORE UPDATE OR DELETE
ON public.muse_author_observation FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
REVOKE ALL ON public.muse_author_observation FROM muse_eval;
GRANT SELECT,INSERT ON public.muse_author_observation TO muse_app;

View File

@ -1,32 +0,0 @@
-- B08固定交付清单与导出字节;正文仍以B05版本为唯一权威。
CREATE TABLE public.muse_delivery (
delivery_id uuid PRIMARY KEY,
author_id text NOT NULL,
work_id text NOT NULL,
revision integer NOT NULL CHECK(revision > 0),
manifest jsonb NOT NULL,
manifest_hash text NOT NULL CHECK(manifest_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(work_id,revision),
CHECK(jsonb_array_length(manifest->'chapters') > 0)
);
CREATE TABLE public.muse_delivery_artifact (
artifact_id uuid PRIMARY KEY,
delivery_id uuid NOT NULL REFERENCES public.muse_delivery,
format text NOT NULL CHECK(format IN ('txt','md','docx','epub')),
formatter_version text NOT NULL,
data bytea NOT NULL,
content_hash text NOT NULL CHECK(content_hash ~ '^[0-9a-f]{64}$'),
verification jsonb NOT NULL,
task_id uuid,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(delivery_id,format,formatter_version),
CHECK(octet_length(data) > 0)
);
CREATE TRIGGER muse_delivery_immutable BEFORE UPDATE OR DELETE
ON public.muse_delivery FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_delivery_artifact_immutable BEFORE UPDATE OR DELETE
ON public.muse_delivery_artifact FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
REVOKE ALL ON public.muse_delivery,public.muse_delivery_artifact FROM muse_eval;
GRANT SELECT,INSERT ON public.muse_delivery,public.muse_delivery_artifact TO muse_app;

View File

@ -1,42 +0,0 @@
-- B08计划与真实发布分开;读者反馈没有run归因字段。
CREATE TABLE public.muse_serial_plan (
plan_id uuid PRIMARY KEY,
author_id text NOT NULL,
work_id text NOT NULL,
revision integer NOT NULL CHECK(revision > 0)
);
CREATE TABLE public.muse_serial_plan_version (
plan_id uuid NOT NULL REFERENCES public.muse_serial_plan,
revision integer NOT NULL CHECK(revision > 0),
payload jsonb NOT NULL,
content_hash text NOT NULL,
PRIMARY KEY(plan_id, revision)
);
CREATE TABLE public.muse_release (
release_id uuid PRIMARY KEY,
author_id text NOT NULL,
work_id text NOT NULL,
delivery_id uuid NOT NULL REFERENCES public.muse_delivery,
payload jsonb NOT NULL,
content_hash text NOT NULL
);
CREATE TABLE public.muse_reader_feedback (
feedback_id uuid PRIMARY KEY,
author_id text NOT NULL,
work_id text NOT NULL,
release_id uuid REFERENCES public.muse_release,
payload jsonb NOT NULL,
content_hash text NOT NULL
);
CREATE TRIGGER muse_serial_history_immutable BEFORE UPDATE OR DELETE
ON public.muse_serial_plan_version FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_release_immutable BEFORE UPDATE OR DELETE
ON public.muse_release FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_reader_feedback_immutable BEFORE UPDATE OR DELETE
ON public.muse_reader_feedback FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
REVOKE ALL ON public.muse_serial_plan,public.muse_serial_plan_version,
public.muse_release,public.muse_reader_feedback FROM muse_eval;
GRANT SELECT,INSERT ON public.muse_serial_plan,public.muse_serial_plan_version,
public.muse_release,public.muse_reader_feedback TO muse_app;
GRANT UPDATE(revision) ON public.muse_serial_plan TO muse_app;

View File

@ -1,2 +0,0 @@
-- 运行角色只读数据版本,不获得迁移账本写权限;用于拒绝不兼容程序继续写入。
GRANT SELECT ON public.muse_migration TO muse_app, muse_eval;

View File

@ -1,15 +0,0 @@
-- 新版交付引用必须指向真实作品,同一发布和反馈不能跨作品错接;不改写历史行。
ALTER TABLE public.muse_delivery ADD CONSTRAINT muse_delivery_work_fk
FOREIGN KEY(work_id) REFERENCES public.muse_work(work_id);
ALTER TABLE public.muse_serial_plan ADD CONSTRAINT muse_serial_work_fk
FOREIGN KEY(work_id) REFERENCES public.muse_work(work_id);
ALTER TABLE public.muse_delivery ADD CONSTRAINT muse_delivery_work_identity
UNIQUE(delivery_id,work_id);
ALTER TABLE public.muse_release ADD CONSTRAINT muse_release_delivery_work_fk
FOREIGN KEY(delivery_id,work_id) REFERENCES public.muse_delivery(delivery_id,work_id);
ALTER TABLE public.muse_release ADD CONSTRAINT muse_release_work_identity
UNIQUE(release_id,work_id);
ALTER TABLE public.muse_reader_feedback ADD CONSTRAINT muse_feedback_work_fk
FOREIGN KEY(work_id) REFERENCES public.muse_work(work_id);
ALTER TABLE public.muse_reader_feedback ADD CONSTRAINT muse_feedback_release_work_fk
FOREIGN KEY(release_id,work_id) REFERENCES public.muse_release(release_id,work_id);

View File

@ -1,10 +0,0 @@
-- 仅聚合本用途的任务,不包含冻结输入、原文或模型凭据;按作者过滤由S02公开入口执行。
CREATE VIEW public.muse_task_inventory WITH (security_barrier=true) AS
SELECT author_id,run_purpose,state,count(*) AS task_count
FROM public.muse_task GROUP BY author_id,run_purpose,state;
CREATE VIEW evaluation.muse_task_inventory WITH (security_barrier=true) AS
SELECT author_id,run_purpose,state,count(*) AS task_count
FROM evaluation.muse_task GROUP BY author_id,run_purpose,state;
REVOKE ALL ON public.muse_task_inventory,evaluation.muse_task_inventory FROM PUBLIC,muse_app,muse_eval;
GRANT SELECT ON public.muse_task_inventory TO muse_app;
GRANT SELECT ON evaluation.muse_task_inventory TO muse_eval;

View File

@ -1,81 +0,0 @@
-- B06 只登记作者控制的有限返修会话、轮任务引用和选择;实际模型任务与候选仍归 S02/B05。
CREATE TABLE public.muse_revision_session (
session_id uuid PRIMARY KEY,
author_id text NOT NULL,
create_command_id text NOT NULL,
request_hash text NOT NULL CHECK(request_hash ~ '^[0-9a-f]{64}$'),
work_id text NOT NULL REFERENCES public.muse_work(work_id),
chapter_id text NOT NULL REFERENCES public.muse_chapter(chapter_id),
branch_id text NOT NULL,
original_revision bigint NOT NULL CHECK(original_revision > 0),
original_document_hash text NOT NULL CHECK(original_document_hash ~ '^[0-9a-f]{64}$'),
source_kind text NOT NULL CHECK(source_kind IN ('diagnosis','selection')),
basis_hash text NOT NULL CHECK(basis_hash ~ '^[0-9a-f]{64}$'),
"authorization" jsonb NOT NULL CHECK(jsonb_typeof("authorization") = 'object'),
authorization_hash text NOT NULL CHECK(authorization_hash ~ '^[0-9a-f]{64}$'),
config_id text NOT NULL,
max_rounds integer NOT NULL CHECK(max_rounds BETWEEN 1 AND 5),
current_round integer NOT NULL DEFAULT 1 CHECK(current_round BETWEEN 1 AND 5),
state text NOT NULL CHECK(state IN ('preparing','running','awaiting_author','closed')),
stop_reason text,
stop_detail text,
author_choice text CHECK(author_choice IN ('original','candidate')),
selected_candidate_id uuid REFERENCES public.muse_writing_candidate(candidate_id),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id,create_command_id),
UNIQUE(session_id,author_id),
CHECK(current_round <= max_rounds),
CHECK((state = 'closed') = (stop_reason IS NOT NULL)),
CHECK(
(author_choice IS NULL AND selected_candidate_id IS NULL)
OR (author_choice = 'original' AND selected_candidate_id IS NULL)
OR (author_choice = 'candidate' AND selected_candidate_id IS NOT NULL)
)
);
CREATE TABLE public.muse_revision_round (
session_id uuid NOT NULL REFERENCES public.muse_revision_session(session_id),
round_number integer NOT NULL CHECK(round_number BETWEEN 1 AND 5),
task_command_id text NOT NULL UNIQUE,
task_request_hash text NOT NULL CHECK(task_request_hash ~ '^[0-9a-f]{64}$'),
task_id uuid UNIQUE REFERENCES public.muse_task(task_id),
state text NOT NULL CHECK(state IN ('preparing','running','candidate','original','rejected')),
outcome text CHECK(outcome IN ('candidate','original','rejected')),
candidate_id uuid REFERENCES public.muse_writing_candidate(candidate_id),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(session_id,round_number),
CHECK((task_id IS NULL) = (state = 'preparing')),
CHECK((outcome IS NULL) = (state IN ('preparing','running'))),
CHECK((outcome = 'candidate') = (candidate_id IS NOT NULL))
);
CREATE TABLE public.muse_revision_session_action (
action_id uuid PRIMARY KEY,
session_id uuid NOT NULL REFERENCES public.muse_revision_session(session_id),
author_id text NOT NULL,
command_id text NOT NULL,
expected_round integer NOT NULL CHECK(expected_round BETWEEN 1 AND 5),
candidate_id uuid NOT NULL REFERENCES public.muse_writing_candidate(candidate_id),
choice text NOT NULL CHECK(choice IN ('original','candidate','retry')),
request_hash text NOT NULL CHECK(request_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id,command_id),
FOREIGN KEY(session_id,author_id)
REFERENCES public.muse_revision_session(session_id,author_id)
);
CREATE TRIGGER muse_revision_session_action_immutable BEFORE UPDATE OR DELETE
ON public.muse_revision_session_action FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
REVOKE ALL ON public.muse_revision_session,public.muse_revision_round,
public.muse_revision_session_action FROM PUBLIC,muse_app,muse_eval;
GRANT SELECT,INSERT ON public.muse_revision_session,public.muse_revision_round TO muse_app;
GRANT UPDATE(current_round,state,stop_reason,stop_detail,author_choice,
selected_candidate_id,updated_at)
ON public.muse_revision_session TO muse_app;
GRANT UPDATE(task_id,state,outcome,candidate_id,updated_at)
ON public.muse_revision_round TO muse_app;
GRANT SELECT,INSERT ON public.muse_revision_session_action TO muse_app;

View File

@ -1,35 +0,0 @@
-- B10只导出无正文的返修比较证明;B06只读,不据此自动采纳候选或修改正文。
CREATE TABLE public.muse_revision_comparison_receipt (
receipt_id uuid PRIMARY KEY,
author_id text NOT NULL,
experiment_id uuid NOT NULL REFERENCES evaluation.muse_experiment(experiment_id),
session_id uuid NOT NULL,
round_number integer NOT NULL CHECK(round_number BETWEEN 1 AND 5),
candidate_id uuid NOT NULL REFERENCES public.muse_writing_candidate(candidate_id),
payload jsonb NOT NULL CHECK(jsonb_typeof(payload)='object'),
payload_hash text NOT NULL CHECK(payload_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
FOREIGN KEY(session_id,author_id)
REFERENCES public.muse_revision_session(session_id,author_id),
FOREIGN KEY(session_id,round_number)
REFERENCES public.muse_revision_round(session_id,round_number),
UNIQUE(author_id,experiment_id,payload_hash)
);
REVOKE ALL ON public.muse_revision_comparison_receipt FROM PUBLIC,muse_app,muse_eval;
CREATE TRIGGER muse_revision_comparison_receipt_immutable BEFORE UPDATE OR DELETE
ON public.muse_revision_comparison_receipt FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
-- 生产侧只能看到已导出的最小证明及实验是否停止,看不到oracle中的固定文本对或匿名映射。
CREATE VIEW public.muse_revision_comparison_status WITH (security_barrier=true) AS
SELECT r.receipt_id,r.author_id,r.experiment_id,r.session_id,r.round_number,
r.candidate_id,r.payload,r.payload_hash,
EXISTS (
SELECT 1 FROM evaluation.muse_experiment_stop s
WHERE s.experiment_id=r.experiment_id
) AS stopped
FROM public.muse_revision_comparison_receipt r;
REVOKE ALL ON public.muse_revision_comparison_status FROM PUBLIC,muse_app,muse_eval;
GRANT SELECT ON public.muse_revision_comparison_status TO muse_app,muse_eval;

View File

@ -1,110 +0,0 @@
-- B10确定性规则诊断的逐例证据与最小公开凭据;B06拥有启停与消费历史。
CREATE TABLE evaluation.muse_rule_diagnostic_run (
run_id uuid PRIMARY KEY,
author_id text NOT NULL,
dataset_version_id uuid NOT NULL REFERENCES evaluation.muse_dataset_version(version_id),
target jsonb NOT NULL CHECK (jsonb_typeof(target)='object'),
policy jsonb NOT NULL CHECK (jsonb_typeof(policy)='object'),
policy_hash text NOT NULL CHECK (policy_hash ~ '^[0-9a-f]{64}$'),
plan_hash text NOT NULL CHECK (plan_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id,dataset_version_id,policy_hash)
);
CREATE TABLE evaluation.muse_rule_diagnostic_result (
run_id uuid NOT NULL REFERENCES evaluation.muse_rule_diagnostic_run(run_id),
sample_id text NOT NULL,
payload jsonb NOT NULL CHECK (jsonb_typeof(payload)='object'),
payload_hash text NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(run_id,sample_id)
);
CREATE TABLE evaluation.muse_rule_diagnostic_assessment (
run_id uuid PRIMARY KEY REFERENCES evaluation.muse_rule_diagnostic_run(run_id),
payload jsonb NOT NULL CHECK (jsonb_typeof(payload)='object'),
payload_hash text NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TABLE evaluation.muse_rule_diagnostic_stop (
run_id uuid PRIMARY KEY REFERENCES evaluation.muse_rule_diagnostic_run(run_id),
author_id text NOT NULL,
command_id text NOT NULL,
stopped_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id,command_id)
);
CREATE TRIGGER rule_diagnostic_run_guard BEFORE UPDATE OR DELETE
ON evaluation.muse_rule_diagnostic_run FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER rule_diagnostic_result_guard BEFORE UPDATE OR DELETE
ON evaluation.muse_rule_diagnostic_result FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER rule_diagnostic_assessment_guard BEFORE UPDATE OR DELETE
ON evaluation.muse_rule_diagnostic_assessment FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER rule_diagnostic_stop_guard BEFORE UPDATE OR DELETE
ON evaluation.muse_rule_diagnostic_stop FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
-- 只有maintenance可导出;production和evaluation只读不含oracle的最小投影。
CREATE TABLE public.muse_rule_diagnostic_receipt (
receipt_id uuid PRIMARY KEY,
author_id text NOT NULL,
run_id uuid NOT NULL REFERENCES evaluation.muse_rule_diagnostic_run(run_id),
rule_version_id uuid NOT NULL REFERENCES public.muse_rule_version(version_id),
payload jsonb NOT NULL CHECK (jsonb_typeof(payload)='object'),
payload_hash text NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
valid_until timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id,run_id,payload_hash)
);
REVOKE ALL ON public.muse_rule_diagnostic_receipt FROM muse_app,muse_eval;
CREATE TRIGGER rule_diagnostic_receipt_guard BEFORE UPDATE OR DELETE
ON public.muse_rule_diagnostic_receipt FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE VIEW public.muse_rule_diagnostic_status WITH (security_barrier=true) AS
SELECT r.receipt_id,r.author_id,r.run_id,r.rule_version_id,r.payload,r.payload_hash,
r.valid_until,
EXISTS (
SELECT 1 FROM evaluation.muse_rule_diagnostic_stop s WHERE s.run_id=r.run_id
) AS stopped
FROM public.muse_rule_diagnostic_receipt r;
REVOKE ALL ON public.muse_rule_diagnostic_status FROM muse_app,muse_eval;
GRANT SELECT ON public.muse_rule_diagnostic_status TO muse_app,muse_eval;
-- 当前投影可变,但每次变更都先追加不可变事件。
CREATE TABLE public.muse_rule_activation_event (
event_id uuid PRIMARY KEY,
author_id text NOT NULL,
rule_id text NOT NULL,
activation_revision bigint NOT NULL CHECK (activation_revision > 0),
version_id uuid NOT NULL REFERENCES public.muse_rule_version(version_id),
action text NOT NULL CHECK (action IN ('enable','disable')),
credential_id uuid REFERENCES public.muse_rule_diagnostic_receipt(receipt_id),
candidate_id text NOT NULL,
candidate_hash text NOT NULL CHECK (candidate_hash ~ '^[0-9a-f]{64}$'),
review_id uuid NOT NULL REFERENCES public.muse_author_review(review_id),
command_id text NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id,rule_id,activation_revision),
UNIQUE(author_id,command_id)
);
CREATE TABLE public.muse_rule_activation_current (
author_id text NOT NULL,
rule_id text NOT NULL,
activation_revision bigint NOT NULL CHECK (activation_revision > 0),
version_id uuid NOT NULL REFERENCES public.muse_rule_version(version_id),
state text NOT NULL CHECK (state IN ('active','disabled')),
credential_id uuid REFERENCES public.muse_rule_diagnostic_receipt(receipt_id),
latest_event_id uuid NOT NULL REFERENCES public.muse_rule_activation_event(event_id),
updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(author_id,rule_id)
);
CREATE TRIGGER rule_activation_event_guard BEFORE UPDATE OR DELETE
ON public.muse_rule_activation_event FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,39 +0,0 @@
-- B07只追加带真实来源的运行观察;与V0046作者自报指标分开。
CREATE TABLE public.muse_run_observation (
observation_id uuid PRIMARY KEY,
author_id text NOT NULL CHECK (btrim(author_id) <> ''),
source_ids uuid[] NOT NULL CONSTRAINT muse_run_observation_source_ids_nonempty
CHECK (cardinality(source_ids) > 0 AND array_position(source_ids,NULL) IS NULL),
source_set_hash text NOT NULL CHECK (source_set_hash ~ '^[0-9a-f]{64}$'),
phenomenon text NOT NULL CHECK (length(btrim(phenomenon)) BETWEEN 1 AND 200),
issue_key text NOT NULL DEFAULT '',
state text NOT NULL DEFAULT 'observation' CHECK (state = 'observation'),
request_hash text NOT NULL CHECK (request_hash ~ '^[0-9a-f]{64}$'),
payload jsonb NOT NULL CONSTRAINT muse_run_observation_sources_present CHECK (
CASE WHEN jsonb_typeof(payload->'sources') = 'array' THEN
jsonb_array_length(payload->'sources') > 0 AND NOT jsonb_path_exists(payload,
'$.sources[*] ? (!exists(@.runtime.task_id) || !exists(@.runtime.attempt_id)
|| !exists(@.locator.content_hash) || @.runtime.task_id.type() != "string"
|| @.runtime.attempt_id.type() != "string" || @.locator.content_hash.type() != "string"
|| @.runtime.task_id == "" || @.runtime.attempt_id == "" || @.locator.content_hash == "")')
ELSE false END),
content_hash text NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
UNIQUE(author_id,source_set_hash,phenomenon)
);
CREATE INDEX ON public.muse_run_observation(author_id,issue_key,created_at);
CREATE TABLE public.muse_run_observation_command (
author_id text NOT NULL,
command_id text NOT NULL CHECK (btrim(command_id) <> ''),
observation_id uuid NOT NULL REFERENCES public.muse_run_observation(observation_id),
request_hash text NOT NULL CHECK (request_hash ~ '^[0-9a-f]{64}$'),
PRIMARY KEY(author_id,command_id)
);
CREATE TRIGGER muse_run_observation_immutable BEFORE UPDATE OR DELETE
ON public.muse_run_observation FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_run_observation_command_immutable BEFORE UPDATE OR DELETE
ON public.muse_run_observation_command FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
REVOKE ALL ON public.muse_run_observation,public.muse_run_observation_command FROM muse_eval;
GRANT SELECT,INSERT ON public.muse_run_observation,public.muse_run_observation_command TO muse_app;

View File

@ -1,55 +0,0 @@
-- 结算不产生执行事件;账目变化由独立版本及只追加命令回执表达。
ALTER TABLE public.muse_task ADD COLUMN accounting_revision bigint NOT NULL DEFAULT 0;
ALTER TABLE evaluation.muse_task ADD COLUMN accounting_revision bigint NOT NULL DEFAULT 0;
CREATE TABLE public.muse_call_settlement (
task_id uuid NOT NULL REFERENCES public.muse_task(task_id),
command_id text NOT NULL CHECK (btrim(command_id) <> ''),
receipt_id uuid NOT NULL UNIQUE,
request_hash text NOT NULL CHECK (request_hash ~ '^[0-9a-f]{64}$'),
receipt jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(task_id,command_id)
);
CREATE TABLE evaluation.muse_call_settlement (LIKE public.muse_call_settlement INCLUDING ALL);
ALTER TABLE evaluation.muse_call_settlement ADD FOREIGN KEY(task_id) REFERENCES evaluation.muse_task(task_id);
CREATE TRIGGER muse_call_settlement_immutable BEFORE UPDATE OR DELETE ON public.muse_call_settlement
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_call_settlement_immutable BEFORE UPDATE OR DELETE ON evaluation.muse_call_settlement
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
GRANT SELECT,INSERT ON public.muse_call_settlement TO muse_app;
GRANT SELECT,INSERT ON evaluation.muse_call_settlement TO muse_eval;
REVOKE ALL ON public.muse_call_settlement FROM muse_eval;
-- 验证专用绑定不能被业务配置读取入口消费,不需要草案先启用。
CREATE TABLE public.muse_config_probe_binding (
task_id uuid PRIMARY KEY REFERENCES public.muse_task(task_id),
config_id text NOT NULL,
version text NOT NULL,
content_hash text NOT NULL CHECK (content_hash ~ '^[0-9a-f]{64}$'),
content jsonb NOT NULL,
created_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
CREATE TABLE evaluation.muse_config_probe_binding (LIKE public.muse_config_probe_binding INCLUDING ALL);
ALTER TABLE evaluation.muse_config_probe_binding ADD FOREIGN KEY(task_id) REFERENCES evaluation.muse_task(task_id);
CREATE TRIGGER muse_config_probe_immutable BEFORE UPDATE OR DELETE ON public.muse_config_probe_binding
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_config_probe_immutable BEFORE UPDATE OR DELETE ON evaluation.muse_config_probe_binding
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
GRANT SELECT,INSERT ON public.muse_config_probe_binding TO muse_app;
GRANT SELECT,INSERT ON evaluation.muse_config_probe_binding TO muse_eval;
REVOKE ALL ON public.muse_config_probe_binding FROM muse_eval;
CREATE TABLE public.muse_call_reconciliation (
task_id uuid NOT NULL REFERENCES public.muse_task(task_id),
command_id text NOT NULL CHECK (btrim(command_id) <> ''),
request_hash text NOT NULL CHECK (request_hash ~ '^[0-9a-f]{64}$'),
receipt jsonb NOT NULL,
PRIMARY KEY(task_id,command_id)
);
CREATE TABLE evaluation.muse_call_reconciliation (LIKE public.muse_call_reconciliation INCLUDING ALL);
ALTER TABLE evaluation.muse_call_reconciliation ADD FOREIGN KEY(task_id) REFERENCES evaluation.muse_task(task_id);
CREATE TRIGGER muse_call_reconciliation_immutable BEFORE UPDATE OR DELETE ON public.muse_call_reconciliation
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
CREATE TRIGGER muse_call_reconciliation_immutable BEFORE UPDATE OR DELETE ON evaluation.muse_call_reconciliation
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();
GRANT SELECT,INSERT ON public.muse_call_reconciliation TO muse_app;
GRANT SELECT,INSERT ON evaluation.muse_call_reconciliation TO muse_eval;
REVOKE ALL ON public.muse_call_reconciliation FROM muse_eval;

View File

@ -1,3 +0,0 @@
-- 当前消费授权与不可变内容版本独立;旧授权集合保持原值。
ALTER TABLE muse_source ADD COLUMN authorization_revision bigint NOT NULL DEFAULT 1
CHECK (authorization_revision >= 1);

View File

@ -1,17 +0,0 @@
-- B08显式完整审计的追加证明;既有成品字节及原始验证记录保持不可变。
CREATE TABLE public.muse_delivery_artifact_validation (
artifact_id uuid NOT NULL REFERENCES public.muse_delivery_artifact,
validation_version text NOT NULL CHECK(validation_version = 'delivery-validation-v2'),
author_id text NOT NULL,
verification jsonb NOT NULL,
audited_at timestamptz NOT NULL DEFAULT clock_timestamp(),
PRIMARY KEY(artifact_id,validation_version),
CHECK((verification->>'validation_version' = validation_version) IS TRUE),
CHECK((verification->'verified' = 'true'::jsonb) IS TRUE),
CHECK((jsonb_typeof(verification->'binding') = 'object') IS TRUE)
);
CREATE TRIGGER muse_delivery_artifact_validation_immutable BEFORE UPDATE OR DELETE
ON public.muse_delivery_artifact_validation FOR EACH ROW
EXECUTE FUNCTION public.muse_guard_literary_history();
REVOKE ALL ON public.muse_delivery_artifact_validation FROM PUBLIC,muse_app,muse_eval;
GRANT SELECT,INSERT ON public.muse_delivery_artifact_validation TO muse_app;

View File

@ -1,31 +0,0 @@
-- B09:索引现有条目和头的事务变更守卫;不产生第二套索引或业务权威。
CREATE TABLE public.muse_method_index_guard (
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
generation bigint NOT NULL DEFAULT 0 CHECK (generation >= 0)
);
INSERT INTO public.muse_method_index_guard (singleton, generation) VALUES (true, 0);
CREATE FUNCTION public.muse_method_index_changed() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $guard$
BEGIN
UPDATE public.muse_method_index_guard
SET generation = generation + 1
WHERE singleton;
IF NOT FOUND THEN
RAISE EXCEPTION 'method index mutation guard is missing';
END IF;
RETURN NULL;
END
$guard$;
-- statement 级别也覆盖批量改写和 TRUNCATE;事务失败时代次一并回滚。
CREATE TRIGGER muse_method_index_changed
AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON public.muse_method_index
FOR EACH STATEMENT EXECUTE FUNCTION public.muse_method_index_changed();
CREATE TRIGGER muse_method_index_head_changed
AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON public.muse_method_index_version
FOR EACH STATEMENT EXECUTE FUNCTION public.muse_method_index_changed();
REVOKE ALL ON public.muse_method_index_guard FROM PUBLIC, muse_app, muse_eval;
GRANT SELECT ON public.muse_method_index_guard TO muse_app, muse_eval;
REVOKE ALL ON FUNCTION public.muse_method_index_changed() FROM PUBLIC;

View File

@ -1,14 +0,0 @@
-- B10逐例复用只追加原来源,不复制任务、响应或费用账。
CREATE TABLE evaluation.muse_evaluation_reuse (
unit_id uuid PRIMARY KEY REFERENCES evaluation.muse_evaluation_unit(unit_id),
source_unit_id uuid NOT NULL REFERENCES evaluation.muse_evaluation_unit(unit_id),
source_experiment_id uuid NOT NULL REFERENCES evaluation.muse_experiment(experiment_id),
payload jsonb NOT NULL CHECK (jsonb_typeof(payload)='object'),
payload_hash text NOT NULL CHECK (payload_hash ~ '^[0-9a-f]{64}$'),
created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
CHECK (unit_id <> source_unit_id)
);
CREATE INDEX evaluation_reuse_source ON evaluation.muse_evaluation_reuse(source_experiment_id);
CREATE TRIGGER evaluation_reuse_guard
BEFORE UPDATE OR DELETE ON evaluation.muse_evaluation_reuse
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_literary_history();

View File

@ -1,87 +0,0 @@
-- B03:场景切片派生缓存与向量代次索引——参考书原文的确定性切窗。
-- 原文以 muse_source_version 为权威;切片与索引是可全量重建的派生数据,禁原地改写。
DO $migration$
BEGIN
-- 切片台账:来源版本的定位窗;窗计划哈希锁定切窗方案,同版本只认一个窗计划。
CREATE TABLE public.muse_source_slice (
slice_id uuid PRIMARY KEY,
source_id uuid NOT NULL REFERENCES public.muse_source(source_id),
revision bigint NOT NULL CHECK (revision > 0),
起点 int NOT NULL CHECK (起点 >= 0),
终点 int NOT NULL CHECK (终点 > 起点),
窗计划哈希 text NOT NULL,
检索文本 text NOT NULL,
检索文本哈希 text NOT NULL CHECK (检索文本哈希 ~ '^[0-9a-f]{64}$'),
built_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
-- 同版本同窗计划内起点唯一;不同窗计划可并存,重建时整计划替换。
CREATE UNIQUE INDEX muse_source_slice_window
ON public.muse_source_slice (source_id, revision, 窗计划哈希, 起点);
CREATE INDEX muse_source_slice_by_source ON public.muse_source_slice (source_id, revision);
-- 派生数据禁原地改写;删除仅允许全量重建事务成批进行。
CREATE FUNCTION public.muse_guard_source_slice() RETURNS trigger LANGUAGE plpgsql AS $guard$
BEGIN
RAISE EXCEPTION '场景切片是派生数据,禁止原地改写;重建请整批删除后重算';
END;
$guard$;
CREATE TRIGGER muse_source_slice_guard BEFORE UPDATE ON public.muse_source_slice
FOR EACH ROW EXECUTE FUNCTION public.muse_guard_source_slice();
-- 切片向量索引:与 muse_method_index 同构(哈希 bigram,内存余弦)。
CREATE TABLE public.muse_source_slice_index (
slice_id uuid PRIMARY KEY REFERENCES public.muse_source_slice(slice_id) ON DELETE CASCADE,
检索文本 text NOT NULL,
检索文本哈希 text NOT NULL,
vector double precision[] NOT NULL CHECK (array_length(vector, 1) > 0),
embedder text NOT NULL,
built_at timestamptz NOT NULL DEFAULT clock_timestamp()
);
-- 代次台账:当前有效索引身份;检索时对不上即拒绝为陈旧。
CREATE TABLE public.muse_source_slice_index_version (
ledger_id bigint PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
索引身份 text NOT NULL,
embedder text NOT NULL,
slice_count int NOT NULL CHECK (slice_count >= 0),
built_at timestamptz NOT NULL DEFAULT clock_timestamp(),
active boolean NOT NULL DEFAULT false
);
CREATE UNIQUE INDEX muse_source_slice_index_active
ON public.muse_source_slice_index_version (active) WHERE active;
-- 代次守卫:切片与索引的事务变更递增代次,检索按代次判新鲜(仿 V0059)。
CREATE TABLE public.muse_source_slice_guard (
singleton boolean PRIMARY KEY DEFAULT true CHECK (singleton),
generation bigint NOT NULL DEFAULT 0 CHECK (generation >= 0)
);
INSERT INTO public.muse_source_slice_guard (singleton, generation) VALUES (true, 0);
CREATE FUNCTION public.muse_source_slice_changed() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER SET search_path = pg_catalog AS $guard$
BEGIN
UPDATE public.muse_source_slice_guard
SET generation = generation + 1
WHERE singleton;
IF NOT FOUND THEN
RAISE EXCEPTION 'source slice mutation guard is missing';
END IF;
RETURN NULL;
END;
$guard$;
CREATE TRIGGER muse_source_slice_changed
AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON public.muse_source_slice
FOR EACH STATEMENT EXECUTE FUNCTION public.muse_source_slice_changed();
CREATE TRIGGER muse_source_slice_index_changed
AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON public.muse_source_slice_index
FOR EACH STATEMENT EXECUTE FUNCTION public.muse_source_slice_changed();
CREATE TRIGGER muse_source_slice_head_changed
AFTER INSERT OR UPDATE OR DELETE OR TRUNCATE ON public.muse_source_slice_index_version
FOR EACH STATEMENT EXECUTE FUNCTION public.muse_source_slice_changed();
REVOKE ALL ON public.muse_source_slice_guard FROM PUBLIC, muse_app, muse_eval;
GRANT SELECT ON public.muse_source_slice_guard TO muse_app, muse_eval;
REVOKE ALL ON FUNCTION public.muse_source_slice_changed() FROM PUBLIC;
END
$migration$;