@furongjun1999/dsh-memory 0.5.1 → 0.6.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 (187) hide show
  1. package/README.md +588 -552
  2. package/codebuddy/CODEBUDDY.md +10 -10
  3. package/data/policy.json +27 -0
  4. package/docs/discipline/harnesses.yaml +31 -0
  5. package/docs/discipline/templates/full.md.tmpl +1 -1
  6. package/docs/discipline/templates/rules.mdc.tmpl +1 -1
  7. package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225v2_/345/217/202/350/200/203dsh-TUI_v1.0.md +248 -0
  8. package/docs/eval/DSH/346/227/245/345/277/227/347/264/242/345/274/225/346/225/210/346/236/234/351/252/214/350/257/201_v1.0.md +209 -0
  9. package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
  10. 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 +418 -0
  11. package/docs/eval/P1b2_/350/257/273/351/235/242/344/273/243/351/231/205/344/277/256/345/244/215_v1.0.md +124 -0
  12. 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 +441 -0
  13. 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 +398 -0
  14. 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 +498 -0
  15. 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 +669 -0
  16. 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 +683 -0
  17. package/docs/eval//345/217/221/345/270/20306_/344/270/200/351/224/256/351/205/215/347/275/256/344/270/216DSH0172_v1.0.md +171 -0
  18. 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 +205 -0
  19. package/docs/eval//346/200/247/350/203/275/344/270/223/351/241/271_/345/206/267/346/237/245/350/257/242/344/270/216/345/206/205/345/255/230_v1.0.md +218 -0
  20. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +10 -0
  21. 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 +646 -0
  22. 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_v18.md +183 -0
  23. 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_v19.md +207 -0
  24. 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_v20.md +283 -0
  25. 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_v21.md +224 -0
  26. 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_v22.md +223 -0
  27. 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_v23.md +293 -0
  28. 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_v24.md +303 -0
  29. 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_v25.md +230 -0
  30. 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 +20 -0
  31. 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
  32. package/docs/mdcg/README/350/257/246/347/273/206/347/211/210_v0.4.10.md +12 -0
  33. 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
  34. package/docs/mdcg//345/217/221/345/270/203/351/227/250/347/246/201/351/223/276_v0.1.md +21 -9
  35. package/docs/mdcg//347/201/265/346/236/242/350/256/260/345/277/206/345/212/250/350/257/215/345/215/217/350/256/256_v1.0-draft.md +7 -2
  36. package/docs/mdcg//350/256/244/347/237/245/345/233/276_/347/264/242/345/274/225/344/270/216/345/267/245/347/250/213/350/247/204/350/214/203/345/214/226_/350/256/241/345/210/222_v0.1.md +39 -3
  37. 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 +220 -0
  38. package/docs/plans//347/234/237/346/272/220/347/264/242/345/274/225_/351/200/232/347/224/250/346/234/272/345/210/266_v0.3.md +227 -0
  39. package/docs/plans//350/234/202/345/267/242/346/250/241/345/236/213/345/257/206/351/222/245/351/205/215/347/275/256/351/235/242_v1.0.md +385 -0
  40. 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 +460 -460
  41. package/dsh/cordis-patch-profile-web.example.yml +35 -0
  42. package/lib/bridge.js +24 -2
  43. package/lib/cli.d.ts +3 -0
  44. package/lib/cli.js +66 -0
  45. package/lib/hooks.d.ts +31 -2
  46. package/lib/hooks.js +219 -16
  47. package/lib/init.d.ts +88 -0
  48. package/lib/init.js +302 -0
  49. package/lib/lib/prompt_safety.d.ts +52 -1
  50. package/lib/lib/prompt_safety.js +76 -1
  51. package/md_cg/audit.py +652 -379
  52. package/md_cg/bench6_arms.py +1 -1
  53. package/md_cg/bench_p0.py +1 -1
  54. package/md_cg/branches.py +2 -2
  55. package/md_cg/ccgc.py +1071 -1006
  56. package/md_cg/chain.py +1 -1
  57. package/md_cg/consistency.py +97 -9
  58. package/md_cg/consolidate.py +34 -9
  59. package/md_cg/crypto.py +59 -26
  60. package/md_cg/docindex.py +16 -1
  61. package/md_cg/evidence.py +585 -582
  62. package/md_cg/evolution.py +17 -1
  63. package/md_cg/export.py +1 -1
  64. package/md_cg/forgetting.py +309 -13
  65. package/md_cg/fsutil.py +454 -8
  66. package/md_cg/identity.py +3 -3
  67. package/md_cg/insight.py +24 -3
  68. package/md_cg/linkref.py +1 -1
  69. package/md_cg/logref.py +327 -0
  70. package/md_cg/mcp_server.py +4152 -3818
  71. package/md_cg/mdcg.py +4208 -3462
  72. package/md_cg/mdcos.py +4491 -4137
  73. package/md_cg/mreview/pipeline.py +15 -1
  74. package/md_cg/nodefile.py +639 -575
  75. package/md_cg/protect.py +158 -11
  76. package/md_cg/protocol.py +41 -2
  77. package/md_cg/provenance.py +22 -4
  78. package/md_cg/reach.py +4 -4
  79. package/md_cg/readcache.py +76 -15
  80. package/md_cg/reconcile.py +1 -1
  81. package/md_cg/refindex.py +246 -37
  82. package/md_cg/refine.py +1 -1
  83. package/md_cg/review_cli.py +49 -4
  84. package/md_cg/routing.py +32 -5
  85. package/md_cg/run_tests.py +17 -0
  86. package/md_cg/scrub.py +30 -3
  87. package/md_cg/security.py +47 -1
  88. package/md_cg/self_state.py +5 -5
  89. package/md_cg/sources.py +23 -6
  90. package/md_cg/srcindex.py +352 -0
  91. package/md_cg/stg.py +47 -3
  92. package/md_cg/subgraph.py +2 -2
  93. package/md_cg/sustain.py +1299 -1168
  94. package/md_cg/tasks.py +469 -470
  95. package/md_cg/test_b1_auto_id_multiproc.py +277 -0
  96. package/md_cg/test_b1b2_write_face.py +417 -0
  97. package/md_cg/test_b2_sensitivity_landing.py +223 -0
  98. package/md_cg/test_b3_merge_keeps_content.py +576 -0
  99. package/md_cg/test_b4_shard_dir_selfheal.py +843 -0
  100. package/md_cg/test_b4_shard_dir_selfheal_guard.py +238 -0
  101. package/md_cg/test_c3_transient_read_negative.py +793 -0
  102. package/md_cg/test_c8_search_rrf_gates.py +502 -0
  103. package/md_cg/test_ccg_form_parity.py +188 -0
  104. package/md_cg/test_ccgc.py +28 -3
  105. package/md_cg/test_govern_directread.py +17 -4
  106. package/md_cg/test_h2_session_view_norm.py +233 -0
  107. package/md_cg/test_h4_sustain_snapshot.py +1377 -0
  108. package/md_cg/test_hive_ingest.py +285 -285
  109. package/md_cg/test_index_crossprocess_reload.py +301 -0
  110. package/md_cg/test_issue39_utf8_stdio.py +307 -16
  111. package/md_cg/test_issue43_default_policy.py +865 -0
  112. package/md_cg/test_legacy_p3_node_id_type.py +375 -0
  113. package/md_cg/test_linkref.py +10 -3
  114. package/md_cg/test_lock.py +2 -2
  115. package/md_cg/test_logref.py +1109 -0
  116. package/md_cg/test_m3_h9_bucket_health_protect_mark.py +301 -0
  117. package/md_cg/test_m3_h9_semantic_guard.py +525 -0
  118. package/md_cg/test_mr_m2.py +15 -3
  119. package/md_cg/test_n130_verify_falsified_protect.py +1 -1
  120. package/md_cg/test_n139_dek_provision_failclosed.py +185 -0
  121. package/md_cg/test_n176_link_trust.py +221 -0
  122. package/md_cg/test_n178_units_jobid_gate.py +212 -0
  123. package/md_cg/test_n184_keys_concurrent_provision.py +307 -0
  124. package/md_cg/test_n195_writepath_reload.py +283 -0
  125. package/md_cg/test_n196_stale_gate_skip.py +169 -0
  126. package/md_cg/test_n197_n208_write_face_gates.py +471 -0
  127. package/md_cg/test_n198_tokens_corrupt_failclosed.py +225 -0
  128. package/md_cg/test_n199_tokens_concurrent_write.py +378 -0
  129. package/md_cg/test_n201_proposal_visibility.py +382 -0
  130. package/md_cg/test_n202_session_notes_visibility.py +461 -0
  131. package/md_cg/test_n204_n205_n226_n227_n228_n229_exit_gates.py +542 -0
  132. package/md_cg/test_n206_stdio_jsonrpc_type.py +403 -0
  133. package/md_cg/test_n209_verify_write_face_gates.py +461 -0
  134. package/md_cg/test_n212_n213_n224_generation_gates.py +513 -0
  135. package/md_cg/test_n214_n215_n221_n222_write_face_gates.py +563 -0
  136. package/md_cg/test_n225_nonobject_load.py +1110 -0
  137. package/md_cg/test_n62_tenant_bind_failclosed.py +200 -0
  138. package/md_cg/test_neg_condition_hits.py +333 -0
  139. package/md_cg/test_neg_tail_honesty.py +663 -0
  140. package/md_cg/test_none_id_write_guard.py +166 -0
  141. package/md_cg/test_opt_batch1_md_cg.py +451 -0
  142. package/md_cg/test_p1.py +5 -5
  143. package/md_cg/test_p11_consistency.py +161 -12
  144. package/md_cg/test_p26_refindex.py +2 -2
  145. package/md_cg/test_p27_docindex.py +777 -774
  146. package/md_cg/test_p28_refcheck.py +740 -13
  147. package/md_cg/test_p29_session_ingest_export.py +2 -2
  148. package/md_cg/test_p2_mcp.py +12 -1
  149. package/md_cg/test_p30_maintain.py +33 -2
  150. package/md_cg/test_p31_insight.py +1 -0
  151. package/md_cg/test_p9c_dedup_hints.py +276 -0
  152. package/md_cg/test_policy_required_ccg.py +426 -0
  153. package/md_cg/test_protocol.py +22 -0
  154. package/md_cg/test_rank_parity_score_mode.py +516 -0
  155. package/md_cg/test_read_face_input_gates.py +363 -0
  156. package/md_cg/test_read_face_semantics.py +489 -0
  157. package/md_cg/test_recall_face_guards.py +812 -0
  158. package/md_cg/test_rejected_credential_forms.py +188 -0
  159. package/md_cg/test_rejected_redact.py +146 -0
  160. package/md_cg/test_retr_s1b.py +2 -2
  161. package/md_cg/test_retr_s5.py +20 -7
  162. package/md_cg/test_review_cli_attribution.py +177 -0
  163. package/md_cg/test_review_cli_visibility.py +235 -0
  164. package/md_cg/test_review_conformance.py +21 -3
  165. package/md_cg/test_security_audit_v21.py +2 -2
  166. package/md_cg/test_server_version.py +67 -0
  167. package/md_cg/test_srcindex.py +171 -0
  168. package/md_cg/test_tenant_env_override_warn.py +63 -50
  169. package/md_cg/test_token_lowercase_form.py +315 -0
  170. package/md_cg/test_v21r1_package_version.py +310 -0
  171. package/md_cg/test_writepipe.py +10 -4
  172. package/md_cg/tokens.py +225 -95
  173. package/md_cg/tool_face.py +4 -4
  174. package/md_cg/trust.py +49 -5
  175. package/md_cg/units.py +204 -6
  176. package/md_cg/weights.py +4 -4
  177. package/md_cg/whitebox_kb/wisdom/knowledge_points.py +1 -1
  178. package/md_cg/writelimit.py +42 -8
  179. package/md_cg/writepipe.py +651 -554
  180. package/package.json +8 -2
  181. package/skills/plugin.json +1 -1
  182. package/src/bridge.ts +25 -2
  183. package/src/cli.ts +65 -0
  184. package/src/hooks.ts +229 -16
  185. package/src/init.ts +361 -0
  186. package/src/lib/prompt_safety.ts +88 -1
  187. package/zcode/AGENTS.md +10 -10
