@furongjun1999/dsh-memory 0.5.0 → 0.6.0

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 (156) hide show
  1. package/README.md +572 -465
  2. package/docs/README.md +1 -0
  3. 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
  4. 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
  5. package/docs/eval/DSH/347/253/257/347/274/272/351/231/267/344/270/223/351/241/271_v1.0.md +254 -0
  6. 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
  7. package/docs/eval/bench_lingshu_self/bench_self.py +140 -0
  8. package/docs/eval/bench_lingshu_self/self_bench_result.json +404 -0
  9. package/docs/eval/bench_lingshu_self//347/201/265/346/236/242/350/207/252/345/272/223/347/253/257/345/210/260/347/253/257/346/243/200/347/264/242/345/256/236/346/265/213_v1.0.md +38 -0
  10. package/docs/eval//344/270/215/345/217/257/351/235/240/346/200/247/350/220/275/345/234/260_P0_v1.0.md +185 -0
  11. 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
  12. package/docs/eval//346/225/205/351/232/234/346/263/250/345/205/245/345/256/236/346/265/213_v1.0.md +422 -0
  13. 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 +100 -0
  14. 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 +197 -0
  15. 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_v1.md +156 -0
  16. 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_v10.md +210 -0
  17. 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_v11.md +227 -0
  18. 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_v12.md +203 -0
  19. 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_v13.md +233 -0
  20. 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_v14.md +191 -0
  21. 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_v15.md +213 -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_v16.md +214 -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_v17.md +199 -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_v18.md +183 -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_v19.md +207 -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_v2.md +156 -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_v3.md +152 -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_v4.md +128 -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_v5.md +114 -0
  30. 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_v6.md +192 -0
  31. 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_v7.md +187 -0
  32. 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_v8.md +207 -0
  33. 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_v9.md +203 -0
  34. package/docs/hive//345/256/211/345/205/250/345/256/241/350/256/241/345/256/236/351/224/232_v0.1.md +202 -0
  35. 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 +19 -3
  36. package/docs/images/lingshu-moonlight-covenant-poster-preview.jpg +0 -0
  37. package/docs/images/lingshu-moonlight-covenant-poster.png +0 -0
  38. 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
  39. package/docs/theory//344/270/215/345/217/257/351/235/240/345/256/232/347/220/206/344/270/216/345/244/261/346/225/210/344/274/230/345/205/210/346/241/206/346/236/266_v0.3.md +365 -0
  40. package/docs//344/270/215/345/217/257/351/235/240/346/200/247/347/220/206/350/256/272_v0.1.md +343 -0
  41. package/dsh/cordis-patch-profile-web.example.yml +35 -0
  42. package/lib/bridge.d.ts +9 -0
  43. package/lib/bridge.js +35 -0
  44. package/lib/cli.d.ts +3 -0
  45. package/lib/cli.js +66 -0
  46. package/lib/index.js +7 -1
  47. package/lib/init.d.ts +88 -0
  48. package/lib/init.js +287 -0
  49. package/lib/lib/roleplay_web.js +530 -443
  50. package/lib/lib/token_store.d.ts +7 -1
  51. package/lib/lib/token_store.js +12 -3
  52. package/md_cg/audit.py +12 -1
  53. package/md_cg/backfill.py +16 -15
  54. package/md_cg/backfill_bucket_zh.py +35 -0
  55. package/md_cg/bench_e2e_judge.py +532 -0
  56. package/md_cg/bench_e2e_locomo_qa.py +368 -0
  57. package/md_cg/bench_e2e_qa.py +256 -0
  58. package/md_cg/branches.py +18 -2
  59. package/md_cg/ccgc.py +3 -2
  60. package/md_cg/chain.py +19 -4
  61. package/md_cg/consolidate.py +7 -6
  62. package/md_cg/crosscheck.py +4 -3
  63. package/md_cg/crypto.py +439 -437
  64. package/md_cg/datapath.py +395 -335
  65. package/md_cg/evidence.py +8 -3
  66. package/md_cg/export.py +3 -1
  67. package/md_cg/forgetting.py +2 -2
  68. package/md_cg/fsutil.py +48 -0
  69. package/md_cg/hotcache.py +255 -238
  70. package/md_cg/interop.py +161 -22
  71. package/md_cg/judgment_manifest.py +177 -0
  72. package/md_cg/links.py +655 -622
  73. package/md_cg/mcp_server.py +282 -31
  74. package/md_cg/mdcg.py +702 -126
  75. package/md_cg/mdcos.py +454 -68
  76. package/md_cg/mreview/govern.py +5 -4
  77. package/md_cg/postings.py +4 -2
  78. package/md_cg/readcache.py +76 -18
  79. package/md_cg/reconcile.py +228 -0
  80. package/md_cg/review_cli.py +215 -0
  81. package/md_cg/routing.py +28 -0
  82. package/md_cg/run_tests.py +211 -0
  83. package/md_cg/scrub.py +862 -852
  84. package/md_cg/security.py +385 -275
  85. package/md_cg/selfreport.py +3 -2
  86. package/md_cg/signer.py +565 -562
  87. package/md_cg/sources.py +3 -2
  88. package/md_cg/stg.py +6 -0
  89. package/md_cg/sustain.py +1168 -1138
  90. package/md_cg/test_access_hints.py +147 -0
  91. package/md_cg/test_branch_discard_tombstone.py +136 -0
  92. package/md_cg/test_branches.py +259 -249
  93. package/md_cg/test_chain_read_isolate.py +168 -0
  94. package/md_cg/test_datapath_device_name.py +203 -0
  95. package/md_cg/test_emit_negtail_cache.py +156 -0
  96. package/md_cg/test_en_pipeline.py +186 -166
  97. package/md_cg/test_govern_directread.py +421 -0
  98. package/md_cg/test_i32_hotcache_env_key.py +218 -0
  99. package/md_cg/test_identity_attribution.py +228 -147
  100. package/md_cg/test_index_crossprocess_reload.py +301 -0
  101. package/md_cg/test_index_durability.py +238 -224
  102. package/md_cg/test_interop.py +4 -2
  103. package/md_cg/test_interop_judgment.py +228 -0
  104. package/md_cg/test_issue39_utf8_stdio.py +273 -0
  105. package/md_cg/test_links_concurrent_write.py +188 -0
  106. package/md_cg/test_merge_upsert.py +168 -0
  107. package/md_cg/test_n123_derive_expiry_chain.py +205 -0
  108. package/md_cg/test_n130_verify_falsified_protect.py +185 -0
  109. package/md_cg/test_n131_merge_gate.py +205 -0
  110. package/md_cg/test_none_id_write_guard.py +165 -0
  111. package/md_cg/test_p1x_ref_root.py +160 -0
  112. package/md_cg/test_p27_docindex.py +774 -765
  113. package/md_cg/test_p2_mcp.py +3 -0
  114. package/md_cg/test_p32_backfill.py +304 -298
  115. package/md_cg/test_p39_verify_flow.py +90 -50
  116. package/md_cg/test_p47_session_view.py +60 -25
  117. package/md_cg/test_propose_tail_index.py +157 -0
  118. package/md_cg/test_read_scope_b27.py +277 -0
  119. package/md_cg/test_readcache_default_on.py +168 -0
  120. package/md_cg/test_readcache_precise_inval.py +270 -0
  121. package/md_cg/test_readcache_prodpath.py +55 -7
  122. package/md_cg/test_reconcile_v0.py +294 -0
  123. package/md_cg/test_retr_s1.py +6 -2
  124. package/md_cg/test_retr_s1b.py +67 -0
  125. package/md_cg/test_retr_s7.py +8 -0
  126. package/md_cg/test_retr_s9_entity_ctx.py +181 -175
  127. package/md_cg/test_retr_score_once.py +208 -0
  128. package/md_cg/test_review_cli_attribution.py +177 -0
  129. package/md_cg/test_review_cli_visibility.py +235 -0
  130. package/md_cg/test_review_onepass.py +170 -0
  131. package/md_cg/test_rrf_graph_seed_cache.py +154 -0
  132. package/md_cg/test_security_audit.py +155 -0
  133. package/md_cg/test_security_audit_b26.py +161 -0
  134. package/md_cg/test_security_audit_v21.py +250 -0
  135. package/md_cg/test_semantic_canonical.py +255 -241
  136. package/md_cg/test_session_isolation.py +168 -0
  137. package/md_cg/test_snapshot_autoclose.py +187 -0
  138. package/md_cg/test_tail_watermark_race.py +208 -0
  139. package/md_cg/test_tenant_env_override_warn.py +139 -0
  140. package/md_cg/test_tenant_registry_corrupt_warn.py +151 -0
  141. package/md_cg/test_v14_fixes.py +415 -397
  142. package/md_cg/test_verify_dirty_reconcile.py +157 -0
  143. package/md_cg/theory.py +276 -273
  144. package/md_cg/tokens.py +734 -677
  145. package/md_cg/units.py +3 -2
  146. package/md_cg/vision_evidence.py +4 -3
  147. package/md_cg/whitebox_kb/wisdom/code_solidified.json +6195 -6195
  148. package/md_cg/whitebox_kb/wisdom/multilang_ir.py +128 -124
  149. package/md_cg/writepipe.py +554 -550
  150. package/package.json +11 -2
  151. package/src/bridge.ts +434 -401
  152. package/src/cli.ts +65 -0
  153. package/src/index.ts +526 -518
  154. package/src/init.ts +347 -0
  155. package/src/lib/roleplay_web.ts +1019 -932
  156. package/src/lib/token_store.ts +202 -192
