@furongjun1999/dsh-memory 0.7.1 → 0.7.3

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 (125) hide show
  1. package/README.md +58 -48
  2. package/codebuddy/CODEBUDDY.md +9 -9
  3. package/docs/README.md +148 -144
  4. package/docs/discipline/harnesses.yaml +16 -8
  5. package/docs/discipline/templates/zcode-user.md.tmpl +49 -0
  6. package/docs/eval/AGI/344/270/203/347/273/264/350/257/204/345/210/206/346/212/245/345/221/212_/345/205/255/345/256/266/350/256/260/345/277/206/347/263/273/347/273/237/345/220/214/345/260/272/345/256/236/346/265/213_v3.1.md +340 -0
  7. package/docs/eval/N225_/347/264/242/345/274/225/346/227/245/345/277/227/351/235/236/345/257/271/350/261/241/350/243/205/350/275/275/351/235/242/347/261/273/345/236/213/351/227/270_v1.0.md +2 -2
  8. 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 +1 -1
  9. 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 +1 -1
  10. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/344/270/216/351/224/256/347/261/273/345/236/213/351/227/270_v1.1.md +1 -1
  11. package/docs/eval/issue43_/351/273/230/350/256/244/347/255/226/347/225/245/345/212/240/350/275/275/344/270/216/345/207/255/346/215/256/346/230/216/346/226/207/351/230/237/345/210/227_v1.0.md +2 -2
  12. 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 +5 -5
  13. 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 +9 -9
  14. 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 +4 -4
  15. 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 +1 -1
  16. 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 +1 -1
  17. 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 +3 -3
  18. 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 +3 -3
  19. 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 +7 -7
  20. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/200/346/211/271_/346/216/245/347/272/277/344/270/216/347/255/211/344/273/267/345/217/230/346/215/242_v1.0.md +2 -2
  21. package/docs/eval//344/274/230/345/214/226/347/254/254/344/270/211/346/211/271_/351/227/250/347/246/201/350/275/254/346/255/243/344/270/216/350/260/203/345/272/246/346/255/242/350/241/200/344/270/216/351/227/250/346/216/247/346/224/266/345/217/243_v1.0.md +1 -1
  22. package/docs/eval//344/274/230/345/214/226/347/254/254/344/272/214/346/211/271_/344/276/235/350/265/226/351/200/217/344/274/240/344/270/216/345/257/271/346/213/215/345/217/243/345/276/204/344/270/216/350/264/237/347/274/223/345/255/230_v1.0.md +7 -7
  23. 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 +10 -10
  24. package/docs/eval//345/217/221/345/270/20307_/345/244/226/351/203/250/346/212/245/345/221/212/345/233/233/346/211/271/344/277/256/345/244/215/344/270/216/346/217/222/344/273/266/351/235/242/345/212/240/345/233/272_v1.0.md +4 -0
  25. 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 +1 -1
  26. 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 +1 -1
  27. package/docs/eval//345/217/221/345/270/20310_/350/257/273/351/235/242/346/215/237/345/235/217UTF8/345/256/266/346/227/217/346/224/266/345/217/243_v1.0.md +71 -0
  28. package/docs/eval//345/217/221/345/270/20311_/345/244/226/346/212/245/345/205/255/350/277/236/344/277/256/344/270/216/344/270/226/347/225/214/346/250/241/345/236/213/345/272/225/345/272/247_v1.0.md +91 -0
  29. 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 +2 -2
  30. package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/270/200/346/211/271_v1.0.md +195 -0
  31. package/docs/eval//347/224/250/346/210/267/350/243/201/345/206/263/345/244/204/347/275/256_/347/254/254/344/272/214/346/211/271B6_/346/234/254/346/234/272/350/267/257/345/276/204/347/233/270/345/257/271/345/214/226_v1.0.md +78 -0
  32. package/docs/eval//347/253/257/345/210/260/347/253/257LoCoMoQA/345/220/214/345/217/243/345/276/204/345/257/271/347/205/247_v1.0.md +1 -1
  33. package/docs/eval//347/253/257/345/210/260/347/253/257/345/271/262/346/211/260/346/261/240/350/257/204/346/265/213_/347/241/256/345/256/232/346/200/247/350/243/201/345/206/263vsLLM_judge_v1.1.md +3 -3
  34. 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 +1 -1
  35. package/docs/eval//347/274/226/347/240/201/351/235/242/345/211/215/347/275/256_/345/205/245/345/217/243/350/207/252/344/277/235/350/257/201UTF8/344/270/216/346/226/207/346/234/254open/345/256/210/345/215/253_v1.0.md +1 -1
  36. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v26.md +196 -0
  37. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v27.md +259 -0
  38. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v28.md +217 -0
  39. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v29.md +270 -0
  40. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v30.md +231 -0
  41. package/docs/eval//347/274/272/351/231/267/346/214/226/346/216/230_/350/207/252/344/270/273/350/277/255/344/273/243_v31.md +281 -0
  42. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +1 -1
  43. package/docs/hive//350/234/202/345/267/242M6_ingest/345/256/236/346/226/275/350/256/241/345/210/222_v0.1.md +1 -1
  44. package/docs/hive//350/234/202/345/267/242/345/217/214/345/256/236/344/276/213/344/272/222/351/252/214_/350/256/276/350/256/241/345/256/232/347/250/277.md +1 -1
  45. package/docs/hive//350/234/202/345/267/242/350/256/276/350/256/241_/347/220/206/350/256/272/345/257/271/351/275/220_v0.1.md +17 -17
  46. package/docs/hive//350/234/202/345/267/242/350/277/255/344/273/243_/345/256/217/350/247/202/344/270/216/347/276/244/344/275/223/350/260/203/345/272/246_v0.1.md +1 -1
  47. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +2 -2
  48. 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 +42 -41
  49. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +29 -8
  50. 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 +71 -1
  51. package/docs/plans//345/205/250/344/270/255/346/226/207/347/274/226/347/240/201/344/270/216/350/234/202/345/267/242/344/273/273/345/212/241/346/240/207/350/257/206/345/245/221/347/272/246_v2.0.md +1 -1
  52. package/docs/plans//345/244/232/344/270/273/344/275/223/344/270/226/347/225/214/346/250/241/345/236/213_/345/257/271/351/275/220/350/257/204/344/274/260/344/270/216/350/220/275/345/234/260/350/256/276/350/256/241_v0.1.md +141 -0
  53. 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 +2 -2
  54. package/docs/plans//350/257/255/344/271/211/346/227/266/347/251/272/345/233/276/350/241/245/345/205/250_/344/270/226/347/225/214/346/250/241/345/236/213/345/212/237/350/203/275/347/253/257_/350/256/276/350/256/241_v0.1.md +107 -0
  55. package/docs//345/267/245/344/275/234/347/272/252/345/276/213_/350/256/244/347/237/245/345/233/276/346/235/241/347/233/256_v1.1.json +9 -8
  56. package/docs//347/247/244_/350/256/260/345/277/206/347/263/273/347/273/237/350/257/204/346/265/213/350/247/204/350/214/203_v2.1.md +225 -0
  57. package/dsh/cordis-patch-profile-web.example.yml +2 -2
  58. package/lib/bridge.js +9 -0
  59. package/lib/hooks.d.ts +8 -1
  60. package/lib/hooks.js +94 -32
  61. package/lib/lib/hook_audit.d.ts +55 -0
  62. package/lib/lib/hook_audit.js +208 -0
  63. package/lib/lib/mutual.js +5 -1
  64. package/lib/lib/roleplay_web.js +17 -2
  65. package/md_cg/auditview.py +248 -0
  66. package/md_cg/bench6_arms.py +10 -6
  67. package/md_cg/bench_e2e_judge.py +4 -2
  68. package/md_cg/branches.py +29 -3
  69. package/md_cg/census.py +2 -1
  70. package/md_cg/consistency.py +38 -13
  71. package/md_cg/datapath.py +38 -11
  72. package/md_cg/forgetting.py +15 -2
  73. package/md_cg/freshness.py +32 -11
  74. package/md_cg/ghostref.py +130 -0
  75. package/md_cg/judgment_manifest.py +7 -0
  76. package/md_cg/mcp_server.py +58 -1
  77. package/md_cg/mdcg.py +4923 -4764
  78. package/md_cg/mdcos.py +40 -15
  79. package/md_cg/mreview/locate.py +1 -1
  80. package/md_cg/nodefile.py +77 -13
  81. package/md_cg/readcache.py +29 -4
  82. package/md_cg/rollback.py +12 -3
  83. package/md_cg/routing.py +10 -1
  84. package/md_cg/selfreport.py +12 -3
  85. package/md_cg/sleep.py +129 -13
  86. package/md_cg/sources.py +1 -1
  87. package/md_cg/state_events.py +131 -0
  88. package/md_cg/sustain.py +1436 -1436
  89. package/md_cg/test_auditview.py +277 -0
  90. package/md_cg/test_autonomy_modes.py +2 -1
  91. package/md_cg/test_boot_openblas_guard.py +197 -0
  92. package/md_cg/test_ccg_form_parity.py +95 -4
  93. package/md_cg/test_cons200_scan_selection.py +4 -2
  94. package/md_cg/test_corrupt_utf8_read_surfaces.py +451 -0
  95. package/md_cg/test_datapath_device_name.py +62 -3
  96. package/md_cg/test_generation_guard.py +20 -11
  97. package/md_cg/test_ghostref.py +182 -0
  98. package/md_cg/test_health_corrupt_utf8.py +37 -0
  99. package/md_cg/test_index_crossprocess_reload.py +39 -24
  100. package/md_cg/test_interop.py +1 -1
  101. package/md_cg/test_issue52_scan_condition_first.py +1 -1
  102. package/md_cg/test_issue53_cipher_health_hash.py +385 -0
  103. package/md_cg/test_lock.py +16 -7
  104. package/md_cg/test_m3_h9_semantic_guard.py +1 -1
  105. package/md_cg/test_n230_dirty_replay.py +375 -375
  106. package/md_cg/test_n238_body_text_parity.py +156 -0
  107. package/md_cg/test_n276_probe_hoist.py +199 -0
  108. package/md_cg/test_p4_freshness.py +95 -1
  109. package/md_cg/test_policy_required_ccg.py +3 -1
  110. package/md_cg/test_readcache_prodpath.py +66 -0
  111. package/md_cg/test_sleep.py +58 -1
  112. package/md_cg/test_sleep_p1.py +218 -2
  113. package/md_cg/test_state_events.py +178 -0
  114. package/md_cg/test_time_core_lint.py +8 -1
  115. package/md_cg/test_writepipe.py +8 -5
  116. package/md_cg/tokens.py +11 -2
  117. package/md_cg/writepipe.py +76 -0
  118. package/package.json +2 -2
  119. package/skills/plugin.json +1 -1
  120. package/src/bridge.ts +9 -0
  121. package/src/hooks.ts +576 -517
  122. package/src/lib/hook_audit.ts +243 -0
  123. package/src/lib/mutual.ts +5 -1
  124. package/src/lib/roleplay_web.ts +17 -2
  125. package/zcode/AGENTS.md +9 -9