@@ -0,0 +1,200 @@
1
+ # -*- coding: utf-8 -*-
2
+ """N62 守卫(第 5 轮,2026-09-27):令牌路径租户上限夹紧 + 租户绑定覆盖 fail-closed。
3
+
4
+ 留档复测(v8 N62 首报 → v9/v15/v16 历轮成立 → 本轮第 5 次,medium 维持):
5
+
6
+ 病灶①(cap 夹紧从未接线):verify_token 令牌路径返回 Principal 时
7
+ clearance=rec["clearance"] 原样透出(tokens.py:450)——TenantRegistry.cap_of /
8
+ principal_for 现成(security.py:323/332)但令牌路径零调用;tenantC 登记上限
9
+ internal 时 designer(secret) 令牌仍以 secret 运行(可读含 private/secret)。
10
+
11
+ 病灶②(绑定覆盖 warn-only):verify_token 形参 tenant(MCP 侧来自
12
+ MDCG_TENANT)与令牌记录 rec["tenant"] 不同时仅 stderr 告警(tokens.py:441-446),
13
+ 形参值照常顶替(:448 tenant=tenant or ...)——tenantA 令牌在 MDCG_TENANT=
14
+ tenantB 下以原权限对 tenantB 运行。
15
+
16
+ 红守卫(哑临时登记表 + 哑令牌库 + 临时根,确定性、零真实状态;登记表经
17
+ MDCG_TENANT_REGISTRY 注入——与 _resolve_root 同源,无新形参,旧代码红为
18
+ 功能红而非 TypeError):
19
+ R1 ② 覆盖被拒:tenantA designer 令牌 + tenant="tenantB" → TokenError
20
+ (fail-closed),消息含两侧租户与校正指引;
21
+ R2 ① cap 夹紧:tenantC(登记 cap=internal)designer 令牌(角色默认 secret)
22
+ 无覆盖 → clearance 夹紧为 internal,allows("private"/"secret") 均 False;
23
+ R3 未登记租户零闸变:tenantD(不在登记表)令牌 clearance 原样;
24
+ R4 无冲突零误伤:tenant=None/""/同值 → 原令牌租户、零异常;
25
+ R5 MCP 进程面:_build_principal 同面复现(env 注入 MDCG_TOKEN + MDCG_TENANT
26
+ + MDCG_TENANT_REGISTRY)——② 拒绝、① 夹紧、同值零误伤。
27
+
28
+ 同面搭车(N174,low,见 deferred,本轮不修):mcp_server.py:3522-3528 legacy
29
+ env 路径 clearance 夹紧另行 deferred。
30
+
31
+ 运行:python -m md_cg.test_n62_tenant_bind_failclosed
32
+ """
33
+ from __future__ import annotations
34
+
35
+ import contextlib
36
+ import io
37
+ import os
38
+ import shutil
39
+ import sys
40
+ import tempfile
41
+
42
+ from . import tokens
43
+ from .security import TenantRegistry
44
+
45
+ PASS = FAIL = 0
46
+ FAILS = []
47
+ MARK = "租户绑定被入参覆盖" # fail-closed 拒绝消息稳定锚点
48
+
49
+
50
+ def check(name, cond, detail=""):
51
+ global PASS, FAIL
52
+ if cond:
53
+ PASS += 1
54
+ print(" OK " + name)
55
+ else:
56
+ FAIL += 1
57
+ FAILS.append(name)
58
+ print(" FAIL " + name + " " + str(detail)[:110])
59
+
60
+
61
+ def _verify(token, tenant=None, path=None):
62
+ """verify_token 捕获异常,返回 (principal|None, exc|None)。"""
63
+ try:
64
+ return tokens.verify_token(token, tenant=tenant, path=path), None
65
+ except tokens.TokenError as e:
66
+ return None, e
67
+
68
+
69
+ def main():
70
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
71
+ tmp = tempfile.mkdtemp(prefix="mdcg_n62_")
72
+ tf = os.path.join(tmp, "_tokens.json")
73
+ reg_path = os.path.join(tmp, "_tenants.json")
74
+ old_reg = os.environ.get("MDCG_TENANT_REGISTRY")
75
+ try:
76
+ # 哑临时登记表:确定性上限(tenantA=secret 不夹、tenantB/tenantC=internal)
77
+ reg = TenantRegistry(reg_path)
78
+ reg.register("tenantA", os.path.join(tmp, "rootA"), clearance_cap="secret",
79
+ description="N62 守卫哑登记 tenantA")
80
+ reg.register("tenantB", os.path.join(tmp, "rootB"),
81
+ clearance_cap="internal", description="N62 哑登记 tenantB")
82
+ reg.register("tenantC", os.path.join(tmp, "rootC"),
83
+ clearance_cap="internal", description="N62 哑登记 tenantC")
84
+ os.environ["MDCG_TENANT_REGISTRY"] = reg_path
85
+ d_a = tokens.issue("designer", actor="dummy-designer-a", tenant="tenantA",
86
+ path=tf) # 角色默认 clearance=secret
87
+ d_c = tokens.issue("designer", actor="dummy-designer-c", tenant="tenantC",
88
+ path=tf) # 登记 cap=internal < secret
89
+ d_d = tokens.issue("designer", actor="dummy-designer-d", tenant="tenantD",
90
+ path=tf) # tenantD 未登记
91
+
92
+ # ---------- R1 ② 覆盖 fail-closed ----------
93
+ print("== R1 租户绑定被入参覆盖 → 拒绝(fail-closed)==")
94
+ p1, e1 = _verify(d_a["token"], tenant="tenantB", path=tf)
95
+ check("R1a 覆盖被拒(TokenError 且含锚点)",
96
+ p1 is None and e1 is not None and MARK in str(e1),
97
+ f"p={p1} exc={type(e1).__name__}: {str(e1)[:80]}")
98
+ check("R1b 拒绝消息含两侧租户与校正指引",
99
+ e1 is not None and "tenantA" in str(e1) and "tenantB" in str(e1)
100
+ and "MDCG_TENANT" in str(e1), str(e1)[:100])
101
+
102
+ # ---------- R2 ① cap 夹紧(登记租户,纯收紧) ----------
103
+ print("== R2 登记租户上限夹紧(cap=internal < 令牌 secret)==")
104
+ p2, e2 = _verify(d_c["token"], path=tf)
105
+ check("R2a clearance 夹紧为 internal",
106
+ e2 is None and p2 is not None and p2.clearance == "internal",
107
+ f"clearance={getattr(p2, 'clearance', None)} err={e2}")
108
+ check("R2b 可读不含 private/secret",
109
+ p2 is not None and not p2.allows("private")
110
+ and not p2.allows("secret"),
111
+ f"private={p2.allows('private') if p2 else None} "
112
+ f"secret={p2.allows('secret') if p2 else None}")
113
+ check("R2c internal 本级仍可读(收紧不误伤)",
114
+ p2 is not None and p2.allows("internal"),
115
+ f"internal={p2.allows('internal') if p2 else None}")
116
+
117
+ # ---------- R3 未登记租户零闸变 ----------
118
+ print("== R3 未登记租户零闸变(fresh install 行为不变)==")
119
+ p3, e3 = _verify(d_d["token"], path=tf)
120
+ check("R3 未登记 tenantD clearance 原样(不夹紧)",
121
+ e3 is None and p3 is not None and p3.clearance == "secret"
122
+ and p3.tenant == "tenantD" and p3.allows("secret"),
123
+ f"clearance={getattr(p3, 'clearance', None)} "
124
+ f"tenant={getattr(p3, 'tenant', None)} err={e3}")
125
+
126
+ # ---------- R4 无冲突零误伤 ----------
127
+ print("== R4 无冲突路径零误伤 ==")
128
+ for label, arg in (("None", None), ("空串", ""), ("同值", "tenantA")):
129
+ p4, e4 = _verify(d_a["token"], tenant=arg, path=tf)
130
+ check(f"R4 tenant={label}:零异常且回落令牌租户 tenantA",
131
+ e4 is None and p4 is not None and p4.tenant == "tenantA"
132
+ and p4.clearance == "secret",
133
+ f"tenant={getattr(p4, 'tenant', None)} "
134
+ f"clearance={getattr(p4, 'clearance', None)} err={e4}")
135
+
136
+ # ---------- R5 MCP 进程面(_build_principal 同面) ----------
137
+ print("== R5 MCP 进程级:MDCG_TOKEN + MDCG_TENANT + MDCG_TENANT_REGISTRY ==")
138
+ from .mcp_server import _build_principal
139
+ saved = {k: os.environ.get(k) for k in
140
+ ("MDCG_TOKEN", "MDCG_TENANT", "MDCG_TOKEN_FILE", "MDCG_ROOT",
141
+ "MDCG_TENANT_REGISTRY", "MDCG_LEGACY_ENV_AUTH",
142
+ "MDCG_LEGACY_ENV_ADMIN", "MDCG_CAN_ADMIN", "MDCG_CAN_WRITE",
143
+ "MDCG_CLEARANCE", "MDCG_ACTOR")}
144
+ try:
145
+ os.environ["MDCG_TOKEN"] = d_a["token"]
146
+ os.environ["MDCG_TOKEN_FILE"] = tf
147
+ os.environ["MDCG_ROOT"] = os.path.join(tmp, "mcp")
148
+ os.environ["MDCG_TENANT_REGISTRY"] = reg_path
149
+ os.environ["MDCG_TENANT"] = "tenantB"
150
+ for k in ("MDCG_LEGACY_ENV_AUTH", "MDCG_LEGACY_ENV_ADMIN",
151
+ "MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_CLEARANCE",
152
+ "MDCG_ACTOR"):
153
+ os.environ.pop(k, None)
154
+ buf = io.StringIO()
155
+ with contextlib.redirect_stderr(buf):
156
+ mp, merr = _build_principal()
157
+ check("R5a MCP 面覆盖被拒(err 含锚点,principal=None)",
158
+ mp is None and merr is not None and MARK in merr,
159
+ f"err={str(merr)[:90]}")
160
+ # ① 经 MCP 面:tenantC(登记 cap=internal)夹紧
161
+ os.environ["MDCG_TOKEN"] = d_c["token"]
162
+ os.environ["MDCG_TENANT"] = "tenantC"
163
+ mp2, merr2 = _build_principal()
164
+ check("R5b MCP 面 cap 夹紧(tenantC designer → internal)",
165
+ merr2 is None and mp2 is not None
166
+ and getattr(mp2, "clearance", None) == "internal",
167
+ f"clearance={getattr(mp2, 'clearance', None)} err={merr2}")
168
+ # 同值零误伤
169
+ os.environ["MDCG_TOKEN"] = d_a["token"]
170
+ os.environ["MDCG_TENANT"] = "tenantA"
171
+ mp3, merr3 = _build_principal()
172
+ check("R5c MDCG_TENANT 同值:零误伤正常通过",
173
+ merr3 is None and mp3 is not None
174
+ and getattr(mp3, "tenant", None) == "tenantA"
175
+ and getattr(mp3, "clearance", None) == "secret",
176
+ f"err={merr3} tenant={getattr(mp3, 'tenant', None)} "
177
+ f"clearance={getattr(mp3, 'clearance', None)}")
178
+ finally:
179
+ for k, v in saved.items():
180
+ if v is None:
181
+ os.environ.pop(k, None)
182
+ else:
183
+ os.environ[k] = v
184
+ finally:
185
+ if old_reg is None:
186
+ os.environ.pop("MDCG_TENANT_REGISTRY", None)
187
+ else:
188
+ os.environ["MDCG_TENANT_REGISTRY"] = old_reg
189
+ shutil.rmtree(tmp, ignore_errors=True)
190
+
191
+ print()
192
+ if FAILS:
193
+ print("FAILED: %d 项 → %s" % (len(FAILS), ", ".join(FAILS)))
194
+ return 1
195
+ print("ALL OK: %d 项(N62 令牌路径 cap 夹紧 + 租户覆盖 fail-closed 守卫全绿)" % PASS)
196
+ return 0
197
+
198
+
199
+ if __name__ == "__main__":
200
+ sys.exit(main())
@@ -0,0 +1,333 @@
1
+ # -*- coding: utf-8 -*-
2
+ """负条件(不适用条件)命中判据的守卫(批次76,2026-09-28)。
3
+
4
+ 运行:python -X utf8 -m md_cg.test_neg_condition_hits # 正常跑
5
+ python -X utf8 -m md_cg.test_neg_condition_hits --head-baseline # 头基线自证
6
+ python -X utf8 -m md_cg.test_neg_condition_hits --branch-baseline # 分支判别力自证
7
+
8
+ 背景(为什么另立一套):改动前判据是
9
+ `neg_hit = [n for n in neg if any(w in scene for w in n.split())]`
10
+ ——**空白切词 + 任一词命中**。三处过粗各有实测支撑:
11
+ · 主题词变扳机:中文句里夹的裸标识符被切成独立词,`CCG`/`state`/`tags`/
12
+ `python3` 在**本节点自己的主题问句**上命中 → 节点把自己判成不适用
13
+ (三条归档实测,批次75 已用措辞侧改写止血);
14
+ · 单个通用词整片否决:`DSH`(16 条)/`MCP`(12 条)/`CI`/`AEIS`/`Actions`
15
+ 这类条目作用域旁注里的词,任何含它的问句都把这些节点打成不适用;
16
+ · 单字符碎片(的/与/3)与长短语同权。
17
+ 新判据(`mdcg.neg_condition_hits`):**整条命中** 或 **全词命中**,词长下限
18
+ `NEG_MIN_TERM`,外加**自主题豁免**(命中内容出现在节点自己声明的主题面里则
19
+ 不计——它与写入期 L1-a「负条件命中自身生效条件/正文即自相矛盾」同向)。
20
+
21
+ 本套断言分七组:
22
+ G1 整条命中(单词语料条目,「问ZXQ7」/「刚才在干什么」/「问完全无关主题」)
23
+ G2 全词命中(多词条目须词面齐全;缺一词即放行;末四例 G2d-G2g 专打**两档分支
24
+ 各自的判别力**——整条档用「词皆单字符」隔断全词档、全词档用「词非连续」隔断
25
+ 整条档,删掉任一分支对应断言即转红)
26
+ G3 词长下限(单字符碎片不否决;G3c/G3d 专打下限自身的判别力——把
27
+ `NEG_MIN_TERM` 改成 1 必须转红,两条路径各一)
28
+ G4 自主题豁免(命中内容在 own_topic 里 → 放行;不在 → 照旧否决)
29
+ G5 病理复现(三条归档的**旧措辞**:旧判据命中、新判据放行——正对照是同一
30
+ 条目在把 own_topic 换成别人的主题后**仍**否决)
31
+ G6 单点结构(`judge_qualification` 走 `neg_condition_hits`,源码里不得再出现
32
+ 「任一词命中」式实现——副本回归即红)
33
+ G7 端到端(真建临时库:完整六要素节点,自主题问句非 REJECT、语料式问句 REJECT)
34
+
35
+ **红基线自证(--head-baseline)**:把 `neg_condition_hits` 临时替换回旧的
36
+ 「任一词命中」实现,G1/G4/G5/G7 必须转红(G2 的「缺一词即放行」在旧判据下也
37
+ 会失败,因为旧判据任一词命中即拒)。若红基线不红,说明断言没有判别力。
38
+
39
+ **分支判别力自证(--branch-baseline)**:逐条删掉判据的五个分支(整条命中/
40
+ 全词命中/自主题豁免两条/词长下限),套件每删一个都必须转红;某分支删掉后
41
+ 仍全绿 = 该分支的断言空转。此模式即独立复核 2026-09-28 那条指摘的机械化
42
+ (当时「整条命中档」删掉后 31 条断言全绿)。变异锚点写死在
43
+ `_BRANCH_MUTATIONS`,实现改动致锚点漂移会报 ANCHOR-MISS 并判 FAIL。
44
+ """
45
+ from __future__ import annotations
46
+
47
+ import json
48
+ import io
49
+ import os
50
+ import re
51
+ import sys
52
+ import tempfile
53
+
54
+ from . import mdcg as M
55
+ from .mdcg import NEG_MIN_TERM, MdCG, neg_condition_hits
56
+
57
+ _PASS = []
58
+ _FAIL = []
59
+
60
+
61
+ def ok(cond, msg, extra=""):
62
+ (_PASS if cond else _FAIL).append(msg)
63
+ print((" PASS " if cond else " FAIL ") + msg + ((" ← " + str(extra)) if (extra and not cond) else ""))
64
+
65
+
66
+ def _scene(q: str) -> str:
67
+ return json.dumps({"query": q}, ensure_ascii=False)
68
+
69
+
70
+ # ---------------------------------------------------------------- 旧判据(红基线用)
71
+ def _old_hits(neg_conditions, scene_text, own_topic_text=""):
72
+ """改动前的实现原文——仅用于 --head-baseline 的判别力自证,不参与生产。"""
73
+ return [n for n in (neg_conditions or [])
74
+ if any(w in scene_text for w in str(n).split())]
75
+
76
+
77
+ # ---------------------------------------------------------------- G1 整条命中
78
+ def g1():
79
+ print("== G1 整条命中(语料口径,必须保持否决)==")
80
+ for neg, q in ((['问ZXQ7'], '问ZXQ7 是什么'),
81
+ (['刚才在干什么'], '你刚才在干什么'),
82
+ (['问完全无关主题'], '问完全无关主题')):
83
+ h = neg_condition_hits(neg, _scene(q), '')
84
+ ok(bool(h), "G1 语料式负条件命中即否决:%s" % neg[0], h)
85
+ h = neg_condition_hits(['问 ZXQ7'], _scene('问ZXQ7 是什么'), '')
86
+ ok(bool(h), "G1a 空白归一:条目「问 ZXQ7」对问句「问ZXQ7」仍命中(判据反向前更稳)", h)
87
+
88
+
89
+ # ---------------------------------------------------------------- G2 全词命中
90
+ def g2():
91
+ print("== G2 全词命中(多词条目要求词面齐全)==")
92
+ neg = ['抓取 管线']
93
+ ok(bool(neg_condition_hits(neg, _scene('顺带说说抓取 管线的事'), '')),
94
+ "G2 两词齐全 → 否决")
95
+ ok(not neg_condition_hits(neg, _scene('抓取这条路怎么走'), ''),
96
+ "G2a 只中一词 → 放行(旧判据在这里误拒)")
97
+ ok(not neg_condition_hits(['其它机器/其它 DSH profile'], _scene('DSH 端怎么接'), ''),
98
+ "G2b 作用域旁注里的单个通用词(DSH)不否决")
99
+ ok(not neg_condition_hits(['非 GitHub Actions 环境'], _scene('GitHub Actions 怎么配'), ''),
100
+ "G2c 旁注词不作为拒绝理由(「非 X 环境」条目对「X 怎么配」放行)")
101
+ # G2d-G2g:**两档分支各自的判别力**(独立复核 2026-09-28 指出:此前 31 条断言没有
102
+ # 一条能把「有整条分支」与「无整条分支」分开——删掉整条分支测试仍全绿)。
103
+ # 整条档:词全为单字符时全词档因词长下限无法命中,命中只能由整条档解释;
104
+ # 全词档:条目词在问句里**非连续**出现时整条档无法命中,命中只能由全词档解释。
105
+ ok(bool(neg_condition_hits(['抓 取 管 线'], _scene('抓取管线的做法'), '')),
106
+ "G2d 整条档判别力:词皆单字符(全词档因词长下限失能)时仍命中")
107
+ ok(not neg_condition_hits(['抓 取 管 线'], _scene('管线'), ''),
108
+ "G2e 整条档反向:不构成整条且全词档失能 → 放行")
109
+ ok(bool(neg_condition_hits(['其它 机器'], _scene('机器 其它'), '')),
110
+ "G2f 全词档判别力:条目词非连续出现(整条档无法命中)时仍命中")
111
+ ok(not neg_condition_hits(['其它 机器'], _scene('其它'), ''),
112
+ "G2g 全词档反向:只中一词 → 放行")
113
+
114
+
115
+ # ---------------------------------------------------------------- G3 词长下限
116
+ def g3():
117
+ print("== G3 词长下限 ==")
118
+ ok(NEG_MIN_TERM == 2, "G3 词长下限常量 =2(单字符碎片不构成拒绝理由)", NEG_MIN_TERM)
119
+ ok(not neg_condition_hits(['的 与 3'], _scene('随便一个问句 3'), ''),
120
+ "G3a 单字符碎片不否决")
121
+ ok(not neg_condition_hits([''], _scene('任意问句'), ''),
122
+ "G3b 空条目不产生命中")
123
+ # G3c/G3d:**词长下限本身也要有判别力**——把 `NEG_MIN_TERM` 改成 1 时,
124
+ # 03-28 的变异核验显示 35 条断言零转红(与「整条档」同类的空转缺陷)。
125
+ ok(not neg_condition_hits(['抓'], _scene('抓取管线的做法'), ''),
126
+ "G3c 整条档也受下限约束:单字符条目(去空白后 <2)不否决")
127
+ ok(bool(neg_condition_hits(['其它 机器 与'], _scene('机器 其它'), '')),
128
+ "G3d 全词档下限语义:单字符词(与)不参与「词面齐全」,缺它也算齐全")
129
+
130
+
131
+ # ---------------------------------------------------------------- G4 自主题豁免
132
+ def g4():
133
+ print("== G4 自主题豁免 ==")
134
+ scene = _scene('写入闸门 tags 扫描面')
135
+ neg = ['(1)写入闸门只扫 content、不扫 tags——内容无命中而 tags 夹带凭据时判 ACCEPT']
136
+ own = '负记忆凭据脱敏的形态补全(全形态令牌 · 跨度合并 · tags 同口径)'
137
+ ok(bool(_old_hits(neg, scene, own)), "G4 前置:旧判据在该问句下确实命中(病态成立)")
138
+ ok(not neg_condition_hits(neg, scene, own),
139
+ "G4a 命中内容属于节点自身主题面 → 放行(不当成「不适用」)")
140
+ ok(not neg_condition_hits(neg, scene, '别的主题'),
141
+ "G4b 换 own_topic 同样不命中(长句条目本就不是拒绝域的判据,与豁免无关)")
142
+ ok(not neg_condition_hits(['刚才在干什么'], _scene('你刚才在干什么'),
143
+ '问的是刚才在干什么这类即时情境'),
144
+ "G4c 自主题豁免对语料式条目同样生效(自身主题含该短语时不当成不适用)")
145
+ ok(bool(neg_condition_hits(['刚才在干什么'], _scene('你刚才在干什么'),
146
+ '问的是写入闸门的判据'),
147
+ ),
148
+ "G4d 正对照:同一条目,own_topic 换成不相干主题 → 仍否决")
149
+ # G4e/G4f:**全词档上的豁免**(`--branch-baseline` 揪出的第四条空转断言——
150
+ # 删掉全词档豁免分支,37 条断言原样全绿)。G4e 与 G4f 只有 own_topic 不同,
151
+ # G2f 则是同一 neg+scene 在 own_topic 为空时的正对照(三者互锁)。
152
+ ok(not neg_condition_hits(['其它 机器'], _scene('机器 其它'),
153
+ '关于机器的检索判据'),
154
+ "G4e 自主题豁免-全词档:命中的词属于节点自身主题面 → 放行")
155
+ ok(bool(neg_condition_hits(['其它 机器'], _scene('机器 其它'),
156
+ '关于写入闸门的判据'),
157
+ ),
158
+ "G4f 正对照:同一条目同一问句,own_topic 换成不相干主题 → 仍否决")
159
+
160
+
161
+ # ---------------------------------------------------------------- G5 病理复现
162
+ def g5():
163
+ print("== G5 病理复现(三条归档的旧措辞)==")
164
+ cases = (
165
+ ("mem_1790589433213",
166
+ ['本次只覆盖解释器「名字不可用」这一族——不解决「候选名可跑但版本/依赖不符」'
167
+ '(如 PATH 上有 python3 却缺 md_cg 所需依赖)的错配,此时须显式设 HIVE_PYTHON'],
168
+ '解释器缺省 python3 探测 HIVE_PYTHON',
169
+ 'HIVE_PYTHON 探测式缺省'),
170
+ ("mem_1790595840708",
171
+ ['不覆盖 DSH/TS 侧是否有并列判据(本仓 src/ 与 lib/ 检索无第二套 CCG 判据)'],
172
+ 'CCG 六要素 判据 单点 冒号',
173
+ 'CCG 六要素判据收单点'),
174
+ ("mem_1790596483649",
175
+ ['(1)写入闸门**只扫 `content`、不扫 `tags`**——内容无命中而 tags 夹带凭据时判 ACCEPT'],
176
+ '写入闸门 tags 扫描面',
177
+ '负记忆凭据脱敏的形态补全(tags 同口径)'),
178
+ )
179
+ for nid, neg, q, own in cases:
180
+ old = _old_hits(neg, _scene(q), own)
181
+ new = neg_condition_hits(neg, _scene(q), own)
182
+ ok(bool(old), "G5 %s 旧判据在这条自主题问句下命中(红基线可复现)" % nid, old)
183
+ ok(not new, "G5a %s 新判据放行(节点不再在自己的主题上消失)" % nid, new)
184
+
185
+
186
+ # ---------------------------------------------------------------- G6 单点结构
187
+ def g6():
188
+ print("== G6 判据单点(防副本回归)==")
189
+ p = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mdcg.py")
190
+ src = open(p, encoding="utf-8").read()
191
+ win = M.__file__
192
+ body = src[src.index("def judge_qualification"):
193
+ src.index("def judge_qualification") + 3000]
194
+ ok("neg_condition_hits(" in body, "G6 judge_qualification 走单点 neg_condition_hits")
195
+ # 只看**可执行行**(说明性注释里引述旧实现是允许的,正是要记下它为何被换掉)
196
+ code = "\n".join(l for l in body.splitlines() if not l.strip().startswith("#"))
197
+ ok("any(w in scene" not in code,
198
+ "G6a 可执行行内不再有「任一词命中」式实现(副本回归即红)")
199
+ ok("def neg_condition_hits" in src, "G6b 单点函数存在于 mdcg.py(真源 %s)" % win)
200
+ n = len(re.findall(r"def neg_condition_hits", src))
201
+ ok(n == 1, "G6c 单点全仓唯一实现(mdcg.py 内出现 %d 次)" % n, n)
202
+ # 三态调用方(mdcos 的 legacy 覆盖)必须仍委托父类判据
203
+ mdsrc = open(os.path.join(os.path.dirname(p), "mdcos.py"), encoding="utf-8").read()
204
+ ok("MdCG.judge_qualification(node_dict, query, context)" in mdsrc,
205
+ "G6d mdcos 的 legacy 分支仍委托 MdCG.judge_qualification(不在副本里重判)")
206
+ # G6e/G6f:批次76 同批收口的**陈旧副本**——`_ccg_line` 自称「与 mdcos._ccg_field
207
+ # 同源」,实为手抄且停在「必须有冒号」的旧语义(旧副本取空/单点非空实测 93 条)。
208
+ from . import nodefile as _nf
209
+ rwin = src[src.index("def _ccg_line"):src.index("def _ccg_line") + 1400]
210
+ ok("nodefile.ccg_field_value(content, name)" in rwin,
211
+ "G6e _ccg_line 委托单点(不许再手抄一行扫描)")
212
+ c_nocolon = ("# 功能名\n无冒号形态的取值\n# 生效条件\n问某种问句\n# 子功能\nx\n"
213
+ "# 执行\nx\n# 验证方式\n编译器/静态检查通过\n# 不适用条件\n其它\n")
214
+ ok(MdCG._ccg_line(c_nocolon, "生效条件") == _nf.ccg_field_value(c_nocolon, "生效条件")
215
+ and (MdCG._ccg_line(c_nocolon, "生效条件") or "").strip() != "",
216
+ "G6f 行为逐字一致:无冒号形态两侧取到同一个非空值(旧副本在此返回空串)",
217
+ repr(MdCG._ccg_line(c_nocolon, "生效条件")))
218
+
219
+
220
+ # ---------------------------------------------------------------- G7 端到端
221
+ def g7():
222
+ print("== G7 端到端(临时库真建节点)==")
223
+ from .mdcos import MdCGOS
224
+ tmp = tempfile.mkdtemp(prefix="negcond_")
225
+ cg = MdCGOS(tmp)
226
+ doc = ("# 功能名:写入闸门对 tags 的扫描面\n"
227
+ "# 生效条件:问写入闸门内容闸门的判据时\n"
228
+ "# 子功能:md_cg/writepipe.py\n# 执行:读闸门代码\n"
229
+ "# 验证方式:编译器/静态检查通过\n"
230
+ "# 不适用条件:本条不覆盖 tags 字段的扫描面,闸门只扫 content\n")
231
+ cg.add("nc1", doc, layer="knowledge", verification_basis="test",
232
+ non_applicable_conditions=["本条不覆盖 tags 字段的扫描面,闸门只扫 content"])
233
+ neg_doc = ("# 功能名:否定事件节点\n"
234
+ "# 生效条件:时间:即时情境\n"
235
+ "# 子功能:子功能说明\n# 执行:执行说明\n"
236
+ "# 验证方式:编译器/静态检查通过\n"
237
+ "# 不适用条件:刚才在干什么\n")
238
+ cg.add("nc2", neg_doc, layer="knowledge", verification_basis="test",
239
+ non_applicable_conditions=["刚才在干什么"])
240
+ cg.flush()
241
+ q1 = MdCG.judge_qualification({"frontmatter": (cg.get("nc1") or {}).get("frontmatter") or {},
242
+ "content": (cg.get("nc1") or {}).get("content") or ""},
243
+ "写入闸门 tags 的扫描面", None)
244
+ ok(q1["state"] != "REJECT",
245
+ "G7 自主题问句不再 REJECT(state=%s)" % q1["state"], q1)
246
+ q2 = MdCG.judge_qualification({"frontmatter": (cg.get("nc2") or {}).get("frontmatter") or {},
247
+ "content": (cg.get("nc2") or {}).get("content") or ""},
248
+ "你刚才在干什么", None)
249
+ ok(q2["state"] == "REJECT",
250
+ "G7a 语料式否定节点仍 REJECT(state=%s)" % q2["state"], q2)
251
+
252
+
253
+ def _run_groups() -> int:
254
+ """跑全部七组断言,返回失败数(静默模式下调用,供变异核验复用)。"""
255
+ _PASS.clear(); _FAIL.clear()
256
+ for g in (g1, g2, g3, g4, g5, g6, g7):
257
+ g()
258
+ return len(_FAIL)
259
+
260
+
261
+ # 分支变异表(`--branch-baseline` 用):逐个删掉判据的一个分支,套件**必须转红**。
262
+ # 独立复核 2026-09-28 的原始指摘正是这条——「整条命中档」当初 31 条断言删掉它仍全绿,
263
+ # 即断言对该分支无判别力。判别力不能靠一次性人工核验,须做成机械模式。
264
+ _BRANCH_MUTATIONS = (
265
+ ("整条命中档", "if len(item) >= NEG_MIN_TERM and item in scene_norm:", "if False:"),
266
+ ("全词命中档", "words = [w for w in str(n).split() if len(w) >= NEG_MIN_TERM]", "words = []"),
267
+ ("自主题豁免-整条", "if not (own and item in own):", "if True:"),
268
+ ("自主题豁免-全词", "if own and any(w in own for w in words):", "if False:"),
269
+ ("词长下限", "NEG_MIN_TERM", "1"),
270
+ )
271
+
272
+
273
+ def _branch_baseline() -> int:
274
+ """逐分支变异核验:删掉任一分支后套件若全绿,则该分支断言为空转(缺判别力)。"""
275
+ import contextlib
276
+ import inspect
277
+ src = inspect.getsource(M.neg_condition_hits)
278
+ live = M.neg_condition_hits
279
+ print("!! 分支变异模式:逐个删掉判据分支,套件应当转红\n")
280
+ bad = []
281
+ with contextlib.redirect_stdout(io.StringIO()):
282
+ clean_fail = _run_groups()
283
+ print(" 未变异基线:失败=%d" % clean_fail)
284
+ if clean_fail:
285
+ bad.append("未变异基线即失败")
286
+ for name, old, new in _BRANCH_MUTATIONS:
287
+ if old not in src:
288
+ print(" ANCHOR-MISS %s —— 变异锚点漂移(实现改了却没同步本表)" % name)
289
+ bad.append(name)
290
+ continue
291
+ ns = {"re": re, "NEG_MIN_TERM": M.NEG_MIN_TERM}
292
+ exec(compile(src.replace(old, new), "branch_mut.py", "exec"), ns)
293
+ M.neg_condition_hits = ns["neg_condition_hits"]
294
+ globals()["neg_condition_hits"] = ns["neg_condition_hits"]
295
+ try:
296
+ with contextlib.redirect_stdout(io.StringIO()):
297
+ fails = _run_groups()
298
+ finally:
299
+ M.neg_condition_hits = live
300
+ globals()["neg_condition_hits"] = live
301
+ verdict = "红(有判别力)" if fails else "**仍全绿 = 该分支断言空转**"
302
+ print(" 删「%s」→ 失败=%d %s" % (name, fails, verdict))
303
+ if not fails:
304
+ bad.append(name)
305
+ print("\n分支判别力:%s" % ("PASS(每个分支都有断言把它钉死)" if not bad
306
+ else "FAIL —— " + "、".join(bad)))
307
+ return 0 if not bad else 1
308
+
309
+
310
+ def main() -> int:
311
+ if "--branch-baseline" in sys.argv:
312
+ return _branch_baseline()
313
+ base = "--head-baseline" in sys.argv
314
+ if base:
315
+ print("!! 红基线模式:neg_condition_hits 临时替换回旧实现,断言应当转红")
316
+ M.neg_condition_hits = _old_hits
317
+ import md_cg.mdcg as _m
318
+ _m.neg_condition_hits = _old_hits
319
+ globals()["neg_condition_hits"] = _old_hits
320
+ for g in (g1, g2, g3, g4, g5, g6, g7):
321
+ g()
322
+ if base:
323
+ print("\n== 红基线判定:应有失败 ==")
324
+ print("红基线失败数 = %d(>0 才算断言有判别力)" % len(_FAIL))
325
+ for f in _FAIL:
326
+ print(" 红:", f)
327
+ return 0 if _FAIL else 1
328
+ print("\n负条件判据守卫:%d 通过,%d 失败" % (len(_PASS), len(_FAIL)))
329
+ return 0 if not _FAIL else 1
330
+
331
+
332
+ if __name__ == "__main__":
333
+ sys.exit(main())