"""明确选区、不可抵消的保真失败与有界停止;不判断文学收益。""" from dataclasses import replace import pytest from pydantic import ValidationError from muse.审校修订.接口 import 核对修订保真, 选择返修 from muse.正文写作.接口 import ( 应用选段, 换行节点, 文本节点, 正文草稿, 正文错误, 段落, 段落哈希, 选段修改, ) def 稿(text): return 正文草稿((段落("p1", (文本节点(text),)),)) @pytest.mark.case_id( "NC-w23-230009", environment="默认离线", given="原文包含数值、时间、推测或不含引文", when="非空替换悄悄删减或新增引文,对照明确整段删除", then=["逐项保真拒绝非空替换中的信息丢失;明确整段删除可以归因"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) @pytest.mark.parametrize( "before,after", [ ("他还有3枚铜钱。", "他还有铜钱。"), ("次日他离开。", "他离开。"), ("他大概还活着。", "他还活着。"), ("他说需要等一等。", "他说“别走”。"), ], ) def test_非删除替换不能悄悄丢数值时间情态或新增引文__230009(before, after): original = 稿(before) patch = 改(original, 0, len(before), after) result = 核对修订保真(original, 应用选段(original, (patch,)), (patch,)) assert not result["mechanical_pass"] and result["errors"] deletion = 改(original, 0, len(before), "") assert 核对修订保真(original, 应用选段(original, (deletion,)), (deletion,))["mechanical_pass"] def 改(doc, start, end, text): p = doc.paragraphs[0] return 选段修改( p.paragraph_id, 段落哈希(p), start, end, p.content[0].text[start:end], text, "finding-1" ) @pytest.mark.case_id( "TC-d3bbf201688b", environment="默认离线", when="执行具名公开保真、离线回放或实际CLI与隔离PG入口", then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"], contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md", ) def test_structural_verify_detects_outside_change__d3bbf2(): original = 稿("她关上门。值得注意的是,天黑了。") text = "她关上门。值得注意的是,天黑了。" start = text.index("值得注意的是,") patch = 改(original, start, start + 7, "") candidate = 稿("她关上门。天黑了。另外多出一句。") result = 核对修订保真(original, candidate, (patch,)) assert not result["mechanical_pass"] and "候选含批准选段之外的修改" in result["errors"] @pytest.mark.case_id( "TC-77456d782193", environment="默认离线", when="执行具名公开保真、离线回放或实际CLI与隔离PG入口", then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"], contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md", ) def test_fact_delta_blocks_new_number__77456d(): original = 稿("值三百两银子") patch = 改(original, 0, 6, "值三百五十两银子") result = 核对修订保真(original, 应用选段(original, (patch,)), (patch,)) assert not result["mechanical_pass"] assert any("新的数字" in s for s in result["errors"]) @pytest.mark.case_id( "TC-9f437af4761f", environment="默认离线", when="执行具名公开保真、离线回放或实际CLI与隔离PG入口", then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"], contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md", ) def test_number_attribution_allows_approved_deletion__9f437a(): text = "值三百两银子。值得一提的是,三天后开门。" original = 稿(text) start = text.index("值得一提的是,三天后") patch = 改(original, start, start + len("值得一提的是,三天后"), "") candidate = 应用选段(original, (patch,)) assert candidate == 稿("值三百两银子。开门。") assert 核对修订保真(original, candidate, (patch,))["mechanical_pass"] @pytest.mark.case_id( "TC-653996eaf810", environment="默认离线", when="执行具名公开保真、离线回放或实际CLI与隔离PG入口", then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"], contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md", ) def test_number_attribution_blocks_unexplained_disappearance__653996(): result = 核对修订保真(稿("值三百两银子。"), 稿("值银子。"), ()) assert not result["mechanical_pass"] @pytest.mark.case_id( "TC-9cdea0d7f385", environment="默认离线", when="执行具名公开保真、离线回放或实际CLI与隔离PG入口", then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"], contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md", ) def test_quote_tamper_fails__9cdea0(): result = 核对修订保真(稿("面板显示:【等级:待定】。"), 稿("面板显示:【等级:最高】。"), ()) assert not result["mechanical_pass"] assert any("引文变化" in s for s in result["errors"]) @pytest.mark.case_id( "TC-d5ab68995993", environment="默认离线", when="执行具名公开保真、离线回放或实际CLI与隔离PG入口", then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"], contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md", ) def test_quote_duplicate_loss_cannot_hide_behind_one_deleted_quote__d5ab68(): original = 稿("前文【同一条】。后文【同一条】。") patch = 改(original, 0, len("前文【同一条】"), "") result = 核对修订保真(original, 稿("前文。后文。"), (patch,)) assert not result["mechanical_pass"] assert any("引文变化" in s for s in result["errors"]) @pytest.mark.case_id( "TC-34ff4927560e", environment="默认离线", when="执行具名公开保真、离线回放或实际CLI与隔离PG入口", then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"], contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md", ) def test_quote_approved_deletion_passes__34ff49(): text = "前文。面板显示:【等级:待定】。后文。" original = 稿(text) start = text.index("面板显示") patch = 改(original, start, start + len("面板显示:【等级:待定】。"), "") candidate = 应用选段(original, (patch,)) assert candidate == 稿("前文。后文。") assert 核对修订保真(original, candidate, (patch,))["mechanical_pass"] @pytest.mark.case_id( "TC-fbb3c1fb3f25", environment="默认离线", when="执行具名公开保真、离线回放或实际CLI与隔离PG入口", then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"], contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md", ) def test_protected_tic_deletion_fails__fbb3c1(): original = 稿("「我说小子,你来了。」") patch = 改(original, 1, 6, "") result = 核对修订保真(original, 应用选段(original, (patch,)), (patch,), 保护表达=("我说小子",)) assert not result["mechanical_pass"] and "保护表达被修改或删除" in result["errors"] @pytest.mark.case_id( "TC-c5ce94956186", environment="默认离线", when="执行具名公开保真、离线回放或实际CLI与隔离PG入口", then=["保留原例输入中的拒绝、保真或范围外不变性,以B05文档树与B06公开核验替代旧内部字典形状。"], contract="docs/系统架构/新版设计/功能规格/P04-声音诊断与受控修订.md", ) def test_snapshot_missing_downgrades_to_audit__c5ce94(): result = 选择返修( 已用轮数=0, 最大轮数=3, 硬门通过=True, 有修改=True, 作者选择="retry", 有完整依据=False ) assert result["action"] == "audit_only" @pytest.mark.case_id( "NC-w23-230001", environment="默认离线", given="test_重复原句依段落身份定位且范围外格式保持__230001", when="经过B05选段及B06保真/停止公开接口", then=["test_重复原句依段落身份定位且范围外格式保持__230001"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_重复原句依段落身份定位且范围外格式保持__230001(): 一 = 段落("p1", (文本节点("他停住。", ("strong",)), 换行节点("\r\n"), 文本节点("又望了一眼。"))) 二 = 段落("p2", (文本节点("他停住。"),)) doc = 正文草稿((一, 段落("blank", ()), 二)) patch = 选段修改("p2", 段落哈希(二), 1, 3, "停住", "回头", "finding-2") new = 应用选段(doc, (patch,)) assert new.paragraphs[:2] == doc.paragraphs[:2] assert new.paragraphs[2].content[0].text == "他回头。" assert doc.paragraphs[2].content[0].text == "他停住。" @pytest.mark.case_id( "NC-w23-230002", environment="默认离线", given="test_错段错文重叠与非法码点拒绝__230002", when="经过B05选段及B06保真/停止公开接口", then=["test_错段错文重叠与非法码点拒绝__230002"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) @pytest.mark.parametrize("bad", ["hash", "id", "original", "overlap", "bool", "range"]) def test_错段错文重叠与非法码点拒绝__230002(bad): doc = 稿("🙂e\u0301他说:甲停住。") p = 改(doc, 3, 4, "她") patches = (p,) with pytest.raises((正文错误, ValidationError)): if bad == "hash": patches = (replace(p, paragraph_hash="0" * 64),) elif bad == "id": patches = (replace(p, paragraph_id="absent"),) elif bad == "original": patches = (replace(p, original="她"),) elif bad == "overlap": patches = (p, p) elif bad == "bool": patches = (replace(p, start=True),) else: patches = (replace(p, end=999),) 应用选段(doc, patches) @pytest.mark.case_id( "NC-w23-230003", environment="默认离线", given="test_组合字符不归一且不能切开CRLF__230003", when="经过B05选段及B06保真/停止公开接口", then=["test_组合字符不归一且不能切开CRLF__230003"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_组合字符不归一且不能切开CRLF__230003(): doc = 稿("🙂e\u0301他说:甲停住。") p = 改(doc, 3, 4, "她") assert 应用选段(doc, (p,)).paragraphs[0].content[0].text == "🙂e\u0301她说:甲停住。" para = 段落("p1", (文本节点("甲"), 换行节点("\r\n"), 文本节点("乙"))) with pytest.raises(正文错误, match="CRLF"): 应用选段(正文草稿((para,)), (选段修改("p1", 段落哈希(para), 1, 2, "\r", ""),)) @pytest.mark.case_id( "NC-w23-230004", environment="默认离线", given="test_数字时间情态引文变化不能用润色掩盖__230004", when="经过B05选段及B06保真/停止公开接口", then=["test_数字时间情态引文变化不能用润色掩盖__230004"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) @pytest.mark.parametrize( "before,after", [ ("付三百两。", "付三百五十两。"), ("3天后走。", "4天后走。"), ("也许他会来。", "他一定会来。"), ("「甲走了。」", "「乙走了。」"), ], ) def test_数字时间情态引文变化不能用润色掩盖__230004(before, after): doc = 稿(before) patch = 改(doc, 0, len(before), after) new = 应用选段(doc, (patch,)) result = 核对修订保真(doc, new, (patch,)) assert not result["mechanical_pass"] and result["errors"] @pytest.mark.case_id( "NC-w23-230005", environment="默认离线", given="test_全局数字守恒也不能抵消逐选段篡改__230005", when="经过B05选段及B06保真/停止公开接口", then=["test_全局数字守恒也不能抵消逐选段篡改__230005"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_全局数字守恒也不能抵消逐选段篡改__230005(): doc = 稿("甲有3枚。乙有4枚。") patches = (改(doc, 2, 3, "4"), 改(doc, 7, 8, "3")) new = 应用选段(doc, patches) assert sorted(c for c in new.paragraphs[0].content[0].text if c.isdigit()) == ["3", "4"] assert not 核对修订保真(doc, new, patches)["mechanical_pass"] @pytest.mark.case_id( "NC-w23-230006", environment="默认离线", given="test_批准整引文删除可归因但不能删保护表达__230006", when="经过B05选段及B06保真/停止公开接口", then=["test_批准整引文删除可归因但不能删保护表达__230006"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_批准整引文删除可归因但不能删保护表达__230006(): doc = 稿("「甲走了。」他没有回头。") patch = 改(doc, 0, 6, "") new = 应用选段(doc, (patch,)) assert 核对修订保真(doc, new, (patch,))["mechanical_pass"] assert not 核对修订保真(doc, new, (patch,), 保护表达=("甲走了",))["mechanical_pass"] assert 核对修订保真(doc, new, (patch,))["semantic_status"] == "unverified" @pytest.mark.case_id( "NC-w23-230007", environment="默认离线", given="test_未经批准的范围外变化失败__230007", when="经过B05选段及B06保真/停止公开接口", then=["test_未经批准的范围外变化失败__230007"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) def test_未经批准的范围外变化失败__230007(): doc = 稿("风停了。他抬头。") patch = 改(doc, 5, 7, "回头") new = 稿("雨停了。他回头。") assert not 核对修订保真(doc, new, (patch,))["mechanical_pass"] @pytest.mark.case_id( "NC-w23-230008", environment="默认离线", given="test_停止条件不由质量分数抵消__230008", when="经过B05选段及B06保真/停止公开接口", then=["test_停止条件不由质量分数抵消__230008"], contract="docs/系统架构/新版设计/模块设计/B06-审校修订.md", ) @pytest.mark.parametrize( "args,action", [ ({"硬门通过": False, "作者选择": "candidate"}, "original"), ({"有完整依据": False}, "audit_only"), ({"有修改": False}, "original"), ({"作者选择": "original"}, "original"), ({"已用轮数": 3}, "stop"), ({"作者选择": None}, "awaiting_author"), ({"作者选择": "retry"}, "retry"), ], ) def test_停止条件不由质量分数抵消__230008(args, action): base = {"已用轮数": 1, "最大轮数": 3, "硬门通过": True, "有修改": True} assert 选择返修(**{**base, **args})["action"] == action