diff --git a/docs/系统架构/新版设计/模块设计/B03-资料研究.md b/docs/系统架构/新版设计/模块设计/B03-资料研究.md index 2ae518f..4bafbdf 100644 --- a/docs/系统架构/新版设计/模块设计/B03-资料研究.md +++ b/docs/系统架构/新版设计/模块设计/B03-资料研究.md @@ -90,7 +90,7 @@ S02检查点是执行证据而非第二分析读取路径。运行中的完成 参考书原文可确定性切片入检索池,为参考改写提供场景原文语料。切片是纯函数:按段落边界聚合至目标窗长(1200~2400 字符,不足并入邻窗),切片参数与窗口清单记入窗计划哈希;同一来源版本只认一个窗计划,重算不一致即拒绝为陈旧。切片与索引是可全量重建的派生缓存(原文以 `muse_source_version` 为权威),禁原地改写,重建走整批替换;代次守卫随事务变更递增。 -片段向量索引与方法索引同构:哈希 bigram embedder(零模型成本、确定性)、代次台账 active 唯一、检索时对不上即拒绝为陈旧。索引构建是显式命令(CLI/HTTP),不耦合导入入口;构建入口按当前具备 `reference_reading` 用途的来源版本全量对账,来源版本更新或用途撤回后重算。切片身份与授权解耦:授权变化只影响检索候选与材料读取,不删历史切片。 +片段向量索引与方法索引同构:哈希 bigram embedder(零模型成本、确定性)、代次台账 active 唯一、检索时对不上即拒绝为陈旧。索引构建是显式命令(HTTP),不耦合导入入口;构建入口按当前具备 `reference_reading` 用途的来源版本全量对账,来源版本更新或用途撤回后重算。切片身份与授权解耦:授权变化只影响检索候选与材料读取,不删历史切片。 读取授权:只有来源当前具备 `reference_reading` 用途,其切片才进入检索候选与参考材料读取;B03 经既有 `核对来源授权` 核验。片段材料必须逐字取自冻结时声明的来源版本内容哈希;版本漂移即失效,不默换新版本。 diff --git a/src/muse/上下文/任务范围.py b/src/muse/上下文/任务范围.py index 870834e..1896bf6 100644 --- a/src/muse/上下文/任务范围.py +++ b/src/muse/上下文/任务范围.py @@ -106,9 +106,17 @@ def _核对材料片段(行: dict, 快照ID: str, 方法版本ID: str, 材料哈 实际 = 按键读取材料(装配) 版本绑定 = {条["source_id"]: 条 for 条 in 行["source_bindings"]} # 方法节与参考节同样校验:方法节经作品绑定关联,参考节无绑定,以版本身份直接关联。 - 冻结条目 = [*行["frozen_scope"].get("methods", []), *行["frozen_scope"].get("references", [])] + # 片段载体不是方法版本,与方法消费核对无关,直接跳过。 + 冻结条目 = [ + 条 + for 条 in [ + *行["frozen_scope"].get("methods", []), + *行["frozen_scope"].get("references", []), + ] + if 条.get("载体") != "slice" and "version_id" in 条 + ] for 条 in 冻结条目: - 绑定键 = 条.get("binding_id", 条["version_id"]) + 绑定键 = 条.get("binding_id") or 条["version_id"] 绑 = 版本绑定.get(绑定键) if 条["version_id"] != 方法版本ID or 条["材料键"] not in 实际 or 绑 is None: continue diff --git a/src/muse/上下文/场景检索.py b/src/muse/上下文/场景检索.py index faf5fe2..2225270 100644 --- a/src/muse/上下文/场景检索.py +++ b/src/muse/上下文/场景检索.py @@ -17,8 +17,8 @@ from muse.基础设施.模型.嵌入 import 特征哈希嵌入器 切片数量上限 = 50 -def 读取新鲜切片索引(连) -> tuple[dict[str, Any], list[dict[str, Any]]]: - """代次与索引一致才可用;对不上即拒绝为陈旧,不默重建。""" +def 读取新鲜切片索引(连) -> dict[str, Any]: + """读取代次台账与当前有效索引身份;尚未构建即拒绝为陈旧,不默重建。""" from psycopg.rows import dict_row 行 = ( @@ -33,7 +33,7 @@ def 读取新鲜切片索引(连) -> tuple[dict[str, Any], list[dict[str, Any]]] ) if 行 is None or 行["ledger_id"] is None: raise 上下文错误("CONTEXT_INDEX_STALE", "场景切片索引尚未构建") - return dict(行), [] + return dict(行) def 场景检索(连, 查询: str, *, 数量: int = 10) -> dict[str, Any]: @@ -44,7 +44,7 @@ def 场景检索(连, 查询: str, *, 数量: int = 10) -> dict[str, Any]: 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: @@ -61,7 +61,9 @@ def 场景检索(连, 查询: str, *, 数量: int = 10) -> dict[str, Any]: "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", + "WHERE s.authorized_uses ? 'reference_reading' AND i.embedder=%s " + "AND x.revision = (SELECT MAX(v2.revision) FROM muse_source_version v2 " + "WHERE v2.source_id=x.source_id)", (台账["embedder"],), ) .fetchall() diff --git a/src/muse/资料研究/场景切片.py b/src/muse/资料研究/场景切片.py index 73e9d22..9a1d025 100644 --- a/src/muse/资料研究/场景切片.py +++ b/src/muse/资料研究/场景切片.py @@ -109,7 +109,7 @@ def 构建场景索引(连, 作者: str, *, 嵌入器: 特征哈希嵌入器 | N "(ARRAY_AGG(v.import_result->>'导入者' ORDER BY v.revision DESC))[1] AS author_id " "FROM muse_source s JOIN muse_source_version v ON v.source_id=s.source_id " "WHERE s.authorized_uses ? 'reference_reading' " - "GROUP BY s.source_id, s.title" + "GROUP BY s.source_id, s.title ORDER BY s.source_id" ).fetchall() 切片行: list[dict[str, Any]] = [] 来源数 = 0 diff --git a/tests/用例清单.json b/tests/用例清单.json index 55669f8..57ed403 100644 --- a/tests/用例清单.json +++ b/tests/用例清单.json @@ -27814,6 +27814,41 @@ "数据库" ] }, + { + "case_id": "NC-w32-a10038", + "environment": "隔离PG与合成HTTP提供方", + "given": "同一选段授权同时携带方法参考与片段参考", + "when": "发起并执行受控修订流程至候选交付", + "then": [ + "混合载体授权整轮完成不崩溃;方法消费正常登记,片段不登记", + "两类材料同时入选装配并绑定进快照" + ], + "contract": "docs/系统架构/新版设计/模块设计/B06-审校修订.md#参考改写选段改写链路扩展", + "file": "tests/集成/test_参考改写.py", + "symbol": "test_混合载体参考授权整轮完成__a10038", + "parameter_ids": [], + "node_ids": [ + "tests/集成/test_参考改写.py::test_混合载体参考授权整轮完成__a10038" + ], + "fixtures": [ + "request", + "tmp_path", + "tmp_path_factory", + "内置种子方案", + "内置结构测试库", + "参考环境", + "数据库底座", + "测试资源接缝", + "源码资源", + "片段环境", + "生成环境", + "离线防护", + "隔离数据库URL" + ], + "markers": [ + "数据库" + ] + }, { "case_id": "NC-w33-a10025", "environment": "离线纯函数", diff --git a/tests/集成/test_参考改写.py b/tests/集成/test_参考改写.py index 90c1238..14573a4 100644 --- a/tests/集成/test_参考改写.py +++ b/tests/集成/test_参考改写.py @@ -378,3 +378,50 @@ def test_片段来源漂移使快照失效__a10036(片段环境): 快照 = env["装配"].要求上下文().读取快照(快照ID) assert 快照.失效说明 and "参考片段" in 快照.失效说明 assert "需重新授权" in 快照.失效说明 + + +@pytest.mark.case_id( + "NC-w32-a10038", + environment="隔离PG与合成HTTP提供方", + given="同一选段授权同时携带方法参考与片段参考", + when="发起并执行受控修订流程至候选交付", + then=[ + "混合载体授权整轮完成不崩溃;方法消费正常登记,片段不登记", + "两类材料同时入选装配并绑定进快照", + ], + contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md#参考改写选段改写链路扩展", +) +def test_混合载体参考授权整轮完成__a10038(片段环境): + env = 片段环境 + # 片段环境默认清空了方法参考;此处恢复混合:方法 + 片段同轮。 + env["授权"] = replace(env["授权"], references=(env["参考版本"],)) + tid = 修订测试.发起(env, command="mix-revision-1") + env["剧本"].append( + { + "类型": "文本", + "文本": { + "edits": [ + { + "selection_id": env["选段"].selection_id, + "action": "replace", + "replacement": "刀锋擦过装甲,反震顺着掌心爬上来。", + "reason": "合成混合参考改写", + } + ] + }, + } + ) + task = 修订测试.跑(env, tid) + assert task.状态 == 任务状态.已完成 + + 快照 = env["装配"].要求上下文().读取快照(_快照ID(task)) + assert "参考方法:" + env["参考版本"] in 快照.装配["入选"] + assert "参考片段:" + env["片段"]["slice_id"] in 快照.装配["入选"] + assert {str(绑.来源ID) for 绑 in 快照.来源绑定} >= {env["参考版本"], env["片段"]["slice_id"]} + + with env["装配"].要求数据库().连接(只读=True) as 连: + rows = 连.execute( + "SELECT method_version_id FROM muse_method_usage WHERE task_id=%s", + (tid,), + ).fetchall() + assert [str(r[0]) for r in rows] == [env["参考版本"]] diff --git a/web/src/功能/正文写作/选段改写.tsx b/web/src/功能/正文写作/选段改写.tsx index b332bfc..f876292 100644 --- a/web/src/功能/正文写作/选段改写.tsx +++ b/web/src/功能/正文写作/选段改写.tsx @@ -198,7 +198,7 @@ export function Ui选段改写({ command_id: refCommandId.current || crypto.randomUUID(), selection: { query: refQuery.trim(), - domain: "", + domain: refDomain, work_id: current.work_id, quote: "", index_generation: 0,