@furongjun1999/dsh-memory 0.6.1 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +49 -18
  2. package/docs/README.md +1 -0
  3. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +223 -0
  4. package/docs/eval/cons200_/345/206/262/347/252/201/346/243/200/346/265/213/351/200/211/351/235/242_/345/256/236/346/226/275/350/256/260/345/275/225_v1.1.md +340 -0
  5. package/docs/eval/issue50_/345/205/203/346/225/260/346/215/256/351/200/217/344/274/240/344/270/216/345/205/234/345/272/225_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +437 -0
  6. package/docs/eval/issue50_/345/215/212/351/207/215/345/244/215/345/276/205/345/256/232/345/244/215/346/240/270_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +488 -0
  7. package/docs/eval/issue50_/345/276/205/345/256/232/345/244/215/346/240/270/345/205/245/351/230/237_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +428 -0
  8. package/docs/eval/issue50_/350/257/273/351/235/242/344/277/235/346/212/244/345/217/252/350/256/244/346/230/276/345/274/217/346/235/245/346/272/220_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +271 -0
  9. package/docs/eval/issue50_/351/207/215/350/246/201/345/272/246/345/220/214/346/272/220/344/270/216/344/277/235/346/212/244/350/257/255/344/271/211_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +306 -0
  10. package/docs/eval/issue51_/344/270/200/351/224/256/345/256/211/350/243/205/345/244/261/350/264/245_/345/275/222/345/261/236/345/210/244/345/256/232_v1.0.md +58 -0
  11. package/docs/eval/issue52_/346/235/241/344/273/266/345/205/210/350/241/214/344/270/216/346/210/252/346/226/255/345/217/257/350/247/202/346/265/213_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +360 -0
  12. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/241/346/241/243/344/275/215/345/215/225/344/270/200/345/205/245/345/217/243_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +651 -0
  13. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/242/345/217/230/346/233/264/345/215/225/344/270/216/345/233/236/346/273/232/345/216/237/350/257/255_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +439 -0
  14. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/243/345/207/206/345/205/245/350/257/273/346/225/260_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1337 -0
  15. package/docs/eval//344/270/211/346/241/243/350/207/252/346/262/273_/346/255/245/351/252/244/342/221/244/346/224/266/345/256/230/344/270/216/345/205/250/351/223/276/351/252/214/346/224/266_/350/220/275/347/240/201/350/256/260/345/275/225_v1.0.md +1817 -0
  16. package/docs/eval//345/207/272/350/264/247/351/235/242/345/206/222/347/203/237_/350/277/233/350/264/247/351/227/250/347/246/201_v1.0.md +460 -0
  17. package/docs/eval//345/217/221/345/270/20308_/350/207/252/350/277/255/344/273/243/344/270/216/347/235/241/347/234/240_/345/233/276/346/243/200/347/264/242/350/267/257/344/270/216/346/235/203/351/207/215_v1.0.md +97 -0
  18. package/docs/eval//345/217/221/345/270/20309_/346/243/200/347/264/242/351/235/242/344/270/211/346/211/271/346/224/266/345/217/243_v1.0.md +66 -0
  19. package/docs/eval//345/275/222/344/270/200/345/261/202/347/274/272/347/234/201/347/277/273/345/205/263_/344/277/256/345/244/215/350/256/260/345/275/225_v1.0.md +458 -0
  20. package/docs/eval//347/254/2543/345/261/202stg/347/273/223/346/236/204/347/264/242/345/274/225_/345/256/236/346/226/275/350/256/260/345/275/225_v1.0.md +250 -0
  21. package/docs/hive//346/243/200/347/264/242/347/256/227/346/263/225/345/217/243/345/276/204/345/257/271/347/205/247_v0.1.md +136 -11
  22. package/docs/hive//346/243/200/347/264/242/350/267/257/345/276/204/344/270/216/350/256/244/347/237/245/347/273/223/346/236/204/345/245/221/347/272/246_v0.1.md +32 -2
  23. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +88 -0
  24. package/docs/mdcg//345/212/237/350/203/275/350/260/203/347/224/250/346/230/240/345/260/204/350/241/250_v0.1.md +40 -40
  25. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +47 -11
  26. package/docs/mdcg//347/235/241/347/234/240/345/221/250/346/234/237_/350/277/220/347/273/264/345/211/215/346/217/220/344/270/216/347/273/264/346/212/244/346/214/207/345/215/227_v1.0.md +183 -0
  27. package/docs/plans/stg/346/235/241/344/273/266/345/214/226/344/270/216/347/273/223/346/236/204/347/264/242/345/274/225_/350/256/276/350/256/241_v0.1.md +124 -0
  28. package/docs/plans//347/235/241/347/234/240/344/270/216/350/207/252/350/277/255/344/273/243_/345/212/237/350/203/275/344/274/230/345/214/226/350/256/276/350/256/241_v0.4.md +547 -0
  29. package/docs/plans//350/256/260/345/277/206/350/207/252/345/244/204/347/220/206/344/270/211/346/241/243/350/207/252/346/262/273_/350/256/276/350/256/241_v0.2.md +207 -0
  30. package/md_cg/admission.py +718 -0
  31. package/md_cg/autonomy_modes.py +642 -0
  32. package/md_cg/bench_e2e_locomo_qa.py +11 -4
  33. package/md_cg/chain.py +47 -0
  34. package/md_cg/consistency.py +133 -8
  35. package/md_cg/forgetting.py +38 -6
  36. package/md_cg/freshness.py +527 -0
  37. package/md_cg/generation.py +409 -0
  38. package/md_cg/hotcache.py +4 -1
  39. package/md_cg/lifecycle.py +30 -2
  40. package/md_cg/mcp_server.py +191 -26
  41. package/md_cg/mdcg.py +594 -39
  42. package/md_cg/mdcos.py +869 -38
  43. package/md_cg/nodefile.py +74 -1
  44. package/md_cg/protect.py +79 -4
  45. package/md_cg/provenance.py +1 -1
  46. package/md_cg/review_cli.py +31 -2
  47. package/md_cg/rollback.py +411 -0
  48. package/md_cg/rollback_cli.py +104 -0
  49. package/md_cg/semantic/canonical.py +22 -0
  50. package/md_cg/semantic/unify.py +73 -22
  51. package/md_cg/semantic/unify_fixture.json +25 -0
  52. package/md_cg/sleep.py +1297 -0
  53. package/md_cg/stg.py +400 -50
  54. package/md_cg/stgidx.py +281 -0
  55. package/md_cg/sustain.py +146 -9
  56. package/md_cg/test_auto_defaults.py +424 -0
  57. package/md_cg/test_autonomy_admission.py +1098 -0
  58. package/md_cg/test_autonomy_modes.py +1972 -0
  59. package/md_cg/test_b1_auto_id_multiproc.py +7 -0
  60. package/md_cg/test_b1b2_write_face.py +7 -0
  61. package/md_cg/test_b3_merge_keeps_content.py +7 -0
  62. package/md_cg/test_boundary_hit.py +410 -0
  63. package/md_cg/test_cons200_scan_selection.py +791 -0
  64. package/md_cg/test_en_pipeline.py +22 -13
  65. package/md_cg/test_generation_guard.py +352 -0
  66. package/md_cg/test_h4_sustain_snapshot.py +14 -4
  67. package/md_cg/test_i50a_half_dup_defer.py +408 -0
  68. package/md_cg/test_i50b_defer_to_review_queue.py +545 -0
  69. package/md_cg/test_i50c_meta_passthrough.py +608 -0
  70. package/md_cg/test_i50d_importance_source.py +531 -0
  71. package/md_cg/test_i50e_readside_protection.py +656 -0
  72. package/md_cg/test_issue39_utf8_stdio.py +9 -1
  73. package/md_cg/test_issue52_scan_condition_first.py +677 -0
  74. package/md_cg/test_linkref.py +7 -0
  75. package/md_cg/test_mode_parity.py +1393 -0
  76. package/md_cg/test_mutation_rollback.py +805 -0
  77. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +1 -1
  78. package/md_cg/test_n212_n213_n224_generation_gates.py +14 -8
  79. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +7 -0
  80. package/md_cg/test_n230_dirty_replay.py +375 -0
  81. package/md_cg/test_p2_mcp.py +8 -0
  82. package/md_cg/test_p2_six_elements.py +463 -0
  83. package/md_cg/test_p3_legacy_closure.py +433 -0
  84. package/md_cg/test_p4_freshness.py +680 -0
  85. package/md_cg/test_p8_subgraph_chain.py +14 -1
  86. package/md_cg/test_p9_forget_protect.py +7 -0
  87. package/md_cg/test_p9c_dedup_hints.py +7 -0
  88. package/md_cg/test_policy_required_ccg.py +5 -1
  89. package/md_cg/test_protocol.py +7 -0
  90. package/md_cg/test_rank_parity_score_mode.py +6 -0
  91. package/md_cg/test_semantic_canonical.py +5 -3
  92. package/md_cg/test_sleep.py +611 -0
  93. package/md_cg/test_sleep_p1.py +784 -0
  94. package/md_cg/test_stgidx_index_parity.py +1020 -0
  95. package/md_cg/test_time_core_lint.py +968 -0
  96. package/md_cg/test_unify_default_off.py +701 -0
  97. package/md_cg/test_unify_scope.py +182 -0
  98. package/md_cg/test_writelimit.py +36 -9
  99. package/md_cg/test_writepipe.py +5 -2
  100. package/md_cg/weights.py +15 -1
  101. package/md_cg/whitebox_kb/aeis_core/time_core.py +8 -0
  102. package/md_cg/writepipe.py +101 -2
  103. package/package.json +3 -2
  104. package/skills/plugin.json +1 -1
  105. package/utf8_boot.py +237 -0