@@ -0,0 +1,228 @@
1
+ # -*- coding: utf-8 -*-
2
+ """test_interop_judgment —— 互验执行链判据强度守卫(issue #37 六项)
3
+
4
+ J1 valid=单一放行位(断言 AND 套件)——套件全挂时必须 False
5
+ J2 A2 标注 self-reported;伪造 subject_fp 不影响 A3(承重墙输入不含自报值)
6
+ J3 failure_reason 枚举四象限结构化
7
+ J4 interop_gate 机械消费方三态(pass 放行 / fail 拒绝 / 缺产物 fail-closed)
8
+ J5 脱敏黑名单扩面(UNC/unix/~/$HOME/密钥形态/Bearer/文本形态)+ 形状白名单
9
+ J6 freeze 显式 out_dir 警告(不被 runner 识别)
10
+
11
+ 运行:python -X utf8 -m md_cg.test_interop_judgment
12
+ """
13
+ import importlib.util
14
+ import json
15
+ import os
16
+ import subprocess
17
+ import sys
18
+ import tempfile
19
+
20
+ HERE = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
21
+ sys.path.insert(0, HERE)
22
+
23
+ from md_cg.interop import ( # noqa: E402
24
+ InteropSanityError, assert_a2, assert_a3, freeze,
25
+ make_verdict, sanity_check_verdict, shape_check_verdict)
26
+
27
+ _spec = importlib.util.spec_from_file_location(
28
+ "verify_runner", os.path.join(HERE, "hive", "verify_runner.py"))
29
+ vr = importlib.util.module_from_spec(_spec)
30
+ _spec.loader.exec_module(vr)
31
+
32
+ passed = failed = 0
33
+
34
+
35
+ def check(name, cond, detail=""):
36
+ global passed, failed
37
+ if cond:
38
+ passed += 1
39
+ print(" [PASS] " + name)
40
+ else:
41
+ failed += 1
42
+ print(" [FAIL] " + name + " " + str(detail)[:200])
43
+
44
+
45
+ def _mk(a_ok, suite_ok):
46
+ return vr._compose_semantics(a_ok, suite_ok)
47
+
48
+
49
+ def main():
50
+ print("[1] J1/J3:verdict 语义四象限")
51
+ v, valid, a_ok, fr = _mk(True, True)
52
+ check("全过 → pass/valid=True/reason=None",
53
+ v == "pass" and valid and a_ok and fr is None)
54
+ v, valid, a_ok, fr = _mk(True, False)
55
+ check("断言过+套件挂 → fail/valid=False/assertions_ok=True/reason=suite_failed"
56
+ "(#37 实测形态:套件全挂时 valid 必须不再是 true)",
57
+ v == "fail" and valid is False and a_ok is True
58
+ and fr == "suite_failed")
59
+ v, valid, a_ok, fr = _mk(False, True)
60
+ check("断言挂+套件过 → reason=assertions_failed",
61
+ v == "fail" and valid is False and a_ok is False
62
+ and fr == "assertions_failed")
63
+ v, valid, a_ok, fr = _mk(False, False)
64
+ check("双挂 → reason=both", v == "fail" and fr == "both")
65
+
66
+ print("[2] J2:A2 self-reported 分层 + A3 不受伪造 subject_fp 影响")
67
+ a2 = assert_a2("a" * 64, "deadbeef" * 8)
68
+ check("伪造 subject_fp 可过 A2(弱断言如实)", a2["ok"] is True)
69
+ check("A2 输出标注 subject_fp_source=self-reported(#27 分层复用)",
70
+ a2.get("subject_fp_source") == "self-reported")
71
+ frozen = {"digest": "f" * 64}
72
+ a3 = assert_a3(frozen, "e" * 64)
73
+ check("A3 输入不含 subject_fp(伪造自报值过不了 A3——承重墙)",
74
+ "subject" not in json.dumps(a3) and a3["ok"] is False)
75
+
76
+ print("[3] J5:脱敏黑名单扩面(issue 列举形态全部拦截)")
77
+ leaks = [
78
+ ("UNC 路径", "\\\\server\\share\\secret.txt"),
79
+ ("macOS /Volumes", "/Volumes/disk/proj/x.md"),
80
+ ("unix /etc", "配置在 /etc/passwd 附近"),
81
+ ("家目录 ~", "见 ~/secret/keys.txt"),
82
+ ("$HOME", "文件在 $HOME/.aws 下"),
83
+ # 假 token 字面量须自带占位符标记(fake/example 等):形状正则
84
+ # (md_cg/interop.py 的 `ghp_[A-Za-z0-9]{20,}`)照旧命中 → 本用例的
85
+ # 拦截断言不变;同时 scripts/check_publish_artifact.py 的 R1 会把它
86
+ # 记为 NOTE 而非 FAIL(否则出货门禁对「测试夹具里的假值」恒红——
87
+ # 红着等于没门禁)。见 FAKE_TOKEN_MARKERS。
88
+ ("ghp_ token", "ghp_dummyABCDEFGHIJKLMNOPQRSTUVWXYZ1234"),
89
+ ("AIza key", "AIzaSyABCDEFGHIJKLMNOPQRSTUVWXYZ1234567"),
90
+ ("AKIA key", "AKIAIOSFODNN7EXAMPLE"),
91
+ ("Bearer", "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.payload.sig"),
92
+ ("文本形态 user_prompt", "note: user_prompt: tell me a secret"),
93
+ ]
94
+ base = {"iter_id": "t", "verifier_instance": "v", "verifier_fingerprint":
95
+ "a" * 64, "subject_instance": "m", "subject_fingerprint":
96
+ "b" * 64, "suite_origin": "w", "frozen_at": "x", "verdict":
97
+ "fail", "passed": 0, "failed": 0, "details": [], "valid": False,
98
+ "assertions_ok": False, "failure_reason": "both"}
99
+ for label, payload in leaks:
100
+ try:
101
+ v2 = dict(base, iter_id=payload[:40])
102
+ v2["details"] = [{"note": payload}]
103
+ sanity_check_verdict(v2)
104
+ check(f"拦截 {label}", False, payload)
105
+ except InteropSanityError:
106
+ check(f"拦截 {label}", True)
107
+ try:
108
+ sanity_check_verdict(dict(base))
109
+ check("正常 verdict 过脱敏门禁(无误伤)", True)
110
+ except InteropSanityError as e:
111
+ check("正常 verdict 过脱敏门禁(无误伤)", False, str(e))
112
+
113
+ print("[4] J5:形状白名单(越权字段/坏形状/坏枚举)")
114
+ for label, mut in [
115
+ ("越权顶层字段", {"extra_key": "x"}),
116
+ ("指纹非十六进制", {"verifier_fingerprint": "zzz!*"}),
117
+ ("verdict 枚举外", {"verdict": "PASS"}),
118
+ ("failure_reason 枚举外", {"failure_reason": "whatever"}),
119
+ ("计数非整数", {"passed": "3"}),
120
+ ("计数负数", {"failed": -1})]:
121
+ try:
122
+ shape_check_verdict(dict(base, **mut))
123
+ check(f"形状拦截 {label}", False)
124
+ except InteropSanityError:
125
+ check(f"形状拦截 {label}", True)
126
+ try:
127
+ shape_check_verdict(dict(base, verdict="fail", failure_reason=None,
128
+ passed=1, failed=2))
129
+ check("合法形状通过白名单", True)
130
+ except InteropSanityError as e:
131
+ check("合法形状通过白名单", False, str(e))
132
+
133
+ print("[5] J6:freeze 显式 out_dir 警告")
134
+ import contextlib
135
+ with tempfile.TemporaryDirectory() as td:
136
+ buf = io_err = None
137
+ try:
138
+ import io as _io
139
+ buf = _io.StringIO()
140
+ with contextlib.redirect_stderr(buf):
141
+ freeze("zz-guard-j6", out_dir=td)
142
+ finally:
143
+ pass
144
+ check("显式 out_dir → stderr 警告(不被 runner 识别)",
145
+ buf is not None and "不被" in (buf.getvalue() or ""))
146
+ std = os.path.join(HERE, "hive", "interop", "zz-guard-j6")
147
+ import shutil
148
+ shutil.rmtree(std, ignore_errors=True)
149
+
150
+ print("[6] J4:interop_gate 机械消费方三态")
151
+ gate = os.path.join(HERE, "scripts", "interop_gate.py")
152
+ with tempfile.TemporaryDirectory() as td:
153
+ vp = os.path.join(td, "verdict.json")
154
+ ok_v = dict(base, verdict="pass", valid=True, assertions_ok=True,
155
+ failure_reason=None, passed=9, failed=0)
156
+ json.dump(ok_v, open(vp, "w", encoding="utf-8"), ensure_ascii=False)
157
+ r = subprocess.run([sys.executable, gate, "--path", vp],
158
+ capture_output=True, text=True, encoding="utf-8")
159
+ check("pass+valid → exit 0(放行)", r.returncode == 0, r.stdout)
160
+ json.dump(dict(base), open(vp, "w", encoding="utf-8"),
161
+ ensure_ascii=False)
162
+ r = subprocess.run([sys.executable, gate, "--path", vp],
163
+ capture_output=True, text=True, encoding="utf-8")
164
+ check("fail/valid=False → exit 1(拒绝合并)", r.returncode == 1)
165
+ os.remove(vp)
166
+ r = subprocess.run([sys.executable, gate, "--path", vp],
167
+ capture_output=True, text=True, encoding="utf-8")
168
+ check("无 verdict → exit 2(未互验 fail-closed)", r.returncode == 2)
169
+ json.dump({"verdict": "pass"}, open(vp, "w", encoding="utf-8"))
170
+ r = subprocess.run([sys.executable, gate, "--path", vp],
171
+ capture_output=True, text=True, encoding="utf-8")
172
+ check("旧版产物缺契约字段 → exit 3(拒绝猜测)", r.returncode == 3)
173
+
174
+ print("[7] make_verdict 全链(新字段经双门禁落盘)")
175
+ v3 = make_verdict(iter_id="t", verifier_instance="v",
176
+ verifier_fingerprint="a" * 64, subject_instance="m",
177
+ subject_fingerprint="b" * 64,
178
+ suite_origin="w", frozen_at="x", verdict="fail",
179
+ passed=0, failed=1,
180
+ details=[assert_a2("a" * 64, "b" * 64)])
181
+ v3.update({"valid": False, "assertions_ok": False,
182
+ "failure_reason": "both"})
183
+ try:
184
+ sanity_check_verdict(v3)
185
+ shape_check_verdict(v3)
186
+ check("含 self-reported 标注的完整产物过双门禁", True)
187
+ except InteropSanityError as e:
188
+ check("含 self-reported 标注的完整产物过双门禁", False, str(e))
189
+
190
+ print("[8] _parse_counts 与 run_tests SUMMARY 实际形态契约(v6 N28)")
191
+ # 契约断裂实形:run_tests.py:216 打印 `===== SUMMARY N/M 通过,Z 跳过…`,
192
+ # 而 _parse_counts 旧第三正则只认 `通过 X / 失败 Y`(run_tests 从未打印)
193
+ # → python 侧计数恒 (0,0),verdict.json passed/failed 系统性失真
194
+ # (判决靠 exit code 仍对,留痕假零)。
195
+ p188, f188 = vr._parse_counts(
196
+ "===== SUMMARY 188/189 通过,1 跳过(依赖缺失/平台不符) =====")
197
+ check("实跑 SUMMARY 形态可解析(188/189 → passed=188,非 0)",
198
+ p188 == 188, p188)
199
+ check("failed 由 runnable-passed 推出(→ 1,非 0)", f188 == 1, f188)
200
+ # 生产者现场取证:真跑 run_tests(scripts 组 -k 过滤到单测),SUMMARY
201
+ # 行必须能被消费端解析——两侧契约同一守卫钉死,任一侧漂移必红。
202
+ r = subprocess.run(
203
+ [sys.executable, "-X", "utf8",
204
+ os.path.join(HERE, "scripts", "run_tests.py"), "scripts",
205
+ "-k", "judgment"],
206
+ capture_output=True, text=True, encoding="utf-8", errors="replace",
207
+ cwd=HERE, timeout=600)
208
+ summ = [ln for ln in (r.stdout or "").splitlines() if "SUMMARY" in ln]
209
+ check("run_tests 实跑产出 SUMMARY 行(生产者在场)", bool(summ),
210
+ (r.stdout or "")[-200:])
211
+ if summ:
212
+ pp, ff = vr._parse_counts(r.stdout)
213
+ check("消费端能解析生产端实跑输出(非 (0,0) 假读)",
214
+ (pp, ff) != (0, 0), (pp, ff, summ[-1]))
215
+ # cargo 形态不回归;跳过单列不计失败(裸 clone SKIP 面不虚增 failed)
216
+ pc, fc = vr._parse_counts("test result: ok. 3 passed; 1 failed; 0 ignored")
217
+ check("cargo 形态照旧(3 passed; 1 failed → (3,1))", (pc, fc) == (3, 1),
218
+ (pc, fc))
219
+ ps, fs = vr._parse_counts("===== SUMMARY 187/187 通过,2 跳过(依赖缺失) =====")
220
+ check("跳过不计入 failed(187/187+2 跳过 → (187,0))", (ps, fs) == (187, 0),
221
+ (ps, fs))
222
+
223
+ print(f"\ninterop_judgment: {passed} 通过 / {failed} 失败")
224
+ return 0 if not failed else 1
225
+
226
+
227
+ if __name__ == "__main__":
228
+ sys.exit(main())
@@ -0,0 +1,273 @@
1
+ # -*- coding: utf-8 -*-
2
+ """test_issue39_utf8_stdio.py · Windows 控制台代码页下 stdio MCP 中文必炸的守卫(issue #39)
3
+
4
+ 现场(外部用户实测,issue #39):Windows 控制台代码页(GBK/CP936)下,md_cg 的
5
+ stdio MCP server 写中文必失败——宿主按 UTF-8 发来的中文参数被按 GBK 解码:
6
+ · ``gbk:strict`` → 读侧直接 ``UnicodeDecodeError``(本守卫实弹注入的形态,已实证
7
+ 「中文标题:编码守卫」的 UTF-8 字节序列按 GBK 严格解码必非法);
8
+ · ``surrogateescape`` → 解出代理对 ``\\udcXX``,``_j()``(ensure_ascii=False)
9
+ 按 UTF-8 编码时抛 ``UnicodeEncodeError: surrogates not allowed``。
10
+ 两种形态整条请求同亡;写英文正常,用户难自查。桥层 0.4.10 已在
11
+ ``src/lib/mdcg_client.ts`` 的 ``mdcgChildEnv()`` 注入 ``PYTHONUTF8=1``,但只覆盖
12
+ DSH 桥路径——Claude Code / code CLI 直接 mcp.json 接入不经桥,仍踩。
13
+
14
+ 修复(本守卫的红绿两态锚点):``md_cg/mcp_server.py`` 与
15
+ ``hive/hive_mcp/mcp_server.py``(同族全修)各加模块级 ``_force_utf8_stdio()``,
16
+ 并在 ``main()`` **首行**调用——进程内把 stdio 三流 reconfigure 成
17
+ ``utf-8``/``errors="replace"``,是对桥层 PYTHONUTF8=1 的纵深补位;``replace``
18
+ 保证坏字节最多丢字符、不炸整条请求。
19
+
20
+ 实弹形态:subprocess spawn ``[sys.executable, "-m", "md_cg.mcp_server"]``
21
+ (cwd=仓根,**不带** ``-X utf8``),env 显式设 ``PYTHONIOENCODING=gbk:strict``
22
+ 模拟 CP936 控制台;按行分隔 JSON-RPC(initialize → notifications/initialized →
23
+ tools/call cg {op:"write", content:中文})。断言:响应行是合法 JSON、write 响应
24
+ 不含 ``UnicodeEncodeError`` 且返回 ``ok``/``moved_to`` 字段。hive 面同款实弹
25
+ (中文 hive_spawn;HIVE_EXE 指向不存在的探针路径——``_ensure_serve`` 在 isfile
26
+ 即返回,绝不拉起真 serve)。修复前此面红(进程在读侧崩、响应行缺失),修复后绿。
27
+
28
+ 隔离纪律:MDCG_ROOT/MDCG_STATE_ROOT/MDCG_DATA_ROOT/MDCG_AUX_ROOT/
29
+ MDCG_TENANT_REGISTRY/HIVE_JOBS_DIR/HIVE_CONFIG 全部指向 tempfile.mkdtemp 临时
30
+ 目录(登记表指向不存在的临时路径=空表回落),身份走既有豁免面
31
+ ``MDCG_LEGACY_ENV_AUTH=1``(env 直连 designer,零令牌、不回落真实 ~/.mdcg),
32
+ MDCG_SUSTAIN=0 关常驻循环;finally rmtree——绝不触真实令牌库与真实数据目录。
33
+
34
+ 自检 floor:实弹断言低于 LIVE_FLOOR 视为失败——防止「spawn 面失效 → 假绿」。
35
+ """
36
+ from __future__ import annotations
37
+
38
+ import json
39
+ import os
40
+ import shutil
41
+ import subprocess
42
+ import sys
43
+ import tempfile
44
+
45
+ try:
46
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
47
+ except AttributeError:
48
+ pass
49
+
50
+ ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
51
+ LIVE_FLOOR = 2 # 实弹断言下限(md_cg 面 ≥1 + hive 面 ≥1)
52
+ PROBE = "中文标题:编码守卫" # UTF-8 字节按 GBK strict 解码必非法(红态机制,见头注)
53
+
54
+ PASS, FAIL, LIVE = 0, 0, 0
55
+
56
+
57
+ def check(name, cond, detail="", live=False):
58
+ global PASS, FAIL, LIVE
59
+ if live:
60
+ LIVE += 1
61
+ if cond:
62
+ PASS += 1
63
+ print(f" [ok] {name}")
64
+ else:
65
+ FAIL += 1
66
+ print(f" [FAIL] {name} {detail}")
67
+
68
+
69
+ # 模拟 CP936 控制台前,先把父进程可能携带的「身份/路径/编码」注入面清干净——
70
+ # 守卫子进程只吃临时目录,绝不吃本机真实 ~/.mdcg / 真实 MDCG_ROOT。
71
+ _DIRTY_KEYS = (
72
+ "MDCG_TOKEN", "MDCG_CLEARANCE", "MDCG_TENANT", "MDCG_TENANT_REGISTRY",
73
+ "MDCG_CAN_ADMIN", "MDCG_CAN_WRITE", "MDCG_LEGACY_ENV_AUTH",
74
+ "MDCG_LEGACY_ENV_ADMIN", "MDCG_SESSION", "DSH_SESSION_ID",
75
+ "MDCG_HARNESS", "MDCG_UNIT", "MDCG_ROOT", "MDCG_STATE_ROOT",
76
+ "MDCG_DATA_ROOT", "MDCG_AUX_ROOT", "MDCG_SUSTAIN", "MDCG_SUSTAIN_NAME",
77
+ "MDCG_MCP_SURFACE", "MDCG_TOOL_FACE", "MDCG_ACTOR",
78
+ "MDCG_VERIFIER_MODULES", "MDCG_HIVE_JOBS",
79
+ "PYTHONUTF8", "PYTHONIOENCODING", "PYTHONLEGACYWINDOWSSTDIO",
80
+ "HIVE_JOBS_DIR", "HIVE_CONFIG", "HIVE_EXE", "HIVE_API_KEY",
81
+ )
82
+
83
+
84
+ def _base_env(tmp: str) -> dict:
85
+ env = {k: v for k, v in os.environ.items() if k not in _DIRTY_KEYS}
86
+ env["PYTHONIOENCODING"] = "gbk:strict" # 模拟 CP936 控制台(无 -X utf8)
87
+ env["MDCG_SUSTAIN"] = "0" # 关常驻循环(线程面归零)
88
+ env["MDCG_ACTOR"] = "issue39-guard"
89
+ # 身份走既有豁免面:env 直连 designer(二次开关齐全),零令牌、不回落真实凭据面
90
+ env["MDCG_LEGACY_ENV_AUTH"] = "1"
91
+ env["MDCG_CAN_ADMIN"] = "1"
92
+ env["MDCG_LEGACY_ENV_ADMIN"] = "1"
93
+ _ = tmp
94
+ return env
95
+
96
+
97
+ def _mdcg_env(tmp: str) -> dict:
98
+ env = _base_env(tmp)
99
+ env["MDCG_ROOT"] = os.path.join(tmp, "cgroot")
100
+ env["MDCG_TENANT_REGISTRY"] = os.path.join(tmp, "_tenants_absent.json")
101
+ env["MDCG_STATE_ROOT"] = os.path.join(tmp, "state")
102
+ env["MDCG_DATA_ROOT"] = os.path.join(tmp, "data")
103
+ # 子目录名不可取 "aux":AUX 是 Windows 保留设备名,ntpath.abspath 经
104
+ # GetFullPathNameW 会把末段 aux 解析成设备路径 \\.\aux(实测),导致
105
+ # aux_root() 覆盖键静默失联——取名 auxroot 避开。
106
+ env["MDCG_AUX_ROOT"] = os.path.join(tmp, "auxroot")
107
+ return env
108
+
109
+
110
+ def _hive_env(tmp: str) -> dict:
111
+ env = _base_env(tmp)
112
+ env["HIVE_JOBS_DIR"] = os.path.join(tmp, "hivejobs")
113
+ # 指向不存在的配置与可执行文件:_ensure_serve 在 isfile 即返回,绝不拉起真 serve
114
+ env["HIVE_CONFIG"] = os.path.join(tmp, "hive_config_absent.json")
115
+ env["HIVE_EXE"] = os.path.join(tmp, "no_such_hive.exe")
116
+ return env
117
+
118
+
119
+ def _rpc(rid, method, params=None):
120
+ msg = {"jsonrpc": "2.0", "id": rid, "method": method}
121
+ if params is not None:
122
+ msg["params"] = params
123
+ return json.dumps(msg, ensure_ascii=False)
124
+
125
+
126
+ def _talk(cmd_args, env, lines, timeout=180):
127
+ """spawn server → 写 JSON-RPC 行 → 收 (returncode, stdout_text, stderr_text)。"""
128
+ proc = subprocess.Popen(
129
+ cmd_args, cwd=ROOT, env=env,
130
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
131
+ text=True, encoding="utf-8", errors="replace")
132
+ try:
133
+ out, err = proc.communicate("\n".join(lines) + "\n", timeout=timeout)
134
+ except subprocess.TimeoutExpired:
135
+ proc.kill()
136
+ out, err = proc.communicate()
137
+ return proc.returncode, out or "", err or ""
138
+
139
+
140
+ def _resp_lines(out: str):
141
+ """stdout 逐行解析成 (raw_line, obj_or_None)。"""
142
+ got = []
143
+ for ln in out.splitlines():
144
+ ln = ln.strip()
145
+ if not ln:
146
+ continue
147
+ try:
148
+ got.append((ln, json.loads(ln)))
149
+ except ValueError:
150
+ got.append((ln, None))
151
+ return got
152
+
153
+
154
+ def _find_resp(resps, rid):
155
+ for _raw, obj in resps:
156
+ if isinstance(obj, dict) and obj.get("id") == rid:
157
+ return obj
158
+ return None
159
+
160
+
161
+ def _main_body_has_call(src: str) -> bool:
162
+ """源断言:main() 函数体首段含 _force_utf8_stdio() 调用(定义与调用都在)。"""
163
+ if "def _force_utf8_stdio()" not in src:
164
+ return False
165
+ idx = src.find("def main")
166
+ if idx < 0:
167
+ return False
168
+ window = src[idx:idx + 400]
169
+ return "_force_utf8_stdio()" in window
170
+
171
+
172
+ def main():
173
+ tmp = tempfile.mkdtemp(prefix="mdcg_issue39_")
174
+ try:
175
+ # ---- 1. md_cg 面实弹:GBK 代码页下写中文 ----
176
+ print("[1] md_cg stdio 实弹(PYTHONIOENCODING=gbk:strict,无 -X utf8)")
177
+ rc, out, err = _talk(
178
+ [sys.executable, "-m", "md_cg.mcp_server"], _mdcg_env(tmp), [
179
+ _rpc(1, "initialize", {}),
180
+ _rpc(None, "notifications/initialized"),
181
+ _rpc(3, "tools/call", {"name": "cg", "arguments": {
182
+ "op": "write", "content": PROBE}}),
183
+ ])
184
+ resps = _resp_lines(out)
185
+ init = _find_resp(resps, 1)
186
+ check("1a initialize 握手在 GBK 控制台下存活(合法 JSON 响应)",
187
+ isinstance(init, dict)
188
+ and (init.get("result") or {}).get("serverInfo", {}).get(
189
+ "name") == "mdcg-mcp",
190
+ f"rc={rc} lines={len(resps)} err_tail={err[-200:]!r}", live=True)
191
+ wr = _find_resp(resps, 3)
192
+ wr_ok = False
193
+ if isinstance(wr, dict):
194
+ try:
195
+ payload = json.loads(
196
+ (wr.get("result") or {}).get("content", [{}])[0].get(
197
+ "text", "{}"))
198
+ wr_ok = ("ok" in payload) or ("moved_to" in payload)
199
+ except (ValueError, AttributeError, IndexError):
200
+ wr_ok = False
201
+ check("1b 中文 write 响应合法且返回 ok/moved_to 字段(红态:进程在读侧崩,本条缺失)",
202
+ wr_ok, f"wr={str(wr)[:200]}", live=True)
203
+ check("1c 全程无 UnicodeEncodeError(stdout/stderr 不见代理对编码炸点)",
204
+ "UnicodeEncodeError" not in out and "UnicodeEncodeError" not in err,
205
+ f"out_tail={out[-160:]!r} err_tail={err[-160:]!r}", live=True)
206
+ check("1d 进程正常下线(EOF 后 returncode=0,非中途崩死)",
207
+ rc == 0, f"rc={rc} err_tail={err[-200:]!r}", live=True)
208
+
209
+ # ---- 2. hive 面实弹:同族 server 同款代码页 ----
210
+ print("[2] hive stdio 实弹(同款 PYTHONIOENCODING=gbk:strict)")
211
+ rc2, out2, err2 = _talk(
212
+ [sys.executable, "-m", "hive.hive_mcp.mcp_server"],
213
+ _hive_env(tmp), [
214
+ _rpc(1, "initialize", {}),
215
+ _rpc(None, "notifications/initialized"),
216
+ _rpc(3, "tools/call", {"name": "hive_spawn", "arguments": {
217
+ "model": "probe-model", "user_prompt": PROBE}}),
218
+ ])
219
+ resps2 = _resp_lines(out2)
220
+ init2 = _find_resp(resps2, 1)
221
+ check("2a initialize 握手存活(合法 JSON 响应,serverInfo=hive-mcp)",
222
+ isinstance(init2, dict)
223
+ and (init2.get("result") or {}).get("serverInfo", {}).get(
224
+ "name") == "hive-mcp",
225
+ f"rc={rc2} lines={len(resps2)} err_tail={err2[-200:]!r}", live=True)
226
+ sp = _find_resp(resps2, 3)
227
+ sp_ok = False
228
+ if isinstance(sp, dict):
229
+ try:
230
+ payload = json.loads(
231
+ (sp.get("result") or {}).get("content", [{}])[0].get(
232
+ "text", "{}"))
233
+ sp_ok = payload.get("ok") is True and bool(payload.get("job_id"))
234
+ except (ValueError, AttributeError, IndexError):
235
+ sp_ok = False
236
+ check("2b 中文 hive_spawn 返回 ok=true+job_id(探针 exe 不存在,绝不拉真 serve)",
237
+ sp_ok, f"resp={str(sp)[:200]}", live=True)
238
+ check("2c hive 面无 UnicodeEncodeError",
239
+ "UnicodeEncodeError" not in out2
240
+ and "UnicodeEncodeError" not in err2,
241
+ f"err_tail={err2[-160:]!r}", live=True)
242
+ # 探针诚实边界:任务必须落在守卫自己的临时池,真实 jobs 池零触碰
243
+ jobs_dir = os.path.join(tmp, "hivejobs")
244
+ probe_jobs = [n for n in (os.listdir(jobs_dir) if os.path.isdir(jobs_dir)
245
+ else []) if n.startswith("h")]
246
+ check("2d 任务落在临时 jobs 池(隔离面自证)",
247
+ len(probe_jobs) == 1 if sp_ok else len(probe_jobs) == 0,
248
+ f"probe_jobs={probe_jobs}")
249
+
250
+ # ---- 3. 源断言:两 server 的 main() 首段强制 UTF-8 ----
251
+ print("[3] 源断言(main() 首段 _force_utf8_stdio())")
252
+ for rel in ("md_cg/mcp_server.py", "hive/hive_mcp/mcp_server.py"):
253
+ with open(os.path.join(ROOT, rel), encoding="utf-8",
254
+ errors="replace") as fh:
255
+ src = fh.read()
256
+ check(f"3·{rel}: 定义存在且 main() 首段调用 _force_utf8_stdio()",
257
+ _main_body_has_call(src),
258
+ "缺定义或 main() 首段未调用(必须早于一切 stderr 中文写与 stdin 读取)")
259
+
260
+ # ---- 4. 守卫自检 floor:实弹断言不足即假绿 ----
261
+ print("[4] 守卫自检")
262
+ check(f"4a 实弹断言数 ≥ {LIVE_FLOOR}(防 spawn/扫描面失效假绿)",
263
+ LIVE >= LIVE_FLOOR, f"LIVE={LIVE}")
264
+ finally:
265
+ shutil.rmtree(tmp, ignore_errors=True)
266
+
267
+ print(f"\n=== issue39 utf8 stdio tests: {PASS} passed, {FAIL} failed "
268
+ f"(live={LIVE}) ===")
269
+ sys.exit(1 if FAIL else 0)
270
+
271
+
272
+ if __name__ == "__main__":
273
+ sys.exit(main())