feat(上下文): 场景检索入口与片段参考进冻结链路——参考书场景原文可检索可改写
- 场景检索.py:切片语料两路等权RRF(键带slice:前缀),授权join过滤、代次陈旧拒绝、结果带书名预览双路得分;RRF抽公共融合入基础设施 - 参考材料双载体:references节判别式(方法/片段),片段冻结身份带来源内容哈希哨兵,现读核当前版本一致否则SOURCE_STALE - 选段授权slice_references→范围授权→校验→依赖复检→装配→快照绑定全链贯通;片段不登记方法消费 - 范围校验source_ids基集、服务包装、依赖锁定键分派同步;B03接口增读取来源当前指纹(架构门禁禁私表) - 集成a10032-a10036:检索知情候选/未建拒绝/撤回空/片段进快照/来源漂移失效
This commit is contained in:
parent
fea8f44327
commit
6d1f635003
@ -20905,6 +20905,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"title": "References",
|
"title": "References",
|
||||||
"default": []
|
"default": []
|
||||||
|
},
|
||||||
|
"slice_references": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array",
|
||||||
|
"title": "Slice References",
|
||||||
|
"default": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
|
|||||||
@ -241,6 +241,7 @@ class 上下文服务:
|
|||||||
含声音: bool = False,
|
含声音: bool = False,
|
||||||
含偏好: bool = False,
|
含偏好: bool = False,
|
||||||
参考版本: tuple[str, ...] = (),
|
参考版本: tuple[str, ...] = (),
|
||||||
|
片段参考: tuple[str, ...] = (),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
return 授权任务范围(
|
return 授权任务范围(
|
||||||
self.数据库,
|
self.数据库,
|
||||||
@ -256,6 +257,7 @@ class 上下文服务:
|
|||||||
含声音=含声音,
|
含声音=含声音,
|
||||||
含偏好=含偏好,
|
含偏好=含偏好,
|
||||||
参考版本=参考版本,
|
参考版本=参考版本,
|
||||||
|
片段参考=片段参考,
|
||||||
)
|
)
|
||||||
|
|
||||||
def 读取范围(self, 任务ID: str) -> dict:
|
def 读取范围(self, 任务ID: str) -> dict:
|
||||||
|
|||||||
@ -294,15 +294,26 @@ def 核对依赖(
|
|||||||
变化项.append(错误.说明)
|
变化项.append(错误.说明)
|
||||||
else:
|
else:
|
||||||
for 条 in 参考:
|
for 条 in 参考:
|
||||||
锁定.append(
|
if 条.get("载体") == "slice":
|
||||||
依赖锁定(
|
锁定.append(
|
||||||
条["version_id"],
|
依赖锁定(
|
||||||
"reference",
|
条["slice_id"],
|
||||||
str(条["version"]),
|
"reference-slice",
|
||||||
条["schema_hash"],
|
str(条["revision"]),
|
||||||
条["projection_version"],
|
条["内容哈希"],
|
||||||
|
"reference-slice-v1",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
锁定.append(
|
||||||
|
依赖锁定(
|
||||||
|
条["version_id"],
|
||||||
|
"reference",
|
||||||
|
str(条["version"]),
|
||||||
|
条["schema_hash"],
|
||||||
|
条["projection_version"],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
return 依赖状态(有效=not 变化项, 锁定依据=tuple(锁定), 变化项=tuple(变化项))
|
return 依赖状态(有效=not 变化项, 锁定依据=tuple(锁定), 变化项=tuple(变化项))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,22 @@ _身份字段 = (
|
|||||||
"projection_version",
|
"projection_version",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_片段身份字段 = (
|
||||||
|
"slice_id",
|
||||||
|
"source_id",
|
||||||
|
"revision",
|
||||||
|
"起点",
|
||||||
|
"终点",
|
||||||
|
"材料键",
|
||||||
|
"内容哈希",
|
||||||
|
"书名",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def 冻结片段身份(条: dict) -> dict:
|
||||||
|
"""场景切片冻结身份;内容哈希是来源版本漂移哨兵,材料文本不入范围。"""
|
||||||
|
return {**{键: 条[键] for 键 in _片段身份字段}, "载体": "slice"}
|
||||||
|
|
||||||
|
|
||||||
def 冻结参考身份(条: dict) -> dict:
|
def 冻结参考身份(条: dict) -> dict:
|
||||||
"""范围冻结只保留身份与哈希;材料文本不入范围,读取时经B04现取。"""
|
"""范围冻结只保留身份与哈希;材料文本不入范围,读取时经B04现取。"""
|
||||||
@ -31,29 +47,71 @@ def 校验参考节(范围: dict) -> None:
|
|||||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结参考清单必须是列表")
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结参考清单必须是列表")
|
||||||
已见: set[str] = set()
|
已见: set[str] = set()
|
||||||
for 条 in 参考:
|
for 条 in 参考:
|
||||||
if not isinstance(条, dict) or any(
|
if not isinstance(条, dict):
|
||||||
not isinstance(条.get(键), str) or not 条[键]
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结参考缺少身份")
|
||||||
for 键 in (
|
载体 = 条.get("载体", "method")
|
||||||
"version_id",
|
if 载体 == "method":
|
||||||
"method_id",
|
_校验方法参考(条, 已见)
|
||||||
"材料键",
|
elif 载体 == "slice":
|
||||||
"材料哈希",
|
_校验片段参考(条, 已见)
|
||||||
"schema_hash",
|
else:
|
||||||
"projection_version",
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"未知参考载体:{载体!r}")
|
||||||
)
|
|
||||||
):
|
|
||||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结参考缺少版本或材料身份")
|
def _校验方法参考(条: dict, 已见: set[str]) -> None:
|
||||||
if type(条.get("version")) is not int or 条["version"] < 1:
|
if any(
|
||||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结参考版本必须为正整数")
|
not isinstance(条.get(键), str) or not 条[键]
|
||||||
if 条["version_id"] in 已见 or 条["材料键"] != "参考方法:" + 条["version_id"]:
|
for 键 in (
|
||||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结参考重复或材料键不匹配")
|
"version_id",
|
||||||
已见.add(条["version_id"])
|
"method_id",
|
||||||
|
"材料键",
|
||||||
|
"材料哈希",
|
||||||
|
"schema_hash",
|
||||||
|
"projection_version",
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结参考缺少版本或材料身份")
|
||||||
|
if type(条.get("version")) is not int or 条["version"] < 1:
|
||||||
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结参考版本必须为正整数")
|
||||||
|
if 条["version_id"] in 已见 or 条["材料键"] != "参考方法:" + 条["version_id"]:
|
||||||
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结参考重复或材料键不匹配")
|
||||||
|
已见.add(条["version_id"])
|
||||||
|
|
||||||
|
|
||||||
|
def _校验片段参考(条: dict, 已见: set[str]) -> None:
|
||||||
|
if any(
|
||||||
|
not isinstance(条.get(键), str) or not 条[键]
|
||||||
|
for 键 in ("slice_id", "source_id", "材料键", "内容哈希", "书名")
|
||||||
|
):
|
||||||
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结片段参考缺少切片或来源身份")
|
||||||
|
if (
|
||||||
|
type(条.get("revision")) is not int
|
||||||
|
or 条["revision"] < 1
|
||||||
|
or type(条.get("起点")) is not int
|
||||||
|
or 条["起点"] < 0
|
||||||
|
or type(条.get("终点")) is not int
|
||||||
|
or 条["终点"] <= 条["起点"]
|
||||||
|
):
|
||||||
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结片段参考定位不合法")
|
||||||
|
if 条["slice_id"] in 已见 or 条["材料键"] != "参考片段:" + 条["slice_id"]:
|
||||||
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结片段参考重复或材料键不匹配")
|
||||||
|
已见.add(条["slice_id"])
|
||||||
|
|
||||||
|
|
||||||
def 核对范围参考(连, 范围: dict, *, 读取预算=None) -> list[dict]:
|
def 核对范围参考(连, 范围: dict, *, 读取预算=None) -> list[dict]:
|
||||||
"""按冻结身份现读当前材料并逐项核对;任一漂移即整体拒绝。"""
|
"""按冻结身份现读当前材料并逐项核对;任一漂移即整体拒绝。两类载体混合处理。"""
|
||||||
校验参考节(范围)
|
校验参考节(范围)
|
||||||
参考 = 范围.get("references")
|
参考 = 范围.get("references")
|
||||||
|
if not 参考:
|
||||||
|
return []
|
||||||
|
方法条 = [条 for 条 in 参考 if 条.get("载体", "method") == "method"]
|
||||||
|
片段条 = [条 for 条 in 参考 if 条.get("载体") == "slice"]
|
||||||
|
结果 = _核对方法参考(连, 范围, 方法条, 读取预算)
|
||||||
|
结果.extend(_核对片段参考(连, 范围["authorized_by"], 片段条))
|
||||||
|
return 结果
|
||||||
|
|
||||||
|
|
||||||
|
def _核对方法参考(连, 范围, 参考, 读取预算) -> list[dict]:
|
||||||
if not 参考:
|
if not 参考:
|
||||||
return []
|
return []
|
||||||
from muse.知识方法.接口 import 方法错误, 读取参考材料
|
from muse.知识方法.接口 import 方法错误, 读取参考材料
|
||||||
@ -83,7 +141,48 @@ def 核对范围参考(连, 范围: dict, *, 读取预算=None) -> list[dict]:
|
|||||||
return 结果
|
return 结果
|
||||||
|
|
||||||
|
|
||||||
|
def _核对片段参考(连, 范围作者, 参考) -> list[dict]:
|
||||||
|
"""场景切片现读:来源仍需 reference_reading 授权且版本内容未漂移。"""
|
||||||
|
if not 参考:
|
||||||
|
return []
|
||||||
|
from muse.资料研究.接口 import 读取场景切片, 读取来源当前指纹
|
||||||
|
|
||||||
|
当前 = {
|
||||||
|
条["slice_id"]: 条 for 条 in 读取场景切片(连, 范围作者, [条["slice_id"] for 条 in 参考])
|
||||||
|
}
|
||||||
|
按来源 = 读取来源当前指纹(连, sorted({条["source_id"] for 条 in 参考}))
|
||||||
|
结果 = []
|
||||||
|
for 条 in 参考:
|
||||||
|
现 = 当前.get(条["slice_id"])
|
||||||
|
if 现 is None:
|
||||||
|
raise 上下文错误("SOURCE_STALE", f"参考片段 {条['slice_id'][:8]} 已退出授权范围")
|
||||||
|
来源行 = 按来源.get(条["source_id"])
|
||||||
|
if (
|
||||||
|
来源行 is None
|
||||||
|
or int(来源行["当前版本"]) != 条["revision"]
|
||||||
|
or 来源行["当前哈希"] != 条["内容哈希"]
|
||||||
|
):
|
||||||
|
raise 上下文错误(
|
||||||
|
"SOURCE_STALE", f"参考片段来源 {条['书名']} 已推进新版本,参考片段需重新授权"
|
||||||
|
)
|
||||||
|
材料 = f"【参考原文·{条['书名']}】\n{现['检索文本']}"
|
||||||
|
from muse.正式变更.接口 import 固定哈希
|
||||||
|
|
||||||
|
结果.append(
|
||||||
|
{
|
||||||
|
**条,
|
||||||
|
"材料": 材料,
|
||||||
|
"材料哈希": 固定哈希([材料]),
|
||||||
|
"载体": "slice",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return 结果
|
||||||
|
|
||||||
|
|
||||||
def 参考绑定(条: dict) -> 来源绑定:
|
def 参考绑定(条: dict) -> 来源绑定:
|
||||||
|
"""方法与片段两类参考条目都构造成快照来源绑定;字段按载体取自冻结身份。"""
|
||||||
|
if 条.get("载体") == "slice":
|
||||||
|
return 来源绑定(条["slice_id"], str(条["revision"]), 条["内容哈希"], "reference-slice-v1")
|
||||||
return 来源绑定(
|
return 来源绑定(
|
||||||
条["version_id"], str(条["version"]), 条["schema_hash"], 条["projection_version"]
|
条["version_id"], str(条["version"]), 条["schema_hash"], 条["projection_version"]
|
||||||
)
|
)
|
||||||
|
|||||||
109
src/muse/上下文/场景检索.py
Normal file
109
src/muse/上下文/场景检索.py
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
"""场景检索:参考书场景切片的关键词+向量两路等权融合;与方法检索平行。
|
||||||
|
|
||||||
|
语料为 B03 当前具备 reference_reading 用途来源的切片;授权过滤在查询时 join。
|
||||||
|
候选键带 ``slice:`` 前缀,与方法版本身份键空间不混。v1 不接偏好重排。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from muse.上下文.模型 import 上下文错误
|
||||||
|
from muse.基础设施.检索.关键词 import 关键词得分
|
||||||
|
from muse.基础设施.检索.向量 import 向量得分
|
||||||
|
from muse.基础设施.检索.融合 import 倒数排名融合
|
||||||
|
from muse.基础设施.模型.嵌入 import 特征哈希嵌入器
|
||||||
|
|
||||||
|
切片数量上限 = 50
|
||||||
|
|
||||||
|
|
||||||
|
def 读取新鲜切片索引(连) -> tuple[dict[str, Any], list[dict[str, Any]]]:
|
||||||
|
"""代次与索引一致才可用;对不上即拒绝为陈旧,不默重建。"""
|
||||||
|
from psycopg.rows import dict_row
|
||||||
|
|
||||||
|
行 = (
|
||||||
|
连.cursor(row_factory=dict_row)
|
||||||
|
.execute(
|
||||||
|
"SELECT v.ledger_id, v.索引身份, v.embedder, v.slice_count, g.generation "
|
||||||
|
"FROM public.muse_source_slice_guard g "
|
||||||
|
"LEFT JOIN public.muse_source_slice_index_version v ON v.active "
|
||||||
|
"WHERE g.singleton"
|
||||||
|
)
|
||||||
|
.fetchone()
|
||||||
|
)
|
||||||
|
if 行 is None or 行["ledger_id"] is None:
|
||||||
|
raise 上下文错误("CONTEXT_INDEX_STALE", "场景切片索引尚未构建")
|
||||||
|
return dict(行), []
|
||||||
|
|
||||||
|
|
||||||
|
def 场景检索(连, 查询: str, *, 数量: int = 10) -> dict[str, Any]:
|
||||||
|
"""两路等权 RRF;只读,不写任何表。结果带书名与原文预览供知情挑选。"""
|
||||||
|
from psycopg.rows import dict_row
|
||||||
|
|
||||||
|
if not isinstance(查询, str) or not 查询.strip():
|
||||||
|
raise 上下文错误("CONTEXT_INPUT_INVALID", "场景检索需要明确的非空查询")
|
||||||
|
if type(数量) is not int or not 1 <= 数量 <= 切片数量上限:
|
||||||
|
raise 上下文错误("CONTEXT_INPUT_INVALID", f"场景检索数量必须在 1 到 {切片数量上限} 之间")
|
||||||
|
台账, _ = 读取新鲜切片索引(连)
|
||||||
|
匹配 = 台账["embedder"].rsplit("-", 2)
|
||||||
|
维度 = int(匹配[1]) if len(匹配) == 3 and 匹配[1].isdigit() else None
|
||||||
|
if 维度 is None:
|
||||||
|
raise 上下文错误("CONTEXT_INDEX_STALE", "切片索引嵌入器身份无法解析")
|
||||||
|
嵌入器 = 特征哈希嵌入器(维度)
|
||||||
|
if 嵌入器.身份 != 台账["embedder"]:
|
||||||
|
raise 上下文错误("CONTEXT_INDEX_STALE", "切片索引嵌入器与当前实现不一致")
|
||||||
|
行 = (
|
||||||
|
连.cursor(row_factory=dict_row)
|
||||||
|
.execute(
|
||||||
|
"SELECT x.slice_id, x.检索文本, i.vector, s.title, x.revision, x.起点, x.终点, "
|
||||||
|
"v.content_hash "
|
||||||
|
"FROM public.muse_source_slice x "
|
||||||
|
"JOIN public.muse_source_slice_index i ON i.slice_id=x.slice_id "
|
||||||
|
"JOIN public.muse_source s ON s.source_id=x.source_id "
|
||||||
|
"JOIN muse_source_version v ON v.source_id=x.source_id AND v.revision=x.revision "
|
||||||
|
"WHERE s.authorized_uses ? 'reference_reading' AND i.embedder=%s",
|
||||||
|
(台账["embedder"],),
|
||||||
|
)
|
||||||
|
.fetchall()
|
||||||
|
)
|
||||||
|
if not 行:
|
||||||
|
return {
|
||||||
|
"结果": [],
|
||||||
|
"索引身份": 台账["索引身份"],
|
||||||
|
"empty_reason": "empty_index",
|
||||||
|
"说明": "当前没有可检索的场景切片",
|
||||||
|
}
|
||||||
|
键集 = {f"slice:{r['slice_id']}": r for r in 行}
|
||||||
|
语料 = {键: r["检索文本"] for 键, r in 键集.items()}
|
||||||
|
词分 = 关键词得分(查询, 语料)
|
||||||
|
量分 = 向量得分(嵌入器.嵌入(查询), {键: r["vector"] for 键, r in 键集.items()})
|
||||||
|
融合 = 倒数排名融合([词分, 量分], [1.0, 1.0])
|
||||||
|
排名 = sorted(融合.items(), key=lambda kv: -kv[1])[:数量]
|
||||||
|
结果 = []
|
||||||
|
for 键, 分 in 排名:
|
||||||
|
r = 键集[键]
|
||||||
|
预览 = r["检索文本"][:80]
|
||||||
|
结果.append(
|
||||||
|
{
|
||||||
|
"slice_id": str(r["slice_id"]),
|
||||||
|
"key": 键,
|
||||||
|
"书名": r["title"],
|
||||||
|
"revision": int(r["revision"]),
|
||||||
|
"起点": int(r["起点"]),
|
||||||
|
"终点": int(r["终点"]),
|
||||||
|
"内容哈希": r["content_hash"],
|
||||||
|
"预览": 预览,
|
||||||
|
"关键词得分": 词分.get(键, 0.0),
|
||||||
|
"向量得分": 量分.get(键, 0.0),
|
||||||
|
"融合得分": 分,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"结果": 结果,
|
||||||
|
"索引身份": 台账["索引身份"],
|
||||||
|
"embedder": 台账["embedder"],
|
||||||
|
"empty_reason": None if 结果 else "no_match",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["场景检索", "读取新鲜切片索引", "切片数量上限"]
|
||||||
@ -19,6 +19,7 @@ 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 (
|
||||||
回放,
|
回放,
|
||||||
@ -72,6 +73,7 @@ __all__ = [
|
|||||||
"核对回放字段策略",
|
"核对回放字段策略",
|
||||||
"保护模型发送",
|
"保护模型发送",
|
||||||
"混合检索",
|
"混合检索",
|
||||||
|
"场景检索",
|
||||||
"合并信号表",
|
"合并信号表",
|
||||||
"权重表",
|
"权重表",
|
||||||
"偏好信号",
|
"偏好信号",
|
||||||
|
|||||||
@ -21,18 +21,13 @@ from muse.上下文.模型 import 上下文错误
|
|||||||
from muse.上下文.索引维护 import 索引字节上限, 索引数量上限, 读取新鲜索引
|
from muse.上下文.索引维护 import 索引字节上限, 索引数量上限, 读取新鲜索引
|
||||||
from muse.基础设施.检索.关键词 import 关键词得分
|
from muse.基础设施.检索.关键词 import 关键词得分
|
||||||
from muse.基础设施.检索.向量 import 向量得分
|
from muse.基础设施.检索.向量 import 向量得分
|
||||||
|
from muse.基础设施.检索.融合 import 倒数排名融合
|
||||||
from muse.基础设施.模型.嵌入 import 嵌入器
|
from muse.基础设施.模型.嵌入 import 嵌入器
|
||||||
from muse.知识方法.接口 import 方法错误, 读取方法检索候选, 读取方法检索身份, 读取生效绑定
|
from muse.知识方法.接口 import 方法错误, 读取方法检索候选, 读取方法检索身份, 读取生效绑定
|
||||||
|
|
||||||
|
|
||||||
def _rrf(得分表列: list[dict[str, float]], 权重: list[float]) -> dict[str, float]:
|
def _rrf(得分表列: list[dict[str, float]], 权重: list[float]) -> dict[str, float]:
|
||||||
融合: dict[str, float] = {}
|
return 倒数排名融合(得分表列, 权重)
|
||||||
常数 = 60.0
|
|
||||||
for 得分表, 权 in zip(得分表列, 权重, strict=True):
|
|
||||||
排名 = sorted(得分表.items(), key=lambda kv: -kv[1])
|
|
||||||
for 位次, (键, _) in enumerate(排名):
|
|
||||||
融合[键] = 融合.get(键, 0.0) + 权 / (常数 + 位次 + 1)
|
|
||||||
return 融合
|
|
||||||
|
|
||||||
|
|
||||||
def _偏好呈现(信号: 偏好信号 | None) -> dict[str, Any]:
|
def _偏好呈现(信号: 偏好信号 | None) -> dict[str, Any]:
|
||||||
|
|||||||
@ -52,14 +52,19 @@ def 授权任务范围(
|
|||||||
含声音: bool = False,
|
含声音: bool = False,
|
||||||
含偏好: bool = False,
|
含偏好: bool = False,
|
||||||
参考版本: tuple[str, ...] = (),
|
参考版本: tuple[str, ...] = (),
|
||||||
|
片段参考: tuple[str, ...] = (),
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""按作者身份冻结历史文稿、目标章细纲与事实系统版本;两种用途随范围绑定。"""
|
"""按作者身份冻结历史文稿、目标章细纲与事实系统版本;两种用途随范围绑定。"""
|
||||||
if not 作品ID:
|
if not 作品ID:
|
||||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "任务范围必须绑定明确作品")
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", "任务范围必须绑定明确作品")
|
||||||
if isinstance(参考版本, str) or not isinstance(参考版本, tuple):
|
if isinstance(参考版本, str) or not isinstance(参考版本, tuple):
|
||||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"参考版本必须以元组显式声明:{参考版本!r}")
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"参考版本必须以元组显式声明:{参考版本!r}")
|
||||||
if len(参考版本) > 8:
|
if isinstance(片段参考, str) or not isinstance(片段参考, tuple):
|
||||||
raise 上下文错误("CONTEXT_SCOPE_INVALID", "参考方法版本不超过8个")
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"片段参考必须以元组显式声明:{片段参考!r}")
|
||||||
|
if len(参考版本) + len(片段参考) > 8:
|
||||||
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", "参考总数(方法加片段)不超过8个")
|
||||||
|
if len(set(片段参考)) != len(片段参考):
|
||||||
|
raise 上下文错误("CONTEXT_SCOPE_INVALID", "片段参考不能重复")
|
||||||
if 身份.作者 is None or 身份.用途 is not 数据库.用途:
|
if 身份.作者 is None or 身份.用途 is not 数据库.用途:
|
||||||
raise 上下文错误("SCOPE_DENIED", "任务范围授权需要与装配用途一致的作者身份")
|
raise 上下文错误("SCOPE_DENIED", "任务范围授权需要与装配用途一致的作者身份")
|
||||||
校验截止(截止)
|
校验截止(截止)
|
||||||
@ -136,7 +141,7 @@ def 授权任务范围(
|
|||||||
from muse.上下文.参考材料 import 冻结参考身份
|
from muse.上下文.参考材料 import 冻结参考身份
|
||||||
from muse.知识方法.接口 import 读取参考材料
|
from muse.知识方法.接口 import 读取参考材料
|
||||||
|
|
||||||
参考 = [
|
参考.extend(
|
||||||
冻结参考身份(条)
|
冻结参考身份(条)
|
||||||
for 条 in 读取参考材料(
|
for 条 in 读取参考材料(
|
||||||
连,
|
连,
|
||||||
@ -146,10 +151,24 @@ def 授权任务范围(
|
|||||||
运行用途=运行用途,
|
运行用途=运行用途,
|
||||||
读取预算=用量.预留来源,
|
读取预算=用量.预留来源,
|
||||||
)
|
)
|
||||||
]
|
)
|
||||||
|
if 片段参考:
|
||||||
|
from muse.上下文.参考材料 import 冻结片段身份
|
||||||
|
from muse.资料研究.接口 import 读取场景切片
|
||||||
|
|
||||||
|
切片行 = 读取场景切片(连, 身份.作者, list(片段参考))
|
||||||
|
按身份 = {条["slice_id"]: 条 for 条 in 切片行}
|
||||||
|
for 身份键 in 片段参考:
|
||||||
|
现 = 按身份.get(身份键)
|
||||||
|
if 现 is None:
|
||||||
|
raise 上下文错误("SOURCE_STALE", f"参考片段 {身份键[:8]} 不存在或未授权")
|
||||||
|
参考.extend(
|
||||||
|
冻结片段身份({**现, "材料键": "参考片段:" + 现["slice_id"]})
|
||||||
|
for 现 in (按身份[键] for 键 in 片段参考)
|
||||||
|
)
|
||||||
来源集 = [d["document_id"] for d in 文稿]
|
来源集 = [d["document_id"] for d in 文稿]
|
||||||
来源集.extend(x["preference_id"] for x in 偏好)
|
来源集.extend(x["preference_id"] for x in 偏好)
|
||||||
来源集.extend(条["version_id"] for 条 in 参考)
|
来源集.extend(条.get("version_id") or 条["slice_id"] for 条 in 参考)
|
||||||
if 声音 is not None:
|
if 声音 is not None:
|
||||||
来源集.append(声音["version_id"])
|
来源集.append(声音["version_id"])
|
||||||
来源集.extend(条["binding_id"] for 条 in 方法)
|
来源集.extend(条["binding_id"] for 条 in 方法)
|
||||||
|
|||||||
@ -78,23 +78,31 @@ def _校验冻结输入(
|
|||||||
条["projection_version"],
|
条["projection_version"],
|
||||||
):
|
):
|
||||||
raise 上下文错误("SOURCE_STALE", "方法版本或材料哈希与授权不一致")
|
raise 上下文错误("SOURCE_STALE", "方法版本或材料哈希与授权不一致")
|
||||||
参考集 = {条["version_id"]: 条 for 条 in 范围.get("references", [])}
|
参考集 = {(条.get("version_id") or 条["slice_id"]): 条 for 条 in 范围.get("references", [])}
|
||||||
|
方法参考 = {键: 条 for 键, 条 in 参考集.items() if 条.get("载体", "method") == "method"}
|
||||||
|
片段参考 = {键: 条 for 键, 条 in 参考集.items() if 条.get("载体") == "slice"}
|
||||||
参考键 = {条["材料键"] for 条 in 参考集.values()}
|
参考键 = {条["材料键"] for 条 in 参考集.values()}
|
||||||
if any(键.startswith("参考方法:") and 键 not in 参考键 for 键 in 装配.入选来源):
|
if any(键.startswith("参考方法:") and 键 not in 参考键 for 键 in 装配.入选来源):
|
||||||
raise 上下文错误("SCOPE_DENIED", "装配包含未授权参考方法材料")
|
raise 上下文错误("SCOPE_DENIED", "装配包含未授权参考方法材料")
|
||||||
|
if any(键.startswith("参考片段:") and 键 not in 参考键 for 键 in 装配.入选来源):
|
||||||
|
raise 上下文错误("SCOPE_DENIED", "装配包含未授权参考片段材料")
|
||||||
入选参考 = {vid for vid, 条 in 参考集.items() if 条["材料键"] in 装配.入选来源}
|
入选参考 = {vid for vid, 条 in 参考集.items() if 条["材料键"] in 装配.入选来源}
|
||||||
实绑参考 = {条.来源ID for 条 in 绑定 if 条.来源ID in 参考集}
|
实绑参考 = {条.来源ID for 条 in 绑定 if 条.来源ID in 参考集}
|
||||||
if 入选参考 != 实绑参考:
|
if 入选参考 != 实绑参考:
|
||||||
raise 上下文错误("CONTEXT_INPUT_INVALID", "入选参考与冻结版本绑定不一致")
|
raise 上下文错误("CONTEXT_INPUT_INVALID", "入选参考与冻结版本绑定不一致")
|
||||||
for 绑 in 绑定:
|
for 绑 in 绑定:
|
||||||
if 绑.来源ID in 参考集:
|
if 绑.来源ID in 方法参考:
|
||||||
条 = 参考集[绑.来源ID]
|
条 = 方法参考[绑.来源ID]
|
||||||
if (绑.数据版本, 绑.结构哈希, 绑.投影版本) != (
|
if (绑.数据版本, 绑.结构哈希, 绑.投影版本) != (
|
||||||
str(条["version"]),
|
str(条["version"]),
|
||||||
条["schema_hash"],
|
条["schema_hash"],
|
||||||
条["projection_version"],
|
条["projection_version"],
|
||||||
):
|
):
|
||||||
raise 上下文错误("SOURCE_STALE", "参考版本或材料哈希与授权不一致")
|
raise 上下文错误("SOURCE_STALE", "参考版本或材料哈希与授权不一致")
|
||||||
|
if 绑.来源ID in 片段参考:
|
||||||
|
条 = 片段参考[绑.来源ID]
|
||||||
|
if 绑.结构哈希 != 条["内容哈希"] or 绑.数据版本 != str(条["revision"]):
|
||||||
|
raise 上下文错误("SOURCE_STALE", "参考片段来源版本与授权不一致")
|
||||||
目录源 = 范围.get("directory_source")
|
目录源 = 范围.get("directory_source")
|
||||||
if 目录源:
|
if 目录源:
|
||||||
for 项 in 绑定:
|
for 项 in 绑定:
|
||||||
@ -220,7 +228,7 @@ def 恢复冻结范围(数据库: 数据库工厂, 任务: 任务快照) -> dict
|
|||||||
方法ID.add(条["binding_id"])
|
方法ID.add(条["binding_id"])
|
||||||
基础 = {项["document_id"] for 项 in 文稿} | 方法ID
|
基础 = {项["document_id"] for 项 in 文稿} | 方法ID
|
||||||
基础.update(x["preference_id"] for x in 范围.get("preferences", []))
|
基础.update(x["preference_id"] for x in 范围.get("preferences", []))
|
||||||
基础.update(x["version_id"] for x in 范围.get("references", []))
|
基础.update(x.get("version_id") or x["slice_id"] for x in 范围.get("references", []))
|
||||||
if 范围.get("voice") is not None:
|
if 范围.get("voice") is not None:
|
||||||
基础.add(范围["voice"]["version_id"])
|
基础.add(范围["voice"]["version_id"])
|
||||||
if 目录源 is not None:
|
if 目录源 is not None:
|
||||||
|
|||||||
16
src/muse/基础设施/检索/融合.py
Normal file
16
src/muse/基础设施/检索/融合.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
"""倒数排名融合(RRF):多路得分表的加权融合;纯函数。"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def 倒数排名融合(得分表列: list[dict[str, float]], 权重: list[float]) -> dict[str, float]:
|
||||||
|
融合: dict[str, float] = {}
|
||||||
|
常数 = 60.0
|
||||||
|
for 得分表, 权 in zip(得分表列, 权重, strict=True):
|
||||||
|
排名 = sorted(得分表.items(), key=lambda kv: -kv[1])
|
||||||
|
for 位次, (键, _) in enumerate(排名):
|
||||||
|
融合[键] = 融合.get(键, 0.0) + 权 / (常数 + 位次 + 1)
|
||||||
|
return 融合
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["倒数排名融合"]
|
||||||
@ -88,6 +88,7 @@ class 选段写作授权:
|
|||||||
selections: tuple[原文选段, ...]
|
selections: tuple[原文选段, ...]
|
||||||
protected_ranges: tuple[原文选段, ...] = ()
|
protected_ranges: tuple[原文选段, ...] = ()
|
||||||
references: tuple[str, ...] = ()
|
references: tuple[str, ...] = ()
|
||||||
|
slice_references: tuple[str, ...] = ()
|
||||||
|
|
||||||
def __post_init__(self):
|
def __post_init__(self):
|
||||||
if self.revision < 1 or not self.purpose.strip() or not 1 <= len(self.selections) <= 32:
|
if self.revision < 1 or not self.purpose.strip() or not 1 <= len(self.selections) <= 32:
|
||||||
@ -96,12 +97,16 @@ class 选段写作授权:
|
|||||||
raise 正文错误("选段改写需明确作品、章节及分支")
|
raise 正文错误("选段改写需明确作品、章节及分支")
|
||||||
if len({s.selection_id for s in self.selections}) != len(self.selections):
|
if len({s.selection_id for s in self.selections}) != len(self.selections):
|
||||||
raise 正文错误("圈选不能重复")
|
raise 正文错误("圈选不能重复")
|
||||||
if len(self.references) > 8:
|
if len(self.references) + len(self.slice_references) > 8:
|
||||||
raise 正文错误("参考方法不超过8个")
|
raise 正文错误("参考总数(方法加片段)不超过8个")
|
||||||
if len(set(self.references)) != len(self.references):
|
if len(set(self.references)) != len(self.references):
|
||||||
raise 正文错误("参考方法版本不能重复")
|
raise 正文错误("参考方法版本不能重复")
|
||||||
if not all(type(r) is str and r.strip() for r in self.references):
|
if not all(type(r) is str and r.strip() for r in self.references):
|
||||||
raise 正文错误("参考方法需要非空版本身份")
|
raise 正文错误("参考方法需要非空版本身份")
|
||||||
|
if len(set(self.slice_references)) != len(self.slice_references) or not all(
|
||||||
|
type(r) is str and r.strip() for r in self.slice_references
|
||||||
|
):
|
||||||
|
raise 正文错误("片段参考需非空切片身份且不重复")
|
||||||
|
|
||||||
|
|
||||||
def 准备选段改写(连, 作者, 授权: 选段写作授权):
|
def 准备选段改写(连, 作者, 授权: 选段写作授权):
|
||||||
|
|||||||
@ -113,6 +113,7 @@ def 发起模型修订(app, 身份, 命令ID, 授权: 模型修订授权 | 选
|
|||||||
含声音=True,
|
含声音=True,
|
||||||
含偏好=True,
|
含偏好=True,
|
||||||
参考版本=tuple(授权.references) if isinstance(授权, 选段写作授权) else (),
|
参考版本=tuple(授权.references) if isinstance(授权, 选段写作授权) else (),
|
||||||
|
片段参考=tuple(授权.slice_references) if isinstance(授权, 选段写作授权) else (),
|
||||||
)
|
)
|
||||||
策略 = 角色策略目录.从发布包()
|
策略 = 角色策略目录.从发布包()
|
||||||
request = 任务请求(
|
request = 任务请求(
|
||||||
@ -211,10 +212,11 @@ def 登记审校修订(登记, app, 计价):
|
|||||||
snap = 上下文.冻结上下文(ctx.任务.任务ID, 组装, tuple(bindings))
|
snap = 上下文.冻结上下文(ctx.任务.任务ID, 组装, tuple(bindings))
|
||||||
# 只把装配入选的参考写入检查点:供真实交付后登记方法消费(kind=review)。
|
# 只把装配入选的参考写入检查点:供真实交付后登记方法消费(kind=review)。
|
||||||
# 未入选参考未进入模型输入,不登记消费;旧任务检查点无该键,向后兼容为空。
|
# 未入选参考未进入模型输入,不登记消费;旧任务检查点无该键,向后兼容为空。
|
||||||
|
# 只收方法载体:片段参考不是方法版本,不登记方法消费(见 B06 契约)。
|
||||||
入选参考 = [
|
入选参考 = [
|
||||||
{"version_id": 条["version_id"], "材料哈希": 条["材料哈希"]}
|
{"version_id": 条["version_id"], "材料哈希": 条["材料哈希"]}
|
||||||
for 条 in 参考材料
|
for 条 in 参考材料
|
||||||
if 条["材料键"] in 组装.入选来源
|
if 条.get("载体", "method") == "method" and 条["材料键"] in 组装.入选来源
|
||||||
]
|
]
|
||||||
return 步骤结果(
|
return 步骤结果(
|
||||||
{"snapshot_id": snap.快照ID},
|
{"snapshot_id": snap.快照ID},
|
||||||
|
|||||||
@ -183,7 +183,7 @@ def 读取场景切片(连, 作者: str, slice_ids: list[str]) -> list[dict[str,
|
|||||||
资料存储(连)
|
资料存储(连)
|
||||||
.查询(
|
.查询(
|
||||||
"SELECT x.slice_id, x.source_id, x.revision, x.起点, x.终点, x.检索文本, "
|
"SELECT x.slice_id, x.source_id, x.revision, x.起点, x.终点, x.检索文本, "
|
||||||
"v.content_hash, s.title, s.authorized_uses "
|
"v.content_hash, v.import_result, s.title, s.authorized_uses "
|
||||||
"FROM muse_source_slice x "
|
"FROM muse_source_slice x "
|
||||||
"JOIN muse_source_version v ON v.source_id=x.source_id AND v.revision=x.revision "
|
"JOIN muse_source_version v ON v.source_id=x.source_id AND v.revision=x.revision "
|
||||||
"JOIN muse_source s ON s.source_id=x.source_id "
|
"JOIN muse_source s ON s.source_id=x.source_id "
|
||||||
@ -205,10 +205,35 @@ def 读取场景切片(连, 作者: str, slice_ids: list[str]) -> list[dict[str,
|
|||||||
"用途": 条["authorized_uses"],
|
"用途": 条["authorized_uses"],
|
||||||
}
|
}
|
||||||
for 条 in 行
|
for 条 in 行
|
||||||
if 条["authorized_uses"] is not None and "reference_reading" in 条["authorized_uses"]
|
if 条["authorized_uses"] is not None
|
||||||
|
and "reference_reading" in 条["authorized_uses"]
|
||||||
|
and (条["import_result"] or {}).get("导入者") == 作者
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def 读取来源当前指纹(连, source_ids: list[str]) -> dict[str, dict[str, Any]]:
|
||||||
|
"""来源→当前版本号与内容哈希;供参考链路的版本漂移哨兵。"""
|
||||||
|
if not source_ids:
|
||||||
|
return {}
|
||||||
|
占位 = ",".join(["%s"] * len(source_ids))
|
||||||
|
行 = (
|
||||||
|
资料存储(连)
|
||||||
|
.查询(
|
||||||
|
"SELECT s.source_id, "
|
||||||
|
"(SELECT MAX(v.revision) FROM muse_source_version v "
|
||||||
|
"WHERE v.source_id=s.source_id) AS 当前版本, "
|
||||||
|
"(SELECT v2.content_hash FROM muse_source_version v2 WHERE v2.source_id=s.source_id "
|
||||||
|
"ORDER BY v2.revision DESC LIMIT 1) AS 当前哈希 "
|
||||||
|
f"FROM muse_source s WHERE s.source_id IN ({占位})",
|
||||||
|
tuple(source_ids),
|
||||||
|
)
|
||||||
|
.fetchall()
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
str(行["source_id"]): {"当前版本": 行["当前版本"], "当前哈希": 行["当前哈希"]} for 行 in 行
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"目标窗长",
|
"目标窗长",
|
||||||
"最短窗长",
|
"最短窗长",
|
||||||
@ -218,4 +243,5 @@ __all__ = [
|
|||||||
"计算切片",
|
"计算切片",
|
||||||
"构建场景索引",
|
"构建场景索引",
|
||||||
"读取场景切片",
|
"读取场景切片",
|
||||||
|
"读取来源当前指纹",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from muse.资料研究.分析保存 import 参考分析请求, 呈现分析, 登
|
|||||||
from muse.资料研究.分析版本 import 承接历史分析, 版本台账, 版本身份, 登记模型分析, 读取版本状态
|
from muse.资料研究.分析版本 import 承接历史分析, 版本台账, 版本身份, 登记模型分析, 读取版本状态
|
||||||
from muse.资料研究.分章切窗 import 切窗, 漂移检查, 窗口, 窗口载荷, 窗计划哈希, 覆盖核对
|
from muse.资料研究.分章切窗 import 切窗, 漂移检查, 窗口, 窗口载荷, 窗计划哈希, 覆盖核对
|
||||||
from muse.资料研究.原文版本 import 读取证据
|
from muse.资料研究.原文版本 import 读取证据
|
||||||
from muse.资料研究.场景切片 import 构建场景索引, 计算切片, 读取场景切片
|
from muse.资料研究.场景切片 import 构建场景索引, 计算切片, 读取场景切片, 读取来源当前指纹
|
||||||
from muse.资料研究.存储 import 资料存储
|
from muse.资料研究.存储 import 资料存储
|
||||||
from muse.资料研究.实体归并 import 归并, 收集实体
|
from muse.资料研究.实体归并 import 归并, 收集实体
|
||||||
from muse.资料研究.拆书分析 import 执行窗口提取
|
from muse.资料研究.拆书分析 import 执行窗口提取
|
||||||
@ -238,4 +238,5 @@ __all__ = [
|
|||||||
"构建场景索引",
|
"构建场景索引",
|
||||||
"计算切片",
|
"计算切片",
|
||||||
"读取场景切片",
|
"读取场景切片",
|
||||||
|
"读取来源当前指纹",
|
||||||
]
|
]
|
||||||
|
|||||||
167
tests/用例清单.json
167
tests/用例清单.json
@ -27745,6 +27745,75 @@
|
|||||||
"数据库"
|
"数据库"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"case_id": "NC-w32-a10035",
|
||||||
|
"environment": "隔离PG与合成HTTP提供方",
|
||||||
|
"given": "带片段参考的选段改写授权且候选真实交付",
|
||||||
|
"when": "发起并执行受控修订流程",
|
||||||
|
"then": [
|
||||||
|
"片段原文材料入选装配并绑定进快照,材料键带参考片段前缀",
|
||||||
|
"写手输入携带片段材料;不登记方法消费(片段非方法版本)"
|
||||||
|
],
|
||||||
|
"contract": "docs/系统架构/新版设计/模块设计/B06-审校修订.md#参考改写选段改写链路扩展",
|
||||||
|
"file": "tests/集成/test_参考改写.py",
|
||||||
|
"symbol": "test_片段参考材料进快照__a10035",
|
||||||
|
"parameter_ids": [],
|
||||||
|
"node_ids": [
|
||||||
|
"tests/集成/test_参考改写.py::test_片段参考材料进快照__a10035"
|
||||||
|
],
|
||||||
|
"fixtures": [
|
||||||
|
"request",
|
||||||
|
"tmp_path",
|
||||||
|
"tmp_path_factory",
|
||||||
|
"内置种子方案",
|
||||||
|
"内置结构测试库",
|
||||||
|
"参考环境",
|
||||||
|
"数据库底座",
|
||||||
|
"测试资源接缝",
|
||||||
|
"源码资源",
|
||||||
|
"片段环境",
|
||||||
|
"生成环境",
|
||||||
|
"离线防护",
|
||||||
|
"隔离数据库URL"
|
||||||
|
],
|
||||||
|
"markers": [
|
||||||
|
"数据库"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case_id": "NC-w32-a10036",
|
||||||
|
"environment": "隔离PG与合成HTTP提供方",
|
||||||
|
"given": "已冻结的片段参考改写任务",
|
||||||
|
"when": "参考书原文推进新版本(内容哈希漂移)",
|
||||||
|
"then": [
|
||||||
|
"快照标注失效且要求重新授权"
|
||||||
|
],
|
||||||
|
"contract": "docs/系统架构/新版设计/模块设计/B06-审校修订.md#参考改写选段改写链路扩展",
|
||||||
|
"file": "tests/集成/test_参考改写.py",
|
||||||
|
"symbol": "test_片段来源漂移使快照失效__a10036",
|
||||||
|
"parameter_ids": [],
|
||||||
|
"node_ids": [
|
||||||
|
"tests/集成/test_参考改写.py::test_片段来源漂移使快照失效__a10036"
|
||||||
|
],
|
||||||
|
"fixtures": [
|
||||||
|
"request",
|
||||||
|
"tmp_path",
|
||||||
|
"tmp_path_factory",
|
||||||
|
"内置种子方案",
|
||||||
|
"内置结构测试库",
|
||||||
|
"参考环境",
|
||||||
|
"数据库底座",
|
||||||
|
"测试资源接缝",
|
||||||
|
"源码资源",
|
||||||
|
"片段环境",
|
||||||
|
"生成环境",
|
||||||
|
"离线防护",
|
||||||
|
"隔离数据库URL"
|
||||||
|
],
|
||||||
|
"markers": [
|
||||||
|
"数据库"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"case_id": "NC-w33-a10025",
|
"case_id": "NC-w33-a10025",
|
||||||
"environment": "离线纯函数",
|
"environment": "离线纯函数",
|
||||||
@ -27938,6 +28007,104 @@
|
|||||||
"数据库"
|
"数据库"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"case_id": "NC-w33-a10032",
|
||||||
|
"environment": "隔离PG与生成环境",
|
||||||
|
"given": "已构建的授权来源切片索引",
|
||||||
|
"when": "以场景相关查询执行场景检索",
|
||||||
|
"then": [
|
||||||
|
"两路融合返回切片:带书名、预览与双路得分;授权来源独占结果"
|
||||||
|
],
|
||||||
|
"contract": "docs/系统架构/新版设计/模块设计/B09-上下文.md#场景片段检索",
|
||||||
|
"file": "tests/集成/test_场景切片.py",
|
||||||
|
"symbol": "test_场景检索返回知情候选__a10032",
|
||||||
|
"parameter_ids": [],
|
||||||
|
"node_ids": [
|
||||||
|
"tests/集成/test_场景切片.py::test_场景检索返回知情候选__a10032"
|
||||||
|
],
|
||||||
|
"fixtures": [
|
||||||
|
"request",
|
||||||
|
"tmp_path",
|
||||||
|
"tmp_path_factory",
|
||||||
|
"内置种子方案",
|
||||||
|
"内置结构测试库",
|
||||||
|
"切片环境",
|
||||||
|
"数据库底座",
|
||||||
|
"测试资源接缝",
|
||||||
|
"源码资源",
|
||||||
|
"生成环境",
|
||||||
|
"离线防护",
|
||||||
|
"隔离数据库URL"
|
||||||
|
],
|
||||||
|
"markers": [
|
||||||
|
"数据库"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case_id": "NC-w33-a10033",
|
||||||
|
"environment": "隔离PG与生成环境",
|
||||||
|
"given": "尚未构建切片索引的库",
|
||||||
|
"when": "执行场景检索",
|
||||||
|
"then": [
|
||||||
|
"拒绝为索引陈旧,不静默返回空结果"
|
||||||
|
],
|
||||||
|
"contract": "docs/系统架构/新版设计/模块设计/B09-上下文.md#场景片段检索",
|
||||||
|
"file": "tests/集成/test_场景切片.py",
|
||||||
|
"symbol": "test_未建索引拒绝__a10033",
|
||||||
|
"parameter_ids": [],
|
||||||
|
"node_ids": [
|
||||||
|
"tests/集成/test_场景切片.py::test_未建索引拒绝__a10033"
|
||||||
|
],
|
||||||
|
"fixtures": [
|
||||||
|
"request",
|
||||||
|
"tmp_path",
|
||||||
|
"tmp_path_factory",
|
||||||
|
"内置种子方案",
|
||||||
|
"内置结构测试库",
|
||||||
|
"数据库底座",
|
||||||
|
"测试资源接缝",
|
||||||
|
"源码资源",
|
||||||
|
"生成环境",
|
||||||
|
"离线防护",
|
||||||
|
"隔离数据库URL"
|
||||||
|
],
|
||||||
|
"markers": [
|
||||||
|
"数据库"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"case_id": "NC-w33-a10034",
|
||||||
|
"environment": "隔离PG与生成环境",
|
||||||
|
"given": "授权来源用途被撤回后的切片索引",
|
||||||
|
"when": "执行场景检索",
|
||||||
|
"then": [
|
||||||
|
"结果为空且说明可读,不扩大为索引不可用"
|
||||||
|
],
|
||||||
|
"contract": "docs/系统架构/新版设计/模块设计/B09-上下文.md#场景片段检索",
|
||||||
|
"file": "tests/集成/test_场景切片.py",
|
||||||
|
"symbol": "test_用途撤回后检索为空__a10034",
|
||||||
|
"parameter_ids": [],
|
||||||
|
"node_ids": [
|
||||||
|
"tests/集成/test_场景切片.py::test_用途撤回后检索为空__a10034"
|
||||||
|
],
|
||||||
|
"fixtures": [
|
||||||
|
"request",
|
||||||
|
"tmp_path",
|
||||||
|
"tmp_path_factory",
|
||||||
|
"内置种子方案",
|
||||||
|
"内置结构测试库",
|
||||||
|
"切片环境",
|
||||||
|
"数据库底座",
|
||||||
|
"测试资源接缝",
|
||||||
|
"源码资源",
|
||||||
|
"生成环境",
|
||||||
|
"离线防护",
|
||||||
|
"隔离数据库URL"
|
||||||
|
],
|
||||||
|
"markers": [
|
||||||
|
"数据库"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"case_id": "NC-work-current-binding",
|
"case_id": "NC-work-current-binding",
|
||||||
"environment": "隔离 PostgreSQL;结构升级为明确的已完成状态夹具,不代替作者升级旅程",
|
"environment": "隔离 PostgreSQL;结构升级为明确的已完成状态夹具,不代替作者升级旅程",
|
||||||
|
|||||||
@ -268,3 +268,113 @@ def test_无参考修订不登记消费__a10024(参考环境):
|
|||||||
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s", (tid,)
|
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s", (tid,)
|
||||||
).fetchone()[0]
|
).fetchone()[0]
|
||||||
assert count == 0
|
assert count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def 片段环境(参考环境):
|
||||||
|
env = 参考环境
|
||||||
|
from muse.上下文.接口 import 场景检索
|
||||||
|
from muse.资料研究.接口 import 构建场景索引, 资料服务
|
||||||
|
|
||||||
|
with env["装配"].要求数据库().连接() as 连:
|
||||||
|
资料服务().导入(
|
||||||
|
连,
|
||||||
|
env["作者"].作者,
|
||||||
|
导入请求(
|
||||||
|
kind="reference",
|
||||||
|
title="打斗场景参考书",
|
||||||
|
origin="seed://slice-rewrite",
|
||||||
|
content="机甲的刀锋嗡鸣着划过装甲。\n火花在真空中凝成细线。\n" * 300,
|
||||||
|
authorized_uses=("reference_reading",),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
构建场景索引(连, env["作者"].作者)
|
||||||
|
env["片段"] = 场景检索(连, "刀锋划过装甲", 数量=1)["结果"][0]
|
||||||
|
env["授权"] = replace(env["授权"], references=(), slice_references=(env["片段"]["slice_id"],))
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.case_id(
|
||||||
|
"NC-w32-a10035",
|
||||||
|
environment="隔离PG与合成HTTP提供方",
|
||||||
|
given="带片段参考的选段改写授权且候选真实交付",
|
||||||
|
when="发起并执行受控修订流程",
|
||||||
|
then=[
|
||||||
|
"片段原文材料入选装配并绑定进快照,材料键带参考片段前缀",
|
||||||
|
"写手输入携带片段材料;不登记方法消费(片段非方法版本)",
|
||||||
|
],
|
||||||
|
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md#参考改写选段改写链路扩展",
|
||||||
|
)
|
||||||
|
def test_片段参考材料进快照__a10035(片段环境):
|
||||||
|
env = 片段环境
|
||||||
|
tid = 修订测试.发起(env, command="slice-revision-1")
|
||||||
|
env["剧本"].append(
|
||||||
|
{
|
||||||
|
"类型": "文本",
|
||||||
|
"文本": {
|
||||||
|
"edits": [
|
||||||
|
{
|
||||||
|
"selection_id": env["选段"].selection_id,
|
||||||
|
"action": "replace",
|
||||||
|
"replacement": "刀锋擦过装甲,反震顺着掌心爬上来。",
|
||||||
|
"reason": "合成片段参考改写",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
task = 修订测试.跑(env, tid)
|
||||||
|
assert task.状态 == 任务状态.已完成
|
||||||
|
输入 = json.loads(env["收到"][-1]["input"])
|
||||||
|
assert "参考原文·打斗场景参考书" in 输入["装配材料"]
|
||||||
|
|
||||||
|
快照 = env["装配"].要求上下文().读取快照(_快照ID(task))
|
||||||
|
assert 快照.失效说明 is None
|
||||||
|
键 = "参考片段:" + env["片段"]["slice_id"]
|
||||||
|
assert 键 in 快照.装配["入选"]
|
||||||
|
assert any(
|
||||||
|
str(绑.来源ID) == env["片段"]["slice_id"] and 绑.结构哈希 == env["片段"]["内容哈希"]
|
||||||
|
for 绑 in 快照.来源绑定
|
||||||
|
)
|
||||||
|
# 片段不是方法版本:不登记方法消费。
|
||||||
|
with env["装配"].要求数据库().连接(只读=True) as 连:
|
||||||
|
count = 连.execute(
|
||||||
|
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s", (tid,)
|
||||||
|
).fetchone()[0]
|
||||||
|
assert count == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.case_id(
|
||||||
|
"NC-w32-a10036",
|
||||||
|
environment="隔离PG与合成HTTP提供方",
|
||||||
|
given="已冻结的片段参考改写任务",
|
||||||
|
when="参考书原文推进新版本(内容哈希漂移)",
|
||||||
|
then=["快照标注失效且要求重新授权"],
|
||||||
|
contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md#参考改写选段改写链路扩展",
|
||||||
|
)
|
||||||
|
def test_片段来源漂移使快照失效__a10036(片段环境):
|
||||||
|
env = 片段环境
|
||||||
|
tid = 修订测试.发起(env, command="slice-revision-drift")
|
||||||
|
svc = env["装配"].任务运行
|
||||||
|
claim = svc.领取步骤("revision-worker", ["revision.freeze"])
|
||||||
|
assert claim is not None
|
||||||
|
svc.执行一步(claim)
|
||||||
|
快照ID = _快照ID(svc.读取任务(tid))
|
||||||
|
|
||||||
|
from muse.资料研究.接口 import 资料服务
|
||||||
|
|
||||||
|
with env["装配"].要求数据库().连接() as 连:
|
||||||
|
资料服务().导入(
|
||||||
|
连,
|
||||||
|
env["作者"].作者,
|
||||||
|
导入请求(
|
||||||
|
kind="reference",
|
||||||
|
title="打斗场景参考书",
|
||||||
|
origin="seed://slice-rewrite",
|
||||||
|
content="改写后的新版内容。\n装甲裂缝里的火花。\n" * 300,
|
||||||
|
authorized_uses=("reference_reading",),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
快照 = env["装配"].要求上下文().读取快照(快照ID)
|
||||||
|
assert 快照.失效说明 and "参考片段" in 快照.失效说明
|
||||||
|
assert "需重新授权" in 快照.失效说明
|
||||||
|
|||||||
@ -118,3 +118,68 @@ def test_非导入者来源不入池__a10031(切片环境):
|
|||||||
回执 = 构建场景索引(连, "other-author")
|
回执 = 构建场景索引(连, "other-author")
|
||||||
assert 回执["来源数"] == 0 and 回执["切片数"] == 0
|
assert 回执["来源数"] == 0 and 回执["切片数"] == 0
|
||||||
assert 计算切片("有字。\n", env["授权来源"], 1)
|
assert 计算切片("有字。\n", env["授权来源"], 1)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.case_id(
|
||||||
|
"NC-w33-a10032",
|
||||||
|
environment="隔离PG与生成环境",
|
||||||
|
given="已构建的授权来源切片索引",
|
||||||
|
when="以场景相关查询执行场景检索",
|
||||||
|
then=["两路融合返回切片:带书名、预览与双路得分;授权来源独占结果"],
|
||||||
|
contract="docs/系统架构/新版设计/模块设计/B09-上下文.md#场景片段检索",
|
||||||
|
)
|
||||||
|
def test_场景检索返回知情候选__a10032(切片环境):
|
||||||
|
env = 切片环境
|
||||||
|
库 = env["装配"].要求数据库()
|
||||||
|
from muse.上下文.接口 import 场景检索
|
||||||
|
|
||||||
|
with 库.连接() as 连:
|
||||||
|
构建场景索引(连, env["作者"].作者)
|
||||||
|
回执 = 场景检索(连, "刀锋擦过装甲 反震", 数量=3)
|
||||||
|
assert 回执["empty_reason"] is None
|
||||||
|
assert 1 <= len(回执["结果"]) <= 3
|
||||||
|
首 = 回执["结果"][0]
|
||||||
|
assert 首["书名"] == "机甲打斗参考"
|
||||||
|
assert "刀锋" in 首["预览"]
|
||||||
|
assert "关键词得分" in 首 and "向量得分" in 首 and 首["融合得分"] > 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.case_id(
|
||||||
|
"NC-w33-a10033",
|
||||||
|
environment="隔离PG与生成环境",
|
||||||
|
given="尚未构建切片索引的库",
|
||||||
|
when="执行场景检索",
|
||||||
|
then=["拒绝为索引陈旧,不静默返回空结果"],
|
||||||
|
contract="docs/系统架构/新版设计/模块设计/B09-上下文.md#场景片段检索",
|
||||||
|
)
|
||||||
|
def test_未建索引拒绝__a10033(生成环境):
|
||||||
|
env = 生成环境
|
||||||
|
from muse.上下文.接口 import 场景检索
|
||||||
|
from muse.上下文.模型 import 上下文错误
|
||||||
|
|
||||||
|
with env["装配"].要求数据库().连接(只读=True) as 连:
|
||||||
|
with pytest.raises(上下文错误):
|
||||||
|
场景检索(连, "任意查询")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.case_id(
|
||||||
|
"NC-w33-a10034",
|
||||||
|
environment="隔离PG与生成环境",
|
||||||
|
given="授权来源用途被撤回后的切片索引",
|
||||||
|
when="执行场景检索",
|
||||||
|
then=["结果为空且说明可读,不扩大为索引不可用"],
|
||||||
|
contract="docs/系统架构/新版设计/模块设计/B09-上下文.md#场景片段检索",
|
||||||
|
)
|
||||||
|
def test_用途撤回后检索为空__a10034(切片环境):
|
||||||
|
env = 切片环境
|
||||||
|
库 = env["装配"].要求数据库()
|
||||||
|
from muse.上下文.接口 import 场景检索
|
||||||
|
|
||||||
|
with 库.连接() as 连:
|
||||||
|
构建场景索引(连, env["作者"].作者)
|
||||||
|
连.execute(
|
||||||
|
"UPDATE muse_source SET authorized_uses='[]'::jsonb WHERE source_id=%s",
|
||||||
|
(env["授权来源"],),
|
||||||
|
)
|
||||||
|
回执 = 场景检索(连, "刀锋", 数量=3)
|
||||||
|
assert 回执["结果"] == []
|
||||||
|
|||||||
@ -236,6 +236,7 @@ export function Ui选段改写({
|
|||||||
selections,
|
selections,
|
||||||
protected_ranges,
|
protected_ranges,
|
||||||
references: refSelected,
|
references: refSelected,
|
||||||
|
slice_references: [],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7153,6 +7153,11 @@ export interface components {
|
|||||||
* @default []
|
* @default []
|
||||||
*/
|
*/
|
||||||
references: string[];
|
references: string[];
|
||||||
|
/**
|
||||||
|
* Slice References
|
||||||
|
* @default []
|
||||||
|
*/
|
||||||
|
slice_references: string[];
|
||||||
};
|
};
|
||||||
/** _定位陈述 */
|
/** _定位陈述 */
|
||||||
muse______________________20: {
|
muse______________________20: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user