muse-agent-example/数据库/旧库迁移/转换质量资产.py
zizi 9e6f1c4481 R2 改造交付:新版模块化单体全量成果
- src/muse 新版全模块(装配/共享/上下文/任务运行/作品规划/故事世界/正文写作/审校修订/知识方法/作者经验/效果评测/交付连载/资料研究/正式变更/元数据/接入/基础设施/编排)+ 测试树(单元/契约/集成/架构/迁移/端到端/夹具)
- 129 项功能全部实现与自动验证(功能覆盖.json/矩阵),含 W31 补齐的规则与代价/节奏安排/伏笔与承诺
- 旧实现按处置清单退出(702 条中 324 删,保护合同与未迁移条目留存有据);web/app.py 旧工作台退役,新工作台为唯一写入口
- 数据库/旧库迁移:真实旧库内容批次迁移链(端点守卫/PG作品正文映射/质量资产缺省投影)
- 运行手册 docs/运行手册.md;W30 本机服务阶段一已运行(infra PG 为正式内容权威)
- R2 执行证据与私有运行材料在 .agents.local/改造/R2-20260909/(不入库)
2026-09-15 12:47:42 +08:00

501 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""旧规则版本与质量案例转换;只经 B06 公开用例,不触碰 owner 私表。"""
from __future__ import annotations
import hashlib
from dataclasses import asdict
from typing import Any
from uuid import NAMESPACE_URL, uuid5
import yaml
from pydantic import TypeAdapter
from muse.审校修订.接口 import 核对规则库, 案例来源, 案例采集请求
from muse.正式变更.接口 import 固定哈希
from muse.正文写作.接口 import 可见文本, 正文结构哈希, 正文草稿
from 建立映射 import 稳定JSON, 读取JSON, 旧快照, 映射配置, 迁移错误
from 读取旧文件 import 解码文件
规则表 = "example_ai_flavor_rule"
例证表 = "example_ai_flavor_sample"
案例表 = "example_ai_flavor_case"
规则历史表 = "example_ai_flavor_rule_event"
允许保全许可 = {"owned", "licensed", "public_domain", "synthetic"}
# 旧DDL 111的精确默认;仅用于缺省键的投影比较,不补写原载荷或授权。
缺省投影默认 = {"case_card_ids": [], "source_license": "synthetic", "source_ref": ""}
def 质量案例ID(作者: str, source_key: str) -> str:
"""与迁移命令及B06案例公开用例相同的确定性身份。"""
键 = hashlib.sha256(稳定JSON([作者, source_key]).encode()).hexdigest()
命令 = str(uuid5(NAMESPACE_URL, "muse:legacy-import:" + 键))
return str(uuid5(NAMESPACE_URL, f"quality-case:{作者}:{命令}"))
def _表(快照: 旧快照) -> str:
return 快照.source.table.rsplit(".", 1)[-1]
def _对象(值: Any, 错误码: str, 说明: str) -> dict:
if isinstance(值, str):
try:
值 = 读取JSON(值)
except (ValueError, RecursionError) as 错:
raise 迁移错误(错误码, 说明) from 错
if not isinstance(值, dict):
raise 迁移错误(错误码, 说明)
return 值
def _清单(值: Any, 错误码: str, 说明: str) -> list:
if isinstance(值, str):
try:
值 = 读取JSON(值)
except (ValueError, RecursionError) as 错:
raise 迁移错误(错误码, 说明) from 错
if not isinstance(值, list):
raise 迁移错误(错误码, 说明)
return 值
def _内容哈希(值: dict) -> str:
return hashlib.sha256(稳定JSON(值).encode()).hexdigest()
def _文件YAML(快照: 旧快照) -> Any:
try:
return yaml.safe_load(解码文件(快照).decode("utf-8"))
except (UnicodeDecodeError, yaml.YAMLError) as 错:
raise 迁移错误("quality_source_invalid", "旧质量文件不是可解释的UTF-8 YAML") from 错
def _核对投影(行: dict, 载荷: dict, 字段: dict[str, str]) -> None:
for 列, 键 in 字段.items():
值 = 行.get(列)
if 列 in {"trigger_json"}:
值 = _对象(值, "quality_source_invalid", "旧规则JSON投影不是对象")
elif 列 in {"carve_out", "function_check", "case_card_ids", "rules"}:
值 = _清单(值, "quality_source_invalid", "旧质量JSON投影不是清单")
elif 列 == "sample_refs":
值 = _对象(值, "quality_source_invalid", "旧规则例证引用不是对象")
载荷值 = 载荷.get(键)
if 列 == 键 and 列 in 缺省投影默认 and 键 not in 载荷:
载荷值 = 缺省投影默认[列]
if 稳定JSON(值) != 稳定JSON(载荷值):
raise 迁移错误("quality_projection_mismatch", "旧运行投影与原载荷不一致:" + 列)
def _规则(快照: 旧快照) -> tuple[dict, str]:
if 快照.source.system == "file":
值 = _文件YAML(快照)
if not isinstance(值, dict):
raise 迁移错误("quality_source_invalid", "旧规则文件必须只含一个规则对象")
return 值, str(值.get("status", ""))
if _表(快照) != 规则表:
raise 迁移错误("quality_source_invalid", "规则绑定未指向旧规则表或文件")
行 = 快照.原行
载荷 = _对象(行.get("payload"), "quality_source_invalid", "旧规则缺完整payload")
if 行.get("content_sha256") != _内容哈希(载荷):
raise 迁移错误("quality_content_hash_mismatch", "旧规则内容哈希不符")
_核对投影(
行,
载荷,
{
"rule_id": "id",
"name": "name",
"layer": "layer",
"carrier_scope": "carrier_scope",
"default_disposition": "default_disposition",
"status": "status",
"version": "version",
"trigger_json": "trigger",
"carve_out": "carve_out",
"function_check": "function_check",
"sample_refs": "samples",
"case_card_ids": "case_card_ids",
"fix_hint": "fix_hint",
"evidence": "evidence",
},
)
return 载荷, str(行.get("status", ""))
def _例证集(快照: 旧快照) -> dict[str, dict]:
if 快照.source.system == "file":
值 = _文件YAML(快照)
if (
not isinstance(值, dict)
or set(值) != {"samples"}
or not isinstance(值["samples"], list)
):
raise 迁移错误("quality_source_invalid", "旧例证文件必须是单一samples清单")
行集 = 值["samples"]
else:
if _表(快照) != 例证表:
raise 迁移错误("quality_source_invalid", "例证绑定未指向旧例证表或文件")
行 = 快照.原行
载荷 = _对象(行.get("payload"), "quality_source_invalid", "旧例证缺完整payload")
if 行.get("content_sha256") != _内容哈希(载荷):
raise 迁移错误("quality_content_hash_mismatch", "旧例证内容哈希不符")
_核对投影(
行,
载荷,
{
"sample_id": "id",
"sample_type": "type",
"carrier": "carrier",
"source": "source",
"source_license": "source_license",
"rules": "rules",
"text": "text",
"note": "note",
"case_card_id": "case_card_id",
"source_ref": "source_ref",
},
)
行集 = [载荷]
结果 = {}
for 行 in 行集:
if not isinstance(行, dict) or not isinstance(行.get("id"), str) or 行["id"] in 结果:
raise 迁移错误("quality_source_invalid", "旧例证身份缺失或重复")
结果[行["id"]] = 行
return 结果
def 转换规则库(快照: 旧快照, 绑定: dict, 可用源: dict[str, 旧快照]) -> tuple[dict, dict]:
规则, 旧状态 = _规则(快照)
参数 = 绑定["parameters"]
if set(参数) != {"rule_id", "version"} or (规则.get("id"), 规则.get("version")) != (
参数.get("rule_id"),
参数.get("version"),
):
raise 迁移错误("quality_parameter_mismatch", "规则身份/版本与冻结参数不一致")
需要 = (
{x for v in 规则.get("samples", {}).values() for x in v}
if isinstance(规则.get("samples"), dict)
else set()
)
已选, 例证 = set(), {}
for 源 in 绑定["sources"]:
if set(源) != {"source_key", "source_hash", "sample_ids", "license", "source_ref"}:
raise 迁移错误("quality_binding_invalid", "规则例证来源未冻结身份、许可或样例集")
原 = 可用源.get(源["source_key"])
if 原 is None:
raise 迁移错误(
"quality_dependency_missing", "规则例证快照缺失:" + str(源["source_key"])
)
if 原.源哈希 != 源["source_hash"]:
raise 迁移错误("quality_source_drift", "规则例证快照哈希改变")
if (
not isinstance(源["sample_ids"], list)
or not 源["sample_ids"]
or not isinstance(源["license"], str)
or 源["license"] not in 允许保全许可
or not isinstance(源["source_ref"], str)
or not 源["source_ref"]
):
raise 迁移错误("quality_binding_invalid", "规则例证来源或许可声明不完整")
候选 = _例证集(原)
for id_ in 源["sample_ids"]:
if id_ in 已选 or id_ not in 候选:
raise 迁移错误("quality_example_mismatch", "例证身份重复或不属于冻结来源")
样例 = dict(候选[id_])
if 样例.get("source_license") not in (None, 源["license"]):
raise 迁移错误("quality_license_mismatch", "例证的旧许可与冻结许可不一致")
if 样例.get("source_ref") not in (None, "", 源["source_ref"]):
raise 迁移错误("quality_source_mismatch", "例证的旧来源与冻结来源不一致")
样例["source_license"], 样例["source_ref"] = 源["license"], 源["source_ref"]
例证[id_] = 样例
已选.add(id_)
if 已选 != 需要:
raise 迁移错误(
"quality_examples_incomplete", "规则的sf/snf/boundary/regression例证与冻结集合不一致"
)
候选规则 = {**规则, "status": "candidate"}
库 = 核对规则库([候选规则], list(例证.values()))
return 库, {"status": 旧状态, "source_key": 快照.source.记录键, "source_hash": 快照.源哈希}
def _旧案例(快照: 旧快照, card_id: str) -> dict:
if 快照.source.system == "file":
值 = _文件YAML(快照)
if not isinstance(值, dict) or not isinstance(值.get("cards"), list):
raise 迁移错误("quality_source_invalid", "旧案例文件必须含cards清单")
项 = [r for r in 值["cards"] if isinstance(r, dict) and r.get("id") == card_id]
if len(项) != 1:
raise 迁移错误("quality_case_ambiguous", "旧案例文件未唯一命中冻结卡片")
卡 = 项[0]
源 = _对象(卡.get("source"), "quality_source_invalid", "旧案例缺来源对象")
return {
"card_id": card_id,
"state": 卡.get("state"),
"label": 卡.get("label", "unclassified"),
"excerpt": 卡.get("excerpt", ""),
"context": 卡.get("context", ""),
"source_ref": 源.get("source_ref", ""),
"work_ref": 源.get("work_ref"),
"source_sha256": 源.get("source_sha256"),
"excerpt_sha256": 源.get("excerpt_sha256"),
"source_location": 源.get("location"),
"source_license": 源.get("license"),
"capture_mode": 卡.get("capture_mode"),
"original": 卡,
}
if _表(快照) != 案例表 or 快照.原行.get("card_id") != card_id:
raise 迁移错误("quality_source_invalid", "案例绑定未指向旧案例表或指定卡片")
行 = 快照.原行
return {
**{
k: 行.get(k)
for k in (
"card_id",
"state",
"label",
"excerpt",
"context",
"source_ref",
"work_ref",
"source_sha256",
"excerpt_sha256",
"source_location",
"source_license",
"capture_mode",
)
},
"original": 行,
}
def _哈希旧片段(案例: dict) -> None:
哈希 = 案例.get("excerpt_sha256")
if not isinstance(哈希, str) or len(哈希) != 64:
raise 迁移错误("quality_excerpt_hash_missing", "旧案例缺少片段哈希")
原文 = 案例.get("excerpt")
if 原文:
if not isinstance(原文, str) or hashlib.sha256(原文.encode()).hexdigest() != 哈希:
raise 迁移错误("quality_excerpt_hash_mismatch", "旧案例片段字节与旧哈希不一致")
def _案例请求(装配, 身份, 快照: 旧快照, 绑定: dict) -> tuple[案例采集请求, dict, str]:
参数 = 绑定["parameters"]
if set(参数) != {"card_id", "preserve_text"} or type(参数.get("preserve_text")) is not bool:
raise 迁移错误("quality_parameter_mismatch", "案例参数只能冻结card_id与preserve_text")
案例 = _旧案例(快照, 参数.get("card_id"))
_哈希旧片段(案例)
if len(绑定["sources"]) != 1:
raise 迁移错误("quality_binding_invalid", "旧案例必须唯一绑定一个新版before来源")
来源 = 绑定["sources"][0]
if (
set(来源) != {"role", "license", "license_ref", "legacy", "target"}
or 来源.get("role") != "before"
):
raise 迁移错误("quality_binding_invalid", "案例来源必须冻结许可、旧引用和新版before目标")
旧引用, 目标 = 来源["legacy"], 来源["target"]
if not isinstance(旧引用, dict) or set(旧引用) != {
"source_ref",
"work_ref",
"source_sha256",
"excerpt_sha256",
"location",
}:
raise 迁移错误("quality_binding_invalid", "旧案例引用身份不完整")
实际旧引用 = {
"source_ref": 案例.get("source_ref"),
"work_ref": 案例.get("work_ref"),
"source_sha256": 案例.get("source_sha256"),
"excerpt_sha256": 案例.get("excerpt_sha256"),
"location": 案例.get("source_location"),
}
if 稳定JSON(旧引用) != 稳定JSON(实际旧引用):
raise 迁移错误("quality_source_mismatch", "冻结旧来源与原案例引用不一致")
if (
来源.get("license") != 案例.get("source_license")
or not isinstance(来源.get("license_ref"), str)
or not 来源["license_ref"]
):
raise 迁移错误("quality_license_mismatch", "旧案例许可没有唯一证明或与原记录不同")
if 参数["preserve_text"] and 来源["license"] not in 允许保全许可:
raise 迁移错误("quality_license_denied", "旧来源许可不允许保全原文")
if not isinstance(目标, dict) or set(目标) != {
"kind",
"ref_id",
"revision",
"source_hash",
"start",
"end",
"excerpt_hash",
"branch_id",
"work_ref",
}:
raise 迁移错误("quality_binding_invalid", "案例新版来源缺少确切版本、哈希或范围")
if 目标.get("excerpt_hash") != 案例["excerpt_sha256"]:
raise 迁移错误("quality_excerpt_hash_mismatch", "旧片段哈希与新版来源范围不同")
目标定位 = {
k: 目标[k]
for k in (
"kind",
"ref_id",
"revision",
"source_hash",
"start",
"end",
"excerpt_hash",
"branch_id",
)
}
try:
定位 = 案例来源(**目标定位)
except (TypeError, ValueError) as 错:
raise 迁移错误("quality_binding_invalid", "案例新版来源定位不符B06合同") from 错
_核对新版案例来源(装配, 身份, 定位, 目标["work_ref"], 参数["preserve_text"])
标签 = 案例.get("label")
if 标签 not in {"unclassified", "sf", "snf", "boundary", "regression"}:
raise 迁移错误("quality_label_invalid", "旧案例标签无法保存,不自动重分类")
请求 = 案例采集请求(
定位, label=标签, disposition="ask", mode="backfill", preserve_text=参数["preserve_text"]
)
return 请求, 案例, 目标["work_ref"]
def _核对新版案例来源(装配, 身份, 定位: 案例来源, work_ref: str, 保全: bool) -> None:
if 定位.kind == "source":
with 装配.要求数据库().连接(只读=True) as 连:
原 = 装配.要求资料().读取案例证据(
连, 身份.作者, 定位.ref_id, 定位.revision, 定位.start, 定位.end
)
实际作品, 实际源哈希, 实际片段 = 原["work_group"], 原["source_hash"], 原["excerpt_hash"]
if 保全 and not 原["text_allowed"]:
raise 迁移错误("quality_license_denied", "新版资料来源未授权作案例原文")
else:
正文 = 装配.要求正文()
if 定位.kind == "document":
原 = 正文.读取正文(身份, 定位.ref_id, 分支=定位.branch_id, 版本=定位.revision)
else:
原 = 正文.读取候选(身份, 定位.ref_id, 版本=定位.revision)
if 原["branch_id"] != 定位.branch_id:
raise 迁移错误("quality_source_mismatch", "新版候选分支与冻结定位不同")
文档 = TypeAdapter(正文草稿).validate_python(原["document"])
文本 = 可见文本(文档)
实际作品, 实际源哈希 = "work:" + 原["work_id"], 正文结构哈希(文档)
实际片段 = hashlib.sha256(文本[定位.start : 定位.end].encode()).hexdigest()
if (实际作品, 实际源哈希, 实际片段) != (work_ref, 定位.source_hash, 定位.excerpt_hash):
raise 迁移错误("quality_source_mismatch", "新版来源作品、版本内容或片段与冻结绑定不同")
def 准备质量请求(装配, 身份, 快照: 旧快照, 映射: 映射配置, 可用源: dict[str, 旧快照]) -> dict:
绑定 = 映射.质量绑定(快照)
if 绑定 is None:
raise 迁移错误("quality_binding_missing", "质量资产缺少显式来源绑定")
if 绑定["author_id"] != 身份.作者:
raise 迁移错误("target_denied", "质量资产绑定不属于当前认证作者")
if 绑定["route"] == "rule_version":
库, 旧源 = 转换规则库(快照, 绑定, 可用源)
if not callable(getattr(装配.要求审校(), "读取规则版本", None)):
raise 迁移错误(
"target_owner_read_unavailable",
"B06缺少按rule_id+version精确读取规则版本的公开接口",
)
return {
"owner": "B06",
"scope": "rule-library:" + 身份.作者,
"subject_ref": 绑定["parameters"]["rule_id"],
"quality_kind": "rule_version",
"legacy_source": 旧源,
"source_bindings": 绑定["sources"],
"payload": 库,
}
if 绑定["route"] != "quality_case":
raise 迁移错误("target_adapter_unavailable", "规则例证依赖不单独创建质量对象")
请求, 旧案例, 作品组 = _案例请求(装配, 身份, 快照, 绑定)
case_id = 质量案例ID(身份.作者, 快照.source.记录键)
return {
"owner": "B06",
"scope": 作品组,
"subject_ref": case_id,
"quality_kind": "quality_case",
"legacy_source": {
"source_key": 快照.source.记录键,
"source_hash": 快照.源哈希,
"card_id": 旧案例["card_id"],
"state": 旧案例["state"],
"capture_mode": 旧案例["capture_mode"],
},
"source_bindings": 绑定["sources"],
"payload": asdict(请求),
}
def 提交质量请求(装配, 身份, 命令ID: str, 冻结: dict) -> dict:
审校 = 装配.要求审校()
if 冻结["quality_kind"] == "rule_version":
return 审校.导入规则候选(身份, 规则库=冻结["payload"])
请求 = TypeAdapter(案例采集请求).validate_python(冻结["payload"])
return 审校.采集案例(身份, 命令ID, 请求)
def 核对规则owner产物(审校, 身份, 冻结: dict, 回执: dict) -> list[dict]:
if 冻结.get("quality_kind") != "rule_version":
raise 迁移错误("target_request_invalid", "owner规则回执校验只接受规则版本")
规则 = next(iter(冻结["payload"]["rules"].values()))
读取 = getattr(审校, "读取规则版本", None)
if not callable(读取):
raise 迁移错误("target_owner_read_unavailable", "B06缺少精确规则版本读回")
实际 = 读取(身份, 规则["id"], 规则["version"])
if (
not isinstance(回执, dict)
or 回执.get("persisted") is not True
or 回执.get("fingerprint") != 冻结["payload"]["fingerprint"]
or not isinstance(回执.get("results"), list)
or len(回执["results"]) != 1
or not isinstance(实际, dict)
or 实际.get("rule_id") != 规则["id"]
or 实际.get("revision") != 规则["version"]
or 实际.get("status") != "candidate"
or 实际.get("activation_recorded") is not False
or 稳定JSON(实际.get("payload"))
!= 稳定JSON({"rule": 规则, "samples": 冻结["payload"]["samples"]})
or 实际.get("payload_hash") != 固定哈希(实际.get("payload"))
or 实际.get("version_id") != 回执["results"][0].get("version_id")
):
raise 迁移错误("target_mismatch", "B06精确规则版本、候选状态或owner回执不符")
return [
{
"id": 实际["version_id"],
"revision": 规则["version"],
"kind": "rule_version",
"owner": "B06",
"subject_ref": 规则["id"],
"scope": 冻结["scope"],
"payload_hash": 实际.get("payload_hash"),
}
]
def 核对质量产物(装配, 身份, 冻结: dict, 回执: dict) -> list[dict]:
审校 = 装配.要求审校()
if 冻结["quality_kind"] == "rule_version":
return 核对规则owner产物(审校, 身份, 冻结, 回执)
结果 = 回执.get("results", []) if isinstance(回执, dict) else []
命中 = [r for r in 结果 if isinstance(r, dict) and r.get("case_id") == 冻结["subject_ref"]]
if 回执.get("action") != "manual_save" or len(命中) != 1 or 命中[0].get("state") != "shadow":
raise 迁移错误("target_receipt_invalid", "案例迁移只接受B06未决采集回执")
实际 = 审校.读取案例(身份, 冻结["subject_ref"], revision=命中[0].get("revision"))
if (
实际.get("state") != "shadow"
or 实际.get("sampleable")
or 实际.get("case_hash") != 命中[0].get("case_hash")
or 稳定JSON(实际.get("payload", {}).get("request")) != 稳定JSON(冻结["payload"])
):
raise 迁移错误("target_mismatch", "B06案例精确版本与冻结回填不符")
return [
{
"id": 实际["case_id"],
"revision": 实际["revision"],
"kind": "quality_case",
"owner": "B06",
"subject_ref": 冻结["subject_ref"],
"scope": 冻结["scope"],
"case_hash": 实际["case_hash"],
}
]