feat(上下文): 场景检索入口与片段参考进冻结链路——参考书场景原文可检索可改写

- 场景检索.py:切片语料两路等权RRF(键带slice:前缀),授权join过滤、代次陈旧拒绝、结果带书名预览双路得分;RRF抽公共融合入基础设施
- 参考材料双载体:references节判别式(方法/片段),片段冻结身份带来源内容哈希哨兵,现读核当前版本一致否则SOURCE_STALE
- 选段授权slice_references→范围授权→校验→依赖复检→装配→快照绑定全链贯通;片段不登记方法消费
- 范围校验source_ids基集、服务包装、依赖锁定键分派同步;B03接口增读取来源当前指纹(架构门禁禁私表)
- 集成a10032-a10036:检索知情候选/未建拒绝/撤回空/片段进快照/来源漂移失效
This commit is contained in:
zizi 2026-09-20 00:08:29 +08:00
parent fea8f44327
commit 6d1f635003
19 changed files with 699 additions and 48 deletions

View File

@ -20905,6 +20905,14 @@
"type": "array",
"title": "References",
"default": []
},
"slice_references": {
"items": {
"type": "string"
},
"type": "array",
"title": "Slice References",
"default": []
}
},
"additionalProperties": false,

View File

@ -241,6 +241,7 @@ class 上下文服务:
含声音: bool = False,
含偏好: bool = False,
参考版本: tuple[str, ...] = (),
片段参考: tuple[str, ...] = (),
) -> dict:
return 授权任务范围(
self.数据库,
@ -256,6 +257,7 @@ class 上下文服务:
含声音=含声音,
含偏好=含偏好,
参考版本=参考版本,
片段参考=片段参考,
)
def 读取范围(self, 任务ID: str) -> dict:

View File