@@ -0,0 +1,277 @@
1
+ # -*- coding: utf-8 -*-
2
+ """test_auditview · L5 证据审计面只读组装器(路线 C)的行为守卫。
3
+
4
+ 多主体世界模型对齐 v0.1 §4 路线 C:cg(op="audit") 给节点 id 输出五面
5
+ (现值/出处链/两存备择/盲区/退役史);**纯读、零写路径改动**。
6
+ 运行:python -m md_cg.test_auditview
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import tempfile
12
+ import time
13
+
14
+ from . import auditview, evolution, lifecycle, nodefile, tokens, trust
15
+ from .mdcos import MdCGOS
16
+ from .mcp_server import _cg_call
17
+ from .writepipe import default_pipeline
18
+
19
+ _ok = 0
20
+ _fail = []
21
+
22
+
23
+ def check(name, cond, detail=""):
24
+ global _ok
25
+ if cond:
26
+ _ok += 1
27
+ print("[ok] " + name)
28
+ else:
29
+ _fail.append(name)
30
+ print("[FAIL] %s · %s" % (name, str(detail)[:240]))
31
+
32
+
33
+ H = ("# 功能名:审计面守卫\n# 生效条件:测试环境\n# 子功能:无\n"
34
+ "# 执行:无\n# 验证方式:test\n# 不适用条件:无\n")
35
+ H2 = ("# 功能名:审计面守卫\n# 生效条件:测试环境\n# 子功能:无\n"
36
+ "# 执行:无\n# 验证方式:test\n# 不适用条件:无\n")
37
+
38
+
39
+ def main():
40
+ tmp = tempfile.mkdtemp(prefix="mdcg_av_test_")
41
+ cg = MdCGOS(os.path.join(tmp, "root"))
42
+ pipe = default_pipeline()
43
+
44
+ def W(nid, body, **kw):
45
+ a = {"op": "write", "node_id": nid, "content": H + body + "\n",
46
+ "layer": "knowledge", "content_kind": "text",
47
+ "verification_basis": "test"}
48
+ a.update(kw)
49
+ return pipe.execute(cg, a)
50
+
51
+ # ---- 1. 结构:五面在场 ----
52
+ W("av_base", "基准记录:集市采购。")
53
+ W("av_subject", "依据见 av_base 记载。上次说的那件事。",
54
+ check_strength="hoop")
55
+ out = _cg_call(cg, {"op": "audit", "node_id": "av_subject", "cap": 5})
56
+ check("T1a ok=True", out.get("ok") is True, out.get("error"))
57
+ for face in ("present", "sources", "alternatives", "blindspots", "history"):
58
+ check("T1b 面在场:%s" % face, isinstance(out.get(face), dict),
59
+ sorted(out.keys()))
60
+
61
+ # ---- 2. 现值面:A1/A2 字段 ----
62
+ p = out.get("present") or {}
63
+ check("T2a check_strength 透传(写链)", p.get("check_strength") == "hoop",
64
+ p.get("check_strength"))
65
+ check("T2b uncertain_refs 呈现", p.get("uncertain_refs") == ["上次说"],
66
+ p.get("uncertain_refs"))
67
+ check("T2c 生命态/验证态/时效在场",
68
+ p.get("lifecycle_state") == "active" and p.get("verification_state")
69
+ and p.get("validity"), p)
70
+
71
+ # ---- 3. 出处链:refers_to(正向)与 referenced_by(反查) ----
72
+ src = out.get("sources") or {}
73
+ rt = src.get("refers_to") or {}
74
+ check("T3a refers_to 含 av_base", any(
75
+ e.get("target") == "av_base" for e in (rt.get("items") or [])), rt)
76
+ rb = src.get("referenced_by") or {}
77
+ out_b = _cg_call(cg, {"op": "audit", "node_id": "av_base"})
78
+ rb_b = (out_b.get("sources") or {}).get("referenced_by") or {}
79
+ check("T3b 反查:av_base 的引用者含 av_subject", any(
80
+ it.get("from") == "av_subject" for it in (rb_b.get("items") or [])), rb_b)
81
+
82
+ # ---- 4. 两存/备择:同条件空间分歧 ⇒ verdict 非 ACCEPT 且可比节点在 ----
83
+ W("av_dup_a", "集市购物,周末进行。")
84
+ W("av_dup_b", "集市购物,改到工作日进行。")
85
+ out_d = _cg_call(cg, {"op": "audit", "node_id": "av_dup_b", "cap": 5})
86
+ alt = out_d.get("alternatives") or {}
87
+ check("T4a 备择面可比非零", (alt.get("comparable") or 0) >= 1, alt)
88
+ check("T4b 同槽分歧如实透出(verdict/missing 至少其一)",
89
+ alt.get("verdict") in ("DEFER", "REJECT", "ACCEPT") and
90
+ (alt.get("reason") is not None or alt.get("verdict") == "ACCEPT"),
91
+ {k: alt.get(k) for k in ("verdict", "reason")})
92
+
93
+ # ---- 5. 盲区面 ----
94
+ bs = out.get("blindspots") or {}
95
+ check("T5a 盲区面含幽灵标记与证据计数",
96
+ bs.get("uncertain_refs") == ["上次说"]
97
+ and bs.get("negative_evidence") == 0
98
+ and isinstance(bs.get("note"), str), bs)
99
+
100
+ # ---- 6. 退役史:状态迁移与验证迁移入史 ----
101
+ lifecycle.set_state(cg, "av_subject", "converged", reason="守卫用例")
102
+ trust.set_state(cg, "av_subject", "verified", reason="守卫用例",
103
+ method="test", actor="guard")
104
+ out_h = _cg_call(cg, {"op": "audit", "node_id": "av_subject"})
105
+ hist = out_h.get("history") or {}
106
+ check("T6a state_history 记录在场", any(
107
+ it.get("to") == "converged" for it in (hist.get("state_history") or [])),
108
+ hist.get("state_history"))
109
+ check("T6b verification_history 记录在场", any(
110
+ it.get("to") == "verified"
111
+ for it in (hist.get("verification_history") or [])),
112
+ hist.get("verification_history"))
113
+ _ev = evolution.record(cg, node_id="av_subject", pattern="审计面守卫",
114
+ missing="无", action="留痕", evidence="av_guard")
115
+ out_h2 = _cg_call(cg, {"op": "audit", "node_id": "av_subject"})
116
+ check("T6c evolution 台账纳入", bool(
117
+ ((out_h2.get("history") or {}).get("evolution") or [])),
118
+ (out_h2.get("history") or {}).get("evolution"))
119
+
120
+ # ---- 7. 错误面:类型错 / 不存在 / 空串 ----
121
+ e1 = _cg_call(cg, {"op": "audit", "node_id": 123})
122
+ check("T7a 类型错结构化", e1.get("error") == "node_id_not_str"
123
+ and e1.get("got_type") == "int", e1)
124
+ e2 = _cg_call(cg, {"op": "audit", "node_id": "av_no_such"})
125
+ check("T7b 不存在结构化", e2.get("error") == "node_not_found", e2)
126
+ e3 = _cg_call(cg, {"op": "audit", "node_id": " "})
127
+ check("T7c 空串结构化", e3.get("error") == "node_id_not_str", e3)
128
+
129
+ # ---- 8. cap 截断如实 ----
130
+ # 目标 id 段长须 ≥3(linkref.ID_SHAPE;av_t0 段 "t0" 不合法会不建边);
131
+ # 单节点一次引用 9 个目标(避免覆写触发 confirm 档出单)。
132
+ _refs = "、".join("av_tk%d" % i for i in range(9))
133
+ for i in range(9):
134
+ W("av_tk%d" % i, "尾目标 %d。" % i)
135
+ pipe.execute(cg, {"op": "write", "node_id": "av_hub",
136
+ "content": H + "引用 %s。" % _refs + "\n",
137
+ "layer": "knowledge", "content_kind": "text",
138
+ "verification_basis": "test"})
139
+ out_c = _cg_call(cg, {"op": "audit", "node_id": "av_hub", "cap": 3})
140
+ rt2 = (out_c.get("sources") or {}).get("refers_to") or {}
141
+ check("T8a cap 截断(items=3)", len(rt2.get("items") or []) == 3, rt2)
142
+ check("T8b total/truncated 如实", (rt2.get("total") or 0) == 9
143
+ and rt2.get("truncated") is True, rt2)
144
+
145
+ # ---- 9. 纯读:调用前后**全库级**文件清单不变(复核 R2 升级口径:
146
+ # 原实现只比单文件,测不到 `_consistency.jsonl` 的隐式追加)。----
147
+ def _snapshot(root):
148
+ out = {}
149
+ for dp, _dns, fns in os.walk(root):
150
+ for fn in fns:
151
+ fp = os.path.join(dp, fn)
152
+ try:
153
+ out[os.path.relpath(fp, root)] = (os.path.getsize(fp),
154
+ open(fp, "rb").read())
155
+ except OSError:
156
+ out[os.path.relpath(fp, root)] = None
157
+ return out
158
+
159
+ cg.flush()
160
+ snap0 = _snapshot(cg.root)
161
+ _cg_call(cg, {"op": "audit", "node_id": "av_subject"})
162
+ _cg_call(cg, {"op": "audit", "node_id": "av_base"})
163
+ snap1 = _snapshot(cg.root)
164
+ check("T9a 纯读(全库级):审计调用不改任何文件",
165
+ snap0 == snap1,
166
+ sorted(set(snap0) ^ set(snap1)) or
167
+ [k for k in snap0 if snap0.get(k) != snap1.get(k)][:4])
168
+ # 直调面同款(auditview.build 本身也须零写)
169
+ snap1b = _snapshot(cg.root)
170
+ auditview.build(cg, "av_subject")
171
+ check("T9b 纯读(直调面)", snap1b == _snapshot(cg.root),
172
+ [k for k in snap1b if snap1b.get(k) != _snapshot(cg.root).get(k)][:4])
173
+
174
+ # ---- 10. 角色面:ops 表登记(fail-closed 缺省不给) ----
175
+ check("T10a ALL_OPS 含 audit", "audit" in tokens.ALL_OPS)
176
+ _verify_ops = tokens.ROLE_SPECS["verify"]["ops_allow"]
177
+ check("T10b verify 角色含 audit", "audit" in _verify_ops, _verify_ops)
178
+ check("T10c 非授权角色不含 audit(fail-closed 缺省)",
179
+ "audit" not in tokens.ROLE_SPECS["output"]["ops_allow"]
180
+ and "audit" not in tokens.ROLE_SPECS["guest"]["ops_allow"],
181
+ tokens.ROLE_SPECS["output"]["ops_allow"])
182
+
183
+ # ---- 11. auditview 直调面:node_not_found 与 cap 钳制 ----
184
+ direct = auditview.build(cg, "av_no_such")
185
+ check("T11a 直调不存在结构化", direct.get("error") == "node_not_found", direct)
186
+ out_cl = _cg_call(cg, {"op": "audit", "node_id": "av_subject", "cap": 999})
187
+ check("T11b cap 上钳 64", (out_cl.get("cap") or 0) == 64, out_cl.get("cap"))
188
+
189
+ # ---- 12. link_ledger 真 schema(复核 R1 修:原按 target/source 匹配恒读空)----
190
+ W("av_led_child", "派生用例。", derived_from=["av_base"], relation="derived_from")
191
+ out_l = _cg_call(cg, {"op": "audit", "node_id": "av_led_child"})
192
+ ll = (out_l.get("sources") or {}).get("link_ledger") or {}
193
+ check("T12 link_ledger 按 child/parent 呈现派生行",
194
+ (ll.get("total") or 0) >= 1 and any(
195
+ it.get("child") == "av_led_child" and it.get("parent") == "av_base"
196
+ for it in (ll.get("items") or [])), ll)
197
+
198
+ # ---- 13. 读隔离(复核 R3 修:反查须过 _readable,不可读节点不泄漏)----
199
+ _nodes = cg.index["nodes"]
200
+ _nodes["sec_iso_node"] = {"layer": "knowledge", "sensitivity": "secret",
201
+ "edges": [{"target": "av_base",
202
+ "relation_type": "reference"}]}
203
+ _nodes["int_iso_node"] = {"layer": "knowledge", "sensitivity": "internal",
204
+ "edges": [{"target": "av_base",
205
+ "relation_type": "reference"}]}
206
+ _orig_rd = getattr(cg, "_readable", None)
207
+ try:
208
+ cg._readable = lambda e: e.get("sensitivity") != "secret"
209
+ out_iso = auditview.build(cg, "av_base")
210
+ names = [it.get("from") for it in
211
+ ((out_iso.get("sources") or {}).get("referenced_by") or {}).get("items", [])]
212
+ check("T13 读隔离:secret 引用者不出现在反查",
213
+ "sec_iso_node" not in names and "int_iso_node" in names, names)
214
+ # T13b(复验 R4):link_ledger 的**行级**隔离——另一端不可见的行不出
215
+ _lp = os.path.join(cg.root, "_link.jsonl")
216
+ with open(_lp, "a", encoding="utf-8") as f:
217
+ f.write('{"schema": 1, "child": "sec_iso_node", "parent": "av_base",'
218
+ ' "rel": "derived_from", "t": 1}\n')
219
+ f.write('{"schema": 1, "child": "int_iso_node", "parent": "av_base",'
220
+ ' "rel": "derived_from", "t": 2}\n')
221
+ out_iso2 = auditview.build(cg, "av_base")
222
+ ll_names = [it.get("child") for it in
223
+ ((out_iso2.get("sources") or {}).get("link_ledger") or {}).get("items", [])]
224
+ check("T13b link_ledger 行级隔离:不可见另一端不出行",
225
+ "sec_iso_node" not in ll_names and "int_iso_node" in ll_names,
226
+ ll_names)
227
+ finally:
228
+ if _orig_rd is None:
229
+ try:
230
+ del cg._readable
231
+ except AttributeError:
232
+ pass
233
+ else:
234
+ cg._readable = _orig_rd
235
+ _nodes.pop("sec_iso_node", None)
236
+ _nodes.pop("int_iso_node", None)
237
+
238
+ # ---- 14. cap 直调防御(复核 Y2 修:非数不裸抛,钳 [1,64])----
239
+ try:
240
+ o_c1 = auditview.build(cg, "av_base", cap="abc")
241
+ check("T14a cap 非数回落缺省", o_c1.get("cap") == auditview.DEFAULT_CAP,
242
+ o_c1.get("cap"))
243
+ except TypeError as e:
244
+ check("T14a cap 非数回落缺省", False, "TypeError 冒泡:%s" % e)
245
+ check("T14b cap 负值钳 1", auditview.build(cg, "av_base", cap=-7).get("cap") == 1)
246
+ check("T14c cap 上钳 64", auditview.build(cg, "av_base", cap=999).get("cap") == 64)
247
+
248
+ # ---- 15. mdcg_remember 两分支透传(复核 Y3 修:此前两条落点无断言)----
249
+ from .mcp_server import _dispatch
250
+ o_d = _dispatch(cg, "mdcg_remember", {
251
+ "content": "# 功能名:直写透传守卫\n# 生效条件:测试\n# 子功能:无\n"
252
+ "# 执行:无\n# 验证方式:test\n# 不适用条件:无\n",
253
+ "node_id": "mem_direct_wire", "layer": "knowledge",
254
+ "verification_basis": "test", "check_strength": "hoop"})
255
+ fm_d = (cg.get("mem_direct_wire") or {}).get("frontmatter") or {}
256
+ check("T15a mdcg_remember 直写分支透传", fm_d.get("check_strength") == "hoop",
257
+ (o_d.get("ok"), fm_d.get("check_strength")))
258
+ o_g = _dispatch(cg, "mdcg_remember", {
259
+ "content": "# 功能名:gated 透传守卫\n# 生效条件:测试\n# 子功能:无\n"
260
+ "# 执行:无\n# 验证方式:test\n# 不适用条件:无\n",
261
+ "node_id": "mem_gated_wire", "layer": "contextual",
262
+ # "gated": True 必须有(复验 Y3:缺它则走直写分支、gated 分支零覆盖)。
263
+ "gated": True,
264
+ "verification_basis": "test", "consistency": False,
265
+ "check_strength": "smoking_gun"})
266
+ fm_g = (cg.get("mem_gated_wire") or {}).get("frontmatter") or {}
267
+ check("T15b mdcg_remember gated 分支透传",
268
+ fm_g.get("check_strength") == "smoking_gun",
269
+ (o_g.get("verdict") or o_g.get("ok"), fm_g.get("check_strength")))
270
+
271
+ print("\ntest_auditview: %d 通过 / %d 失败" % (_ok, len(_fail)))
272
+ if _fail:
273
+ raise SystemExit(1)
274
+
275
+
276
+ if __name__ == "__main__":
277
+ main()
@@ -1584,8 +1584,9 @@ _SRC_MUTATIONS = (
1584
1584
  ' if False:', 3),
1585
1585
  # ⑭ SHIM 登记丢失(**常量型变异**,kind="attr")——baseline 假包缺
1586
1586
  # autonomy_modes(补强一 的原形态:容器栈一 37/1 的那条红)。
1587
+ # 锚点随 _SHIMS 文本演进同步(2026-10-05 A2:_SHIMS 尾部新增 ghostref)。
1587
1588
  ("SHIM 登记丢失(_SHIMS 去掉 autonomy_modes)", "attr", _tpc, "_SHIMS",
1588
- '"autonomy_modes")',
1589
+ '"autonomy_modes", "ghostref")',
1589
1590
  'tuple(x for x in _SHIMS if x != "autonomy_modes")', 2),
1590
1591
  # ⑮ 执行桥不拍前像(批次③)——三字段的「执行时点」半支失效:before 空、
1591
1592
  # rollback 空,回滚句柄不存在、影响面缺失。
@@ -0,0 +1,197 @@
1
+ # -*- coding: utf-8 -*-
2
+ """守卫:md_cg MCP server 的**入口自保证 OpenBLAS 线程数**(2026-10-05 返修)
3
+
4
+ 病灶(本机取证,2026-10-05):numpy(md_cg 可选依赖,经 whitebox_kb 系列模块
5
+ 加载)在**导入期**由 OpenBLAS 按线程数(默认 = 核数)申请每线程缓冲——32 核机
6
+ 实测单进程 ``import numpy`` 的全机提交内存增量 **0.73 GiB**。启动链上的代校验
7
+ (``verify_delegations`` → 导入含 numpy 的目标模块)把这笔分配带到**服务应答
8
+ 之前**:机器提交内存吃紧、或**多进程同启**(并行测试套件 / 多会话并存)时,
9
+ OpenBLAS 重试 10 次后放弃并 ``exit(1)``——进程在 initialize 应答前死亡,桥侧
10
+ 只见「握手失败 / 插件激活失败」(TS 套件实测失败用例的子进程 stderr 逐字为
11
+ ``OpenBLAS error: Memory allocation still failed after 10 retries``)。
12
+
13
+ 修法(本件守门):``md_cg/mcp_server.py`` **模块级**(早于 main 的代校验,即
14
+ numpy 的首个导入点之前)在未设/空白时置 ``OPENBLAS_NUM_THREADS=1``——实测单
15
+ 进程提交增量 0.73 → 0.013 GiB,12 个并发 ``import numpy`` 由 4~9/12 失败(两轮
16
+ 独立读数,随机器提交内存压力波动)转为 0/12(两轮);单线程对现有数值面无已观测
17
+ 的性能意义(⚠ 未做基准实测,见 mcp_server.py 入口块注释)。
18
+
19
+ 断言(红基线 = 去掉该入口块;HEAD worktree 实测:G1/G3/G4/G6a/G6b 转红;
20
+ G2 两侧皆绿=独立于入口块的口径锚;G5 为压力依赖,本轮未触发红):
21
+ G1 缺省:子进程 import md_cg.mcp_server 后 env 值 = "1"(进程自保证)
22
+ G2 显式设值优先:预置 "4" → 保持 "4"(不覆盖部署方调优)
23
+ G3 空白视为未设:预置 " " → 归一 "1"(仓库口径:空白视为未设置)
24
+ G4 时序:import 完成后 numpy 尚未加载(模块级早于 main 的代校验);随后同
25
+ 进程 import numpy 成功——自保证早于首个 numpy 导入生效
26
+ G5 实弹并发:6 个并发子进程各走「import mcp_server + verify_delegations」
27
+ 全部成功(本套件红案的收敛形态:并行下子进程启动不得死于 OpenBLAS)
28
+ G6 站点锚(AST):入口块是**模块体顶层**的赋值语句且行号早于该文件首个模块级
29
+ `def`(须早于一切模块级 I/O 的既有锚口径,与 test_utf8_boot_guard 的
30
+ ANCHOR ⑤ 同款;防未来把保证挪进函数/挪到文件尾)
31
+
32
+ 定点变异自证(本守卫做不了运行期反证——保证是 import 期一次性副作用,故按
33
+ 仓内惯例在**临时物化副本**上做,绝不改工作树;本机实测读数记录于此):
34
+ 取 md_cg 树副本、删掉入口块后跑 G1/G4/G5 同款子进程:
35
+ G1 → env 值为 None(红);G5 → 6 并发在提交内存吃紧时出现 OpenBLAS 失败
36
+ (同机同压力量级实测:不导入 mcp_server 的同形并发 12 个默认 9/12 败)。
37
+ 本守卫自带 G1~G6 判据不含变异装置(保持套件轻量);上表为一次性变异取证。
38
+
39
+ 运行:python -X utf8 -m md_cg.test_boot_openblas_guard
40
+ """
41
+ from __future__ import annotations
42
+
43
+ import ast
44
+ import os
45
+ import shutil
46
+ import subprocess
47
+ import sys
48
+ import tempfile
49
+
50
+ REPO = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
51
+
52
+ PASS = FAIL = 0
53
+ FAILS = []
54
+
55
+
56
+ def check(name, cond, detail=""):
57
+ global PASS, FAIL
58
+ if cond:
59
+ PASS += 1
60
+ print(f" OK {name}")
61
+ else:
62
+ FAIL += 1
63
+ FAILS.append(name)
64
+ print(f" FAIL {name} {detail}")
65
+
66
+
67
+ # 生效条件:总是返回一份「清掉 OPENBLAS_NUM_THREADS、锚定本仓 PYTHONPATH、隔离
68
+ # 记忆根到 tmp 之下」的子进程 env(extra 最后覆盖,供 G2/G3 预置取值)。
69
+ def _child_env(tmp, extra=None):
70
+ env = dict(os.environ)
71
+ env.pop("OPENBLAS_NUM_THREADS", None)
72
+ env["PYTHONPATH"] = REPO
73
+ env["PYTHONUTF8"] = "1"
74
+ env["PYTHONIOENCODING"] = "utf-8"
75
+ # 防御性隔离:本守卫只 import 模块与跑代校验(纯读),但仍把记忆根钉在
76
+ # 临时面——绝不因未来代码路径变化回落到真实 ~/.mdcg。
77
+ env.setdefault("MDCG_ROOT", os.path.join(tmp, "cgroot"))
78
+ env.setdefault("MDCG_AUX_ROOT", os.path.join(tmp, "auxroot"))
79
+ env.setdefault("MDCG_STATE_ROOT", os.path.join(tmp, "state"))
80
+ env.setdefault("MDCG_DATA_ROOT", os.path.join(tmp, "data"))
81
+ if extra:
82
+ env.update(extra)
83
+ return env
84
+
85
+
86
+ # 生效条件:code 在子解释器中执行(cwd=仓根、argv 列表、显式 UTF-8),返回
87
+ # (returncode, stdout 去尾空白, stderr 末 400 字符)。
88
+ def _run_child(code, env, timeout=180):
89
+ r = subprocess.run([sys.executable, "-c", code], cwd=REPO, env=env,
90
+ capture_output=True, text=True, encoding="utf-8",
91
+ errors="replace", timeout=timeout, shell=False)
92
+ return r.returncode, (r.stdout or "").strip(), (r.stderr or "")[-400:]
93
+
94
+
95
+ _G1_CODE = "import os\nimport md_cg.mcp_server\nprint(os.environ.get('OPENBLAS_NUM_THREADS'))\n"
96
+
97
+ _G4_CODE = ("import os\nimport sys\nimport md_cg.mcp_server\n"
98
+ "pre = 'numpy' in sys.modules\n"
99
+ "import numpy\n"
100
+ "print(os.environ.get('OPENBLAS_NUM_THREADS'), pre)\n")
101
+
102
+ # 收敛形态:与 TS 套件受害路径同构——「服务启动期:导入模块 → 代校验导入 numpy」。
103
+ _G5_CODE = ("import md_cg.mcp_server\n"
104
+ "from md_cg import generation\n"
105
+ "r = generation.verify_delegations()\n"
106
+ "print('OK', r['ok'])\n")
107
+
108
+
109
+ # 生效条件:能读到被守卫源码与 G6 需要的两处行号时返回 (module_body 首个
110
+ # def 行号, 入口块行号);入口块(模块体顶层的 os.environ["OPENBLAS_NUM_THREADS"]
111
+ # 赋值所在 If)不存在时返回 (def 行号, None)。
112
+ def _entry_block_lines():
113
+ ap = os.path.join(REPO, "md_cg", "mcp_server.py")
114
+ with open(ap, encoding="utf-8") as fh:
115
+ tree = ast.parse(fh.read(), filename="md_cg/mcp_server.py")
116
+ defs = [n.lineno for n in tree.body
117
+ if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef))]
118
+ first_def = min(defs) if defs else None
119
+ block = None
120
+ for node in tree.body: # 只认模块体顶层(函数内不算)
121
+ if not isinstance(node, ast.If):
122
+ continue
123
+ for sub in node.body:
124
+ if not (isinstance(sub, ast.Assign) and sub.targets):
125
+ continue
126
+ tgt = sub.targets[0]
127
+ if (isinstance(tgt, ast.Subscript)
128
+ and isinstance(tgt.value, ast.Attribute)
129
+ and tgt.value.attr == "environ"
130
+ and isinstance(tgt.slice, ast.Constant)
131
+ and tgt.slice.value == "OPENBLAS_NUM_THREADS"):
132
+ block = node.lineno
133
+ return first_def, block
134
+
135
+
136
+ def main():
137
+ tmp = tempfile.mkdtemp(prefix="mdcg_openblas_guard_")
138
+ try:
139
+ print("== G1~G3 env 口径(进程自保证 / 显式优先 / 空白归一)==")
140
+ rc, out, err = _run_child(_G1_CODE, _child_env(tmp))
141
+ check("G1 缺省:import md_cg.mcp_server 后 OPENBLAS_NUM_THREADS=1",
142
+ rc == 0 and out == "1", f"rc={rc} out={out!r} err={err!r}")
143
+
144
+ rc, out, err = _run_child(_G1_CODE, _child_env(
145
+ tmp, {"OPENBLAS_NUM_THREADS": "4"}))
146
+ check("G2 显式设值优先:预置 4 → 保持 4(不覆盖部署方调优)",
147
+ rc == 0 and out == "4", f"rc={rc} out={out!r} err={err!r}")
148
+
149
+ rc, out, err = _run_child(_G1_CODE, _child_env(
150
+ tmp, {"OPENBLAS_NUM_THREADS": " "}))
151
+ check("G3 空白视为未设:预置空白 → 归一为 1",
152
+ rc == 0 and out == "1", f"rc={rc} out={out!r} err={err!r}")
153
+
154
+ print("== G4 时序:自保证早于首个 numpy 导入,且 numpy 随后可导入 ==")
155
+ rc, out, err = _run_child(_G4_CODE, _child_env(tmp))
156
+ check("G4 import mcp_server 后 numpy 未加载;numpy 导入成功且值=1",
157
+ rc == 0 and out == "1 False", f"rc={rc} out={out!r} err={err!r}")
158
+
159
+ print("== G5 实弹并发:6 个「启动期(import + 代校验)」子进程全部成功 ==")
160
+ env5 = _child_env(tmp)
161
+ procs = [subprocess.Popen([sys.executable, "-c", _G5_CODE], cwd=REPO,
162
+ env=env5, stdout=subprocess.PIPE,
163
+ stderr=subprocess.PIPE)
164
+ for _ in range(6)]
165
+ bad = []
166
+ for i, p in enumerate(procs):
167
+ o, e = p.communicate(timeout=300)
168
+ o = (o or b"").decode("utf-8", "replace")
169
+ e = (e or b"").decode("utf-8", "replace")
170
+ if p.returncode != 0 or "OK True" not in o:
171
+ bad.append("child%d rc=%s out=%r err=%r" % (i, p.returncode,
172
+ o[-120:], e[-320:]))
173
+ check("G5 6 个并发启动期子进程全部成功(无 OpenBLAS 分配失败)",
174
+ not bad, ";".join(bad)[:600])
175
+
176
+ print("== G6 站点锚(AST):入口块在模块体顶层且早于首个模块级 def ==")
177
+ first_def, block = _entry_block_lines()
178
+ check("G6a 入口块存在且为模块体顶层语句(函数内不算)",
179
+ block is not None,
180
+ "md_cg/mcp_server.py 模块体未见 os.environ['OPENBLAS_NUM_THREADS'] 赋值")
181
+ check("G6b 入口块早于首个模块级 def(不晚于一切模块级 I/O 的既有锚口径)",
182
+ block is not None and first_def is not None and block < first_def,
183
+ f"入口块行={block} 首个 def 行={first_def}")
184
+ finally:
185
+ shutil.rmtree(tmp, ignore_errors=True)
186
+
187
+ print("\n" + "=" * 64)
188
+ print(f"PASS={PASS} FAIL={FAIL}")
189
+ if FAILS:
190
+ print("失败项:" + ";".join(FAILS))
191
+ return 0 if FAIL == 0 else 1
192
+
193
+
194
+ if __name__ == "__main__":
195
+ if hasattr(sys.stdout, "reconfigure"):
196
+ sys.stdout.reconfigure(encoding="utf-8")
197
+ sys.exit(main())
@@ -15,13 +15,18 @@
15
15
  (冒号可有可无),`mdcos._ccg_field`、`ccgc._has_ccg_line`、
16
16
  `consolidate._has_ccg_line`、`tasks._field_line` 一律委托该单点。
17
17
 
18
- 本套的六组断言:
18
+ 本套的七组断言:
19
19
  ① 两形态都判齐(形态不改变「齐不齐」)
20
20
  ② 缺要素仍判不全(放宽的是标点,不是要求——判别力自证,两种形态各测)
21
21
  ③ 写入闸门与检索面**结论一致**(读真源 data/policy.json 复算,直接钉住分叉)
22
22
  ④ 取值面同源:无冒号的「值」= 标题后首个非空非标题行
23
23
  ⑤ 四个副本函数与单点结论一致(防未来再长出第五、第六套口径)
24
24
  ⑥ 写读闭环:无冒号节点经 upsert 后取得到新值(不出现「写进去读不出」)
25
+ ⑦ 「不适用条件」两函数(`has_non_applicable` / `positive_body`)× 两形态同权:
26
+ 无冒号形态同样算「已声明」(旧码只认冒号写法),且 positive_body 连**值行**
27
+ 一并剥净(旧码只剥标题行 ⇒ 同一条正文写不写冒号会给出两种召回面);
28
+ 并含**红基线自证**——`git show HEAD:md_cg/nodefile.py` 物化后跑同一批断言,
29
+ HEAD 版对无冒号形态必须失手(否则本节断言空转;本改动入库后该腿 SKIP)。
25
30
  """
