重构(数据库): 迁移链压扁为单基线并精简测试至保留集
- 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:
parent
dca0032001
commit
713cc45c63
20
docs/运行手册.md
20
docs/运行手册.md
@ -39,6 +39,26 @@ ps -p "$pid" -o command=
|
||||
- 版本化范围:`配置/本机正式.toml`、`配置/本机维护.toml` 保存非敏感配置及受控引用;仅创建文件不代表已经提交 Git。
|
||||
- 本机受控根:`~/.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 秒);只调优当次部署的借还行为,不改变数据库目标身份。
|
||||
|
||||
### 数据库结构变更(基线模式)
|
||||
|
||||
自 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/`。恢复时复制所需文件到本机受控根并核对权限;禁止输出凭据内容到日志或提交明文凭据。
|
||||
- 正式连接仅使用 PG `100.64.0.8:5433`,带 `keepalives=1 keepalives_idle=15 keepalives_interval=5 keepalives_count=3`。Redis 与本项目无关。
|
||||
- 原迁移目录中的库外原文、草稿和切换回执尚未找回。当前文件根是恢复后新建的目录,不代表原文件资产已恢复。
|
||||
|
||||
2050
tests/用例清单.json
2050
tests/用例清单.json
File diff suppressed because it is too large
Load Diff
@ -1,21 +1,23 @@
|
||||
"""备份恢复的核心保护:七域真实往返与拒绝覆盖非空目标。
|
||||
|
||||
个人工具的最小安全网——全部作品资产在一个 PG 库里,备份必须能真恢复,
|
||||
恢复不得覆盖任何已有内容。其余切换/篡改/权限变体用例已按简化决策删除。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
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 应用配置
|
||||
|
||||
pytestmark = pytest.mark.数据库
|
||||
|
||||
@ -24,47 +26,13 @@ def _资源身份() -> str:
|
||||
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 _应用配置(
|
||||
工厂: 数据库工厂,
|
||||
raw: Path,
|
||||
drafts: Path,
|
||||
*,
|
||||
引用: 数据库引用 | None = None,
|
||||
) -> 应用配置:
|
||||
return 应用配置(
|
||||
引用 or 工厂.引用,
|
||||
工厂.引用,
|
||||
_资源身份(),
|
||||
运行用途=用途.维护,
|
||||
原文暂存=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]:
|
||||
raw = tmp_path / f"{前缀}-raw"
|
||||
drafts = tmp_path / f"{前缀}-drafts"
|
||||
@ -126,130 +84,80 @@ def _建立数据库探针(工厂: 数据库工厂) -> None:
|
||||
)
|
||||
|
||||
|
||||
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})),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.case_id(
|
||||
"NC-w29-29b001",
|
||||
environment="隔离PG",
|
||||
given="显式隔离配置、合成数据与已知发布构建",
|
||||
when="经真实维护、业务或浏览器入口执行并读取持久结果",
|
||||
then=["完整PG文件权限关系与动态内容实际往返"],
|
||||
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
|
||||
"TC-0f7c45927fbe",
|
||||
environment="隔离 PostgreSQL 与临时目录",
|
||||
when="备份、校验、恢复。",
|
||||
contract="docs/系统架构/新版设计/迁移与验收.md",
|
||||
)
|
||||
def test_完整PG文件权限关系与动态内容实际往返__29b001(
|
||||
def test_备份单快照七域往返且同内容重放一致__0f7c45(
|
||||
应用测试库: dict, 新空目标库: SimpleNamespace, tmp_path: Path
|
||||
) -> None:
|
||||
"""数据库与文件进入同一份校验清单;关闭连接后从磁盘复验一致。"""
|
||||
来源工厂 = 应用测试库[用途.维护]
|
||||
_建立数据库探针(来源工厂)
|
||||
raw, drafts = _准备文件(tmp_path, "source")
|
||||
敏感引用 = _带敏感值引用(来源工厂, tmp_path, "source-secret.txt")
|
||||
来源配置 = _应用配置(来源工厂, 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 = _准备文件(tmp_path, "domains")
|
||||
备份目录 = tmp_path / "domains-backup"
|
||||
清单 = 创建备份(_应用配置(来源工厂, 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"
|
||||
原字节 = 数据库归档.read_bytes()
|
||||
数据库归档.write_bytes(原字节[:-1] + bytes([原字节[-1] ^ 0xFF]))
|
||||
数据库归档.chmod(0o600)
|
||||
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"):
|
||||
核对备份(备份目录)
|
||||
目标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(
|
||||
"NC-w29-29b004",
|
||||
environment="隔离PG",
|
||||
given="显式隔离配置、合成数据与已知发布构建",
|
||||
when="经真实维护、业务或浏览器入口执行并读取持久结果",
|
||||
given="显式隔离配置与合成数据",
|
||||
when="向来源库或已有内容目标发起恢复",
|
||||
then=["拒绝来源库与已有内容目标且不覆盖文件"],
|
||||
contract="docs/系统架构/新版设计/迁移与验收.md#备份与恢复入口",
|
||||
)
|
||||
@ -282,588 +190,3 @@ def test_拒绝来源库与已有内容目标且不覆盖文件__29b004(
|
||||
with 新空目标库.工厂.连接() as 连:
|
||||
assert 连.execute("SELECT value FROM public.keep_me").fetchone() == ("保留",)
|
||||
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()
|
||||
|
||||
@ -1,4 +1,8 @@
|
||||
"""公开任务接口的持久状态、独立连接竞争与恢复合同。"""
|
||||
"""任务租约的核心保护:重启不重复执行、未知调用先对账。
|
||||
|
||||
按简化决策删除其余变体用例;夹具从按例克隆改为共享库以消除建库开销。
|
||||
`任务库` 与 `请求` 同时被 test_任务租约与迟到结果.py 复用。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@ -6,26 +10,19 @@ import dataclasses
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from pathlib import Path
|
||||
from threading import Barrier
|
||||
|
||||
import psycopg
|
||||
import pytest
|
||||
|
||||
from muse.任务运行.接口 import (
|
||||
事件类型,
|
||||
任务服务,
|
||||
任务状态,
|
||||
任务请求,
|
||||
任务错误,
|
||||
作用域,
|
||||
步骤处理器,
|
||||
步骤结果,
|
||||
步骤计划,
|
||||
状态冲突,
|
||||
租约失效,
|
||||
)
|
||||
from muse.共享.调用身份 import 内容用途, 用途
|
||||
from muse.基础设施.数据库.连接 import 数据库工厂
|
||||
@ -36,7 +33,7 @@ pytestmark = pytest.mark.数据库
|
||||
|
||||
@pytest.fixture
|
||||
def 任务库(数据库底座, monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
|
||||
with 数据库底座.借库(tmp_path / "连接引用") as 工厂:
|
||||
with 数据库底座.共享库(tmp_path / "连接引用") as 工厂:
|
||||
# 子进程恢复入口继承相同的按例数据库,仍跨真实连接验证提交。
|
||||
for 声明, 当前 in 工厂.items():
|
||||
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 运行.读取任务(身份).步骤)
|
||||
|
||||
|
||||
@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(
|
||||
"NC-task-unknown-reconciliation",
|
||||
environment="隔离PostgreSQL",
|
||||
@ -340,189 +182,3 @@ def test_未知调用先对账并复用可靠输出__a51006(任务库) -> None:
|
||||
运行.执行一步(收尾)
|
||||
assert 运行.读取任务(身份).状态 is 任务状态.已完成
|
||||
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"
|
||||
|
||||
@ -80,11 +80,10 @@ def 独立安装环境(tmp_path):
|
||||
for part in Path(name).parts
|
||||
)
|
||||
migrations = [n for n in names if n.startswith("muse/资源/迁移/") and n.endswith(".sql")]
|
||||
assert len(migrations) == len(list((根 / "数据库/迁移").glob("*.sql")))
|
||||
assert (
|
||||
archive.read("muse/资源/迁移/V0001__共享标识与版本.sql")
|
||||
== (根 / "数据库/迁移/V0001__共享标识与版本.sql").read_bytes()
|
||||
)
|
||||
源迁移 = sorted((根 / "数据库/迁移").glob("*.sql"))
|
||||
assert sorted(Path(n).name for n in migrations) == [p.name for p in 源迁移]
|
||||
for 源 in 源迁移:
|
||||
assert archive.read(f"muse/资源/迁移/{源.name}") == 源.read_bytes()
|
||||
requirements = tmp_path / "runtime.txt"
|
||||
_命令(
|
||||
[
|
||||
|
||||
@ -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
|
||||
)
|
||||
@ -1,350 +1,43 @@
|
||||
"""真实连接、权限、事务与迁移验证;仅使用显式隔离 PostgreSQL。"""
|
||||
"""基线建库的最小验证:空库执行基线后账本登记且业务角色可连。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import psycopg
|
||||
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 数据库引用, 连接
|
||||
|
||||
迁移目录 = Path(__file__).resolve().parents[2] / "数据库" / "迁移"
|
||||
角色表 = {用途.生产: "muse_app", 用途.评测: "muse_eval", 用途.维护: "muse_maint"}
|
||||
pytestmark = pytest.mark.数据库
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def 临时库(空测试库) -> Iterator[dict[用途, 数据库引用]]:
|
||||
"""空库没有预执行被测 DDL;真实提交、并发和迁移动作由测试执行。"""
|
||||
yield {用途值: 工厂.引用 for 用途值, 工厂 in 空测试库.items()}
|
||||
|
||||
|
||||
def 维护连接(临时库: dict[用途, 数据库引用], **参数: object) -> psycopg.Connection:
|
||||
return 连接(临时库[用途.维护], 用途标记=用途.维护, **参数)
|
||||
yield 空测试库
|
||||
|
||||
|
||||
@pytest.mark.case_id(
|
||||
"NC-db-no-fallback",
|
||||
environment="离线",
|
||||
given="数据库凭据引用未提供",
|
||||
when="从真实装配取得数据库连接",
|
||||
then=["报环境缺失并拒绝任何默认库回退"],
|
||||
contract="docs/系统架构/新版设计/文件设计/数据库与迁移.md",
|
||||
"TC-base-0001",
|
||||
environment="隔离PG",
|
||||
given="全新空库与包内基线 SQL",
|
||||
when="以维护用途执行基线迁移",
|
||||
then=["账本登记基线版本,生产角色可连接并读取结构"],
|
||||
)
|
||||
def test_缺配置拒绝回退默认库__d10001(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""given 缺少连接引用;when 装配连接;then 拒绝默认库回退。"""
|
||||
monkeypatch.delenv("MUSE_MISSING_DATABASE_URL", raising=False)
|
||||
配置 = 应用配置(数据库引用("环境变量", "MUSE_MISSING_DATABASE_URL"), "test")
|
||||
装配 = 构建(配置)
|
||||
工厂 = 装配.要求数据库()
|
||||
with 装配.生命周期():
|
||||
# 打开空池不读凭据;首次借用才解析,缺环境变量时拒绝默认库回退。
|
||||
with pytest.raises(环境缺失错误, match="拒绝回退"):
|
||||
with 工厂.连接():
|
||||
pass
|
||||
def test_空库执行基线登记账本且业务角色可连__base001(临时库) -> None:
|
||||
# 空测试库不预执行 DDL;在本用例内从零执行基线并验证账本与角色连接。
|
||||
维护 = 连接(临时库[用途.维护].引用, 用途标记=用途.维护)
|
||||
with 维护 as 连:
|
||||
from muse.基础设施.数据库.迁移 import 执行迁移
|
||||
|
||||
|
||||
@pytest.mark.case_id(
|
||||
"NC-db-readonly-write",
|
||||
environment="隔离 PostgreSQL",
|
||||
given="维护角色连接且附加普通 options",
|
||||
when="请求只读并写入",
|
||||
then=["两种 options 下 PostgreSQL 都拒绝写入"],
|
||||
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 "
|
||||
"WHERE table_name IN ('participant_one', 'participant_two')"
|
||||
).fetchone()[0]
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
@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"
|
||||
)
|
||||
执行迁移(连, Path("数据库/迁移"))
|
||||
项 = 列出迁移()
|
||||
assert [i.版本 for i in 项] == [1]
|
||||
assert 已应用版本(连) == {1: 项[0].校验和}
|
||||
生产 = 连接(临时库[用途.生产].引用, 用途标记=用途.生产)
|
||||
with 生产 as 连:
|
||||
表数 = 连.execute(
|
||||
"SELECT count(*) FROM information_schema.tables "
|
||||
"WHERE table_schema = 'public' AND table_name LIKE 'muse_%'"
|
||||
).fetchone()[0]
|
||||
assert 表数 > 30, "基线应建出完整的业务表集合"
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -127,7 +127,10 @@ class 合成依赖:
|
||||
@pytest.fixture
|
||||
def 变更环境(应用测试库):
|
||||
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);
|
||||
CREATE TABLE s01_test_candidate(
|
||||
id text PRIMARY KEY,revision int,content text,check_state text);
|
||||
|
||||
@ -191,13 +191,21 @@ def test_答案写入失败回滚整个发布__251003(应用测试库):
|
||||
"CREATE TRIGGER fail_answer BEFORE INSERT ON oracle.muse_dataset_answers "
|
||||
"FOR EACH ROW EXECUTE FUNCTION oracle.fail_answer()"
|
||||
)
|
||||
with pytest.raises(评测错误, match="评测存储操作失败"):
|
||||
评测服务(pool).发布数据集(_身份(用途.维护), _数据())
|
||||
with pool.连接(只读=True) as conn:
|
||||
assert (
|
||||
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
|
||||
try:
|
||||
with pytest.raises(评测错误, match="评测存储操作失败"):
|
||||
评测服务(pool).发布数据集(_身份(用途.维护), _数据())
|
||||
with pool.连接(只读=True) as conn:
|
||||
assert (
|
||||
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
|
||||
)
|
||||
finally:
|
||||
# 共享库例间只清业务数据;失败触发器与函数必须自愈,否则污染后续用例。
|
||||
with pool.连接() as conn, conn.transaction():
|
||||
conn.execute("DROP FUNCTION IF EXISTS oracle.fail_answer() CASCADE")
|
||||
|
||||
|
||||
@pytest.mark.case_id(
|
||||
@ -390,16 +398,25 @@ def test_匿名映射中途失败回滚全部实验并能原命令重试__251009
|
||||
"CREATE TRIGGER fail_assignment BEFORE INSERT ON oracle.muse_blind_assignment "
|
||||
"FOR EACH ROW EXECUTE FUNCTION oracle.fail_assignment()"
|
||||
)
|
||||
with pytest.raises(评测错误, match="评测存储操作失败"):
|
||||
svc.创建实验(_身份(用途.评测), "recover", req)
|
||||
with pools[用途.评测].连接(只读=True) as conn:
|
||||
assert 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():
|
||||
conn.execute("DROP TRIGGER fail_assignment ON oracle.muse_blind_assignment")
|
||||
result = svc.创建实验(_身份(用途.评测), "recover", req)
|
||||
assert len(result["sample_ids"]) == 3
|
||||
assert svc.创建实验(_身份(用途.评测), "recover", req) == result
|
||||
try:
|
||||
with pytest.raises(评测错误, match="评测存储操作失败"):
|
||||
svc.创建实验(_身份(用途.评测), "recover", req)
|
||||
with pools[用途.评测].连接(只读=True) as conn:
|
||||
assert (
|
||||
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():
|
||||
conn.execute("DROP TRIGGER fail_assignment ON oracle.muse_blind_assignment")
|
||||
result = svc.创建实验(_身份(用途.评测), "recover", req)
|
||||
assert len(result["sample_ids"]) == 3
|
||||
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(
|
||||
|
||||
@ -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):
|
||||
cfg = 接入测试._配置文件(pool, path)
|
||||
cfg.write_text(
|
||||
@ -125,6 +140,7 @@ def test_系统页核实际代码资源版本且配置草案不启用__28b001(
|
||||
@pytest.mark.parametrize("mode", ["future", "checksum"])
|
||||
def test_未知数据库版本或校验和漂移拒绝HTTP写入保留只读核对__28b002(
|
||||
应用测试库,
|
||||
脏账本还原,
|
||||
tmp_path,
|
||||
mode,
|
||||
):
|
||||
@ -166,7 +182,7 @@ def test_未知数据库版本或校验和漂移拒绝HTTP写入保留只读核
|
||||
then=["不兼容时CLI拒绝配置写入而系统诊断可读"],
|
||||
contract="docs/系统架构/新版设计/文件设计/工程配置与部署.md",
|
||||
)
|
||||
def test_不兼容时CLI拒绝配置写入而系统诊断可读__28b003(应用测试库, tmp_path, capsys):
|
||||
def test_不兼容时CLI拒绝配置写入而系统诊断可读__28b003(应用测试库, 脏账本还原, tmp_path, capsys):
|
||||
_配置(应用测试库[用途.生产], tmp_path)
|
||||
cfg = next(tmp_path.glob("*.toml"))
|
||||
with 应用测试库[用途.维护].连接() as conn, conn.transaction():
|
||||
|
||||
@ -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).扩展 == 扩展
|
||||
)
|
||||
@ -1,28 +1,22 @@
|
||||
"""真实 PG 的预算原子预留、成本状态与配置版本;不调用外部模型。"""
|
||||
"""预算的核心保护:并发不超额、重复调用不重复计费。
|
||||
|
||||
from __future__ import annotations
|
||||
按简化决策删除其余变体用例;夹具从按例克隆改为共享库以消除建库开销。
|
||||
"""
|
||||
|
||||
import dataclasses
|
||||
import time
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import UTC, datetime, timedelta
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
from threading import Barrier
|
||||
|
||||
import psycopg
|
||||
import pytest
|
||||
|
||||
from muse.任务运行.接口 import 任务服务, 任务请求, 步骤处理器, 步骤结果, 步骤计划
|
||||
from muse.任务运行.模型 import 内容哈希
|
||||
from muse.任务运行.配置版本 import (
|
||||
凭据引用,
|
||||
提供方配置,
|
||||
运行配置内容,
|
||||
配置版本管理,
|
||||
配置版本错误,
|
||||
配置验证证据,
|
||||
from muse.任务运行.接口 import (
|
||||
任务服务,
|
||||
任务请求,
|
||||
步骤处理器,
|
||||
步骤结果,
|
||||
步骤计划,
|
||||
)
|
||||
from muse.任务运行.预算管理 import (
|
||||
任务预算计划,
|
||||
@ -41,8 +35,7 @@ pytestmark = pytest.mark.数据库
|
||||
|
||||
@pytest.fixture
|
||||
def 预算环境(数据库底座, monkeypatch: pytest.MonkeyPatch, tmp_path: Path):
|
||||
with 数据库底座.借库(tmp_path / "连接引用") as 工厂:
|
||||
# 子进程恢复入口继承相同的按例数据库,仍跨真实连接验证提交。
|
||||
with 数据库底座.共享库(tmp_path / "连接引用") as 工厂:
|
||||
for 声明, 当前 in 工厂.items():
|
||||
monkeypatch.setenv(f"MUSE_BUDGET_{声明.name}_URL", Path(当前.引用.位置).read_text())
|
||||
yield 工厂
|
||||
@ -102,6 +95,8 @@ def 建预算(工厂: 数据库工厂, 任务ID: str, *, 窗口金额="24", 单
|
||||
contract="docs/系统架构/新版设计/模块设计/S02-任务运行.md",
|
||||
)
|
||||
def test_并发预留共享窗口且不超额__a61001(预算环境) -> None:
|
||||
from threading import Barrier
|
||||
|
||||
A, 领取A = 新任务(预算环境[用途.生产])
|
||||
B, 领取B = 新任务(预算环境[用途.生产])
|
||||
预算A = 建预算(预算环境[用途.生产], A, 窗口金额="10")
|
||||
@ -144,237 +139,3 @@ def test_重复预留发送和结算不重复计费__a61002(预算环境) -> Non
|
||||
预算.结算("call-1", Decimal("0.2"), 回执ID="changed")
|
||||
assert 预算.窗口余额()["已知成本"] == Decimal("0.125")
|
||||
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")
|
||||
|
||||
@ -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;
|
||||
10085
数据库/迁移/V0001__基线.sql
Normal file
10085
数据库/迁移/V0001__基线.sql
Normal file
File diff suppressed because it is too large
Load Diff
@ -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;
|
||||
@ -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)
|
||||
);
|
||||
@ -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$;
|
||||
@ -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;
|
||||
@ -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);
|
||||
@ -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$;
|
||||
@ -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;
|
||||
@ -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$;
|
||||
@ -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$;
|
||||
@ -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$;
|
||||
@ -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$;
|
||||
@ -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;
|
||||
@ -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
|
||||
$迁移$;
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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));
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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 $$;
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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();
|
||||
@ -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'));
|
||||
@ -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();
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,2 +0,0 @@
|
||||
-- 运行角色只读数据版本,不获得迁移账本写权限;用于拒绝不兼容程序继续写入。
|
||||
GRANT SELECT ON public.muse_migration TO muse_app, muse_eval;
|
||||
@ -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);
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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();
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,3 +0,0 @@
|
||||
-- 当前消费授权与不可变内容版本独立;旧授权集合保持原值。
|
||||
ALTER TABLE muse_source ADD COLUMN authorization_revision bigint NOT NULL DEFAULT 1
|
||||
CHECK (authorization_revision >= 1);
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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();
|
||||
@ -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$;
|
||||
Loading…
x
Reference in New Issue
Block a user