@@ -0,0 +1,791 @@
1
+ # -*- coding: utf-8 -*-
2
+ """cons200 守卫:consistency.check 的选面(相关性预筛 + 保底面 + 超限可观测)。
3
+
4
+ 背景(设计稿签收点 5,第二实现批;2026-10-03 使用者签收):
5
+ `md_cg/consistency.py::check` 旧选面=`for nid, e in list(nodes.items())`
6
+ **索引序前 MAX_SCAN=200** 并静默 break——与 issue #52 同族(按索引序截断 +
7
+ 静默):在役库 17882 节点 ⇒ 检测面 ≈1.1%,且 id 字典序最前的 `code_*` 索引
8
+ 节点占满名额,真正相关的记忆节点(`mem_*` 在字典序后段)几乎从不进入比对
9
+ 面。写链三处(`writepipe._gate_consistency` / `mdcos.check_consistency` /
10
+ `mcp_server._consistency_call`)全走同一入口,缺陷面即全写入面。
11
+
12
+ 本批修法(契约,不得偏离的三条硬线):MAX_SCAN 数值**不动**(200)、检出集
13
+ **修后 ⊇ 修前**、预筛**不读盘**(快照级)。落地形态:
14
+ · 预筛(`_sift_score`,快照级)+ 唯一剔除档=「新节点未声明可展开条件
15
+ (tw_pos/tw_neg 皆空)时的非纪律候选」(可证无产出);
16
+ · 精比面=相关面前 limit ∪ 保底面(过滤后索引序前 limit ∩ 候选)——
17
+ 保底面即修前扫描面,逐位保留是「修后 ⊇ 修前」的结构性保证;
18
+ · 读数 scanned(预筛后候选数)/kept(实际精比数)/truncated(scanned>kept)
19
+ 超限附 hint。
20
+
21
+ 收口轮①(2026-10-03 复核 DEFER 后,设计侧裁决修法):剔除档加**面内例外**
22
+ ——剔除**永不触及修前扫描面**(过滤后索引序前 limit 内候选一律保留进候选面、
23
+ 落保底面通道):「修后检出 ⊇ 修前」由此**与「条目 tags 与盘面 fm.tags
24
+ 同源」无关**(预筛读条目快照、精比读盘面;非同源形态下面内候选仍进精比面、
25
+ 由盘面判据裁决)。面外非同源残余如实为已声明边界(见 G6/F2 与 `check`
26
+ docstring 诚实边界:修前亦检不出,不构成回归)。另修夹具收尾脏留问题:
27
+ `_Fixtures.close` 改为「从 `mdcg._LIVE_CGS` 摘除 + 优雅 close + 删临时目录」
28
+ ——原版删目录后被进程退出兜底(`_atexit_flush_all`)写回 `_index.json`/
29
+ `.lock`,每次运行残留一个 `mdcg_cons200_*` 目录。
30
+
31
+ 断言分组(每处判据都有定点变异把它打红,见 `_MUTATIONS`):
32
+ G1 选面:冲突对落索引尾也能检出(c1 面 + discipline 面);多层(layer);
33
+ 索引序选面回退即红(变异①重建修前选面,G1 的核心断言转红)。
34
+ G2 对拍:legacy(修前选面,运行时源码重建)vs 修后——修后检出集 ⊇ 修前,
35
+ 且新增检出**只能来自原被切面**(不在修前扫描面内的节点,逐条核验);
36
+ 修前扫描面内的检出(保底面)修后仍在。
37
+ G3 可观测:truncated/scanned/kept 口径(kept≤候选数、truncated⟺cand>kept、
38
+ 超限附 hint 且含「细化生效条件/不适用条件」「条件索引」语义);未超限
39
+ 不标记不附 hint;MCP 面(`_consistency_call`)同批读数透传。
40
+ G4 成本:预筛为**不读盘**的快照级操作——用读盘计数(包装 cg.get)断言
41
+ 「读盘数=kept」(预筛阶段零读盘)、「无条件内容读盘数降到纪律面
42
+ (1)而非修前 200」、相关候选全部进面(kept≥3)。
43
+ G5 语义红线:MAX_SCAN==200(数值红线);面内例外不误伤判定(无条件内容下
44
+ 面内声明候选与纪律候选仍比对);exclude 自排除语义保持;小库(未超限)
45
+ 四态照旧。(收口轮①同步:D3/E4 因「面内例外」语义变化改写数值与文案。)
46
+ G6 收口轮缺口闭合(复核 DEFER 两条件的守卫侧钉):F1 非同源形态(内存
47
+ 条目 tags 置空+盘面 tags=[discipline]+落修前扫描面内)无词面输入仍
48
+ REJECT(面内例外转正;去掉面内例外即红);F2 面外非同源残余与修前形态
49
+ 同判(均不检出,不构成回归,已声明边界);F3 无词面口径定点(scanned
50
+ =面内候选+面外纪律候选、kept=scanned、truncated=False、不附 hint);
51
+ F4 结构保证(无词面输入下面内候选**逐位**进入精比面/读盘面)。
52
+
53
+ 判别力自证(与 `md_cg/test_neg_condition_hits.py` / `test_issue52_*` 同口径):
54
+ · `--branch-baseline`:逐处**定点变异**(①选面回索引序截断 ②去掉 truncated
55
+ 上报 ③预筛把真冲突候选剔掉(假阴性)④MAX_SCAN 数值偷调大(契约禁止面)
56
+ ⑤剔除档失效 ⑥去掉修前扫描面例外(收口轮①面内例外失效——F1/F3 必红))
57
+ ——每处必须**恰好**打红预期条数的断言(多一条少一条都报红),且**所有**
58
+ 断言至少被一处变异打红(一条都没被打红的=空转,判 FAIL)。
59
+ 锚点漂移报 ANCHOR-MISS 且**退出码 2**(fail-closed,不静默失效)。
60
+ · **防误删自检(收口轮增补,对齐姊妹守卫 test_mode_parity / test_autonomy_modes)**:
61
+ `_mutation_table_gaps` 按编号钉 `_MUTATION_PINS`(①–⑥)核验表长与编号——
62
+ 删条目/改名/重排 ⇒ 三入口(main / --branch-baseline / --legacy-baseline)
63
+ 打印 TABLE-GAPS 并 **fail-closed 退出码 2**(此前删⑥为静默 rc=0,复核
64
+ uncovered 第 3 条)。复现(机械可跑):内存
65
+ `G._MUTATIONS = G._MUTATIONS[:-1]` 后 `G._mutation_table_gaps()` 必报
66
+ `len:5≠6` 与 `缺: ⑥`;经 `G.main()` / `G._branch_baseline` 入口得 rc=2。
67
+ · `--legacy-baseline`:把「修前形态」(索引序前 limit 选面)在运行时源码上
68
+ 重建,核心断言必须转红——证明本守卫抓得住 cons200 这个缺陷本身。注意
69
+ 收口轮①后:F1 在 legacy 形态下**绿**(HEAD 读盘面本就判 REJECT——F1 的
70
+ 红项钉是变异⑥,它不是「索引序截断」面的缺陷);F3 在 legacy 形态下红
71
+ (新口径为修后形态所定义)。legacy 判据仍是核心三条 A1/B2/B5 全红
72
+ (不要求恰好条数)。
73
+ · 基线源=**运行时源码**(`inspect.getsource(consistency)`),**不是 git
74
+ HEAD**——基线绑提交即失效(本仓已有两次教训);锚点都写在 `_MUTATIONS`
75
+ 里,实现改动致锚点漂移时会当场报 ANCHOR-MISS。
76
+ · 诚实边界(已知盲区,不假装覆盖):`rejection_terms` 只覆盖负条件面;
77
+ 「旧节点仅声明生效条件(正文 `# 生效条件:` 行)、无任何负条件迹象」的
78
+ 结点在快照里**无字段可判**,其与新内容负条件的冲突(c2/c3 面)在候选
79
+ 超限且该节点既不落修前扫描面、也不落相关面时可能不被精比——由
80
+ truncated 可观测(hint 给方向),不作「已覆盖」断言;**面外非同源形态
81
+ 残余**(F2)同为已声明边界(修前亦检不出,不构成回归)。
82
+
83
+ 运行:python -X utf8 -m md_cg.test_cons200_scan_selection
84
+ python -X utf8 -m md_cg.test_cons200_scan_selection --branch-baseline
85
+ python -X utf8 -m md_cg.test_cons200_scan_selection --legacy-baseline
86
+ """
87
+ from __future__ import annotations
88
+
89
+ import contextlib
90
+ import inspect
91
+ import io
92
+ import os
93
+ import shutil
94
+ import sys
95
+ import tempfile
96
+ import time
97
+ import types
98
+
99
+ from . import consistency as _cons
100
+ from . import mdcg as _mdcg_mod # 进程退出兜底登记(_LIVE_CGS)摘除用
101
+ from .mdcos import MdCGOS
102
+
103
+ S = _cons # 当前被测模块(变异模式 = exec 出的变异体命名空间)
104
+ _CUR_SRC = None # 变异模式下当前的变异体源码
105
+ _PASS = []
106
+ _FAIL = []
107
+
108
+
109
+ def ok(cond, label, extra=""):
110
+ (_PASS if cond else _FAIL).append(label)
111
+ print((" PASS " if cond else " FAIL ") + label +
112
+ ((" ← " + str(extra)[:200]) if (extra and not cond) else ""))
113
+
114
+
115
+ @contextlib.contextmanager
116
+ def _quiet():
117
+ with contextlib.redirect_stdout(io.StringIO()):
118
+ yield
119
+
120
+
121
+ # ---------------------------------------------------------------- 合成库(系统临时目录)
122
+ # 硬边界:不动任何在役数据根——库建在 tempfile.mkdtemp 下,用完即删。
123
+
124
+ def _mk_main(root, n_fill=260):
125
+ """264 节点:`a_conf` 索引序首位(修前扫描面**内**的冲突对)+ n_fill 条
126
+ 填充(无声明;session 多值=多会话形态)+索引序尾部的三个目标节点:
127
+ · zz_offline:负条件「生产环境」(c1 面冲突对,尾部);
128
+ · zz_disc :纪律节点(tags=discipline,禁令「删除生产数据」,尾部);
129
+ · b_ctx :contextual 层(layer 面)。
130
+ 候选数 = 1 + n_fill + 2 = n_fill+3 > MAX_SCAN(200) ⇒ 截断场景成立。
131
+ """
132
+ cg = MdCGOS(os.path.join(root, "main"))
133
+ cg.add("a_conf", "# 功能名:前置冲突\n# 生效条件:前置环境\n"
134
+ "# 不适用条件:生产环境\n",
135
+ layer="knowledge", non_applicable_conditions=["生产环境"],
136
+ session="sess_front")
137
+ for i in range(n_fill):
138
+ cg.add("f%04d" % i,
139
+ "# 功能名:填充 %d\n正文:填充节点 %d\n" % (i, i),
140
+ layer="knowledge", session=("sess_a" if i % 2 else "sess_b"))
141
+ cg.add("zz_offline", "# 功能名:离线批处理\n# 生效条件:离线环境\n"
142
+ "# 不适用条件:生产环境\n",
143
+ layer="knowledge", non_applicable_conditions=["生产环境"],
144
+ session="sess_tail")
145
+ cg.add("zz_disc", "# 功能名:生产纪律\n# 执行:禁止删除生产数据\n",
146
+ layer="knowledge", tags=["discipline"],
147
+ non_applicable_conditions=["删除生产数据"], session="sess_tail")
148
+ cg.add("b_ctx", "# 功能名:语境节点\n# 生效条件:语境残留时\n"
149
+ "# 不适用条件:语境残留\n",
150
+ layer="contextual", non_applicable_conditions=["语境残留"],
151
+ session="sess_ctx")
152
+ cg.flush()
153
+ return cg
154
+
155
+
156
+ def _mk_small(root):
157
+ """8 节点小库:一条有条件冲突对(未超限路径)。"""
158
+ cg = MdCGOS(os.path.join(root, "small"))
159
+ for i in range(6):
160
+ cg.add("s%02d" % i, "# 功能名:小填充 %d\n正文:小填充 %d\n" % (i, i),
161
+ layer="knowledge", session="sess_s")
162
+ cg.add("s_off", "# 功能名:小冲突\n# 不适用条件:生产环境\n",
163
+ layer="knowledge", non_applicable_conditions=["生产环境"])
164
+ cg.add("s_oth", "# 功能名:小另层\n# 不适用条件:生产环境\n",
165
+ layer="contextual", non_applicable_conditions=["生产环境"])
166
+ cg.flush()
167
+ return cg
168
+
169
+
170
+ def _mk_gapface(root):
171
+ """收口轮 G6 专用小库(非同源形态 · 面内):`q_ns`=非合作写者形态节点
172
+ ——盘面 fm.tags=[discipline]+禁令「删除生产数据」(cg.add 正常写入),
173
+ 运行时把**内存索引条目** tags 置空(模拟条目陈化/非合作写者直改 .md 不
174
+ 重建索引);小库(7 节点 ≤ limit)⇒ 该节点必在修前扫描面**内**。
175
+ 条目改动由调用方 finally 还原(共享 fixture 不残留)。"""
176
+ cg = MdCGOS(os.path.join(root, "gapface"))
177
+ for i in range(5):
178
+ cg.add("g%02d" % i, "# 功能名:面内填充 %d\n正文:填充 %d\n" % (i, i),
179
+ layer="knowledge", session="sess_g")
180
+ cg.add("q_ns", "# 功能名:生产纪律(非合作写者形态)\n"
181
+ "# 执行:禁止删除生产数据\n",
182
+ layer="knowledge", tags=["discipline"],
183
+ non_applicable_conditions=["删除生产数据"], session="sess_g")
184
+ cg.add("q_fill", "# 功能名:尾部填充\n正文:填充\n",
185
+ layer="knowledge", session="sess_g")
186
+ cg.flush()
187
+ return cg
188
+
189
+
190
+ def _mk_tail_residual(root):
191
+ """收口轮 G6 专用库(非同源形态 · 面外):261 节点;`t_ns`=非合作写者
192
+ 形态节点(盘面 discipline、条目 tags 置空)落**修前扫描面外**——钉住
193
+ 「面外残余」:修前形态亦检不出(不构成回归)。"""
194
+ cg = MdCGOS(os.path.join(root, "tailns"))
195
+ for i in range(260):
196
+ cg.add("t%04d" % i, "# 功能名:填充 %d\n正文:填充 %d\n" % (i, i),
197
+ layer="knowledge", session="sess_t")
198
+ cg.add("t_ns", "# 功能名:生产纪律(面外非合作写者形态)\n"
199
+ "# 执行:禁止删除生产数据\n",
200
+ layer="knowledge", tags=["discipline"],
201
+ non_applicable_conditions=["删除生产数据"], session="sess_t")
202
+ cg.flush()
203
+ return cg
204
+
205
+
206
+ class _Fixtures:
207
+ """一次构建、多轮复用(变异核验要重跑 N 轮)——库面与变异面正交。"""
208
+
209
+ def __init__(self):
210
+ self.tmp = tempfile.mkdtemp(prefix="mdcg_cons200_")
211
+ self.main = _mk_main(self.tmp)
212
+ self.small = _mk_small(self.tmp)
213
+ self.gapface = _mk_gapface(self.tmp) # G6/F1:非同源·面内
214
+ self.tail = _mk_tail_residual(self.tmp) # G6/F2:非同源·面外残余
215
+
216
+ def close(self):
217
+ """收口轮①(2026-10-03)补:先优雅关闭各 fixture 实例并**从 md_cg 的
218
+ 进程退出兜底登记(`mdcg._LIVE_CGS`)摘除**,再删临时目录。
219
+
220
+ 为什么必须摘除:实例存活时,进程退出会触发 `mdcg._atexit_flush_all`
221
+ → 对每个存活实例 `close()` → compact/rebuild 把 `_index.json`(含
222
+ `.lock`)**写回刚被删掉的临时根**(makedirs 重建目录)——实测每次
223
+ 运行残留一个 `mdcg_cons200_*` 目录(每库 2 个文件);摘除后退出兜底
224
+ 不再写,删目录才真正干净(本修复实测三连跑零残留)。"""
225
+ for name in ("main", "small", "gapface", "tail"):
226
+ cg = getattr(self, name, None)
227
+ if cg is None:
228
+ continue
229
+ live = getattr(_mdcg_mod, "_LIVE_CGS", None)
230
+ try:
231
+ if live is not None:
232
+ live.discard(cg)
233
+ except Exception: # noqa: BLE001 兜底路径不该再抛
234
+ pass
235
+ try:
236
+ cg.close()
237
+ except Exception: # noqa: BLE001 同款:清理不该再抛
238
+ pass
239
+ for _ in range(3):
240
+ shutil.rmtree(self.tmp, ignore_errors=True)
241
+ if not os.path.exists(self.tmp):
242
+ break
243
+ time.sleep(0.05)
244
+
245
+
246
+ # ---------------------------------------------------------------- 场景集与检出集
247
+
248
+ _CASES = (
249
+ # (名, layer, 待检内容, 期望检出节点)
250
+ ("P1", "knowledge", "# 功能名:生产批处理\n# 生效条件:生产环境\n",
251
+ "zz_offline"),
252
+ ("P2", "knowledge", "删除生产数据", "zz_disc"),
253
+ ("P3", "contextual", "# 功能名:语境待检\n# 生效条件:语境残留\n",
254
+ "b_ctx"),
255
+ )
256
+ _CASE_LAYER = {c[0]: c[1] for c in _CASES}
257
+
258
+
259
+ def _detect(mod, cg):
260
+ """跑场景集,返回检出集 {(case, with_id, type)}(with 为空的自否定不计)。"""
261
+ out = set()
262
+ for name, layer, content, _exp in _CASES:
263
+ r = mod.check(cg, content, layer=layer, depth=0)
264
+ for c in (r.get("conflicts") or []):
265
+ if c.get("with"):
266
+ out.add((name, str(c["with"]), str(c.get("type"))))
267
+ return out
268
+
269
+
270
+ def _pre_face(cg, layer, limit=None):
271
+ """修前扫描面(**与修前实现逐位同口径**):exclude/layer 过滤后的索引序
272
+ 前 limit 名。守卫据此核验「新增检出只能来自原被切面」。"""
273
+ lim = int(_cons.MAX_SCAN if limit is None else limit)
274
+ face = []
275
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
276
+ if layer and e.get("layer") != layer:
277
+ continue
278
+ if len(face) >= lim:
279
+ break
280
+ face.append(nid)
281
+ return face
282
+
283
+
284
+ def _face_and_disc(cg, layer=None):
285
+ """独立重算(不依赖被测实现):过滤后索引序前 limit 内候选数 face_n,
286
+ 与面外条目中 `_is_discipline` 判真者数 disc_out——无词面(no_terms)输入的
287
+ 候选面口径=face_n+disc_out(收口轮①面内例外:面内一律保留、面外仅纪律)。"""
288
+ lim = int(_cons.MAX_SCAN)
289
+ face_n = disc_out = seen = 0
290
+ # H-4(a):从 cg.index 取 nodes 先取快照(与守卫全域纪律一致)
291
+ for nid, e in list((cg.index.get("nodes") or {}).items()):
292
+ if layer and e.get("layer") != layer:
293
+ continue
294
+ if seen < lim:
295
+ face_n += 1
296
+ elif _cons._is_discipline({"tags": e.get("tags")}, nid):
297
+ disc_out += 1
298
+ seen += 1
299
+ return face_n, disc_out
300
+
301
+
302
+ def _clear_entry_tags(cg, nid):
303
+ """把内存索引条目的 tags 置空(模拟**非同源形态**:盘面有、条目没有),
304
+ 返回原值供还原。"""
305
+ e = cg.index["nodes"].get(nid) or {}
306
+ old = e.get("tags")
307
+ e["tags"] = []
308
+ return old
309
+
310
+
311
+ def _restore_entry_tags(cg, nid, old):
312
+ e = cg.index["nodes"].get(nid) or {}
313
+ if old is None:
314
+ e.pop("tags", None)
315
+ else:
316
+ e["tags"] = old
317
+
318
+
319
+ class _CountGet:
320
+ """读盘计数器:转发一切到真库,给 cg.get 计数并记 id(成本/结构断言用)。"""
321
+
322
+ def __init__(self, cg):
323
+ d = self.__dict__
324
+ d["_cg"] = cg
325
+ d["gets"] = 0
326
+ d["ids"] = []
327
+
328
+ def __getattr__(self, k):
329
+ return getattr(self.__dict__["_cg"], k)
330
+
331
+ def get(self, nid):
332
+ self.__dict__["gets"] += 1
333
+ self.__dict__["ids"].append(nid)
334
+ return self.__dict__["_cg"].get(nid)
335
+
336
+
337
+ # ---------------------------------------------------------------- G1 选面
338
+ def g1_selection(fx):
339
+ print("== G1 选面:冲突对落索引尾也能检出(相关面)==")
340
+ r1 = S.check(fx.main, _CASES[0][2], layer="knowledge", depth=0)
341
+ got1 = {str(c["with"]) for c in r1.get("conflicts") or [] if c.get("with")}
342
+ ok("zz_offline" in got1,
343
+ "A1 索引序尾部的冲突对(c1 面)被检出(修前扫描面读不到它;回退即红)",
344
+ (sorted(got1), r1.get("verdict")))
345
+ ok(r1.get("verdict") == "DEFER" and r1.get("conflict_strength", 0) >= 0.6,
346
+ "A2 尾部冲突对使判定进入 DEFER/强度 ≥ CLASH_HIGH(修前为 BLINDSPOT/0)",
347
+ (r1.get("verdict"), r1.get("conflict_strength")))
348
+
349
+ r2 = S.check(fx.main, _CASES[1][2], layer="knowledge", depth=0)
350
+ got2 = {str(c["with"]) for c in r2.get("conflicts") or [] if c.get("with")}
351
+ ok("zz_disc" in got2 and r2.get("verdict") == "REJECT",
352
+ "A3 索引序尾部的纪律节点(discipline 面)被检出(REJECT)",
353
+ (sorted(got2), r2.get("verdict")))
354
+
355
+ r3 = S.check(fx.main, _CASES[2][2], layer="contextual", depth=0)
356
+ got3 = {str(c["with"]) for c in r3.get("conflicts") or [] if c.get("with")}
357
+ ok("b_ctx" in got3,
358
+ "A4 layer 面:contextual 层冲突对在层过滤后照常检出",
359
+ (sorted(got3), r3.get("verdict")))
360
+
361
+
362
+ # ---------------------------------------------------------------- G2 对拍
363
+ def g2_parity(fx):
364
+ print("== G2 对拍:修后检出集 ⊇ 修前(legacy 形态运行时重建)==")
365
+ mod, raw_ns, src = _build_mutant(_LEGACY_REPLS)
366
+ if mod is None:
367
+ # 锚点已不在(如变异①下当前形态即修前形态):修前检出集=当前形态
368
+ r_old = _detect(S, fx.main)
369
+ else:
370
+ saved = _activate(mod, raw_ns, src)
371
+ try:
372
+ r_old = _detect(S, fx.main) # S 已是 legacy 形态
373
+ finally:
374
+ _deactivate(saved)
375
+ r_new = _detect(S, fx.main) # 当前(修后)形态
376
+ ok(r_old and r_old <= r_new,
377
+ "B1 修后检出集 ⊇ 修前(legacy 重建:索引序前 limit 选面)",
378
+ (sorted(r_old), sorted(r_new)))
379
+ new_items = sorted(r_new - r_old)
380
+ ok(bool(new_items),
381
+ "B2 修复有效:存在「修前漏检、修后检出」的新增检出", new_items)
382
+ bad = []
383
+ for case, wid, _typ in new_items:
384
+ face = _pre_face(fx.main, _CASE_LAYER.get(case))
385
+ if wid in face:
386
+ bad.append((case, wid))
387
+ ok(bool(new_items) and not bad,
388
+ "B3 新增检出非空且**只能来自原被切面**(逐条核验:不在修前扫描面内)",
389
+ (new_items, bad))
390
+ ok(bool(r_old) and all(item in r_new for item in r_old),
391
+ "B4 修前扫描面内的检出(保底面)修后逐条仍在", sorted(r_old))
392
+ ok(("P1", "zz_offline", "condition_clash") in r_new
393
+ and ("P1", "zz_offline", "condition_clash") not in r_old,
394
+ "B5 尾部冲突对:修前漏检、修后检出(缺陷取证的守卫内复现)",
395
+ (sorted(r_old), sorted(r_new)))
396
+
397
+
398
+ # ---------------------------------------------------------------- G3 可观测
399
+ def g3_reads(fx):
400
+ print("== G3 可观测:truncated/scanned/kept/hint 与 MCP 透传 ==")
401
+ r = S.check(fx.main, _CASES[0][2], layer="knowledge", depth=0)
402
+ scanned, kept, trunc = r.get("scanned"), r.get("kept"), r.get("truncated")
403
+ ok(trunc is True,
404
+ "C1 候选超限 ⇒ truncated=True(修前无任何标记)", trunc)
405
+ ok(isinstance(scanned, int) and isinstance(kept, int)
406
+ and scanned > kept,
407
+ "C2 口径:scanned>kept(scanned=预筛后候选数、kept=实际精比数)",
408
+ (scanned, kept))
409
+ ok(isinstance(kept, int) and kept >= 3
410
+ and kept <= 2 * _cons.MAX_SCAN,
411
+ "C3 kept 覆盖全部相关候选(≥3:a_conf/zz_offline/zz_disc)且 ≤2×MAX_SCAN",
412
+ (kept, _cons.MAX_SCAN))
413
+ hint = r.get("hint") or ""
414
+ ok(bool(hint) and "细化生效条件" in hint and "不适用条件" in hint
415
+ and "条件索引" in hint,
416
+ "C4 截断附可操作 hint(细化生效条件/不适用条件 or 建立条件索引)", hint)
417
+
418
+ rs = S.check(fx.small, "# 功能名:小批处理\n# 生效条件:生产环境\n",
419
+ layer="knowledge", depth=0)
420
+ ok(rs.get("truncated") is False and "hint" not in rs
421
+ and rs.get("scanned") == rs.get("kept"),
422
+ "C5 未超限:truncated=False、不附 hint、scanned==kept(口径自洽)",
423
+ {k: rs.get(k) for k in ("scanned", "kept", "truncated")})
424
+
425
+ from .mcp_server import _consistency_call
426
+ mcp = _consistency_call(fx.main, {"action": "check",
427
+ "content": _CASES[0][2],
428
+ "layer": "knowledge", "depth": 0})
429
+ ok(mcp.get("truncated") is True and isinstance(mcp.get("kept"), int)
430
+ and isinstance(mcp.get("scanned"), int) and bool(mcp.get("hint")),
431
+ "C6 MCP 面(_consistency_call)同批读数随返回体透传(旧调用方不读新键不受影响)",
432
+ {k: mcp.get(k) for k in ("scanned", "kept", "truncated")})
433
+
434
+
435
+ # ---------------------------------------------------------------- G4 成本
436
+ def g4_cost(fx):
437
+ print("== G4 成本:预筛不读盘(读盘计数断言,不用时间阈值)==")
438
+ c1 = _CountGet(fx.main)
439
+ r1 = S.check(c1, _CASES[0][2], layer="knowledge", depth=0)
440
+ ok(c1.gets == r1.get("kept") and c1.gets < len(fx.main.index["nodes"]),
441
+ "D1 读盘数=kept(预筛/选面阶段零读盘;未全量读盘)",
442
+ (c1.gets, r1.get("kept"), len(fx.main.index["nodes"])))
443
+ ok(isinstance(r1.get("kept"), int) and r1.get("kept") >= 3,
444
+ "D2 相关候选(a_conf/zz_offline/zz_disc)全部进入精比面(kept≥3)",
445
+ r1.get("kept"))
446
+
447
+ c2 = _CountGet(fx.main)
448
+ r2 = S.check(c2, _CASES[1][2], layer="knowledge", depth=0)
449
+ fn, dout = _face_and_disc(fx.main, "knowledge")
450
+ ok(c2.gets == r2.get("kept") == r2.get("scanned") == fn + dout
451
+ and r2.get("truncated") is False,
452
+ "D3 无条件内容:剔除档只剔**面外**非纪律候选(面内一律保留——收口轮①"
453
+ "面内例外;读盘=kept=scanned=%d=面内 %d+面外纪律 %d;修前为 200 次"
454
+ "且面外纪律漏检)——纪律候选(zz_disc)保留" % (fn + dout, fn, dout),
455
+ (c2.gets, r2.get("kept"), r2.get("scanned")))
456
+
457
+ c3 = _CountGet(fx.small)
458
+ r3 = S.check(c3, "# 功能名:小批处理\n# 生效条件:生产环境\n",
459
+ layer="knowledge", depth=0)
460
+ ok(c3.gets == r3.get("kept") and c3.gets <= len(fx.small.index["nodes"]),
461
+ "D4 小库溯源:读盘数=kept 且不超过全库节点数(快照级候选面)",
462
+ (c3.gets, r3.get("kept"), len(fx.small.index["nodes"])))
463
+
464
+
465
+ # ---------------------------------------------------------------- G5 语义红线
466
+ def g5_redlines(fx):
467
+ print("== G5 语义红线:MAX_SCAN / 剔除档安全 / exclude / 四态 ==")
468
+ ok(_cons.MAX_SCAN == 200,
469
+ "E1 MAX_SCAN==200 一字未动(契约禁止面:数值放大不是修法)",
470
+ _cons.MAX_SCAN)
471
+
472
+ r = S.check(fx.main, _CASES[0][2], layer="knowledge", depth=0,
473
+ exclude="zz_offline")
474
+ got = {str(c["with"]) for c in r.get("conflicts") or [] if c.get("with")}
475
+ ok("zz_offline" not in got and "a_conf" in got,
476
+ "E2 exclude 自排除语义保持(被排除节点不检出不比对;其余照常)",
477
+ sorted(got))
478
+
479
+ rs = S.check(fx.small, "# 功能名:无冲突小批\n# 生效条件:完全无关条件\n",
480
+ layer="knowledge", depth=0)
481
+ ok(rs.get("verdict") == "ACCEPT" and rs.get("conflict_strength") == 0.0,
482
+ "E3 四态照旧:无关条件 → ACCEPT/0.0(小库未超限路径)",
483
+ (rs.get("verdict"), rs.get("conflict_strength")))
484
+
485
+ rn = S.check(fx.main, "这是一条无条件的普通记录", layer="knowledge",
486
+ depth=0)
487
+ fn, dout = _face_and_disc(fx.main, "knowledge")
488
+ ok(rn.get("verdict") == "ACCEPT" and rn.get("comparable") == 2
489
+ and rn.get("scanned") == fn + dout,
490
+ "E4 面内例外不误伤:无条件内容下面内声明候选与纪律候选仍比对"
491
+ "(comparable=2:a_conf/zz_disc)、判定 ACCEPT 不误报 BLINDSPOT;"
492
+ "scanned=面内+面外纪律(收口轮①口径)",
493
+ (rn.get("verdict"), rn.get("comparable"), rn.get("scanned")))
494
+
495
+
496
+ # ---------------------------------------------------------------- G6 收口轮缺口闭合
497
+ def g6_gapfix(fx):
498
+ print("== G6 收口轮缺口闭合:面内例外(F1)+面外残余(F2)+口径(F3)+结构保证(F4)==")
499
+ # ---- F1 转正:非同源形态(条目 tags 置空+盘面 discipline)落修前扫描面内 ----
500
+ old1 = _clear_entry_tags(fx.gapface, "q_ns")
501
+ try:
502
+ r1 = S.check(fx.gapface, "删除生产数据", layer="knowledge", depth=0)
503
+ finally:
504
+ _restore_entry_tags(fx.gapface, "q_ns", old1)
505
+ got1 = {str(c["with"]) for c in r1.get("conflicts") or [] if c.get("with")}
506
+ ok("q_ns" in got1 and r1.get("verdict") == "REJECT",
507
+ "F1 面内例外转正(非同源形态):条目 tags 置空+盘面 tags=[discipline]+"
508
+ "落修前扫描面内 ⇒ 无词面输入下仍检出 REJECT(收口轮①;变异⑥去面内"
509
+ "例外即红)",
510
+ (sorted(got1), r1.get("verdict")))
511
+
512
+ # ---- F2 面外残余:与修前形态(legacy 重建)同判(不构成回归,已声明边界) ----
513
+ old2 = _clear_entry_tags(fx.tail, "t_ns")
514
+ drift = False
515
+ try:
516
+ r_new = S.check(fx.tail, "删除生产数据", layer="knowledge", depth=0)
517
+ mod, raw_ns, src = _build_mutant(_LEGACY_REPLS)
518
+ if mod is None:
519
+ # 未变异上下文里重建失败=锚点漂移(实现改了、变异表未同步):
520
+ # 记 drift 使本断言转红(fail-closed);变异①下 _CUR_SRC 非空
521
+ # (当前形态即修前形态)属预期回落,不算漂移。
522
+ drift = _CUR_SRC is None
523
+ r_old = r_new
524
+ else:
525
+ saved = _activate(mod, raw_ns, src)
526
+ try:
527
+ r_old = S.check(fx.tail, "删除生产数据", layer="knowledge",
528
+ depth=0)
529
+ finally:
530
+ _deactivate(saved)
531
+ finally:
532
+ _restore_entry_tags(fx.tail, "t_ns", old2)
533
+ got_n = {str(c["with"]) for c in r_new.get("conflicts") or []
534
+ if c.get("with")}
535
+ got_o = {str(c["with"]) for c in r_old.get("conflicts") or []
536
+ if c.get("with")}
537
+ ok((not drift) and "t_ns" not in got_n and "t_ns" not in got_o
538
+ and got_n == got_o,
539
+ "F2 面外非同源残余:修后与修前形态同判(均不检出——修前亦检不出,"
540
+ "不构成回归;预筛读条目快照、协作写者协议下同源),已声明边界"
541
+ "(修前形态重建锚点漂移 ⇒ 本断言转红,fail-closed)",
542
+ (sorted(got_n), sorted(got_o), "drift" if drift else "ok"))
543
+
544
+ # ---- F3 无词面口径定点(收口轮②) ----
545
+ fn, dout = _face_and_disc(fx.main, "knowledge")
546
+ c3 = _CountGet(fx.main)
547
+ r3 = S.check(c3, _CASES[1][2], layer="knowledge", depth=0)
548
+ ok(r3.get("scanned") == fn + dout and r3.get("kept") == r3.get("scanned")
549
+ and r3.get("truncated") is False and "hint" not in r3
550
+ and c3.gets == r3.get("kept"),
551
+ "F3 无词面口径定点:scanned=面内候选+面外纪律候选(独立重算 %d+%d)、"
552
+ "kept=scanned、truncated=False、不附 hint、读盘=kept" % (fn, dout),
553
+ {k: r3.get(k) for k in ("scanned", "kept", "truncated")})
554
+
555
+ # ---- F4 结构保证:面内候选逐位进入精比面(读盘 id 集合 ⊇ 面内集合) ----
556
+ face_ids = _pre_face(fx.main, "knowledge")
557
+ c4 = _CountGet(fx.main)
558
+ S.check(c4, _CASES[1][2], layer="knowledge", depth=0)
559
+ ok(bool(face_ids) and set(c4.ids) >= set(face_ids),
560
+ "F4 结构保证:无词面输入下修前扫描面(%d 名)逐位进入精比面/读盘面"
561
+ "——与「条目/盘面同源」无关的结构保证(收口轮①)" % len(face_ids),
562
+ (len(set(c4.ids)), len(face_ids)))
563
+
564
+
565
+ _GROUPS = (g1_selection, g2_parity, g3_reads, g4_cost, g5_redlines, g6_gapfix)
566
+
567
+
568
+ def _run_groups(fx) -> list:
569
+ """跑全部分组,返回失败标签列表(静默模式复用;单组异常记为 EXC 标签)。"""
570
+ _PASS.clear()
571
+ _FAIL.clear()
572
+ for g in _GROUPS:
573
+ try:
574
+ g(fx)
575
+ except Exception as e: # noqa: BLE001 变异可能炸——记标签不中断
576
+ label = "EXC:%s:%s" % (g.__name__, type(e).__name__)
577
+ _FAIL.append(label)
578
+ print(" FAIL " + label + " ← " + str(e)[:160])
579
+ return list(_FAIL)
580
+
581
+
582
+ # ---------------------------------------------------------------- 定点变异表
583
+ # 每条 = (名字, [(旧源码, 新源码), ...], 预期打红条数)。
584
+ # 锚点缺失 ⇒ ANCHOR-MISS + 退出码 2(fail-closed);预期条数不符 ⇒ 报红(「恰好」语义)。
585
+ # 预期值为 2026-10-03 实测校准(六处变异各自恰好命中、无空转断言;收口轮①
586
+ # 新增 ⑥「去掉修前扫描面例外」并把 ①–⑤ 的预期值按新增/改写断言重校)。
587
+ _LEGACY_REPLS = [
588
+ (" scan_face = [(t[3], t[4]) for t in rel]\n"
589
+ " scan_face += [(t[3], t[4]) for t in cand if t[2] and t[3] not in rel_ids]\n",
590
+ " scan_face = [(t[3], t[4]) for t in cand if t[2]]\n"),
591
+ ]
592
+ _M2 = [
593
+ ('"missing": missing, "scanned": scanned, "kept": kept,\n'
594
+ ' "truncated": truncated, "comparable": comparable,\n',
595
+ '"missing": missing, "scanned": scanned, "comparable": comparable,\n'),
596
+ (' if truncated:\n rec["hint"] = _SCAN_HINT % (scanned, lim, kept)\n',
597
+ ' pass # 变异:去掉 hint 上报\n'),
598
+ ]
599
+ _M3 = [
600
+ (" if no_terms and not disc and not in_before:\n continue\n",
601
+ " if not disc:\n continue\n"),
602
+ ]
603
+ #: 收口轮①的判别力钉:去掉面内例外(回到首轮形态)——F1/F3 断言必红。
604
+ _M6 = [
605
+ (" if no_terms and not disc and not in_before:\n continue\n",
606
+ " if no_terms and not disc:\n continue\n"),
607
+ ]
608
+
609
+ #: 变异表完整性自检的编号钉(收口轮增补,对齐姊妹守卫 test_mode_parity /
610
+ #: test_autonomy_modes):表长/编号与其不一致 ⇒ TABLE-GAPS + fail-closed 退出码 2。
611
+ _MUTATION_PINS = ("①", "②", "③", "④", "⑤", "⑥")
612
+
613
+
614
+ # 生效条件:entries 为形如 _MUTATIONS 的序列((名字, 替换式, 预期红项数),名字以编号打头)或缺省(取本模块 _MUTATIONS)时,按 _MUTATION_PINS 逐位核验长度与编号(缺/重复/顺序错位),返回缺口说明列表(空=完好);只判「表被删改」,不判锚点漂移(后者归 _build_mutant 的 ANCHOR-MISS)。
615
+ def _mutation_table_gaps(entries=None):
616
+ """变异表完整性自检(收口轮增补):防「删条目/改名/重排」静默降级。
617
+
618
+ 由来(第一轮复核 uncovered 第 3 条):内存删除一条变异(⑥)时 rc=0 不转红
619
+ ——其余变异覆盖了其标签,删条目无人察觉;姊妹守卫 test_mode_parity /
620
+ test_autonomy_modes 均有防误删自检,本守卫原缺。本判据把它变成机械铆钉:
621
+ 表长/编号与 `_MUTATION_PINS` 不一致 ⇒ 入口处打印 TABLE-GAPS 并
622
+ **fail-closed 退出码 2**(三个入口 main / _branch_baseline / _legacy_baseline
623
+ 同层接入)。判别力自证(可复跑,见 docstring「防误删自检」节):
624
+ `G._MUTATIONS = G._MUTATIONS[:-1]` 后调本函数必报缺口。
625
+ """
626
+ es = _MUTATIONS if entries is None else entries
627
+ gaps = []
628
+ if len(es) != len(_MUTATION_PINS):
629
+ gaps.append("len:%d≠%d" % (len(es), len(_MUTATION_PINS)))
630
+ for i, (e, pid) in enumerate(zip(es, _MUTATION_PINS)):
631
+ if not str(e[0]).startswith(pid):
632
+ gaps.append("序%d 编号不符: %r ≠ %s…" % (i, str(e[0])[:12], pid))
633
+ seen = []
634
+ for e in es:
635
+ for pid in _MUTATION_PINS:
636
+ if str(e[0]).startswith(pid) and pid not in seen:
637
+ seen.append(pid)
638
+ for pid in _MUTATION_PINS:
639
+ if pid not in seen:
640
+ gaps.append("缺: %s" % pid)
641
+ return gaps
642
+
643
+
644
+ _MUTATIONS = (
645
+ ("①选面回索引序截断(修前形态)", _LEGACY_REPLS, 8),
646
+ ("②去掉 truncated/kept 上报与 hint", _M2, 11),
647
+ ("③预筛把真冲突候选剔掉(假阴性)", _M3, 19),
648
+ ("④MAX_SCAN 数值偷调大(契约禁止面)",
649
+ [("MAX_SCAN = 200", "MAX_SCAN = 2000")], 10),
650
+ ("⑤剔除档失效(预筛退化,读盘面回到全候选)",
651
+ [(" if no_terms and not disc and not in_before:\n continue\n",
652
+ " if False:\n continue\n")], 4),
653
+ ("⑥去掉修前扫描面例外(收口轮①面内例外失效)", _M6, 5),
654
+ )
655
+
656
+
657
+ def _build_mutant(replacements):
658
+ """在**运行时源码**上做替换并 exec 成模块命名空间。
659
+
660
+ 返回 (attr_ns, raw_ns, src);锚点缺失返回 (None, None, None)。
661
+ """
662
+ src = _CUR_SRC if _CUR_SRC is not None else inspect.getsource(_cons)
663
+ for old, new in replacements:
664
+ if old not in src:
665
+ return None, None, None
666
+ src = src.replace(old, new)
667
+ ns = {"__name__": "md_cg.consistency_mutant", "__package__": "md_cg",
668
+ "__file__": getattr(_cons, "__file__", "<consistency>")}
669
+ exec(compile(src, "<consistency-mutant>", "exec"), ns) # noqa: S102 测试内变异自证
670
+ return types.SimpleNamespace(**ns), ns, src
671
+
672
+
673
+ def _activate(mod, raw_ns, src):
674
+ """把变异体的可调用面临时挂上真源模块(mdcos/mcp_server 经模块属性取用)。"""
675
+ global S, _CUR_SRC
676
+ saved = {}
677
+ for k in list(_cons.__dict__):
678
+ if k.startswith("__") or k not in raw_ns:
679
+ continue
680
+ saved[k] = _cons.__dict__[k]
681
+ setattr(_cons, k, raw_ns[k])
682
+ S = mod
683
+ _CUR_SRC = src
684
+ return saved
685
+
686
+
687
+ def _deactivate(saved):
688
+ global S, _CUR_SRC
689
+ for k, v in saved.items():
690
+ setattr(_cons, k, v)
691
+ S = _cons
692
+ _CUR_SRC = None
693
+
694
+
695
+ def _branch_baseline(fx) -> int:
696
+ print("!! 定点变异自证:逐处变异,断言须**恰好**打红预期条数;锚点漂移=ANCHOR-MISS+退出码 2\n")
697
+ gaps = _mutation_table_gaps()
698
+ if gaps:
699
+ print(" TABLE-GAPS —— 变异表完整性自检未过(防误删):%s" % "、".join(gaps))
700
+ return 2
701
+ # 锚点预检(fail-closed,收口轮①补):修前形态锚点必须能在**当前源码**上重建
702
+ # ——否则是「实现改了、变异表未同步」的锚点漂移:若不做预检,干净基线的
703
+ # g2 会先把它吞成 B2 红(rc=1)而非 ANCHOR-MISS(rc=2),漂移被误报为
704
+ # 「修复无效」。故先于一切断言直接判漂移。
705
+ if _build_mutant(_LEGACY_REPLS)[0] is None:
706
+ print(" ANCHOR-MISS 修前形态重建锚点(_LEGACY_REPLS)——实现改了却没同步变异表")
707
+ return 2
708
+ with _quiet():
709
+ clean_red = _run_groups(fx)
710
+ all_labels = set(_PASS) | set(_FAIL)
711
+ if clean_red:
712
+ print(" 未变异基线即失败:%s" % clean_red)
713
+ return 1
714
+ print(" 未变异基线:%d 条断言全绿" % len(all_labels))
715
+ reddened = set()
716
+ bad = []
717
+ for name, repls, expect in _MUTATIONS:
718
+ mod, raw_ns, src = _build_mutant(repls)
719
+ if mod is None:
720
+ print(" ANCHOR-MISS %s —— 变异锚点漂移(实现改了却没同步 _MUTATIONS)" % name)
721
+ return 2
722
+ saved = _activate(mod, raw_ns, src)
723
+ try:
724
+ with _quiet():
725
+ reds = _run_groups(fx)
726
+ finally:
727
+ _deactivate(saved)
728
+ reddened |= set(reds)
729
+ hit = len(reds) == expect
730
+ print(" 变异「%s」→ 打红 %d 条(预期 %d)%s"
731
+ % (name, len(reds), expect, "OK" if hit else "**不符**"))
732
+ if not hit:
733
+ bad.append("%s:红 %d ≠ 预期 %d" % (name, len(reds), expect))
734
+ for r in sorted(reds):
735
+ print(" red: " + r)
736
+ never = sorted(all_labels - reddened)
737
+ if never:
738
+ print(" **空转断言(无任何变异能打红)**:%s" % "、".join(never))
739
+ bad.append("空转断言 %d 条" % len(never))
740
+ print("\n判别力自证:%s" % ("PASS(六处变异各自恰好命中,且无空转断言)"
741
+ if not bad else "FAIL —— " + ";".join(bad)))
742
+ return 0 if not bad else 1
743
+
744
+
745
+ def _legacy_baseline(fx) -> int:
746
+ print("!! 修前形态重建(索引序前 limit 选面):核心断言应当转红\n")
747
+ gaps = _mutation_table_gaps()
748
+ if gaps:
749
+ print(" TABLE-GAPS —— 变异表完整性自检未过(防误删):%s" % "、".join(gaps))
750
+ return 2
751
+ mod, raw_ns, src = _build_mutant(_LEGACY_REPLS)
752
+ if mod is None:
753
+ print(" ANCHOR-MISS —— 修前形态重建的锚点漂移")
754
+ return 2
755
+ saved = _activate(mod, raw_ns, src)
756
+ try:
757
+ with _quiet():
758
+ reds = _run_groups(fx)
759
+ finally:
760
+ _deactivate(saved)
761
+ print(" 修前形态下打红 %d 条:%s" % (len(reds), "、".join(sorted(reds))))
762
+ core = {l for l in reds
763
+ if l.startswith("A1 ") or l.startswith("B5 ") or l.startswith("B2 ")}
764
+ miss = {"A1/B2/B5(选面与对拍的核心断言)"} if len(core) < 3 else set()
765
+ print("\n修前形态判定:%s" % ("PASS(核心断言转红——本守卫抓得住 cons200 本身)"
766
+ if not miss else "FAIL —— 核心断言未转红:%s" % miss))
767
+ return 0 if not miss else 1
768
+
769
+
770
+ def main() -> int:
771
+ gaps = _mutation_table_gaps()
772
+ if gaps:
773
+ print(" TABLE-GAPS —— 变异表完整性自检未过(防误删):%s" % "、".join(gaps))
774
+ print(" fail-closed 退出码 2(对齐姊妹守卫防误删自检;复现:内存删 "
775
+ "_MUTATIONS 一条后调本入口)")
776
+ return 2
777
+ fx = _Fixtures()
778
+ try:
779
+ if "--branch-baseline" in sys.argv:
780
+ return _branch_baseline(fx)
781
+ if "--legacy-baseline" in sys.argv:
782
+ return _legacy_baseline(fx)
783
+ _run_groups(fx)
784
+ finally:
785
+ fx.close()
786
+ print("\ncons200 守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
787
+ return 0 if not _FAIL else 1
788
+
789
+
790
+ if __name__ == "__main__":
791
+ sys.exit(main())