26
31
  from __future__ import annotations
27
32
 
@@ -29,12 +34,15 @@ import io
29
34
  import json
30
35
  import os
31
36
  import re
37
+ import subprocess
32
38
  import sys
39
+ import types
33
40
 
34
- from . import ccgc, consolidate, nodefile, tasks
41
+ from . import ccgc, consistency, consolidate, nodefile, tasks
42
+ from .mdcg import MdCG
35
43
  from .mdcos import _ccg_field
36
44
 
37
- PASS = FAIL = 0
45
+ PASS = FAIL = SKIPPED = 0
38
46
  FAILS = []
39
47
 
40
48
 
@@ -49,6 +57,34 @@ def ok(cond, label):
49
57
  print(" [FAIL] %s" % label)
50
58
 
51
59
 
60
+ def skip(label, why):
61
+ """不适用/前提缺失的腿:只报不判——不计入 FAIL(如红基线随 HEAD 前移而不可复现)。"""
62
+ global SKIPPED
63
+ SKIPPED += 1
64
+ print(" [SKIP] %s(%s)" % (label, why))
65
+
66
+
67
+ def _load_head_nodefile():
68
+ """物化 HEAD 版 `md_cg/nodefile.py` 并 exec 成模块(红基线自证用)。
69
+
70
+ nodefile 只依赖 stdlib、顶层无副作用(无包内相对导入、无模块级 I/O),故
71
+ HEAD 字节可独立 exec 进一个空模块命名空间。取不到(无 git / 无提交 / 路径
72
+ 不在仓内)时返回 (None, 原因)——由调用方 SKIP,不静默当成功。
73
+ """
74
+ root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
75
+ r = subprocess.run(["git", "-c", "core.quotepath=false", "show",
76
+ "HEAD:md_cg/nodefile.py"],
77
+ cwd=root, capture_output=True, text=True,
78
+ encoding="utf-8", errors="replace")
79
+ if r.returncode != 0 or not (r.stdout or "").strip():
80
+ return None, ("git show 失败 rc=%s %s"
81
+ % (r.returncode, (r.stderr or "").strip()[:200]))
82
+ mod = types.ModuleType("_head_nodefile_legacy")
83
+ mod.__file__ = os.path.join(root, "md_cg", "nodefile.py")
84
+ exec(compile(r.stdout, "HEAD:md_cg/nodefile.py", "exec"), mod.__dict__)
85
+ return mod, ""
86
+
87
+
52
88
  MARKS = ("功能名", "生效条件", "子功能", "执行", "验证方式", "不适用条件")