@ -294,6 +294,17 @@ def 核对依赖(
变化项.append(错误.说明)
else:
for 条 in 参考:
if 条.get("载体") == "slice":
锁定.append(
依赖锁定(
条["slice_id"],
"reference-slice",
str(条["revision"]),
条["内容哈希"],
"reference-slice-v1",
)
)
else:
锁定.append(
依赖锁定(
条["version_id"],

View File

@ -17,6 +17,22 @@ _身份字段 = (
"projection_version",
)
_片段身份字段 = (
"slice_id",
"source_id",
"revision",
"起点",
"终点",
"材料键",
"内容哈希",
"书名",
)
def 冻结片段身份(条: dict) -> dict:
"""场景切片冻结身份;内容哈希是来源版本漂移哨兵,材料文本不入范围。"""
return {**{键: 条[键] for 键 in _片段身份字段}, "载体": "slice"}
def 冻结参考身份(条: dict) -> dict:
"""范围冻结只保留身份与哈希;材料文本不入范围,读取时经B04现取。"""
@ -31,7 +47,19 @@ def 校验参考节(范围: dict) -> None:
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结参考清单必须是列表")
已见: set[str] = set()
for 条 in 参考:
if not isinstance(条, dict) or any(
if not isinstance(条, dict):
raise 上下文错误("CONTEXT_SCOPE_INVALID", "冻结参考缺少身份")
载体 = 条.get("载体", "method")
if 载体 == "method":
_校验方法参考(条, 已见)
elif 载体 == "slice":
_校验片段参考(条, 已见)
else:
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"未知参考载体:{载体!r}")
def _校验方法参考(条: dict, 已见: set[str]) -> None:
if any(
not isinstance(条.get(键), str) or not 条[键]
for 键 in (
"version_id",
@ -50,10 +78,40 @@ def 校验参考节(范围: dict) -> None:
已见.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]:
"""按冻结身份现读当前材料并逐项核对;任一漂移即整体拒绝。"""
"""按冻结身份现读当前材料并逐项核对;任一漂移即整体拒绝。两类载体混合处理。"""
校验参考节(范围)
参考 = 范围.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 参考:
return []
from muse.知识方法.接口 import 方法错误, 读取参考材料
@ -83,7 +141,48 @@ def 核对范围参考(连, 范围: dict, *, 读取预算=None) -> list[dict]:
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) -> 来源绑定:
"""方法与片段两类参考条目都构造成快照来源绑定;字段按载体取自冻结身份。"""
if 条.get("载体") == "slice":
return 来源绑定(条["slice_id"], str(条["revision"]), 条["内容哈希"], "reference-slice-v1")
return 来源绑定(
条["version_id"], str(条["version"]), 条["schema_hash"], 条["projection_version"]
)

View 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__ = ["场景检索", "读取新鲜切片索引", "切片数量上限"]

View File

@ -19,6 +19,7 @@ from muse.上下文.冻结快照 import (
)
from muse.上下文.参考材料 import 参考绑定, 校验参考节, 核对范围参考
from muse.上下文.回放材料 import 历史版本选择, 回放材料选择, 核对回放字段策略, 读取回放材料
from muse.上下文.场景检索 import 场景检索
from muse.上下文.声音材料 import 核对范围声音
from muse.上下文.探索回放 import (
回放,
@ -72,6 +73,7 @@ __all__ = [
"核对回放字段策略",
"保护模型发送",
"混合检索",
"场景检索",
"合并信号表",
"权重表",
"偏好信号",

View File

@ -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 方法错误, 读取方法检索候选, 读取方法检索身份, 读取生效绑定
def _rrf(得分表列: 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 融合
return 倒数排名融合(得分表列, 权重)
def _偏好呈现(信号: 偏好信号 | None) -> dict[str, Any]:

View File

@ -52,14 +52,19 @@ def 授权任务范围(
含声音: bool = False,
含偏好: bool = False,
参考版本: tuple[str, ...] = (),
片段参考: tuple[str, ...] = (),
) -> dict:
"""按作者身份冻结历史文稿、目标章细纲与事实系统版本;两种用途随范围绑定。"""
if not 作品ID:
raise 上下文错误("CONTEXT_SCOPE_INVALID", "任务范围必须绑定明确作品")
if isinstance(参考版本, str) or not isinstance(参考版本, tuple):
raise 上下文错误("CONTEXT_SCOPE_INVALID", f"参考版本必须以元组显式声明:{参考版本!r}")
if len(参考版本) > 8:
raise 上下文错误("CONTEXT_SCOPE_INVALID", "参考方法版本不超过8个")
if isinstance(片段参考, str) or not isinstance(片段参考, tuple):
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 数据库.用途:
raise 上下文错误("SCOPE_DENIED", "任务范围授权需要与装配用途一致的作者身份")
校验截止(截止)
@ -136,7 +141,7 @@ def 授权任务范围(
from muse.上下文.参考材料 import 冻结参考身份
from muse.知识方法.接口 import 读取参考材料
参考 = [
参考.extend(
冻结参考身份(条)
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 文稿]
来源集.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:
来源集.append(声音["version_id"])
来源集.extend(条["binding_id"] for 条 in 方法)

View File

@ -78,23 +78,31 @@ def _校验冻结输入(
条["projection_version"],
):
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()}
if any(键.startswith("参考方法:") and 键 not in 参考键 for 键 in 装配.入选来源):
raise 上下文错误("SCOPE_DENIED", "装配包含未授权参考方法材料")
if any(键.startswith("参考片段:") and 键 not in 参考键 for 键 in 装配.入选来源):
raise 上下文错误("SCOPE_DENIED", "装配包含未授权参考片段材料")
入选参考 = {vid for vid, 条 in 参考集.items() if 条["材料键"] in 装配.入选来源}
实绑参考 = {条.来源ID for 条 in 绑定 if 条.来源ID in 参考集}
if 入选参考 != 实绑参考:
raise 上下文错误("CONTEXT_INPUT_INVALID", "入选参考与冻结版本绑定不一致")
for 绑 in 绑定:
if 绑.来源ID in 参考集:
条 = 参考集[绑.来源ID]
if 绑.来源ID in 方法参考:
条 = 方法参考[绑.来源ID]
if (绑.数据版本, 绑.结构哈希, 绑.投影版本) != (
str(条["version"]),
条["schema_hash"],
条["projection_version"],
):
raise 上下文错误("SOURCE_STALE", "参考版本或材料哈希与授权不一致")
if 绑.来源ID in 片段参考:
条 = 片段参考[绑.来源ID]
if 绑.结构哈希 != 条["内容哈希"] or 绑.数据版本 != str(条["revision"]):
raise 上下文错误("SOURCE_STALE", "参考片段来源版本与授权不一致")
目录源 = 范围.get("directory_source")
if 目录源:
for 项 in 绑定:
@ -220,7 +228,7 @@ def 恢复冻结范围(数据库: 数据库工厂, 任务: 任务快照) -> dict
方法ID.add(条["binding_id"])
基础 = {项["document_id"] for 项 in 文稿} | 方法ID
基础.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:
基础.add(范围["voice"]["version_id"])
if 目录源 is not None:

View 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__ = ["倒数排名融合"]

View File

@ -88,6 +88,7 @@ class 选段写作授权:
selections: tuple[原文选段, ...]
protected_ranges: tuple[原文选段, ...] = ()
references: tuple[str, ...] = ()
slice_references: tuple[str, ...] = ()
def __post_init__(self):
if self.revision < 1 or not self.purpose.strip() or not 1 <= len(self.selections) <= 32:
@ -96,12 +97,16 @@ class 选段写作授权:
raise 正文错误("选段改写需明确作品、章节及分支")
if len({s.selection_id for s in self.selections}) != len(self.selections):
raise 正文错误("圈选不能重复")
if len(self.references) > 8:
raise 正文错误("参考方法不超过8个")
if len(self.references) + len(self.slice_references) > 8:
raise 正文错误("参考总数(方法加片段)不超过8个")
if len(set(self.references)) != len(self.references):
raise 正文错误("参考方法版本不能重复")
if not all(type(r) is str and r.strip() for r in self.references):
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 准备选段改写(连, 作者, 授权: 选段写作授权):

View File

@ -113,6 +113,7 @@ def 发起模型修订(app, 身份, 命令ID, 授权: 模型修订授权 | 选
含声音=True,
含偏好=True,
参考版本=tuple(授权.references) if isinstance(授权, 选段写作授权) else (),
片段参考=tuple(授权.slice_references) if isinstance(授权, 选段写作授权) else (),
)
策略 = 角色策略目录.从发布包()
request = 任务请求(
@ -211,10 +212,11 @@ def 登记审校修订(登记, app, 计价):
snap = 上下文.冻结上下文(ctx.任务.任务ID, 组装, tuple(bindings))
# 只把装配入选的参考写入检查点:供真实交付后登记方法消费(kind=review)。
# 未入选参考未进入模型输入,不登记消费;旧任务检查点无该键,向后兼容为空。
# 只收方法载体:片段参考不是方法版本,不登记方法消费(见 B06 契约)。
入选参考 = [
{"version_id": 条["version_id"], "材料哈希": 条["材料哈希"]}
for 条 in 参考材料
if 条["材料键"] in 组装.入选来源
if 条.get("载体", "method") == "method" and 条["材料键"] in 组装.入选来源
]
return 步骤结果(
{"snapshot_id": snap.快照ID},

View File

@ -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.检索文本, "
"v.content_hash, s.title, s.authorized_uses "
"v.content_hash, v.import_result, s.title, s.authorized_uses "
"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 s ON s.source_id=x.source_id "
@ -205,10 +205,35 @@ def 读取场景切片(连, 作者: str, slice_ids: list[str]) -> list[dict[str,
"用途": 条["authorized_uses"],
}
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__ = [
"目标窗长",
"最短窗长",
@ -218,4 +243,5 @@ __all__ = [
"计算切片",
"构建场景索引",
"读取场景切片",
"读取来源当前指纹",
]

View File

@ -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 执行窗口提取
@ -238,4 +238,5 @@ __all__ = [
"构建场景索引",
"计算切片",
"读取场景切片",
"读取来源当前指纹",
]

View File

@ -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",
"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",
"environment": "隔离 PostgreSQL;结构升级为明确的已完成状态夹具,不代替作者升级旅程",

View File

@ -268,3 +268,113 @@ def test_无参考修订不登记消费__a10024(参考环境):
"SELECT COUNT(*) FROM muse_method_usage WHERE task_id=%s", (tid,)
).fetchone()[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 快照.失效说明

View File

@ -118,3 +118,68 @@ def test_非导入者来源不入池__a10031(切片环境):
回执 = 构建场景索引(连, "other-author")
assert 回执["来源数"] == 0 and 回执["切片数"] == 0
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 回执["结果"] == []

View File

@ -236,6 +236,7 @@ export function Ui选段改写({
selections,
protected_ranges,
references: refSelected,
slice_references: [],
},
};
}

View File

@ -7153,6 +7153,11 @@ export interface components {
* @default []
*/
references: string[];
/**
* Slice References
* @default []
*/
slice_references: string[];
};
/** _定位陈述 */
muse______________________20: {