53
89
 
54
90
  # ① 带冒号形态(既有惯例形态)
@@ -177,8 +213,63 @@ def main() -> int:
177
213
  except ValueError:
178
214
  ok(True, "⑥%s:值含换行被拒(N208 fail-closed)" % enc)
179
215
 
216
+ print("== ⑦ 「不适用条件」两函数 × 两形态同权(A3:N238 同族残面收口)==")
217
+ # 被测:`has_non_applicable`(判「有没有声明」)与 `positive_body`(剥声明段后再作召回键)。
218
+ # 判据全部在单点(`ccg_mark_present` / `_ccg_heading_rest` / `ccg_field_value`,①-⑤ 已验收)
219
+ # ——两函数都不得自带一套「冒号可有可无」的本地口径。
220
+ NEG = "不适用条件"
221
+ COLON_NEG = "# 功能名:p\n# 不适用条件:ZXQ7\n正文第一行\n"
222
+ BARE_NEG = "# 功能名:p\n# 不适用条件\nZXQ7\n正文第一行\n"
223
+ for name, body in (("带冒号", COLON), ("无冒号", NONCOLON), ("混形态", MIXED)):
224
+ ok(nodefile.has_non_applicable(body) == nodefile.ccg_mark_present(body, NEG),
225
+ "⑦%s:has_non_applicable 与单点同判(%s)"
226
+ % (name, nodefile.has_non_applicable(body)))
227
+ ok(nodefile.has_non_applicable(BARE_NEG) is True,
228
+ "⑦无冒号形态:通体判 True(旧码 False——真声明被判成没声明)")
229
+ ok(nodefile.has_non_applicable(COLON_NEG) is True
230
+ and nodefile.has_non_applicable("") is False
231
+ and nodefile.has_non_applicable("正文提到不适用条件,但不是 CCG 行\n") is False,
232
+ "⑦带冒号 True / 空正文 False / 行内提及 False(放宽的是标点,不是判据)")
233
+
234
+ pb_bare = nodefile.positive_body(BARE_NEG)
235
+ pb_colon = nodefile.positive_body(COLON_NEG)
236
+ ok(pb_bare == pb_colon,
237
+ "⑦positive_body 两形态剥除结论一致(无冒号得到 %r)" % pb_bare)
238
+ ok("ZXQ7" not in pb_bare and "正文第一行" in pb_bare,
239
+ "⑦无冒号形态:值行一并剥净、正文保留(旧码残留值行 ZXQ7)")
240
+ ok(nodefile.positive_body("# 不适用条件\n# 执行:x\n后文\n") == "# 执行:x\n后文\n",
241
+ "⑦裸标题后紧跟另一标题:不吞下一要素(取值口径同 ccg_field_value)")
242
+ plain_line = "正文提到不适用条件这个词,但不是 CCG 行。\n"
243
+ ok(nodefile.positive_body(plain_line) == plain_line,
244
+ "⑦普通句子不误伤(只剥 CCG 行)")
245
+ only_neg = "# 不适用条件\nZXQ7\n正文第一行\n"
246
+ ok(consistency._body_text(only_neg) == nodefile.positive_body(only_neg),
247
+ "⑦只声明该要素时:positive_body 与 consistency._body_text 同口径(同一剥除算法单点)")
248
+ # 消费点用中文反例词面(不做大小写归一——`_like` 只小写 body/tags,
249
+ # 拿大写词面测会因边被消而空转,测不出「值行残留 ⇒ 反例被召回」)
250
+ BARE_NEG_CN = "# 功能名:p\n# 不适用条件\n不得把甲当作乙\n正文第一行\n"
251
+ ok(MdCG._like(BARE_NEG_CN, {}, ["不得把甲当作乙"]) is False,
252
+ "⑦消费点 _like:无冒号形态的反例词不作召回键(旧码残留值行 ⇒ 会被召回)")
253
+ ok(MdCG._like(BARE_NEG_CN, {}, ["正文第一行"]) is True,
254
+ "⑦消费点 _like:正条件行照常命中(判别力未削弱)")
255
+
256
+ print("-- ⑦红基线自证:HEAD 版 nodefile 对无冒号形态必须失手 --")
257
+ head, why = _load_head_nodefile()
258
+ if head is None:
259
+ skip("⑦红基线自证", "取不到 HEAD 版:%s" % why)
260
+ else:
261
+ h_present = head.has_non_applicable(BARE_NEG)
262
+ h_leak = "ZXQ7" in head.positive_body(BARE_NEG)
263
+ if h_present is True and not h_leak:
264
+ skip("⑦红基线自证",
265
+ "HEAD 版已含修复(本改动已入库)——红基线只在其提交前可复现")
266
+ else:
267
+ ok(h_present is False and h_leak,
268
+ "⑦红基线可复现:HEAD 版判 has_non_applicable=%s、positive_body 残留值行=%s"
269
+ % (h_present, h_leak))
270
+
180
271
  print()
181
- print("ccg_form_parity: PASS=%d FAIL=%d" % (PASS, FAIL))
272
+ print("ccg_form_parity: PASS=%d FAIL=%d SKIP=%d" % (PASS, FAIL, SKIPPED))
182
273
  if FAILS:
183
274
  print("FAILS: " + "; ".join(FAILS))
184
275
  return 1 if FAIL else 0
@@ -328,10 +328,12 @@ class _CountGet:
328
328
  def __getattr__(self, k):
329
329
  return getattr(self.__dict__["_cg"], k)
330
330
 
331
- def get(self, nid):
331
+ def get(self, nid, probe=True):
332
+ # N276:get 增可选 probe 形参(批内单探针);夹具同步接口并原样透传
333
+ # ——计数语义不变(每次 get 仍计一次,不论探针与否),判据一字未动。
332
334
  self.__dict__["gets"] += 1
333
335
  self.__dict__["ids"].append(nid)
334
- return self.__dict__["_cg"].get(nid)
336
+ return self.__dict__["_cg"].get(nid, probe=probe)
335
337
 
336
338
 
337
339
  # ---------------------------------------------------------------- G1 选面